diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 0491d33ba..ad9df4bf9 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -12,11 +12,6 @@ on: jobs: test-redistribute: runs-on: ubuntu-latest - strategy: - matrix: - package: - - fastapi - - fastapi-slim steps: - name: Dump GitHub context env: @@ -30,8 +25,6 @@ jobs: - name: Install build dependencies run: pip install build - name: Build source distribution - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build --sdist - name: Decompress source distribution run: | @@ -41,8 +34,6 @@ jobs: run: | cd dist/fastapi*/ pip install --group tests --editable .[all] - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Run source distribution tests run: | cd dist/fastapi*/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cbbde61f..338f6c390 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ on: env: UV_NO_SYNC: true + INLINE_SNAPSHOT_DEFAULT_FLAGS: review jobs: changes: @@ -49,31 +50,41 @@ jobs: matrix: os: [ windows-latest, macos-latest ] python-version: [ "3.14" ] + uv-resolution: + - highest + starlette-src: + - starlette-pypi + - starlette-git include: - - os: ubuntu-latest - python-version: "3.9" - coverage: coverage - os: macos-latest python-version: "3.10" coverage: coverage + uv-resolution: lowest-direct - os: windows-latest python-version: "3.12" coverage: coverage + uv-resolution: lowest-direct - os: ubuntu-latest python-version: "3.13" coverage: coverage + uv-resolution: highest # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" coverage: coverage + uv-resolution: highest codspeed: codspeed - os: ubuntu-latest python-version: "3.14" coverage: coverage + uv-resolution: highest + starlette-src: starlette-git fail-fast: false runs-on: ${{ matrix.os }} env: UV_PYTHON: ${{ matrix.python-version }} + UV_RESOLUTION: ${{ matrix.uv-resolution }} + STARLETTE_SRC: ${{ matrix.starlette-src }} steps: - name: Dump GitHub context env: @@ -92,11 +103,14 @@ jobs: pyproject.toml uv.lock - name: Install Dependencies - run: uv sync --locked --no-dev --group tests --extra all + run: uv sync --no-dev --group tests --extra all + - name: Install Starlette from source + if: matrix.starlette-src == 'starlette-git' + run: uv pip install "git+https://github.com/Kludex/starlette@main" - run: mkdir coverage - name: Test if: matrix.codspeed != 'codspeed' - run: uv run bash scripts/test.sh + run: uv run --no-sync bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} @@ -108,7 +122,7 @@ jobs: CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} with: mode: simulation - run: uv run coverage run -m pytest tests/ --codspeed + run: uv run --no-sync 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' diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 83518614b..bb23fa32d 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -35,6 +35,11 @@ on: type: boolean required: false default: false + max: + description: Maximum number of items to translate (e.g. 10) + type: number + required: false + default: 10 jobs: langs: @@ -115,3 +120,4 @@ jobs: EN_PATH: ${{ github.event.inputs.en_path }} COMMAND: ${{ matrix.command }} COMMIT_IN_PLACE: ${{ github.event.inputs.commit_in_place }} + MAX: ${{ github.event.inputs.max }} diff --git a/README.md b/README.md index 963de51ed..16d149f8f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The key features are: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). * **Fast to code**: Increase the speed to develop features by about 200% to 300%. * * **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. * **Easy**: Designed to be easy to use and learn. Less time reading docs. * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Robust**: Get production-ready code. With automatic interactive documentation. @@ -371,7 +371,7 @@ item: Item * Validation of data: * Automatic and clear errors when the data is invalid. * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* Conversion of input data: coming from the network to Python data and types. Reading from: * JSON. * Path parameters. * Query parameters. @@ -379,7 +379,7 @@ item: Item * Headers. * Forms. * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): +* Conversion of output data: converting from Python data and types to network data (as JSON): * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). * `datetime` objects. * `UUID` objects. @@ -442,7 +442,7 @@ For a more complete example including more features, see the Dependency Injection** system. +* A very powerful and easy to use **Dependency Injection** system. * Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. * More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). * **GraphQL** integration with Strawberry and other libraries. @@ -527,7 +527,7 @@ Used by Starlette: * httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. Used by FastAPI: diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index 0b95fe3a8..498a2e5b6 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -35,7 +35,7 @@ Siehe Abschnitt `### Content of code snippets` im allgemeinen Prompt in `scripts //// tab | Test -Gestern schrieb mein Freund: „Wenn man unkorrekt korrekt schreibt, hat man es unkorrekt geschrieben“. Worauf ich antwortete: „Korrekt, aber ‚unkorrekt‘ ist unkorrekterweise nicht ‚„unkorrekt“‘“. +Gestern schrieb mein Freund: „Wenn man ‚incorrectly‘ korrekt schreibt, hat man es falsch geschrieben“. Worauf ich antwortete: „Korrekt, aber ‚incorrectly‘ ist inkorrekterweise nicht ‚„incorrectly“‘“. /// note | Hinweis @@ -202,11 +202,6 @@ Hier einige Dinge, die in HTML-„abbr“-Elemente gepackt sind (einige sind erf * XWT * PSGI -### Das abbr gibt eine Erklärung { #the-abbr-gives-an-explanation } - -* Cluster -* Deep Learning - ### Das abbr gibt eine vollständige Phrase und eine Erklärung { #the-abbr-gives-a-full-phrase-and-an-explanation } * MDN @@ -224,6 +219,11 @@ Siehe Abschnitt `### HTML abbr elements` im allgemeinen Prompt in `scripts/trans //// +## HTML „dfn“-Elemente { #html-dfn-elements } + +* Cluster +* Deep Learning + ## Überschriften { #headings } //// tab | Test @@ -248,7 +248,7 @@ Die einzige strenge Regel für Überschriften ist, dass das LLM den Hash-Teil in Siehe Abschnitt `### Headings` im allgemeinen Prompt in `scripts/translate.py`. -Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headings` in `docs/de/llm-prompt.md`. +Für einige sprachsspezifische Anweisungen, siehe z. B. den Abschnitt `### Headings` in `docs/de/llm-prompt.md`. //// diff --git a/docs/de/docs/advanced/wsgi.md b/docs/de/docs/advanced/wsgi.md index 3cd776a6a..0090883ce 100644 --- a/docs/de/docs/advanced/wsgi.md +++ b/docs/de/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ Dazu können Sie die `WSGIMiddleware` verwenden und damit Ihre WSGI-Anwendung wr ## `WSGIMiddleware` verwenden { #using-wsgimiddleware } -Sie müssen `WSGIMiddleware` importieren. +/// info | Info + +Dafür muss `a2wsgi` installiert sein, z. B. mit `pip install a2wsgi`. + +/// + +Sie müssen `WSGIMiddleware` aus `a2wsgi` importieren. Wrappen Sie dann die WSGI-Anwendung (z. B. Flask) mit der Middleware. Und dann mounten Sie das auf einem Pfad. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Hinweis + +Früher wurde empfohlen, `WSGIMiddleware` aus `fastapi.middleware.wsgi` zu verwenden, dies ist jetzt deprecatet. + +Stattdessen wird empfohlen, das Paket `a2wsgi` zu verwenden. Die Nutzung bleibt gleich. + +Stellen Sie lediglich sicher, dass das Paket `a2wsgi` installiert ist und importieren Sie `WSGIMiddleware` korrekt aus `a2wsgi`. + +/// ## Es testen { #check-it } diff --git a/docs/de/docs/deployment/docker.md b/docs/de/docs/deployment/docker.md index d4b74635d..1e28efe52 100644 --- a/docs/de/docs/deployment/docker.md +++ b/docs/de/docs/deployment/docker.md @@ -145,8 +145,6 @@ Es gibt andere Formate und Tools zum Definieren und Installieren von Paketabhän * Erstellen Sie eine `main.py`-Datei mit: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 11fb6c983..3ce5cb27f 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Erstellen Sie eine Datei `main.py` mit: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Wenn Ihr Code `async` / `await` verwendet, benutzen Sie `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Sie sehen die alternative automatische Dokumentation (bereitgestellt von Requestbody-Deklarationen an. +Nun, da wir gesehen haben, wie `Path` und `Query` verwendet werden, schauen wir uns fortgeschrittenere Verwendungsmöglichkeiten von Requestbody-Deklarationen an. ## `Path`-, `Query`- und Body-Parameter vermischen { #mix-path-query-and-body-parameters } @@ -101,13 +101,13 @@ Natürlich können Sie auch, wann immer Sie das brauchen, weitere Query-Paramete Da einfache Werte standardmäßig als Query-Parameter interpretiert werden, müssen Sie `Query` nicht explizit hinzufügen, Sie können einfach schreiben: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Oder in Python 3.10 und darüber: +Oder in Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Zum Beispiel: diff --git a/docs/de/docs/tutorial/path-operation-configuration.md b/docs/de/docs/tutorial/path-operation-configuration.md index 3427b3052..a06c85e57 100644 --- a/docs/de/docs/tutorial/path-operation-configuration.md +++ b/docs/de/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ In diesem Fall macht es Sinn, die Tags in einem `Enum` zu speichern. Sie können eine `summary` und eine `description` hinzufügen: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Beschreibung mittels Docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Es wird in der interaktiven Dokumentation verwendet: Sie können die Response mit dem Parameter `response_description` beschreiben: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Info diff --git a/docs/en/docs/_llm-test.md b/docs/en/docs/_llm-test.md index d218f7c76..ff0e24f26 100644 --- a/docs/en/docs/_llm-test.md +++ b/docs/en/docs/_llm-test.md @@ -202,11 +202,6 @@ Here some things wrapped in HTML "abbr" elements (Some are invented): * XWT * PSGI -### The abbr gives an explanation { #the-abbr-gives-an-explanation } - -* cluster -* Deep Learning - ### The abbr gives a full phrase and an explanation { #the-abbr-gives-a-full-phrase-and-an-explanation } * MDN @@ -224,6 +219,11 @@ See section `### HTML abbr elements` in the general prompt in `scripts/translate //// +## HTML "dfn" elements { #html-dfn-elements } + +* cluster +* Deep Learning + ## Headings { #headings } //// tab | Test diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 37f5c78f2..0d03507b7 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -120,7 +120,7 @@ The exit code, the automatic closing of the `Session` in: {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} -...would be run after the the response finishes sending the slow data: +...would be run after the response finishes sending the slow data: {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} diff --git a/docs/en/docs/advanced/advanced-python-types.md b/docs/en/docs/advanced/advanced-python-types.md new file mode 100644 index 000000000..6495cbe44 --- /dev/null +++ b/docs/en/docs/advanced/advanced-python-types.md @@ -0,0 +1,61 @@ +# Advanced Python Types { #advanced-python-types } + +Here are some additional ideas that might be useful when working with Python types. + +## Using `Union` or `Optional` { #using-union-or-optional } + +If your code for some reason can't use `|`, for example if it's not in a type annotation but in something like `response_model=`, instead of using the vertical bar (`|`) you can use `Union` from `typing`. + +For example, you could declare that something could be a `str` or `None`: + +```python +from typing import Union + + +def say_hi(name: Union[str, None]): + print(f"Hi {name}!") +``` + +`typing` also has a shortcut to declare that something could be `None`, with `Optional`. + +Here's a tip from my very **subjective** point of view: + +* 🚨 Avoid using `Optional[SomeType]` +* Instead ✨ **use `Union[SomeType, None]`** ✨. + +Both are equivalent and underneath they are the same, but I would recommend `Union` instead of `Optional` because the word "**optional**" would seem to imply that the value is optional, and it actually means "it can be `None`", even if it's not optional and is still required. + +I think `Union[SomeType, None]` is more explicit about what it means. + +It's just about the words and names. But those words can affect how you and your teammates think about the code. + +As an example, let's take this function: + +```python +from typing import Optional + + +def say_hi(name: Optional[str]): + print(f"Hey {name}!") +``` + +The parameter `name` is defined as `Optional[str]`, but it is **not optional**, you cannot call the function without the parameter: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +The `name` parameter is **still required** (not *optional*) because it doesn't have a default value. Still, `name` accepts `None` as the value: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +The good news is, in most cases, you will be able to simply use `|` to define unions of types: + +```python +def say_hi(name: str | None): + print(f"Hey {name}!") +``` + +So, normally you don't have to worry about names like `Optional` and `Union`. 😎 diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index dbc91409a..be85303bf 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -64,7 +64,7 @@ In that case, you can simply swap the standard `dataclasses` with `pydantic.data 6. Here we are returning a dictionary that contains `items` which is a list of dataclasses. - FastAPI is still capable of serializing the data to JSON. + FastAPI is still capable of serializing the data to JSON. 7. Here the `response_model` is using a type annotation of a list of `Author` dataclasses. diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index e0e3c96a0..3d7bfdee5 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -141,7 +141,7 @@ You could do that with `openapi_extra`: {* ../../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. +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. Nevertheless, we can declare the expected schema for the request body. diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index e65681543..73a6c1cb5 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -137,7 +137,7 @@ There are several Flask REST frameworks, but after investing the time and work i ### Marshmallow { #marshmallow } -One of the main features needed by API systems is data "serialization" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc. +One of the main features needed by API systems is data "serialization" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc. Another big feature needed by APIs is data validation, making sure that the data is valid, given certain parameters. For example, that some field is an `int`, and not some random string. This is especially useful for incoming data. @@ -145,7 +145,7 @@ Without a data validation system, you would have to do all the checks by hand, i These features are what Marshmallow was built to provide. It is a great library, and I have used it a lot before. -But it was created before there existed Python type hints. So, to define every schema you need to use specific utils and classes provided by Marshmallow. +But it was created before there existed Python type hints. So, to define every schema you need to use specific utils and classes provided by Marshmallow. /// check | Inspired **FastAPI** to @@ -155,7 +155,7 @@ Use code to define "schemas" that provide data types and validation, automatical ### Webargs { #webargs } -Another big feature required by APIs is parsing data from incoming requests. +Another big feature required by APIs is parsing data from incoming requests. Webargs is a tool that was made to provide that on top of several frameworks, including Flask. @@ -419,7 +419,7 @@ Handle all the data validation, data serialization and automatic model documenta ### Starlette { #starlette } -Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. +Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. It is very simple and intuitive. It's designed to be easily extensible, and have modular components. diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 1505dfd1e..af7944e75 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -179,19 +179,23 @@ as Uvicorn by default will use the port `8000`, the documentation on port `8008` 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. - -#### 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! +#### LLM Prompt per Language -* 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`. +Each language has a directory: https://github.com/fastapi/fastapi/tree/master/docs, in it you can see a file `llm-prompt.md` with the prompt specific for that language. -* 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. +For example, for Spanish, the prompt is at: `docs/es/llm-prompt.md`. -* 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`. +If you see mistakes in your language, you can make suggestions to the prompt in that file for your language, and request the specific pages you would like to re-generate after the changes. + +#### Reviewing Translation PRs + +You can also 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`. + +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 @@ -201,9 +205,9 @@ Check the docs about 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. +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. -* To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. +To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. #### Request a New Language diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 87111ff64..7c50dbd9b 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -203,3 +203,8 @@ Inspired by Termynal's CSS tricks with modifications -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; } + +.md-typeset dfn { + border-bottom: .05rem dotted var(--md-default-fg-color--light); + cursor: help; +} diff --git a/docs/en/docs/deployment/docker.md b/docs/en/docs/deployment/docker.md index 7219f3afc..4806d779d 100644 --- a/docs/en/docs/deployment/docker.md +++ b/docs/en/docs/deployment/docker.md @@ -14,7 +14,7 @@ In a hurry and already know this stuff? Jump to the [`Dockerfile` below 👇](#b Dockerfile Preview 👀 ```Dockerfile -FROM python:3.9 +FROM python:3.14 WORKDIR /code @@ -166,7 +166,7 @@ Now in the same project directory create a file `Dockerfile` with: ```{ .dockerfile .annotate } # (1)! -FROM python:3.9 +FROM python:3.14 # (2)! WORKDIR /code @@ -390,7 +390,7 @@ If your FastAPI is a single file, for example, `main.py` without an `./app` dire Then you would just have to change the corresponding paths to copy the file inside the `Dockerfile`: ```{ .dockerfile .annotate hl_lines="10 13" } -FROM python:3.9 +FROM python:3.14 WORKDIR /code @@ -454,7 +454,7 @@ Without using containers, making applications run on startup and with restarts c ## Replication - Number of Processes { #replication-number-of-processes } -If you have a cluster of machines with **Kubernetes**, Docker Swarm Mode, Nomad, or another similar complex system to manage distributed containers on multiple machines, then you will probably want to **handle replication** at the **cluster level** instead of using a **process manager** (like Uvicorn with workers) in each container. +If you have a cluster of machines with **Kubernetes**, Docker Swarm Mode, Nomad, or another similar complex system to manage distributed containers on multiple machines, then you will probably want to **handle replication** at the **cluster level** instead of using a **process manager** (like Uvicorn with workers) in each container. One of those distributed container management systems like Kubernetes normally has some integrated way of handling **replication of containers** while still supporting **load balancing** for the incoming requests. All at the **cluster level**. @@ -499,7 +499,7 @@ Of course, there are **special cases** where you could want to have **a containe In those cases, you can use the `--workers` command line option to set the number of workers that you want to run: ```{ .dockerfile .annotate } -FROM python:3.9 +FROM python:3.14 WORKDIR /code diff --git a/docs/en/docs/deployment/https.md b/docs/en/docs/deployment/https.md index a249a3672..6ae1228b9 100644 --- a/docs/en/docs/deployment/https.md +++ b/docs/en/docs/deployment/https.md @@ -65,7 +65,7 @@ Here's an example of how an HTTPS API could look like, step by step, paying atte It would probably all start by you **acquiring** some **domain name**. Then, you would configure it in a DNS server (possibly your same cloud provider). -You would probably get a cloud server (a virtual machine) or something similar, and it would have a fixed **public IP address**. +You would probably get a cloud server (a virtual machine) or something similar, and it would have a fixed **public IP address**. In the DNS server(s) you would configure a record (an "`A record`") to point **your domain** to the public **IP address of your server**. diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index a345e4a0e..307607ad7 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -6,7 +6,7 @@ ### Based on open standards { #based-on-open-standards } -* OpenAPI for API creation, including declarations of path operations, parameters, request bodies, security, etc. +* OpenAPI for API creation, including declarations of path operations, parameters, request bodies, security, etc. * Automatic data model documentation with JSON Schema (as OpenAPI itself is based on JSON Schema). * Designed around these standards, after a meticulous study. Instead of an afterthought layer on top. * This also allows using automatic **client code generation** in many languages. @@ -136,7 +136,7 @@ All built as reusable tools and components that are easy to integrate with your ### Dependency Injection { #dependency-injection } -FastAPI includes an extremely easy to use, but extremely powerful Dependency Injection system. +FastAPI includes an extremely easy to use, but extremely powerful Dependency Injection system. * Even dependencies can have dependencies, creating a hierarchy or **"graph" of dependencies**. * All **automatically handled** by the framework. @@ -153,8 +153,8 @@ Any integration is designed to be so simple to use (with dependencies) that you ### Tested { #tested } -* 100% test coverage. -* 100% type annotated code base. +* 100% test coverage. +* 100% type annotated code base. * Used in production applications. ## Starlette features { #starlette-features } @@ -190,7 +190,7 @@ With **FastAPI** you get all of **Pydantic**'s features (as FastAPI is based on * **No brainfuck**: * No new schema definition micro-language to learn. * If you know Python types you know how to use Pydantic. -* Plays nicely with your **IDE/linter/brain**: +* Plays nicely with your **IDE/linter/brain**: * Because pydantic data structures are just instances of classes you define; auto-completion, linting, mypy and your intuition should all work properly with your validated data. * Validate **complex structures**: * Use of hierarchical Pydantic models, Python `typing`’s `List` and `Dict`, etc. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index ff0f4e9d9..7f9d5efbf 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -40,7 +40,7 @@ The key features are: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). * **Fast to code**: Increase the speed to develop features by about 200% to 300%. * * **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. * **Easy**: Designed to be easy to use and learn. Less time reading docs. * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Robust**: Get production-ready code. With automatic interactive documentation. @@ -368,7 +368,7 @@ item: Item * Validation of data: * Automatic and clear errors when the data is invalid. * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* Conversion of input data: coming from the network to Python data and types. Reading from: * JSON. * Path parameters. * Query parameters. @@ -376,7 +376,7 @@ item: Item * Headers. * Forms. * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): +* Conversion of output data: converting from Python data and types to network data (as JSON): * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). * `datetime` objects. * `UUID` objects. @@ -439,7 +439,7 @@ For a more complete example including more features, see the Dependency Injection** system. +* A very powerful and easy to use **Dependency Injection** system. * Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. * More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). * **GraphQL** integration with Strawberry and other libraries. @@ -524,7 +524,7 @@ Used by Starlette: * httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. Used by FastAPI: diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index b685deef2..f2ad35d71 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -2,7 +2,7 @@ Python has support for optional "type hints" (also called "type annotations"). -These **"type hints"** or annotations are a special syntax that allow declaring the type of a variable. +These **"type hints"** or annotations are a special syntax that allow declaring the type of a variable. By declaring types for your variables, editors and tools can give you better support. @@ -34,7 +34,7 @@ The function does the following: * Takes a `first_name` and `last_name`. * Converts the first letter of each one to upper case with `title()`. -* Concatenates them with a space in the middle. +* Concatenates them with a space in the middle. {* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} @@ -135,27 +135,30 @@ You can use, for example: {* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} -### Generic types with type parameters { #generic-types-with-type-parameters } +### `typing` module { #typing-module } -There are some data structures that can contain other values, like `dict`, `list`, `set` and `tuple`. And the internal values can have their own type too. +For some additional use cases, you might need to import some things from the standard library `typing` module, for example when you want to declare that something has "any type", you can use `Any` from `typing`: -These types that have internal types are called "**generic**" types. And it's possible to declare them, even with their internal types. +```python +from typing import Any -To declare those types and the internal types, you can use the standard Python module `typing`. It exists specifically to support these type hints. -#### Newer versions of Python { #newer-versions-of-python } +def some_function(data: Any): + print(data) +``` -The syntax using `typing` is **compatible** with all versions, from Python 3.6 to the latest ones, including Python 3.9, Python 3.10, etc. +### Generic types { #generic-types } -As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. +Some types can take "type parameters" in square brackets, to define their internal types, for example a "list of strings" would be declared `list[str]`. -If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. +These types that can take type parameters are called **Generic types** or **Generics**. -In all the docs there are examples compatible with each version of Python (when there's a difference). +You can use the same builtin types as generics (with square brackets and types inside): -For example "**Python 3.6+**" means it's compatible with Python 3.6 or above (including 3.7, 3.8, 3.9, 3.10, etc). And "**Python 3.9+**" means it's compatible with Python 3.9 or above (including 3.10, etc). - -If you can use the **latest versions of Python**, use the examples for the latest version, those will have the **best and simplest syntax**, for example, "**Python 3.10+**". +* `list` +* `tuple` +* `set` +* `dict` #### List { #list } @@ -220,44 +223,20 @@ This means: You can declare that a variable can be any of **several types**, for example, an `int` or a `str`. -In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. +To define it you use the vertical bar (`|`) to separate both types. -In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). - -//// tab | Python 3.10+ +This is called a "union", because the variable can be anything in the union of those two sets of types. ```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!} -``` - -//// - -In both cases this means that `item` could be an `int` or a `str`. +This means that `item` could be an `int` or a `str`. #### Possibly `None` { #possibly-none } You can declare that a value could have a type, like `str`, but that it could also be `None`. -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_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. - -`Optional[Something]` is actually a shortcut for `Union[Something, None]`, they are equivalent. - -This also means that in Python 3.10, you can use `Something | None`: - //// tab | Python 3.10+ ```Python hl_lines="1" @@ -266,96 +245,7 @@ This also means that in Python 3.10, you can use `Something | None`: //// -//// 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!} -``` - -//// - -#### Using `Union` or `Optional` { #using-union-or-optional } - -If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: - -* 🚨 Avoid using `Optional[SomeType]` -* Instead ✨ **use `Union[SomeType, None]`** ✨. - -Both are equivalent and underneath they are the same, but I would recommend `Union` instead of `Optional` because the word "**optional**" would seem to imply that the value is optional, and it actually means "it can be `None`", even if it's not optional and is still required. - -I think `Union[SomeType, None]` is more explicit about what it means. - -It's just about the words and names. But those words can affect how you and your teammates think about the code. - -As an example, let's take this function: - -{* ../../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: - -```Python -say_hi() # Oh, no, this throws an error! 😱 -``` - -The `name` parameter is **still required** (not *optional*) because it doesn't have a default value. Still, `name` accepts `None` as the value: - -```Python -say_hi(name=None) # This works, None is valid 🎉 -``` - -The good news is, once you are on Python 3.10 you won't have to worry about that, as you will be able to simply use `|` to define unions of types: - -{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} - -And then you won't have to worry about names like `Optional` and `Union`. 😎 - -#### Generic types { #generic-types } - -These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: - -//// tab | Python 3.10+ - -You can use the same builtin types as generics (with square brackets and types inside): - -* `list` -* `tuple` -* `set` -* `dict` - -And the same as with previous Python versions, from the `typing` module: - -* `Union` -* `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. - -//// - -//// tab | Python 3.9+ - -You can use the same builtin types as generics (with square brackets and types inside): - -* `list` -* `tuple` -* `set` -* `dict` - -And generics from the `typing` module: - -* `Union` -* `Optional` -* ...and others. - -//// +Using `str | None` 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. ### Classes as types { #classes-as-types } @@ -403,17 +293,11 @@ To learn more about Required Optional fields. - -/// - ## Type Hints with Metadata Annotations { #type-hints-with-metadata-annotations } -Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. +Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. -Since Python 3.9, `Annotated` is a part of the standard library, so you can import it from `typing`. +You can import `Annotated` from `typing`. {* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8d90983d4..7640bd6fa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,145 @@ hide: ## Latest Changes +### Breaking Changes + +* ➖ Drop support for Python 3.9. PR [#14897](https://github.com/fastapi/fastapi/pull/14897) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* 🎨 Update internal types for Python 3.10. PR [#14898](https://github.com/fastapi/fastapi/pull/14898) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.8 + +### Docs + +* 📝 Fix grammar in `docs/en/docs/tutorial/first-steps.md`. PR [#14708](https://github.com/fastapi/fastapi/pull/14708) by [@SanjanaS10](https://github.com/SanjanaS10). + +### Internal + +* 🔨 Tweak PDM hook script. PR [#14895](https://github.com/fastapi/fastapi/pull/14895) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Update build setup for `fastapi-slim`, deprecate it, and make it only depend on `fastapi`. PR [#14894](https://github.com/fastapi/fastapi/pull/14894) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.7 + +### Features + +* ✨ Show a clear error on attempt to include router into itself. PR [#14258](https://github.com/fastapi/fastapi/pull/14258) by [@JavierSanchezCastro](https://github.com/JavierSanchezCastro). +* ✨ Replace `dict` by `Mapping` on `HTTPException.headers`. PR [#12997](https://github.com/fastapi/fastapi/pull/12997) by [@rijenkii](https://github.com/rijenkii). + +### Refactors + +* ♻️ Simplify reading files in memory, do it sequentially instead of (fake) parallel. PR [#14884](https://github.com/fastapi/fastapi/pull/14884) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Use `dfn` tag for definitions instead of `abbr` in docs. PR [#14744](https://github.com/fastapi/fastapi/pull/14744) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Internal + +* ✅ Tweak comment in test to reference PR. PR [#14885](https://github.com/fastapi/fastapi/pull/14885) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update LLM-prompt for `abbr` and `dfn` tags. PR [#14747](https://github.com/fastapi/fastapi/pull/14747) by [@YuriiMotov](https://github.com/YuriiMotov). +* ✅ Test order for the submitted byte Files. PR [#14828](https://github.com/fastapi/fastapi/pull/14828) by [@valentinDruzhinin](https://github.com/valentinDruzhinin). +* 🔧 Configure `test` workflow to run tests with `inline-snapshot=review`. PR [#14876](https://github.com/fastapi/fastapi/pull/14876) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.128.6 + +### Fixes + +* 🐛 Fix `on_startup` and `on_shutdown` parameters of `APIRouter`. PR [#14873](https://github.com/fastapi/fastapi/pull/14873) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Translations + +* 🌐 Update translations for zh (update-outdated). PR [#14843](https://github.com/fastapi/fastapi/pull/14843) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ✅ Fix parameterized tests with snapshots. PR [#14875](https://github.com/fastapi/fastapi/pull/14875) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.128.5 + +### Refactors + +* ♻️ Refactor and simplify Pydantic v2 (and v1) compatibility internal utils. PR [#14862](https://github.com/fastapi/fastapi/pull/14862) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ✅ Add inline snapshot tests for OpenAPI before changes from Pydantic v2. PR [#14864](https://github.com/fastapi/fastapi/pull/14864) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.4 + +### Refactors + +* ♻️ Refactor internals, simplify Pydantic v2/v1 utils, `create_model_field`, better types for `lenient_issubclass`. PR [#14860](https://github.com/fastapi/fastapi/pull/14860) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Simplify internals, remove Pydantic v1 only logic, no longer needed. PR [#14857](https://github.com/fastapi/fastapi/pull/14857) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor internals, cleanup unneeded Pydantic v1 specific logic. PR [#14856](https://github.com/fastapi/fastapi/pull/14856) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🌐 Update translations for tr (outdated and missing). PR [#14838](https://github.com/fastapi/fastapi/pull/14838) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Internal + +* ⬆️ Upgrade development dependencies. PR [#14854](https://github.com/fastapi/fastapi/pull/14854) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.3 + +### Refactors + +* ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Upgrade Starlette supported version range to `starlette>=0.40.0,<1.0.0`. PR [#14853](https://github.com/fastapi/fastapi/pull/14853) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Update translations for ru (update-outdated). PR [#14834](https://github.com/fastapi/fastapi/pull/14834) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👷 Run tests with Starlette from git. PR [#14849](https://github.com/fastapi/fastapi/pull/14849) by [@tiangolo](https://github.com/tiangolo). +* 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.2 + +### Features + +* ✨ Add support for PEP695 `TypeAliasType`. PR [#13920](https://github.com/fastapi/fastapi/pull/13920) by [@cstruct](https://github.com/cstruct). +* ✨ Allow `Response` type hint as dependency annotation. PR [#14794](https://github.com/fastapi/fastapi/pull/14794) by [@jonathan-fulton](https://github.com/jonathan-fulton). + +### Fixes + +* 🐛 Fix using `Json[list[str]]` type (issue #10997). PR [#14616](https://github.com/fastapi/fastapi/pull/14616) by [@mkanetsuna](https://github.com/mkanetsuna). + +### Docs + +* 📝 Update docs for translations. PR [#14830](https://github.com/fastapi/fastapi/pull/14830) by [@tiangolo](https://github.com/tiangolo). +* 📝 Fix duplicate word in `advanced-dependencies.md`. PR [#14815](https://github.com/fastapi/fastapi/pull/14815) by [@Rayyan-Oumlil](https://github.com/Rayyan-Oumlil). + +### Translations + +* 🌐 Enable Traditional Chinese translations. PR [#14842](https://github.com/fastapi/fastapi/pull/14842) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Enable French docs translations. PR [#14841](https://github.com/fastapi/fastapi/pull/14841) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for fr (translate-page). PR [#14837](https://github.com/fastapi/fastapi/pull/14837) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for ko (update-outdated). PR [#14835](https://github.com/fastapi/fastapi/pull/14835) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for es (update-outdated). PR [#14832](https://github.com/fastapi/fastapi/pull/14832) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for tr (update-outdated). PR [#14831](https://github.com/fastapi/fastapi/pull/14831) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for tr (add-missing). PR [#14790](https://github.com/fastapi/fastapi/pull/14790) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for fr (update-outdated). PR [#14826](https://github.com/fastapi/fastapi/pull/14826) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for zh-hant (update-outdated). PR [#14825](https://github.com/fastapi/fastapi/pull/14825) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔨 Add max pages to translate to configs. PR [#14840](https://github.com/fastapi/fastapi/pull/14840) by [@tiangolo](https://github.com/tiangolo). + +## 0.128.1 + ### Features * ✨ Add `viewport` meta tag to improve Swagger UI on mobile devices. PR [#14777](https://github.com/fastapi/fastapi/pull/14777) by [@Joab0](https://github.com/Joab0). @@ -14,6 +153,7 @@ hide: ### Fixes +* 🐛 Update `ValidationError` schema to include `input` and `ctx`. PR [#14791](https://github.com/fastapi/fastapi/pull/14791) by [@jonathan-fulton](https://github.com/jonathan-fulton). * 🐛 Fix TYPE_CHECKING annotations for Python 3.14 (PEP 649). PR [#14789](https://github.com/fastapi/fastapi/pull/14789) by [@mgu](https://github.com/mgu). * 🐛 Strip whitespaces from `Authorization` header credentials. PR [#14786](https://github.com/fastapi/fastapi/pull/14786) by [@WaveTheory1](https://github.com/WaveTheory1). * 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). @@ -26,6 +166,7 @@ hide: ### Docs +* 📝 Update docs for contributing translations, simplify title. PR [#14817](https://github.com/fastapi/fastapi/pull/14817) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix typing issue in `docs_src/app_testing/app_b` code example. PR [#14573](https://github.com/fastapi/fastapi/pull/14573) by [@timakaa](https://github.com/timakaa). * 📝 Fix example of license identifier in documentation. PR [#14492](https://github.com/fastapi/fastapi/pull/14492) by [@johnson-earls](https://github.com/johnson-earls). * 📝 Add banner to translated pages. PR [#14809](https://github.com/fastapi/fastapi/pull/14809) by [@YuriiMotov](https://github.com/YuriiMotov). @@ -44,6 +185,8 @@ hide: ### Translations +* 🌐 Improve LLM prompt of `uk` documentation. PR [#14795](https://github.com/fastapi/fastapi/pull/14795) by [@roli2py](https://github.com/roli2py). +* 🌐 Update translations for ja (update-outdated). PR [#14588](https://github.com/fastapi/fastapi/pull/14588) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update outdated, found by fixer tool). PR [#14739](https://github.com/fastapi/fastapi/pull/14739) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for tr (update-outdated). PR [#14745](https://github.com/fastapi/fastapi/pull/14745) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update `llm-prompt.md` for Korean language. PR [#14763](https://github.com/fastapi/fastapi/pull/14763) by [@seuthootDev](https://github.com/seuthootDev). @@ -66,6 +209,8 @@ hide: ### Internal +* ⬇️ Downgrade LLM translations model to GPT-5 to reduce mistakes. PR [#14823](https://github.com/fastapi/fastapi/pull/14823) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix translation script commit in place. PR [#14818](https://github.com/fastapi/fastapi/pull/14818) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translation script to retry if LLM-response doesn't pass validation with Translation Fixer tool. PR [#14749](https://github.com/fastapi/fastapi/pull/14749) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run tests only on relevant code changes (not on docs). PR [#14813](https://github.com/fastapi/fastapi/pull/14813) by [@tiangolo](https://github.com/tiangolo). * 👷 Run mypy by pre-commit. PR [#14806](https://github.com/fastapi/fastapi/pull/14806) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index bb0c58368..d904fb839 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -106,13 +106,6 @@ As, by default, singular values are interpreted as query parameters, you don't h q: str | None = None ``` -Or in Python 3.9: - -```Python -q: Union[str, None] = None -``` - - For example: {* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 2d0dfcbb5..fb4471836 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -155,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.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`. +The `str | None` 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 016a65d7f..609838f76 100644 --- a/docs/en/docs/tutorial/cookie-param-models.md +++ b/docs/en/docs/tutorial/cookie-param-models.md @@ -46,7 +46,7 @@ But even if you **fill the data** and click "Execute", because the docs UI works In some special use cases (probably not very common), you might want to **restrict** the cookies that you want to receive. -Your API now has the power to control its own cookie consent. 🤪🍪 +Your API now has the power to control its own cookie consent. 🤪🍪 You can use Pydantic's model configuration to `forbid` any `extra` fields: @@ -54,9 +54,9 @@ You can use Pydantic's model configuration to `forbid` any `extra` fields: If a client tries to send some **extra cookies**, they will receive an **error** response. -Poor cookie banners with all their effort to get your consent for the API to reject it. 🍪 +Poor cookie banners with all their effort to get your consent for the API to reject it. 🍪 -For example, if the client tries to send a `santa_tracker` cookie with a value of `good-list-please`, the client will receive an **error** response telling them that the `santa_tracker` cookie is not allowed: +For example, if the client tries to send a `santa_tracker` cookie with a value of `good-list-please`, the client will receive an **error** response telling them that the `santa_tracker` cookie is not allowed: ```json { @@ -73,4 +73,4 @@ For example, if the client tries to send a `santa_tracker` cookie with a value o ## Summary { #summary } -You can use **Pydantic models** to declare **cookies** in **FastAPI**. 😎 +You can use **Pydantic models** to declare **cookies** in **FastAPI**. 😎 diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 0a6a786b5..d9a02d5b8 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -101,7 +101,7 @@ Now you can declare your dependency using this class. Notice how we write `CommonQueryParams` twice in the above code: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -109,7 +109,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip @@ -137,7 +137,7 @@ It is from this one that FastAPI will extract the declared parameters and that i In this case, the first `CommonQueryParams`, in: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[CommonQueryParams, ... @@ -145,7 +145,7 @@ commons: Annotated[CommonQueryParams, ... //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip @@ -163,7 +163,7 @@ commons: CommonQueryParams ... You could actually write just: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[Any, Depends(CommonQueryParams)] @@ -171,7 +171,7 @@ commons: Annotated[Any, Depends(CommonQueryParams)] //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip @@ -197,7 +197,7 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -205,7 +205,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip @@ -225,7 +225,7 @@ For those specific cases, you can do the following: Instead of writing: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -233,7 +233,7 @@ commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip @@ -249,7 +249,7 @@ commons: CommonQueryParams = Depends(CommonQueryParams) ...you write: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```Python commons: Annotated[CommonQueryParams, Depends()] @@ -257,7 +257,7 @@ commons: Annotated[CommonQueryParams, Depends()] //// -//// tab | Python 3.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index d9f334561..cfcd961ba 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,6 +1,6 @@ # Dependencies with yield { #dependencies-with-yield } -FastAPI supports dependencies that do some extra steps after finishing. +FastAPI supports dependencies that do some extra steps after finishing. To do this, use `yield` instead of `return`, and write the extra steps (code) after. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index d1116b2be..4a1bb774d 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -1,6 +1,6 @@ # Dependencies { #dependencies } -**FastAPI** has a very powerful but intuitive **Dependency Injection** system. +**FastAPI** has a very powerful but intuitive **Dependency Injection** system. It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 967bffda7..99588dd3c 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -58,11 +58,11 @@ query_extractor --> query_or_cookie_extractor --> read_query If one of your dependencies is declared multiple times for the same *path operation*, for example, multiple dependencies have a common sub-dependency, **FastAPI** will know to call that sub-dependency only once per request. -And it will save the returned value in a "cache" and pass it to all the "dependants" that need it in that specific request, instead of calling the dependency multiple times for the same request. +And it will save the returned value in a "cache" and pass it to all the "dependants" that need it in that specific request, instead of calling the dependency multiple times for the same request. In an advanced scenario where you know you need the dependency to be called at every step (possibly multiple times) in the same request instead of using the "cached" value, you can set the parameter `use_cache=False` when using `Depends`: -//// tab | Python 3.9+ +//// tab | Python 3.10+ ```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.9+ non-Annotated +//// tab | Python 3.10+ non-Annotated /// tip diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index d82df561f..7e7fa146d 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -190,7 +190,7 @@ But if we put that in the assignment `response_model=PlaneItem | CarItem` we wou The same way, you can declare responses of lists of objects. -For that, use the standard Python `typing.List` (or just `list` in Python 3.9 and above): +For that, use the standard Python `list`: {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} @@ -200,7 +200,7 @@ You can also declare a response using a plain arbitrary `dict`, declaring just t This is useful if you don't know the valid field/attribute names (that would be needed for a Pydantic model) beforehand. -In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above): +In this case, you can use `dict`: {* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index df8e91ecd..ceae6c475 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -54,7 +54,7 @@ In the output, there's a line with something like: INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -That line shows the URL where your app is being served, in your local machine. +That line shows the URL where your app is being served on your local machine. ### Check it { #check-it } @@ -271,7 +271,7 @@ We are going to call them "**operations**" too. The `@app.get("/")` tells **FastAPI** that the function right below is in charge of handling requests that go to: * the path `/` -* using a get operation +* using a get operation /// info | `@decorator` Info diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 1bb7ee554..f5c603880 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -56,7 +56,7 @@ You can add a `summary` and `description`: ## Description from docstring { #description-from-docstring } -As descriptions tend to be long and cover multiple lines, you can declare the *path operation* description in the function docstring and **FastAPI** will read it from there. +As descriptions tend to be long and cover multiple lines, you can declare the *path operation* description in the function docstring and **FastAPI** will read it from there. You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). @@ -90,7 +90,7 @@ So, if you don't provide one, **FastAPI** will automatically generate one of "Su ## Deprecate a *path operation* { #deprecate-a-path-operation } -If you need to mark a *path operation* as deprecated, but without removing it, pass the parameter `deprecated`: +If you need to mark a *path operation* as deprecated, but without removing it, pass the parameter `deprecated`: {* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index ea4307900..cf312f1fe 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -26,7 +26,7 @@ This will give you editor support inside of your function, with error checks, co /// -## Data conversion { #data-conversion } +## Data conversion { #data-conversion } If you run this example and open your browser at http://127.0.0.1:8000/items/3, you will see a response of: @@ -38,7 +38,7 @@ If you run this example and open your browser at "parsing". +So, with that type declaration, **FastAPI** gives you automatic request "parsing". /// @@ -144,7 +144,7 @@ Then create class attributes with fixed values, which will be the available vali /// tip -If you are wondering, "AlexNet", "ResNet", and "LeNet" are just names of Machine Learning models. +If you are wondering, "AlexNet", "ResNet", and "LeNet" are just names of Machine Learning models. /// @@ -242,7 +242,7 @@ In that case, the URL would be: `/files//home/johndoe/myfile.txt`, with a double With **FastAPI**, by using short, intuitive and standard Python type declarations, you get: * Editor support: error checks, autocompletion, etc. -* Data "parsing" +* Data "parsing" * Data validation * API annotation and automatic documentation diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 4b8cc9d29..b83960270 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -47,40 +47,16 @@ Now it's the time to use it with FastAPI. 🚀 We had this type annotation: -//// tab | Python 3.10+ - ```Python q: str | None = None ``` -//// - -//// tab | Python 3.9+ - -```Python -q: Union[str, None] = None -``` - -//// - What we will do is wrap that with `Annotated`, so it becomes: -//// tab | Python 3.10+ - ```Python q: Annotated[str | None] = None ``` -//// - -//// tab | Python 3.9+ - -```Python -q: Annotated[Union[str, None]] = None -``` - -//// - Both of those versions mean the same thing, `q` is a parameter that can be a `str` or `None`, and by default, it is `None`. Now let's jump to the fun stuff. 🎉 @@ -109,7 +85,7 @@ FastAPI will now: ## Alternative (old): `Query` as the default value { #alternative-old-query-as-the-default-value } -Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. +Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. /// tip @@ -192,7 +168,7 @@ You can also add a parameter `min_length`: ## Add regular expressions { #add-regular-expressions } -You can define a regular expression `pattern` that the parameter should match: +You can define a regular expression `pattern` that the parameter should match: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -372,7 +348,7 @@ Then you can declare an `alias`, and that alias is what will be used to find the Now let's say you don't like this parameter anymore. -You have to leave it there a while because there are clients using it, but you want the docs to clearly show it as deprecated. +You have to leave it there a while because there are clients using it, but you want the docs to clearly show it as deprecated. Then pass the parameter `deprecated=True` to `Query`: @@ -402,7 +378,7 @@ Pydantic also has ISBN book number or with `imdb-` for an IMDB movie URL ID: +For example, this custom validator checks that the item ID starts with `isbn-` for an ISBN book number or with `imdb-` for an IMDB movie URL ID: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -436,7 +412,7 @@ Did you notice? a string using `value.startswith()` can take a tuple, and it wil #### A Random Item { #a-random-item } -With `data.items()` we get an iterable object with tuples containing the key and value for each dictionary item. +With `data.items()` we get an iterable object with tuples containing the key and value for each dictionary item. We convert this iterable object into a proper `list` with `list(data.items())`. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 3c9c225fb..eeb59f925 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -24,7 +24,7 @@ But when you declare them with Python types (in the example above, as `int`), th All the same process that applied for path parameters also applies for query parameters: * Editor support (obviously) -* Data "parsing" +* Data "parsing" * Data validation * Automatic documentation diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index d743c2ec4..d63ffd565 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -28,7 +28,7 @@ Create form parameters the same way you would for `Body` or `Query`: For example, in one of the ways the OAuth2 specification can be used (called "password flow") it is required to send a `username` and `password` as form fields. -The spec requires the fields to be exactly named `username` and `password`, and to be sent as form fields, not JSON. +The spec requires the fields to be exactly named `username` and `password`, and to be sent as form fields, not JSON. With `Form` you can declare the same configurations as with `Body` (and `Query`, `Path`, `Cookie`), including validation, examples, an alias (e.g. `user-name` instead of `username`), etc. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index b287cdf50..ac35390fa 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -201,7 +201,7 @@ This will also work because `RedirectResponse` is a subclass of `Response`, and But when you return some other arbitrary object that is not a valid Pydantic type (e.g. a database object) and you annotate it like that in the function, FastAPI will try to create a Pydantic response model from that type annotation, and will fail. -The same would happen if you had something like a union between different types where one or more of them are not valid Pydantic types, for example this would fail 💥: +The same would happen if you had something like a union between different types where one or more of them are not valid Pydantic types, for example this would fail 💥: {* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index daaea73c8..432f58b1e 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -74,7 +74,7 @@ You can of course also pass multiple `examples`: When you do this, the examples will be part of the internal **JSON Schema** for that body data. -Nevertheless, at the time of writing this, Swagger UI, the tool in charge of showing the docs UI, doesn't support showing multiple examples for the data in **JSON Schema**. But read below for a workaround. +Nevertheless, at the time of writing this, Swagger UI, the tool in charge of showing the docs UI, doesn't support showing multiple examples for the data in **JSON Schema**. But read below for a workaround. ### OpenAPI-specific `examples` { #openapi-specific-examples } diff --git a/docs/en/docs/virtual-environments.md b/docs/en/docs/virtual-environments.md index c02e43ab9..615d7949f 100644 --- a/docs/en/docs/virtual-environments.md +++ b/docs/en/docs/virtual-environments.md @@ -53,7 +53,7 @@ $ cd awesome-project ## Create a Virtual Environment { #create-a-virtual-environment } -When you start working on a Python project **for the first time**, create a virtual environment **inside your project**. +When you start working on a Python project **for the first time**, create a virtual environment **inside your project**. /// tip diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 1c2954619..66ad67e9d 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -191,6 +191,7 @@ nav: - advanced/openapi-webhooks.md - advanced/wsgi.md - advanced/generate-clients.md + - advanced/advanced-python-types.md - fastapi-cli.md - Deployment: - deployment/index.md @@ -317,14 +318,24 @@ extra: name: de - Deutsch - link: /es/ name: es - español + - 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: /zh/ + name: zh - 简体中文 + - link: /zh-hant/ + name: zh-hant - 繁體中文 extra_css: - css/termynal.css - css/custom.css diff --git a/docs/es/docs/_llm-test.md b/docs/es/docs/_llm-test.md index 591b1008b..dda425acb 100644 --- a/docs/es/docs/_llm-test.md +++ b/docs/es/docs/_llm-test.md @@ -202,15 +202,10 @@ Aquí algunas cosas envueltas en elementos HTML "abbr" (algunas son inventadas): * 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. +* I/O. //// @@ -224,6 +219,11 @@ Consulta la sección `### HTML abbr elements` en el prompt general en `scripts/t //// +## Elementos HTML "dfn" { #html-dfn-elements } + +* clúster +* Deep Learning + ## Encabezados { #headings } //// tab | Prueba @@ -433,7 +433,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading * el motor de plantillas * la anotación de tipos -* las anotaciones de tipos +* la anotación de tipos * el worker del servidor * el worker de Uvicorn diff --git a/docs/es/docs/advanced/wsgi.md b/docs/es/docs/advanced/wsgi.md index d5113250a..ae31185ee 100644 --- a/docs/es/docs/advanced/wsgi.md +++ b/docs/es/docs/advanced/wsgi.md @@ -2,17 +2,33 @@ 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. +Para eso, puedes usar el `WSGIMiddleware` y usarlo para envolver tu aplicación WSGI, por ejemplo, Flask, Django, etc. ## Usando `WSGIMiddleware` { #using-wsgimiddleware } -Necesitas importar `WSGIMiddleware`. +/// info | Información + +Esto requiere instalar `a2wsgi`, por ejemplo con `pip install a2wsgi`. + +/// + +Necesitas importar `WSGIMiddleware` de `a2wsgi`. Luego envuelve la aplicación WSGI (p. ej., Flask) con el middleware. Y luego móntala bajo un path. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Nota + +Anteriormente, se recomendaba usar `WSGIMiddleware` de `fastapi.middleware.wsgi`, pero ahora está deprecado. + +Se aconseja usar el paquete `a2wsgi` en su lugar. El uso sigue siendo el mismo. + +Solo asegúrate de tener instalado el paquete `a2wsgi` e importar `WSGIMiddleware` correctamente desde `a2wsgi`. + +/// ## Revisa { #check-it } diff --git a/docs/es/docs/deployment/docker.md b/docs/es/docs/deployment/docker.md index 114a62ec3..9a0b88955 100644 --- a/docs/es/docs/deployment/docker.md +++ b/docs/es/docs/deployment/docker.md @@ -145,8 +145,6 @@ Existen otros formatos y herramientas para definir e instalar dependencias de pa * Crea un archivo `main.py` con: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -572,7 +570,7 @@ Si tienes una configuración simple, con un **contenedor único** que luego inic ### Imagen Base de Docker { #base-docker-image } -Solía haber una imagen official de Docker de FastAPI: tiangolo/uvicorn-gunicorn-fastapi-docker. Pero ahora está obsoleta. ⛔️ +Solía haber una imagen official de Docker de FastAPI: tiangolo/uvicorn-gunicorn-fastapi. Pero ahora está obsoleta. ⛔️ Probablemente **no** deberías usar esta imagen base de Docker (o cualquier otra similar). diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 14fa07e41..ffea0ed54 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Crea un archivo `main.py` con: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Si tu código usa `async` / `await`, usa `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Ahora modifica el archivo `main.py` para recibir un body desde un request `PUT`. Declara el body usando tipos estándar de Python, gracias a Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/es/docs/tutorial/body-multiple-params.md b/docs/es/docs/tutorial/body-multiple-params.md index 57cec1674..c52486f9b 100644 --- a/docs/es/docs/tutorial/body-multiple-params.md +++ b/docs/es/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Por supuesto, también puedes declarar parámetros adicionales de query siempre Como, por defecto, los valores singulares se interpretan como parámetros de query, no tienes que añadir explícitamente un `Query`, solo puedes hacer: ```Python -q: Union[str, None] = None +q: str | None = None ``` -O en Python 3.10 y superior: +O en Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Por ejemplo: diff --git a/docs/es/docs/tutorial/path-operation-configuration.md b/docs/es/docs/tutorial/path-operation-configuration.md index 945574b9e..f57b322fb 100644 --- a/docs/es/docs/tutorial/path-operation-configuration.md +++ b/docs/es/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ En estos casos, podría tener sentido almacenar las tags en un `Enum`. Puedes añadir un `summary` y `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Descripción desde docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Será usado en la documentación interactiva: Puedes especificar la descripción del response con el parámetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Información diff --git a/docs/fr/docs/advanced/additional-responses.md b/docs/fr/docs/advanced/additional-responses.md index 38527aad3..dabcded52 100644 --- a/docs/fr/docs/advanced/additional-responses.md +++ b/docs/fr/docs/advanced/additional-responses.md @@ -1,6 +1,6 @@ -# Réponses supplémentaires dans OpenAPI +# Réponses supplémentaires dans OpenAPI { #additional-responses-in-openapi } -/// warning | Attention +/// warning | Alertes Ceci concerne un sujet plutôt avancé. @@ -14,9 +14,9 @@ Ces réponses supplémentaires seront incluses dans le schéma OpenAPI, elles ap Mais pour ces réponses supplémentaires, vous devez vous assurer de renvoyer directement une `Response` comme `JSONResponse`, avec votre code HTTP et votre contenu. -## Réponse supplémentaire avec `model` +## Réponse supplémentaire avec `model` { #additional-response-with-model } -Vous pouvez ajouter à votre décorateur de *paramètre de chemin* un paramètre `responses`. +Vous pouvez passer à vos décorateurs de *chemin d'accès* un paramètre `responses`. Il prend comme valeur un `dict` dont les clés sont des codes HTTP pour chaque réponse, comme `200`, et la valeur de ces clés sont d'autres `dict` avec des informations pour chacun d'eux. @@ -26,7 +26,7 @@ Chacun de ces `dict` de réponse peut avoir une clé `model`, contenant un modè Par exemple, pour déclarer une autre réponse avec un code HTTP `404` et un modèle Pydantic `Message`, vous pouvez écrire : -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Remarque @@ -49,7 +49,7 @@ Le bon endroit est : /// -Les réponses générées au format OpenAPI pour cette *opération de chemin* seront : +Les réponses générées au format OpenAPI pour ce *chemin d'accès* seront : ```JSON hl_lines="3-12" { @@ -169,13 +169,13 @@ Les schémas sont référencés à un autre endroit du modèle OpenAPI : } ``` -## Types de médias supplémentaires pour la réponse principale +## Types de médias supplémentaires pour la réponse principale { #additional-media-types-for-the-main-response } Vous pouvez utiliser ce même paramètre `responses` pour ajouter différents types de médias pour la même réponse principale. -Par exemple, vous pouvez ajouter un type de média supplémentaire `image/png`, en déclarant que votre *opération de chemin* peut renvoyer un objet JSON (avec le type de média `application/json`) ou une image PNG : +Par exemple, vous pouvez ajouter un type de média supplémentaire `image/png`, en déclarant que votre *chemin d'accès* peut renvoyer un objet JSON (avec le type de média `application/json`) ou une image PNG : -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Remarque @@ -191,7 +191,7 @@ Mais si vous avez spécifié une classe de réponse personnalisée avec `None` c /// -## Combinaison d'informations +## Combiner les informations { #combining-information } Vous pouvez également combiner des informations de réponse provenant de plusieurs endroits, y compris les paramètres `response_model`, `status_code` et `responses`. @@ -203,17 +203,17 @@ Par exemple, vous pouvez déclarer une réponse avec un code HTTP `404` qui util Et une réponse avec un code HTTP `200` qui utilise votre `response_model`, mais inclut un `example` personnalisé : -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Tout sera combiné et inclus dans votre OpenAPI, et affiché dans la documentation de l'API : -## Combinez les réponses prédéfinies et les réponses personnalisées +## Combinez les réponses prédéfinies et les réponses personnalisées { #combine-predefined-responses-and-custom-ones } -Vous voulez peut-être avoir des réponses prédéfinies qui s'appliquent à de nombreux *paramètre de chemin*, mais vous souhaitez les combiner avec des réponses personnalisées nécessaires à chaque *opération de chemin*. +Vous voulez peut-être avoir des réponses prédéfinies qui s'appliquent à de nombreux *chemins d'accès*, mais vous souhaitez les combiner avec des réponses personnalisées nécessaires à chaque *chemin d'accès*. -Dans ces cas, vous pouvez utiliser la technique Python "d'affection par décomposition" (appelé _unpacking_ en anglais) d'un `dict` avec `**dict_to_unpack` : +Dans ces cas, vous pouvez utiliser la technique Python « unpacking » d'un `dict` avec `**dict_to_unpack` : ```Python old_dict = { @@ -233,15 +233,15 @@ Ici, `new_dict` contiendra toutes les paires clé-valeur de `old_dict` plus la n } ``` -Vous pouvez utiliser cette technique pour réutiliser certaines réponses prédéfinies dans vos *paramètres de chemin* et les combiner avec des réponses personnalisées supplémentaires. +Vous pouvez utiliser cette technique pour réutiliser certaines réponses prédéfinies dans vos *chemins d'accès* et les combiner avec des réponses personnalisées supplémentaires. Par exemple: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} -## Plus d'informations sur les réponses OpenAPI +## Plus d'informations sur les réponses OpenAPI { #more-information-about-openapi-responses } Pour voir exactement ce que vous pouvez inclure dans les réponses, vous pouvez consulter ces sections dans la spécification OpenAPI : -* Objet Responses de OpenAPI , il inclut le `Response Object`. -* Objet Response de OpenAPI , vous pouvez inclure n'importe quoi directement dans chaque réponse à l'intérieur de votre paramètre `responses`. Y compris `description`, `headers`, `content` (à l'intérieur de cela, vous déclarez différents types de médias et schémas JSON) et `links`. +* Objet Responses de OpenAPI, il inclut le `Response Object`. +* Objet Response de OpenAPI, vous pouvez inclure n'importe quoi directement dans chaque réponse à l'intérieur de votre paramètre `responses`. Y compris `description`, `headers`, `content` (à l'intérieur de cela, vous déclarez différents types de médias et schémas JSON) et `links`. diff --git a/docs/fr/docs/advanced/additional-status-codes.md b/docs/fr/docs/advanced/additional-status-codes.md index dde6b9a63..b2befffa8 100644 --- a/docs/fr/docs/advanced/additional-status-codes.md +++ b/docs/fr/docs/advanced/additional-status-codes.md @@ -1,28 +1,28 @@ -# Codes HTTP supplémentaires +# Codes HTTP supplémentaires { #additional-status-codes } Par défaut, **FastAPI** renverra les réponses à l'aide d'une structure de données `JSONResponse`, en plaçant la réponse de votre *chemin d'accès* à l'intérieur de cette `JSONResponse`. Il utilisera le code HTTP par défaut ou celui que vous avez défini dans votre *chemin d'accès*. -## Codes HTTP supplémentaires +## Codes HTTP supplémentaires { #additional-status-codes_1 } Si vous souhaitez renvoyer des codes HTTP supplémentaires en plus du code principal, vous pouvez le faire en renvoyant directement une `Response`, comme une `JSONResponse`, et en définissant directement le code HTTP supplémentaire. -Par exemple, disons que vous voulez avoir un *chemin d'accès* qui permet de mettre à jour les éléments et renvoie les codes HTTP 200 "OK" en cas de succès. +Par exemple, disons que vous voulez avoir un *chemin d'accès* qui permet de mettre à jour les éléments et renvoie les codes HTTP 200 « OK » en cas de succès. -Mais vous voulez aussi qu'il accepte de nouveaux éléments. Et lorsque les éléments n'existaient pas auparavant, il les crée et renvoie un code HTTP de 201 "Créé". +Mais vous voulez aussi qu'il accepte de nouveaux éléments. Et lorsque les éléments n'existaient pas auparavant, il les crée et renvoie un code HTTP de 201 « Créé ». Pour y parvenir, importez `JSONResponse` et renvoyez-y directement votre contenu, en définissant le `status_code` que vous souhaitez : -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} -/// warning | Attention +/// warning | Alertes Lorsque vous renvoyez une `Response` directement, comme dans l'exemple ci-dessus, elle sera renvoyée directement. Elle ne sera pas sérialisée avec un modèle. -Assurez-vous qu'il contient les données souhaitées et que les valeurs soient dans un format JSON valides (si vous utilisez une `JSONResponse`). +Assurez-vous qu'il contient les données souhaitées et que les valeurs sont dans un format JSON valide (si vous utilisez une `JSONResponse`). /// @@ -30,12 +30,12 @@ Assurez-vous qu'il contient les données souhaitées et que les valeurs soient d Vous pouvez également utiliser `from starlette.responses import JSONResponse`. -Pour plus de commodités, **FastAPI** fournit les objets `starlette.responses` sous forme d'un alias accessible par `fastapi.responses`. Mais la plupart des réponses disponibles proviennent directement de Starlette. Il en est de même avec l'objet `statut`. +Pour plus de commodités, **FastAPI** fournit les objets `starlette.responses` sous forme d'un alias accessible par `fastapi.responses`. Mais la plupart des réponses disponibles proviennent directement de Starlette. Il en est de même avec `status`. /// -## Documents OpenAPI et API +## Documents OpenAPI et API { #openapi-and-api-docs } Si vous renvoyez directement des codes HTTP et des réponses supplémentaires, ils ne seront pas inclus dans le schéma OpenAPI (la documentation de l'API), car FastAPI n'a aucun moyen de savoir à l'avance ce que vous allez renvoyer. -Mais vous pouvez documenter cela dans votre code, en utilisant : [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Mais vous pouvez documenter cela dans votre code, en utilisant : [Réponses supplémentaires](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md index d9d8ad8e6..a2f9d3b1b 100644 --- a/docs/fr/docs/advanced/index.md +++ b/docs/fr/docs/advanced/index.md @@ -1,27 +1,21 @@ -# Guide de l'utilisateur avancé +# Guide de l'utilisateur avancé { #advanced-user-guide } -## Caractéristiques supplémentaires +## Caractéristiques supplémentaires { #additional-features } Le [Tutoriel - Guide de l'utilisateur](../tutorial/index.md){.internal-link target=_blank} devrait suffire à vous faire découvrir toutes les fonctionnalités principales de **FastAPI**. Dans les sections suivantes, vous verrez des options, configurations et fonctionnalités supplémentaires. -/// note | Remarque +/// tip | Astuce -Les sections de ce chapitre ne sont **pas nécessairement "avancées"**. +Les sections suivantes ne sont **pas nécessairement « avancées »**. -Et il est possible que pour votre cas d'utilisation, la solution se trouve dans l'un d'entre eux. +Et il est possible que, pour votre cas d'utilisation, la solution se trouve dans l'une d'entre elles. /// -## Lisez d'abord le didacticiel +## Lire d'abord le tutoriel { #read-the-tutorial-first } Vous pouvez utiliser la plupart des fonctionnalités de **FastAPI** grâce aux connaissances du [Tutoriel - Guide de l'utilisateur](../tutorial/index.md){.internal-link target=_blank}. Et les sections suivantes supposent que vous l'avez lu et que vous en connaissez les idées principales. - -## Cours TestDriven.io - -Si vous souhaitez suivre un cours pour débutants avancés pour compléter cette section de la documentation, vous pouvez consulter : Développement piloté par les tests avec FastAPI et Docker par **TestDriven.io**. - -10 % de tous les bénéfices de ce cours sont reversés au développement de **FastAPI**. 🎉 😄 diff --git a/docs/fr/docs/advanced/path-operation-advanced-configuration.md b/docs/fr/docs/advanced/path-operation-advanced-configuration.md index 7daf0fc65..fc88f3363 100644 --- a/docs/fr/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/fr/docs/advanced/path-operation-advanced-configuration.md @@ -1,106 +1,108 @@ -# Configuration avancée des paramètres de chemin +# Configuration avancée des chemins d'accès { #path-operation-advanced-configuration } -## ID d'opération OpenAPI +## ID d’opération OpenAPI { #openapi-operationid } -/// warning | Attention +/// warning | Alertes -Si vous n'êtes pas un "expert" en OpenAPI, vous n'en avez probablement pas besoin. +Si vous n’êtes pas un « expert » d’OpenAPI, vous n’en avez probablement pas besoin. /// -Dans OpenAPI, les chemins sont des ressources, tels que /users/ ou /items/, exposées par votre API, et les opérations sont les méthodes HTTP utilisées pour manipuler ces chemins, telles que GET, POST ou DELETE. Les operationId sont des chaînes uniques facultatives utilisées pour identifier une opération d'un chemin. Vous pouvez définir l'OpenAPI `operationId` à utiliser dans votre *opération de chemin* avec le paramètre `operation_id`. +Vous pouvez définir l’OpenAPI `operationId` à utiliser dans votre chemin d’accès avec le paramètre `operation_id`. -Vous devez vous assurer qu'il est unique pour chaque opération. +Vous devez vous assurer qu’il est unique pour chaque opération. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### Utilisation du nom *path operation function* comme operationId +### Utiliser le nom de la fonction de chemin d’accès comme operationId { #using-the-path-operation-function-name-as-the-operationid } -Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer chaque `operation_id` de l'*opération de chemin* en utilisant leur `APIRoute.name`. +Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer l’`operation_id` de chaque chemin d’accès en utilisant leur `APIRoute.name`. -Vous devriez le faire après avoir ajouté toutes vos *paramètres de chemin*. +Vous devez le faire après avoir ajouté tous vos chemins d’accès. -{* ../../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 | Astuce -Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant. +Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant cela. /// -/// warning | Attention +/// warning | Alertes -Pour faire cela, vous devez vous assurer que chacun de vos *chemin* ait un nom unique. +Si vous faites cela, vous devez vous assurer que chacune de vos fonctions de chemin d’accès a un nom unique. -Même s'ils se trouvent dans des modules différents (fichiers Python). +Même si elles se trouvent dans des modules différents (fichiers Python). /// -## Exclusion d'OpenAPI +## Exclusion d’OpenAPI { #exclude-from-openapi } -Pour exclure un *chemin* du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et assignez-lui la valeur `False` : +Pour exclure un chemin d’accès du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et définissez-le à `False` : -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## Description avancée de docstring +## Description avancée depuis la docstring { #advanced-description-from-docstring } -Vous pouvez limiter le texte utilisé de la docstring d'une *fonction de chemin* qui sera affiché sur OpenAPI. +Vous pouvez limiter les lignes utilisées de la docstring d’une fonction de chemin d’accès pour OpenAPI. -L'ajout d'un `\f` (un caractère d'échappement "form feed") va permettre à **FastAPI** de tronquer la sortie utilisée pour OpenAPI à ce stade. +L’ajout d’un `\f` (un caractère « saut de page » échappé) amène **FastAPI** à tronquer la sortie utilisée pour OpenAPI à cet endroit. -Il n'apparaîtra pas dans la documentation, mais d'autres outils (tel que Sphinx) pourront utiliser le reste. +Cela n’apparaîtra pas dans la documentation, mais d’autres outils (comme Sphinx) pourront utiliser le reste. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} -## Réponses supplémentaires +## Réponses supplémentaires { #additional-responses } -Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour une *opération de chemin*. +Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour un chemin d’accès. -Cela définit les métadonnées sur la réponse principale d'une *opération de chemin*. +Cela définit les métadonnées sur la réponse principale d’un chemin d’accès. Vous pouvez également déclarer des réponses supplémentaires avec leurs modèles, codes de statut, etc. -Il y a un chapitre entier ici dans la documentation à ce sujet, vous pouvez le lire sur [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Il y a un chapitre entier dans la documentation à ce sujet, vous pouvez le lire dans [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. -## OpenAPI supplémentaire +## OpenAPI supplémentaire { #openapi-extra } -Lorsque vous déclarez un *chemin* dans votre application, **FastAPI** génère automatiquement les métadonnées concernant ce *chemin* à inclure dans le schéma OpenAPI. +Lorsque vous déclarez un chemin d’accès dans votre application, **FastAPI** génère automatiquement les métadonnées pertinentes à propos de ce chemin d’accès à inclure dans le schéma OpenAPI. /// note | Détails techniques -La spécification OpenAPI appelle ces métadonnées des Objets d'opération. +Dans la spécification OpenAPI, cela s’appelle l’objet Operation. /// -Il contient toutes les informations sur le *chemin* et est utilisé pour générer automatiquement la documentation. +Il contient toutes les informations sur le chemin d’accès et est utilisé pour générer la documentation automatique. Il inclut les `tags`, `parameters`, `requestBody`, `responses`, etc. -Ce schéma OpenAPI spécifique aux *operations* est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l'étendre. +Ce schéma OpenAPI spécifique à un chemin d’accès est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l’étendre. /// tip | Astuce -Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d'utiliser les [réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Ceci est un point d’extension de bas niveau. + +Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d’utiliser [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. /// -Vous pouvez étendre le schéma OpenAPI pour une *opération de chemin* en utilisant le paramètre `openapi_extra`. +Vous pouvez étendre le schéma OpenAPI pour un chemin d’accès en utilisant le paramètre `openapi_extra`. -### Extensions OpenAPI +### Extensions OpenAPI { #openapi-extensions } -Cet `openapi_extra` peut être utile, par exemple, pour déclarer [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) : +Cet `openapi_extra` peut être utile, par exemple, pour déclarer des [Extensions 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 vous ouvrez la documentation automatique de l'API, votre extension apparaîtra au bas du *chemin* spécifique. +Si vous ouvrez la documentation automatique de l’API, votre extension apparaîtra en bas du chemin d’accès spécifique. -Et dans le fichier openapi généré (`/openapi.json`), vous verrez également votre extension dans le cadre du *chemin* spécifique : +Et si vous consultez l’OpenAPI résultant (à `/openapi.json` dans votre API), vous verrez également votre extension comme partie du chemin d’accès spécifique : ```JSON hl_lines="22" { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -127,44 +129,44 @@ Et dans le fichier openapi généré (`/openapi.json`), vous verrez également v } ``` -### Personnalisation du Schéma OpenAPI pour un chemin +### Personnaliser le schéma OpenAPI d’un chemin d’accès { #custom-openapi-path-operation-schema } -Le dictionnaire contenu dans la variable `openapi_extra` sera fusionné avec le schéma OpenAPI généré automatiquement pour l'*opération de chemin*. +Le dictionnaire dans `openapi_extra` sera fusionné en profondeur avec le schéma OpenAPI généré automatiquement pour le chemin d’accès. Ainsi, vous pouvez ajouter des données supplémentaires au schéma généré automatiquement. -Par exemple, vous pouvez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de validation proposée par Pydantic, mais vous pouvez toujours définir la requête dans le schéma OpenAPI. +Par exemple, vous pourriez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de FastAPI avec Pydantic, mais vous pourriez tout de même vouloir définir la requête dans le schéma OpenAPI. -Vous pouvez le faire avec `openapi_extra` : +Vous pourriez le faire avec `openapi_extra` : -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[20:37,39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} -Dans cet exemple, nous n'avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n'est même pas parsé en tant que JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargé de l'analyser d'une manière ou d'une autre. +Dans cet exemple, nous n’avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n’est même pas parsé en JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargée de l’analyser d’une manière ou d’une autre. Néanmoins, nous pouvons déclarer le schéma attendu pour le corps de la requête. -### Type de contenu OpenAPI personnalisé +### Type de contenu OpenAPI personnalisé { #custom-openapi-content-type } -En utilisant cette même astuce, vous pouvez utiliser un modèle Pydantic pour définir le schéma JSON qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le *chemin* concerné. +En utilisant cette même astuce, vous pourriez utiliser un modèle Pydantic pour définir le JSON Schema qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le chemin d’accès. -Et vous pouvez le faire même si le type de données dans la requête n'est pas au format JSON. +Et vous pourriez le faire même si le type de données dans la requête n’est pas du JSON. -Dans cet exemple, nous n'utilisons pas les fonctionnalités de FastAPI pour extraire le schéma JSON des modèles Pydantic ni la validation automatique pour JSON. En fait, nous déclarons le type de contenu de la requête en tant que YAML, et non JSON : +Par exemple, dans cette application nous n’utilisons pas la fonctionnalité intégrée de FastAPI pour extraire le JSON Schema des modèles Pydantic ni la validation automatique pour le JSON. En fait, nous déclarons le type de contenu de la requête comme YAML, pas JSON : -{* ../../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] *} -Néanmoins, bien que nous n'utilisions pas la fonctionnalité par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le schéma JSON pour les données que nous souhaitons recevoir en YAML. +Néanmoins, bien que nous n’utilisions pas la fonctionnalité intégrée par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le JSON Schema pour les données que nous souhaitons recevoir en YAML. -Ensuite, nous utilisons directement la requête et extrayons son contenu en tant qu'octets. Cela signifie que FastAPI n'essaiera même pas d'analyser le payload de la requête en tant que JSON. +Ensuite, nous utilisons directement la requête et extrayons le corps en tant que `bytes`. Cela signifie que FastAPI n’essaiera même pas d’analyser le payload de la requête en JSON. -Et nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : +Ensuite, dans notre code, nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} /// tip | Astuce Ici, nous réutilisons le même modèle Pydantic. -Mais nous aurions pu tout aussi bien pu le valider d'une autre manière. +Mais de la même manière, nous aurions pu le valider autrement. /// diff --git a/docs/fr/docs/advanced/response-directly.md b/docs/fr/docs/advanced/response-directly.md index 4ff883c77..f35c39c06 100644 --- a/docs/fr/docs/advanced/response-directly.md +++ b/docs/fr/docs/advanced/response-directly.md @@ -1,20 +1,20 @@ -# Renvoyer directement une réponse +# Renvoyer directement une réponse { #return-a-response-directly } -Lorsque vous créez une *opération de chemins* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. +Lorsque vous créez un *chemin d'accès* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. -Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [Encodeur compatible JSON](../tutorial/encoder.md){.internal-link target=_blank}. Ensuite, en arrière-plan, il mettra ces données JSON-compatible (par exemple un `dict`) à l'intérieur d'un `JSONResponse` qui sera utilisé pour envoyer la réponse au client. -Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *opérations de chemin*. +Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *chemins d'accès*. Cela peut être utile, par exemple, pour retourner des en-têtes personnalisés ou des cookies. -## Renvoyer une `Response` +## Renvoyer une `Response` { #return-a-response } En fait, vous pouvez retourner n'importe quelle `Response` ou n'importe quelle sous-classe de celle-ci. -/// note | Remarque +/// tip | Astuce `JSONResponse` est elle-même une sous-classe de `Response`. @@ -24,27 +24,27 @@ Et quand vous retournez une `Response`, **FastAPI** la transmet directement. Elle ne fera aucune conversion de données avec les modèles Pydantic, elle ne convertira pas le contenu en un type quelconque. -Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données. +Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données, etc. -## Utiliser le `jsonable_encoder` dans une `Response` +## Utiliser le `jsonable_encoder` dans une `Response` { #using-the-jsonable-encoder-in-a-response } -Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt à être utilisé (sérialisable). +Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt pour cela. Par exemple, vous ne pouvez pas mettre un modèle Pydantic dans une `JSONResponse` sans d'abord le convertir en un `dict` avec tous les types de données (comme `datetime`, `UUID`, etc.) convertis en types compatibles avec JSON. -Pour ces cas, vous pouvez spécifier un appel à `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : +Pour ces cas, vous pouvez utiliser le `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Détails techniques Vous pouvez aussi utiliser `from starlette.responses import JSONResponse`. -**FastAPI** fournit le même objet `starlette.responses` que `fastapi.responses` juste par commodité pour le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. +**FastAPI** fournit le même `starlette.responses` que `fastapi.responses` juste par commodité pour vous, le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. /// -## Renvoyer une `Response` personnalisée +## Renvoyer une `Response` personnalisée { #returning-a-custom-response } L'exemple ci-dessus montre toutes les parties dont vous avez besoin, mais il n'est pas encore très utile, car vous auriez pu retourner l'`item` directement, et **FastAPI** l'aurait mis dans une `JSONResponse` pour vous, en le convertissant en `dict`, etc. Tout cela par défaut. @@ -54,9 +54,9 @@ Disons que vous voulez retourner une réponse étant l'un des frameworks Python les plus rapides disponibles, seulement inférieur à Starlette et Uvicorn (tous deux utilisés au cœur de FastAPI). (*) +Les benchmarks indépendants de TechEmpower montrent que les applications **FastAPI** s’exécutant avec Uvicorn sont parmi les frameworks Python les plus rapides disponibles, seulement en dessous de Starlette et Uvicorn eux‑mêmes (tous deux utilisés en interne par FastAPI). -Mais en prêtant attention aux tests de performance et aux comparaisons, il faut tenir compte de ce qu'il suit. +Mais en prêtant attention aux tests de performance et aux comparaisons, vous devez tenir compte de ce qui suit. -## Tests de performance et rapidité +## Tests de performance et rapidité { #benchmarks-and-speed } Lorsque vous vérifiez les tests de performance, il est commun de voir plusieurs outils de différents types comparés comme équivalents. En particulier, on voit Uvicorn, Starlette et FastAPI comparés (parmi de nombreux autres outils). -Plus le problème résolu par un outil est simple, mieux seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. +Plus le problème résolu par un outil est simple, meilleures seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. La hiérarchie est la suivante : * **Uvicorn** : un serveur ASGI - * **Starlette** : (utilise Uvicorn) un micro-framework web - * **FastAPI**: (utilise Starlette) un micro-framework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. + * **Starlette** : (utilise Uvicorn) un microframework web + * **FastAPI**: (utilise Starlette) un microframework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. * **Uvicorn** : - * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis-à-part le serveur en lui-même. - * On n'écrit pas une application avec uniquement Uvicorn. Cela signifie que le code devrait inclure plus ou moins, au minimum, tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale apportera les mêmes complications que si on avait utilisé un framework et que l'on avait minimisé la quantité de code et de bugs. - * Si on compare Uvicorn, il faut le comparer à d'autre applications de serveurs comme Daphne, Hypercorn, uWSGI, etc. + * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis à part le serveur en lui‑même. + * On n'écrit pas une application directement avec Uvicorn. Cela signifie que le code devrait inclure, au minimum, plus ou moins tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale aura la même surcharge que si on avait utilisé un framework, tout en minimisant la quantité de code et les bugs. + * Si on compare Uvicorn, il faut le comparer à d'autres serveurs d'applications comme Daphne, Hypercorn, uWSGI, etc. * **Starlette** : - * A les seconde meilleures performances après Uvicorn. Starlette utilise en réalité Uvicorn. De ce fait, il ne peut qu’être plus "lent" qu'Uvicorn car il requiert l'exécution de plus de code. - * Cependant il nous apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. - * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou micorframework) comme Sanic, Flask, Django, etc. + * A les secondes meilleures performances après Uvicorn. En réalité, Starlette utilise Uvicorn. De ce fait, il ne peut qu’être plus « lent » qu'Uvicorn car il requiert l'exécution de plus de code. + * Cependant, il apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. + * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou microframeworks) comme Sanic, Flask, Django, etc. * **FastAPI** : - * Comme Starlette, FastAPI utilise Uvicorn et ne peut donc pas être plus rapide que ce dernier. - * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités qui sont nécessaires presque systématiquement lors de la création d'une API, comme la validation des données, la sérialisation. En utilisant FastAPI, on obtient une documentation automatiquement (qui ne requiert aucune manipulation pour être mise en place). - * Si on n'utilisait pas FastAPI mais directement Starlette (ou un outil équivalent comme Sanic, Flask, Responder, etc) il faudrait implémenter la validation des données et la sérialisation par nous-même. Le résultat serait donc le même dans les deux cas mais du travail supplémentaire serait à réaliser avec Starlette, surtout en considérant que la validation des données et la sérialisation représentent la plus grande quantité de code à écrire dans une application. - * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient les mêmes performances (si ce n'est de meilleurs performances) que l'on aurait pu avoir sans ce framework (en ayant à implémenter de nombreuses fonctionnalités importantes par nous-mêmes). - * Si on compare FastAPI, il faut le comparer à d'autres frameworks web (ou ensemble d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. + * Comme Starlette utilise Uvicorn et ne peut donc pas être plus rapide que lui, **FastAPI** utilise Starlette et ne peut donc pas être plus rapide que lui. + * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités dont vous avez presque toujours besoin lors de la création d'une API, comme la validation des données et la sérialisation. En l'utilisant, vous obtenez une documentation automatique « gratuitement » (la documentation automatique n'ajoute même pas de surcharge à l’exécution, elle est générée au démarrage). + * Si on n'utilisait pas FastAPI mais directement Starlette (ou un autre outil comme Sanic, Flask, Responder, etc.), il faudrait implémenter toute la validation des données et la sérialisation soi‑même. L'application finale aurait donc la même surcharge que si elle avait été construite avec FastAPI. Et dans de nombreux cas, cette validation des données et cette sérialisation représentent la plus grande quantité de code écrite dans les applications. + * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient probablement les mêmes performances (voire de meilleures performances) que l'on aurait pu avoir sans ce framework (car il aurait fallu tout implémenter dans votre code). + * Si on compare FastAPI, il faut le comparer à d'autres frameworks d’application web (ou ensembles d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. Des frameworks avec validation des données, sérialisation et documentation automatiques intégrées. diff --git a/docs/fr/docs/deployment/docker.md b/docs/fr/docs/deployment/docker.md index 05b597a2d..ec30f9607 100644 --- a/docs/fr/docs/deployment/docker.md +++ b/docs/fr/docs/deployment/docker.md @@ -1,75 +1,150 @@ -# Déployer avec Docker +# FastAPI dans des conteneurs - Docker { #fastapi-in-containers-docker } -Dans cette section, vous verrez des instructions et des liens vers des guides pour savoir comment : +Lors du déploiement d'applications FastAPI, une approche courante consiste à construire une **image de conteneur Linux**. C'est généralement fait avec **Docker**. Vous pouvez ensuite déployer cette image de conteneur de plusieurs façons possibles. -* Faire de votre application **FastAPI** une image/conteneur Docker avec une performance maximale. En environ **5 min**. -* (Optionnellement) comprendre ce que vous, en tant que développeur, devez savoir sur HTTPS. -* Configurer un cluster en mode Docker Swarm avec HTTPS automatique, même sur un simple serveur à 5 dollars US/mois. En environ **20 min**. -* Générer et déployer une application **FastAPI** complète, en utilisant votre cluster Docker Swarm, avec HTTPS, etc. En environ **10 min**. - -Vous pouvez utiliser **Docker** pour le déploiement. Il présente plusieurs avantages comme la sécurité, la réplicabilité, la simplicité de développement, etc. - -Si vous utilisez Docker, vous pouvez utiliser l'image Docker officielle : - -## tiangolo/uvicorn-gunicorn-fastapi - -Cette image est dotée d'un mécanisme d'"auto-tuning", de sorte qu'il vous suffit d'ajouter votre code pour obtenir automatiquement des performances très élevées. Et sans faire de sacrifices. - -Mais vous pouvez toujours changer et mettre à jour toutes les configurations avec des variables d'environnement ou des fichiers de configuration. +L'utilisation de conteneurs Linux présente plusieurs avantages, notamment la **sécurité**, la **réplicabilité**, la **simplicité**, entre autres. /// tip | Astuce -Pour voir toutes les configurations et options, rendez-vous sur la page de l'image Docker : tiangolo/uvicorn-gunicorn-fastapi. +Vous êtes pressé et vous connaissez déjà tout ça ? Allez directement au [`Dockerfile` ci-dessous 👇](#build-a-docker-image-for-fastapi). /// -## Créer un `Dockerfile` - -* Allez dans le répertoire de votre projet. -* Créez un `Dockerfile` avec : +
+Aperçu du Dockerfile 👀 ```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +FROM python:3.9 -COPY ./app /app +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["fastapi", "run", "app/main.py", "--port", "80"] + +# Si vous exécutez derrière un proxy comme Nginx ou Traefik, ajoutez --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ``` -### Applications plus larges +
-Si vous avez suivi la section sur la création d' [Applications avec plusieurs fichiers](../tutorial/bigger-applications.md){.internal-link target=_blank}, votre `Dockerfile` pourrait ressembler à ceci : +## Qu'est-ce qu'un conteneur { #what-is-a-container } -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +Les conteneurs (principalement les conteneurs Linux) sont un moyen très **léger** d'empaqueter des applications, y compris toutes leurs dépendances et les fichiers nécessaires, tout en les isolant des autres conteneurs (autres applications ou composants) dans le même système. -COPY ./app /app/app +Les conteneurs Linux s'exécutent en utilisant le même noyau Linux que l'hôte (machine, machine virtuelle, serveur cloud, etc.). Cela signifie simplement qu'ils sont très légers (comparés à des machines virtuelles complètes émulant un système d'exploitation entier). + +Ainsi, les conteneurs consomment **peu de ressources**, une quantité comparable à l'exécution directe des processus (alors qu'une machine virtuelle consommerait beaucoup plus). + +Les conteneurs ont également leurs propres processus d'exécution **isolés** (généralement un seul processus), leur système de fichiers et leur réseau, ce qui simplifie le déploiement, la sécurité, le développement, etc. + +## Qu'est-ce qu'une image de conteneur { #what-is-a-container-image } + +Un **conteneur** s'exécute à partir d'une **image de conteneur**. + +Une image de conteneur est une version **statique** de tous les fichiers, des variables d'environnement et de la commande/le programme par défaut devant être présents dans un conteneur. Ici, **statique** signifie que l'**image** du conteneur ne s'exécute pas, elle n'est pas en cours d'exécution, ce ne sont que les fichiers et métadonnées empaquetés. + +Par opposition à une « **image de conteneur** » qui correspond aux contenus statiques stockés, un « **conteneur** » fait normalement référence à l'instance en cours d'exécution, la chose qui est **exécutée**. + +Lorsque le **conteneur** est démarré et en cours d'exécution (démarré à partir d'une **image de conteneur**), il peut créer ou modifier des fichiers, des variables d'environnement, etc. Ces changements n'existeront que dans ce conteneur, mais ne persisteront pas dans l'image de conteneur sous-jacente (ils ne seront pas enregistrés sur le disque). + +Une image de conteneur est comparable au **programme** et à ses contenus, par exemple `python` et un fichier `main.py`. + +Et le **conteneur** lui-même (par opposition à l'**image de conteneur**) est l'instance en cours d'exécution réelle de l'image, comparable à un **processus**. En fait, un conteneur ne fonctionne que lorsqu'il a un **processus en cours d'exécution** (et normalement, il s'agit d'un seul processus). Le conteneur s'arrête lorsqu'aucun processus n'y est en cours d'exécution. + +## Images de conteneur { #container-images } + +Docker a été l'un des principaux outils pour créer et gérer des **images de conteneur** et des **conteneurs**. + +Et il existe un Docker Hub public avec des **images de conteneur officielles** pré-construites pour de nombreux outils, environnements, bases de données et applications. + +Par exemple, il existe une image Python officielle. + +Et il existe beaucoup d'autres images pour différentes choses comme des bases de données, par exemple : + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +En utilisant une image de conteneur pré-construite, il est très facile de **combiner** et d'utiliser différents outils. Par exemple, pour essayer une nouvelle base de données. Dans la plupart des cas, vous pouvez utiliser les **images officielles** et simplement les configurer avec des variables d'environnement. + +Ainsi, dans de nombreux cas, vous pouvez apprendre les conteneurs et Docker et réutiliser ces connaissances avec de nombreux outils et composants différents. + +Vous exécuteriez donc **plusieurs conteneurs** avec des éléments différents, comme une base de données, une application Python, un serveur web avec une application frontend React, et les connecter entre eux via leur réseau interne. + +Tous les systèmes de gestion de conteneurs (comme Docker ou Kubernetes) disposent de ces fonctionnalités réseau intégrées. + +## Conteneurs et processus { #containers-and-processes } + +Une **image de conteneur** inclut normalement dans ses métadonnées le programme/la commande par défaut à exécuter lorsque le **conteneur** est démarré et les paramètres à transmettre à ce programme. Très similaire à ce que vous utiliseriez en ligne de commande. + +Lorsqu'un **conteneur** est démarré, il exécutera cette commande/ce programme (bien que vous puissiez la/le remplacer et faire exécuter une autre commande/un autre programme). + +Un conteneur fonctionne tant que le **processus principal** (commande ou programme) est en cours d'exécution. + +Un conteneur a normalement un **seul processus**, mais il est aussi possible de démarrer des sous-processus à partir du processus principal, et ainsi vous aurez **plusieurs processus** dans le même conteneur. + +Mais il n'est pas possible d'avoir un conteneur en cours d'exécution sans **au moins un processus en cours**. Si le processus principal s'arrête, le conteneur s'arrête. + +## Construire une image Docker pour FastAPI { #build-a-docker-image-for-fastapi } + +Très bien, construisons quelque chose maintenant ! 🚀 + +Je vais vous montrer comment construire une **image Docker** pour FastAPI **à partir de zéro**, basée sur l'image **officielle Python**. + +C'est ce que vous voudrez faire dans **la plupart des cas**, par exemple : + +* Utiliser **Kubernetes** ou des outils similaires +* Exécuter sur un **Raspberry Pi** +* Utiliser un service cloud qui exécuterait une image de conteneur pour vous, etc. + +### Dépendances des paquets { #package-requirements } + +Vous aurez normalement les **dépendances des paquets** de votre application dans un fichier. + +Cela dépendra principalement de l'outil que vous utilisez pour **installer** ces dépendances. + +La manière la plus courante consiste à avoir un fichier `requirements.txt` avec les noms des paquets et leurs versions, un par ligne. + +Vous utiliserez bien sûr les mêmes idées que vous avez lues dans [À propos des versions de FastAPI](versions.md){.internal-link target=_blank} pour définir les plages de versions. + +Par exemple, votre `requirements.txt` pourrait ressembler à : + +``` +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` -### Raspberry Pi et autres architectures +Et vous installerez normalement ces dépendances de paquets avec `pip`, par exemple : -Si vous utilisez Docker sur un Raspberry Pi (qui a un processeur ARM) ou toute autre architecture, vous pouvez créer un `Dockerfile` à partir de zéro, basé sur une image de base Python (qui est multi-architecture) et utiliser Uvicorn seul. +
-Dans ce cas, votre `Dockerfile` pourrait ressembler à ceci : - -```Dockerfile -FROM python:3.7 - -RUN pip install fastapi uvicorn - -EXPOSE 80 - -COPY ./app /app - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic ``` -## Créer le code **FastAPI**. +
-* Créer un répertoire `app` et y entrer. -* Créez un fichier `main.py` avec : +/// info + +Il existe d'autres formats et outils pour définir et installer des dépendances de paquets. + +/// + +### Créer le code **FastAPI** { #create-the-fastapi-code } + +* Créez un répertoire `app` et entrez dedans. +* Créez un fichier vide `__init__.py`. +* Créez un fichier `main.py` avec : ```Python -from typing import Optional - from fastapi import FastAPI app = FastAPI() @@ -81,22 +156,168 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -* Vous devriez maintenant avoir une structure de répertoire telle que : +### Dockerfile { #dockerfile } + +Maintenant, dans le même répertoire de projet, créez un fichier `Dockerfile` avec : + +```{ .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 ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +1. Démarrer à partir de l'image de base Python officielle. + +2. Définir le répertoire de travail courant sur `/code`. + + C'est là que nous placerons le fichier `requirements.txt` et le répertoire `app`. + +3. Copier le fichier des dépendances vers le répertoire `/code`. + + Copier **uniquement** le fichier des dépendances en premier, pas le reste du code. + + Comme ce fichier **ne change pas souvent**, Docker le détectera et utilisera le **cache** pour cette étape, ce qui activera le cache pour l'étape suivante aussi. + +4. Installer les dépendances listées dans le fichier des dépendances. + + L'option `--no-cache-dir` indique à `pip` de ne pas enregistrer localement les paquets téléchargés, car cela ne sert que si `pip` devait être relancé pour installer les mêmes paquets, mais ce n'est pas le cas lorsque l'on travaille avec des conteneurs. + + /// note | Remarque + + Le `--no-cache-dir` concerne uniquement `pip`, cela n'a rien à voir avec Docker ou les conteneurs. + + /// + + L'option `--upgrade` indique à `pip` de mettre à niveau les paquets s'ils sont déjà installés. + + Comme l'étape précédente de copie du fichier peut être détectée par le **cache Docker**, cette étape **utilisera également le cache Docker** lorsqu'il est disponible. + + L'utilisation du cache à cette étape vous **fera gagner** beaucoup de **temps** lors de la reconstruction de l'image encore et encore pendant le développement, au lieu de **télécharger et installer** toutes les dépendances **à chaque fois**. + +5. Copier le répertoire `./app` dans le répertoire `/code`. + + Comme cela contient tout le code qui est ce qui **change le plus fréquemment**, le **cache** Docker ne sera pas facilement utilisé pour cette étape ou pour les **étapes suivantes**. + + Il est donc important de placer cela **vers la fin** du `Dockerfile`, pour optimiser les temps de construction de l'image de conteneur. + +6. Définir la **commande** pour utiliser `fastapi run`, qui utilise Uvicorn sous le capot. + + `CMD` prend une liste de chaînes, chacune de ces chaînes correspond à ce que vous taperiez en ligne de commande séparé par des espaces. + + Cette commande sera exécutée à partir du **répertoire de travail courant**, le même répertoire `/code` que vous avez défini plus haut avec `WORKDIR /code`. + +/// tip | Astuce + +Passez en revue ce que fait chaque ligne en cliquant sur chaque bulle numérotée dans le code. 👆 + +/// + +/// warning | Alertes + +Vous devez vous assurer d'utiliser **toujours** la **forme exec** de l'instruction `CMD`, comme expliqué ci-dessous. + +/// + +#### Utiliser `CMD` - Forme Exec { #use-cmd-exec-form } + +L'instruction Docker `CMD` peut être écrite sous deux formes : + +✅ Forme **Exec** : + +```Dockerfile +# ✅ À faire +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ Forme **Shell** : + +```Dockerfile +# ⛔️ À ne pas faire +CMD fastapi run app/main.py --port 80 +``` + +Assurez-vous d'utiliser toujours la forme **exec** pour garantir que FastAPI peut s'arrêter proprement et que les [événements de cycle de vie](../advanced/events.md){.internal-link target=_blank} sont déclenchés. + +Vous pouvez en lire davantage dans la documentation Docker sur les formes shell et exec. + +Cela peut être très visible lors de l'utilisation de `docker compose`. Voir cette section de la FAQ Docker Compose pour plus de détails techniques : Pourquoi mes services mettent-ils 10 secondes à se recréer ou à s'arrêter ?. + +#### Structure du répertoire { #directory-structure } + +Vous devriez maintenant avoir une structure de répertoire comme : ``` . ├── app +│   ├── __init__.py │ └── main.py -└── Dockerfile +├── Dockerfile +└── requirements.txt ``` -## Construire l'image Docker +#### Derrière un proxy de terminaison TLS { #behind-a-tls-termination-proxy } -* Allez dans le répertoire du projet (dans lequel se trouve votre `Dockerfile`, contenant votre répertoire `app`). +Si vous exécutez votre conteneur derrière un proxy de terminaison TLS (load balancer) comme Nginx ou Traefik, ajoutez l'option `--proxy-headers`, cela indiquera à Uvicorn (via la CLI FastAPI) de faire confiance aux en-têtes envoyés par ce proxy lui indiquant que l'application s'exécute derrière HTTPS, etc. + +```Dockerfile +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] +``` + +#### Cache Docker { #docker-cache } + +Il y a une astuce importante dans ce `Dockerfile`, nous copions d'abord **le fichier des dépendances seul**, pas le reste du code. Laissez-moi vous expliquer pourquoi. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker et d'autres outils **construisent** ces images de conteneur **de manière incrémentale**, en ajoutant **une couche au-dessus de l'autre**, en commençant par le haut du `Dockerfile` et en ajoutant tous les fichiers créés par chacune des instructions du `Dockerfile`. + +Docker et des outils similaires utilisent également un **cache interne** lors de la construction de l'image : si un fichier n'a pas changé depuis la dernière construction de l'image de conteneur, alors il va **réutiliser la même couche** créée la dernière fois, au lieu de recopier le fichier et créer une nouvelle couche à partir de zéro. + +Éviter simplement la copie des fichiers n'améliore pas nécessairement les choses de manière significative, mais comme il a utilisé le cache pour cette étape, il peut **utiliser le cache pour l'étape suivante**. Par exemple, il peut utiliser le cache pour l'instruction qui installe les dépendances avec : + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Le fichier des dépendances **ne changera pas fréquemment**. Ainsi, en copiant uniquement ce fichier, Docker pourra **utiliser le cache** pour cette étape. + +Et ensuite, Docker pourra **utiliser le cache pour l'étape suivante** qui télécharge et installe ces dépendances. Et c'est là que nous **gagnons beaucoup de temps**. ✨ ... et évitons l'ennui en attendant. 😪😆 + +Télécharger et installer les dépendances de paquets **peut prendre des minutes**, mais utiliser le **cache** ne **prendra que quelques secondes** au plus. + +Et comme vous reconstruirez l'image de conteneur encore et encore pendant le développement pour vérifier que vos modifications de code fonctionnent, cela vous fera gagner beaucoup de temps cumulé. + +Ensuite, vers la fin du `Dockerfile`, nous copions tout le code. Comme c'est ce qui **change le plus fréquemment**, nous le plaçons vers la fin, car presque toujours, tout ce qui suit cette étape ne pourra pas utiliser le cache. + +```Dockerfile +COPY ./app /code/app +``` + +### Construire l'image Docker { #build-the-docker-image } + +Maintenant que tous les fichiers sont en place, construisons l'image de conteneur. + +* Allez dans le répertoire du projet (là où se trouve votre `Dockerfile`, contenant votre répertoire `app`). * Construisez votre image FastAPI :
@@ -109,9 +330,17 @@ $ docker build -t myimage .
-## Démarrer le conteneur Docker +/// tip | Astuce -* Exécutez un conteneur basé sur votre image : +Remarquez le `.` à la fin, équivalent à `./`, il indique à Docker le répertoire à utiliser pour construire l'image de conteneur. + +Dans ce cas, c'est le même répertoire courant (`.`). + +/// + +### Démarrer le conteneur Docker { #start-the-docker-container } + +* Exécutez un conteneur basé sur votre image :
@@ -121,65 +350,269 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-Vous disposez maintenant d'un serveur FastAPI optimisé dans un conteneur Docker. Configuré automatiquement pour votre -serveur actuel (et le nombre de cœurs du CPU). +## Vérifier { #check-it } -## Vérifier +Vous devriez pouvoir le vérifier via l'URL de votre conteneur Docker, par exemple : http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou équivalent, en utilisant votre hôte Docker). -Vous devriez pouvoir accéder à votre application via l'URL de votre conteneur Docker, par exemple : http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou équivalent, en utilisant votre hôte Docker). - -Vous verrez quelque chose comme : +Vous verrez quelque chose comme : ```JSON {"item_id": 5, "q": "somequery"} ``` -## Documentation interactive de l'API +## Documentation interactive de l'API { #interactive-api-docs } -Vous pouvez maintenant visiter http://192.168.99.100/docs ou http://127.0.0.1/docs (ou équivalent, en utilisant votre hôte Docker). +Vous pouvez maintenant aller sur http://192.168.99.100/docs ou http://127.0.0.1/docs (ou équivalent, en utilisant votre hôte Docker). -Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : +Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## Documentation de l'API alternative +## Documentation alternative de l'API { #alternative-api-docs } -Et vous pouvez également aller sur http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou équivalent, en utilisant votre hôte Docker). +Et vous pouvez aussi aller sur http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou équivalent, en utilisant votre hôte Docker). -Vous verrez la documentation automatique alternative (fournie par ReDoc) : +Vous verrez la documentation automatique alternative (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Traefik +## Construire une image Docker avec un FastAPI mono-fichier { #build-a-docker-image-with-a-single-file-fastapi } -Traefik est un reverse proxy/load balancer -haute performance. Il peut faire office de "Proxy de terminaison TLS" (entre autres fonctionnalités). +Si votre FastAPI est un seul fichier, par exemple `main.py` sans répertoire `./app`, votre structure de fichiers pourrait ressembler à ceci : -Il est intégré à Let's Encrypt. Ainsi, il peut gérer toutes les parties HTTPS, y compris l'acquisition et le renouvellement des certificats. +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` -Il est également intégré à Docker. Ainsi, vous pouvez déclarer vos domaines dans les configurations de chaque application et faire en sorte qu'elles lisent ces configurations, génèrent les certificats HTTPS et servent via HTTPS à votre application automatiquement, sans nécessiter aucune modification de leurs configurations. +Vous n'auriez alors qu'à changer les chemins correspondants pour copier le fichier dans le `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 ["fastapi", "run", "main.py", "--port", "80"] +``` + +1. Copier le fichier `main.py` directement dans le répertoire `/code` (sans répertoire `./app`). + +2. Utiliser `fastapi run` pour servir votre application dans le fichier unique `main.py`. + +Lorsque vous passez le fichier à `fastapi run`, il détectera automatiquement qu'il s'agit d'un fichier unique et non d'un package et saura comment l'importer et servir votre application FastAPI. 😎 + +## Concepts de déploiement { #deployment-concepts } + +Parlons à nouveau de certains des mêmes [Concepts de déploiement](concepts.md){.internal-link target=_blank} en termes de conteneurs. + +Les conteneurs sont principalement un outil pour simplifier le processus de **construction et de déploiement** d'une application, mais ils n'imposent pas une approche particulière pour gérer ces **concepts de déploiement**, et il existe plusieurs stratégies possibles. + +La **bonne nouvelle**, c'est qu'avec chaque stratégie différente, il existe un moyen de couvrir tous les concepts de déploiement. 🎉 + +Passons en revue ces **concepts de déploiement** en termes de conteneurs : + +* HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage + +## HTTPS { #https } + +Si l'on se concentre uniquement sur l'**image de conteneur** pour une application FastAPI (et plus tard sur le **conteneur** en cours d'exécution), HTTPS serait normalement géré **à l'extérieur** par un autre outil. + +Cela pourrait être un autre conteneur, par exemple avec Traefik, gérant **HTTPS** et l'acquisition **automatique** des **certificats**. + +/// tip | Astuce + +Traefik s'intègre avec Docker, Kubernetes, et d'autres, donc il est très facile de configurer HTTPS pour vos conteneurs avec lui. + +/// + +Alternativement, HTTPS pourrait être géré par un fournisseur cloud comme l'un de leurs services (tout en exécutant l'application dans un conteneur). + +## Exécution au démarrage et redémarrages { #running-on-startup-and-restarts } + +Il y a normalement un autre outil chargé de **démarrer et exécuter** votre conteneur. + +Cela pourrait être **Docker** directement, **Docker Compose**, **Kubernetes**, un **service cloud**, etc. + +Dans la plupart (ou toutes) des situations, il existe une option simple pour activer l'exécution du conteneur au démarrage et activer les redémarrages en cas d'échec. Par exemple, dans Docker, c'est l'option de ligne de commande `--restart`. + +Sans utiliser de conteneurs, faire en sorte que les applications s'exécutent au démarrage et avec redémarrages peut être fastidieux et difficile. Mais en **travaillant avec des conteneurs**, dans la plupart des cas, cette fonctionnalité est incluse par défaut. ✨ + +## Réplication - Nombre de processus { #replication-number-of-processes } + +Si vous avez un cluster de machines avec **Kubernetes**, Docker Swarm Mode, Nomad, ou un autre système complexe similaire pour gérer des conteneurs distribués sur plusieurs machines, alors vous voudrez probablement **gérer la réplication** au **niveau du cluster** plutôt que d'utiliser un **gestionnaire de processus** (comme Uvicorn avec workers) dans chaque conteneur. + +L'un de ces systèmes de gestion de conteneurs distribués comme Kubernetes dispose normalement d'une manière intégrée de gérer la **réplication des conteneurs** tout en supportant l'**équilibrage de charge** des requêtes entrantes. Le tout au **niveau du cluster**. + +Dans ces cas, vous voudrez probablement construire une **image Docker à partir de zéro** comme [expliqué ci-dessus](#dockerfile), en installant vos dépendances et en exécutant **un seul processus Uvicorn** au lieu d'utiliser plusieurs workers Uvicorn. + +### Équilibreur de charge { #load-balancer } + +Lors de l'utilisation de conteneurs, vous aurez normalement un composant **à l'écoute sur le port principal**. Cela pourrait être un autre conteneur qui est également un **proxy de terminaison TLS** pour gérer **HTTPS** ou un outil similaire. + +Comme ce composant prend la **charge** des requêtes et la distribue entre les workers de façon (espérons-le) **équilibrée**, on l'appelle également communément un **équilibreur de charge**. + +/// tip | Astuce + +Le même composant de **proxy de terminaison TLS** utilisé pour HTTPS sera probablement aussi un **équilibreur de charge**. + +/// + +Et en travaillant avec des conteneurs, le même système que vous utilisez pour les démarrer et les gérer dispose déjà d'outils internes pour transmettre la **communication réseau** (par ex. les requêtes HTTP) depuis cet **équilibreur de charge** (qui peut aussi être un **proxy de terminaison TLS**) vers le ou les conteneurs avec votre application. + +### Un équilibreur de charge - Plusieurs conteneurs worker { #one-load-balancer-multiple-worker-containers } + +Lorsque vous travaillez avec **Kubernetes** ou des systèmes de gestion de conteneurs distribués similaires, l'utilisation de leurs mécanismes réseau internes permet au **seul équilibreur de charge** à l'écoute sur le **port** principal de transmettre la communication (les requêtes) vers potentiellement **plusieurs conteneurs** exécutant votre application. + +Chacun de ces conteneurs exécutant votre application aura normalement **un seul processus** (par ex. un processus Uvicorn exécutant votre application FastAPI). Ils seront tous des **conteneurs identiques**, exécutant la même chose, mais chacun avec son propre processus, sa mémoire, etc. De cette façon, vous profiterez de la **parallélisation** sur **différents cœurs** du CPU, voire sur **différentes machines**. + +Et le système de conteneurs distribués avec l'**équilibreur de charge** **distribuera les requêtes** à chacun des conteneurs exécutant votre application **à tour de rôle**. Ainsi, chaque requête pourrait être traitée par l'un des multiples **conteneurs répliqués** exécutant votre application. + +Et normalement cet **équilibreur de charge** pourra gérer des requêtes qui vont vers *d'autres* applications dans votre cluster (par ex. vers un autre domaine, ou sous un autre préfixe de chemin d'URL), et transmettra cette communication aux bons conteneurs pour *cette autre* application s'exécutant dans votre cluster. + +### Un processus par conteneur { #one-process-per-container } + +Dans ce type de scénario, vous voudrez probablement avoir **un seul processus (Uvicorn) par conteneur**, puisque vous gérez déjà la réplication au niveau du cluster. + +Donc, dans ce cas, vous **ne voudrez pas** avoir plusieurs workers dans le conteneur, par exemple avec l'option de ligne de commande `--workers`. Vous voudrez avoir **un seul processus Uvicorn** par conteneur (mais probablement plusieurs conteneurs). + +Avoir un autre gestionnaire de processus à l'intérieur du conteneur (comme ce serait le cas avec plusieurs workers) n'ajouterait que de la **complexité inutile** que vous gérez très probablement déjà avec votre système de cluster. + +### Conteneurs avec plusieurs processus et cas particuliers { #containers-with-multiple-processes-and-special-cases } + +Bien sûr, il existe des **cas particuliers** où vous pourriez vouloir avoir **un conteneur** avec plusieurs **processus worker Uvicorn** à l'intérieur. + +Dans ces cas, vous pouvez utiliser l'option de ligne de commande `--workers` pour définir le nombre de workers que vous souhaitez exécuter : + +```{ .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. Ici, nous utilisons l'option de ligne de commande `--workers` pour définir le nombre de workers à 4. + +Voici quelques exemples où cela pourrait avoir du sens : + +#### Une application simple { #a-simple-app } + +Vous pourriez vouloir un gestionnaire de processus dans le conteneur si votre application est **suffisamment simple** pour s'exécuter sur un **seul serveur**, pas un cluster. + +#### Docker Compose { #docker-compose } + +Vous pourriez déployer sur un **seul serveur** (pas un cluster) avec **Docker Compose**, donc vous n'auriez pas un moyen simple de gérer la réplication des conteneurs (avec Docker Compose) tout en préservant le réseau partagé et l'**équilibrage de charge**. + +Vous pourriez alors vouloir avoir **un seul conteneur** avec un **gestionnaire de processus** qui démarre **plusieurs processus worker** à l'intérieur. --- -Avec ces informations et ces outils, passez à la section suivante pour tout combiner. +L'idée principale est que **rien** de tout cela ne sont des **règles gravées dans la pierre** que vous devez suivre aveuglément. Vous pouvez utiliser ces idées pour **évaluer votre propre cas d'usage** et décider de la meilleure approche pour votre système, en vérifiant comment gérer les concepts suivants : -## Cluster en mode Docker Swarm avec Traefik et HTTPS +* Sécurité - HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage -Vous pouvez avoir un cluster en mode Docker Swarm configuré en quelques minutes (environ 20 min) avec un processus Traefik principal gérant HTTPS (y compris l'acquisition et le renouvellement des certificats). +## Mémoire { #memory } -En utilisant le mode Docker Swarm, vous pouvez commencer par un "cluster" d'une seule machine (il peut même s'agir -d'un serveur à 5 USD/mois) et ensuite vous pouvez vous développer autant que vous le souhaitez en ajoutant d'autres serveurs. +Si vous exécutez **un seul processus par conteneur**, vous aurez une quantité de mémoire consommée plus ou moins bien définie, stable et limitée par chacun de ces conteneurs (plus d'un s'ils sont répliqués). -Pour configurer un cluster en mode Docker Swarm avec Traefik et la gestion de HTTPS, suivez ce guide : +Vous pouvez alors définir ces mêmes limites et exigences de mémoire dans vos configurations pour votre système de gestion de conteneurs (par exemple dans **Kubernetes**). De cette façon, il pourra **répliquer les conteneurs** sur les **machines disponibles** en tenant compte de la quantité de mémoire dont ils ont besoin et de la quantité disponible sur les machines du cluster. -### Docker Swarm Mode et Traefik pour un cluster HTTPS +Si votre application est **simple**, cela ne sera probablement **pas un problème**, et vous n'aurez peut-être pas besoin de spécifier des limites de mémoire strictes. Mais si vous **utilisez beaucoup de mémoire** (par exemple avec des modèles de **machine learning**), vous devez vérifier combien de mémoire vous consommez et ajuster le **nombre de conteneurs** qui s'exécutent sur **chaque machine** (et peut-être ajouter plus de machines à votre cluster). -### Déployer une application FastAPI +Si vous exécutez **plusieurs processus par conteneur**, vous devez vous assurer que le nombre de processus démarrés ne **consomme pas plus de mémoire** que ce qui est disponible. -La façon la plus simple de tout mettre en place, serait d'utiliser les [**Générateurs de projet FastAPI**](../project-generation.md){.internal-link target=_blank}. +## Étapes préalables au démarrage et conteneurs { #previous-steps-before-starting-and-containers } -Le génerateur de projet adéquat est conçu pour être intégré à ce cluster Docker Swarm avec Traefik et HTTPS décrit ci-dessus. +Si vous utilisez des conteneurs (par ex. Docker, Kubernetes), alors il existe deux approches principales que vous pouvez utiliser. -Vous pouvez générer un projet en 2 min environ. +### Plusieurs conteneurs { #multiple-containers } -Le projet généré a des instructions pour le déployer et le faire prend 2 min de plus. +Si vous avez **plusieurs conteneurs**, probablement chacun exécutant un **seul processus** (par exemple, dans un cluster **Kubernetes**), alors vous voudrez probablement avoir un **conteneur séparé** effectuant le travail des **étapes préalables** dans un seul conteneur, exécutant un seul processus, **avant** d'exécuter les conteneurs worker répliqués. + +/// info + +Si vous utilisez Kubernetes, ce sera probablement un Init Container. + +/// + +Si, dans votre cas d'usage, il n'y a pas de problème à exécuter ces étapes préalables **plusieurs fois en parallèle** (par exemple si vous n'exécutez pas de migrations de base de données, mais vérifiez simplement si la base de données est prête), alors vous pourriez aussi simplement les mettre dans chaque conteneur juste avant de démarrer le processus principal. + +### Un seul conteneur { #single-container } + +Si vous avez une configuration simple, avec **un seul conteneur** qui démarre ensuite plusieurs **processus worker** (ou un seul processus aussi), vous pouvez alors exécuter ces étapes préalables dans le même conteneur, juste avant de démarrer le processus avec l'application. + +### Image Docker de base { #base-docker-image } + +Il existait une image Docker officielle FastAPI : tiangolo/uvicorn-gunicorn-fastapi. Mais elle est désormais dépréciée. ⛔️ + +Vous ne devriez probablement **pas** utiliser cette image Docker de base (ni aucune autre similaire). + +Si vous utilisez **Kubernetes** (ou autres) et que vous définissez déjà la **réplication** au niveau du cluster, avec plusieurs **conteneurs**. Dans ces cas, il est préférable de **construire une image à partir de zéro** comme décrit ci-dessus : [Construire une image Docker pour FastAPI](#build-a-docker-image-for-fastapi). + +Et si vous devez avoir plusieurs workers, vous pouvez simplement utiliser l'option de ligne de commande `--workers`. + +/// note | Détails techniques + +L'image Docker a été créée à une époque où Uvicorn ne supportait pas la gestion et le redémarrage des workers morts, il fallait donc utiliser Gunicorn avec Uvicorn, ce qui ajoutait pas mal de complexité, uniquement pour que Gunicorn gère et redémarre les processus worker Uvicorn. + +Mais maintenant qu'Uvicorn (et la commande `fastapi`) supporte l'usage de `--workers`, il n'y a plus de raison d'utiliser une image Docker de base au lieu de construire la vôtre (c'est à peu près la même quantité de code 😅). + +/// + +## Déployer l'image de conteneur { #deploy-the-container-image } + +Après avoir une image de conteneur (Docker), il existe plusieurs façons de la déployer. + +Par exemple : + +* Avec **Docker Compose** sur un seul serveur +* Avec un cluster **Kubernetes** +* Avec un cluster Docker Swarm Mode +* Avec un autre outil comme Nomad +* Avec un service cloud qui prend votre image de conteneur et la déploie + +## Image Docker avec `uv` { #docker-image-with-uv } + +Si vous utilisez uv pour installer et gérer votre projet, vous pouvez suivre leur guide Docker pour uv. + +## Récapitulatif { #recap } + +Avec les systèmes de conteneurs (par ex. avec **Docker** et **Kubernetes**), il devient assez simple de gérer tous les **concepts de déploiement** : + +* HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage + +Dans la plupart des cas, vous ne voudrez probablement pas utiliser d'image de base, et au contraire **construire une image de conteneur à partir de zéro** basée sur l'image Docker Python officielle. + +En prenant soin de l'**ordre** des instructions dans le `Dockerfile` et du **cache Docker**, vous pouvez **minimiser les temps de construction**, maximiser votre productivité (et éviter l'ennui). 😎 diff --git a/docs/fr/docs/deployment/https.md b/docs/fr/docs/deployment/https.md index 3f7068ff0..74d38cdb9 100644 --- a/docs/fr/docs/deployment/https.md +++ b/docs/fr/docs/deployment/https.md @@ -1,56 +1,231 @@ -# À propos de HTTPS +# À propos de HTTPS { #about-https } -Il est facile de penser que HTTPS peut simplement être "activé" ou non. +Il est facile de supposer que HTTPS est quelque chose qui est simplement « activé » ou non. Mais c'est beaucoup plus complexe que cela. -/// tip +/// tip | Astuce -Si vous êtes pressé ou si cela ne vous intéresse pas, passez aux sections suivantes pour obtenir des instructions étape par étape afin de tout configurer avec différentes techniques. +Si vous êtes pressé ou si cela ne vous intéresse pas, continuez avec les sections suivantes pour obtenir des instructions étape par étape afin de tout configurer avec différentes techniques. /// -Pour apprendre les bases du HTTPS, du point de vue d'un utilisateur, consultez https://howhttps.works/. +Pour apprendre les bases du HTTPS, du point de vue d'un utilisateur, consultez https://howhttps.works/. Maintenant, du point de vue d'un développeur, voici plusieurs choses à avoir en tête en pensant au HTTPS : -* Pour le HTTPS, le serveur a besoin de "certificats" générés par une tierce partie. - * Ces certificats sont en fait acquis auprès de la tierce partie, et non "générés". -* Les certificats ont une durée de vie. - * Ils expirent. - * Puis ils doivent être renouvelés et acquis à nouveau auprès de la tierce partie. -* Le cryptage de la connexion se fait au niveau du protocole TCP. - * C'est une couche en dessous de HTTP. - * Donc, le certificat et le traitement du cryptage sont faits avant HTTP. -* TCP ne connaît pas les "domaines", seulement les adresses IP. - * L'information sur le domaine spécifique demandé se trouve dans les données HTTP. -* Les certificats HTTPS "certifient" un certain domaine, mais le protocole et le cryptage se font au niveau TCP, avant de savoir quel domaine est traité. -* Par défaut, cela signifie que vous ne pouvez avoir qu'un seul certificat HTTPS par adresse IP. - * Quelle que soit la taille de votre serveur ou la taille de chacune des applications qu'il contient. - * Il existe cependant une solution à ce problème. -* Il existe une extension du protocole TLS (celui qui gère le cryptage au niveau TCP, avant HTTP) appelée SNI (indication du nom du serveur). - * Cette extension SNI permet à un seul serveur (avec une seule adresse IP) d'avoir plusieurs certificats HTTPS et de servir plusieurs domaines/applications HTTPS. - * Pour que cela fonctionne, un seul composant (programme) fonctionnant sur le serveur, écoutant sur l'adresse IP publique, doit avoir tous les certificats HTTPS du serveur. -* Après avoir obtenu une connexion sécurisée, le protocole de communication est toujours HTTP. - * Le contenu est crypté, même s'il est envoyé avec le protocole HTTP. +* Pour le HTTPS, **le serveur** doit **disposer de « certificats »** générés par une **tierce partie**. + * Ces certificats sont en réalité **acquis** auprès de la tierce partie, et non « générés ». +* Les certificats ont une **durée de vie**. + * Ils **expirent**. + * Puis ils doivent être **renouvelés**, **acquis à nouveau** auprès de la tierce partie. +* Le cryptage de la connexion se fait au **niveau TCP**. + * C'est une couche **en dessous de HTTP**. + * Donc, la gestion du **certificat et du cryptage** est effectuée **avant HTTP**. +* **TCP ne connaît pas les « domaines »**. Il ne connaît que les adresses IP. + * L'information sur le **domaine spécifique** demandé se trouve dans les **données HTTP**. +* Les **certificats HTTPS** « certifient » un **certain domaine**, mais le protocole et le cryptage se font au niveau TCP, **avant de savoir** quel domaine est traité. +* **Par défaut**, cela signifie que vous ne pouvez avoir qu'**un seul certificat HTTPS par adresse IP**. + * Quelle que soit la taille de votre serveur ou la petitesse de chacune des applications qu'il contient. + * Il existe cependant une **solution** à ce problème. +* Il existe une **extension** du protocole **TLS** (celui qui gère le cryptage au niveau TCP, avant HTTP) appelée **SNI**. + * Cette extension SNI permet à un seul serveur (avec une **seule adresse IP**) d'avoir **plusieurs certificats HTTPS** et de servir **plusieurs domaines/applications HTTPS**. + * Pour que cela fonctionne, un **seul** composant (programme) fonctionnant sur le serveur, écoutant sur l'**adresse IP publique**, doit avoir **tous les certificats HTTPS** du serveur. +* **Après** l'établissement d'une connexion sécurisée, le protocole de communication est **toujours HTTP**. + * Le contenu est **crypté**, même s'il est envoyé avec le **protocole HTTP**. -Il est courant d'avoir un seul programme/serveur HTTP fonctionnant sur le serveur (la machine, l'hôte, etc.) et -gérant toutes les parties HTTPS : envoyer les requêtes HTTP décryptées à l'application HTTP réelle fonctionnant sur -le même serveur (dans ce cas, l'application **FastAPI**), prendre la réponse HTTP de l'application, la crypter en utilisant le certificat approprié et la renvoyer au client en utilisant HTTPS. Ce serveur est souvent appelé un Proxy de terminaison TLS. +Il est courant d'avoir **un seul programme/serveur HTTP** fonctionnant sur le serveur (la machine, l'hôte, etc.) et **gérant toutes les parties HTTPS** : recevoir les **requêtes HTTPS chiffrées**, envoyer les **requêtes HTTP déchiffrées** à l'application HTTP réelle fonctionnant sur le même serveur (l'application **FastAPI**, dans ce cas), prendre la **réponse HTTP** de l'application, la **chiffrer** en utilisant le **certificat HTTPS** approprié et la renvoyer au client en utilisant **HTTPS**. Ce serveur est souvent appelé un **Proxy de terminaison TLS**. -## Let's Encrypt +Parmi les options que vous pourriez utiliser comme Proxy de terminaison TLS : -Avant Let's Encrypt, ces certificats HTTPS étaient vendus par des tiers de confiance. +* Traefik (qui peut également gérer les renouvellements de certificats) +* Caddy (qui peut également gérer les renouvellements de certificats) +* Nginx +* HAProxy -Le processus d'acquisition d'un de ces certificats était auparavant lourd, nécessitait pas mal de paperasses et les certificats étaient assez chers. +## Let's Encrypt { #lets-encrypt } -Mais ensuite, Let's Encrypt a été créé. +Avant Let's Encrypt, ces **certificats HTTPS** étaient vendus par des tiers de confiance. -Il s'agit d'un projet de la Fondation Linux. Il fournit des certificats HTTPS gratuitement. De manière automatisée. Ces certificats utilisent toutes les sécurités cryptographiques standard et ont une durée de vie courte (environ 3 mois), de sorte que la sécurité est en fait meilleure en raison de leur durée de vie réduite. +Le processus d'acquisition de l'un de ces certificats était auparavant lourd, nécessitait pas mal de paperasses et les certificats étaient assez chers. + +Mais ensuite, **Let's Encrypt** a été créé. + +Il s'agit d'un projet de la Fondation Linux. Il fournit **des certificats HTTPS gratuitement**, de manière automatisée. Ces certificats utilisent toutes les sécurités cryptographiques standard et ont une durée de vie courte (environ 3 mois), de sorte que la **sécurité est en fait meilleure** en raison de leur durée de vie réduite. Les domaines sont vérifiés de manière sécurisée et les certificats sont générés automatiquement. Cela permet également d'automatiser le renouvellement de ces certificats. -L'idée est d'automatiser l'acquisition et le renouvellement de ces certificats, afin que vous puissiez disposer d'un HTTPS sécurisé, gratuitement et pour toujours. +L'idée est d'automatiser l'acquisition et le renouvellement de ces certificats, afin que vous puissiez disposer d'un **HTTPS sécurisé, gratuitement et pour toujours**. + +## HTTPS pour les développeurs { #https-for-developers } + +Voici un exemple de ce à quoi pourrait ressembler une API HTTPS, étape par étape, en portant principalement attention aux idées importantes pour les développeurs. + +### Nom de domaine { #domain-name } + +Tout commencerait probablement par le fait que vous **acquériez** un **nom de domaine**. Ensuite, vous le configureriez dans un serveur DNS (possiblement le même que votre fournisseur cloud). + +Vous obtiendriez probablement un serveur cloud (une machine virtuelle) ou quelque chose de similaire, et il aurait une adresse IP **publique** fixe. + +Dans le ou les serveurs DNS, vous configureriez un enregistrement (un « `A record` ») pour faire pointer **votre domaine** vers l'**adresse IP publique de votre serveur**. + +Vous feriez probablement cela une seule fois, la première fois, lors de la mise en place de l'ensemble. + +/// tip | Astuce + +Cette partie relative au nom de domaine intervient bien avant HTTPS, mais comme tout dépend du domaine et de l'adresse IP, il vaut la peine de la mentionner ici. + +/// + +### DNS { #dns } + +Concentrons-nous maintenant sur toutes les parties réellement liées à HTTPS. + +D'abord, le navigateur vérifierait auprès des **serveurs DNS** quelle est l'**IP du domaine**, dans ce cas, `someapp.example.com`. + +Les serveurs DNS indiqueraient au navigateur d'utiliser une **adresse IP** spécifique. Ce serait l'adresse IP publique utilisée par votre serveur, celle que vous avez configurée dans les serveurs DNS. + + + +### Début de la négociation TLS (Handshake) { #tls-handshake-start } + +Le navigateur communiquerait ensuite avec cette adresse IP sur le **port 443** (le port HTTPS). + +La première partie de la communication consiste simplement à établir la connexion entre le client et le serveur et à décider des clés cryptographiques qu'ils utiliseront, etc. + + + +Cette interaction entre le client et le serveur pour établir la connexion TLS s'appelle la **négociation TLS (TLS handshake)**. + +### TLS avec l'extension SNI { #tls-with-sni-extension } + +**Un seul processus** sur le serveur peut écouter sur un **port** spécifique d'une **adresse IP** spécifique. Il pourrait y avoir d'autres processus écoutant sur d'autres ports de la même adresse IP, mais un seul pour chaque combinaison d'adresse IP et de port. + +TLS (HTTPS) utilise par défaut le port spécifique `443`. C'est donc le port dont nous aurions besoin. + +Comme un seul processus peut écouter sur ce port, le processus qui le ferait serait le **Proxy de terminaison TLS**. + +Le Proxy de terminaison TLS aurait accès à un ou plusieurs **certificats TLS** (certificats HTTPS). + +En utilisant l'**extension SNI** mentionnée plus haut, le Proxy de terminaison TLS vérifierait lequel des certificats TLS (HTTPS) disponibles il devrait utiliser pour cette connexion, en choisissant celui qui correspond au domaine attendu par le client. + +Dans ce cas, il utiliserait le certificat pour `someapp.example.com`. + + + +Le client **fait déjà confiance** à l'entité qui a généré ce certificat TLS (dans ce cas Let's Encrypt, mais nous y reviendrons plus tard), il peut donc **vérifier** que le certificat est valide. + +Ensuite, en utilisant le certificat, le client et le Proxy de terminaison TLS **décident comment chiffrer** le reste de la **communication TCP**. Cela termine la partie **négociation TLS**. + +Après cela, le client et le serveur disposent d'une **connexion TCP chiffrée**, c'est ce que fournit TLS. Ils peuvent alors utiliser cette connexion pour démarrer la **communication HTTP** proprement dite. + +Et c'est ce qu'est **HTTPS** : c'est simplement du **HTTP** à l'intérieur d'une **connexion TLS sécurisée** au lieu d'une connexion TCP pure (non chiffrée). + +/// tip | Astuce + +Remarquez que le cryptage de la communication se produit au **niveau TCP**, pas au niveau HTTP. + +/// + +### Requête HTTPS { #https-request } + +Maintenant que le client et le serveur (spécifiquement le navigateur et le Proxy de terminaison TLS) ont une **connexion TCP chiffrée**, ils peuvent démarrer la **communication HTTP**. + +Ainsi, le client envoie une **requête HTTPS**. Ce n'est qu'une requête HTTP à travers une connexion TLS chiffrée. + + + +### Déchiffrer la requête { #decrypt-the-request } + +Le Proxy de terminaison TLS utiliserait le chiffrement convenu pour **déchiffrer la requête**, et transmettrait la **requête HTTP en clair (déchiffrée)** au processus exécutant l'application (par exemple un processus avec Uvicorn exécutant l'application FastAPI). + + + +### Réponse HTTP { #http-response } + +L'application traiterait la requête et enverrait une **réponse HTTP en clair (non chiffrée)** au Proxy de terminaison TLS. + + + +### Réponse HTTPS { #https-response } + +Le Proxy de terminaison TLS **chiffrerait ensuite la réponse** en utilisant la cryptographie convenue auparavant (qui a commencé avec le certificat pour `someapp.example.com`), et la renverrait au navigateur. + +Ensuite, le navigateur vérifierait que la réponse est valide et chiffrée avec la bonne clé cryptographique, etc. Il **déchiffrerait la réponse** et la traiterait. + + + +Le client (navigateur) saura que la réponse provient du bon serveur parce qu'elle utilise la cryptographie convenue auparavant à l'aide du **certificat HTTPS**. + +### Applications multiples { #multiple-applications } + +Sur le même serveur (ou les mêmes serveurs), il pourrait y avoir **plusieurs applications**, par exemple d'autres programmes d'API ou une base de données. + +Un seul processus peut gérer l'adresse IP et le port spécifiques (le Proxy de terminaison TLS dans notre exemple), mais les autres applications/processus peuvent également s'exécuter sur le ou les serveurs, tant qu'ils n'essaient pas d'utiliser la même **combinaison d'adresse IP publique et de port**. + + + +De cette façon, le Proxy de terminaison TLS pourrait gérer HTTPS et les certificats pour **plusieurs domaines**, pour plusieurs applications, puis transmettre les requêtes à la bonne application dans chaque cas. + +### Renouvellement des certificats { #certificate-renewal } + +À un moment donné dans le futur, chaque certificat **expirerait** (environ 3 mois après son acquisition). + +Ensuite, il y aurait un autre programme (dans certains cas c'est un autre programme, dans d'autres cas cela pourrait être le même Proxy de terminaison TLS) qui communiquerait avec Let's Encrypt et renouvellerait le ou les certificats. + + + +Les **certificats TLS** sont **associés à un nom de domaine**, pas à une adresse IP. + +Ainsi, pour renouveler les certificats, le programme de renouvellement doit **prouver** à l'autorité (Let's Encrypt) qu'il **« possède » et contrôle ce domaine**. + +Pour ce faire, et pour s'adapter aux différents besoins des applications, il existe plusieurs façons de procéder. Parmi les plus courantes : + +* **Modifier certains enregistrements DNS**. + * Pour cela, le programme de renouvellement doit prendre en charge les API du fournisseur DNS ; ainsi, selon le fournisseur DNS que vous utilisez, cela peut être ou non une option. +* **S'exécuter comme un serveur** (au moins pendant le processus d'acquisition du certificat) sur l'adresse IP publique associée au domaine. + * Comme nous l'avons dit plus haut, un seul processus peut écouter sur une adresse IP et un port spécifiques. + * C'est l'une des raisons pour lesquelles il est très utile que le même Proxy de terminaison TLS prenne également en charge le processus de renouvellement des certificats. + * Sinon, vous pourriez avoir à arrêter le Proxy de terminaison TLS momentanément, démarrer le programme de renouvellement pour acquérir les certificats, puis les configurer avec le Proxy de terminaison TLS, et ensuite redémarrer le Proxy de terminaison TLS. Ce n'est pas idéal, car votre/vos application(s) ne seront pas disponibles pendant le temps où le Proxy de terminaison TLS est arrêté. + +Tout ce processus de renouvellement, tout en continuant à servir l'application, est l'une des principales raisons pour lesquelles vous voudriez avoir un **système séparé pour gérer HTTPS** avec un Proxy de terminaison TLS, au lieu d'utiliser directement les certificats TLS avec le serveur d'application (par exemple Uvicorn). + +## En-têtes Proxy Forwarded { #proxy-forwarded-headers } + +Lorsque vous utilisez un proxy pour gérer HTTPS, votre **serveur d'application** (par exemple Uvicorn via FastAPI CLI) ne connaît rien du processus HTTPS, il communique en HTTP en clair avec le **Proxy de terminaison TLS**. + +Ce **proxy** définirait normalement certains en-têtes HTTP à la volée avant de transmettre la requête au **serveur d'application**, pour informer le serveur d'application que la requête est **transmise** par le proxy. + +/// note | Détails techniques + +Les en-têtes du proxy sont : + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +Néanmoins, comme le **serveur d'application** ne sait pas qu'il se trouve derrière un **proxy** de confiance, par défaut, il ne ferait pas confiance à ces en-têtes. + +Mais vous pouvez configurer le **serveur d'application** pour qu'il fasse confiance aux en-têtes transmis (*forwarded*) envoyés par le **proxy**. Si vous utilisez FastAPI CLI, vous pouvez utiliser l'*option CLI* `--forwarded-allow-ips` pour lui indiquer à partir de quelles IP il doit faire confiance à ces en-têtes transmis. + +Par exemple, si le **serveur d'application** ne reçoit des communications que du **proxy** de confiance, vous pouvez définir `--forwarded-allow-ips="*"` pour lui faire faire confiance à toutes les IP entrantes, puisqu'il ne recevra des requêtes que depuis l'IP utilisée par le **proxy**. + +De cette façon, l'application sera en mesure de savoir quelle est sa propre URL publique, si elle utilise HTTPS, le domaine, etc. + +Cela serait utile, par exemple, pour gérer correctement les redirections. + +/// tip | Astuce + +Vous pouvez en savoir plus dans la documentation [Derrière un proxy - Activer les en-têtes transmis par le proxy](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## Récapitulatif { #recap } + +Disposer de **HTTPS** est très important, et assez **critique** dans la plupart des cas. La majeure partie de l'effort que vous, en tant que développeur, devez fournir autour de HTTPS consiste simplement à **comprendre ces concepts** et leur fonctionnement. + +Mais une fois que vous connaissez les informations de base sur **HTTPS pour les développeurs**, vous pouvez facilement combiner et configurer différents outils pour vous aider à tout gérer simplement. + +Dans certains des prochains chapitres, je vous montrerai plusieurs exemples concrets de configuration de **HTTPS** pour des applications **FastAPI**. 🔒 diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md index e2014afe9..3b08e9af7 100644 --- a/docs/fr/docs/deployment/index.md +++ b/docs/fr/docs/deployment/index.md @@ -1,8 +1,8 @@ -# Déploiement +# Déploiement { #deployment } Le déploiement d'une application **FastAPI** est relativement simple. -## Que signifie le déploiement +## Que signifie le déploiement { #what-does-deployment-mean } **Déployer** une application signifie effectuer les étapes nécessaires pour la rendre **disponible pour les utilisateurs**. @@ -14,7 +14,7 @@ l'application efficacement et sans interruption ni problème. Ceci contraste avec les étapes de **développement**, où vous êtes constamment en train de modifier le code, de le casser et de le réparer, d'arrêter et de redémarrer le serveur de développement, _etc._ -## Stratégies de déploiement +## Stratégies de déploiement { #deployment-strategies } Il existe plusieurs façons de procéder, en fonction de votre cas d'utilisation spécifique et des outils que vous utilisez. @@ -22,6 +22,8 @@ utilisez. Vous pouvez **déployer un serveur** vous-même en utilisant une combinaison d'outils, vous pouvez utiliser un **service cloud** qui fait une partie du travail pour vous, ou encore d'autres options possibles. +Par exemple, nous, l'équipe derrière FastAPI, avons créé **FastAPI Cloud**, pour rendre le déploiement d'applications FastAPI dans le cloud aussi fluide que possible, avec la même expérience développeur que lorsque vous travaillez avec FastAPI. + Je vais vous montrer certains des principaux concepts que vous devriez probablement avoir à l'esprit lors du déploiement d'une application **FastAPI** (bien que la plupart de ces concepts s'appliquent à tout autre type d'application web). diff --git a/docs/fr/docs/deployment/manually.md b/docs/fr/docs/deployment/manually.md index 3f99ccd9f..c0c388b02 100644 --- a/docs/fr/docs/deployment/manually.md +++ b/docs/fr/docs/deployment/manually.md @@ -1,33 +1,82 @@ -# Exécuter un serveur manuellement - Uvicorn +# Exécuter un serveur manuellement { #run-a-server-manually } -La principale chose dont vous avez besoin pour exécuter une application **FastAPI** sur une machine serveur distante est un programme serveur ASGI tel que **Uvicorn**. +## Utiliser la commande `fastapi run` { #use-the-fastapi-run-command } -Il existe 3 principales alternatives : +En bref, utilisez `fastapi run` pour servir votre application FastAPI : + +
+ +```console +$ fastapi run main.py + + 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 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) +``` + +
+ +Cela fonctionnerait pour la plupart des cas. 😎 + +Vous pourriez utiliser cette commande par exemple pour démarrer votre application **FastAPI** dans un conteneur, sur un serveur, etc. + +## Serveurs ASGI { #asgi-servers } + +Allons un peu plus en détail. + +FastAPI utilise un standard pour construire des frameworks web Python et des serveurs appelé ASGI. FastAPI est un framework web ASGI. + +La principale chose dont vous avez besoin pour exécuter une application **FastAPI** (ou toute autre application ASGI) sur une machine serveur distante est un programme serveur ASGI comme **Uvicorn**, c'est celui utilisé par défaut par la commande `fastapi`. + +Il existe plusieurs alternatives, notamment : * Uvicorn : un serveur ASGI haute performance. -* Hypercorn : un serveur - ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. -* Daphne : le serveur ASGI - conçu pour Django Channels. +* Hypercorn : un serveur ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. +* Daphne : le serveur ASGI conçu pour Django Channels. +* Granian : un serveur HTTP Rust pour les applications Python. +* NGINX Unit : NGINX Unit est un environnement d'exécution d'applications web léger et polyvalent. -## Machine serveur et programme serveur +## Machine serveur et programme serveur { #server-machine-and-server-program } Il y a un petit détail sur les noms à garder à l'esprit. 💡 -Le mot "**serveur**" est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). +Le mot « serveur » est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). -Gardez cela à l'esprit lorsque vous lisez "serveur" en général, cela pourrait faire référence à l'une de ces deux choses. +Gardez cela à l'esprit lorsque vous lisez « serveur » en général, cela pourrait faire référence à l'une de ces deux choses. -Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant Linux, en règle générale, sur laquelle vous exécutez des programmes. +Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant normalement Linux, sur laquelle vous exécutez des programmes. +## Installer le programme serveur { #install-the-server-program } -## Installer le programme serveur +Lorsque vous installez FastAPI, il est fourni avec un serveur de production, Uvicorn, et vous pouvez le démarrer avec la commande `fastapi run`. -Vous pouvez installer un serveur compatible ASGI avec : +Mais vous pouvez également installer un serveur ASGI manuellement. -//// tab | Uvicorn +Vous devez créer un [environnement virtuel](../virtual-environments.md){.internal-link target=_blank}, l'activer, puis vous pouvez installer l'application serveur. -* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. +Par exemple, pour installer Uvicorn :
@@ -39,39 +88,21 @@ $ pip install "uvicorn[standard]"
+Un processus similaire s'appliquerait à tout autre programme de serveur ASGI. + /// tip | Astuce En ajoutant `standard`, Uvicorn va installer et utiliser quelques dépendances supplémentaires recommandées. -Cela inclut `uvloop`, le remplaçant performant de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. +Cela inclut `uvloop`, le remplaçant hautes performances de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. + +Lorsque vous installez FastAPI avec quelque chose comme `pip install "fastapi[standard]"`, vous obtenez déjà `uvicorn[standard]` aussi. /// -//// +## Exécuter le programme serveur { #run-the-server-program } -//// tab | Hypercorn - -* Hypercorn, un serveur ASGI également compatible avec HTTP/2. - -
- -```console -$ pip install hypercorn - ----> 100% -``` - -
- -...ou tout autre serveur ASGI. - -//// - -## Exécutez le programme serveur - -Vous pouvez ensuite exécuter votre application de la même manière que vous l'avez fait dans les tutoriels, mais sans l'option `--reload`, par exemple : - -//// tab | Uvicorn +Si vous avez installé un serveur ASGI manuellement, vous devrez normalement passer une chaîne d'import dans un format spécial pour qu'il importe votre application FastAPI :
@@ -83,85 +114,44 @@ $ uvicorn main:app --host 0.0.0.0 --port 80
-//// +/// note | Remarque -//// tab | Hypercorn +La commande `uvicorn main:app` fait référence à : -
+* `main` : le fichier `main.py` (le « module » Python). +* `app` : l'objet créé dans `main.py` avec la ligne `app = FastAPI()`. -```console -$ hypercorn main:app --bind 0.0.0.0:80 +C'est équivalent à : -Running on 0.0.0.0:8080 over http (CTRL + C to quit) +```Python +from main import app ``` -
- -//// - -/// warning - -N'oubliez pas de supprimer l'option `--reload` si vous l'utilisiez. - - L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. - - Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. - /// -## Hypercorn avec Trio +Chaque programme de serveur ASGI alternatif aurait une commande similaire, vous pouvez en lire plus dans leur documentation respective. -Starlette et **FastAPI** sont basés sur -AnyIO, qui les rend -compatibles avec asyncio, de la bibliothèque standard Python et -Trio. +/// warning | Alertes -Néanmoins, Uvicorn n'est actuellement compatible qu'avec asyncio, et il utilise normalement `uvloop`, le remplaçant hautes performances de `asyncio`. +Uvicorn et d'autres serveurs prennent en charge une option `--reload` utile pendant le développement. -Mais si vous souhaitez utiliser directement **Trio**, vous pouvez utiliser **Hypercorn** car il le prend en charge. ✨ +L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. -### Installer Hypercorn avec Trio +Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. -Vous devez d'abord installer Hypercorn avec le support Trio : +/// -
+## Concepts de déploiement { #deployment-concepts } -```console -$ pip install "hypercorn[trio]" ----> 100% -``` +Ces exemples exécutent le programme serveur (par exemple Uvicorn), en démarrant **un seul processus**, à l'écoute sur toutes les IP (`0.0.0.0`) sur un port prédéfini (par exemple `80`). -
+C'est l'idée de base. Mais vous voudrez probablement vous occuper de certaines choses supplémentaires, comme : -### Exécuter avec Trio +* Sécurité - HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes précédant le démarrage -Ensuite, vous pouvez passer l'option de ligne de commande `--worker-class` avec la valeur `trio` : - -
- -```console -$ hypercorn main:app --worker-class trio -``` - -
- -Et cela démarrera Hypercorn avec votre application en utilisant Trio comme backend. - -Vous pouvez désormais utiliser Trio en interne dans votre application. Ou mieux encore, vous pouvez utiliser AnyIO pour que votre code reste compatible avec Trio et asyncio. 🎉 - -## Concepts de déploiement - -Ces exemples lancent le programme serveur (e.g. Uvicorn), démarrant **un seul processus**, sur toutes les IPs (`0.0. -0.0`) sur un port prédéfini (par example, `80`). - -C'est l'idée de base. Mais vous vous préoccuperez probablement de certains concepts supplémentaires, tels que ... : - -* la sécurité - HTTPS -* l'exécution au démarrage -* les redémarrages -* la réplication (le nombre de processus en cours d'exécution) -* la mémoire -* les étapes précédant le démarrage - -Je vous en dirai plus sur chacun de ces concepts, sur la façon de les aborder, et donnerai quelques exemples concrets avec des stratégies pour les traiter dans les prochains chapitres. 🚀 +Je vous en dirai plus sur chacun de ces concepts, sur la manière d'y réfléchir, et donnerai quelques exemples concrets avec des stratégies pour les gérer dans les prochains chapitres. 🚀 diff --git a/docs/fr/docs/deployment/versions.md b/docs/fr/docs/deployment/versions.md index 9d84274e2..81794428f 100644 --- a/docs/fr/docs/deployment/versions.md +++ b/docs/fr/docs/deployment/versions.md @@ -1,101 +1,93 @@ -# À propos des versions de FastAPI +# À propos des versions de FastAPI { #about-fastapi-versions } -**FastAPI** est déjà utilisé en production dans de nombreuses applications et systèmes. Et la couverture de test est maintenue à 100 %. Mais son développement est toujours aussi rapide. +**FastAPI** est déjà utilisé en production dans de nombreuses applications et de nombreux systèmes. Et la couverture de tests est maintenue à 100 %. Mais son développement avance toujours rapidement. -De nouvelles fonctionnalités sont ajoutées fréquemment, des bogues sont corrigés régulièrement et le code est -amélioré continuellement. +De nouvelles fonctionnalités sont ajoutées fréquemment, des bogues sont corrigés régulièrement et le code s'améliore continuellement. -C'est pourquoi les versions actuelles sont toujours `0.x.x`, cela reflète que chaque version peut potentiellement -recevoir des changements non rétrocompatibles. Cela suit les conventions de versionnage sémantique. +C'est pourquoi les versions actuelles sont toujours `0.x.x`, cela reflète que chaque version pourrait potentiellement comporter des changements non rétrocompatibles. Cela suit les conventions de versionnage sémantique. Vous pouvez créer des applications de production avec **FastAPI** dès maintenant (et vous le faites probablement depuis un certain temps), vous devez juste vous assurer que vous utilisez une version qui fonctionne correctement avec le reste de votre code. -## Épinglez votre version de `fastapi` +## Épingler votre version de `fastapi` { #pin-your-fastapi-version } -Tout d'abord il faut "épingler" la version de **FastAPI** que vous utilisez à la dernière version dont vous savez -qu'elle fonctionne correctement pour votre application. +La première chose que vous devez faire est « épingler » la version de **FastAPI** que vous utilisez à la dernière version spécifique dont vous savez qu’elle fonctionne correctement pour votre application. -Par exemple, disons que vous utilisez la version `0.45.0` dans votre application. +Par exemple, disons que vous utilisez la version `0.112.0` dans votre application. -Si vous utilisez un fichier `requirements.txt`, vous pouvez spécifier la version avec : +Si vous utilisez un fichier `requirements.txt`, vous pouvez spécifier la version avec : ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -ce qui signifierait que vous utiliseriez exactement la version `0.45.0`. +ce qui signifierait que vous utiliseriez exactement la version `0.112.0`. -Ou vous pourriez aussi l'épingler avec : +Ou vous pourriez aussi l'épingler avec : ```txt -fastapi>=0.45.0,<0.46.0 +fastapi[standard]>=0.112.0,<0.113.0 ``` -cela signifierait que vous utiliseriez les versions `0.45.0` ou supérieures, mais inférieures à `0.46.0`, par exemple, une version `0.45.2` serait toujours acceptée. +cela signifierait que vous utiliseriez les versions `0.112.0` ou supérieures, mais inférieures à `0.113.0`, par exemple, une version `0.112.2` serait toujours acceptée. -Si vous utilisez un autre outil pour gérer vos installations, comme Poetry, Pipenv, ou autres, ils ont tous un moyen que vous pouvez utiliser pour définir des versions spécifiques pour vos paquets. +Si vous utilisez un autre outil pour gérer vos installations, comme `uv`, Poetry, Pipenv, ou autres, ils ont tous un moyen que vous pouvez utiliser pour définir des versions spécifiques pour vos paquets. -## Versions disponibles +## Versions disponibles { #available-versions } Vous pouvez consulter les versions disponibles (par exemple, pour vérifier quelle est la dernière version en date) dans les [Notes de version](../release-notes.md){.internal-link target=_blank}. -## À propos des versions +## À propos des versions { #about-versions } -Suivant les conventions de versionnage sémantique, toute version inférieure à `1.0.0` peut potentiellement ajouter -des changements non rétrocompatibles. +Suivant les conventions de versionnage sémantique, toute version inférieure à `1.0.0` peut potentiellement ajouter des changements non rétrocompatibles. -FastAPI suit également la convention que tout changement de version "PATCH" est pour des corrections de bogues et -des changements rétrocompatibles. +FastAPI suit également la convention selon laquelle tout changement de version « PATCH » concerne des corrections de bogues et des changements rétrocompatibles. /// tip | Astuce -Le "PATCH" est le dernier chiffre, par exemple, dans `0.2.3`, la version PATCH est `3`. +Le « PATCH » est le dernier chiffre, par exemple, dans `0.2.3`, la version PATCH est `3`. /// -Donc, vous devriez être capable d'épingler une version comme suit : +Donc, vous devriez être en mesure d'épingler une version comme suit : ```txt fastapi>=0.45.0,<0.46.0 ``` -Les changements non rétrocompatibles et les nouvelles fonctionnalités sont ajoutés dans les versions "MINOR". +Les changements non rétrocompatibles et les nouvelles fonctionnalités sont ajoutés dans les versions « MINOR ». /// tip | Astuce -Le "MINOR" est le numéro au milieu, par exemple, dans `0.2.3`, la version MINOR est `2`. +Le « MINOR » est le numéro au milieu, par exemple, dans `0.2.3`, la version MINOR est `2`. /// -## Mise à jour des versions FastAPI +## Mettre à niveau les versions de FastAPI { #upgrading-the-fastapi-versions } -Vous devriez tester votre application. +Vous devez ajouter des tests pour votre application. -Avec **FastAPI** c'est très facile (merci à Starlette), consultez la documentation : [Testing](../tutorial/testing.md){.internal-link target=_blank} +Avec **FastAPI** c'est très facile (merci à Starlette), consultez les documents : [Tests](../tutorial/testing.md){.internal-link target=_blank} -Après avoir effectué des tests, vous pouvez mettre à jour la version **FastAPI** vers une version plus récente, et vous assurer que tout votre code fonctionne correctement en exécutant vos tests. +Après avoir des tests, vous pouvez mettre à niveau la version de **FastAPI** vers une version plus récente et vous assurer que tout votre code fonctionne correctement en exécutant vos tests. -Si tout fonctionne, ou après avoir fait les changements nécessaires, et que tous vos tests passent, vous pouvez -épingler votre version de `fastapi` à cette nouvelle version récente. +Si tout fonctionne, ou après avoir effectué les changements nécessaires, et que tous vos tests passent, vous pouvez alors épingler votre `fastapi` à cette nouvelle version récente. -## À propos de Starlette +## À propos de Starlette { #about-starlette } -Vous ne devriez pas épingler la version de `starlette`. +Vous ne devez pas épingler la version de `starlette`. Différentes versions de **FastAPI** utiliseront une version spécifique plus récente de Starlette. Ainsi, vous pouvez simplement laisser **FastAPI** utiliser la bonne version de Starlette. -## À propos de Pydantic +## À propos de Pydantic { #about-pydantic } -Pydantic inclut des tests pour **FastAPI** avec ses propres tests, ainsi les nouvelles versions de Pydantic (au-dessus -de `1.0.0`) sont toujours compatibles avec **FastAPI**. +Pydantic inclut les tests pour **FastAPI** avec ses propres tests, ainsi les nouvelles versions de Pydantic (au-dessus de `1.0.0`) sont toujours compatibles avec FastAPI. -Vous pouvez épingler Pydantic à toute version supérieure à `1.0.0` qui fonctionne pour vous et inférieure à `2.0.0`. +Vous pouvez épingler Pydantic à toute version supérieure à `1.0.0` qui fonctionne pour vous. -Par exemple : +Par exemple : ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 99ea8dda1..2aeaa1c69 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

Framework FastAPI, haute performance, facile à apprendre, rapide à coder, prêt pour la production @@ -27,7 +27,7 @@ --- -**Documentation** : https://fastapi.tiangolo.com +**Documentation** : https://fastapi.tiangolo.com/fr **Code Source** : https://github.com/fastapi/fastapi @@ -37,128 +37,130 @@ FastAPI est un framework web moderne et rapide (haute performance) pour la créa Les principales fonctionnalités sont : -* **Rapidité** : De très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). -* **Rapide à coder** : Augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * -* **Moins de bugs** : Réduit d'environ 40 % les erreurs induites par le développeur. * -* **Intuitif** : Excellente compatibilité avec les IDE. Complétion complète. Moins de temps passé à déboguer. -* **Facile** : Conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire la documentation. -* **Concis** : Diminue la duplication de code. De nombreuses fonctionnalités liées à la déclaration de chaque paramètre. Moins de bugs. -* **Robuste** : Obtenez un code prêt pour la production. Avec une documentation interactive automatique. -* **Basé sur des normes** : Basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. +* **Rapide** : très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). +* **Rapide à coder** : augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * +* **Moins de bugs** : réduit d'environ 40 % les erreurs induites par le développeur. * +* **Intuitif** : excellente compatibilité avec les éditeurs. Autocomplétion partout. Moins de temps passé à déboguer. +* **Facile** : conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire les documents. +* **Concis** : diminue la duplication de code. Plusieurs fonctionnalités à partir de chaque déclaration de paramètre. Moins de bugs. +* **Robuste** : obtenez un code prêt pour la production. Avec une documentation interactive automatique. +* **Basé sur des normes** : basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. * estimation basée sur des tests d'une équipe de développement interne, construisant des applications de production. -## Sponsors +## Sponsors { #sponsors } -{% if sponsors %} +### Sponsor clé de voûte { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Sponsors Or et Argent { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Other sponsors +Autres sponsors -## Opinions +## Opinions { #opinions } -"_[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux seront intégrés dans le coeur de **Windows** et dans certains produits **Office**._" +« _[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux sont intégrés au cœur de **Windows** et à certains produits **Office**._ »

Kabir Khan - Microsoft (ref)
--- -"_Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_" +« _Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_ » -
Piero Molino, Yaroslav Dudin et Sai Sumanth Miryala - Uber (ref)
+
Piero Molino, Yaroslav Dudin, et Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** a le plaisir d'annoncer la sortie en open-source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_" +« _**Netflix** est heureux d'annoncer la publication en open source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_ »
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_Je suis très enthousiaste à propos de **FastAPI**. C'est un bonheur !_" +« _Je suis plus qu'enthousiaste à propos de **FastAPI**. C'est tellement fun !_ » -
Brian Okken - Auteur du podcast Python Bytes (ref)
+
Brian Okken - Animateur du podcast Python Bytes (ref)
--- -"_Honnêtement, ce que vous avez construit a l'air super solide et élégant. A bien des égards, c'est comme ça que je voulais que **Hug** soit - c'est vraiment inspirant de voir quelqu'un construire ça._" +« _Honnêtement, ce que vous avez construit a l'air super solide et soigné. À bien des égards, c'est ce que je voulais que **Hug** soit — c'est vraiment inspirant de voir quelqu'un construire ça._ » -
Timothy Crosley - Créateur de Hug (ref)
+
Timothy Crosley - Créateur de Hug (ref)
--- -"_Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et à apprendre [...]_" +« _Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et facile à apprendre [...]_ » -"_Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_" +« _Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_ »
Ines Montani - Matthew Honnibal - Fondateurs de Explosion AI - Créateurs de spaCy (ref) - (ref)
--- -"_Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **bien conçu**, **simple à utiliser** et **très évolutif**. Il est devenu un **composant clé** dans notre stratégie de développement API first et il est à l'origine de nombreux automatismes et services tels que notre ingénieur virtuel TAC._" +« _Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **magnifiquement conçu**, **simple à utiliser** et **hautement scalable**. Il est devenu un **composant clé** de notre stratégie de développement API-first et alimente de nombreuses automatisations et services tels que notre ingénieur TAC virtuel._ »
Deon Pillsbury - Cisco (ref)
--- -## **Typer**, le FastAPI des CLI +## Mini documentaire FastAPI { #fastapi-mini-documentary } + +Un mini documentaire FastAPI est sorti fin 2025, vous pouvez le regarder en ligne : + +FastAPI Mini Documentary + +## **Typer**, le FastAPI des CLIs { #typer-the-fastapi-of-clis } -Si vous souhaitez construire une application CLI utilisable dans un terminal au lieu d'une API web, regardez **Typer**. +Si vous construisez une application CLI à utiliser dans un terminal au lieu d'une API web, regardez **Typer**. -**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLI**. ⌨️ 🚀 +**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLIs**. ⌨️ 🚀 -## Prérequis +## Prérequis { #requirements } FastAPI repose sur les épaules de géants : * Starlette pour les parties web. * Pydantic pour les parties données. -## Installation +## Installation { #installation } + +Créez et activez un environnement virtuel puis installez FastAPI :
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn. +**Remarque** : Vous devez vous assurer de mettre « fastapi[standard] » entre guillemets pour garantir que cela fonctionne dans tous les terminaux. -
+## Exemple { #example } -```console -$ pip install "uvicorn[standard]" +### Créer { #create-it } ----> 100% -``` - -
- -## Exemple - -### Créez - -* Créez un fichier `main.py` avec : +Créez un fichier `main.py` avec : ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -170,18 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-Ou utilisez async def ... +Ou utilisez async def... Si votre code utilise `async` / `await`, utilisez `async def` : -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -193,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -**Note** +**Remarque** : -Si vous n'êtes pas familier avec cette notion, consultez la section _"Vous êtes pressés ?"_ à propos de `async` et `await` dans la documentation. +Si vous ne savez pas, consultez la section « Vous êtes pressés ? » à propos de `async` et `await` dans la documentation.
-### Lancez +### Lancer { #run-it } Lancez le serveur avec :
```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. ``` @@ -222,34 +235,34 @@ INFO: Application startup complete.
-À propos de la commande uvicorn main:app --reload ... +À propos de la commande fastapi dev main.py... -La commande `uvicorn main:app` fait référence à : +La commande `fastapi dev` lit votre fichier `main.py`, détecte l'application **FastAPI** qu'il contient et lance un serveur avec Uvicorn. -* `main` : le fichier `main.py` (le "module" Python). -* `app` : l'objet créé à l'intérieur de `main.py` avec la ligne `app = FastAPI()`. -* `--reload` : fait redémarrer le serveur après des changements de code. À n'utiliser que pour le développement. +Par défaut, `fastapi dev` démarre avec le rechargement automatique activé pour le développement local. + +Vous pouvez en savoir plus dans la documentation de la CLI FastAPI.
-### Vérifiez +### Vérifier { #check-it } Ouvrez votre navigateur à l'adresse http://127.0.0.1:8000/items/5?q=somequery. -Vous obtenez alors cette réponse JSON : +Vous verrez la réponse JSON : ```JSON {"item_id": 5, "q": "somequery"} ``` -Vous venez de créer une API qui : +Vous avez déjà créé une API qui : -* Reçoit les requêtes HTTP pour les _chemins_ `/` et `/items/{item_id}`. -* Les deux _chemins_ acceptent des opérations `GET` (également connu sous le nom de _méthodes_ HTTP). -* Le _chemin_ `/items/{item_id}` a un _paramètre_ `item_id` qui doit être un `int`. -* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. +* Reçoit des requêtes HTTP sur les _chemins_ `/` et `/items/{item_id}`. +* Les deux _chemins_ acceptent des opérations `GET` (également connues sous le nom de _méthodes_ HTTP). +* Le _chemin_ `/items/{item_id}` a un _paramètre de chemin_ `item_id` qui doit être un `int`. +* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. -### Documentation API interactive +### Documentation API interactive { #interactive-api-docs } Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. @@ -257,23 +270,21 @@ Vous verrez la documentation interactive automatique de l'API (fournie par http://127.0.0.1:8000/redoc. -Vous verrez la documentation interactive automatique de l'API (fournie par ReDoc) : +Vous verrez la documentation alternative automatique (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Exemple plus poussé +## Mettre à niveau l'exemple { #example-upgrade } -Maintenant, modifiez le fichier `main.py` pour recevoir le corps d'une requête `PUT`. +Modifiez maintenant le fichier `main.py` pour recevoir un corps depuis une requête `PUT`. -Déclarez ce corps en utilisant les types Python standards, grâce à Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union +Déclarez le corps en utilisant les types Python standard, grâce à Pydantic. +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -283,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -292,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -301,35 +312,35 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Le serveur se recharge normalement automatiquement (car vous avez pensé à `--reload` dans la commande `uvicorn` ci-dessus). +Le serveur `fastapi dev` devrait se recharger automatiquement. -### Plus loin avec la documentation API interactive +### Mettre à niveau la documentation API interactive { #interactive-api-docs-upgrade } Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps de la requête : +* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Cliquez sur le bouton "Try it out", il vous permet de renseigner les paramètres et d'interagir directement avec l'API : +* Cliquez sur le bouton « Try it out », il vous permet de renseigner les paramètres et d'interagir directement avec l'API : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Cliquez ensuite sur le bouton "Execute", l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : +* Cliquez ensuite sur le bouton « Execute », l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Plus loin avec la documentation API alternative +### Mettre à niveau la documentation API alternative { #alternative-api-docs-upgrade } Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -* La documentation alternative reflétera également le nouveau paramètre de requête et le nouveau corps : +* La documentation alternative reflètera également le nouveau paramètre de requête et le nouveau corps : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### En résumé +### En résumé { #recap } -En résumé, vous déclarez **une fois** les types de paramètres, le corps de la requête, etc. en tant que paramètres de fonction. +En résumé, vous déclarez **une fois** les types de paramètres, le corps, etc. en tant que paramètres de fonction. Vous faites cela avec les types Python standard modernes. @@ -337,7 +348,7 @@ Vous n'avez pas à apprendre une nouvelle syntaxe, les méthodes ou les classes Juste du **Python** standard. -Par exemple, pour un `int`: +Par exemple, pour un `int` : ```Python item_id: int @@ -351,54 +362,54 @@ item: Item ... et avec cette déclaration unique, vous obtenez : -* Une assistance dans votre IDE, notamment : - * la complétion. +* Une assistance dans l'éditeur, notamment : + * l'autocomplétion. * la vérification des types. * La validation des données : * des erreurs automatiques et claires lorsque les données ne sont pas valides. - * une validation même pour les objets JSON profondément imbriqués. -* Une conversion des données d'entrée : venant du réseau et allant vers les données et types de Python, permettant de lire : - * le JSON. - * les paramètres du chemin. - * les paramètres de la requête. - * les cookies. - * les en-têtes. - * les formulaires. - * les fichiers. -* La conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON), permettant de convertir : - * les types Python (`str`, `int`, `float`, `bool`, `list`, etc). - * les objets `datetime`. - * les objets `UUID`. - * les modèles de base de données. - * ... et beaucoup plus. -* La documentation API interactive automatique, avec 2 interfaces utilisateur au choix : + * une validation même pour les objets JSON profondément imbriqués. +* Conversion des données d'entrée : venant du réseau vers les données et types Python. Lecture depuis : + * JSON. + * Paramètres de chemin. + * Paramètres de requête. + * Cookies. + * En-têtes. + * Formulaires. + * Fichiers. +* Conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON) : + * Conversion des types Python (`str`, `int`, `float`, `bool`, `list`, etc). + * Objets `datetime`. + * Objets `UUID`. + * Modèles de base de données. + * ... et bien plus. +* Documentation API interactive automatique, avec 2 interfaces utilisateur au choix : * Swagger UI. * ReDoc. --- -Pour revenir à l'exemple de code précédent, **FastAPI** permet de : +Pour revenir à l'exemple de code précédent, **FastAPI** va : -* Valider que `item_id` existe dans le chemin des requêtes `GET` et `PUT`. +* Valider la présence d'un `item_id` dans le chemin pour les requêtes `GET` et `PUT`. * Valider que `item_id` est de type `int` pour les requêtes `GET` et `PUT`. - * Si ce n'est pas le cas, le client voit une erreur utile et claire. -* Vérifier qu'il existe un paramètre de requête facultatif nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. - * Puisque le paramètre `q` est déclaré avec `= None`, il est facultatif. - * Sans le `None`, il serait nécessaire (comme l'est le corps de la requête dans le cas du `PUT`). -* Pour les requêtes `PUT` vers `/items/{item_id}`, de lire le corps en JSON : - * Vérifier qu'il a un attribut obligatoire `name` qui devrait être un `str`. - * Vérifier qu'il a un attribut obligatoire `prix` qui doit être un `float`. - * Vérifier qu'il a un attribut facultatif `is_offer`, qui devrait être un `bool`, s'il est présent. - * Tout cela fonctionnerait également pour les objets JSON profondément imbriqués. -* Convertir de et vers JSON automatiquement. -* Documenter tout avec OpenAPI, qui peut être utilisé par : - * Les systèmes de documentation interactifs. - * Les systèmes de génération automatique de code client, pour de nombreuses langues. + * Si ce n'est pas le cas, le client verra une erreur utile et claire. +* Vérifier s'il existe un paramètre de requête optionnel nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. + * Comme le paramètre `q` est déclaré avec `= None`, il est optionnel. + * Sans le `None`, il serait requis (comme l'est le corps dans le cas de `PUT`). +* Pour les requêtes `PUT` vers `/items/{item_id}`, lire le corps au format JSON : + * Vérifier qu'il a un attribut obligatoire `name` qui doit être un `str`. + * Vérifier qu'il a un attribut obligatoire `price` qui doit être un `float`. + * Vérifier qu'il a un attribut optionnel `is_offer`, qui doit être un `bool`, s'il est présent. + * Tout cela fonctionne également pour les objets JSON profondément imbriqués. +* Convertir automatiquement depuis et vers JSON. +* Tout documenter avec OpenAPI, qui peut être utilisé par : + * des systèmes de documentation interactive. + * des systèmes de génération automatique de clients, pour de nombreux langages. * Fournir directement 2 interfaces web de documentation interactive. --- -Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout cela fonctionne. +Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout fonctionne. Essayez de changer la ligne contenant : @@ -412,61 +423,137 @@ Essayez de changer la ligne contenant : ... "item_name": item.name ... ``` -... vers : +... à : ```Python ... "item_price": item.price ... ``` -... et voyez comment votre éditeur complétera automatiquement les attributs et connaîtra leurs types : +... et voyez comment votre éditeur complète automatiquement les attributs et connaît leurs types : -![compatibilité IDE](https://fastapi.tiangolo.com/img/vscode-completion.png) +![compatibilité éditeur](https://fastapi.tiangolo.com/img/vscode-completion.png) Pour un exemple plus complet comprenant plus de fonctionnalités, voir le Tutoriel - Guide utilisateur. -**Spoiler alert** : le tutoriel - guide utilisateur inclut : +**Alerte spoiler** : le tutoriel - guide utilisateur inclut : -* Déclaration de **paramètres** provenant d'autres endroits différents comme : **en-têtes.**, **cookies**, **champs de formulaire** et **fichiers**. -* L'utilisation de **contraintes de validation** comme `maximum_length` ou `regex`. -* Un **système d'injection de dépendance ** très puissant et facile à utiliser . -* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec les **jetons JWT** et l'authentification **HTTP Basic**. -* Des techniques plus avancées (mais tout aussi faciles) pour déclarer les **modèles JSON profondément imbriqués** (grâce à Pydantic). -* Intégration de **GraphQL** avec Strawberry et d'autres bibliothèques. -* D'obtenir de nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : +* Déclaration de **paramètres** provenant d'autres emplacements comme : **en-têtes**, **cookies**, **champs de formulaire** et **fichiers**. +* Comment définir des **contraintes de validation** comme `maximum_length` ou `regex`. +* Un système **d'injection de dépendances** très puissant et facile à utiliser. +* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec des **JWT tokens** et l'authentification **HTTP Basic**. +* Des techniques plus avancées (mais tout aussi faciles) pour déclarer des **modèles JSON profondément imbriqués** (grâce à Pydantic). +* Intégration **GraphQL** avec Strawberry et d'autres bibliothèques. +* De nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : * **WebSockets** - * de tester le code très facilement avec `requests` et `pytest` - * **CORS** + * des tests extrêmement faciles basés sur HTTPX et `pytest` + * **CORS** * **Cookie Sessions** * ... et plus encore. -## Performance +### Déployer votre application (optionnel) { #deploy-your-app-optional } -Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks existants en Python les plus rapides , juste derrière Starlette et Uvicorn (utilisés en interne par FastAPI). (*) +Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur FastAPI Cloud, allez vous inscrire sur la liste d'attente si ce n'est pas déjà fait. 🚀 + +Si vous avez déjà un compte **FastAPI Cloud** (nous vous avons invité depuis la liste d'attente 😉), vous pouvez déployer votre application avec une seule commande. + +Avant de déployer, assurez-vous d'être connecté : + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Puis déployez votre application : + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +C'est tout ! Vous pouvez maintenant accéder à votre application à cette URL. ✨ + +#### À propos de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** est construit par le même auteur et la même équipe derrière **FastAPI**. + +Il simplifie le processus de **construction**, de **déploiement** et **d'accès** à une API avec un effort minimal. + +Il apporte la même **expérience développeur** de la création d'applications avec FastAPI au **déploiement** dans le cloud. 🎉 + +FastAPI Cloud est le principal sponsor et financeur des projets open source *FastAPI and friends*. ✨ + +#### Déployer sur d'autres fournisseurs cloud { #deploy-to-other-cloud-providers } + +FastAPI est open source et basé sur des standards. Vous pouvez déployer des applications FastAPI sur n'importe quel fournisseur cloud de votre choix. + +Suivez les guides de votre fournisseur cloud pour y déployer des applications FastAPI. 🤓 + +## Performance { #performance } + +Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks Python les plus rapides, juste derrière Starlette et Uvicorn eux-mêmes (utilisés en interne par FastAPI). (*) Pour en savoir plus, consultez la section Benchmarks. -## Dépendances facultatives +## Dépendances { #dependencies } -Utilisées par Pydantic: +FastAPI dépend de Pydantic et Starlette. -* email-validator - pour la validation des adresses email. +### Dépendances `standard` { #standard-dependencies } + +Lorsque vous installez FastAPI avec `pip install "fastapi[standard]"`, il inclut le groupe `standard` de dépendances optionnelles : + +Utilisées par Pydantic : + +* email-validator - pour la validation des adresses e-mail. Utilisées par Starlette : -* requests - Obligatoire si vous souhaitez utiliser `TestClient`. +* httpx - Obligatoire si vous souhaitez utiliser le `TestClient`. * jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par défaut. -* python-multipart - Obligatoire si vous souhaitez supporter le "décodage" de formulaire avec `request.form()`. -* itsdangerous - Obligatoire pour la prise en charge de `SessionMiddleware`. -* pyyaml - Obligatoire pour le support `SchemaGenerator` de Starlette (vous n'en avez probablement pas besoin avec FastAPI). +* python-multipart - Obligatoire si vous souhaitez prendre en charge l’« parsing » de formulaires avec `request.form()`. -Utilisées par FastAPI / Starlette : +Utilisées par FastAPI : -* uvicorn - Pour le serveur qui charge et sert votre application. -* orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. +* uvicorn - pour le serveur qui charge et sert votre application. Cela inclut `uvicorn[standard]`, qui comprend certaines dépendances (par ex. `uvloop`) nécessaires pour une haute performance. +* `fastapi-cli[standard]` - pour fournir la commande `fastapi`. + * Cela inclut `fastapi-cloud-cli`, qui vous permet de déployer votre application FastAPI sur FastAPI Cloud. + +### Sans les dépendances `standard` { #without-standard-dependencies } + +Si vous ne souhaitez pas inclure les dépendances optionnelles `standard`, vous pouvez installer avec `pip install fastapi` au lieu de `pip install "fastapi[standard]"`. + +### Sans `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Si vous souhaitez installer FastAPI avec les dépendances standard mais sans `fastapi-cloud-cli`, vous pouvez installer avec `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Dépendances optionnelles supplémentaires { #additional-optional-dependencies } + +Il existe des dépendances supplémentaires que vous pourriez vouloir installer. + +Dépendances optionnelles supplémentaires pour Pydantic : + +* pydantic-settings - pour la gestion des paramètres. +* pydantic-extra-types - pour des types supplémentaires à utiliser avec Pydantic. + +Dépendances optionnelles supplémentaires pour FastAPI : + +* orjson - Obligatoire si vous souhaitez utiliser `ORJSONResponse`. * ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. -Vous pouvez tout installer avec `pip install fastapi[all]`. - -## Licence +## Licence { #license } Ce projet est soumis aux termes de la licence MIT. diff --git a/docs/fr/docs/learn/index.md b/docs/fr/docs/learn/index.md index 46fc095dc..552687703 100644 --- a/docs/fr/docs/learn/index.md +++ b/docs/fr/docs/learn/index.md @@ -1,5 +1,5 @@ -# Apprendre +# Apprendre { #learn } Voici les sections introductives et les tutoriels pour apprendre **FastAPI**. -Vous pouvez considérer ceci comme un **manuel**, un **cours**, la **méthode officielle** et recommandée pour appréhender FastAPI. 😎 +Vous pouvez considérer ceci comme un **livre**, un **cours**, la **méthode officielle** et recommandée pour apprendre FastAPI. 😎 diff --git a/docs/fr/docs/project-generation.md b/docs/fr/docs/project-generation.md index 4c04dc167..f062ffecf 100644 --- a/docs/fr/docs/project-generation.md +++ b/docs/fr/docs/project-generation.md @@ -1,84 +1,28 @@ -# Génération de projets - Modèle +# Modèle Full Stack FastAPI { #full-stack-fastapi-template } -Vous pouvez utiliser un générateur de projet pour commencer, qui réalisera pour vous la mise en place de bases côté architecture globale, sécurité, base de données et premières routes d'API. +Les modèles, bien qu'ils soient généralement livrés avec une configuration spécifique, sont conçus pour être flexibles et personnalisables. Cela vous permet de les modifier et de les adapter aux exigences de votre projet, ce qui en fait un excellent point de départ. 🏁 -Un générateur de projet fera toujours une mise en place très subjective que vous devriez modifier et adapter suivant vos besoins, mais cela reste un bon point de départ pour vos projets. +Vous pouvez utiliser ce modèle pour démarrer, car il inclut une grande partie de la configuration initiale, la sécurité, la base de données et quelques endpoints d'API déjà prêts pour vous. -## Full Stack FastAPI PostgreSQL +Dépôt GitHub : Modèle Full Stack FastAPI -GitHub : https://github.com/tiangolo/full-stack-fastapi-postgresql +## Modèle Full Stack FastAPI - Pile technologique et fonctionnalités { #full-stack-fastapi-template-technology-stack-and-features } -### Full Stack FastAPI PostgreSQL - Fonctionnalités - -* Intégration **Docker** complète (basée sur Docker). -* Déploiement Docker en mode Swarm -* Intégration **Docker Compose** et optimisation pour développement local. -* Serveur web Python **prêt au déploiement** utilisant Uvicorn et Gunicorn. -* Backend Python **FastAPI** : - * **Rapide** : Très hautes performances, comparables à **NodeJS** ou **Go** (grâce à Starlette et Pydantic). - * **Intuitif** : Excellent support des éditeurs. Complétion partout. Moins de temps passé à déboguer. - * **Facile** : Fait pour être facile à utiliser et apprendre. Moins de temps passé à lire de la documentation. - * **Concis** : Minimise la duplication de code. Plusieurs fonctionnalités à chaque déclaration de paramètre. - * **Robuste** : Obtenez du code prêt pour être utilisé en production. Avec de la documentation automatique interactive. - * **Basé sur des normes** : Basé sur (et totalement compatible avec) les normes ouvertes pour les APIs : OpenAPI et JSON Schema. - * **Et bien d'autres fonctionnalités** comme la validation automatique, la sérialisation, l'authentification avec OAuth2 JWT tokens, etc. -* Hashage de **mots de passe sécurisé** par défaut. -* Authentification par **jetons JWT**. -* Modèles **SQLAlchemy** (indépendants des extensions Flask, afin qu'ils puissent être utilisés directement avec des *workers* Celery). -* Modèle de démarrages basiques pour les utilisateurs (à modifier et supprimer au besoin). -* Migrations **Alembic**. -* **CORS** (partage des ressources entre origines multiples, ou *Cross Origin Resource Sharing*). -* *Worker* **Celery** pouvant importer et utiliser les modèles et le code du reste du backend. -* Tests du backend REST basés sur **Pytest**, intégrés dans Docker, pour que vous puissiez tester toutes les interactions de l'API indépendamment de la base de données. Étant exécutés dans Docker, les tests peuvent utiliser un nouvel entrepôt de données créé de zéro à chaque fois (vous pouvez donc utiliser ElasticSearch, MongoDB, CouchDB, etc. et juste tester que l'API fonctionne). -* Intégration Python facile avec **Jupyter Kernels** pour le développement à distance ou intra-Docker avec des extensions comme Atom Hydrogen ou Visual Studio Code Jupyter. -* Frontend **Vue** : - * Généré avec Vue CLI. - * Gestion de l'**Authentification JWT**. - * Page de connexion. - * Après la connexion, page de tableau de bord principal. - * Tableau de bord principal avec création et modification d'utilisateurs. - * Modification de ses propres caractéristiques utilisateur. - * **Vuex**. - * **Vue-router**. - * **Vuetify** pour de magnifiques composants *material design*. - * **TypeScript**. - * Serveur Docker basé sur **Nginx** (configuré pour être facilement manipulé avec Vue-router). - * Utilisation de *Docker multi-stage building*, pour ne pas avoir besoin de sauvegarder ou *commit* du code compilé. - * Tests frontend exécutés à la compilation (pouvant être désactivés). - * Fait aussi modulable que possible, pour pouvoir fonctionner comme tel, tout en pouvant être utilisé qu'en partie grâce à Vue CLI. -* **PGAdmin** pour les bases de données PostgreSQL, facilement modifiable pour utiliser PHPMYAdmin ou MySQL. -* **Flower** pour la surveillance de tâches Celery. -* Équilibrage de charge entre le frontend et le backend avec **Traefik**, afin de pouvoir avoir les deux sur le même domaine, séparés par chemins, mais servis par différents conteneurs. -* Intégration Traefik, comprenant la génération automatique de certificat **HTTPS** Let's Encrypt. -* GitLab **CI** (intégration continue), comprenant des tests pour le frontend et le backend. - -## Full Stack FastAPI Couchbase - -GitHub : https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **ATTENTION** ⚠️ - -Si vous démarrez un nouveau projet de zéro, allez voir les alternatives au début de cette page. - -Par exemple, le générateur de projet Full Stack FastAPI PostgreSQL peut être une meilleure alternative, étant activement maintenu et utilisé et comprenant toutes les nouvelles fonctionnalités et améliorations. - -Vous êtes toujours libre d'utiliser le générateur basé sur Couchbase si vous le voulez, cela devrait probablement fonctionner correctement, et si vous avez déjà un projet généré en utilisant ce dernier, cela devrait fonctionner aussi (et vous l'avez déjà probablement mis à jour suivant vos besoins). - -Vous pouvez en apprendre plus dans la documentation du dépôt GithHub. - -## Full Stack FastAPI MongoDB - -...viendra surement plus tard, suivant le temps que j'ai. 😅 🎉 - -## Modèles d'apprentissage automatique avec spaCy et FastAPI - -GitHub : https://github.com/microsoft/cookiecutter-spacy-fastapi - -## Modèles d'apprentissage automatique avec spaCy et FastAPI - Fonctionnalités - -* Intégration d'un modèle NER **spaCy**. -* Formatage de requête pour **Azure Cognitive Search**. -* Serveur Python web **prêt à utiliser en production** utilisant Uvicorn et Gunicorn. -* Déploiement CI/CD Kubernetes pour **Azure DevOps** (AKS). -* **Multilangues**. Choisissez facilement l'une des langues intégrées à spaCy durant la mise en place du projet. -* **Facilement généralisable** à d'autres bibliothèques similaires (Pytorch, Tensorflow), et non juste spaCy. +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/fr) pour l'API backend Python. + - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) pour les interactions avec la base de données SQL en Python (ORM). + - 🔍 [Pydantic](https://docs.pydantic.dev), utilisé par FastAPI, pour la validation des données et la gestion des paramètres. + - 💾 [PostgreSQL](https://www.postgresql.org) comme base de données SQL. +- 🚀 [React](https://react.dev) pour le frontend. + - 💃 Utilisation de TypeScript, des hooks, de Vite et d'autres éléments d'un stack frontend moderne. + - 🎨 [Tailwind CSS](https://tailwindcss.com) et [shadcn/ui](https://ui.shadcn.com) pour les composants frontend. + - 🤖 Un client frontend généré automatiquement. + - 🧪 [Playwright](https://playwright.dev) pour les tests de bout en bout. + - 🦇 Prise en charge du mode sombre. +- 🐋 [Docker Compose](https://www.docker.com) pour le développement et la production. +- 🔒 Hachage sécurisé des mots de passe par défaut. +- 🔑 Authentification JWT (JSON Web Token). +- 📫 Récupération de mot de passe par e-mail. +- ✅ Tests avec [Pytest](https://pytest.org). +- 📞 [Traefik](https://traefik.io) comme proxy inverse / répartiteur de charge. +- 🚢 Instructions de déploiement avec Docker Compose, y compris la configuration d'un proxy Traefik frontal pour gérer les certificats HTTPS automatiques. +- 🏭 CI (intégration continue) et CD (déploiement continu) basés sur GitHub Actions. diff --git a/docs/fr/docs/python-types.md b/docs/fr/docs/python-types.md index 99ca90827..f393b0f5c 100644 --- a/docs/fr/docs/python-types.md +++ b/docs/fr/docs/python-types.md @@ -1,70 +1,68 @@ -# Introduction aux Types Python +# Introduction aux types Python { #python-types-intro } -Python supporte des annotations de type (ou *type hints*) optionnelles. +Python prend en charge des « type hints » (aussi appelées « annotations de type ») facultatives. -Ces annotations de type constituent une syntaxe spéciale qui permet de déclarer le type d'une variable. +Ces « type hints » ou annotations sont une syntaxe spéciale qui permet de déclarer le type d'une variable. -En déclarant les types de vos variables, cela permet aux différents outils comme les éditeurs de texte d'offrir un meilleur support. +En déclarant les types de vos variables, les éditeurs et outils peuvent vous offrir un meilleur support. -Ce chapitre n'est qu'un **tutoriel rapide / rappel** sur les annotations de type Python. -Seulement le minimum nécessaire pour les utiliser avec **FastAPI** sera couvert... ce qui est en réalité très peu. +Ceci est un **tutoriel rapide / rappel** à propos des annotations de type Python. Il couvre uniquement le minimum nécessaire pour les utiliser avec **FastAPI** ... ce qui est en réalité très peu. -**FastAPI** est totalement basé sur ces annotations de type, qui lui donnent de nombreux avantages. +**FastAPI** est totalement basé sur ces annotations de type, elles lui donnent de nombreux avantages et bénéfices. -Mais même si vous n'utilisez pas ou n'utiliserez jamais **FastAPI**, vous pourriez bénéficier d'apprendre quelques choses sur ces dernières. +Mais même si vous n'utilisez jamais **FastAPI**, vous auriez intérêt à en apprendre un peu à leur sujet. -/// note +/// note | Remarque -Si vous êtes un expert Python, et que vous savez déjà **tout** sur les annotations de type, passez au chapitre suivant. +Si vous êtes un expert Python, et que vous savez déjà tout sur les annotations de type, passez au chapitre suivant. /// -## Motivations +## Motivation { #motivation } -Prenons un exemple simple : +Commençons par un exemple simple : -{*../../docs_src/python_types/tutorial001.py*} +{* ../../docs_src/python_types/tutorial001_py39.py *} -Exécuter ce programe affiche : +Exécuter ce programme affiche : ``` John Doe ``` -La fonction : +La fonction fait ce qui suit : * Prend un `first_name` et un `last_name`. -* Convertit la première lettre de chaque paramètre en majuscules grâce à `title()`. -* Concatène les résultats avec un espace entre les deux. +* Convertit la première lettre de chacun en majuscule avec `title()`. +* Concatène ces deux valeurs avec un espace au milieu. -{*../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} -### Limitations +### Modifier le code { #edit-it } C'est un programme très simple. Mais maintenant imaginez que vous l'écriviez de zéro. -À un certain point vous auriez commencé la définition de la fonction, vous aviez les paramètres prêts. +À un certain moment, vous auriez commencé la définition de la fonction, vous aviez les paramètres prêts ... -Mais vous aviez besoin de "cette méthode qui convertit la première lettre en majuscule". +Mais ensuite vous devez appeler « cette méthode qui convertit la première lettre en majuscule ». -Était-ce `upper` ? `uppercase` ? `first_uppercase` ? `capitalize` ? +Était-ce `upper` ? Était-ce `uppercase` ? `first_uppercase` ? `capitalize` ? -Vous essayez donc d'utiliser le vieil ami du programmeur, l'auto-complétion de l'éditeur. +Vous essayez alors avec l'ami de toujours des programmeurs, l'autocomplétion de l'éditeur. -Vous écrivez le premier paramètre, `first_name`, puis un point (`.`) et appuyez sur `Ctrl+Espace` pour déclencher l'auto-complétion. +Vous tapez le premier paramètre de la fonction, `first_name`, puis un point (`.`) et appuyez sur `Ctrl+Espace` pour déclencher l'autocomplétion. -Mais malheureusement, rien d'utile n'en résulte : +Mais, malheureusement, vous n'obtenez rien d'utile : -### Ajouter des types +### Ajouter des types { #add-types } Modifions une seule ligne de la version précédente. -Nous allons changer seulement cet extrait, les paramètres de la fonction, de : - +Nous allons changer exactement ce fragment, les paramètres de la fonction, de : ```Python first_name, last_name @@ -78,222 +76,389 @@ Nous allons changer seulement cet extrait, les paramètres de la fonction, de : C'est tout. -Ce sont des annotations de types : +Ce sont les « annotations de type » : -{*../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} -À ne pas confondre avec la déclaration de valeurs par défaut comme ici : +Ce n'est pas la même chose que de déclarer des valeurs par défaut, ce qui serait : ```Python first_name="john", last_name="doe" ``` -C'est une chose différente. +C'est différent. -On utilise un deux-points (`:`), et pas un égal (`=`). +Nous utilisons des deux-points (`:`), pas des signes égal (`=`). -Et ajouter des annotations de types ne crée normalement pas de différence avec le comportement qui aurait eu lieu si elles n'étaient pas là. +Et ajouter des annotations de type ne change normalement pas ce qui se passe par rapport à ce qui se passerait sans elles. -Maintenant, imaginez que vous êtes en train de créer cette fonction, mais avec des annotations de type cette fois. +Mais maintenant, imaginez que vous êtes à nouveau en train de créer cette fonction, mais avec des annotations de type. -Au même moment que durant l'exemple précédent, vous essayez de déclencher l'auto-complétion et vous voyez : +Au même moment, vous essayez de déclencher l'autocomplétion avec `Ctrl+Espace` et vous voyez : -Vous pouvez donc dérouler les options jusqu'à trouver la méthode à laquelle vous pensiez. +Avec cela, vous pouvez faire défiler en voyant les options, jusqu'à trouver celle qui « vous dit quelque chose » : -## Plus de motivations +## Plus de motivation { #more-motivation } -Cette fonction possède déjà des annotations de type : +Regardez cette fonction, elle a déjà des annotations de type : -{*../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} -Comme l'éditeur connaît le type des variables, vous n'avez pas seulement l'auto-complétion, mais aussi de la détection d'erreurs : +Comme l'éditeur connaît les types des variables, vous n'obtenez pas seulement l'autocomplétion, vous obtenez aussi des vérifications d'erreurs : -Maintenant que vous avez connaissance du problème, convertissez `age` en chaîne de caractères grâce à `str(age)` : +Vous savez maintenant qu'il faut corriger, convertir `age` en chaîne avec `str(age)` : -{*../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} -## Déclarer des types +## Déclarer des types { #declaring-types } -Vous venez de voir là où les types sont généralement déclarés : dans les paramètres de fonctions. +Vous venez de voir l'endroit principal pour déclarer des annotations de type : dans les paramètres des fonctions. -C'est aussi ici que vous les utiliseriez avec **FastAPI**. +C'est aussi l'endroit principal où vous les utiliserez avec **FastAPI**. -### Types simples +### Types simples { #simple-types } -Vous pouvez déclarer tous les types de Python, pas seulement `str`. +Vous pouvez déclarer tous les types standards de Python, pas seulement `str`. -Comme par exemple : +Vous pouvez utiliser, par exemple : * `int` * `float` * `bool` * `bytes` -{*../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} -### Types génériques avec des paramètres de types +### Types génériques avec paramètres de type { #generic-types-with-type-parameters } -Il existe certaines structures de données qui contiennent d'autres valeurs, comme `dict`, `list`, `set` et `tuple`. Et les valeurs internes peuvent elles aussi avoir leurs propres types. +Il existe certaines structures de données qui peuvent contenir d'autres valeurs, comme `dict`, `list`, `set` et `tuple`. Et les valeurs internes peuvent aussi avoir leur propre type. -Pour déclarer ces types et les types internes, on utilise le module standard de Python `typing`. +Ces types qui ont des types internes sont appelés types « génériques ». Et il est possible de les déclarer, même avec leurs types internes. -Il existe spécialement pour supporter ces annotations de types. +Pour déclarer ces types et les types internes, vous pouvez utiliser le module standard Python `typing`. Il existe spécifiquement pour prendre en charge ces annotations de type. -#### `List` +#### Versions plus récentes de Python { #newer-versions-of-python } -Par exemple, définissons une variable comme `list` de `str`. +La syntaxe utilisant `typing` est compatible avec toutes les versions, de Python 3.6 aux plus récentes, y compris Python 3.9, Python 3.10, etc. -Importez `List` (avec un `L` majuscule) depuis `typing`. +Au fur et à mesure que Python évolue, les versions plus récentes apportent un meilleur support pour ces annotations de type et dans de nombreux cas vous n'aurez même pas besoin d'importer et d'utiliser le module `typing` pour les déclarer. -{*../../docs_src/python_types/tutorial006.py hl[1] *} +Si vous pouvez choisir une version plus récente de Python pour votre projet, vous pourrez profiter de cette simplicité supplémentaire. -Déclarez la variable, en utilisant la syntaxe des deux-points (`:`). +Dans toute la documentation, il y a des exemples compatibles avec chaque version de Python (lorsqu'il y a une différence). -Et comme type, mettez `List`. +Par exemple « Python 3.6+ » signifie que c'est compatible avec Python 3.6 ou supérieur (y compris 3.7, 3.8, 3.9, 3.10, etc.). Et « Python 3.9+ » signifie que c'est compatible avec Python 3.9 ou supérieur (y compris 3.10, etc). -Les listes étant un type contenant des types internes, mettez ces derniers entre crochets (`[`, `]`) : +Si vous pouvez utiliser les dernières versions de Python, utilisez les exemples pour la dernière version, ils auront la meilleure et la plus simple syntaxe, par exemple, « Python 3.10+ ». -{*../../docs_src/python_types/tutorial006.py hl[4] *} +#### Liste { #list } -/// tip | Astuce +Par exemple, définissons une variable comme une `list` de `str`. -Ces types internes entre crochets sont appelés des "paramètres de type". +Déclarez la variable, en utilisant la même syntaxe avec deux-points (`:`). -Ici, `str` est un paramètre de type passé à `List`. +Comme type, mettez `list`. + +Comme la liste est un type qui contient des types internes, mettez-les entre crochets : + +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} + +/// info + +Ces types internes entre crochets sont appelés « paramètres de type ». + +Dans ce cas, `str` est le paramètre de type passé à `list`. /// -Ce qui signifie : "la variable `items` est une `list`, et chacun de ses éléments a pour type `str`. +Cela signifie : « la variable `items` est une `list`, et chacun des éléments de cette liste est un `str` ». -En faisant cela, votre éditeur pourra vous aider, même pendant que vous traitez des éléments de la liste. +En faisant cela, votre éditeur peut vous fournir de l'aide même pendant le traitement des éléments de la liste : Sans types, c'est presque impossible à réaliser. -Vous remarquerez que la variable `item` n'est qu'un des éléments de la list `items`. +Remarquez que la variable `item` est l'un des éléments de la liste `items`. -Et pourtant, l'éditeur sait qu'elle est de type `str` et pourra donc vous aider à l'utiliser. +Et pourtant, l'éditeur sait que c'est un `str` et fournit le support approprié. -#### `Tuple` et `Set` +#### Tuple et Set { #tuple-and-set } -C'est le même fonctionnement pour déclarer un `tuple` ou un `set` : +Vous feriez la même chose pour déclarer des `tuple` et des `set` : -{*../../docs_src/python_types/tutorial007.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} -Dans cet exemple : +Cela signifie : -* La variable `items_t` est un `tuple` avec 3 éléments, un `int`, un deuxième `int`, et un `str`. +* La variable `items_t` est un `tuple` avec 3 éléments, un `int`, un autre `int`, et un `str`. * La variable `items_s` est un `set`, et chacun de ses éléments est de type `bytes`. -#### `Dict` +#### Dict { #dict } -Pour définir un `dict`, il faut lui passer 2 paramètres, séparés par une virgule (`,`). +Pour définir un `dict`, vous passez 2 paramètres de type, séparés par des virgules. -Le premier paramètre de type est pour les clés et le second pour les valeurs du dictionnaire (`dict`). +Le premier paramètre de type est pour les clés du `dict`. -{*../../docs_src/python_types/tutorial008.py hl[1,4] *} +Le second paramètre de type est pour les valeurs du `dict` : -Dans cet exemple : +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} -* La variable `prices` est de type `dict` : - * Les clés de ce dictionnaire sont de type `str`. - * Les valeurs de ce dictionnaire sont de type `float`. +Cela signifie : -#### `Optional` +* La variable `prices` est un `dict` : + * Les clés de ce `dict` sont de type `str` (disons, le nom de chaque article). + * Les valeurs de ce `dict` sont de type `float` (disons, le prix de chaque article). -Vous pouvez aussi utiliser `Optional` pour déclarer qu'une variable a un type, comme `str` mais qu'il est "optionnel" signifiant qu'il pourrait aussi être `None`. +#### Union { #union } -{*../../docs_src/python_types/tutorial009.py hl[1,4] *} +Vous pouvez déclarer qu'une variable peut être de plusieurs types, par exemple, un `int` ou un `str`. -Utiliser `Optional[str]` plutôt que `str` permettra à l'éditeur de vous aider à détecter les erreurs où vous supposeriez qu'une valeur est toujours de type `str`, alors qu'elle pourrait aussi être `None`. +Dans Python 3.6 et supérieur (y compris Python 3.10), vous pouvez utiliser le type `Union` de `typing` et mettre entre crochets les types possibles à accepter. -#### Types génériques +Dans Python 3.10, il existe aussi une nouvelle syntaxe où vous pouvez mettre les types possibles séparés par une barre verticale (`|`). -Les types qui peuvent contenir des paramètres de types entre crochets, comme : +//// tab | Python 3.10+ -* `List` -* `Tuple` -* `Set` -* `Dict` +```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!} +``` + +//// + +Dans les deux cas, cela signifie que `item` peut être un `int` ou un `str`. + +#### Possiblement `None` { #possibly-none } + +Vous pouvez déclarer qu'une valeur peut avoir un type, comme `str`, mais qu'elle peut aussi être `None`. + +Dans Python 3.6 et supérieur (y compris Python 3.10), vous pouvez le déclarer en important et en utilisant `Optional` depuis le module `typing`. + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +Utiliser `Optional[str]` au lieu de simplement `str` permettra à l'éditeur de vous aider à détecter des erreurs où vous supposeriez qu'une valeur est toujours un `str`, alors qu'elle pourrait en fait aussi être `None`. + +`Optional[Something]` est en réalité un raccourci pour `Union[Something, None]`, ils sont équivalents. + +Cela signifie aussi que dans Python 3.10, vous pouvez utiliser `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!} +``` + +//// + +#### Utiliser `Union` ou `Optional` { #using-union-or-optional } + +Si vous utilisez une version de Python inférieure à 3.10, voici un conseil de mon point de vue très **subjectif** : + +* 🚨 Évitez d'utiliser `Optional[SomeType]` +* À la place ✨ **utilisez `Union[SomeType, None]`** ✨. + +Les deux sont équivalents et sous le capot ce sont les mêmes, mais je recommanderais `Union` plutôt que `Optional` parce que le mot « facultatif » semble impliquer que la valeur est optionnelle, alors que cela signifie en fait « elle peut être `None` », même si elle n'est pas facultative et est toujours requise. + +Je pense que `Union[SomeType, None]` est plus explicite sur ce que cela signifie. + +Il ne s'agit que des mots et des noms. Mais ces mots peuvent influencer la manière dont vous et vos coéquipiers pensez au code. + +Par exemple, prenons cette fonction : + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +Le paramètre `name` est défini comme `Optional[str]`, mais il n'est pas facultatif, vous ne pouvez pas appeler la fonction sans le paramètre : + +```Python +say_hi() # Oh non, cela lève une erreur ! 😱 +``` + +Le paramètre `name` est toujours requis (pas « optionnel ») parce qu'il n'a pas de valeur par défaut. Néanmoins, `name` accepte `None` comme valeur : + +```Python +say_hi(name=None) # Cela fonctionne, None est valide 🎉 +``` + +La bonne nouvelle est que, dès que vous êtes sur Python 3.10, vous n'avez plus à vous en soucier, car vous pourrez simplement utiliser `|` pour définir des unions de types : + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +Et alors vous n'aurez plus à vous soucier de noms comme `Optional` et `Union`. 😎 + +#### Types génériques { #generic-types } + +Ces types qui prennent des paramètres de type entre crochets sont appelés des **types génériques** ou **Generics**, par exemple : + +//// tab | Python 3.10+ + +Vous pouvez utiliser les mêmes types intégrés comme génériques (avec des crochets et des types à l'intérieur) : + +* `list` +* `tuple` +* `set` +* `dict` + +Et, comme avec les versions précédentes de Python, depuis le module `typing` : + +* `Union` * `Optional` -* ...et d'autres. +* ... et d'autres. -sont appelés des **types génériques** ou **Generics**. +Dans Python 3.10, comme alternative à l'utilisation des génériques `Union` et `Optional`, vous pouvez utiliser la barre verticale (`|`) pour déclarer des unions de types, c'est bien mieux et plus simple. -### Classes en tant que types +//// + +//// tab | Python 3.9+ + +Vous pouvez utiliser les mêmes types intégrés comme génériques (avec des crochets et des types à l'intérieur) : + +* `list` +* `tuple` +* `set` +* `dict` + +Et des génériques depuis le module `typing` : + +* `Union` +* `Optional` +* ... et d'autres. + +//// + +### Classes en tant que types { #classes-as-types } Vous pouvez aussi déclarer une classe comme type d'une variable. -Disons que vous avez une classe `Person`, avec une variable `name` : - -{*../../docs_src/python_types/tutorial010.py hl[1:3] *} +Disons que vous avez une classe `Person`, avec un nom : +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Vous pouvez ensuite déclarer une variable de type `Person` : -{*../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} -Et vous aurez accès, encore une fois, au support complet offert par l'éditeur : +Et là encore, vous obtenez tout le support de l'éditeur : -## Les modèles Pydantic +Remarquez que cela signifie « `one_person` est une instance de la classe `Person` ». + +Cela ne signifie pas « `one_person` est la classe appelée `Person` ». + +## Modèles Pydantic { #pydantic-models } Pydantic est une bibliothèque Python pour effectuer de la validation de données. -Vous déclarez la forme de la donnée avec des classes et des attributs. +Vous déclarez la « forme » de la donnée sous forme de classes avec des attributs. -Chaque attribut possède un type. +Et chaque attribut a un type. -Puis vous créez une instance de cette classe avec certaines valeurs et **Pydantic** validera les valeurs, les convertira dans le type adéquat (si c'est nécessaire et possible) et vous donnera un objet avec toute la donnée. +Ensuite, vous créez une instance de cette classe avec certaines valeurs et elle validera les valeurs, les convertira dans le type approprié (le cas échéant) et vous donnera un objet avec toutes les données. -Ainsi, votre éditeur vous offrira un support adapté pour l'objet résultant. +Et vous obtenez tout le support de l'éditeur avec cet objet résultant. -Extrait de la documentation officielle de **Pydantic** : +Un exemple tiré de la documentation officielle de Pydantic : -{*../../docs_src/python_types/tutorial011.py*} +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info -Pour en savoir plus à propos de Pydantic, allez jeter un coup d'oeil à sa documentation. +Pour en savoir plus à propos de Pydantic, consultez sa documentation. /// -**FastAPI** est basé entièrement sur **Pydantic**. +**FastAPI** est entièrement basé sur Pydantic. -Vous verrez bien plus d'exemples de son utilisation dans [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. +Vous verrez beaucoup plus de tout cela en pratique dans le [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. -## Les annotations de type dans **FastAPI** +/// tip | Astuce -**FastAPI** utilise ces annotations pour faire différentes choses. +Pydantic a un comportement spécial lorsque vous utilisez `Optional` ou `Union[Something, None]` sans valeur par défaut, vous pouvez en lire davantage dans la documentation de Pydantic à propos des champs Optionals requis. -Avec **FastAPI**, vous déclarez des paramètres grâce aux annotations de types et vous obtenez : +/// -* **du support de l'éditeur** -* **de la vérification de types** +## Annotations de type avec métadonnées { #type-hints-with-metadata-annotations } -...et **FastAPI** utilise ces mêmes déclarations pour : +Python dispose également d'une fonctionnalité qui permet de mettre des **métadonnées supplémentaires** dans ces annotations de type en utilisant `Annotated`. -* **Définir les prérequis** : depuis les paramètres de chemins des requêtes, les entêtes, les corps, les dépendances, etc. -* **Convertir des données** : depuis la requête vers les types requis. -* **Valider des données** : venant de chaque requête : - * Générant automatiquement des **erreurs** renvoyées au client quand la donnée est invalide. +Depuis Python 3.9, `Annotated` fait partie de la bibliothèque standard, vous pouvez donc l'importer depuis `typing`. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Python lui-même ne fait rien avec ce `Annotated`. Et pour les éditeurs et autres outils, le type est toujours `str`. + +Mais vous pouvez utiliser cet espace dans `Annotated` pour fournir à **FastAPI** des métadonnées supplémentaires sur la façon dont vous voulez que votre application se comporte. + +L'important à retenir est que le premier paramètre de type que vous passez à `Annotated` est le type réel. Le reste n'est que des métadonnées pour d'autres outils. + +Pour l'instant, vous avez juste besoin de savoir que `Annotated` existe, et que c'est du Python standard. 😎 + +Plus tard, vous verrez à quel point cela peut être puissant. + +/// tip | Astuce + +Le fait que ce soit du Python standard signifie que vous bénéficierez toujours de la meilleure expérience développeur possible dans votre éditeur, avec les outils que vous utilisez pour analyser et refactoriser votre code, etc. ✨ + +Et aussi que votre code sera très compatible avec de nombreux autres outils et bibliothèques Python. 🚀 + +/// + +## Annotations de type dans **FastAPI** { #type-hints-in-fastapi } + +**FastAPI** tire parti de ces annotations de type pour faire plusieurs choses. + +Avec **FastAPI**, vous déclarez des paramètres avec des annotations de type et vous obtenez : + +* **Du support de l'éditeur**. +* **Des vérifications de types**. + +... et **FastAPI** utilise les mêmes déclarations pour : + +* **Définir des prérequis** : à partir des paramètres de chemin de la requête, des paramètres de requête, des en-têtes, des corps, des dépendances, etc. +* **Convertir des données** : de la requête vers le type requis. +* **Valider des données** : provenant de chaque requête : + * En générant des **erreurs automatiques** renvoyées au client lorsque la donnée est invalide. * **Documenter** l'API avec OpenAPI : - * ce qui ensuite utilisé par les interfaces utilisateur automatiques de documentation interactive. + * ce qui est ensuite utilisé par les interfaces utilisateur de documentation interactive automatiques. -Tout cela peut paraître bien abstrait, mais ne vous inquiétez pas, vous verrez tout ça en pratique dans [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. +Tout cela peut sembler abstrait. Ne vous inquiétez pas. Vous verrez tout cela en action dans le [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. -Ce qu'il faut retenir c'est qu'en utilisant les types standard de Python, à un seul endroit (plutôt que d'ajouter plus de classes, de décorateurs, etc.), **FastAPI** fera une grande partie du travail pour vous. +L'important est qu'en utilisant les types standards de Python, en un seul endroit (au lieu d'ajouter plus de classes, de décorateurs, etc.), **FastAPI** fera une grande partie du travail pour vous. /// info -Si vous avez déjà lu le tutoriel et êtes revenus ici pour voir plus sur les types, une bonne ressource est la "cheat sheet" de `mypy`. +Si vous avez déjà parcouru tout le tutoriel et êtes revenu pour en voir plus sur les types, une bonne ressource est l'« aide-mémoire » de `mypy`. /// diff --git a/docs/fr/docs/tutorial/background-tasks.md b/docs/fr/docs/tutorial/background-tasks.md index 6efd16e07..ed7494669 100644 --- a/docs/fr/docs/tutorial/background-tasks.md +++ b/docs/fr/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# Tâches d'arrière-plan +# Tâches d'arrière-plan { #background-tasks } Vous pouvez définir des tâches d'arrière-plan qui seront exécutées après avoir retourné une réponse. @@ -7,20 +7,19 @@ Ceci est utile pour les opérations qui doivent avoir lieu après une requête, Cela comprend, par exemple : * Les notifications par email envoyées après l'exécution d'une action : - * Étant donné que se connecter à un serveur et envoyer un email a tendance à être «lent» (plusieurs secondes), vous pouvez retourner la réponse directement et envoyer la notification en arrière-plan. + * Étant donné que se connecter à un serveur et envoyer un email a tendance à être « lent » (plusieurs secondes), vous pouvez retourner la réponse directement et envoyer la notification en arrière-plan. * Traiter des données : - * Par exemple, si vous recevez un fichier qui doit passer par un traitement lent, vous pouvez retourner une réponse «Accepted» (HTTP 202) puis faire le traitement en arrière-plan. + * Par exemple, si vous recevez un fichier qui doit passer par un traitement lent, vous pouvez retourner une réponse « Accepted » (HTTP 202) puis faire le traitement en arrière-plan. +## Utiliser `BackgroundTasks` { #using-backgroundtasks } -## Utiliser `BackgroundTasks` +Pour commencer, importez `BackgroundTasks` et définissez un paramètre dans votre *fonction de chemin d'accès* avec `BackgroundTasks` comme type déclaré. -Pour commencer, importez `BackgroundTasks` et définissez un paramètre dans votre *fonction de chemin* avec `BackgroundTasks` comme type déclaré. - -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** créera l'objet de type `BackgroundTasks` pour vous et le passera comme paramètre. -## Créer une fonction de tâche +## Créer une fonction de tâche { #create-a-task-function } Une fonction à exécuter comme tâche d'arrière-plan est juste une fonction standard qui peut recevoir des paramètres. @@ -30,14 +29,13 @@ Dans cet exemple, la fonction de tâche écrira dans un fichier (afin de simuler L'opération d'écriture n'utilisant ni `async` ni `await`, on définit la fonction avec un `def` normal. -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Ajouter une tâche d'arrière-plan +## Ajouter une tâche d'arrière-plan { #add-the-background-task } -Dans votre *fonction de chemin*, passez votre fonction de tâche à l'objet de type `BackgroundTasks` (`background_tasks` ici) grâce à la méthode `.add_task()` : +Dans votre *fonction de chemin d'accès*, passez votre fonction de tâche à l'objet de type `BackgroundTasks` (`background_tasks` ici) grâce à la méthode `.add_task()` : - -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` reçoit comme arguments : @@ -45,40 +43,40 @@ Dans votre *fonction de chemin*, passez votre fonction de tâche à l'objet de t * Les arguments positionnels à passer à la fonction de tâche dans l'ordre (`email`). * Les arguments nommés à passer à la fonction de tâche (`message="some notification"`). -## Injection de dépendances +## Injection de dépendances { #dependency-injection } -Utiliser `BackgroundTasks` fonctionne aussi avec le système d'injection de dépendances. Vous pouvez déclarer un paramètre de type `BackgroundTasks` à différents niveaux : dans une *fonction de chemin*, dans une dépendance, dans une sous-dépendance... +Utiliser `BackgroundTasks` fonctionne aussi avec le système d'injection de dépendances. Vous pouvez déclarer un paramètre de type `BackgroundTasks` à différents niveaux : dans une *fonction de chemin d'accès*, dans une dépendance (dependable), dans une sous-dépendance, etc. -**FastAPI** sait quoi faire dans chaque cas et comment réutiliser le même objet, afin que tous les paramètres de type `BackgroundTasks` soient fusionnés et que les tâches soient exécutées en arrière-plan : +**FastAPI** sait quoi faire dans chaque cas et comment réutiliser le même objet, afin que toutes les tâches d'arrière-plan soient fusionnées et que les tâches soient ensuite exécutées en arrière-plan : -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} Dans cet exemple, les messages seront écrits dans le fichier `log.txt` après que la réponse soit envoyée. -S'il y avait une `query` (paramètre nommé `q`) dans la requête, alors elle sera écrite dans `log.txt` via une tâche d'arrière-plan. +S'il y avait un paramètre de requête dans la requête, alors il sera écrit dans le journal via une tâche d'arrière-plan. -Et ensuite une autre tâche d'arrière-plan (générée dans les paramètres de la *la fonction de chemin*) écrira un message dans `log.txt` comprenant le paramètre de chemin `email`. +Et ensuite une autre tâche d'arrière-plan (générée dans la *fonction de chemin d'accès*) écrira un message comprenant le paramètre de chemin `email`. -## Détails techniques +## Détails techniques { #technical-details } La classe `BackgroundTasks` provient directement de `starlette.background`. Elle est importée/incluse directement dans **FastAPI** pour que vous puissiez l'importer depuis `fastapi` et éviter d'importer accidentellement `BackgroundTask` (sans `s` à la fin) depuis `starlette.background`. -En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possible de l'utiliser en tant que paramètre de *fonction de chemin* et de laisser **FastAPI** gérer le reste pour vous, comme en utilisant l'objet `Request` directement. +En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possible de l'utiliser en tant que paramètre de *fonction de chemin d'accès* et de laisser **FastAPI** gérer le reste pour vous, comme en utilisant l'objet `Request` directement. Il est tout de même possible d'utiliser `BackgroundTask` seul dans **FastAPI**, mais dans ce cas il faut créer l'objet dans le code et renvoyer une `Response` Starlette l'incluant. -Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). +Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan. -## Avertissement +## Avertissement { #caveat } Si vous avez besoin de réaliser des traitements lourds en tâche d'arrière-plan et que vous n'avez pas besoin que ces traitements aient lieu dans le même process (par exemple, pas besoin de partager la mémoire, les variables, etc.), il peut s'avérer profitable d'utiliser des outils plus importants tels que Celery. -Ces outils nécessitent généralement des configurations plus complexes ainsi qu'un gestionnaire de queue de message, comme RabbitMQ ou Redis, mais ils permettent d'exécuter des tâches d'arrière-plan dans différents process, et potentiellement, sur plusieurs serveurs. +Ces outils nécessitent généralement des configurations plus complexes ainsi qu'un gestionnaire de queue de message, comme RabbitMQ ou Redis, mais ils permettent d'exécuter des tâches d'arrière-plan dans différents process, et surtout, sur plusieurs serveurs. Mais si vous avez besoin d'accéder aux variables et objets de la même application **FastAPI**, ou si vous avez besoin d'effectuer de petites tâches d'arrière-plan (comme envoyer des notifications par email), vous pouvez simplement vous contenter d'utiliser `BackgroundTasks`. -## Résumé +## Résumé { #recap } -Importez et utilisez `BackgroundTasks` grâce aux paramètres de *fonction de chemin* et les dépendances pour ajouter des tâches d'arrière-plan. +Importez et utilisez `BackgroundTasks` grâce aux paramètres de *fonction de chemin d'accès* et les dépendances pour ajouter des tâches d'arrière-plan. diff --git a/docs/fr/docs/tutorial/body-multiple-params.md b/docs/fr/docs/tutorial/body-multiple-params.md index 0541acc74..92ca2afc3 100644 --- a/docs/fr/docs/tutorial/body-multiple-params.md +++ b/docs/fr/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# Body - Paramètres multiples +# Body - Paramètres multiples { #body-multiple-parameters } -Maintenant que nous avons vu comment manipuler `Path` et `Query`, voyons comment faire pour le corps d'une requête, communément désigné par le terme anglais "body". +Maintenant que nous avons vu comment utiliser `Path` et `Query`, voyons des usages plus avancés des déclarations de paramètres du corps de la requête. -## Mélanger les paramètres `Path`, `Query` et body +## Mélanger les paramètres `Path`, `Query` et du corps de la requête { #mix-path-query-and-body-parameters } -Tout d'abord, sachez que vous pouvez mélanger les déclarations des paramètres `Path`, `Query` et body, **FastAPI** saura quoi faire. +Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du corps de la requête, **FastAPI** saura quoi faire. -Vous pouvez également déclarer des paramètres body comme étant optionnels, en leur assignant une valeur par défaut à `None` : +Et vous pouvez également déclarer des paramètres du corps de la requête comme étant optionnels, en leur assignant une valeur par défaut à `None` : {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} -/// note +/// note | Remarque -Notez que, dans ce cas, le paramètre `item` provenant du `Body` est optionnel (sa valeur par défaut est `None`). +Notez que, dans ce cas, l'élément `item` récupéré depuis le corps de la requête est optionnel. Comme sa valeur par défaut est `None`. /// -## Paramètres multiples du body +## Paramètres multiples du corps de la requête { #multiple-body-parameters } -Dans l'exemple précédent, les opérations de routage attendaient un body JSON avec les attributs d'un `Item`, par exemple : +Dans l'exemple précédent, les chemins d'accès attendraient un corps de la requête JSON avec les attributs d'un `Item`, par exemple : ```JSON { @@ -29,13 +29,13 @@ Dans l'exemple précédent, les opérations de routage attendaient un body JSON } ``` -Mais vous pouvez également déclarer plusieurs paramètres provenant de body, par exemple `item` et `user` simultanément : +Mais vous pouvez également déclarer plusieurs paramètres provenant du corps de la requête, par exemple `item` et `user` : {* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre dans le body (chacun correspondant à un modèle Pydantic). +Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du corps de la requête dans la fonction (il y a deux paramètres qui sont des modèles Pydantic). -Il utilisera alors les noms des paramètres comme clés, et s'attendra à recevoir quelque chose de semblable à : +Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le corps de la requête, et s'attendra à recevoir un corps de la requête semblable à : ```JSON { @@ -52,29 +52,29 @@ Il utilisera alors les noms des paramètres comme clés, et s'attendra à recevo } ``` -/// note +/// note | Remarque -"Notez que, bien que nous ayons déclaré le paramètre `item` de la même manière que précédemment, il est maintenant associé à la clé `item` dans le corps de la requête."`. +Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du corps de la requête sous la clé `item`. /// -**FastAPI** effectue la conversion de la requête de façon transparente, de sorte que les objets `item` et `user` se trouvent correctement définis. +**FastAPI** effectuera la conversion automatique depuis la requête, de sorte que le paramètre `item` reçoive son contenu spécifique, et de même pour `user`. -Il effectue également la validation des données (même imbriquées les unes dans les autres), et permet de les documenter correctement (schéma OpenAPI et documentation auto-générée). +Il effectuera la validation des données composées, et les documentera ainsi pour le schéma OpenAPI et la documentation automatique. -## Valeurs scalaires dans le body +## Valeurs singulières dans le corps de la requête { #singular-values-in-body } -De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres query et path, **FastAPI** fournit un équivalent `Body`. +De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres de requête et de chemin, **FastAPI** fournit un équivalent `Body`. -Par exemple, en étendant le modèle précédent, vous pouvez vouloir ajouter un paramètre `importance` dans le même body, en plus des paramètres `item` et `user`. +Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même corps de la requête, en plus de `item` et `user`. -Si vous le déclarez tel quel, comme c'est une valeur [scalaire](https://docs.github.com/fr/graphql/reference/scalars), **FastAPI** supposera qu'il s'agit d'un paramètre de requête (`Query`). +Si vous le déclarez tel quel, comme c'est une valeur singulière, **FastAPI** supposera qu'il s'agit d'un paramètre de requête. -Mais vous pouvez indiquer à **FastAPI** de la traiter comme une variable de body en utilisant `Body` : +Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du corps de la requête en utilisant `Body` : {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -Dans ce cas, **FastAPI** s'attendra à un body semblable à : +Dans ce cas, **FastAPI** s'attendra à un corps de la requête semblable à : ```JSON { @@ -92,51 +92,51 @@ Dans ce cas, **FastAPI** s'attendra à un body semblable à : } ``` -Encore une fois, cela convertira les types de données, les validera, permettra de générer la documentation, etc... +Encore une fois, il convertira les types de données, validera, documentera, etc. -## Paramètres multiples body et query +## Paramètres multiples du corps de la requête et paramètres de requête { #multiple-body-params-and-query } -Bien entendu, vous pouvez déclarer autant de paramètres que vous le souhaitez, en plus des paramètres body déjà déclarés. +Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du corps de la requête. -Par défaut, les valeurs [scalaires](https://docs.github.com/fr/graphql/reference/scalars) sont interprétées comme des paramètres query, donc inutile d'ajouter explicitement `Query`. Vous pouvez juste écrire : - -```Python -q: Union[str, None] = None -``` - -Ou bien, en Python 3.10 et supérieur : +Comme, par défaut, les valeurs singulières sont interprétées comme des paramètres de requête, vous n'avez pas besoin d'ajouter explicitement `Query`, vous pouvez simplement écrire : ```Python q: str | None = None ``` +Ou en Python 3.9 : + +```Python +q: Union[str, None] = None +``` + Par exemple : -{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[27] *} +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} /// info -`Body` possède les mêmes paramètres de validation additionnels et de gestion des métadonnées que `Query` et `Path`, ainsi que d'autres que nous verrons plus tard. +`Body` possède également les mêmes paramètres supplémentaires de validation et de métadonnées que `Query`, `Path` et d'autres que vous verrez plus tard. /// -## Inclure un paramètre imbriqué dans le body +## Intégrer un seul paramètre du corps de la requête { #embed-a-single-body-parameter } -Disons que vous avez seulement un paramètre `item` dans le body, correspondant à un modèle Pydantic `Item`. +Supposons que vous n'ayez qu'un seul paramètre `item` dans le corps de la requête, provenant d'un modèle Pydantic `Item`. -Par défaut, **FastAPI** attendra sa déclaration directement dans le body. +Par défaut, **FastAPI** attendra alors son contenu directement. -Cependant, si vous souhaitez qu'il interprête correctement un JSON avec une clé `item` associée au contenu du modèle, comme cela serait le cas si vous déclariez des paramètres body additionnels, vous pouvez utiliser le paramètre spécial `embed` de `Body` : +Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du corps de la requête, vous pouvez utiliser le paramètre spécial `embed` de `Body` : ```Python item: Item = Body(embed=True) ``` -Voici un exemple complet : +comme dans : {* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} -Dans ce cas **FastAPI** attendra un body semblable à : +Dans ce cas **FastAPI** s'attendra à un corps de la requête semblable à : ```JSON hl_lines="2" { @@ -160,12 +160,12 @@ au lieu de : } ``` -## Pour résumer +## Récapitulatif { #recap } -Vous pouvez ajouter plusieurs paramètres body dans votre fonction de routage, même si une requête ne peut avoir qu'un seul body. +Vous pouvez ajouter plusieurs paramètres du corps de la requête à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul corps de la requête. -Cependant, **FastAPI** se chargera de faire opérer sa magie, afin de toujours fournir à votre fonction des données correctes, les validera et documentera le schéma associé. +Mais **FastAPI** s'en chargera, vous fournira les bonnes données dans votre fonction, et validera et documentera le schéma correct dans le chemin d'accès. -Vous pouvez également déclarer des valeurs [scalaires](https://docs.github.com/fr/graphql/reference/scalars) à recevoir dans le body. +Vous pouvez également déclarer des valeurs singulières à recevoir dans le corps de la requête. -Et vous pouvez indiquer à **FastAPI** d'inclure le body dans une autre variable, même lorsqu'un seul paramètre est déclaré. +Et vous pouvez indiquer à **FastAPI** d'intégrer le corps de la requête sous une clé même lorsqu'un seul paramètre est déclaré. diff --git a/docs/fr/docs/tutorial/body.md b/docs/fr/docs/tutorial/body.md index 760b6d80a..ca115fabc 100644 --- a/docs/fr/docs/tutorial/body.md +++ b/docs/fr/docs/tutorial/body.md @@ -1,10 +1,10 @@ -# Corps de la requête +# Corps de la requête { #request-body } Quand vous avez besoin d'envoyer de la donnée depuis un client (comme un navigateur) vers votre API, vous l'envoyez en tant que **corps de requête**. Le corps d'une **requête** est de la donnée envoyée par le client à votre API. Le corps d'une **réponse** est la donnée envoyée par votre API au client. -Votre API aura presque toujours à envoyer un corps de **réponse**. Mais un client n'a pas toujours à envoyer un corps de **requête**. +Votre API aura presque toujours à envoyer un corps de **réponse**. Mais un client n'a pas toujours à envoyer un **corps de requête** : parfois il demande seulement un chemin, peut-être avec quelques paramètres de requête, mais n'envoie pas de corps. Pour déclarer un corps de **requête**, on utilise les modèles de Pydantic en profitant de tous leurs avantages et fonctionnalités. @@ -18,23 +18,23 @@ Ceci étant découragé, la documentation interactive générée par Swagger UI /// -## Importez le `BaseModel` de Pydantic +## Importer le `BaseModel` de Pydantic { #import-pydantics-basemodel } Commencez par importer la classe `BaseModel` du module `pydantic` : -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Créez votre modèle de données +## Créer votre modèle de données { #create-your-data-model } Déclarez ensuite votre modèle de données en tant que classe qui hérite de `BaseModel`. Utilisez les types Python standard pour tous les attributs : -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Tout comme pour la déclaration de paramètres de requête, quand un attribut de modèle a une valeur par défaut, il n'est pas nécessaire. Sinon, cet attribut doit être renseigné dans le corps de la requête. Pour rendre ce champ optionnel simplement, utilisez `None` comme valeur par défaut. +Tout comme pour la déclaration de paramètres de requête, quand un attribut de modèle a une valeur par défaut, il n'est pas nécessaire. Sinon, cet attribut doit être renseigné dans le corps de la requête. Utilisez `None` pour le rendre simplement optionnel. -Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) tel que : +Par exemple, le modèle ci-dessus déclare un JSON « `object` » (ou `dict` Python) tel que : ```JSON { @@ -45,7 +45,7 @@ Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) te } ``` -...`description` et `tax` étant des attributs optionnels (avec `None` comme valeur par défaut), cet "objet" JSON serait aussi valide : +... `description` et `tax` étant des attributs optionnels (avec `None` comme valeur par défaut), ce JSON « `object` » serait aussi valide : ```JSON { @@ -54,28 +54,28 @@ Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) te } ``` -## Déclarez-le comme paramètre +## Le déclarer comme paramètre { #declare-it-as-a-parameter } Pour l'ajouter à votre *opération de chemin*, déclarez-le comme vous déclareriez des paramètres de chemin ou de requête : -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...et déclarez que son type est le modèle que vous avez créé : `Item`. +... et déclarez que son type est le modèle que vous avez créé : `Item`. -## Résultats +## Résultats { #results } En utilisant uniquement les déclarations de type Python, **FastAPI** réussit à : * Lire le contenu de la requête en tant que JSON. * Convertir les types correspondants (si nécessaire). * Valider la donnée. - * Si la donnée est invalide, une erreur propre et claire sera renvoyée, indiquant exactement où était la donnée incorrecte. + * Si la donnée est invalide, une erreur propre et claire sera renvoyée, indiquant exactement où et quelle était la donnée incorrecte. * Passer la donnée reçue dans le paramètre `item`. - * Ce paramètre ayant été déclaré dans la fonction comme étant de type `Item`, vous aurez aussi tout le support offert par l'éditeur (auto-complétion, etc.) pour tous les attributs de ce paramètre et les types de ces attributs. -* Générer des définitions JSON Schema pour votre modèle, qui peuvent être utilisées où vous en avez besoin dans votre projet ensuite. -* Ces schémas participeront à la constitution du schéma généré OpenAPI, et seront donc utilisés par les documentations automatiquement générées. + * Ce paramètre ayant été déclaré dans la fonction comme étant de type `Item`, vous aurez aussi tout le support offert par l'éditeur (autocomplétion, etc.) pour tous les attributs de ce paramètre et les types de ces attributs. +* Générer des définitions JSON Schema pour votre modèle ; vous pouvez également les utiliser partout ailleurs si cela a du sens pour votre projet. +* Ces schémas participeront à la constitution du schéma généré OpenAPI, et seront utilisés par les documentations automatiques UIs. -## Documentation automatique +## Documentation automatique { #automatic-docs } Les schémas JSON de vos modèles seront intégrés au schéma OpenAPI global de votre application, et seront donc affichés dans la documentation interactive de l'API : @@ -85,63 +85,63 @@ Et seront aussi utilisés dans chaque *opération de chemin* de la documentation -## Support de l'éditeur +## Support de l'éditeur { #editor-support } -Dans votre éditeur, vous aurez des annotations de types et de l'auto-complétion partout dans votre fonction (ce qui n'aurait pas été le cas si vous aviez utilisé un classique `dict` plutôt qu'un modèle Pydantic) : +Dans votre éditeur, vous aurez des annotations de type et de l'autocomplétion partout dans votre fonction (ce qui n'aurait pas été le cas si vous aviez reçu un `dict` plutôt qu'un modèle Pydantic) : -Et vous obtenez aussi de la vérification d'erreur pour les opérations incorrectes de types : +Et vous obtenez aussi des vérifications d'erreurs pour les opérations de types incorrectes : Ce n'est pas un hasard, ce framework entier a été bâti avec ce design comme objectif. -Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour s'assurer que cela fonctionnerait avec tous les éditeurs. +Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour vous assurer que cela fonctionnerait avec tous les éditeurs. Des changements sur Pydantic ont même été faits pour supporter cela. -Les captures d'écrans précédentes ont été prises sur Visual Studio Code. +Les captures d'écran précédentes ont été prises sur Visual Studio Code. -Mais vous auriez le même support de l'éditeur avec PyCharm et la majorité des autres éditeurs de code Python. +Mais vous auriez le même support de l'éditeur avec PyCharm et la majorité des autres éditeurs de code Python : /// tip | Astuce -Si vous utilisez PyCharm comme éditeur, vous pouvez utiliser le Plugin Pydantic PyCharm Plugin. +Si vous utilisez PyCharm comme éditeur, vous pouvez utiliser le plug-in Pydantic PyCharm Plugin. Ce qui améliore le support pour les modèles Pydantic avec : -* de l'auto-complétion +* de l'autocomplétion * des vérifications de type -* du "refactoring" (ou remaniement de code) +* du « refactoring » (ou remaniement de code) * de la recherche -* de l'inspection +* des inspections /// -## Utilisez le modèle +## Utiliser le modèle { #use-the-model } Dans la fonction, vous pouvez accéder à tous les attributs de l'objet du modèle directement : -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Corps de la requête + paramètres de chemin +## Corps de la requête + paramètres de chemin { #request-body-path-parameters } Vous pouvez déclarer des paramètres de chemin et un corps de requête pour la même *opération de chemin*. **FastAPI** est capable de reconnaître que les paramètres de la fonction qui correspondent aux paramètres de chemin doivent être **récupérés depuis le chemin**, et que les paramètres de fonctions déclarés comme modèles Pydantic devraient être **récupérés depuis le corps de la requête**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Corps de la requête + paramètres de chemin et de requête +## Corps de la requête + paramètres de chemin et de requête { #request-body-path-query-parameters } Vous pouvez aussi déclarer un **corps**, et des paramètres de **chemin** et de **requête** dans la même *opération de chemin*. **FastAPI** saura reconnaître chacun d'entre eux et récupérer la bonne donnée au bon endroit. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Les paramètres de la fonction seront reconnus comme tel : @@ -149,14 +149,16 @@ Les paramètres de la fonction seront reconnus comme tel : * Si le paramètre est d'un **type singulier** (comme `int`, `float`, `str`, `bool`, etc.), il sera interprété comme un paramètre de **requête**. * Si le paramètre est déclaré comme ayant pour type un **modèle Pydantic**, il sera interprété comme faisant partie du **corps** de la requête. -/// note +/// note | Remarque -**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `=None`. +**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. -Le type `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI**, mais sera utile à votre éditeur pour améliorer le support offert par ce dernier et détecter plus facilement des erreurs de type. +L'annotation de type `str | None` (Python 3.10+) ou `Union` dans `Union[str, None]` (Python 3.9+) n'est pas utilisée par **FastAPI** pour déterminer que la valeur n'est pas requise, il le saura parce qu'elle a une valeur par défaut `= None`. + +Mais ajouter ces annotations de type permettra à votre éditeur de vous offrir un meilleur support et de détecter des erreurs. /// -## Sans Pydantic +## Sans Pydantic { #without-pydantic } -Si vous ne voulez pas utiliser des modèles Pydantic, vous pouvez aussi utiliser des paramètres de **Corps**. Pour cela, allez voir la partie de la documentation sur [Corps de la requête - Paramètres multiples](body-multiple-params.md){.internal-link target=_blank}. +Si vous ne voulez pas utiliser des modèles Pydantic, vous pouvez aussi utiliser des paramètres de **Body**. Pour cela, allez voir la documentation sur [Corps de la requête - Paramètres multiples : Valeurs singulières dans le corps](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/fr/docs/tutorial/debugging.md b/docs/fr/docs/tutorial/debugging.md index ab00fbdeb..a88fa2b23 100644 --- a/docs/fr/docs/tutorial/debugging.md +++ b/docs/fr/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# Débogage +# Débogage { #debugging } Vous pouvez connecter le débogueur dans votre éditeur, par exemple avec Visual Studio Code ou PyCharm. -## Faites appel à `uvicorn` +## Appeler `uvicorn` { #call-uvicorn } Dans votre application FastAPI, importez et exécutez directement `uvicorn` : -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### À propos de `__name__ == "__main__"` +### À propos de `__name__ == "__main__"` { #about-name-main } Le but principal de `__name__ == "__main__"` est d'avoir du code qui est exécuté lorsque votre fichier est appelé avec : @@ -26,7 +26,7 @@ mais qui n'est pas appelé lorsqu'un autre fichier l'importe, comme dans : from myapp import app ``` -#### Pour davantage de détails +#### Pour davantage de détails { #more-details } Imaginons que votre fichier s'appelle `myapp.py`. @@ -78,7 +78,7 @@ Pour plus d'informations, consultez débogueur +## Exécuter votre code avec votre débogueur { #run-your-code-with-your-debugger } Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous pouvez appeler votre programme Python (votre application FastAPI) directement depuis le débogueur. @@ -86,10 +86,10 @@ Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous Par exemple, dans Visual Studio Code, vous pouvez : -- Cliquer sur l'onglet "Debug" de la barre d'activités de Visual Studio Code. -- "Add configuration...". -- Sélectionnez "Python". -- Lancez le débogueur avec l'option "`Python: Current File (Integrated Terminal)`". +- Allez dans le panneau « Debug ». +- « Add configuration... ». +- Sélectionnez « Python ». +- Lancez le débogueur avec l'option « Python: Current File (Integrated Terminal) ». Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. @@ -101,8 +101,8 @@ Voici à quoi cela pourrait ressembler : Si vous utilisez Pycharm, vous pouvez : -- Ouvrir le menu "Run". -- Sélectionnez l'option "Debug...". +- Ouvrez le menu « Run ». +- Sélectionnez l'option « Debug... ». - Un menu contextuel s'affiche alors. - Sélectionnez le fichier à déboguer (dans ce cas, `main.py`). diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 96ea56e62..b2693b3e5 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -1,107 +1,122 @@ -# Démarrage +# Démarrage { #first-steps } -Le fichier **FastAPI** le plus simple possible pourrait ressembler à cela : +Le fichier **FastAPI** le plus simple possible pourrait ressembler à ceci : -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} -Copiez ce code dans un fichier nommé `main.py`. +Copiez cela dans un fichier `main.py`. -Démarrez le serveur : +Démarrez le serveur en direct :
```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. ```
-/// note - -La commande `uvicorn main:app` fait référence à : - -* `main` : le fichier `main.py` (le module Python). -* `app` : l'objet créé dans `main.py` via la ligne `app = FastAPI()`. -* `--reload` : l'option disant à uvicorn de redémarrer le serveur à chaque changement du code. À ne pas utiliser en production ! - -/// - -Vous devriez voir dans la console, une ligne semblable à la suivante : +Dans la sortie, il y a une ligne semblable à : ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -Cette ligne montre l'URL par laquelle l'app est actuellement accessible, sur votre machine locale. +Cette ligne montre l’URL où votre application est servie, sur votre machine locale. -### Allez voir le résultat +### Vérifiez { #check-it } -Ouvrez votre navigateur à l'adresse
http://127.0.0.1:8000. +Ouvrez votre navigateur à l’adresse http://127.0.0.1:8000. -Vous obtiendrez cette réponse JSON : +Vous verrez la réponse JSON suivante : ```JSON {"message": "Hello World"} ``` -### Documentation interactive de l'API +### Documentation interactive de l’API { #interactive-api-docs } -Rendez-vous sur http://127.0.0.1:8000/docs. +Allez maintenant sur http://127.0.0.1:8000/docs. -Vous verrez la documentation interactive de l'API générée automatiquement (via Swagger UI) : +Vous verrez la documentation interactive de l’API générée automatiquement (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Documentation alternative +### Documentation alternative de l’API { #alternative-api-docs } -Ensuite, rendez-vous sur http://127.0.0.1:8000/redoc. +Et maintenant, allez sur http://127.0.0.1:8000/redoc. -Vous y verrez la documentation alternative (via ReDoc) : +Vous verrez la documentation automatique alternative (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** génère un "schéma" contenant toute votre API dans le standard de définition d'API **OpenAPI**. +**FastAPI** génère un « schéma » contenant toute votre API en utilisant le standard **OpenAPI** pour définir des API. -#### "Schéma" +#### « Schéma » { #schema } -Un "schéma" est une définition ou une description de quelque chose. Pas le code qui l'implémente, uniquement une description abstraite. +Un « schéma » est une définition ou une description de quelque chose. Pas le code qui l’implémente, mais uniquement une description abstraite. -#### "Schéma" d'API +#### « Schéma » d’API { #api-schema } Ici, OpenAPI est une spécification qui dicte comment définir le schéma de votre API. -Le schéma inclut les chemins de votre API, les paramètres potentiels de chaque chemin, etc. +Cette définition de schéma inclut les chemins de votre API, les paramètres possibles qu’ils prennent, etc. -#### "Schéma" de données +#### « Schéma » de données { #data-schema } -Le terme "schéma" peut aussi faire référence à la forme de la donnée, comme un contenu JSON. +Le terme « schéma » peut également faire référence à la forme d’une donnée, comme un contenu JSON. -Dans ce cas, cela signifierait les attributs JSON, ainsi que les types de ces attributs, etc. +Dans ce cas, cela désignerait les attributs JSON, ainsi que leurs types, etc. -#### OpenAPI et JSON Schema +#### OpenAPI et JSON Schema { #openapi-and-json-schema } -**OpenAPI** définit un schéma d'API pour votre API. Il inclut des définitions (ou "schémas") de la donnée envoyée et reçue par votre API en utilisant **JSON Schema**, le standard des schémas de données JSON. +OpenAPI définit un schéma d’API pour votre API. Et ce schéma inclut des définitions (ou « schémas ») des données envoyées et reçues par votre API en utilisant **JSON Schema**, le standard pour les schémas de données JSON. -#### Allez voir `openapi.json` +#### Voir le `openapi.json` { #check-the-openapi-json } -Si vous êtes curieux d'à quoi ressemble le schéma brut **OpenAPI**, **FastAPI** génère automatiquement un (schéma) JSON avec les descriptions de toute votre API. +Si vous êtes curieux de voir à quoi ressemble le schéma OpenAPI brut, FastAPI génère automatiquement un JSON (schéma) avec les descriptions de toute votre API. -Vous pouvez le voir directement à cette adresse : http://127.0.0.1:8000/openapi.json. - -Le schéma devrait ressembler à ceci : +Vous pouvez le voir directement à l’adresse : http://127.0.0.1:8000/openapi.json. +Il affichera un JSON commençant par quelque chose comme : ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -120,79 +135,87 @@ Le schéma devrait ressembler à ceci : ... ``` -#### À quoi sert OpenAPI +#### À quoi sert OpenAPI { #what-is-openapi-for } -Le schéma **OpenAPI** est ce qui alimente les deux systèmes de documentation interactive. +Le schéma OpenAPI est ce qui alimente les deux systèmes de documentation interactive inclus. -Et il existe des dizaines d'alternatives, toutes basées sur **OpenAPI**. Vous pourriez facilement ajouter n'importe laquelle de ces alternatives à votre application **FastAPI**. +Et il existe des dizaines d’alternatives, toutes basées sur OpenAPI. Vous pourriez facilement ajouter n’importe laquelle de ces alternatives à votre application construite avec **FastAPI**. -Vous pourriez aussi l'utiliser pour générer du code automatiquement, pour les clients qui communiquent avec votre API. Comme par exemple, des applications frontend, mobiles ou IOT. +Vous pourriez également l’utiliser pour générer du code automatiquement, pour les clients qui communiquent avec votre API. Par exemple, des applications frontend, mobiles ou IoT. -## Récapitulatif, étape par étape +### Déployer votre application (optionnel) { #deploy-your-app-optional } -### Étape 1 : import `FastAPI` +Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur FastAPI Cloud, allez rejoindre la liste d’attente si ce n’est pas déjà fait. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Si vous avez déjà un compte **FastAPI Cloud** (nous vous avons invité depuis la liste d’attente 😉), vous pouvez déployer votre application avec une seule commande. -`FastAPI` est une classe Python qui fournit toutes les fonctionnalités nécessaires au lancement de votre API. +Avant de déployer, vous devez vous assurer que vous êtes connecté : + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Puis déployez votre application : + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +C’est tout ! Vous pouvez maintenant accéder à votre application à cette URL. ✨ + +## Récapitulatif, étape par étape { #recap-step-by-step } + +### Étape 1 : importer `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} + +`FastAPI` est une classe Python qui fournit toutes les fonctionnalités nécessaires à votre API. /// note | Détails techniques -`FastAPI` est une classe héritant directement de `Starlette`. +`FastAPI` est une classe qui hérite directement de `Starlette`. -Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. +Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette avec `FastAPI`. /// -### Étape 2 : créer une "instance" `FastAPI` +### Étape 2 : créer une « instance » `FastAPI` { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} -Ici la variable `app` sera une "instance" de la classe `FastAPI`. +Ici, la variable `app` sera une « instance » de la classe `FastAPI`. -Ce sera le point principal d'interaction pour créer toute votre API. +Ce sera le point principal d’interaction pour créer toute votre API. -Cette `app` est la même que celle à laquelle fait référence `uvicorn` dans la commande : +### Étape 3 : créer un « chemin d’accès » { #step-3-create-a-path-operation } -
+#### Chemin { #path } -```console -$ uvicorn main:app --reload +« Chemin » fait ici référence à la dernière partie de l’URL à partir du premier `/`. -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -Si vous créez votre app avec : - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -Et la mettez dans un fichier `main.py`, alors vous appelleriez `uvicorn` avec : - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### Étape 3: créer une *opération de chemin* - -#### Chemin - -Chemin, ou "path" fait référence ici à la dernière partie de l'URL démarrant au premier `/`. - -Donc, dans un URL tel que : +Donc, dans une URL telle que : ``` https://example.com/items/foo ``` -...le "path" serait : +... le chemin serait : ``` /items/foo @@ -200,66 +223,67 @@ https://example.com/items/foo /// info -Un chemin, ou "path" est aussi souvent appelé route ou "endpoint". +Un « chemin » est aussi couramment appelé « endpoint » ou « route ». /// -#### Opération +Lors de la création d’une API, le « chemin » est la manière principale de séparer les « préoccupations » et les « ressources ». -"Opération" fait référence à une des "méthodes" HTTP. +#### Opération { #operation } -Une de : +« Opération » fait ici référence à l’une des « méthodes » HTTP. + +L’une de : * `POST` * `GET` * `PUT` * `DELETE` -...ou une des plus exotiques : +... et les plus exotiques : * `OPTIONS` * `HEAD` * `PATCH` * `TRACE` -Dans le protocol HTTP, vous pouvez communiquer avec chaque chemin en utilisant une (ou plus) de ces "méthodes". +Dans le protocole HTTP, vous pouvez communiquer avec chaque chemin en utilisant une (ou plusieurs) de ces « méthodes ». --- -En construisant des APIs, vous utilisez généralement ces méthodes HTTP spécifiques pour effectuer une action précise. +En construisant des APIs, vous utilisez normalement ces méthodes HTTP spécifiques pour effectuer une action précise. -Généralement vous utilisez : +En général, vous utilisez : -* `POST` : pour créer de la donnée. -* `GET` : pour lire de la donnée. -* `PUT` : pour mettre à jour de la donnée. -* `DELETE` : pour supprimer de la donnée. +* `POST` : pour créer des données. +* `GET` : pour lire des données. +* `PUT` : pour mettre à jour des données. +* `DELETE` : pour supprimer des données. -Donc, dans **OpenAPI**, chaque méthode HTTP est appelée une "opération". +Donc, dans OpenAPI, chacune des méthodes HTTP est appelée une « opération ». -Nous allons donc aussi appeler ces dernières des "**opérations**". +Nous allons donc aussi les appeler « opérations ». +#### Définir un « décorateur de chemin d’accès » { #define-a-path-operation-decorator } -#### Définir un *décorateur d'opération de chemin* +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} - -Le `@app.get("/")` dit à **FastAPI** que la fonction en dessous est chargée de gérer les requêtes qui vont sur : +Le `@app.get("/")` indique à **FastAPI** que la fonction juste en dessous est chargée de gérer les requêtes qui vont vers : * le chemin `/` -* en utilisant une opération get +* en utilisant une get opération /// info | `@décorateur` Info -Cette syntaxe `@something` en Python est appelée un "décorateur". +Cette syntaxe `@something` en Python est appelée un « décorateur ». -Vous la mettez au dessus d'une fonction. Comme un joli chapeau décoratif (j'imagine que ce terme vient de là 🤷🏻‍♂). +Vous la mettez au-dessus d’une fonction. Comme un joli chapeau décoratif (j’imagine que c’est de là que vient le terme 🤷🏻‍♂). -Un "décorateur" prend la fonction en dessous et en fait quelque chose. +Un « décorateur » prend la fonction en dessous et fait quelque chose avec. -Dans notre cas, ce décorateur dit à **FastAPI** que la fonction en dessous correspond au **chemin** `/` avec l'**opération** `get`. +Dans notre cas, ce décorateur indique à **FastAPI** que la fonction en dessous correspond au **chemin** `/` avec une **opération** `get`. -C'est le "**décorateur d'opération de chemin**". +C’est le « décorateur de chemin d’accès ». /// @@ -269,7 +293,7 @@ Vous pouvez aussi utiliser les autres opérations : * `@app.put()` * `@app.delete()` -Tout comme celles les plus exotiques : +Ainsi que les plus exotiques : * `@app.options()` * `@app.head()` @@ -278,58 +302,79 @@ Tout comme celles les plus exotiques : /// tip | Astuce -Vous êtes libres d'utiliser chaque opération (méthode HTTP) comme vous le désirez. +Vous êtes libre d’utiliser chaque opération (méthode HTTP) comme vous le souhaitez. -**FastAPI** n'impose pas de sens spécifique à chacune d'elle. +**FastAPI** n’impose aucune signification spécifique. -Les informations qui sont présentées ici forment une directive générale, pas des obligations. +Les informations ici sont présentées comme des lignes directrices, pas comme une obligation. -Par exemple, quand l'on utilise **GraphQL**, toutes les actions sont effectuées en utilisant uniquement des opérations `POST`. +Par exemple, lorsque vous utilisez GraphQL, vous effectuez normalement toutes les actions en utilisant uniquement des opérations `POST`. /// -### Étape 4 : définir la **fonction de chemin**. +### Étape 4 : définir la **fonction de chemin d’accès** { #step-4-define-the-path-operation-function } -Voici notre "**fonction de chemin**" (ou fonction d'opération de chemin) : +Voici notre « fonction de chemin d’accès » : * **chemin** : `/`. * **opération** : `get`. -* **fonction** : la fonction sous le "décorateur" (sous `@app.get("/")`). +* **fonction** : la fonction sous le « décorateur » (sous `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} -C'est une fonction Python. +C’est une fonction Python. -Elle sera appelée par **FastAPI** quand une requête sur l'URL `/` sera reçue via une opération `GET`. +Elle sera appelée par **FastAPI** chaque fois qu’il recevra une requête vers l’URL « / » en utilisant une opération `GET`. -Ici, c'est une fonction asynchrone (définie avec `async def`). +Dans ce cas, c’est une fonction `async`. --- -Vous pourriez aussi la définir comme une fonction classique plutôt qu'avec `async def` : +Vous pouvez aussi la définir comme une fonction normale au lieu de `async def` : -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note -Si vous ne connaissez pas la différence, allez voir la section [Concurrence : *"Vous êtes pressés ?"*](../async.md#vous-etes-presses){.internal-link target=_blank}. +Si vous ne connaissez pas la différence, consultez [Asynchrone : « Pressé ? »](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Étape 5 : retourner le contenu +### Étape 5 : retourner le contenu { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -Vous pouvez retourner un dictionnaire (`dict`), une liste (`list`), des valeurs seules comme des chaines de caractères (`str`) et des entiers (`int`), etc. +Vous pouvez retourner un `dict`, une `list`, des valeurs uniques comme `str`, `int`, etc. -Vous pouvez aussi retourner des models **Pydantic** (qui seront détaillés plus tard). +Vous pouvez également retourner des modèles Pydantic (vous en verrez plus à ce sujet plus tard). -Il y a de nombreux autres objets et modèles qui seront automatiquement convertis en JSON. Essayez d'utiliser vos favoris, il est fort probable qu'ils soient déjà supportés. +Il existe de nombreux autres objets et modèles qui seront automatiquement convertis en JSON (y compris des ORM, etc.). Essayez d’utiliser vos favoris, il est fort probable qu’ils soient déjà pris en charge. -## Récapitulatif +### Étape 6 : le déployer { #step-6-deploy-it } + +Déployez votre application sur **FastAPI Cloud** avec une seule commande : `fastapi deploy`. 🎉 + +#### À propos de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** est construit par le même auteur et l’équipe derrière **FastAPI**. + +Il simplifie le processus de **construction**, de **déploiement** et d’**accès** à une API avec un minimum d’effort. + +Il apporte la même **expérience développeur** de création d’applications avec FastAPI au **déploiement** dans le cloud. 🎉 + +FastAPI Cloud est le sponsor principal et le financeur des projets open source *FastAPI and friends*. ✨ + +#### Déployer sur d’autres fournisseurs cloud { #deploy-to-other-cloud-providers } + +FastAPI est open source et basé sur des standards. Vous pouvez déployer des applications FastAPI chez n’importe quel fournisseur cloud de votre choix. + +Suivez les guides de votre fournisseur cloud pour y déployer des applications FastAPI. 🤓 + +## Récapitulatif { #recap } * Importez `FastAPI`. -* Créez une instance d'`app`. -* Ajoutez une **décorateur d'opération de chemin** (tel que `@app.get("/")`). -* Ajoutez une **fonction de chemin** (telle que `def root(): ...` comme ci-dessus). -* Lancez le serveur de développement (avec `uvicorn main:app --reload`). +* Créez une instance `app`. +* Écrivez un **décorateur de chemin d’accès** avec des décorateurs comme `@app.get("/")`. +* Définissez une **fonction de chemin d’accès** ; par exemple, `def root(): ...`. +* Exécutez le serveur de développement avec la commande `fastapi dev`. +* Déployez éventuellement votre application avec `fastapi deploy`. diff --git a/docs/fr/docs/tutorial/index.md b/docs/fr/docs/tutorial/index.md index 83cc5f9e8..0251b9b4b 100644 --- a/docs/fr/docs/tutorial/index.md +++ b/docs/fr/docs/tutorial/index.md @@ -1,29 +1,53 @@ -# Tutoriel - Guide utilisateur - Introduction +# Tutoriel - Guide utilisateur { #tutorial-user-guide } Ce tutoriel vous montre comment utiliser **FastAPI** avec la plupart de ses fonctionnalités, étape par étape. -Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour résoudre vos besoins spécifiques en matière d'API. +Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour répondre à vos besoins spécifiques d'API. -Il est également conçu pour fonctionner comme une référence future. +Il est également conçu pour servir de référence ultérieure, afin que vous puissiez revenir voir exactement ce dont vous avez besoin. -Vous pouvez donc revenir et voir exactement ce dont vous avez besoin. - -## Exécuter le code +## Exécuter le code { #run-the-code } Tous les blocs de code peuvent être copiés et utilisés directement (il s'agit en fait de fichiers Python testés). -Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et commencez `uvicorn` avec : +Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et démarrez `fastapi dev` avec :
```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 @@ L'utiliser dans votre éditeur est ce qui vous montre vraiment les avantages de --- -## Installer FastAPI +## Installer FastAPI { #install-fastapi } La première étape consiste à installer FastAPI. -Pour le tutoriel, vous voudrez peut-être l'installer avec toutes les dépendances et fonctionnalités optionnelles : +Assurez-vous de créer un [environnement virtuel](../virtual-environments.md){.internal-link target=_blank}, de l'activer, puis **d'installer FastAPI** :
```console -$ pip install fastapi[all] +$ pip install "fastapi[standard]" ---> 100% ```
-... qui comprend également `uvicorn`, que vous pouvez utiliser comme serveur pour exécuter votre code. +/// note | Remarque -/// note +Lorsque vous installez avec `pip install "fastapi[standard]"` cela inclut des dépendances standard optionnelles par défaut, y compris `fastapi-cloud-cli`, qui vous permet de déployer sur FastAPI Cloud. -Vous pouvez également l'installer pièce par pièce. +Si vous ne souhaitez pas avoir ces dépendances optionnelles, vous pouvez à la place installer `pip install fastapi`. -C'est ce que vous feriez probablement une fois que vous voudrez déployer votre application en production : - -``` -pip install fastapi -``` - -Installez également `uvicorn` pour qu'il fonctionne comme serveur : - -``` -pip install uvicorn -``` - -Et la même chose pour chacune des dépendances facultatives que vous voulez utiliser. +Si vous souhaitez installer les dépendances standard mais sans `fastapi-cloud-cli`, vous pouvez installer avec `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Guide utilisateur avancé +## Guide d'utilisation avancé { #advanced-user-guide } Il existe également un **Guide d'utilisation avancé** que vous pouvez lire plus tard après ce **Tutoriel - Guide d'utilisation**. diff --git a/docs/fr/docs/tutorial/path-params-numeric-validations.md b/docs/fr/docs/tutorial/path-params-numeric-validations.md index 3f3280e64..c80710777 100644 --- a/docs/fr/docs/tutorial/path-params-numeric-validations.md +++ b/docs/fr/docs/tutorial/path-params-numeric-validations.md @@ -1,8 +1,8 @@ -# Paramètres de chemin et validations numériques +# Paramètres de chemin et validations numériques { #path-parameters-and-numeric-validations } De la même façon que vous pouvez déclarer plus de validations et de métadonnées pour les paramètres de requête avec `Query`, vous pouvez déclarer le même type de validations et de métadonnées pour les paramètres de chemin avec `Path`. -## Importer Path +## Importer `Path` { #import-path } Tout d'abord, importez `Path` de `fastapi`, et importez `Annotated` : @@ -14,11 +14,11 @@ FastAPI a ajouté le support pour `Annotated` (et a commencé à le recommander) Si vous avez une version plus ancienne, vous obtiendrez des erreurs en essayant d'utiliser `Annotated`. -Assurez-vous de [Mettre à jour la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} à la version 0.95.1 à minima avant d'utiliser `Annotated`. +Assurez-vous de [Mettre à niveau la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} à la version 0.95.1 à minima avant d'utiliser `Annotated`. /// -## Déclarer des métadonnées +## Déclarer des métadonnées { #declare-metadata } Vous pouvez déclarer les mêmes paramètres que pour `Query`. @@ -26,15 +26,15 @@ Par exemple, pour déclarer une valeur de métadonnée `title` pour le paramètr {* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} -/// note +/// note | Remarque Un paramètre de chemin est toujours requis car il doit faire partie du chemin. Même si vous l'avez déclaré avec `None` ou défini une valeur par défaut, cela ne changerait rien, il serait toujours requis. /// -## Ordonnez les paramètres comme vous le souhaitez +## Ordonner les paramètres comme vous le souhaitez { #order-the-parameters-as-you-need } -/// tip +/// tip | Astuce Ce n'est probablement pas aussi important ou nécessaire si vous utilisez `Annotated`. @@ -46,7 +46,7 @@ Et vous n'avez pas besoin de déclarer autre chose pour ce paramètre, donc vous Mais vous avez toujours besoin d'utiliser `Path` pour le paramètre de chemin `item_id`. Et vous ne voulez pas utiliser `Annotated` pour une raison quelconque. -Python se plaindra si vous mettez une valeur avec une "défaut" avant une valeur qui n'a pas de "défaut". +Python se plaindra si vous mettez une valeur avec une « valeur par défaut » avant une valeur qui n'a pas de « valeur par défaut ». Mais vous pouvez les réorganiser, et avoir la valeur sans défaut (le paramètre de requête `q`) en premier. @@ -54,15 +54,15 @@ Cela n'a pas d'importance pour **FastAPI**. Il détectera les paramètres par le Ainsi, vous pouvez déclarer votre fonction comme suit : -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} Mais gardez à l'esprit que si vous utilisez `Annotated`, vous n'aurez pas ce problème, cela n'aura pas d'importance car vous n'utilisez pas les valeurs par défaut des paramètres de fonction pour `Query()` ou `Path()`. -{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -## Ordonnez les paramètres comme vous le souhaitez (astuces) +## Ordonner les paramètres comme vous le souhaitez, astuces { #order-the-parameters-as-you-need-tricks } -/// tip +/// tip | Astuce Ce n'est probablement pas aussi important ou nécessaire si vous utilisez `Annotated`. @@ -77,38 +77,29 @@ Si vous voulez : * les avoir dans un ordre différent * ne pas utiliser `Annotated` -...Python a une petite syntaxe spéciale pour cela. +... Python a une petite syntaxe spéciale pour cela. Passez `*`, comme premier paramètre de la fonction. Python ne fera rien avec ce `*`, mais il saura que tous les paramètres suivants doivent être appelés comme arguments "mots-clés" (paires clé-valeur), également connus sous le nom de kwargs. Même s'ils n'ont pas de valeur par défaut. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -# Avec `Annotated` +### Mieux avec `Annotated` { #better-with-annotated } Gardez à l'esprit que si vous utilisez `Annotated`, comme vous n'utilisez pas les valeurs par défaut des paramètres de fonction, vous n'aurez pas ce problème, et vous n'aurez probablement pas besoin d'utiliser `*`. {* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -## Validations numériques : supérieur ou égal +## Validations numériques : supérieur ou égal { #number-validations-greater-than-or-equal } Avec `Query` et `Path` (et d'autres que vous verrez plus tard) vous pouvez déclarer des contraintes numériques. -Ici, avec `ge=1`, `item_id` devra être un nombre entier "`g`reater than or `e`qual" à `1`. +Ici, avec `ge=1`, `item_id` devra être un nombre entier « `g`reater than or `e`qual » à `1`. {* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} -## Validations numériques : supérieur ou égal et inférieur ou égal - -La même chose s'applique pour : - -* `gt` : `g`reater `t`han -* `le` : `l`ess than or `e`qual - -{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} - -## Validations numériques : supérieur et inférieur ou égal +## Validations numériques : supérieur et inférieur ou égal { #number-validations-greater-than-and-less-than-or-equal } La même chose s'applique pour : @@ -117,7 +108,7 @@ La même chose s'applique pour : {* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Validations numériques : flottants, supérieur et inférieur +## Validations numériques : flottants, supérieur et inférieur { #number-validations-floats-greater-than-and-less-than } Les validations numériques fonctionnent également pour les valeurs `float`. @@ -129,7 +120,7 @@ Et la même chose pour lt. {* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Pour résumer +## Pour résumer { #recap } Avec `Query`, `Path` (et d'autres que vous verrez plus tard) vous pouvez déclarer des métadonnées et des validations de chaînes de la même manière qu'avec les [Paramètres de requête et validations de chaînes](query-params-str-validations.md){.internal-link target=_blank}. diff --git a/docs/fr/docs/tutorial/path-params.md b/docs/fr/docs/tutorial/path-params.md index 71c96b18e..3b2955a95 100644 --- a/docs/fr/docs/tutorial/path-params.md +++ b/docs/fr/docs/tutorial/path-params.md @@ -1,205 +1,196 @@ -# Paramètres de chemin +# Paramètres de chemin { #path-parameters } -Vous pouvez déclarer des "paramètres" ou "variables" de chemin avec la même syntaxe que celle utilisée par le -formatage de chaîne Python : +Vous pouvez déclarer des « paramètres » ou « variables » de chemin avec la même syntaxe utilisée par les chaînes de format Python : +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +La valeur du paramètre de chemin `item_id` sera transmise à votre fonction dans l'argument `item_id`. -La valeur du paramètre `item_id` sera transmise à la fonction dans l'argument `item_id`. - -Donc, si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/foo, -vous verrez comme réponse : +Donc, si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/foo, vous verrez comme réponse : ```JSON {"item_id":"foo"} ``` -## Paramètres de chemin typés +## Paramètres de chemin typés { #path-parameters-with-types } -Vous pouvez déclarer le type d'un paramètre de chemin dans la fonction, en utilisant les annotations de type Python : +Vous pouvez déclarer le type d'un paramètre de chemin dans la fonction, en utilisant les annotations de type Python standard : - -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} Ici, `item_id` est déclaré comme `int`. -/// check | vérifier +/// check | Vérifications -Ceci vous permettra d'obtenir des fonctionnalités de l'éditeur dans votre fonction, telles -que des vérifications d'erreur, de l'auto-complétion, etc. +Cela vous apporte la prise en charge par l'éditeur dans votre fonction, avec vérifications d'erreurs, autocomplétion, etc. /// -## Conversion de données +## Conversion de données { #data-conversion } -Si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/3, vous aurez comme réponse : +Si vous exécutez cet exemple et ouvrez votre navigateur sur http://127.0.0.1:8000/items/3, vous verrez comme réponse : ```JSON {"item_id":3} ``` -/// check | vérifier +/// check | Vérifications -Comme vous l'avez remarqué, la valeur reçue par la fonction (et renvoyée ensuite) est `3`, -en tant qu'entier (`int`) Python, pas la chaîne de caractères (`string`) `"3"`. +Remarquez que la valeur reçue par votre fonction (et renvoyée) est `3`, en tant qu'entier (`int`) Python, pas la chaîne de caractères « 3 ». -Grâce aux déclarations de types, **FastAPI** fournit du -"parsing" automatique. +Ainsi, avec cette déclaration de type, **FastAPI** vous fournit automatiquement le « parsing » de la requête. /// -## Validation de données +## Validation de données { #data-validation } -Si vous allez sur http://127.0.0.1:8000/items/foo, vous aurez une belle erreur HTTP : +Mais si vous allez dans le navigateur sur http://127.0.0.1:8000/items/foo, vous verrez une belle erreur 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" + } + ] } ``` -car le paramètre de chemin `item_id` possède comme valeur `"foo"`, qui ne peut pas être convertie en entier (`int`). +car le paramètre de chemin `item_id` a pour valeur « foo », qui n'est pas un `int`. -La même erreur se produira si vous passez un nombre flottant (`float`) et non un entier, comme ici -http://127.0.0.1:8000/items/4.2. +La même erreur apparaîtrait si vous fournissiez un `float` au lieu d'un `int`, comme ici : http://127.0.0.1:8000/items/4.2 +/// check | Vérifications -/// check | vérifier +Ainsi, avec la même déclaration de type Python, **FastAPI** vous fournit la validation de données. -Donc, avec ces mêmes déclarations de type Python, **FastAPI** vous fournit de la validation de données. +Remarquez que l'erreur indique clairement l'endroit exact où la validation n'a pas réussi. -Notez que l'erreur mentionne le point exact où la validation n'a pas réussi. - -Ce qui est incroyablement utile au moment de développer et débugger du code qui interagit avec votre API. +C'est incroyablement utile lors du développement et du débogage du code qui interagit avec votre API. /// -## Documentation +## Documentation { #documentation } -Et quand vous vous rendez sur http://127.0.0.1:8000/docs, vous verrez la -documentation générée automatiquement et interactive : +Et lorsque vous ouvrez votre navigateur sur http://127.0.0.1:8000/docs, vous verrez une documentation d'API automatique et interactive comme : -/// info +/// check | Vérifications -À nouveau, en utilisant uniquement les déclarations de type Python, **FastAPI** vous fournit automatiquement une documentation interactive (via Swagger UI). +À nouveau, simplement avec cette même déclaration de type Python, **FastAPI** vous fournit une documentation interactive automatique (intégrant Swagger UI). -On voit bien dans la documentation que `item_id` est déclaré comme entier. +Remarquez que le paramètre de chemin est déclaré comme entier. /// -## Les avantages d'avoir une documentation basée sur une norme, et la documentation alternative. +## Les avantages d'une norme, documentation alternative { #standards-based-benefits-alternative-documentation } -Le schéma généré suivant la norme OpenAPI, -il existe de nombreux outils compatibles. +Et comme le schéma généré suit la norme OpenAPI, il existe de nombreux outils compatibles. -Grâce à cela, **FastAPI** lui-même fournit une documentation alternative (utilisant ReDoc), qui peut être lue -sur http://127.0.0.1:8000/redoc : +Grâce à cela, **FastAPI** fournit lui-même une documentation d'API alternative (utilisant ReDoc), accessible sur http://127.0.0.1:8000/redoc : -De la même façon, il existe bien d'autres outils compatibles, y compris des outils de génération de code -pour de nombreux langages. +De la même façon, il existe de nombreux outils compatibles, y compris des outils de génération de code pour de nombreux langages. -## Pydantic +## Pydantic { #pydantic } -Toute la validation de données est effectué en arrière-plan avec Pydantic, -dont vous bénéficierez de tous les avantages. Vous savez donc que vous êtes entre de bonnes mains. +Toute la validation de données est effectuée sous le capot par Pydantic, vous en bénéficiez donc pleinement. Vous savez ainsi que vous êtes entre de bonnes mains. -## L'ordre importe +Vous pouvez utiliser les mêmes déclarations de type avec `str`, `float`, `bool` et de nombreux autres types de données complexes. -Quand vous créez des *fonctions de chemins*, vous pouvez vous retrouver dans une situation où vous avez un chemin fixe. +Plusieurs d'entre eux sont explorés dans les prochains chapitres du tutoriel. -Tel que `/users/me`, disons pour récupérer les données sur l'utilisateur actuel. +## L'ordre importe { #order-matters } -Et vous avez un second chemin : `/users/{user_id}` pour récupérer de la donnée sur un utilisateur spécifique grâce à son identifiant d'utilisateur +Quand vous créez des *chemins d'accès*, vous pouvez vous retrouver dans une situation avec un chemin fixe. -Les *fonctions de chemin* étant évaluées dans l'ordre, il faut s'assurer que la fonction correspondant à `/users/me` est déclarée avant celle de `/users/{user_id}` : +Par exemple `/users/me`, disons pour récupérer les données de l'utilisateur actuel. -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +Et vous pouvez aussi avoir un chemin `/users/{user_id}` pour récupérer des données sur un utilisateur spécifique grâce à un identifiant d'utilisateur. -Sinon, le chemin `/users/{user_id}` correspondrait aussi à `/users/me`, la fonction "croyant" qu'elle a reçu un paramètre `user_id` avec pour valeur `"me"`. +Comme les *chemins d'accès* sont évalués dans l'ordre, vous devez vous assurer que le chemin `/users/me` est déclaré avant celui de `/users/{user_id}` : -## Valeurs prédéfinies +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -Si vous avez une *fonction de chemin* qui reçoit un *paramètre de chemin*, mais que vous voulez que les valeurs possibles des paramètres soient prédéfinies, vous pouvez utiliser les `Enum` de Python. +Sinon, le chemin `/users/{user_id}` correspondrait aussi à `/users/me`, « pensant » qu'il reçoit un paramètre `user_id` avec la valeur « me ». -### Création d'un `Enum` +De même, vous ne pouvez pas redéfinir un chemin d'accès : -Importez `Enum` et créez une sous-classe qui hérite de `str` et `Enum`. +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -En héritant de `str` la documentation sera capable de savoir que les valeurs doivent être de type `string` et pourra donc afficher cette `Enum` correctement. +Le premier sera toujours utilisé puisque le chemin correspond en premier. -Créez ensuite des attributs de classe avec des valeurs fixes, qui seront les valeurs autorisées pour cette énumération. +## Valeurs prédéfinies { #predefined-values } -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} +Si vous avez un *chemin d'accès* qui reçoit un *paramètre de chemin*, mais que vous voulez que les valeurs possibles de ce *paramètre de chemin* soient prédéfinies, vous pouvez utiliser une `Enum` Python standard. -/// info +### Créer une classe `Enum` { #create-an-enum-class } -Les énumérations (ou enums) sont disponibles en Python depuis la version 3.4. +Importez `Enum` et créez une sous-classe qui hérite de `str` et de `Enum`. -/// +En héritant de `str`, la documentation de l'API saura que les valeurs doivent être de type `string` et pourra donc s'afficher correctement. + +Créez ensuite des attributs de classe avec des valeurs fixes, qui seront les valeurs valides disponibles : + +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Astuce -Pour ceux qui se demandent, "AlexNet", "ResNet", et "LeNet" sont juste des noms de modèles de Machine Learning. +Si vous vous demandez, « AlexNet », « ResNet » et « LeNet » sont juste des noms de modèles de Machine Learning. /// -### Déclarer un paramètre de chemin +### Déclarer un paramètre de chemin { #declare-a-path-parameter } -Créez ensuite un *paramètre de chemin* avec une annotation de type désignant l'énumération créée précédemment (`ModelName`) : +Créez ensuite un *paramètre de chemin* avec une annotation de type utilisant la classe d'énumération que vous avez créée (`ModelName`) : -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### Documentation +### Consulter la documentation { #check-the-docs } -Les valeurs disponibles pour le *paramètre de chemin* sont bien prédéfinies, la documentation les affiche correctement : +Comme les valeurs disponibles pour le *paramètre de chemin* sont prédéfinies, la documentation interactive peut les afficher clairement : -### Manipuler les *énumérations* Python +### Travailler avec les *énumérations* Python { #working-with-python-enumerations } -La valeur du *paramètre de chemin* sera un des "membres" de l'énumération. +La valeur du *paramètre de chemin* sera un *membre d'énumération*. -#### Comparer les *membres d'énumération* +#### Comparer des *membres d'énumération* { #compare-enumeration-members } -Vous pouvez comparer ce paramètre avec les membres de votre énumération `ModelName` : +Vous pouvez le comparer avec le *membre d'énumération* dans votre enum `ModelName` : -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Récupérer la *valeur de l'énumération* +#### Obtenir la *valeur de l'énumération* { #get-the-enumeration-value } -Vous pouvez obtenir la valeur réel d'un membre (une chaîne de caractères ici), avec `model_name.value`, ou en général, `votre_membre_d'enum.value` : +Vous pouvez obtenir la valeur réelle (une `str` dans ce cas) avec `model_name.value`, ou en général, `votre_membre_d_enum.value` : -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Astuce -Vous pouvez aussi accéder la valeur `"lenet"` avec `ModelName.lenet.value`. +Vous pouvez aussi accéder à la valeur « lenet » avec `ModelName.lenet.value`. /// -#### Retourner des *membres d'énumération* +#### Retourner des *membres d'énumération* { #return-enumeration-members } -Vous pouvez retourner des *membres d'énumération* dans vos *fonctions de chemin*, même imbriquée dans un JSON (e.g. un `dict`). +Vous pouvez retourner des *membres d'énumération* depuis votre *chemin d'accès*, même imbriqués dans un corps JSON (par ex. un `dict`). -Ils seront convertis vers leurs valeurs correspondantes (chaînes de caractères ici) avant d'être transmis au client : +Ils seront convertis vers leurs valeurs correspondantes (des chaînes de caractères ici) avant d'être renvoyés au client : -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -Le client recevra une réponse JSON comme celle-ci : +Dans votre client, vous recevrez une réponse JSON comme : ```JSON { @@ -208,53 +199,53 @@ Le client recevra une réponse JSON comme celle-ci : } ``` -## Paramètres de chemin contenant des chemins +## Paramètres de chemin contenant des chemins { #path-parameters-containing-paths } -Disons que vous avez une *fonction de chemin* liée au chemin `/files/{file_path}`. +Disons que vous avez un *chemin d'accès* avec un chemin `/files/{file_path}`. -Mais que `file_path` lui-même doit contenir un *chemin*, comme `home/johndoe/myfile.txt` par exemple. +Mais vous avez besoin que `file_path` lui-même contienne un *chemin*, comme `home/johndoe/myfile.txt`. -Donc, l'URL pour ce fichier pourrait être : `/files/home/johndoe/myfile.txt`. +Ainsi, l'URL pour ce fichier serait : `/files/home/johndoe/myfile.txt`. -### Support d'OpenAPI +### Support d'OpenAPI { #openapi-support } -OpenAPI ne supporte pas de manière de déclarer un paramètre de chemin contenant un *chemin*, cela pouvant causer des scénarios difficiles à tester et définir. +OpenAPI ne prend pas en charge une manière de déclarer un *paramètre de chemin* contenant un *chemin* à l'intérieur, car cela peut conduire à des scénarios difficiles à tester et à définir. -Néanmoins, cela reste faisable dans **FastAPI**, via les outils internes de Starlette. +Néanmoins, vous pouvez toujours le faire dans **FastAPI**, en utilisant l'un des outils internes de Starlette. -Et la documentation fonctionne quand même, bien qu'aucune section ne soit ajoutée pour dire que la paramètre devrait contenir un *chemin*. +Et la documentation fonctionnera quand même, même si aucune indication supplémentaire ne sera ajoutée pour dire que le paramètre doit contenir un chemin. -### Convertisseur de *chemin* +### Convertisseur de chemin { #path-convertor } -En utilisant une option de Starlette directement, vous pouvez déclarer un *paramètre de chemin* contenant un *chemin* avec une URL comme : +En utilisant une option directement depuis Starlette, vous pouvez déclarer un *paramètre de chemin* contenant un *chemin* avec une URL comme : ``` /files/{file_path:path} ``` -Dans ce cas, le nom du paramètre est `file_path`, et la dernière partie, `:path`, indique à Starlette que le paramètre devrait correspondre à un *chemin*. +Dans ce cas, le nom du paramètre est `file_path`, et la dernière partie, `:path`, indique que le paramètre doit correspondre à n'importe quel *chemin*. -Vous pouvez donc l'utilisez comme tel : +Vous pouvez donc l'utiliser ainsi : -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Astuce -Vous pourriez avoir besoin que le paramètre contienne `/home/johndoe/myfile.txt`, avec un slash au début (`/`). +Vous pourriez avoir besoin que le paramètre contienne `/home/johndoe/myfile.txt`, avec un slash initial (`/`). Dans ce cas, l'URL serait : `/files//home/johndoe/myfile.txt`, avec un double slash (`//`) entre `files` et `home`. /// -## Récapitulatif +## Récapitulatif { #recap } -Avec **FastAPI**, en utilisant les déclarations de type rapides, intuitives et standards de Python, vous bénéficiez de : +Avec **FastAPI**, en utilisant des déclarations de type Python courtes, intuitives et standard, vous obtenez : -* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. -* "Parsing" de données. -* Validation de données. -* Annotations d'API et documentation automatique. +* Support de l'éditeur : vérifications d'erreurs, autocomplétion, etc. +* Données « parsing » +* Validation de données +* Annotations d'API et documentation automatique -Et vous n'avez besoin de le déclarer qu'une fois. +Et vous n'avez besoin de les déclarer qu'une seule fois. -C'est probablement l'avantage visible principal de **FastAPI** comparé aux autres *frameworks* (outre les performances pures). +C'est probablement l'avantage visible principal de **FastAPI** comparé aux autres frameworks (outre les performances pures). diff --git a/docs/fr/docs/tutorial/query-params-str-validations.md b/docs/fr/docs/tutorial/query-params-str-validations.md index c54c0c717..544d10328 100644 --- a/docs/fr/docs/tutorial/query-params-str-validations.md +++ b/docs/fr/docs/tutorial/query-params-str-validations.md @@ -1,166 +1,273 @@ -# Paramètres de requête et validations de chaînes de caractères +# Paramètres de requête et validations de chaînes de caractères { #query-parameters-and-string-validations } -**FastAPI** vous permet de déclarer des informations et des validateurs additionnels pour vos paramètres de requêtes. +**FastAPI** vous permet de déclarer des informations et des validations supplémentaires pour vos paramètres. -Commençons avec cette application pour exemple : +Prenons cette application comme exemple : -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -Le paramètre de requête `q` a pour type `Union[str, None]` (ou `str | None` en Python 3.10), signifiant qu'il est de type `str` mais pourrait aussi être égal à `None`, et bien sûr, la valeur par défaut est `None`, donc **FastAPI** saura qu'il n'est pas requis. +Le paramètre de requête `q` est de type `str | None`, cela signifie qu’il est de type `str` mais peut aussi être `None`, et en effet, la valeur par défaut est `None`, donc FastAPI saura qu’il n’est pas requis. -/// note +/// note | Remarque -**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. +FastAPI saura que la valeur de `q` n’est pas requise grâce à la valeur par défaut `= None`. -Le `Union` dans `Union[str, None]` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. +Avoir `str | None` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. /// -## Validation additionnelle +## Validation additionnelle { #additional-validation } -Nous allons imposer que bien que `q` soit un paramètre optionnel, dès qu'il est fourni, **sa longueur n'excède pas 50 caractères**. +Nous allons imposer que, même si `q` est optionnel, dès qu’il est fourni, **sa longueur n’excède pas 50 caractères**. -## Importer `Query` +### Importer `Query` et `Annotated` { #import-query-and-annotated } -Pour cela, importez d'abord `Query` depuis `fastapi` : +Pour ce faire, importez d’abord : -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +- `Query` depuis `fastapi` +- `Annotated` depuis `typing` -## Utiliser `Query` comme valeur par défaut +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -Construisez ensuite la valeur par défaut de votre paramètre avec `Query`, en choisissant 50 comme `max_length` : +/// info -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI a ajouté la prise en charge de `Annotated` (et a commencé à le recommander) dans la version 0.95.0. -Comme nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous pouvons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, il sert le même objectif qui est de définir cette valeur par défaut. +Si vous avez une version plus ancienne, vous obtiendrez des erreurs en essayant d’utiliser `Annotated`. -Donc : +Assurez-vous de [mettre à niveau la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} vers au moins 0.95.1 avant d’utiliser `Annotated`. + +/// + +## Utiliser `Annotated` dans le type pour le paramètre `q` { #use-annotated-in-the-type-for-the-q-parameter } + +Vous vous souvenez que je vous ai dit plus tôt que `Annotated` peut être utilisé pour ajouter des métadonnées à vos paramètres dans l’[Introduction aux types Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} ? + +C’est le moment de l’utiliser avec FastAPI. 🚀 + +Nous avions cette annotation de type : + +//// tab | Python 3.10+ ```Python -q: Union[str, None] = Query(default=None) +q: str | None = None ``` -... rend le paramètre optionnel, et est donc équivalent à : +//// + +//// tab | Python 3.9+ ```Python q: Union[str, None] = None ``` -Mais déclare explicitement `q` comme étant un paramètre de requête. +//// -/// info +Ce que nous allons faire, c’est l’englober avec `Annotated`, de sorte que cela devienne : -Gardez à l'esprit que la partie la plus importante pour rendre un paramètre optionnel est : +//// tab | Python 3.10+ ```Python -= None +q: Annotated[str | None] = None ``` -ou : +//// + +//// tab | Python 3.9+ ```Python -= Query(None) +q: Annotated[Union[str, None]] = None ``` -et utilisera ce `None` pour détecter que ce paramètre de requête **n'est pas requis**. +//// -Le `Union[str, None]` est uniquement là pour permettre à votre éditeur un meilleur support. +Les deux versions signifient la même chose, `q` est un paramètre qui peut être une `str` ou `None`, et par défaut, c’est `None`. + +Passons maintenant aux choses amusantes. 🎉 + +## Ajouter `Query` à `Annotated` dans le paramètre `q` { #add-query-to-annotated-in-the-q-parameter } + +Maintenant que nous avons cet `Annotated` dans lequel nous pouvons mettre plus d’informations (dans ce cas une validation supplémentaire), ajoutez `Query` à l’intérieur de `Annotated`, et définissez le paramètre `max_length` à `50` : + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Remarquez que la valeur par défaut est toujours `None`, donc le paramètre est toujours optionnel. + +Mais maintenant, avec `Query(max_length=50)` à l’intérieur de `Annotated`, nous indiquons à FastAPI que nous voulons **une validation supplémentaire** pour cette valeur, nous voulons qu’elle ait au maximum 50 caractères. 😎 + +/// tip | Astuce + +Ici nous utilisons `Query()` parce qu’il s’agit d’un **paramètre de requête**. Plus tard nous verrons d’autres comme `Path()`, `Body()`, `Header()` et `Cookie()`, qui acceptent également les mêmes arguments que `Query()`. /// -Ensuite, nous pouvons passer d'autres paramètres à `Query`. Dans cet exemple, le paramètre `max_length` qui s'applique aux chaînes de caractères : +FastAPI va maintenant : -```Python -q: Union[str, None] = Query(default=None, max_length=50) -``` +- **Valider** les données en s’assurant que la longueur maximale est de 50 caractères +- Afficher une **erreur claire** au client quand les données ne sont pas valides +- **Documenter** le paramètre dans la *chemin d'accès* du schéma OpenAPI (il apparaîtra donc dans l’**interface de documentation automatique**) -Cela va valider les données, montrer une erreur claire si ces dernières ne sont pas valides, et documenter le paramètre dans le schéma `OpenAPI` de cette *path operation*. +## Alternative (ancienne) : `Query` comme valeur par défaut { #alternative-old-query-as-the-default-value } -## Rajouter plus de validation +Les versions précédentes de FastAPI (avant 0.95.0) exigeaient d’utiliser `Query` comme valeur par défaut de votre paramètre, au lieu de le mettre dans `Annotated`. Il y a de fortes chances que vous voyiez du code qui l’utilise encore, je vais donc vous l’expliquer. -Vous pouvez aussi rajouter un second paramètre `min_length` : +/// tip | Astuce -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[9] *} - -## Ajouter des validations par expressions régulières - -On peut définir une expression régulière à laquelle le paramètre doit correspondre : - -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[10] *} - -Cette expression régulière vérifie que la valeur passée comme paramètre : - -* `^` : commence avec les caractères qui suivent, avec aucun caractère avant ceux-là. -* `fixedquery` : a pour valeur exacte `fixedquery`. -* `$` : se termine directement ensuite, n'a pas d'autres caractères après `fixedquery`. - -Si vous vous sentez perdu avec le concept d'**expression régulière**, pas d'inquiétudes. Il s'agit d'une notion difficile pour beaucoup, et l'on peut déjà réussir à faire beaucoup sans jamais avoir à les manipuler. - -Mais si vous décidez d'apprendre à les utiliser, sachez qu'ensuite vous pouvez les utiliser directement dans **FastAPI**. - -## Valeurs par défaut - -De la même façon que vous pouvez passer `None` comme premier argument pour l'utiliser comme valeur par défaut, vous pouvez passer d'autres valeurs. - -Disons que vous déclarez le paramètre `q` comme ayant une longueur minimale de `3`, et une valeur par défaut étant `"fixedquery"` : - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note | Rappel - -Avoir une valeur par défaut rend le paramètre optionnel. +Pour du nouveau code et dès que possible, utilisez `Annotated` comme expliqué ci-dessus. Il y a de multiples avantages (expliqués ci-dessous) et aucun inconvénient. 🍰 /// -## Rendre ce paramètre requis +Voici comment vous utiliseriez `Query()` comme valeur par défaut du paramètre de votre fonction, en définissant le paramètre `max_length` à 50 : -Quand on ne déclare ni validation, ni métadonnée, on peut rendre le paramètre `q` requis en ne lui déclarant juste aucune valeur par défaut : +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Comme, dans ce cas (sans utiliser `Annotated`), nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous devons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, cela sert le même objectif de définir cette valeur par défaut (au moins pour FastAPI). + +Donc : + +```Python +q: str | None = Query(default=None) +``` + +... rend le paramètre optionnel, avec une valeur par défaut de `None`, comme : + +```Python +q: str | None = None +``` + +Mais la version avec `Query` le déclare explicitement comme étant un paramètre de requête. + +Ensuite, nous pouvons passer plus de paramètres à `Query`. Dans ce cas, le paramètre `max_length` qui s’applique aux chaînes de caractères : + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Cela validera les données, affichera une erreur claire lorsque les données ne sont pas valides et documentera le paramètre dans la *chemin d'accès* du schéma OpenAPI. + +### `Query` comme valeur par défaut ou dans `Annotated` { #query-as-the-default-value-or-in-annotated } + +Gardez à l’esprit qu’en utilisant `Query` à l’intérieur de `Annotated`, vous ne pouvez pas utiliser le paramètre `default` de `Query`. + +Utilisez à la place la valeur par défaut réelle du paramètre de fonction. Sinon, ce serait incohérent. + +Par exemple, ceci n’est pas autorisé : + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +... parce qu’il n’est pas clair si la valeur par défaut doit être « rick » ou « morty ». + +Donc, vous utiliseriez (de préférence) : + +```Python +q: Annotated[str, Query()] = "rick" +``` + +... ou dans des bases de code plus anciennes, vous trouverez : + +```Python +q: str = Query(default="rick") +``` + +### Avantages de `Annotated` { #advantages-of-annotated } + +**L’utilisation de `Annotated` est recommandée** plutôt que la valeur par défaut dans les paramètres de fonction, c’est **mieux** pour plusieurs raisons. 🤓 + +La valeur **par défaut** du **paramètre de fonction** est la **vraie valeur par défaut**, c’est plus intuitif en Python en général. 😌 + +Vous pouvez **appeler** cette même fonction dans **d’autres endroits** sans FastAPI, et elle **fonctionnera comme prévu**. S’il y a un paramètre **requis** (sans valeur par défaut), votre **éditeur** vous le signalera avec une erreur, **Python** se plaindra aussi si vous l’exécutez sans passer le paramètre requis. + +Quand vous n’utilisez pas `Annotated` et utilisez à la place l’**ancienne** méthode avec la **valeur par défaut**, si vous appelez cette fonction sans FastAPI dans **d’autres endroits**, vous devez **penser** à passer les arguments à la fonction pour qu’elle fonctionne correctement, sinon les valeurs seront différentes de ce que vous attendez (par ex. `QueryInfo` ou quelque chose de similaire au lieu d’une `str`). Et votre éditeur ne se plaindra pas, et Python ne se plaindra pas en exécutant cette fonction, seulement quand les opérations internes échoueront. + +Comme `Annotated` peut avoir plus d’une annotation de métadonnées, vous pouvez maintenant même utiliser la même fonction avec d’autres outils, comme Typer. 🚀 + +## Ajouter plus de validations { #add-more-validations } + +Vous pouvez également ajouter un paramètre `min_length` : + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Ajouter des expressions régulières { #add-regular-expressions } + +Vous pouvez définir un `pattern` d’expression régulière auquel le paramètre doit correspondre : + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Ce pattern d’expression régulière spécifique vérifie que la valeur reçue pour le paramètre : + +- `^` : commence avec les caractères qui suivent, n’a pas de caractères avant. +- `fixedquery` : a exactement la valeur `fixedquery`. +- `$` : se termine là, n’a pas d’autres caractères après `fixedquery`. + +Si vous vous sentez perdu avec toutes ces idées d’**« expression régulière »**, pas d’inquiétude. C’est un sujet difficile pour beaucoup. Vous pouvez déjà faire beaucoup de choses sans avoir besoin d’expressions régulières. + +Désormais, vous savez que, lorsque vous en aurez besoin, vous pourrez les utiliser dans **FastAPI**. + +## Valeurs par défaut { #default-values } + +Vous pouvez, bien sûr, utiliser des valeurs par défaut autres que `None`. + +Disons que vous voulez déclarer le paramètre de requête `q` avec un `min_length` de `3`, et avec une valeur par défaut de « fixedquery » : + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Remarque + +Avoir une valeur par défaut de n’importe quel type, y compris `None`, rend le paramètre optionnel (non requis). + +/// + +## Paramètres requis { #required-parameters } + +Quand nous n’avons pas besoin de déclarer plus de validations ou de métadonnées, nous pouvons rendre le paramètre de requête `q` requis en n’indiquant simplement pas de valeur par défaut, comme : ```Python q: str ``` -à la place de : +au lieu de : ```Python -q: Union[str, None] = None +q: str | None = None ``` -Mais maintenant, on déclare `q` avec `Query`, comme ceci : +Mais maintenant nous le déclarons avec `Query`, par exemple ainsi : ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -Donc pour déclarer une valeur comme requise tout en utilisant `Query`, il faut utiliser `...` comme premier argument : +Donc, lorsque vous avez besoin de déclarer une valeur comme requise tout en utilisant `Query`, vous pouvez simplement ne pas déclarer de valeur par défaut : -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info +### Requis, peut valoir `None` { #required-can-be-none } -Si vous n'avez jamais vu ce `...` auparavant : c'est une des constantes natives de Python appelée "Ellipsis". +Vous pouvez déclarer qu’un paramètre accepte `None`, mais qu’il est tout de même requis. Cela obligerait les clients à envoyer une valeur, même si la valeur est `None`. -/// +Pour ce faire, vous pouvez déclarer que `None` est un type valide tout en ne déclarant pas de valeur par défaut : -Cela indiquera à **FastAPI** que la présence de ce paramètre est obligatoire. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Liste de paramètres / valeurs multiples via Query +## Liste de paramètres de requête / valeurs multiples { #query-parameter-list-multiple-values } -Quand on définit un paramètre de requête explicitement avec `Query` on peut aussi déclarer qu'il reçoit une liste de valeur, ou des "valeurs multiples". +Quand vous définissez un paramètre de requête explicitement avec `Query`, vous pouvez aussi déclarer qu’il reçoit une liste de valeurs, autrement dit, qu’il reçoit des valeurs multiples. -Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans une URL, on écrit : +Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans l’URL, vous pouvez écrire : -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Ce qui fait qu'avec une URL comme : +Ensuite, avec une URL comme : ``` http://localhost:8000/items/?q=foo&q=bar ``` -vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python au sein de votre fonction de **path operation**, dans le paramètre de fonction `q`. +vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python à l’intérieur de votre fonction de *chemin d'accès*, dans le *paramètre de fonction* `q`. -Donc la réponse de cette URL serait : +Donc, la réponse pour cette URL serait : ```JSON { @@ -173,19 +280,19 @@ Donc la réponse de cette URL serait : /// tip | Astuce -Pour déclarer un paramètre de requête de type `list`, comme dans l'exemple ci-dessus, il faut explicitement utiliser `Query`, sinon cela sera interprété comme faisant partie du corps de la requête. +Pour déclarer un paramètre de requête avec un type `list`, comme dans l’exemple ci-dessus, vous devez explicitement utiliser `Query`, sinon il serait interprété comme faisant partie du corps de la requête. /// -La documentation sera donc mise à jour automatiquement pour autoriser plusieurs valeurs : +L’interface de documentation interactive de l’API sera mise à jour en conséquence, pour autoriser plusieurs valeurs : -### Combiner liste de paramètres et valeurs par défaut +### Liste de paramètres de requête / valeurs multiples avec valeurs par défaut { #query-parameter-list-multiple-values-with-defaults } -Et l'on peut aussi définir une liste de valeurs par défaut si aucune n'est fournie : +Vous pouvez également définir une `list` de valeurs par défaut si aucune n’est fournie : -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} Si vous allez à : @@ -193,9 +300,7 @@ Si vous allez à : http://localhost:8000/items/ ``` -la valeur par défaut de `q` sera : `["foo", "bar"]` - -et la réponse sera : +la valeur par défaut de `q` sera : `["foo", "bar"]` et votre réponse sera : ```JSON { @@ -206,93 +311,163 @@ et la réponse sera : } ``` -#### Utiliser `list` +#### Utiliser simplement `list` { #using-just-list } -Il est aussi possible d'utiliser directement `list` plutôt que `List[str]` : +Vous pouvez aussi utiliser `list` directement au lieu 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 +/// note | Remarque -Dans ce cas-là, **FastAPI** ne vérifiera pas le contenu de la liste. +Gardez à l’esprit que dans ce cas, FastAPI ne vérifiera pas le contenu de la liste. -Par exemple, `List[int]` vérifiera (et documentera) que la liste est bien entièrement composée d'entiers. Alors qu'un simple `list` ne ferait pas cette vérification. +Par exemple, `list[int]` vérifierait (et documenterait) que le contenu de la liste est composé d’entiers. Mais un simple `list` ne le ferait pas. /// -## Déclarer des métadonnées supplémentaires +## Déclarer plus de métadonnées { #declare-more-metadata } -On peut aussi ajouter plus d'informations sur le paramètre. +Vous pouvez ajouter plus d’informations à propos du paramètre. -Ces informations seront incluses dans le schéma `OpenAPI` généré et utilisées par la documentation interactive ou les outils externes utilisés. +Ces informations seront incluses dans l’OpenAPI généré et utilisées par les interfaces de documentation et les outils externes. -/// note +/// note | Remarque -Gardez en tête que les outils externes utilisés ne supportent pas forcément tous parfaitement OpenAPI. +Gardez à l’esprit que différents outils peuvent avoir des niveaux de prise en charge d’OpenAPI différents. -Il se peut donc que certains d'entre eux n'utilisent pas toutes les métadonnées que vous avez déclarées pour le moment, bien que dans la plupart des cas, les fonctionnalités manquantes ont prévu d'être implémentées. +Certains d’entre eux pourraient ne pas encore afficher toutes les informations supplémentaires déclarées, bien que, dans la plupart des cas, la fonctionnalité manquante soit déjà prévue au développement. /// Vous pouvez ajouter un `title` : -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} Et une `description` : -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Alias de paramètres +## Paramètres avec alias { #alias-parameters } -Imaginez que vous vouliez que votre paramètre se nomme `item-query`. +Imaginez que vous vouliez que le paramètre soit `item-query`. -Comme dans la requête : +Comme dans : ``` http://127.0.0.1:8000/items/?item-query=foobaritems ``` -Mais `item-query` n'est pas un nom de variable valide en Python. +Mais `item-query` n’est pas un nom de variable Python valide. -Le nom le plus proche serait `item_query`. +Le plus proche serait `item_query`. -Mais vous avez vraiment envie que ce soit exactement `item-query`... +Mais vous avez quand même besoin que ce soit exactement `item-query` ... -Pour cela vous pouvez déclarer un `alias`, et cet alias est ce qui sera utilisé pour trouver la valeur du paramètre : +Vous pouvez alors déclarer un `alias`, et cet alias sera utilisé pour trouver la valeur du paramètre : -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Déprécier des paramètres +## Déprécier des paramètres { #deprecating-parameters } -Disons que vous ne vouliez plus utiliser ce paramètre désormais. +Disons que vous n’aimez plus ce paramètre. -Il faut qu'il continue à exister pendant un certain temps car vos clients l'utilisent, mais vous voulez que la documentation mentionne clairement que ce paramètre est déprécié. +Vous devez le laisser là quelque temps car des clients l’utilisent, mais vous voulez que les documents l’affichent clairement comme déprécié. -On utilise alors l'argument `deprecated=True` de `Query` : +Passez alors le paramètre `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] *} -La documentation le présentera comme il suit : +Les documents l’afficheront ainsi : -## Pour résumer +## Exclure des paramètres d’OpenAPI { #exclude-parameters-from-openapi } -Il est possible d'ajouter des validateurs et métadonnées pour vos paramètres. +Pour exclure un paramètre de requête du schéma OpenAPI généré (et donc, des systèmes de documentation automatiques), définissez le paramètre `include_in_schema` de `Query` à `False` : -Validateurs et métadonnées génériques: +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} -* `alias` -* `title` -* `description` -* `deprecated` +## Validation personnalisée { #custom-validation } -Validateurs spécifiques aux chaînes de caractères : +Il peut y avoir des cas où vous devez faire une **validation personnalisée** qui ne peut pas être réalisée avec les paramètres montrés ci-dessus. -* `min_length` -* `max_length` -* `regex` +Dans ces cas, vous pouvez utiliser une **fonction de validation personnalisée** qui est appliquée après la validation normale (par ex. après avoir validé que la valeur est une `str`). -Parmi ces exemples, vous avez pu voir comment déclarer des validateurs pour les chaînes de caractères. +Vous pouvez y parvenir en utilisant `AfterValidator` de Pydantic à l’intérieur de `Annotated`. -Dans les prochains chapitres, vous verrez comment déclarer des validateurs pour d'autres types, comme les nombres. +/// tip | Astuce + +Pydantic a aussi `BeforeValidator` et d’autres. 🤓 + +/// + +Par exemple, ce validateur personnalisé vérifie que l’ID d’item commence par `isbn-` pour un numéro de livre ISBN ou par `imdb-` pour un ID d’URL de film IMDB : + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info + +C’est disponible avec Pydantic version 2 ou supérieure. 😎 + +/// + +/// tip | Astuce + +Si vous devez faire un type de validation qui nécessite de communiquer avec un **composant externe**, comme une base de données ou une autre API, vous devez plutôt utiliser les **Dépendances de FastAPI**, vous en apprendrez davantage plus tard. + +Ces validateurs personnalisés sont destinés aux éléments qui peuvent être vérifiés **uniquement** avec les **mêmes données** fournies dans la requête. + +/// + +### Comprendre ce code { #understand-that-code } + +Le point important est simplement d’utiliser **`AfterValidator` avec une fonction à l’intérieur de `Annotated`**. N’hésitez pas à passer cette partie. 🤸 + +--- + +Mais si vous êtes curieux de cet exemple de code spécifique et que vous êtes toujours partant, voici quelques détails supplémentaires. + +#### Chaîne avec `value.startswith()` { #string-with-value-startswith } + +Avez-vous remarqué ? Une chaîne utilisant `value.startswith()` peut prendre un tuple, et elle vérifiera chaque valeur du tuple : + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Un élément aléatoire { #a-random-item } + +Avec `data.items()` nous obtenons un objet itérable avec des tuples contenant la clé et la valeur pour chaque élément du dictionnaire. + +Nous convertissons cet objet itérable en une `list` propre avec `list(data.items())`. + +Ensuite, avec `random.choice()` nous pouvons obtenir une **valeur aléatoire** depuis la liste, nous obtenons donc un tuple `(id, name)`. Ce sera quelque chose comme `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Puis nous **affectons ces deux valeurs** du tuple aux variables `id` et `name`. + +Ainsi, si l’utilisateur n’a pas fourni d’ID d’item, il recevra quand même une suggestion aléatoire. + +... nous faisons tout cela en **une seule ligne simple**. 🤯 Vous n’adorez pas Python ? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Récapitulatif { #recap } + +Vous pouvez déclarer des validations et des métadonnées supplémentaires pour vos paramètres. + +Validations et métadonnées génériques : + +- `alias` +- `title` +- `description` +- `deprecated` + +Validations spécifiques aux chaînes : + +- `min_length` +- `max_length` +- `pattern` + +Validations personnalisées avec `AfterValidator`. + +Dans ces exemples, vous avez vu comment déclarer des validations pour des valeurs `str`. + +Voyez les prochains chapitres pour apprendre à déclarer des validations pour d’autres types, comme les nombres. diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md index b87c26c78..1a4880ced 100644 --- a/docs/fr/docs/tutorial/query-params.md +++ b/docs/fr/docs/tutorial/query-params.md @@ -1,10 +1,10 @@ -# Paramètres de requête +# Paramètres de requête { #query-parameters } -Quand vous déclarez des paramètres dans votre fonction de chemin qui ne font pas partie des paramètres indiqués dans le chemin associé, ces paramètres sont automatiquement considérés comme des paramètres de "requête". +Quand vous déclarez d'autres paramètres de fonction qui ne font pas partie des paramètres de chemin, ils sont automatiquement interprétés comme des paramètres de « query ». -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} -La partie appelée requête (ou **query**) dans une URL est l'ensemble des paires clés-valeurs placées après le `?` , séparées par des `&`. +La query est l'ensemble des paires clé-valeur placées après le `?` dans une URL, séparées par des caractères `&`. Par exemple, dans l'URL : @@ -12,27 +12,27 @@ Par exemple, dans l'URL : http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -...les paramètres de requête sont : +... les paramètres de requête sont : -* `skip` : avec une valeur de`0` +* `skip` : avec une valeur de `0` * `limit` : avec une valeur de `10` -Faisant partie de l'URL, ces valeurs sont des chaînes de caractères (`str`). +Comme ils font partie de l'URL, ce sont « naturellement » des chaînes de caractères. -Mais quand on les déclare avec des types Python (dans l'exemple précédent, en tant qu'`int`), elles sont converties dans les types renseignés. +Mais lorsque vous les déclarez avec des types Python (dans l'exemple ci-dessus, en tant que `int`), ils sont convertis vers ce type et validés par rapport à celui-ci. -Toutes les fonctionnalités qui s'appliquent aux paramètres de chemin s'appliquent aussi aux paramètres de requête : +Tous les mêmes processus qui s'appliquaient aux paramètres de chemin s'appliquent aussi aux paramètres de requête : -* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. -* "Parsing" de données. -* Validation de données. -* Annotations d'API et documentation automatique. +* Prise en charge de l'éditeur (évidemment) +* « parsing » des données +* Validation des données +* Documentation automatique -## Valeurs par défaut +## Valeurs par défaut { #defaults } -Les paramètres de requête ne sont pas une partie fixe d'un chemin, ils peuvent être optionnels et avoir des valeurs par défaut. +Comme les paramètres de requête ne sont pas une partie fixe d'un chemin, ils peuvent être optionnels et avoir des valeurs par défaut. -Dans l'exemple ci-dessus, ils ont des valeurs par défaut qui sont `skip=0` et `limit=10`. +Dans l'exemple ci-dessus, ils ont des valeurs par défaut `skip=0` et `limit=10`. Donc, accéder à l'URL : @@ -40,52 +40,44 @@ Donc, accéder à l'URL : http://127.0.0.1:8000/items/ ``` -serait équivalent à accéder à l'URL : +serait équivalent à accéder à : ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -Mais si vous accédez à, par exemple : +Mais si vous accédez, par exemple, à : ``` http://127.0.0.1:8000/items/?skip=20 ``` -Les valeurs des paramètres de votre fonction seront : +Les valeurs des paramètres dans votre fonction seront : -* `skip=20` : car c'est la valeur déclarée dans l'URL. -* `limit=10` : car `limit` n'a pas été déclaré dans l'URL, et que la valeur par défaut était `10`. +* `skip=20` : car vous l'avez défini dans l'URL +* `limit=10` : car c'était la valeur par défaut -## Paramètres optionnels +## Paramètres optionnels { #optional-parameters } -De la même façon, vous pouvez définir des paramètres de requête comme optionnels, en leur donnant comme valeur par défaut `None` : +De la même façon, vous pouvez déclarer des paramètres de requête optionnels, en définissant leur valeur par défaut à `None` : -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} -Ici, le paramètre `q` sera optionnel, et aura `None` comme valeur par défaut. +Dans ce cas, le paramètre de fonction `q` sera optionnel et vaudra `None` par défaut. -/// check | Remarque +/// check | Vérifications -On peut voir que **FastAPI** est capable de détecter que le paramètre de chemin `item_id` est un paramètre de chemin et que `q` n'en est pas un, c'est donc un paramètre de requête. +Notez également que FastAPI est suffisamment intelligent pour remarquer que le paramètre de chemin `item_id` est un paramètre de chemin et que `q` ne l'est pas, c'est donc un paramètre de requête. /// -/// note +## Conversion des types des paramètres de requête { #query-parameter-type-conversion } -**FastAPI** saura que `q` est optionnel grâce au `=None`. +Vous pouvez aussi déclarer des types `bool`, ils seront convertis : -Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre éditeur de texte pour détecter des erreurs dans votre code. +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} -/// - -## Conversion des types des paramètres de requête - -Vous pouvez aussi déclarer des paramètres de requête comme booléens (`bool`), **FastAPI** les convertira : - -{* ../../docs_src/query_params/tutorial003.py hl[9] *} - -Avec ce code, en allant sur : +Dans ce cas, si vous allez sur : ``` http://127.0.0.1:8000/items/foo?short=1 @@ -115,60 +107,61 @@ ou http://127.0.0.1:8000/items/foo?short=yes ``` -ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la première lettre en majuscule, etc.), votre fonction considérera le paramètre `short` comme ayant une valeur booléenne à `True`. Sinon la valeur sera à `False`. +ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la première lettre en majuscule, etc.), votre fonction verra le paramètre `short` avec une valeur `bool` à `True`. Sinon la valeur sera à `False`. -## Multiples paramètres de chemin et de requête +## Multiples paramètres de chemin et de requête { #multiple-path-and-query-parameters } -Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête dans la même fonction, **FastAPI** saura comment les gérer. +Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête en même temps, FastAPI sait lequel est lequel. Et vous n'avez pas besoin de les déclarer dans un ordre spécifique. -Ils seront détectés par leurs noms : +Ils seront détectés par leur nom : -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Paramètres de requête requis +## Paramètres de requête requis { #required-query-parameters } -Quand vous déclarez une valeur par défaut pour un paramètre qui n'est pas un paramètre de chemin (actuellement, nous n'avons vu que les paramètres de requête), alors ce paramètre n'est pas requis. +Quand vous déclarez une valeur par défaut pour des paramètres qui ne sont pas des paramètres de chemin (pour l'instant, nous n'avons vu que les paramètres de requête), alors ils ne sont pas requis. -Si vous ne voulez pas leur donner de valeur par défaut mais juste les rendre optionnels, utilisez `None` comme valeur par défaut. +Si vous ne voulez pas leur donner de valeur spécifique mais simplement les rendre optionnels, définissez la valeur par défaut à `None`. -Mais si vous voulez rendre un paramètre de requête obligatoire, vous pouvez juste ne pas y affecter de valeur par défaut : +Mais si vous voulez rendre un paramètre de requête obligatoire, vous pouvez simplement ne déclarer aucune valeur par défaut : -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -Ici le paramètre `needy` est un paramètre requis (ou obligatoire) de type `str`. +Ici, le paramètre de requête `needy` est un paramètre de requête requis de type `str`. -Si vous ouvrez une URL comme : +Si vous ouvrez dans votre navigateur une URL comme : ``` http://127.0.0.1:8000/items/foo-item ``` -...sans ajouter le paramètre requis `needy`, vous aurez une erreur : +... sans ajouter le paramètre requis `needy`, vous verrez une erreur comme : ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` -La présence de `needy` étant nécessaire, vous auriez besoin de l'insérer dans l'URL : +Comme `needy` est un paramètre requis, vous devez le définir dans l'URL : ``` http://127.0.0.1:8000/items/foo-item?needy=sooooneedy ``` -...ce qui fonctionnerait : +... cela fonctionnerait : ```JSON { @@ -177,18 +170,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` -Et bien sur, vous pouvez définir certains paramètres comme requis, certains avec des valeurs par défaut et certains entièrement optionnels : +Et bien sûr, vous pouvez définir certains paramètres comme requis, certains avec une valeur par défaut et certains entièrement optionnels : -{* ../../docs_src/query_params/tutorial006.py hl[10] *} +{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} -Ici, on a donc 3 paramètres de requête : +Dans ce cas, il y a 3 paramètres de requête : -* `needy`, requis et de type `str`. -* `skip`, un `int` avec comme valeur par défaut `0`. +* `needy`, un `str` requis. +* `skip`, un `int` avec une valeur par défaut de `0`. * `limit`, un `int` optionnel. /// tip | Astuce -Vous pouvez utiliser les `Enum`s de la même façon qu'avec les [Paramètres de chemin](path-params.md#valeurs-predefinies){.internal-link target=_blank}. +Vous pourriez aussi utiliser des `Enum`s de la même façon qu'avec les [Paramètres de chemin](path-params.md#predefined-values){.internal-link target=_blank}. /// diff --git a/docs/ja/docs/advanced/additional-status-codes.md b/docs/ja/docs/advanced/additional-status-codes.md index 33457f591..14b7e8ba8 100644 --- a/docs/ja/docs/advanced/additional-status-codes.md +++ b/docs/ja/docs/advanced/additional-status-codes.md @@ -1,41 +1,41 @@ -# 追加のステータスコード +# 追加のステータスコード { #additional-status-codes } -デフォルトでは、 **FastAPI** は `JSONResponse` を使ってレスポンスを返します。その `JSONResponse` の中には、 *path operation* が返した内容が入ります。 +デフォルトでは、 **FastAPI** は `JSONResponse` を使ってレスポンスを返し、*path operation* から返した内容をその `JSONResponse` の中に入れます。 -それは、デフォルトのステータスコードか、 *path operation* でセットしたものを利用します。 +デフォルトのステータスコード、または *path operation* で設定したステータスコードが使用されます。 -## 追加のステータスコード +## 追加のステータスコード { #additional-status-codes_1 } -メインのステータスコードとは別に、他のステータスコードを返したい場合は、`Response` (`JSONResponse` など) に追加のステータスコードを設定して直接返します。 +メインのステータスコードとは別に追加のステータスコードを返したい場合は、`JSONResponse` のような `Response` を直接返し、追加のステータスコードを直接設定できます。 -例えば、itemを更新し、成功した場合は200 "OK"のHTTPステータスコードを返す *path operation* を作りたいとします。 +たとえば、item を更新でき、成功時に HTTP ステータスコード 200 "OK" を返す *path operation* を作りたいとします。 -しかし、新しいitemも許可したいです。itemが存在しない場合は、それらを作成して201 "Created"を返します。 +しかし、新しい item も受け付けたいとします。そして、item が以前存在しなかった場合には作成し、HTTP ステータスコード 201「Created」を返します。 -これを達成するには、 `JSONResponse` をインポートし、 `status_code` を設定して直接内容を返します。 +これを実現するには、`JSONResponse` をインポートし、望む `status_code` を設定して、そこで内容を直接返します。 -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} /// warning | 注意 -上記の例のように `Response` を明示的に返す場合、それは直接返されます。 +上の例のように `Response` を直接返すと、それはそのまま返されます。 -モデルなどはシリアライズされません。 +モデルなどによってシリアライズされません。 -必要なデータが含まれていることや、値が有効なJSONであること (`JSONResponse` を使う場合) を確認してください。 +必要なデータが含まれていること、そして(`JSONResponse` を使用している場合)値が有効な JSON であることを確認してください。 /// /// note | 技術詳細 -`from starlette.responses import JSONResponse` を利用することもできます。 +`from starlette.responses import JSONResponse` を使うこともできます。 -**FastAPI** は `fastapi.responses` と同じ `starlette.responses` を、開発者の利便性のために提供しています。しかし有効なレスポンスはほとんどStarletteから来ています。 `status` についても同じです。 +**FastAPI** は開発者の利便性のために、`fastapi.responses` と同じ `starlette.responses` を提供しています。しかし、利用可能なレスポンスのほとんどは Starlette から直接提供されています。`status` も同様です。 /// -## OpenAPIとAPIドキュメント +## OpenAPI と API ドキュメント { #openapi-and-api-docs } -ステータスコードとレスポンスを直接返す場合、それらはOpenAPIスキーマ (APIドキュメント) には含まれません。なぜなら、FastAPIは何が返されるのか事前に知ることができないからです。 +追加のステータスコードとレスポンスを直接返す場合、それらは OpenAPI スキーマ(API ドキュメント)には含まれません。FastAPI には、事前に何が返されるかを知る方法がないからです。 -しかし、 [Additional Responses](additional-responses.md){.internal-link target=_blank} を使ってコードの中にドキュメントを書くことができます。 +しかし、[Additional Responses](additional-responses.md){.internal-link target=_blank} を使ってコード内にドキュメント化できます。 diff --git a/docs/ja/docs/advanced/custom-response.md b/docs/ja/docs/advanced/custom-response.md index 1b2cd914d..9d881c013 100644 --- a/docs/ja/docs/advanced/custom-response.md +++ b/docs/ja/docs/advanced/custom-response.md @@ -1,34 +1,40 @@ -# カスタムレスポンス - HTML、ストリーム、ファイル、その他のレスポンス +# カスタムレスポンス - HTML、ストリーム、ファイル、その他のレスポンス { #custom-response-html-stream-file-others } デフォルトでは、**FastAPI** は `JSONResponse` を使ってレスポンスを返します。 [レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、 `Response` を直接返すことでこの挙動をオーバーライドできます。 -しかし、`Response` を直接返すと、データは自動的に変換されず、ドキュメントも自動生成されません (例えば、生成されるOpenAPIの一部としてHTTPヘッダー `Content-Type` に特定の「メディアタイプ」を含めるなど) 。 +しかし、`Response` を直接返すと(または `JSONResponse` のような任意のサブクラスを返すと)、データは自動的に変換されず(`response_model` を宣言していても)、ドキュメントも自動生成されません(例えば、生成されるOpenAPIの一部としてHTTPヘッダー `Content-Type` に特定の「メディアタイプ」を含めるなど)。 -しかし、*path operationデコレータ* に、使いたい `Response` を宣言することもできます。 +`response_class` パラメータを使用して、*path operation デコレータ* で使用したい `Response`(任意の `Response` サブクラス)を宣言することもできます。 -*path operation関数* から返されるコンテンツは、その `Response` に含まれます。 +*path operation 関数* から返されるコンテンツは、その `Response` に含まれます。 -そしてもし、`Response` が、`JSONResponse` や `UJSONResponse` の場合のようにJSONメディアタイプ (`application/json`) ならば、データは *path operationデコレータ* に宣言したPydantic `response_model` により自動的に変換 (もしくはフィルタ) されます。 +そしてその `Response` が、`JSONResponse` や `UJSONResponse` の場合のようにJSONメディアタイプ(`application/json`)なら、関数の返り値は *path operationデコレータ* に宣言した任意のPydantic `response_model` により自動的に変換(およびフィルタ)されます。 /// note | 備考 -メディアタイプを指定せずにレスポンスクラスを利用すると、FastAPIは何もコンテンツがないことを期待します。そのため、生成されるOpenAPIドキュメントにレスポンスフォーマットが記載されません。 +メディアタイプを指定せずにレスポンスクラスを利用すると、FastAPIはレスポンスにコンテンツがないことを期待します。そのため、生成されるOpenAPIドキュメントにレスポンスフォーマットが記載されません。 /// -## `ORJSONResponse` を使う +## `ORJSONResponse` を使う { #use-orjsonresponse } -例えば、パフォーマンスを出したい場合は、`orjson`をインストールし、`ORJSONResponse`をレスポンスとしてセットすることができます。 +例えば、パフォーマンスを絞り出したい場合は、`orjson`をインストールし、レスポンスとして `ORJSONResponse` をセットできます。 -使いたい `Response` クラス (サブクラス) をインポートし、 *path operationデコレータ* に宣言します。 +使いたい `Response` クラス(サブクラス)をインポートし、*path operationデコレータ* に宣言します。 -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +大きなレスポンスの場合、`Response` を直接返すほうが、辞書を返すよりもはるかに高速です。 + +これは、デフォルトではFastAPIがチュートリアルで説明した同じ[JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}を使って、内部の各アイテムを検査し、JSONとしてシリアライズ可能であることを確認するためです。これにより、例えばデータベースモデルのような**任意のオブジェクト**を返せます。 + +しかし、返そうとしているコンテンツが **JSONでシリアライズ可能**であることが確実なら、それを直接レスポンスクラスに渡して、FastAPIがレスポンスクラスへ渡す前に返却コンテンツを `jsonable_encoder` に通すことで発生する追加のオーバーヘッドを回避できます。 + +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | 情報 -パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するために利用することもできます。 +パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するためにも利用されます。 この場合、HTTPヘッダー `Content-Type` には `application/json` がセットされます。 @@ -38,70 +44,70 @@ /// tip | 豆知識 -`ORJSONResponse` は、現在はFastAPIのみで利用可能で、Starletteでは利用できません。 +`ORJSONResponse` はFastAPIでのみ利用可能で、Starletteでは利用できません。 /// -## HTMLレスポンス +## HTMLレスポンス { #html-response } **FastAPI** からHTMLを直接返す場合は、`HTMLResponse` を使います。 * `HTMLResponse` をインポートする。 -* *path operation* のパラメータ `content_type` に `HTMLResponse` を渡す。 +* *path operation デコレータ* のパラメータ `response_class` に `HTMLResponse` を渡す。 -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | 情報 -パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するために利用されます。 +パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するためにも利用されます。 この場合、HTTPヘッダー `Content-Type` には `text/html` がセットされます。 -そして、OpenAPIにはそのようにドキュメント化されます。 +そして、OpenAPIにはそのようにドキュメントされます。 /// -### `Response` を返す +### `Response` を返す { #return-a-response } -[レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、レスポンスを直接返すことで、*path operation* の中でレスポンスをオーバーライドできます。 +[レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、レスポンスを返すことで、*path operation* の中でレスポンスを直接オーバーライドすることもできます。 上記と同じ例において、 `HTMLResponse` を返すと、このようになります: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | 注意 -*path operation関数* から直接返される `Response` は、OpenAPIにドキュメントされず (例えば、 `Content-Type` がドキュメントされない) 、自動的な対話的ドキュメントからも閲覧できません。 +*path operation関数* から直接返される `Response` は、OpenAPIにドキュメントされず(例えば、`Content-Type` がドキュメントされない)、自動的な対話的ドキュメントでも表示されません。 /// /// info | 情報 -もちろん、実際の `Content-Type` ヘッダーやステータスコードなどは、返された `Response` オブジェクトに由来しています。 +もちろん、実際の `Content-Type` ヘッダーやステータスコードなどは、返された `Response` オブジェクトに由来します。 /// -### OpenAPIドキュメントと `Response` のオーバーライド +### OpenAPIドキュメントと `Response` のオーバーライド { #document-in-openapi-and-override-response } -関数の中でレスポンスをオーバーライドしつつも、OpenAPI に「メディアタイプ」をドキュメント化したいなら、 `response_class` パラメータを使い、 `Response` オブジェクトを返します。 +関数の中でレスポンスをオーバーライドしつつも、OpenAPI に「メディアタイプ」をドキュメント化したいなら、`response_class` パラメータを使用し、かつ `Response` オブジェクトを返します。 -`response_class` はOpenAPIの *path operation* ドキュメントにのみ使用されますが、 `Response` はそのまま使用されます。 +`response_class` はOpenAPIの*path operation*のドキュメント化のためにのみ使用され、`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()` は、`str` のHTMLを返すのではなく `Response` を生成して返しています。 +この例では、関数 `generate_html_response()` は、`str` のHTMLを返すのではなく、`Response` を生成して返しています。 -`generate_html_response()` を呼び出した結果を返すことにより、**FastAPI** の振る舞いを上書きする `Response` が既に返されています。 +`generate_html_response()` を呼び出した結果を返すことにより、デフォルトの **FastAPI** の挙動をオーバーライドする `Response` をすでに返しています。 -しかし、一方では `response_class` に `HTMLResponse` を渡しているため、 **FastAPI** はOpenAPIや対話的ドキュメントでHTMLとして `text/html` でドキュメント化する方法を知っています。 +しかし、`response_class` にも `HTMLResponse` を渡しているため、**FastAPI** はOpenAPIと対話的ドキュメントで、`text/html` のHTMLとしてどのようにドキュメント化すればよいかを理解できます: -## 利用可能なレスポンス +## 利用可能なレスポンス { #available-responses } 以下が利用可能なレスポンスの一部です。 @@ -111,11 +117,11 @@ `from starlette.responses import HTMLResponse` も利用できます。 -**FastAPI** は開発者の利便性のために `fastapi.responses` として `starlette.responses` と同じものを提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 +**FastAPI** は開発者の利便性のために、`starlette.responses` と同じものを `fastapi.responses` として提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 /// -### `Response` +### `Response` { #response } メインの `Response` クラスで、他の全てのレスポンスはこれを継承しています。 @@ -128,41 +134,53 @@ * `headers` - 文字列の `dict` 。 * `media_type` - メディアタイプを示す `str` 。例えば `"text/html"` 。 -FastAPI (実際にはStarlette) は自動的にContent-Lengthヘッダーを含みます。また、media_typeに基づいたContent-Typeヘッダーを含み、テキストタイプのためにcharsetを追加します。 +FastAPI(実際にはStarlette)は自動的にContent-Lengthヘッダーを含みます。また、`media_type` に基づいたContent-Typeヘッダーを含み、テキストタイプのためにcharsetを追加します。 -{* ../../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` としてエンコードされたレスポンスを返します。 +データを受け取り、`application/json` としてエンコードされたレスポンスを返します。 上で読んだように、**FastAPI** のデフォルトのレスポンスとして利用されます。 -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } 上で読んだように、`orjson`を使った、高速な代替のJSONレスポンスです。 -### `UJSONResponse` +/// info | 情報 -`ujson`を使った、代替のJSONレスポンスです。 - -/// warning | 注意 - -`ujson` は、いくつかのエッジケースの取り扱いについて、Pythonにビルトインされた実装よりも作りこまれていません。 +これは、例えば `pip install orjson` で `orjson` をインストールする必要があります。 /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +### `UJSONResponse` { #ujsonresponse } + +`ujson`を使った、代替のJSONレスポンスです。 + +/// info | 情報 + +これは、例えば `pip install ujson` で `ujson` をインストールする必要があります。 + +/// + +/// warning | 注意 + +`ujson` は、いくつかのエッジケースの取り扱いについて、Pythonにビルトインされた実装ほど注意深くありません。 + +/// + +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | 豆知識 @@ -170,33 +188,61 @@ FastAPI (実際にはStarlette) は自動的にContent-Lengthヘッダーを含 /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } -HTTPリダイレクトを返します。デフォルトでは307ステータスコード (Temporary Redirect) となります。 +HTTPリダイレクトを返します。デフォルトでは307ステータスコード(Temporary Redirect)となります。 -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +`RedirectResponse` を直接返せます: -### `StreamingResponse` +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} -非同期なジェネレータか通常のジェネレータ・イテレータを受け取り、レスポンスボディをストリームします。 +--- -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +または、`response_class` パラメータで使用できます: -#### `StreamingResponse` をファイルライクなオブジェクトとともに使う +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} -ファイルライクなオブジェクト (例えば、 `open()` で返されたオブジェクト) がある場合、 `StreamingResponse` に含めて返すことができます。 +その場合、*path operation*関数からURLを直接返せます。 -これにはクラウドストレージとの連携や映像処理など、多くのライブラリが含まれています。 +この場合に使用される `status_code` は `RedirectResponse` のデフォルトである `307` になります。 -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +--- + +また、`status_code` パラメータを `response_class` パラメータと組み合わせて使うこともできます: + +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} + +### `StreamingResponse` { #streamingresponse } + +非同期ジェネレータ、または通常のジェネレータ/イテレータを受け取り、レスポンスボディをストリームします。 + +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} + +#### ファイルライクオブジェクトで `StreamingResponse` を使う { #using-streamingresponse-with-file-like-objects } + +file-like オブジェクト(例: `open()` で返されるオブジェクト)がある場合、そのfile-likeオブジェクトを反復処理するジェネレータ関数を作れます。 + +そうすれば、最初にすべてをメモリへ読み込む必要はなく、そのジェネレータ関数を `StreamingResponse` に渡して返せます。 + +これにはクラウドストレージとの連携、映像処理など、多くのライブラリが含まれます。 + +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} + +1. これはジェネレータ関数です。内部に `yield` 文を含むため「ジェネレータ関数」です。 +2. `with` ブロックを使うことで、ジェネレータ関数が終わった後(つまりレスポンスの送信が完了した後)にfile-likeオブジェクトが確実にクローズされるようにします。 +3. この `yield from` は、`file_like` という名前のものを反復処理するように関数へ指示します。そして反復された各パートについて、そのパートをこのジェネレータ関数(`iterfile`)から来たものとして `yield` します。 + + つまり、内部的に「生成」の作業を別のものへ移譲するジェネレータ関数です。 + + このようにすることで `with` ブロックに入れられ、完了後にfile-likeオブジェクトが確実にクローズされます。 /// tip | 豆知識 -ここでは `async` や `await` をサポートしていない標準の `open()` を使っているので、通常の `def` でpath operationを宣言していることに注意してください。 +ここでは `async` と `await` をサポートしていない標準の `open()` を使っているため、通常の `def` でpath operationを宣言している点に注意してください。 /// -### `FileResponse` +### `FileResponse` { #fileresponse } レスポンスとしてファイルを非同期的にストリームします。 @@ -204,29 +250,63 @@ HTTPリダイレクトを返します。デフォルトでは307ステータス * `path` - ストリームするファイルのファイルパス。 * `headers` - 含めたい任意のカスタムヘッダーの辞書。 -* `media_type` - メディアタイプを示す文字列。セットされなかった場合は、ファイル名やパスからメディアタイプが推察されます。 -* `filename` - セットされた場合、レスポンスの `Content-Disposition` に含まれます。 +* `media_type` - メディアタイプを示す文字列。未設定の場合、ファイル名やパスからメディアタイプが推測されます。 +* `filename` - 設定した場合、レスポンスの `Content-Disposition` に含まれます。 -ファイルレスポンスには、適切な `Content-Length` 、 `Last-Modified` 、 `ETag` ヘッダーが含まれます。 +ファイルレスポンスには、適切な `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` パラメータを使うこともできます: -**FastAPI** クラスのインスタンスか `APIRouter` を生成するときに、デフォルトのレスポンスクラスを指定できます。 +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} -定義するためのパラメータは、 `default_response_class` です。 +この場合、*path operation*関数からファイルパスを直接返せます。 -以下の例では、 **FastAPI** は、全ての *path operation* で `JSONResponse` の代わりに `ORJSONResponse` をデフォルトとして利用します。 +## カスタムレスポンスクラス { #custom-response-class } -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +`Response` を継承した独自のカスタムレスポンスクラスを作成して利用できます。 + +例えば、`orjson`を使いたいが、同梱の `ORJSONResponse` クラスで使われていないカスタム設定も使いたいとします。 + +例えば、インデントされ整形されたJSONを返したいので、orjsonオプション `orjson.OPT_INDENT_2` を使いたいとします。 + +`CustomORJSONResponse` を作れます。主に必要なのは、コンテンツを `bytes` として返す `Response.render(content)` メソッドを作ることです: + +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} + +これまでは次のように返していたものが: + +```json +{"message": "Hello World"} +``` + +...このレスポンスでは次のように返されます: + +```json +{ + "message": "Hello World" +} +``` + +もちろん、JSONの整形よりも、これを活用するもっと良い方法が見つかるはずです。 😉 + +## デフォルトレスポンスクラス { #default-response-class } + +**FastAPI** クラスのインスタンス、または `APIRouter` を作成する際に、デフォルトで使用するレスポンスクラスを指定できます。 + +これを定義するパラメータは `default_response_class` です。 + +以下の例では、**FastAPI** はすべての*path operation*で、`JSONResponse` の代わりに `ORJSONResponse` をデフォルトとして使います。 + +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | 豆知識 -前に見たように、 *path operation* の中で `response_class` をオーバーライドできます。 +これまでと同様に、*path operation*で `response_class` をオーバーライドできます。 /// -## その他のドキュメント +## その他のドキュメント { #additional-documentation } -また、OpenAPIでは `responses` を使ってメディアタイプやその他の詳細を宣言することもできます: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank} +OpenAPIでは `responses` を使ってメディアタイプやその他の詳細を宣言することもできます: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}。 diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md index 22eaf6eb8..1d0f7566c 100644 --- a/docs/ja/docs/advanced/index.md +++ b/docs/ja/docs/advanced/index.md @@ -1,27 +1,21 @@ -# 高度なユーザーガイド +# 高度なユーザーガイド { #advanced-user-guide } -## さらなる機能 +## さらなる機能 { #additional-features } -[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}により、**FastAPI**の主要な機能は十分に理解できたことでしょう。 +メインの[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}だけで、**FastAPI**の主要な機能を一通り把握するには十分なはずです。 -以降のセクションでは、チュートリアルでは説明しきれなかったオプションや設定、および機能について説明します。 +以降のセクションでは、その他のオプション、設定、追加機能を見ていきます。 /// tip | 豆知識 -以降のセクションは、 **必ずしも"応用編"ではありません**。 +以降のセクションは、**必ずしも「高度」ではありません**。 -ユースケースによっては、その中から解決策を見つけられるかもしれません。 +また、あなたのユースケースに対する解決策が、その中のどれかにある可能性もあります。 /// -## 先にチュートリアルを読む +## 先にチュートリアルを読む { #read-the-tutorial-first } -[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}の知識があれば、**FastAPI**の主要な機能を利用することができます。 +メインの[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}で得た知識があれば、**FastAPI**の機能の多くは引き続き利用できます。 -以降のセクションは、すでにチュートリアルを読んで、その主要なアイデアを理解できていることを前提としています。 - -## テスト駆動開発のコース - -このセクションの内容を補完するために脱初心者用コースを受けたい場合は、**TestDriven.io**による、Test-Driven Development with FastAPI and Dockerを確認するのがよいかもしれません。 - -現在、このコースで得られた利益の10%が**FastAPI**の開発のために寄付されています。🎉 😄 +また、以降のセクションでは、すでにそれを読んでいて、主要な考え方を理解していることを前提としています。 diff --git a/docs/ja/docs/advanced/path-operation-advanced-configuration.md b/docs/ja/docs/advanced/path-operation-advanced-configuration.md index 05188d5b2..a78c3cb02 100644 --- a/docs/ja/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/ja/docs/advanced/path-operation-advanced-configuration.md @@ -1,30 +1,30 @@ -# Path Operationの高度な設定 +# Path Operationの高度な設定 { #path-operation-advanced-configuration } -## OpenAPI operationId +## OpenAPI operationId { #openapi-operationid } /// warning | 注意 -あなたがOpenAPIの「エキスパート」でなければ、これは必要ないかもしれません。 +OpenAPIの「エキスパート」でなければ、これはおそらく必要ありません。 /// *path operation* で `operation_id` パラメータを利用することで、OpenAPIの `operationId` を設定できます。 -`operation_id` は各オペレーションで一意にする必要があります。 +各オペレーションで一意になるようにする必要があります。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### *path operation関数* の名前をoperationIdとして使用する +### *path operation関数* の名前をoperationIdとして使用する { #using-the-path-operation-function-name-as-the-operationid } -APIの関数名を `operationId` として利用したい場合、すべてのAPIの関数をイテレーションし、各 *path operation* の `operationId` を `APIRoute.name` で上書きすれば可能です。 +APIの関数名を `operationId` として利用したい場合、すべてのAPI関数をイテレーションし、各 *path operation* の `operation_id` を `APIRoute.name` で上書きすれば可能です。 -そうする場合は、すべての *path operation* を追加した後に行う必要があります。 +すべての *path operation* を追加した後に行うべきです。 -{* ../../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 | 豆知識 -`app.openapi()` を手動でコールする場合、その前に`operationId`を更新する必要があります。 +`app.openapi()` を手動で呼び出す場合、その前に `operationId` を更新するべきです。 /// @@ -32,22 +32,141 @@ APIの関数名を `operationId` として利用したい場合、すべてのAP この方法をとる場合、各 *path operation関数* が一意な名前である必要があります。 -それらが異なるモジュール (Pythonファイル) にあるとしてもです。 +異なるモジュール(Pythonファイル)にある場合でも同様です。 /// -## OpenAPIから除外する +## OpenAPIから除外する { #exclude-from-openapi } -生成されるOpenAPIスキーマ (つまり、自動ドキュメント生成の仕組み) から *path operation* を除外するには、 `include_in_schema` パラメータを `False` にします。 +生成されるOpenAPIスキーマ(つまり、自動ドキュメント生成の仕組み)から *path operation* を除外するには、`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による説明の高度な設定 +## docstringによる説明の高度な設定 { #advanced-description-from-docstring } -*path operation関数* のdocstringからOpenAPIに使用する行を制限することができます。 +*path operation関数* のdocstringからOpenAPIに使用する行を制限できます。 -`\f` (「書式送り (Form Feed)」のエスケープ文字) を付与することで、**FastAPI** はOpenAPIに使用される出力をその箇所までに制限します。 +`\f`(エスケープされた「書式送り(form feed)」文字)を追加すると、**FastAPI** はその地点でOpenAPIに使用される出力を切り詰めます。 -ドキュメントには表示されませんが、他のツール (例えばSphinx) では残りの部分を利用できるでしょう。 +ドキュメントには表示されませんが、他のツール(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 } + +*path operation* に対して `response_model` と `status_code` を宣言する方法はすでに見たことがあるでしょう。 + +それにより、*path operation* のメインのレスポンスに関するメタデータが定義されます。 + +追加のレスポンスについても、モデルやステータスコードなどとともに宣言できます。 + +これについてはドキュメントに章全体があります。 [OpenAPIの追加レスポンス](additional-responses.md){.internal-link target=_blank} で読めます。 + +## OpenAPI Extra { #openapi-extra } + +アプリケーションで *path operation* を宣言すると、**FastAPI** はOpenAPIスキーマに含めるために、その *path operation* に関連するメタデータを自動的に生成します。 + +/// note | 技術詳細 + +OpenAPI仕様では Operation Object と呼ばれています。 + +/// + +これには *path operation* に関するすべての情報が含まれ、自動ドキュメントを生成するために使われます。 + +`tags`、`parameters`、`requestBody`、`responses` などが含まれます。 + +この *path operation* 固有のOpenAPIスキーマは通常 **FastAPI** により自動生成されますが、拡張することもできます。 + +/// tip | 豆知識 + +これは低レベルな拡張ポイントです。 + +追加レスポンスを宣言するだけなら、より便利な方法として [OpenAPIの追加レスポンス](additional-responses.md){.internal-link target=_blank} を使うことができます。 + +/// + +`openapi_extra` パラメータを使って、*path operation* のOpenAPIスキーマを拡張できます。 + +### OpenAPI Extensions { #openapi-extensions } + +この `openapi_extra` は、例えば [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) を宣言するのに役立ちます。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +自動APIドキュメントを開くと、その拡張は特定の *path operation* の下部に表示されます。 + + + +そして(APIの `/openapi.json` にある)生成されたOpenAPIを見ると、その拡張も特定の *path operation* の一部として確認できます。 + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### カスタムOpenAPI *path operation* スキーマ { #custom-openapi-path-operation-schema } + +`openapi_extra` 内の辞書は、*path operation* 用に自動生成されたOpenAPIスキーマと深くマージされます。 + +そのため、自動生成されたスキーマに追加データを加えることができます。 + +例えば、Pydanticを使ったFastAPIの自動機能を使わずに独自のコードでリクエストを読み取り・検証することを選べますが、それでもOpenAPIスキーマでリクエストを定義したい場合があります。 + +それは `openapi_extra` で行えます。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +この例では、Pydanticモデルを一切宣言していません。実際、リクエストボディはJSONとして parsed されず、直接 `bytes` として読み取られます。そして `magic_data_reader()` 関数が、何らかの方法でそれをパースする責務を担います。 + +それでも、リクエストボディに期待されるスキーマを宣言できます。 + +### カスタムOpenAPI content type { #custom-openapi-content-type } + +同じトリックを使って、PydanticモデルでJSON Schemaを定義し、それを *path operation* 用のカスタムOpenAPIスキーマセクションに含めることができます。 + +また、リクエスト内のデータ型がJSONでない場合でもこれを行えます。 + +例えばこのアプリケーションでは、PydanticモデルからJSON Schemaを抽出するFastAPIの統合機能や、JSONの自動バリデーションを使っていません。実際、リクエストのcontent typeをJSONではなくYAMLとして宣言しています。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +それでも、デフォルトの統合機能を使っていないにもかかわらず、YAMLで受け取りたいデータのために、Pydanticモデルを使って手動でJSON Schemaを生成しています。 + +そしてリクエストを直接使い、ボディを `bytes` として抽出します。これは、FastAPIがリクエストペイロードをJSONとしてパースしようとすらしないことを意味します。 + +その後、コード内でそのYAMLコンテンツを直接パースし、さらに同じPydanticモデルを使ってYAMLコンテンツを検証しています。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip | 豆知識 + +ここでは同じPydanticモデルを再利用しています。 + +ただし同様に、別の方法で検証することもできます。 + +/// diff --git a/docs/ja/docs/advanced/response-directly.md b/docs/ja/docs/advanced/response-directly.md index 42412d507..7e83b9ffb 100644 --- a/docs/ja/docs/advanced/response-directly.md +++ b/docs/ja/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# レスポンスを直接返す +# レスポンスを直接返す { #return-a-response-directly } **FastAPI** の *path operation* では、通常は任意のデータを返すことができます: 例えば、 `dict`、`list`、Pydanticモデル、データベースモデルなどです。 @@ -10,7 +10,7 @@ これは例えば、カスタムヘッダーやcookieを返すときに便利です。 -## `Response` を返す +## `Response` を返す { #return-a-response } 実際は、`Response` やそのサブクラスを返すことができます。 @@ -26,7 +26,7 @@ これは多くの柔軟性を提供します。任意のデータ型を返したり、任意のデータ宣言やバリデーションをオーバーライドできます。 -## `jsonable_encoder` を `Response` の中で使う +## `jsonable_encoder` を `Response` の中で使う { #using-the-jsonable-encoder-in-a-response } **FastAPI** はあなたが返す `Response` に対して何も変更を加えないので、コンテンツが準備できていることを保証しなければなりません。 @@ -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 | 技術詳細 @@ -44,7 +44,7 @@ /// -## カスタム `Response` を返す +## カスタム `Response` を返す { #returning-a-custom-response } 上記の例では必要な部分を全て示していますが、あまり便利ではありません。`item` を直接返すことができるし、**FastAPI** はそれを `dict` に変換して `JSONResponse` に含めてくれるなど。すべて、デフォルトの動作です。 @@ -54,9 +54,9 @@ XMLを文字列にし、`Response` に含め、それを返します。 -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -## 備考 +## 備考 { #notes } `Response` を直接返す場合、バリデーションや、変換 (シリアライズ) や、自動ドキュメントは行われません。 diff --git a/docs/ja/docs/advanced/websockets.md b/docs/ja/docs/advanced/websockets.md index 2517530ab..6c68c9f0b 100644 --- a/docs/ja/docs/advanced/websockets.md +++ b/docs/ja/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSocket +# WebSockets { #websockets } -**FastAPI**でWebSocketが使用できます。 +**FastAPI**でWebSocketsが使用できます。 -## `WebSockets`のインストール +## `websockets`のインストール { #install-websockets } -まず `WebSockets`のインストールが必要です。 +[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成し、それを有効化してから、「WebSocket」プロトコルを簡単に使えるようにするPythonライブラリの`websockets`をインストールしてください。
@@ -16,13 +16,13 @@ $ pip install websockets
-## WebSocket クライアント +## WebSockets クライアント { #websockets-client } -### 本番環境 +### 本番環境 { #in-production } 本番環境では、React、Vue.js、Angularなどの最新のフレームワークで作成されたフロントエンドを使用しているでしょう。 -そして、バックエンドとWebSocketを使用して通信するために、おそらくフロントエンドのユーティリティを使用することになるでしょう。 +そして、バックエンドとWebSocketsを使用して通信するために、おそらくフロントエンドのユーティリティを使用することになるでしょう。 または、ネイティブコードでWebSocketバックエンドと直接通信するネイティブモバイルアプリケーションがあるかもしれません。 @@ -30,21 +30,21 @@ $ pip install websockets --- -ただし、この例では非常にシンプルなHTML文書といくつかのJavaScriptを、すべてソースコードの中に入れて使用することにします。 +ただし、この例では非常にシンプルなHTML文書といくつかのJavaScriptを、すべて長い文字列の中に入れて使用することにします。 もちろん、これは最適な方法ではありませんし、本番環境で使うことはないでしょう。 本番環境では、上記の方法のいずれかの選択肢を採用することになるでしょう。 -しかし、これはWebSocketのサーバーサイドに焦点を当て、実用的な例を示す最も簡単な方法です。 +しかし、これは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 | 技術詳細 @@ -54,22 +54,22 @@ $ pip install websockets /// -## メッセージの送受信 +## メッセージを待機して送信する { #await-for-messages-and-send-messages } -WebSocketルートでは、 `await` を使ってメッセージの送受信ができます。 +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` である場合、以下でアプリケーションを実行します。
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -86,7 +86,7 @@ $ uvicorn main:app --reload -そして、 WebSocketを使用した**FastAPI**アプリケーションが応答します。 +そして、 WebSocketsを使用した**FastAPI**アプリケーションが応答します。 @@ -96,7 +96,7 @@ $ uvicorn main:app --reload そして、これらの通信はすべて同じWebSocket接続を使用します。 -## 依存関係 +## `Depends` などの使用 { #using-depends-and-others } WebSocketエンドポイントでは、`fastapi` から以下をインポートして使用できます。 @@ -107,28 +107,26 @@ WebSocketエンドポイントでは、`fastapi` から以下をインポート * `Path` * `Query` -これらは、他のFastAPI エンドポイント/*path operation* の場合と同じように機能します。 +これらは、他のFastAPI エンドポイント/*path operations* の場合と同じように機能します。 -{* ../../docs_src/websockets/tutorial002.py hl[58:65,68:83] *} +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | 情報 -WebSocket で `HTTPException` を発生させることはあまり意味がありません。したがって、WebSocketの接続を直接閉じる方がよいでしょう。 +これはWebSocketであるため、`HTTPException` を発生させることはあまり意味がありません。代わりに `WebSocketException` を発生させます。 クロージングコードは、仕様で定義された有効なコードの中から使用することができます。 -将来的には、どこからでも `raise` できる `WebSocketException` が用意され、専用の例外ハンドラを追加できるようになる予定です。これは、Starlette の PR #527 に依存するものです。 - /// -### 依存関係を用いてWebSocketsを試してみる +### 依存関係を用いてWebSocketsを試してみる { #try-the-websockets-with-dependencies } -ファイル名が `main.py` である場合、以下の方法でアプリケーションを実行します。 +ファイル名が `main.py` である場合、以下でアプリケーションを実行します。
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -137,14 +135,14 @@ $ uvicorn main:app --reload ブラウザで http://127.0.0.1:8000 を開きます。 -クライアントが設定できる項目は以下の通りです。 +そこで、以下を設定できます。 * パスで使用される「Item ID」 * クエリパラメータとして使用される「Token」 /// tip | 豆知識 -クエリ `token` は依存パッケージによって処理されることに注意してください。 +クエリ `token` は依存関係によって処理されることに注意してください。 /// @@ -152,11 +150,11 @@ $ uvicorn main:app --reload -## 切断や複数クライアントへの対応 +## 切断や複数クライアントの処理 { #handling-disconnections-and-multiple-clients } WebSocket接続が閉じられると、 `await websocket.receive_text()` は例外 `WebSocketDisconnect` を発生させ、この例のようにキャッチして処理することができます。 -{* ../../docs_src/websockets/tutorial003.py hl[81:83] *} +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} 試してみるには、 @@ -174,15 +172,15 @@ Client #1596980209979 left the chat 上記のアプリは、複数の WebSocket 接続に対してメッセージを処理し、ブロードキャストする方法を示すための最小限のシンプルな例です。 -しかし、すべての接続がメモリ内の単一のリストで処理されるため、プロセスの実行中にのみ機能し、単一のプロセスでのみ機能することに注意してください。 +しかし、すべてがメモリ内の単一のリストで処理されるため、プロセスの実行中にのみ機能し、単一のプロセスでのみ機能することに注意してください。 -もしFastAPIと簡単に統合できて、RedisやPostgreSQLなどでサポートされている、より堅牢なものが必要なら、encode/broadcaster を確認してください。 +FastAPIと簡単に統合できて、RedisやPostgreSQLなどでサポートされている、より堅牢なものが必要なら、encode/broadcaster を確認してください。 /// -## その他のドキュメント +## 詳細情報 { #more-info } オプションの詳細については、Starletteのドキュメントを確認してください。 -* `WebSocket` クラス -* クラスベースのWebSocket処理 +* `WebSocket` クラス. +* クラスベースのWebSocket処理. diff --git a/docs/ja/docs/benchmarks.md b/docs/ja/docs/benchmarks.md index 966d199c5..fbfba2e63 100644 --- a/docs/ja/docs/benchmarks.md +++ b/docs/ja/docs/benchmarks.md @@ -1,34 +1,34 @@ -# ベンチマーク +# ベンチマーク { #benchmarks } -TechEmpowerの独立したベンチマークでは、Uvicornの下で動作する**FastAPI**アプリケーションは、利用可能な最速のPythonフレームワークの1つであり、下回っているのはStarletteとUvicorn自体 (FastAPIによって内部で使用される) のみだと示されています。 +TechEmpowerの独立したベンチマークでは、Uvicornの下で動作する**FastAPI**アプリケーションは、利用可能な最速のPythonフレームワークの1つであり、下回っているのはStarletteとUvicorn自体(FastAPIによって内部で使用される)のみだと示されています。 ただし、ベンチマークを確認し、比較する際には下記の内容に気を付けてください。 -## ベンチマークと速度 +## ベンチマークと速度 { #benchmarks-and-speed } -ベンチマークを確認する時、異なるツールを同等なものと比較するのが一般的です。 +ベンチマークを確認する時、異なるタイプの複数のツールが同等のものとして比較されているのを目にするのが一般的です。 -具体的には、Uvicorn、Starlette、FastAPIを (他の多くのツールと) 比較しました。 +具体的には、Uvicorn、Starlette、FastAPIを(他の多くのツールの中で)まとめて比較しているのを目にすることがあります。 ツールで解決する問題がシンプルなほど、パフォーマンスが向上します。また、ほとんどのベンチマークは、ツールから提供される追加機能をテストしていません。 階層関係はこのようになります。 * **Uvicorn**: ASGIサーバー - * **Starlette**: (Uvicornを使用) WEBマイクロフレームワーク - * **FastAPI**: (Starletteを使用) データバリデーションなどの、APIを構築する追加機能を備えたAPIマイクロフレームワーク + * **Starlette**: (Uvicornを使用)webマイクロフレームワーク + * **FastAPI**: (Starletteを使用)データバリデーションなど、APIを構築するためのいくつかの追加機能を備えたAPIマイクロフレームワーク * **Uvicorn**: - * サーバー自体に余分なコードが少ないので、最高のパフォーマンスが得られます。 - * Uvicornにアプリケーションを直接書くことはできません。つまり、あなたのコードには、Starlette (または** FastAPI **) が提供するコードを、多かれ少なかれ含める必要があります。そうすると、最終的なアプリケーションは、フレームワークを使用してアプリのコードとバグを最小限に抑えた場合と同じオーバーヘッドになります。 - * もしUvicornを比較する場合は、Daphne、Hypercorn、uWSGIなどのアプリケーションサーバーと比較してください。 + * サーバー自体以外に余分なコードがあまりないため、最高のパフォーマンスになります。 + * Uvicornにアプリケーションを直接書くことはないでしょう。それは、あなたのコードに、Starlette(または**FastAPI**)が提供するコードを、少なくとも多かれ少なかれ含める必要があるということです。そして、もしそうした場合、最終的なアプリケーションは、フレームワークを使用してアプリのコードとバグを最小限に抑えた場合と同じオーバーヘッドになります。 + * Uvicornを比較する場合は、Daphne、Hypercorn、uWSGIなどのアプリケーションサーバーと比較してください。 * **Starlette**: - * Uvicornに次ぐ性能を持つでしょう。実際、StarletteはUvicornを使用しています。だから、より多くのコードを実行する必要があり、Uvicornよりも「遅く」なってしまうだけなのです。 - * しかし、パスベースのルーティングなどのシンプルなWEBアプリケーションを構築する機能を提供します。 - * もしStarletteを比較する場合は、Sanic、Flask、DjangoなどのWEBフレームワーク (もしくはマイクロフレームワーク) と比較してください。 + * Uvicornに次ぐ性能になるでしょう。実際、Starletteは実行にUvicornを使用しています。そのため、おそらく、より多くのコードを実行しなければならない分だけ、Uvicornより「遅く」なるだけです。 + * しかし、パスに基づくルーティングなどを使って、シンプルなwebアプリケーションを構築するためのツールを提供します。 + * Starletteを比較する場合は、Sanic、Flask、Djangoなどのwebフレームワーク(またはマイクロフレームワーク)と比較してください。 * **FastAPI**: - * StarletteがUvicornを使っているのと同じで、**FastAPI**はStarletteを使っており、それより速くできません。 - * FastAPIはStarletteの上にさらに多くの機能を提供します。データの検証やシリアライゼーションなど、APIを構築する際に常に必要な機能です。また、それを使用することで、自動ドキュメント化を無料で取得できます (ドキュメントは実行中のアプリケーションにオーバーヘッドを追加せず、起動時に生成されます) 。 - * FastAPIを使用せず、直接Starlette (またはSanic, Flask, Responderなど) を使用した場合、データの検証とシリアライズをすべて自分で実装する必要があります。そのため、最終的なアプリケーションはFastAPIを使用して構築した場合と同じオーバーヘッドが発生します。そして、多くの場合、このデータ検証とシリアライズは、アプリケーションのコードの中で最大の記述量になります。 - * FastAPIを使用することで、開発時間、バグ、コード行数を節約でき、使用しない場合 (あなたが全ての機能を実装し直した場合) と同じかそれ以上のパフォーマンスを得られます。 - * もしFastAPIを比較する場合は、Flask-apispec、NestJS、Moltenなどのデータ検証や、シリアライズの機能を提供するWEBフレームワーク (や機能のセット) と比較してください。これらはデータの自動検証や、シリアライズ、ドキュメント化が統合されたフレームワークです。 + * StarletteがUvicornを使用しており、それより速くできないのと同じように、**FastAPI**はStarletteを使用しているため、それより速くできません。 + * FastAPIはStarletteの上に、より多くの機能を提供します。データバリデーションやシリアライゼーションのように、APIを構築する際にほとんど常に必要な機能です。また、それを使用することで、自動ドキュメント化を無料で利用できます(自動ドキュメントは実行中のアプリケーションにオーバーヘッドを追加せず、起動時に生成されます)。 + * FastAPIを使用せず、Starletteを直接(またはSanic、Flask、Responderなど別のツールを)使用した場合、データバリデーションとシリアライゼーションをすべて自分で実装する必要があります。そのため、最終的なアプリケーションはFastAPIを使用して構築した場合と同じオーバーヘッドが発生します。そして多くの場合、このデータバリデーションとシリアライゼーションは、アプリケーションで書かれるコードの大部分になります。 + * そのため、FastAPIを使用することで、開発時間、バグ、コード行数を節約でき、使用しない場合(あなたがそれをすべて自分のコードで実装する必要があるため)と比べて、同じパフォーマンス(またはそれ以上)を得られる可能性があります。 + * FastAPIを比較する場合は、Flask-apispec、NestJS、Moltenなど、データバリデーション、シリアライゼーション、ドキュメント化を提供するwebアプリケーションフレームワーク(またはツール群)と比較してください。自動データバリデーション、シリアライゼーション、ドキュメント化が統合されたフレームワークです。 diff --git a/docs/ja/docs/deployment/concepts.md b/docs/ja/docs/deployment/concepts.md index a0d4fb35b..787eb2e73 100644 --- a/docs/ja/docs/deployment/concepts.md +++ b/docs/ja/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# デプロイメントのコンセプト +# デプロイメントのコンセプト { #deployments-concepts } **FastAPI**を用いたアプリケーションをデプロイするとき、もしくはどのようなタイプのWeb APIであっても、おそらく気になるコンセプトがいくつかあります。 @@ -10,12 +10,12 @@ * 起動時の実行 * 再起動 * レプリケーション(実行中のプロセス数) -* メモリー +* メモリ * 開始前の事前のステップ これらが**デプロイメント**にどのような影響を与えるかを見ていきましょう。 -最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。🚀 +最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。 🚀 この章では前述した**コンセプト**についてそれぞれ説明します。 @@ -27,16 +27,16 @@ しかし、今はこれらの重要な**コンセプトに基づくアイデア**を確認しましょう。これらのコンセプトは、他のどのタイプのWeb APIにも当てはまります。💡 -## セキュリティ - HTTPS +## セキュリティ - HTTPS { #security-https } [前チャプターのHTTPSについて](https.md){.internal-link target=_blank}では、HTTPSがどのようにAPIを暗号化するのかについて学びました。 通常、アプリケーションサーバにとって**外部の**コンポーネントである**TLS Termination Proxy**によって提供されることが一般的です。このプロキシは通信の暗号化を担当します。 -さらにセキュアな通信において、HTTPS証明書の定期的な更新を行いますが、これはTLS Termination Proxyと同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 +さらに、HTTPS証明書の更新を担当するものが必要で、同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 -### HTTPS 用ツールの例 +### HTTPS 用ツールの例 { #example-tools-for-https } TLS Termination Proxyとして使用できるツールには以下のようなものがあります: * Traefik @@ -59,11 +59,11 @@ TLS Termination Proxyとして使用できるツールには以下のような 次に考慮すべきコンセプトは、実際のAPIを実行するプログラム(例:Uvicorn)に関連するものすべてです。 -## プログラム と プロセス +## プログラム と プロセス { #program-and-process } 私たちは「**プロセス**」という言葉についてたくさん話すので、その意味や「**プログラム**」という言葉との違いを明確にしておくと便利です。 -### プログラムとは何か +### プログラムとは何か { #what-is-a-program } **プログラム**という言葉は、一般的にいろいろなものを表現するのに使われます: @@ -71,7 +71,7 @@ TLS Termination Proxyとして使用できるツールには以下のような * OSによって実行することができるファイル(例: `python`, `python.exe` or `uvicorn`) * OS上で**実行**している間、CPUを使用し、メモリ上に何かを保存する特定のプログラム(**プロセス**とも呼ばれる) -### プロセスとは何か +### プロセスとは何か { #what-is-a-process } **プロセス**という言葉は通常、より具体的な意味で使われ、OSで実行されているものだけを指します(先ほどの最後の説明のように): @@ -92,27 +92,29 @@ OSの「タスク・マネージャー」や「システム・モニター」( さて、**プロセス**と**プログラム**という用語の違いを確認したところで、デプロイメントについて話を続けます。 -## 起動時の実行 +## 起動時の実行 { #running-on-startup } ほとんどの場合、Web APIを作成するときは、クライアントがいつでもアクセスできるように、**常に**中断されることなく**実行される**ことを望みます。もちろん、特定の状況でのみ実行させたい特別な理由がある場合は別ですが、その時間のほとんどは、常に実行され、**利用可能**であることを望みます。 -### リモートサーバー上での実行 +### リモートサーバー上での実行 { #in-a-remote-server } -リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、Uvicorn(または同様のもの)を手動で実行することです。 この方法は**開発中**には役に立つと思われます。 +リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、`fastapi run`(Uvicornを使用します)や同様のものを手動で実行することです。 + +そしてこれは動作し、**開発中**には役に立つでしょう。 しかし、サーバーへの接続が切れた場合、**実行中のプロセス**はおそらくダウンしてしまうでしょう。 そしてサーバーが再起動された場合(アップデートやクラウドプロバイダーからのマイグレーションの後など)、おそらくあなたはそれに**気づかないでしょう**。そのため、プロセスを手動で再起動しなければならないことすら気づかないでしょう。つまり、APIはダウンしたままなのです。😱 -### 起動時に自動的に実行 +### 起動時に自動的に実行 { #run-automatically-on-startup } 一般的に、サーバープログラム(Uvicornなど)はサーバー起動時に自動的に開始され、**人の介入**を必要とせずに、APIと一緒にプロセスが常に実行されるようにしたいと思われます(UvicornがFastAPIアプリを実行するなど)。 -### 別のプログラムの用意 +### 別のプログラムの用意 { #separate-program } これを実現するために、通常は**別のプログラム**を用意し、起動時にアプリケーションが実行されるようにします。そして多くの場合、他のコンポーネントやアプリケーション、例えばデータベースも実行されるようにします。 -### 起動時に実行するツールの例 +### 起動時に実行するツールの例 { #example-tools-to-run-at-startup } 実行するツールの例をいくつか挙げます: @@ -127,31 +129,33 @@ OSの「タスク・マネージャー」や「システム・モニター」( 次の章で、より具体的な例を挙げていきます。 -## 再起動 +## 再起動 { #restarts } 起動時にアプリケーションが実行されることを確認するのと同様に、失敗後にアプリケーションが**再起動**されることも確認したいと思われます。 -### 我々は間違いを犯す +### 我々は間違いを犯す { #we-make-mistakes } 私たち人間は常に**間違い**を犯します。ソフトウェアには、ほとんど常に**バグ**があらゆる箇所に隠されています。🐛 -### 小さなエラーは自動的に処理される +そして私たち開発者は、それらのバグを見つけたり新しい機能を実装したりしながらコードを改善し続けます(新しいバグも追加してしまうかもしれません😅)。 + +### 小さなエラーは自動的に処理される { #small-errors-automatically-handled } FastAPIでWeb APIを構築する際に、コードにエラーがある場合、FastAPIは通常、エラーを引き起こした単一のリクエストにエラーを含めます。🛡 クライアントはそのリクエストに対して**500 Internal Server Error**を受け取りますが、アプリケーションは完全にクラッシュするのではなく、次のリクエストのために動作を続けます。 -### 重大なエラー - クラッシュ +### 重大なエラー - クラッシュ { #bigger-errors-crashes } しかしながら、**アプリケーション全体をクラッシュさせるようなコードを書いて**UvicornとPythonをクラッシュさせるようなケースもあるかもしれません。💥 -それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*パスオペレーション*については、**実行し続けたい**はずです。 +それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*path operation*については、**実行し続けたい**はずです。 -### クラッシュ後の再起動 +### クラッシュ後の再起動 { #restart-after-crash } しかし、実行中の**プロセス**をクラッシュさせるような本当にひどいエラーの場合、少なくとも2〜3回ほどプロセスを**再起動**させる外部コンポーネントが必要でしょう。 -/// tip +/// tip | 豆知識 ...とはいえ、アプリケーション全体が**すぐにクラッシュする**のであれば、いつまでも再起動し続けるのは意味がないでしょう。しかし、その場合はおそらく開発中か少なくともデプロイ直後に気づくと思われます。 @@ -161,7 +165,7 @@ FastAPIでWeb APIを構築する際に、コードにエラーがある場合、 あなたはおそらく**外部コンポーネント**がアプリケーションの再起動を担当することを望むと考えます。 なぜなら、その時点でUvicornとPythonを使った同じアプリケーションはすでにクラッシュしており、同じアプリケーションの同じコードに対して何もできないためです。 -### 自動的に再起動するツールの例 +### 自動的に再起動するツールの例 { #example-tools-to-restart-automatically } ほとんどの場合、前述した**起動時にプログラムを実行する**ために使用されるツールは、自動で**再起動**することにも利用されます。 @@ -176,19 +180,19 @@ FastAPIでWeb APIを構築する際に、コードにエラーがある場合、 * クラウドプロバイダーがサービスの一部として内部的に処理 * そのほか... -## レプリケーション - プロセスとメモリー +## レプリケーション - プロセスとメモリ { #replication-processes-and-memory } -FastAPI アプリケーションでは、Uvicorn のようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 +FastAPI アプリケーションでは、Uvicorn を実行する `fastapi` コマンドのようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 しかし、多くの場合、複数のワーカー・プロセスを同時に実行したいと考えるでしょう。 -### 複数のプロセス - Worker +### 複数のプロセス - Worker { #multiple-processes-workers } クライアントの数が単一のプロセスで処理できる数を超えており(たとえば仮想マシンがそれほど大きくない場合)、かつサーバーの CPU に**複数のコア**がある場合、同じアプリケーションで同時に**複数のプロセス**を実行させ、すべてのリクエストを分散させることができます。 同じAPIプログラムの**複数のプロセス**を実行する場合、それらは一般的に**Worker/ワーカー**と呼ばれます。 -### ワーカー・プロセス と ポート +### ワーカー・プロセス と ポート { #worker-processes-and-ports } [HTTPSについて](https.md){.internal-link target=_blank}のドキュメントで、1つのサーバーで1つのポートとIPアドレスの組み合わせでリッスンできるのは1つのプロセスだけであることを覚えていますでしょうか? @@ -197,13 +201,13 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ そのため、**複数のプロセス**を同時に持つには**ポートでリッスンしている単一のプロセス**が必要であり、それが何らかの方法で各ワーカー・プロセスに通信を送信することが求められます。 -### プロセスあたりのメモリー +### プロセスあたりのメモリ { #memory-per-process } さて、プログラムがメモリにロードする際には、例えば機械学習モデルや大きなファイルの内容を変数に入れたりする場合では、**サーバーのメモリ(RAM)**を少し消費します。 そして複数のプロセスは通常、**メモリを共有しません**。これは、実行中の各プロセスがそれぞれ独自の変数やメモリ等を持っていることを意味します。つまり、コード内で大量のメモリを消費している場合、**各プロセス**は同等の量のメモリを消費することになります。 -### サーバーメモリー +### サーバーメモリ { #server-memory } 例えば、あなたのコードが **1GBのサイズの機械学習モデル**をロードする場合、APIで1つのプロセスを実行すると、少なくとも1GBのRAMを消費します。 @@ -211,7 +215,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ リモートサーバーや仮想マシンのRAMが3GBしかない場合、4GB以上のRAMをロードしようとすると問題が発生します。🚨 -### 複数プロセス - 例 +### 複数プロセス - 例 { #multiple-processes-an-example } この例では、2つの**ワーカー・プロセス**を起動し制御する**マネージャー・ プロセス**があります。 @@ -227,7 +231,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ 毎回同程度の計算を行うAPIがあり、多くのクライアントがいるのであれば、**CPU使用率**もおそらく**安定**するでしょう(常に急激に上下するのではなく)。 -### レプリケーション・ツールと戦略の例 +### レプリケーション・ツールと戦略の例 { #examples-of-replication-tools-and-strategies } これを実現するにはいくつかのアプローチがありますが、具体的な戦略については次の章(Dockerやコンテナの章など)で詳しく説明します。 @@ -237,25 +241,22 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ 考えられる組み合わせと戦略をいくつか紹介します: -* **Gunicorn**が**Uvicornワーカー**を管理 - * Gunicornは**IP**と**ポート**をリッスンする**プロセスマネージャ**で、レプリケーションは**複数のUvicornワーカー・プロセス**を持つことによって行われる。 -* **Uvicorn**が**Uvicornワーカー**を管理 +* `--workers` を指定した **Uvicorn** * 1つのUvicornの**プロセスマネージャー**が**IP**と**ポート**をリッスンし、**複数のUvicornワーカー・プロセス**を起動する。 * **Kubernetes**やその他の分散**コンテナ・システム** * **Kubernetes**レイヤーの何かが**IP**と**ポート**をリッスンする。レプリケーションは、**複数のコンテナ**にそれぞれ**1つのUvicornプロセス**を実行させることで行われる。 * **クラウド・サービス**によるレプリケーション * クラウド・サービスはおそらく**あなたのためにレプリケーションを処理**します。**実行するプロセス**や使用する**コンテナイメージ**を定義できるかもしれませんが、いずれにせよ、それはおそらく**単一のUvicornプロセス**であり、クラウドサービスはそのレプリケーションを担当するでしょう。 -/// tip +/// tip | 豆知識 これらの**コンテナ**やDockerそしてKubernetesに関する項目が、まだあまり意味をなしていなくても心配しないでください。 - -コンテナ・イメージ、Docker、Kubernetesなどについては、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. +コンテナ・イメージ、Docker、Kubernetesなどについては、将来の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. /// -## 開始前の事前のステップ +## 開始前の事前のステップ { #previous-steps-before-starting } アプリケーションを**開始する前**に、いくつかのステップを実行したい場合が多くあります。 @@ -271,7 +272,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ もちろん、事前のステップを何度も実行しても問題がない場合もあり、その際は対処がかなり楽になります。 -/// tip +/// tip | 豆知識 また、セットアップによっては、アプリケーションを開始する前の**事前のステップ**が必要ない場合もあることを覚えておいてください。 @@ -279,7 +280,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ /// -### 事前ステップの戦略例 +### 事前ステップの戦略例 { #examples-of-previous-steps-strategies } これは**システムを**デプロイする方法に**大きく依存**するだろうし、おそらくプログラムの起動方法や再起動の処理などにも関係してくるでしょう。 @@ -289,14 +290,13 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ * 事前のステップを実行し、アプリケーションを起動するbashスクリプト * 利用するbashスクリプトを起動/再起動したり、エラーを検出したりする方法は以前として必要になるでしょう。 -/// tip +/// tip | 豆知識 - -コンテナを使った具体的な例については、次の章で紹介します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. +コンテナを使った具体的な例については、将来の章で紹介します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. /// -## リソースの利用 +## リソースの利用 { #resource-utilization } あなたのサーバーは**リソース**であり、プログラムを実行しCPUの計算時間や利用可能なRAMメモリを消費または**利用**することができます。 @@ -319,7 +319,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ `htop`のような単純なツールを使って、サーバーで使用されているCPUやRAM、あるいは各プロセスで使用されている量を見ることができます。あるいは、より複雑な監視ツールを使って、サーバに分散して使用することもできます。 -## まとめ +## まとめ { #recap } アプリケーションのデプロイ方法を決定する際に、考慮すべきであろう主要なコンセプトのいくつかを紹介していきました: @@ -327,7 +327,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ * 起動時の実行 * 再起動 * レプリケーション(実行中のプロセス数) -* メモリー +* メモリ * 開始前の事前ステップ これらの考え方とその適用方法を理解することで、デプロイメントを設定したり調整したりする際に必要な直感的な判断ができるようになるはずです。🤓 diff --git a/docs/ja/docs/deployment/docker.md b/docs/ja/docs/deployment/docker.md index 53fc851f1..6c182448c 100644 --- a/docs/ja/docs/deployment/docker.md +++ b/docs/ja/docs/deployment/docker.md @@ -1,20 +1,17 @@ -# コンテナ内のFastAPI - Docker +# コンテナ内のFastAPI - Docker { #fastapi-in-containers-docker } -FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。 - -基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 +FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 Linuxコンテナの使用には、**セキュリティ**、**反復可能性(レプリカビリティ)**、**シンプリシティ**など、いくつかの利点があります。 -/// tip +/// tip | 豆知識 -TODO: なぜか遷移できない お急ぎで、すでにこれらの情報をご存じですか? [以下の`Dockerfile`の箇所👇](#build-a-docker-image-for-fastapi)へジャンプしてください。 ///
-Dockerfile プレビュー 👀 +Dockerfile Preview 👀 ```Dockerfile FROM python:3.9 @@ -27,15 +24,15 @@ 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 } コンテナ(主にLinuxコンテナ)は、同じシステム内の他のコンテナ(他のアプリケーションやコンポーネント)から隔離された状態を保ちながら、すべての依存関係や必要なファイルを含むアプリケーションをパッケージ化する非常に**軽量**な方法です。 @@ -45,7 +42,7 @@ Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサ コンテナはまた、独自の**分離された**実行プロセス(通常は1つのプロセスのみ)や、ファイルシステム、ネットワークを持ちます。 このことはデプロイ、セキュリティ、開発などを簡素化させます。 -## コンテナ・イメージとは何か +## コンテナ・イメージとは何か { #what-is-a-container-image } **コンテナ**は、**コンテナ・イメージ**から実行されます。 @@ -53,23 +50,17 @@ Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサ 保存された静的コンテンツである「**コンテナイメージ**」とは対照的に、「**コンテナ**」は通常、実行中のインスタンス、つまり**実行**されているものを指します。 -**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。 - -これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 +**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 コンテナイメージは **プログラム** ファイルやその内容、例えば `python` と `main.py` ファイルに匹敵します。 -そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。 +そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 -実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 - -## コンテナ・イメージ +## コンテナ・イメージ { #container-images } Dockerは、**コンテナ・イメージ**と**コンテナ**を作成・管理するための主要なツールの1つです。 -そして、DockerにはDockerイメージ(コンテナ)を共有するDocker Hubというものがあります。 - -Docker Hubは 多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しています。 +そして、多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しているDocker Hubというものがあります。 例えば、公式イメージの1つにPython Imageがあります。 @@ -88,7 +79,7 @@ Docker Hubは 多くのツールや環境、データベース、アプリケー すべてのコンテナ管理システム(DockerやKubernetesなど)には、こうしたネットワーキング機能が統合されています。 -## コンテナとプロセス +## コンテナとプロセス { #containers-and-processes } 通常、**コンテナ・イメージ**はそのメタデータに**コンテナ**の起動時に実行されるデフォルトのプログラムまたはコマンドと、そのプログラムに渡されるパラメータを含みます。コマンドラインでの操作とよく似ています。 @@ -100,7 +91,7 @@ Docker Hubは 多くのツールや環境、データベース、アプリケー しかし、**少なくとも1つの実行中のプロセス**がなければ、実行中のコンテナを持つことはできないです。メイン・プロセスが停止すれば、コンテナも停止します。 -## Build a Docker Image for FastAPI +## FastAPI用のDockerイメージをビルドする { #build-a-docker-image-for-fastapi } ということで、何か作りましょう!🚀 @@ -112,7 +103,7 @@ FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づ * **Raspberry Pi**で実行する場合 * コンテナ・イメージを実行してくれるクラウド・サービスなどを利用する場合 -### パッケージ要件(package requirements) +### パッケージ要件 { #package-requirements } アプリケーションの**パッケージ要件**は通常、何らかのファイルに記述されているはずです。 @@ -125,9 +116,8 @@ FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づ 例えば、`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` を使ってこれらのパッケージの依存関係をインストールします: @@ -137,28 +127,24 @@ uvicorn>=0.15.0,<0.16.0 ```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ```
-/// info +/// info | 情報 パッケージの依存関係を定義しインストールするためのフォーマットやツールは他にもあります。 -Poetryを使った例は、後述するセクションでご紹介します。👇 - /// -### **FastAPI**コードを作成する +### **FastAPI**コードを作成する { #create-the-fastapi-code } -* `app` ディレクトリを作成し、その中に入ります -* 空のファイル `__init__.py` を作成します -* `main.py` ファイルを作成します: +* `app` ディレクトリを作成し、その中に入ります。 +* 空のファイル `__init__.py` を作成します。 +* 次の内容で `main.py` ファイルを作成します: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -170,32 +156,32 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: 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. 公式のPythonベースイメージから始めます @@ -211,9 +197,10 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] このファイルは**頻繁には変更されない**ので、Dockerはこのステップではそれを検知し**キャッシュ**を使用し、次のステップでもキャッシュを有効にします。 4. 要件ファイルにあるパッケージの依存関係をインストールします + `--no-cache-dir` オプションはダウンロードしたパッケージをローカルに保存しないように `pip` に指示します。これは、同じパッケージをインストールするために `pip` を再度実行する場合にのみ有効ですが、コンテナで作業する場合はそうではないです。 - /// note + /// note | 備考 `--no-cache-dir`は`pip`に関連しているだけで、Dockerやコンテナとは何の関係もないです。 @@ -225,26 +212,56 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] このステップでキャッシュを使用すると、開発中にイメージを何度もビルドする際に、**毎回**すべての依存関係を**ダウンロードしてインストールする**代わりに多くの**時間**を**節約**できます。 -5. ./app` ディレクトリを `/code` ディレクトリの中にコピーする。 +5. `./app` ディレクトリを `/code` ディレクトリの中にコピーする。 これには**最も頻繁に変更される**すべてのコードが含まれているため、Dockerの**キャッシュ**は**これ以降のステップ**に簡単に使用されることはありません。 そのため、コンテナイメージのビルド時間を最適化するために、`Dockerfile`の **最後** にこれを置くことが重要です。 -6. `uvicorn`サーバーを実行するための**コマンド**を設定します +6. 内部でUvicornを使用する `fastapi run` を使うための**コマンド**を設定します `CMD` は文字列のリストを取り、それぞれの文字列はスペースで区切られたコマンドラインに入力するものです。 このコマンドは **現在の作業ディレクトリ**から実行され、上記の `WORKDIR /code` にて設定した `/code` ディレクトリと同じです。 - そのためプログラムは `/code` で開始しその中にあなたのコードがある `./app` ディレクトリがあるので、**Uvicorn** は `app.main` から `app` を参照し、**インポート** することができます。 +/// tip | 豆知識 -/// tip - -コード内の"+"の吹き出しをクリックして、各行が何をするのかをレビューしてください。👆 +コード内の各番号バブルをクリックして、各行が何をするのかをレビューしてください。👆 /// +/// warning | 注意 + +以下で説明する通り、`CMD` 命令は**常に** **exec形式**を使用してください。 + +/// + +#### `CMD` を使う - Exec形式 { #use-cmd-exec-form } + +Docker命令 `CMD` は2つの形式で書けます: + +✅ **Exec** 形式: + +```Dockerfile +# ✅ Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** 形式: + +```Dockerfile +# ⛔️ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +FastAPIが正常にシャットダウンでき、[lifespan events](../advanced/events.md){.internal-link target=_blank}がトリガーされるように、常に **exec** 形式を使用してください。 + +詳しくは、shell形式とexec形式に関するDockerドキュメントをご覧ください。 + +これは `docker compose` を使用する場合にかなり目立つことがあります。より技術的な詳細は、このDocker ComposeのFAQセクションをご覧ください:Why do my services take 10 seconds to recreate or stop?。 + +#### ディレクトリ構造 { #directory-structure } + これで、次のようなディレクトリ構造になるはずです: ``` @@ -256,17 +273,15 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] └── requirements.txt ``` -#### TLS Termination Proxyの裏側 +#### TLS Termination Proxyの裏側 { #behind-a-tls-termination-proxy } -Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションを追加します。 - -このオプションは、Uvicornにプロキシ経由でHTTPSで動作しているアプリケーションに対して、送信されるヘッダを信頼するよう指示します。 +Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--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キャッシュ +#### Dockerキャッシュ { #docker-cache } この`Dockerfile`には重要なトリックがあり、まず**依存関係だけのファイル**をコピーします。その理由を説明します。 @@ -300,11 +315,11 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app ``` -### Dockerイメージをビルドする +### Dockerイメージをビルドする { #build-the-docker-image } すべてのファイルが揃ったので、コンテナ・イメージをビルドしましょう。 -* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります) +* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります)。 * FastAPI イメージをビルドします:
@@ -317,7 +332,7 @@ $ docker build -t myimage .
-/// tip +/// tip | 豆知識 末尾の `.` に注目してほしいです。これは `./` と同じ意味です。 これはDockerにコンテナイメージのビルドに使用するディレクトリを指示します。 @@ -325,7 +340,7 @@ $ docker build -t myimage . /// -### Dockerコンテナの起動する +### Dockerコンテナの起動する { #start-the-docker-container } * イメージに基づいてコンテナを実行します: @@ -337,7 +352,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-## 確認する +## 確認する { #check-it } Dockerコンテナのhttp://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (またはそれに相当するDockerホストを使用したもの)といったURLで確認できるはずです。 @@ -347,7 +362,7 @@ Dockerコンテナのhttp://192.168.99.100/docshttp://127.0.0.1/docs (またはそれに相当するDockerホストを使用したもの) @@ -355,7 +370,7 @@ Dockerコンテナのhttp://192.168.99.100/redochttp://127.0.0.1/redoc (またはそれに相当するDockerホストを使用したもの)にもアクセスできます。 @@ -363,9 +378,10 @@ DockerコンテナのTraefikのように、**HTTPS**と**証明書**の**自動**取得を扱う別のコンテナである可能性もあります。 -/// tip +/// tip | 豆知識 TraefikはDockerやKubernetesなどと統合されているので、コンテナ用のHTTPSの設定や構成はとても簡単です。 @@ -428,7 +444,7 @@ TraefikはDockerやKubernetesなどと統合されているので、コンテナ あるいは、(コンテナ内でアプリケーションを実行しながら)クラウド・プロバイダーがサービスの1つとしてHTTPSを処理することもできます。 -## 起動時および再起動時の実行 +## 起動時および再起動時の実行 { #running-on-startup-and-restarts } 通常、コンテナの**起動と実行**を担当する別のツールがあります。 @@ -438,21 +454,21 @@ TraefikはDockerやKubernetesなどと統合されているので、コンテナ コンテナを使わなければ、アプリケーションを起動時や再起動時に実行させるのは面倒で難しいかもしれません。しかし、**コンテナ**で作業する場合、ほとんどのケースでその機能はデフォルトで含まれています。✨ -## レプリケーション - プロセス数 +## レプリケーション - プロセス数 { #replication-number-of-processes } -**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのクラスターを構成している場合、 各コンテナで(Workerを持つGunicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 +**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのclusterを構成している場合、 各コンテナで(Workerを持つUvicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 Kubernetesのような分散コンテナ管理システムの1つは通常、入ってくるリクエストの**ロードバランシング**をサポートしながら、**コンテナのレプリケーション**を処理する統合された方法を持っています。このことはすべて**クラスタレベル**にてです。 -そのような場合、UvicornワーカーでGunicornのようなものを実行するのではなく、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。 +そのような場合、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。複数のUvicornワーカーを使う代わりにです。 -### ロードバランサー +### ロードバランサー { #load-balancer } コンテナを使用する場合、通常はメイン・ポート**でリスニング**しているコンポーネントがあるはずです。それはおそらく、**HTTPS**を処理するための**TLS Termination Proxy**でもある別のコンテナであったり、同様のツールであったりするでしょう。 このコンポーネントはリクエストの **負荷** を受け、 (うまくいけば) その負荷を**バランスよく** ワーカーに分配するので、一般に **ロードバランサ** とも呼ばれます。 -/// tip +/// tip | 豆知識 HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネントは、おそらく**ロードバランサー**にもなるでしょう。 @@ -460,7 +476,7 @@ HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネン そしてコンテナで作業する場合、コンテナの起動と管理に使用する同じシステムには、**ロードバランサー**(**TLS Termination Proxy**の可能性もある)から**ネットワーク通信**(HTTPリクエストなど)をアプリのあるコンテナ(複数可)に送信するための内部ツールが既にあるはずです。 -### 1つのロードバランサー - 複数のワーカーコンテナー +### 1つのロードバランサー - 複数のワーカーコンテナー { #one-load-balancer-multiple-worker-containers } **Kubernetes**や同様の分散コンテナ管理システムで作業する場合、その内部のネットワーキングのメカニズムを使用することで、メインの**ポート**でリッスンしている単一の**ロードバランサー**が、アプリを実行している可能性のある**複数のコンテナ**に通信(リクエスト)を送信できるようになります。 @@ -470,56 +486,61 @@ HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネン そして通常、この**ロードバランサー**は、クラスタ内の*他の*アプリケーション(例えば、異なるドメインや異なるURLパスのプレフィックスの配下)へのリクエストを処理することができ、その通信をクラスタ内で実行されている*他の*アプリケーションのための適切なコンテナに送信します。 -### 1コンテナにつき1プロセス +### 1コンテナにつき1プロセス { #one-process-per-container } この種のシナリオでは、すでにクラスタ・レベルでレプリケーションを処理しているため、おそらくコンテナごとに**単一の(Uvicorn)プロセス**を持ちたいでしょう。 -この場合、Uvicornワーカーを持つGunicornのようなプロセスマネージャーや、Uvicornワーカーを使うUvicornは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 +この場合、例えばコマンドラインオプションの `--workers` で、コンテナ内に複数のワーカーを持つことは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 -(GunicornやUvicornがUvicornワーカーを管理するように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 +(複数のワーカーの場合のように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 -### Containers with Multiple Processes and Special Cases +### 複数プロセスのコンテナと特殊なケース { #containers-with-multiple-processes-and-special-cases } -もちろん、**特殊なケース**として、**Gunicornプロセスマネージャ**を持つ**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 +もちろん、**特殊なケース**として、**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 -このような場合、**公式のDockerイメージ**を使用することができます。このイメージには、複数の**Uvicornワーカープロセス**を実行するプロセスマネージャとして**Gunicorn**が含まれており、現在のCPUコアに基づいてワーカーの数を自動的に調整するためのデフォルト設定がいくつか含まれています。詳しくは後述の[Gunicornによる公式Dockerイメージ - Uvicorn](#gunicorndocker-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"] +``` + +1. ここでは `--workers` コマンドラインオプションを使って、ワーカー数を4に設定しています。 以下は、それが理にかなっている場合の例です: -#### シンプルなアプリケーション +#### シンプルなアプリ { #a-simple-app } -アプリケーションを**シンプル**な形で実行する場合、プロセス数の細かい調整が必要ない場合、自動化されたデフォルトを使用するだけで、コンテナ内にプロセスマネージャが必要かもしれません。例えば、公式Dockerイメージでシンプルな設定が可能です。 +アプリケーションが、クラスタではなく**単一サーバ**で実行できるほど**シンプル**である場合、コンテナ内にプロセスマネージャが欲しくなることがあります。 -#### Docker Compose +#### Docker Compose { #docker-compose } -Docker Composeで**シングルサーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 +Docker Composeで**単一サーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 その場合、**単一のコンテナ**で、**プロセスマネージャ**が内部で**複数のワーカープロセス**を起動するようにします。 -#### Prometheusとその他の理由 - -また、**1つのコンテナ**に**1つのプロセス**を持たせるのではなく、**1つのコンテナ**に**複数のプロセス**を持たせる方が簡単だという**他の理由**もあるでしょう。 - -例えば、(セットアップにもよりますが)Prometheusエクスポーターのようなツールを同じコンテナ内に持つことができます。 - -この場合、**複数のコンテナ**があると、デフォルトでは、Prometheusが**メトリクスを**読みに来たとき、すべてのレプリケートされたコンテナの**蓄積されたメトリクス**を取得するのではなく、毎回**単一のコンテナ**(その特定のリクエストを処理したコンテナ)のものを取得することになります。 - -その場合、**複数のプロセス**を持つ**1つのコンテナ**を用意し、同じコンテナ上のローカルツール(例えばPrometheusエクスポーター)がすべての内部プロセスのPrometheusメトリクスを収集し、その1つのコンテナ上でそれらのメトリクスを公開する方がシンプルかもしれません。 - --- -重要なのは、盲目的に従わなければならない普遍のルールはないということです。 - -これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用することができます: +重要なのは、これらのどれも、盲目的に従わなければならない「**絶対的なルール**」ではないということです。これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用できます。次の概念をどう管理するかを確認してください: * セキュリティ - HTTPS * 起動時の実行 * 再起動 -* **レプリケーション(実行中のプロセス数)** +* レプリケーション(実行中のプロセス数) * メモリ * 開始前の事前ステップ -## メモリー +## メモリ { #memory } コンテナごとに**単一のプロセスを実行する**と、それらのコンテナ(レプリケートされている場合は1つ以上)によって消費される多かれ少なかれ明確に定義された、安定し制限された量のメモリを持つことになります。 @@ -531,109 +552,47 @@ Docker Composeで**シングルサーバ**(クラスタではない)にデ しかし、**多くのメモリを使用**している場合(たとえば**機械学習**モデルなど)、どれだけのメモリを消費しているかを確認し、**各マシンで実行するコンテナの数**を調整する必要があります(そしておそらくクラスタにマシンを追加します)。 -**コンテナごとに複数のプロセス**を実行する場合(たとえば公式のDockerイメージで)、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 +**コンテナごとに複数のプロセス**を実行する場合、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 -## 開始前の事前ステップとコンテナ +## 開始前の事前ステップとコンテナ { #previous-steps-before-starting-and-containers } コンテナ(DockerやKubernetesなど)を使っている場合、主に2つのアプローチがあります。 -### 複数のコンテナ +### 複数のコンテナ { #multiple-containers } -複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 +複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(例えば、**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 -/// info +/// info | 情報 -もしKubernetesを使用している場合, これはおそらくInit コンテナでしょう。 +もしKubernetesを使用している場合, これはおそらくInit Containerでしょう。 /// -ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースの準備チェック)、メインプロセスを開始する前に、それらのステップを各コンテナに入れることが可能です。 +ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースマイグレーションを実行するのではなく、データベースの準備ができたかをチェックするだけの場合)、メインプロセスを開始する直前に、それらのステップを各コンテナに入れることも可能です。 -### 単一コンテナ +### 単一コンテナ { #single-container } -単純なセットアップで、**単一のコンテナ**で複数の**ワーカー・プロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。公式Dockerイメージは、内部的にこれをサポートしています。 +単純なセットアップで、**単一のコンテナ**で複数の**ワーカープロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。 -## Gunicornによる公式Dockerイメージ - Uvicorn +### ベースDockerイメージ { #base-docker-image } -前の章で詳しく説明したように、Uvicornワーカーで動作するGunicornを含む公式のDockerイメージがあります: [Server Workers - Gunicorn と Uvicorn](server-workers.md){.internal-link target=_blank}で詳しく説明しています。 +以前は、公式のFastAPI Dockerイメージがありました:tiangolo/uvicorn-gunicorn-fastapi。しかし、現在は非推奨です。⛔️ -このイメージは、主に上記で説明した状況で役に立つでしょう: [複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases) +おそらく、このベースDockerイメージ(またはその他の類似のもの)は**使用しない**方がよいでしょう。 -* tiangolo/uvicorn-gunicorn-fastapi. +すでに**Kubernetes**(または他のもの)を使用していて、複数の**コンテナ**で、クラスタレベルで**レプリケーション**を設定している場合。そのような場合は、上記で説明したように**ゼロから**イメージを構築する方がよいでしょう:[FastAPI用のDockerイメージをビルドする](#build-a-docker-image-for-fastapi)。 -/// warning +また、複数のワーカーが必要な場合は、単純に `--workers` コマンドラインオプションを使用できます。 -このベースイメージや類似のイメージは**必要ない**可能性が高いので、[上記の: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi)のようにゼロからイメージをビルドする方が良いでしょう。 +/// note | 技術詳細 + +このDockerイメージは、Uvicornが停止したワーカーの管理と再起動をサポートしていなかった頃に作成されたため、Uvicornと一緒にGunicornを使う必要がありました。これは、GunicornにUvicornワーカープロセスの管理と再起動をさせるだけのために、かなりの複雑さを追加していました。 + +しかし現在は、Uvicorn(および `fastapi` コマンド)が `--workers` をサポートしているため、自分でビルドする代わりにベースDockerイメージを使う理由はありません(コード量もだいたい同じです 😅)。 /// -このイメージには、利用可能なCPUコアに基づいて**ワーカー・プロセスの数**を設定する**オートチューニング**メカニズムが含まれています。 - -これは**賢明なデフォルト**を備えていますが、**環境変数**や設定ファイルを使ってすべての設定を変更したり更新したりすることができます。 - -また、スクリプトで**開始前の事前ステップ**を実行することもサポートしている。 - -/// tip - -すべての設定とオプションを見るには、Dockerイメージのページをご覧ください: tiangolo/uvicorn-gunicorn-fastapi - -/// - -### 公式Dockerイメージのプロセス数 - -このイメージの**プロセス数**は、利用可能な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 -``` - -### いつ使うのか - -おそらく、**Kubernetes**(または他のもの)を使用していて、すでにクラスタレベルで複数の**コンテナ**で**レプリケーション**を設定している場合は、この公式ベースイメージ(または他の類似のもの)は**使用すべきではありません**。 - -そのような場合は、上記のように**ゼロから**イメージを構築する方がよいでしょう: [FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi) を参照してください。 - -このイメージは、主に上記の[複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases)で説明したような特殊なケースで役に立ちます。 - -例えば、アプリケーションが**シンプル**で、CPUに応じたデフォルトのプロセス数を設定すればうまくいく場合や、クラスタレベルでレプリケーションを手動で設定する手間を省きたい場合、アプリで複数のコンテナを実行しない場合などです。 - -または、**Docker Compose**でデプロイし、単一のサーバで実行している場合などです。 - -## コンテナ・イメージのデプロイ +## コンテナ・イメージのデプロイ { #deploy-the-container-image } コンテナ(Docker)イメージを手に入れた後、それをデプロイするにはいくつかの方法があります。 @@ -645,104 +604,21 @@ COPY ./app /app/app * Nomadのような別のツール * コンテナ・イメージをデプロイするクラウド・サービス -## Poetryを利用したDockerイメージ +## `uv` を使ったDockerイメージ { #docker-image-with-uv } -もしプロジェクトの依存関係を管理するためにPoetryを利用する場合、マルチステージビルドを使うと良いでしょう。 +uv を使ってプロジェクトのインストールと管理をしている場合は、uv Docker guideに従ってください。 -```{ .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. このDockerステージにPoetryをインストールします - -4. pyproject.toml`と`poetry.lock`ファイルを`/tmp` ディレクトリにコピーします - - `./poetry.lock*`(末尾に`*`)を使用するため、そのファイルがまだ利用できない場合でもクラッシュすることはないです。 -5. requirements.txt`ファイルを生成します - -6. これは最後のステージであり、ここにあるものはすべて最終的なコンテナ・イメージに保存されます -7. 現在の作業ディレクトリを `/code` に設定します -8. `requirements.txt`ファイルを `/code` ディレクトリにコピーします - このファイルは前のDockerステージにしか存在しないため、`--from-requirements-stage`を使ってコピーします。 -9. 生成された `requirements.txt` ファイルにあるパッケージの依存関係をインストールします -10. app` ディレクトリを `/code` ディレクトリにコピーします -11. uvicorn` コマンドを実行して、`app.main` からインポートした `app` オブジェクトを使用するように指示します -/// tip - -"+"の吹き出しをクリックすると、それぞれの行が何をするのかを見ることができます - -/// - -**Dockerステージ**は`Dockerfile`の一部で、**一時的なコンテナイメージ**として動作します。 - -最初のステージは **Poetryのインストール**と Poetry の `pyproject.toml` ファイルからプロジェクトの依存関係を含む**`requirements.txt`を生成**するためだけに使用されます。 - -この `requirements.txt` ファイルは後半の **次のステージ**で `pip` と共に使用されます。 - -最終的なコンテナイメージでは、**最終ステージ**のみが保存されます。前のステージは破棄されます。 - -Poetryを使用する場合、**Dockerマルチステージビルド**を使用することは理にかなっています。 - -なぜなら、最終的なコンテナイメージにPoetryとその依存関係がインストールされている必要はなく、**必要なのは**プロジェクトの依存関係をインストールするために生成された `requirements.txt` ファイルだけだからです。 - -そして次の(そして最終的な)ステージでは、前述とほぼ同じ方法でイメージをビルドします。 - -### TLS Termination Proxyの裏側 - Poetry - -繰り返しになりますが、NginxやTraefikのようなTLS Termination Proxy(ロードバランサー)の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションをコマンドに追加します: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## まとめ +## まとめ { #recap } コンテナ・システム(例えば**Docker**や**Kubernetes**など)を使えば、すべての**デプロイメントのコンセプト**を扱うのがかなり簡単になります: -* セキュリティ - HTTPS +* HTTPS * 起動時の実行 * 再起動 -* **レプリケーション(実行中のプロセス数)** +* レプリケーション(実行中のプロセス数) * メモリ * 開始前の事前ステップ ほとんどの場合、ベースとなるイメージは使用せず、公式のPython Dockerイメージをベースにした**コンテナイメージをゼロからビルド**します。 -`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**することができ、生産性を最大化することができます(そして退屈を避けることができます)。😎 - -特別なケースでは、FastAPI用の公式Dockerイメージを使いたいかもしれません。🤓 +`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**し、生産性を最大化できます(そして退屈を避けることができます)。😎 diff --git a/docs/ja/docs/deployment/https.md b/docs/ja/docs/deployment/https.md index 7b0f567aa..d5a6daf0c 100644 --- a/docs/ja/docs/deployment/https.md +++ b/docs/ja/docs/deployment/https.md @@ -1,10 +1,10 @@ -# HTTPS について +# HTTPS について { #about-https } HTTPSは単に「有効」か「無効」かで決まるものだと思いがちです。 しかし、それよりもはるかに複雑です。 -/// tip +/// tip | 豆知識 もし急いでいたり、HTTPSの仕組みについて気にしないのであれば、次のセクションに進み、さまざまなテクニックを使ってすべてをセットアップするステップ・バイ・ステップの手順をご覧ください。 @@ -22,25 +22,19 @@ HTTPSは単に「有効」か「無効」かで決まるものだと思いがち * 接続の暗号化は**TCPレベル**で行われます。 * それは**HTTPの1つ下**のレイヤーです。 * つまり、**証明書と暗号化**の処理は、**HTTPの前**に行われます。 -* **TCPは "ドメイン "について知りません**。IPアドレスについてのみ知っています。 +* **TCPは「ドメイン」について知りません**。IPアドレスについてのみ知っています。 * 要求された**特定のドメイン**に関する情報は、**HTTPデータ**に入ります。 * **HTTPS証明書**は、**特定のドメイン**を「証明」しますが、プロトコルと暗号化はTCPレベルで行われ、どのドメインが扱われているかを**知る前**に行われます。 * **デフォルトでは**、**IPアドレスごとに1つのHTTPS証明書**しか持てないことになります。 * これは、サーバーの規模やアプリケーションの規模に寄りません。 * しかし、これには**解決策**があります。 -* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 +* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 * このSNI拡張機能により、1つのサーバー(**単一のIPアドレス**を持つ)が**複数のHTTPS証明書**を持ち、**複数のHTTPSドメイン/アプリケーション**にサービスを提供できるようになります。 * これが機能するためには、**パブリックIPアドレス**でリッスンしている、サーバー上で動作している**単一の**コンポーネント(プログラム)が、サーバー内の**すべてのHTTPS証明書**を持っている必要があります。 - * セキュアな接続を取得した**後**でも、通信プロトコルは**HTTPのまま**です。 * コンテンツは**HTTPプロトコル**で送信されているにもかかわらず、**暗号化**されています。 - -サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。 - -**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。 - -このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 +サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 TLS Termination Proxyとして使えるオプションには、以下のようなものがあります: @@ -50,7 +44,7 @@ TLS Termination Proxyとして使えるオプションには、以下のよう * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三者によって販売されていました。 @@ -64,27 +58,27 @@ Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三 このアイデアは、これらの証明書の取得と更新を自動化することで、**安全なHTTPSを、無料で、永遠に**利用できるようにすることです。 -## 開発者のための HTTPS +## 開発者のための HTTPS { #https-for-developers } ここでは、HTTPS APIがどのように見えるかの例を、主に開発者にとって重要なアイデアに注意を払いながら、ステップ・バイ・ステップで説明します。 -### ドメイン名 +### ドメイン名 { #domain-name } ステップの初めは、**ドメイン名**を**取得すること**から始まるでしょう。その後、DNSサーバー(おそらく同じクラウドプロバイダー)に設定します。 -おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、固定の **パブリックIPアドレス**を持つことになるでしょう。 +おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、fixed **パブリックIPアドレス**を持つことになるでしょう。 -DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`Aレコード`」)を設定します。 +DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`A record`」)を設定します。 これはおそらく、最初の1回だけあり、すべてをセットアップするときに行うでしょう。 -/// tip +/// tip | 豆知識 ドメイン名の話はHTTPSに関する話のはるか前にありますが、すべてがドメインとIPアドレスに依存するため、ここで言及する価値があります。 /// -### DNS +### DNS { #dns } では、実際のHTTPSの部分に注目してみよう。 @@ -94,7 +88,7 @@ DNSサーバーは、ブラウザに特定の**IPアドレス**を使用する -### TLS Handshake の開始 +### TLS Handshake の開始 { #tls-handshake-start } ブラウザはIPアドレスと**ポート443**(HTTPSポート)で通信します。 @@ -104,7 +98,7 @@ DNSサーバーは、ブラウザに特定の**IPアドレス**を使用する TLS接続を確立するためのクライアントとサーバー間のこのやりとりは、**TLSハンドシェイク**と呼ばれます。 -### SNI拡張機能付きのTLS +### SNI拡張機能付きのTLS { #tls-with-sni-extension } サーバー内の**1つのプロセス**だけが、特定 の**IPアドレス**の特定の**ポート** で待ち受けることができます。 @@ -112,7 +106,7 @@ TLS接続を確立するためのクライアントとサーバー間のこの TLS(HTTPS)はデフォルトで`443`という特定のポートを使用する。つまり、これが必要なポートです。 -このポートをリッスンできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 +このポートをリクエストできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)にアクセスできます。 @@ -130,13 +124,13 @@ TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)に これが**HTTPS**であり、純粋な(暗号化されていない)TCP接続ではなく、**セキュアなTLS接続**の中に**HTTP**があるだけです。 -/// tip +/// tip | 豆知識 通信の暗号化は、HTTPレベルではなく、**TCPレベル**で行われることに注意してください。 /// -### HTTPS リクエスト +### HTTPS リクエスト { #https-request } これでクライアントとサーバー(具体的にはブラウザとTLS Termination Proxy)は**暗号化されたTCP接続**を持つことになり、**HTTP通信**を開始することができます。 @@ -144,19 +138,19 @@ TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)に -### リクエストの復号化 +### リクエストの復号化 { #decrypt-the-request } TLS Termination Proxy は、合意が取れている暗号化を使用して、**リクエストを復号化**し、**プレーン (復号化された) HTTP リクエスト** をアプリケーションを実行しているプロセス (例えば、FastAPI アプリケーションを実行している Uvicorn を持つプロセス) に送信します。 -### HTTP レスポンス +### HTTP レスポンス { #http-response } アプリケーションはリクエストを処理し、**プレーン(暗号化されていない)HTTPレスポンス** をTLS Termination Proxyに送信します。 -### HTTPS レスポンス +### HTTPS レスポンス { #https-response } TLS Termination Proxyは次に、事前に合意が取れている暗号(`someapp.example.com`の証明書から始まる)を使って**レスポンスを暗号化し**、ブラウザに送り返す。 @@ -166,7 +160,7 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap クライアント(ブラウザ)は、レスポンスが正しいサーバーから来たことを知ることができます。 なぜなら、そのサーバーは、以前に**HTTPS証明書**を使って合意した暗号を使っているからです。 -### 複数のアプリケーション +### 複数のアプリケーション { #multiple-applications } 同じサーバー(または複数のサーバー)に、例えば他のAPIプログラムやデータベースなど、**複数のアプリケーション**が存在する可能性があります。 @@ -176,7 +170,7 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap そうすれば、TLS Termination Proxy は、**複数のドメイン**や複数のアプリケーションのHTTPSと証明書を処理し、それぞれのケースで適切なアプリケーションにリクエストを送信することができます。 -### 証明書の更新 +### 証明書の更新 { #certificate-renewal } 将来のある時点で、各証明書は(取得後約3ヶ月で)**失効**します。 @@ -200,10 +194,42 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap アプリを提供しながらこのような更新処理を行うことは、アプリケーション・サーバー(Uvicornなど)でTLS証明書を直接使用するのではなく、TLS Termination Proxyを使用して**HTTPSを処理する別のシステム**を用意したくなる主な理由の1つです。 -## まとめ +## プロキシ転送ヘッダー { #proxy-forwarded-headers } + +プロキシを使ってHTTPSを処理する場合、**アプリケーションサーバー**(たとえばFastAPI CLI経由のUvicorn)はHTTPS処理について何も知らず、**TLS Termination Proxy**とはプレーンなHTTPで通信します。 + +この**プロキシ**は通常、リクエストを**アプリケーションサーバー**に転送する前に、その場でいくつかのHTTPヘッダーを設定し、リクエストがプロキシによって**転送**されていることをアプリケーションサーバーに知らせます。 + +/// note | 技術詳細 + +プロキシヘッダーは次のとおりです: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +それでも、**アプリケーションサーバー**は信頼できる**プロキシ**の背後にあることを知らないため、デフォルトではそれらのヘッダーを信頼しません。 + +しかし、**アプリケーションサーバー**が**プロキシ**から送信される*forwarded*ヘッダーを信頼するように設定できます。FastAPI CLIを使用している場合は、*CLI Option* `--forwarded-allow-ips` を使って、どのIPからの*forwarded*ヘッダーを信頼すべきかを指定できます。 + +たとえば、**アプリケーションサーバー**が信頼できる**プロキシ**からの通信のみを受け取っている場合、`--forwarded-allow-ips="*"` に設定して、受信するすべてのIPを信頼するようにできます。受け取るリクエストは、**プロキシ**が使用するIPからのものだけになるためです。 + +こうすることで、アプリケーションは、HTTPSを使用しているかどうか、ドメインなど、自身のパブリックURLが何であるかを把握できるようになります。 + +これは、たとえばリダイレクトを適切に処理するのに便利です。 + +/// tip | 豆知識 + +これについては、[Behind a Proxy - Enable Proxy Forwarded Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} のドキュメントで詳しく学べます。 + +/// + +## まとめ { #recap } **HTTPS**を持つことは非常に重要であり、ほとんどの場合、かなり**クリティカル**です。開発者として HTTPS に関わる労力のほとんどは、これらの**概念とその仕組みを理解する**ことです。 しかし、ひとたび**開発者向けHTTPS**の基本的な情報を知れば、簡単な方法ですべてを管理するために、さまざまなツールを組み合わせて設定することができます。 -次の章では、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 +次の章のいくつかでは、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 897956e38..eba6eae6e 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -1,7 +1,23 @@ -# デプロイ +# デプロイ { #deployment } -**FastAPI** 製のアプリケーションは比較的容易にデプロイできます。 +**FastAPI** アプリケーションのデプロイは比較的簡単です。 -ユースケースや使用しているツールによっていくつかの方法に分かれます。 +## デプロイとは { #what-does-deployment-mean } -次のセクションでより詳しくそれらの方法について説明します。 +アプリケーションを**デプロイ**するとは、**ユーザーが利用できるようにする**ために必要な手順を実行することを意味します。 + +**Web API** の場合、通常は **リモートマシン** 上に配置し、優れたパフォーマンス、安定性などを提供する **サーバープログラム** と組み合わせて、**ユーザー** が中断や問題なく効率的にアプリケーションへ**アクセス**できるようにします。 + +これは **開発** 段階とは対照的です。開発では、コードを常に変更し、壊しては直し、開発サーバーを停止したり再起動したりします。 + +## デプロイ戦略 { #deployment-strategies } + +具体的なユースケースや使用するツールによって、いくつかの方法があります。 + +複数のツールを組み合わせて自分で**サーバーをデプロイ**することもできますし、作業の一部を代行してくれる **クラウドサービス** を使うこともできます。ほかにも選択肢があります。 + +たとえば、FastAPI の開発チームである私たちは、クラウドへの FastAPI アプリのデプロイを可能な限り合理化し、FastAPI を使って開発するのと同じ開発者体験を提供するために、**FastAPI Cloud** を構築しました。 + +**FastAPI** アプリケーションをデプロイする際に、おそらく念頭に置くべき主要な概念をいくつか紹介します(ただし、そのほとんどは他の種類の Web アプリケーションにも当てはまります)。 + +次のセクションでは、留意すべき点の詳細や、それを実現するためのいくつかの手法を確認します。 ✨ diff --git a/docs/ja/docs/deployment/server-workers.md b/docs/ja/docs/deployment/server-workers.md index 38ceab017..933b875d7 100644 --- a/docs/ja/docs/deployment/server-workers.md +++ b/docs/ja/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Server Workers - Gunicorn と Uvicorn +# Server Workers - ワーカー付きUvicorn { #server-workers-uvicorn-with-workers } 前回のデプロイメントのコンセプトを振り返ってみましょう: @@ -9,124 +9,79 @@ * メモリ * 開始前の事前ステップ -ここまでのドキュメントのチュートリアルでは、おそらくUvicornのような**サーバープログラム**を**単一のプロセス**で実行しています。 +ここまでのドキュメントのチュートリアルでは、おそらく `fastapi` コマンドなど(Uvicornを実行するもの)を使って、**単一のプロセス**として動作する**サーバープログラム**を実行してきたはずです。 アプリケーションをデプロイする際には、**複数のコア**を利用し、そしてより多くのリクエストを処理できるようにするために、プロセスの**レプリケーション**を持つことを望むでしょう。 前のチャプターである[デプロイメントのコンセプト](concepts.md){.internal-link target=_blank}にて見てきたように、有効な戦略がいくつかあります。 -ここでは**Gunicorn**が**Uvicornのワーカー・プロセス**を管理する場合の使い方について紹介していきます。 +ここでは、`fastapi` コマンド、または `uvicorn` コマンドを直接使って、**ワーカープロセス**付きの **Uvicorn** を使う方法を紹介します。 -/// info +/// info | 情報 - -DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank} +DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}。 -特に**Kubernetes**上で実行する場合は、おそらく**Gunicornを使用せず**、**コンテナごとに単一のUvicornプロセス**を実行することになりますが、それについてはこの章の後半で説明します。 +特に**Kubernetes**上で実行する場合は、おそらくワーカーは使わず、代わりに**コンテナごとに単一のUvicornプロセス**を実行したいはずですが、それについてはその章の後半で説明します。 /// -## GunicornによるUvicornのワーカー・プロセスの管理 +## 複数ワーカー { #multiple-workers } -**Gunicorn**は**WSGI標準**のアプリケーションサーバーです。このことは、GunicornはFlaskやDjangoのようなアプリケーションにサービスを提供できることを意味します。Gunicornそれ自体は**FastAPI**と互換性がないですが、というのもFastAPIは最新の**ASGI 標準**を使用しているためです。 +`--workers` コマンドラインオプションで複数のワーカーを起動できます。 -しかし、Gunicornは**プロセスマネージャー**として動作し、ユーザーが特定の**ワーカー・プロセスクラス**を使用するように指示することができます。するとGunicornはそのクラスを使い1つ以上の**ワーカー・プロセス**を開始します。 +//// tab | `fastapi` -そして**Uvicorn**には**Gunicorn互換のワーカークラス**があります。 - -この組み合わせで、Gunicornは**プロセスマネージャー**として動作し、**ポート**と**IP**をリッスンします。そして、**Uvicornクラス**を実行しているワーカー・プロセスに通信を**転送**します。 - -そして、Gunicorn互換の**Uvicornワーカー**クラスが、FastAPIが使えるように、Gunicornから送られてきたデータをASGI標準に変換する役割を担います。 - -## GunicornとUvicornをインストールする +`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. ```
-これによりUvicornと(高性能を得るための)標準(`standard`)の追加パッケージとGunicornの両方がインストールされます。 +//// -## UvicornのワーカーとともにGunicornを実行する +//// tab | `uvicorn` -Gunicornを以下のように起動させることができます: - -
- -```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`: `main`は"`main`"という名前のPythonモジュール、つまりファイル`main.py`を意味します。そして `app` は **FastAPI** アプリケーションの変数名です。 - * main:app`はPythonの`import`文と同じようなものだと想像できます: - - ```Python - from main import app - ``` - - * つまり、`main:app`のコロンは、`from main import app`のPythonの`import`の部分と同じになります。 - -* `--workers`: 使用するワーカー・プロセスの数で、それぞれがUvicornのワーカーを実行します。 - -* `--worker-class`: ワーカー・プロセスで使用するGunicorn互換のワーカークラスです。 - * ここではGunicornがインポートして使用できるクラスを渡します: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: GunicornにリッスンするIPとポートを伝えます。コロン(`:`)でIPとポートを区切ります。 - * Uvicornを直接実行している場合は、`--bind 0.0.0.0:80` (Gunicornのオプション)の代わりに、`--host 0.0.0.0`と `--port 80`を使います。 - -出力では、各プロセスの**PID**(プロセスID)が表示されているのがわかります(単なる数字です)。 - -以下の通りです: - -* Gunicornの**プロセス・マネージャー**はPID `19499`(あなたの場合は違う番号でしょう)で始まります。 -* 次に、`Listening at: http://0.0.0.0:80`を開始します。 -* それから `uvicorn.workers.UvicornWorker` でワーカークラスを使用することを検出します。 -* そして、**4つのワーカー**を起動します。それぞれのワーカーのPIDは、`19511`、`19513`、`19514`、`19515`です。 - -Gunicornはまた、ワーカーの数を維持するために必要であれば、**ダウンしたプロセス**を管理し、**新しいプロセスを**再起動**させます。そのため、上記のリストにある**再起動**の概念に一部役立ちます。 - -しかしながら、必要であればGunicornを**再起動**させ、**起動時に実行**させるなど、外部のコンポーネントを持たせることも必要かもしれません。 - -## Uvicornとワーカー - -Uvicornには複数の**ワーカー・プロセス**を起動し実行するオプションもあります。 - -とはいうものの、今のところUvicornのワーカー・プロセスを扱う機能はGunicornよりも制限されています。そのため、このレベル(Pythonレベル)でプロセスマネージャーを持ちたいのであれば、Gunicornをプロセスマネージャーとして使ってみた方が賢明かもしれないです。 - -どんな場合であれ、以下のように実行します: +`uvicorn` コマンドを直接使いたい場合:
@@ -150,36 +105,35 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
-ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカー・プロセスを起動するように指示しています。 +//// -各プロセスの **PID** が表示され、親プロセスの `27365` (これは **プロセスマネージャ**) と、各ワーカー・プロセスの **PID** が表示されます: `27368`、`27369`、`27370`、`27367`になります。 +ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカープロセスを起動するように指示しています。 -## デプロイメントのコンセプト +各プロセスの **PID** も表示されていて、親プロセス(これは**プロセスマネージャー**)が `27365`、各ワーカープロセスがそれぞれ `27368`、`27369`、`27370`、`27367` です。 -ここでは、アプリケーションの実行を**並列化**し、CPUの**マルチコア**を活用し、**より多くのリクエスト**に対応できるようにするために、**Gunicorn**(またはUvicorn)を使用して**Uvicornワーカー・プロセス**を管理する方法を見ていきました。 +## デプロイメントのコンセプト { #deployment-concepts } -上記のデプロイのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれます: +ここでは、複数の **ワーカー** を使ってアプリケーションの実行を**並列化**し、CPUの**複数コア**を活用して、**より多くのリクエスト**を処理できるようにする方法を見てきました。 -* セキュリティ - HTTPS -* 起動時の実行 -* 再起動 +上のデプロイメントのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれますが、それ以外については引き続き対処が必要です: + +* **セキュリティ - HTTPS** +* **起動時の実行** +* ***再起動*** * レプリケーション(実行中のプロセス数) -* メモリー -* 開始前の事前のステップ +* **メモリ** +* **開始前の事前ステップ** +## コンテナとDocker { #containers-and-docker } -## コンテナとDocker - -次章の[コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}では、その他の**デプロイのコンセプト**を扱うために実施するであろう戦略をいくつか紹介します。 +次章の[コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}では、その他の**デプロイメントのコンセプト**を扱うために使える戦略をいくつか説明します。 -また、**GunicornとUvicornワーカー**を含む**公式Dockerイメージ**と、簡単なケースに役立ついくつかのデフォルト設定も紹介します。 +単一のUvicornプロセスを実行するために、**ゼロから独自のイメージを構築する**方法も紹介します。これは簡単なプロセスで、**Kubernetes**のような分散コンテナ管理システムを使う場合に、おそらくやりたいことでしょう。 -また、(Gunicornを使わずに)Uvicornプロセスを1つだけ実行するために、**ゼロから独自のイメージを**構築する方法も紹介します。これは簡単なプロセスで、おそらく**Kubernetes**のような分散コンテナ管理システムを使うときにやりたいことでしょう。 +## まとめ { #recap } -## まとめ +`fastapi` または `uvicorn` コマンドで `--workers` CLIオプションを使うことで、**マルチコアCPU**を活用し、**複数のプロセスを並列実行**できるように複数のワーカープロセスを利用できます。 -Uvicornワーカーを使ったプロセスマネージャとして**Gunicorn**(またはUvicorn)を使えば、**マルチコアCPU**を活用して**複数のプロセスを並列実行**できます。 +他のデプロイメントのコンセプトを自分で対応しながら、**独自のデプロイシステム**を構築している場合にも、これらのツールやアイデアを使えます。 -これらのツールやアイデアは、**あなた自身のデプロイシステム**をセットアップしながら、他のデプロイコンセプトを自分で行う場合にも使えます。 - -次の章では、コンテナ(DockerやKubernetesなど)を使った**FastAPI**について学んでいきましょう。これらのツールには、他の**デプロイのコンセプト**も解決する簡単な方法があることがわかるでしょう。✨ +次の章で、コンテナ(例:DockerやKubernetes)を使った **FastAPI** について学びましょう。これらのツールにも、他の**デプロイメントのコンセプト**を解決する簡単な方法があることがわかります。✨ diff --git a/docs/ja/docs/deployment/versions.md b/docs/ja/docs/deployment/versions.md index 7575fc4f7..7980b8be2 100644 --- a/docs/ja/docs/deployment/versions.md +++ b/docs/ja/docs/deployment/versions.md @@ -1,93 +1,93 @@ -# FastAPIのバージョンについて +# FastAPIのバージョンについて { #about-fastapi-versions } -**FastAPI** は既に多くのアプリケーションやシステムに本番環境で使われています。また、100%のテストカバレッジを維持しています。しかし、活発な開発が続いています。 +**FastAPI** はすでに多くのアプリケーションやシステムで本番環境にて使われています。また、テストカバレッジは 100% に維持されています。しかし、開発は依然として急速に進んでいます。 -高頻度で新機能が追加され、定期的にバグが修正され、実装は継続的に改善されています。 +新機能が高頻度で追加され、定期的にバグが修正され、コードは継続的に改善されています。 これが現在のバージョンがいまだに `0.x.x` な理由であり、それぞれのバージョンは破壊的な変更がなされる可能性があります。これは、セマンティック バージョニングの規則に則っています。 -**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 } + +セマンティック バージョニングの規約に従って、`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 } -```txt -fastapi>=0.45.0,<0.46.0 -``` +アプリケーションにテストを追加すべきです。 -破壊的な変更と新機能実装は「マイナー」バージョンで加えられます。 +**FastAPI** では非常に簡単に実現できます(Starlette のおかげです)。ドキュメントを確認して下さい: [テスト](../tutorial/testing.md){.internal-link target=_blank} -/// tip | 豆知識 +テストを追加したら、**FastAPI** のバージョンをより新しいものにアップグレードし、テストを実行することで全てのコードが正しく動作するか確認できます。 -「マイナー」は真ん中の数字です。例えば、`0.2.3` ではマイナーバージョンは `2` です。 +全てが動作する、または必要な変更を行った後に全てのテストが通るなら、その新しいバージョンに `fastapi` を固定できます。 -/// +## Starletteについて { #about-starlette } -## FastAPIのバージョンのアップグレード +`starlette` のバージョンは固定すべきではありません。 -アプリケーションにテストを加えるべきです。 +**FastAPI** のバージョンが異なれば、Starlette の特定のより新しいバージョンが使われます。 -**FastAPI** では非常に簡単に実現できます (Starletteのおかげで)。ドキュメントを確認して下さい: [テスト](../tutorial/testing.md){.internal-link target=_blank} +そのため、正しい Starlette バージョンを **FastAPI** に任せればよいです。 -テストを加えた後で、**FastAPI** のバージョンをより最新のものにアップグレードし、テストを実行することで全てのコードが正常に動作するか確認できます。 +## Pydanticについて { #about-pydantic } -全てが動作するか、修正を行った上で全てのテストを通過した場合、使用している`fastapi` のバージョンをより最新のバージョンに固定できます。 +Pydantic は自身のテストに **FastAPI** のテストも含んでいるため、Pydantic の新しいバージョン(`1.0.0` より上)は常に FastAPI と互換性があります。 -## Starletteについて - -`Starlette` のバージョンは固定すべきではありません。 - -**FastAPI** は、バージョン毎にStarletteのより新しいバージョンを使用します。 - -よって、最適なStarletteのバージョン選択を**FastAPI** に任せることができます。 - -## Pydanticについて - -Pydanticは自身のテストだけでなく**FastAPI** のためのテストを含んでいます。なので、Pydanticの新たなバージョン ( `1.0.0` 以降) は全てFastAPIと整合性があります。 - -Pydanticのバージョンを、動作が保証できる`1.0.0`以降のいずれかのバージョンから`2.0.0` 未満の間に固定できます。 +Pydantic は、自分にとって動作する `1.0.0` より上の任意のバージョンに固定できます。 例えば: ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/ja/docs/environment-variables.md b/docs/ja/docs/environment-variables.md index 507af3a0c..45dbfc71f 100644 --- a/docs/ja/docs/environment-variables.md +++ b/docs/ja/docs/environment-variables.md @@ -1,18 +1,18 @@ -# 環境変数 +# 環境変数 { #environment-variables } -/// tip +/// tip | 豆知識 もし、「環境変数」とは何か、それをどう使うかを既に知っている場合は、このセクションをスキップして構いません。 /// -環境変数(**env var**とも呼ばれる)はPythonコードの**外側**、つまり**OS**に存在する変数で、Pythonから読み取ることができます。(他のプログラムでも同様に読み取れます。) +環境変数(「**env var**」とも呼ばれます)とは、Pythonコードの**外側**、つまり**オペレーティングシステム**に存在する変数で、Pythonコード(または他のプログラム)から読み取れます。 -環境変数は、アプリケーションの**設定**の管理や、Pythonの**インストール**などに役立ちます。 +環境変数は、アプリケーションの**設定**の扱い、Pythonの**インストール**の一部などで役立ちます。 -## 環境変数の作成と使用 +## 環境変数の作成と使用 { #create-and-use-env-vars } -環境変数は**シェル(ターミナル)**内で**作成**して使用でき、それらにPythonは不要です。 +環境変数は、Pythonを必要とせず、**シェル(ターミナル)**で**作成**して使用できます。 //// tab | Linux, macOS, Windows Bash @@ -36,7 +36,6 @@ Hello Wade Wilson
- ```console // Create an env var MY_NAME $ $Env:MY_NAME = "Wade Wilson" @@ -51,9 +50,9 @@ Hello Wade Wilson //// -## Pythonで環境変数を読み取る +## Pythonで環境変数を読み取る { #read-env-vars-in-python } -環境変数をPythonの**外側**、ターミナル(や他の方法)で作成し、**Python内で読み取る**こともできます。 +環境変数はPythonの**外側**(ターミナル、またはその他の方法)で作成し、その後に**Pythonで読み取る**こともできます。 例えば、以下のような`main.py`ファイルを用意します: @@ -64,11 +63,11 @@ name = os.getenv("MY_NAME", "World") print(f"Hello {name} from Python") ``` -/// tip +/// tip | 豆知識 -`os.getenv()` の第2引数は、デフォルトで返される値を指定します。 +`os.getenv()` の第2引数は、デフォルトで返される値です。 -この引数を省略するとデフォルト値として`None`が返されますが、ここではデフォルト値として`"World"`を指定しています。 +指定しない場合、デフォルトは`None`ですが、ここでは使用するデフォルト値として`"World"`を指定しています。 /// @@ -128,11 +127,11 @@ Hello Wade Wilson from Python //// -環境変数はコードの外側で設定し、内側から読み取ることができるので、他のファイルと一緒に(`git`に)保存する必要がありません。そのため、環境変数をコンフィグレーションや**設定**に使用することが一般的です。 +環境変数はコードの外側で設定でき、コードから読み取れ、他のファイルと一緒に(`git`に)保存(コミット)する必要がないため、設定や**settings**に使うのが一般的です。 -また、**特定のプログラムの呼び出し**のための環境変数を、そのプログラムのみ、その実行中に限定して利用できるよう作成できます。 +また、**特定のプログラムの呼び出し**のためだけに、そのプログラムでのみ、実行中の間だけ利用できる環境変数を作成することもできます。 -そのためには、プログラム起動コマンドと同じコマンドライン上の、起動コマンド直前で環境変数を作成してください。 +そのためには、同じ行で、プログラム自体の直前に作成してください。
@@ -152,25 +151,25 @@ Hello World from Python
-/// tip +/// tip | 豆知識 詳しくは The Twelve-Factor App: Config を参照してください。 /// -## 型とバリデーション +## 型とバリデーション { #types-and-validation } -環境変数は**テキスト文字列**のみを扱うことができます。これは、環境変数がPython外部に存在し、他のプログラムやシステム全体(Linux、Windows、macOS間の互換性を含む)と連携する必要があるためです。 +これらの環境変数が扱えるのは**テキスト文字列**のみです。環境変数はPythonの外部にあり、他のプログラムやシステム全体(Linux、Windows、macOSなど異なるオペレーティングシステム間も)との互換性が必要になるためです。 -つまり、Pythonが環境変数から読み取る**あらゆる値**は **`str`型となり**、他の型への変換やバリデーションはコード内で行う必要があります。 +つまり、環境変数からPythonで読み取る**あらゆる値**は **`str`になり**、他の型への変換やバリデーションはコード内で行う必要があります。 -環境変数を使用して**アプリケーション設定**を管理する方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}で詳しく学べます。 +環境変数を使って**アプリケーション設定**を扱う方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank} で詳しく学べます。 -## `PATH`環境変数 +## `PATH`環境変数 { #path-environment-variable } -**`PATH`**という**特別な**環境変数があります。この環境変数は、OS(Linux、macOS、Windows)が実行するプログラムを発見するために使用されます。 +**`PATH`**という**特別な**環境変数があります。これはオペレーティングシステム(Linux、macOS、Windows)が実行するプログラムを見つけるために使用されます。 -`PATH`変数は、複数のディレクトリのパスから成る長い文字列です。このパスはLinuxやMacOSの場合は`:`で、Windowsの場合は`;`で区切られています。 +変数`PATH`の値は長い文字列で、LinuxとmacOSではコロン`:`、Windowsではセミコロン`;`で区切られたディレクトリで構成されます。 例えば、`PATH`環境変数は次のような文字列かもしれません: @@ -180,7 +179,7 @@ Hello World from Python /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin ``` -これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: +これは、システムが次のディレクトリでプログラムを探すことを意味します: * `/usr/local/bin` * `/usr/bin` @@ -196,7 +195,7 @@ Hello World from Python C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 ``` -これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: +これは、システムが次のディレクトリでプログラムを探すことを意味します: * `C:\Program Files\Python312\Scripts` * `C:\Program Files\Python312` @@ -204,63 +203,61 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 //// -ターミナル上で**コマンド**を入力すると、 OSはそのプログラムを見つけるために、`PATH`環境変数のリストに記載された**それぞれのディレクトリを探し**ます。 +ターミナル上で**コマンド**を入力すると、オペレーティングシステムは`PATH`環境変数に記載された**それぞれのディレクトリ**の中からプログラムを**探し**ます。 -例えば、ターミナル上で`python`を入力すると、OSは`python`によって呼ばれるプログラムを見つけるために、そのリストの**先頭のディレクトリ**を最初に探します。 +例えば、ターミナルで`python`と入力すると、オペレーティングシステムはそのリストの**最初のディレクトリ**で`python`というプログラムを探します。 -OSは、もしそのプログラムをそこで発見すれば**実行し**ますが、そうでなければリストの**他のディレクトリ**を探していきます。 +見つかればそれを**使用**します。見つからなければ、**他のディレクトリ**を探し続けます。 -### PythonのインストールとPATH環境変数の更新 +### Pythonのインストールと`PATH`の更新 { #installing-python-and-updating-the-path } -Pythonのインストール時に`PATH`環境変数を更新したいか聞かれるかもしれません。 +Pythonのインストール時に、`PATH`環境変数を更新するかどうかを尋ねられるかもしれません。 -/// tab | Linux, macOS +//// tab | Linux, macOS -Pythonをインストールして、そのプログラムが`/opt/custompython/bin`というディレクトリに配置されたとします。 +Pythonをインストールして、その結果`/opt/custompython/bin`というディレクトリに配置されたとします。 -もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`/opt/custompython/bin`が追加されます。 +`PATH`環境変数を更新することに同意すると、インストーラーは`PATH`環境変数に`/opt/custompython/bin`を追加します。 -`PATH`環境変数は以下のように更新されるでしょう: +例えば次のようになります: -``` plaintext +```plaintext /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin ``` -このようにして、ターミナルで`python`と入力したときに、OSは`/opt/custompython/bin`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 +このようにして、ターミナルで`python`と入力すると、システムは`/opt/custompython/bin`(最後のディレクトリ)にあるPythonプログラムを見つけ、それを使用します。 -/// +//// -/// tab | Windows +//// tab | Windows -Pythonをインストールして、そのプログラムが`C:\opt\custompython\bin`というディレクトリに配置されたとします。 +Pythonをインストールして、その結果`C:\opt\custompython\bin`というディレクトリに配置されたとします。 -もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`C:\opt\custompython\bin`が追加されます。 - -`PATH`環境変数は以下のように更新されるでしょう: +`PATH`環境変数を更新することに同意すると、インストーラーは`PATH`環境変数に`C:\opt\custompython\bin`を追加します。 ```plaintext C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin ``` -このようにして、ターミナルで`python`と入力したときに、OSは`C:\opt\custompython\bin\python`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 +このようにして、ターミナルで`python`と入力すると、システムは`C:\opt\custompython\bin`(最後のディレクトリ)にあるPythonプログラムを見つけ、それを使用します。 -/// +//// -つまり、ターミナルで以下のコマンドを入力すると: +つまり、ターミナルで次のように入力すると:
-``` console +```console $ python ```
-/// tab | Linux, macOS +//// tab | Linux, macOS -OSは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 +システムは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 -これは、次のコマンドを入力した場合とほとんど同等です: +これは、次のように入力するのとおおむね同等です:
@@ -270,13 +267,13 @@ $ /opt/custompython/bin/python
-/// +//// -/// tab | Windows +//// tab | Windows -OSは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 +システムは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 -これは、次のコマンドを入力した場合とほとんど同等です: +これは、次のように入力するのとおおむね同等です:
@@ -286,16 +283,16 @@ $ C:\opt\custompython\bin\python
-/// +//// -この情報は、[Virtual Environments](virtual-environments.md) について学ぶ際にも役立ちます。 +この情報は、[Virtual Environments](virtual-environments.md){.internal-link target=_blank} について学ぶ際にも役立ちます。 -## まとめ +## まとめ { #conclusion } これで、**環境変数**とは何か、Pythonでどのように使用するかについて、基本的な理解が得られたはずです。 -環境変数についての詳細は、Wikipedia: Environment Variable を参照してください。 +環境変数についての詳細は、Wikipedia for Environment Variable も参照してください。 -環境変数の用途や適用方法が最初は直感的ではないかもしれませんが、開発中のさまざまなシナリオで繰り返し登場します。そのため、基本を知っておくことが重要です。 +多くの場合、環境変数がどのように役立ち、すぐに適用できるのかはあまり明確ではありません。しかし、開発中のさまざまなシナリオで何度も登場するため、知っておくとよいでしょう。 -たとえば、この情報は次のセクションで扱う[Virtual Environments](virtual-environments.md)にも関連します。 +例えば、次のセクションの[Virtual Environments](virtual-environments.md)でこの情報が必要になります。 diff --git a/docs/ja/docs/how-to/conditional-openapi.md b/docs/ja/docs/how-to/conditional-openapi.md index bfaa9e6d7..9478f5c03 100644 --- a/docs/ja/docs/how-to/conditional-openapi.md +++ b/docs/ja/docs/how-to/conditional-openapi.md @@ -1,8 +1,8 @@ -# 条件付き OpenAPI +# 条件付き OpenAPI { #conditional-openapi } 必要であれば、設定と環境変数を利用して、環境に応じて条件付きでOpenAPIを構成することが可能です。また、完全にOpenAPIを無効にすることもできます。 -## セキュリティとAPI、およびドキュメントについて +## セキュリティとAPI、およびドキュメントについて { #about-security-apis-and-docs } 本番環境においてドキュメントのUIを非表示にすることによって、APIを保護しようと *すべきではありません*。 @@ -17,19 +17,19 @@ * リクエストボディとレスポンスのためのPydanticモデルの定義を見直す。 * 依存関係に基づきすべての必要なパーミッションとロールを設定する。 * パスワードを絶対に平文で保存しない。パスワードハッシュのみを保存する。 -* PasslibやJWTトークンに代表される、よく知られた暗号化ツールを使って実装する。 +* pwdlibやJWTトークンに代表される、よく知られた暗号化ツールを使って実装する。 * そして必要なところでは、もっと細かいパーミッション制御をOAuth2スコープを使って行う。 -* など +* ...など それでも、例えば本番環境のような特定の環境のみで、あるいは環境変数の設定によってAPIドキュメントをどうしても無効にしたいという、非常に特殊なユースケースがあるかもしれません。 -## 設定と環境変数による条件付き OpenAPI +## 設定と環境変数による条件付き OpenAPI { #conditional-openapi-from-settings-and-env-vars } 生成するOpenAPIとドキュメントUIの構成は、共通のPydanticの設定を使用して簡単に切り替えられます。 例えば、 -{* ../../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/ja/docs/index.md b/docs/ja/docs/index.md index 8dee1ee03..67e01ed53 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -1,14 +1,14 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + FastAPI フレームワーク、高パフォーマンス、学びやすい、素早くコーディングできる、本番運用に対応

@@ -27,129 +27,138 @@ --- -**ドキュメント**: https://fastapi.tiangolo.com +**ドキュメント**: https://fastapi.tiangolo.com **ソースコード**: https://github.com/fastapi/fastapi --- -FastAPI は、Pythonの標準である型ヒントに基づいてPython 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 +FastAPI は、Python の標準である型ヒントに基づいて Python で API を構築するための、モダンで、高速(高パフォーマンス)な Web フレームワークです。 主な特徴: -- **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげです)。 [最も高速な Python フレームワークの一つです](#_10). +* **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス(Starlette と Pydantic のおかげです)。 [利用可能な最も高速な Python フレームワークの一つです](#performance)。 +* **高速なコーディング**: 開発速度を約 200%〜300% 向上させます。* +* **少ないバグ**: 開発者起因のヒューマンエラーを約 40% 削減します。* +* **直感的**: 素晴らしいエディタサポート。あらゆる場所で 補完 が使えます。デバッグ時間を削減します。 +* **簡単**: 簡単に利用・習得できるようにデザインされています。ドキュメントを読む時間を削減します。 +* **短い**: コードの重複を最小限にします。各パラメータ宣言から複数の機能を得られます。バグも減ります。 +* **堅牢性**: 自動対話型ドキュメントにより、本番環境向けのコードが得られます。 +* **Standards-based**: API のオープンスタンダードに基づいており(そして完全に互換性があります)、OpenAPI(以前は Swagger として知られていました)や JSON Schema をサポートします。 -- **高速なコーディング**: 開発速度を約 200%~300%向上させます。 \* -- **少ないバグ**: 開発者起因のヒューマンエラーを約 40%削減します。 \* -- **直感的**: 素晴らしいエディタのサポートや オートコンプリート。 デバッグ時間を削減します。 -- **簡単**: 簡単に利用、習得できるようにデザインされています。ドキュメントを読む時間を削減します。 -- **短い**: コードの重複を最小限にしています。各パラメータからの複数の機能。少ないバグ。 -- **堅牢性**: 自動対話ドキュメントを使用して、本番環境で使用できるコードを取得します。 -- **Standards-based**: API のオープンスタンダードに基づいており、完全に互換性があります: OpenAPI (以前は Swagger として知られていました) や JSON スキーマ. +* 本番アプリケーションを構築している社内開発チームのテストに基づく見積もりです。 -\* 本番アプリケーションを構築している開発チームのテストによる見積もり。 - -## Sponsors +## 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 %} -Other sponsors +その他のスポンサー -## 評価 +## 評価 { #opinions } -"_[...] 最近 **FastAPI** を使っています。 [...] 実際に私のチームの全ての **Microsoft の機械学習サービス** で使用する予定です。 そのうちのいくつかのコアな**Windows**製品と**Office**製品に統合されつつあります。_" +"_[...] 最近 **FastAPI** を使っています。 [...] 実際に私のチームの全ての **Microsoft の機械学習サービス** で使用する予定です。 そのうちのいくつかのコアな **Windows** 製品と **Office** 製品に統合されつつあります。_"

Kabir Khan - Microsoft (ref)
--- -"_FastAPIライブラリを採用し、クエリで**予測値**を取得できる**REST**サーバを構築しました。 [for Ludwig]_" +"_FastAPIライブラリを採用し、クエリで **予測値** を取得できる **REST** サーバを構築しました。 [for Ludwig]_"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** は、**危機管理**オーケストレーションフレームワーク、**Dispatch**のオープンソースリリースを発表できることをうれしく思います。 [built with **FastAPI**]_" +"_**Netflix** は、**危機管理**オーケストレーションフレームワーク、**Dispatch** のオープンソースリリースを発表できることをうれしく思います。 [built with **FastAPI**]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_私は**FastAPI**にワクワクしています。 めちゃくちゃ楽しいです!_" +"_私は **FastAPI** にワクワクしています。 めちゃくちゃ楽しいです!_"
Brian Okken - Python Bytes podcast host (ref)
--- -"_正直、超堅実で洗練されているように見えます。いろんな意味で、それは私がハグしたかったものです。_" +"_正直、あなたが作ったものは超堅実で洗練されているように見えます。いろんな意味で、それは私が **Hug** にそうなってほしかったものです。誰かがそれを作るのを見るのは本当に刺激的です。_"
Timothy Crosley - Hug creator (ref)
--- -"_REST API を構築するための**モダンなフレームワーク**を学びたい方は、**FastAPI** [...] をチェックしてみてください。 [...] 高速で, 使用、習得が簡単です。[...]_" +"_REST API を構築するための **モダンなフレームワーク** を学びたい方は、**FastAPI** [...] をチェックしてみてください。 [...] 高速で、使用・習得が簡単です [...]_" -"_私たちの**API**は**FastAPI**に切り替えました。[...] きっと気に入ると思います。 [...]_" +"_私たちの **API** は **FastAPI** に切り替えました [...] きっと気に入ると思います [...]_"
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
--- -## **Typer**, the FastAPI of CLIs +"_本番運用の 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 } -もし Web API の代わりにターミナルで使用するCLIアプリを構築する場合は、**Typer**を確認してください。 +Web API の代わりにターミナルで使用する CLI アプリを構築する場合は、**Typer** を確認してください。 -**Typer**は FastAPI の弟分です。そして、**CLI 版 の FastAPI**を意味しています。 +**Typer** は FastAPI の弟分です。そして、**CLI 版 FastAPI** を意図しています。 ⌨️ 🚀 -## 必要条件 +## 必要条件 { #requirements } FastAPI は巨人の肩の上に立っています。 -- Web の部分はStarlette -- データの部分はPydantic +* Web の部分は Starlette +* データの部分は Pydantic -## インストール +## インストール { #installation } + +virtual environment を作成して有効化し、それから FastAPI をインストールします。
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-本番環境では、Uvicorn または、 Hypercornのような、 ASGI サーバーが必要になります。 +**注**: すべてのターミナルで動作するように、`"fastapi[standard]"` は必ずクォートで囲んでください。 -
+## アプリケーション例 { #example } -```console -$ pip install "uvicorn[standard]" +### 作成 { #create-it } ----> 100% -``` - -
- -## アプリケーション例 - -### アプリケーションの作成 - -- `main.py` を作成し、以下のコードを入力します: +`main.py` ファイルを作成し、以下のコードを入力します。 ```Python from fastapi import FastAPI @@ -163,16 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: str = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-またはasync defを使います... +または async def を使います... -`async` / `await`を使用するときは、 `async def`を使います: +コードで `async` / `await` を使用する場合は、`async def` を使います。 -```Python hl_lines="7 12" +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -184,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: str = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` **注**: -わからない場合は、ドキュメントの`async` と `await`にある"In a hurry?"セクションをチェックしてください。 +わからない場合は、ドキュメントの `async` と `await` の _"In a hurry?"_ セクションを確認してください。
-### 実行 +### 実行 { #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. ``` @@ -213,56 +235,56 @@ 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 docs を参照してください。
-### 動作確認 +### 動作確認 { #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"} ``` -もうすでに以下の API が作成されています: +すでに以下の API が作成されています。 -- `/` と `/items/{item_id}`のパスで HTTP リクエストを受けます。 -- どちらのパスも `GET` 操作 を取ります。(HTTP メソッドとしても知られています。) -- `/items/{item_id}` パスのパスパラメータ `item_id` は `int` でなければなりません。 -- パス `/items/{item_id}` はオプションの `str` クエリパラメータ `q` を持ちます。 +* _パス_ `/` と `/items/{item_id}` で HTTP リクエストを受け取ります。 +* 両方の _パス_ は `GET` 操作(HTTP _メソッド_ としても知られています)を取ります。 +* _パス_ `/items/{item_id}` は `int` であるべき _パスパラメータ_ `item_id` を持ちます。 +* _パス_ `/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 } -`PUT`リクエストからボディを受け取るために`main.py`を修正しましょう。 +次に、`PUT` リクエストからボディを受け取るために `main.py` ファイルを修正しましょう。 -Pydantic によって、Python の標準的な型を使ってボディを宣言します。 +Pydantic によって、標準的な Python の型を使ってボディを宣言します。 -```Python hl_lines="2 7 8 9 10 23 24 25" +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -272,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: bool = None + is_offer: bool | None = None @app.get("/") @@ -281,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: str = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -290,173 +312,248 @@ 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にアクセスしましょう。 +次に、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 にアクセスします。 -- 代替の API ドキュメントにも新しいクエリパラメータやボディが反映されます。 +* 代替のドキュメントにも新しいクエリパラメータやボディが反映されます。 ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### まとめ +### まとめ { #recap } -要約すると、関数のパラメータとして、パラメータやボディ などの型を**一度だけ**宣言します。 +要約すると、関数のパラメータとして、パラメータやボディなどの型を **一度だけ** 宣言します。 -標準的な最新の Python の型を使っています。 +標準的な最新の Python の型を使います。 新しい構文や特定のライブラリのメソッドやクラスなどを覚える必要はありません。 -単なる標準的な**3.8 以降の Python**です。 +単なる標準的な **Python** です。 -例えば、`int`の場合: +例えば、`int` の場合: ```Python item_id: int ``` -または、より複雑な`Item`モデルの場合: +または、より複雑な `Item` モデルの場合: ```Python item: Item ``` -...そして、この一度の宣言で、以下のようになります: +...そして、この一度の宣言で、以下のようになります。 -- 以下を含むエディタサポート: - - 補完 - - タイプチェック -- データの検証: - - データが無効な場合に自動でエラーをクリアします。 - - 深い入れ子になった JSON オブジェクトでも検証が可能です。 -- 入力データの変換: ネットワークから Python のデータや型に変換してから読み取ります: - - JSON. - - パスパラメータ - - クエリパラメータ - - クッキー - - ヘッダー - - フォーム - - ファイル -- 出力データの変換: Python のデータや型からネットワークデータへ変換します (JSON として): - - Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - - `datetime` オブジェクト - - `UUID` オブジェクト - - データベースモデル - - ...などなど -- 2 つの代替ユーザーインターフェースを含む自動インタラクティブ API ドキュメント: - - Swagger UI. - - ReDoc. +* 以下を含むエディタサポート: + * 補完。 + * 型チェック。 +* データの検証: + * データが無効な場合に自動で明確なエラーを返します。 + * 深い入れ子になった JSON オブジェクトでも検証が可能です。 +* 入力データの 変換: ネットワークから Python のデータや型へ。以下から読み取ります: + * JSON。 + * パスパラメータ。 + * クエリパラメータ。 + * Cookie。 + * ヘッダー。 + * フォーム。 + * ファイル。 +* 出力データの 変換: Python のデータや型からネットワークデータへ(JSON として)変換します: + * Python の型(`str`、`int`、`float`、`bool`、`list` など)の変換。 + * `datetime` オブジェクト。 + * `UUID` オブジェクト。 + * データベースモデル。 + * ...などなど。 +* 2 つの代替ユーザーインターフェースを含む自動対話型 API ドキュメント: + * Swagger UI。 + * ReDoc。 --- -コード例に戻りましょう、**FastAPI** は次のようになります: +前のコード例に戻ると、**FastAPI** は次のように動作します。 -- `GET`および`PUT`リクエストのパスに`item_id` があることを検証します。 -- `item_id`が`GET`および`PUT`リクエストに対して`int` 型であることを検証します。 - - そうでない場合は、クライアントは有用で明確なエラーが表示されます。 -- `GET` リクエストに対してオプションのクエリパラメータ `q` (`http://127.0.0.1:8000/items/foo?q=somequery` のように) が存在するかどうかを調べます。 - - パラメータ `q` は `= None` で宣言されているので、オプションです。 - - `None`がなければ必須になります(`PUT`の場合のボディと同様です)。 -- `PUT` リクエストを `/items/{item_id}` に送信する場合は、ボディを JSON として読み込みます: - - 必須の属性 `name` を確認してください。 それは `str` であるべきです。 - - 必須の属性 `price` を確認してください。それは `float` でなければならないです。 - - オプションの属性 `is_offer` を確認してください。値がある場合は、`bool` であるべきです。 - - これらはすべて、深くネストされた JSON オブジェクトに対しても動作します。 -- JSON から JSON に自動的に変換します。 -- OpenAPIですべてを文書化し、以下を使用することができます: - - 対話的なドキュメントシステム。 - - 多くの言語に対応した自動クライアントコード生成システム。 -- 2 つの対話的なドキュメントのWebインターフェイスを直接提供します。 +* `GET` および `PUT` リクエストのパスに `item_id` があることを検証します。 +* `GET` および `PUT` リクエストに対して `item_id` が `int` 型であることを検証します。 + * そうでない場合、クライアントは有用で明確なエラーを受け取ります。 +* `GET` リクエストに対して、`q` という名前のオプションのクエリパラメータ(`http://127.0.0.1:8000/items/foo?q=somequery` のような)が存在するかどうかを調べます。 + * `q` パラメータは `= None` で宣言されているため、オプションです。 + * `None` がなければ必須になります(`PUT` の場合のボディと同様です)。 +* `PUT` リクエストを `/items/{item_id}` に送信する場合、ボディを JSON として読み込みます: + * 必須の属性 `name` があり、`str` であるべきことを確認します。 + * 必須の属性 `price` があり、`float` でなければならないことを確認します。 + * オプションの属性 `is_offer` があり、存在する場合は `bool` であるべきことを確認します。 + * これらはすべて、深くネストされた JSON オブジェクトに対しても動作します。 +* JSON への/からの変換を自動的に行います。 +* OpenAPI ですべてを文書化し、以下で利用できます: + * 対話型ドキュメントシステム。 + * 多くの言語に対応した自動クライアントコード生成システム。 +* 2 つの対話型ドキュメント Web インターフェースを直接提供します。 --- -まだ表面的な部分に触れただけですが、もう全ての仕組みは分かっているはずです。 +まだ表面的な部分に触れただけですが、仕組みはすでにイメージできているはずです。 -以下の行を変更してみてください: +以下の行を変更してみてください。 ```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) -より多くの機能を含む、より完全な例については、チュートリアル - ユーザーガイドをご覧ください。 +より多くの機能を含む、より完全な例については、Tutorial - User Guide を参照してください。 -**ネタバレ注意**: チュートリアル - ユーザーガイドは以下の情報が含まれています: +**ネタバレ注意**: tutorial - user guide には以下が含まれます。 -- **ヘッダー**、**クッキー**、**フォームフィールド**、**ファイル**などの他の場所からの **パラメータ** 宣言。 -- `maximum_length`や`regex`のような**検証や制約**を設定する方法。 -- 非常に強力で使いやすい **依存性注入**システム。 -- **JWT トークン**を用いた **OAuth2** や **HTTP Basic 認証** のサポートを含む、セキュリティと認証。 -- **深くネストされた JSON モデル**を宣言するためのより高度な(しかし同様に簡単な)技術(Pydantic のおかげです)。 -- 以下のようなたくさんのおまけ機能(Starlette のおかげです): - - **WebSockets** - - **GraphQL** - - `httpx` や `pytest`をもとにした極限に簡単なテスト - - **CORS** - - **クッキーセッション** - - ...などなど。 +* **ヘッダー**、**Cookie**、**フォームフィールド**、**ファイル**など、他のさまざまな場所からの **パラメータ** 宣言。 +* `maximum_length` や `regex` のような **検証制約** を設定する方法。 +* 非常に強力で使いやすい **依存性注入** システム。 +* **JWT トークン**を用いた **OAuth2** や **HTTP Basic** 認証のサポートを含む、セキュリティと認証。 +* **深くネストされた JSON モデル**を宣言するための、より高度な(しかし同様に簡単な)手法(Pydantic のおかげです)。 +* Strawberry および他のライブラリによる **GraphQL** 統合。 +* 以下のようなたくさんのおまけ機能(Starlette のおかげです): + * **WebSockets** + * HTTPX と `pytest` に基づく極めて簡単なテスト + * **CORS** + * **Cookie Sessions** + * ...などなど。 -## パフォーマンス +### アプリをデプロイ(任意) { #deploy-your-app-optional } -独立した TechEmpower のベンチマークでは、Uvicorn で動作する**FastAPI**アプリケーションが、Python フレームワークの中で最も高速なものの 1 つであり、Starlette と Uvicorn(FastAPI で内部的に使用されています)にのみ下回っていると示されています。 +必要に応じて FastAPI アプリを FastAPI Cloud にデプロイできます。まだの場合はウェイティングリストに参加してください。 🚀 -詳細はベンチマークセクションをご覧ください。 +すでに **FastAPI Cloud** アカウント(ウェイティングリストから招待されました 😉)がある場合は、1 コマンドでアプリケーションをデプロイできます。 -## オプションの依存関係 +デプロイ前に、ログインしていることを確認してください。 + +
+ +```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 で内部的に使用されています)にのみ下回っていると示されています。(*) + +詳細は Benchmarks セクションをご覧ください。 + +## 依存関係 { #dependencies } + +FastAPI は Pydantic と Starlette に依存しています。 + +### `standard` 依存関係 { #standard-dependencies } + +FastAPI を `pip install "fastapi[standard]"` でインストールすると、`standard` グループのオプション依存関係が含まれます。 Pydantic によって使用されるもの: -- email-validator - E メールの検証 +* email-validator - メール検証のため。 Starlette によって使用されるもの: -- httpx - `TestClient`を使用するために必要です。 -- jinja2 - デフォルトのテンプレート設定を使用する場合は必要です。 -- python-multipart - "parsing"`request.form()`からの変換をサポートしたい場合は必要です。 -- itsdangerous - `SessionMiddleware` サポートのためには必要です。 -- pyyaml - Starlette の `SchemaGenerator` サポートのために必要です。 (FastAPI では必要ないでしょう。) -- graphene - `GraphQLApp` サポートのためには必要です。 +* httpx - `TestClient` を使用したい場合に必要です。 +* jinja2 - デフォルトのテンプレート設定を使用したい場合に必要です。 +* python-multipart - `request.form()` とともに、フォームの 「parsing」 をサポートしたい場合に必要です。 -FastAPI / Starlette に使用されるもの: +FastAPI によって使用されるもの: -- uvicorn - アプリケーションをロードしてサーブするサーバーのため。 -- orjson - `ORJSONResponse`を使用したい場合は必要です。 -- ujson - `UJSONResponse`を使用する場合は必須です。 +* uvicorn - アプリケーションをロードして提供するサーバーのため。これには `uvicorn[standard]` も含まれ、高性能なサービングに必要な依存関係(例: `uvloop`)が含まれます。 +* `fastapi-cli[standard]` - `fastapi` コマンドを提供します。 + * これには `fastapi-cloud-cli` が含まれ、FastAPI アプリケーションを FastAPI Cloud にデプロイできます。 -これらは全て `pip install fastapi[all]`でインストールできます。 +### `standard` 依存関係なし { #without-standard-dependencies } -## ライセンス +`standard` のオプション依存関係を含めたくない場合は、`pip install "fastapi[standard]"` の代わりに `pip install fastapi` でインストールできます。 -このプロジェクトは MIT ライセンスです。 +### `fastapi-cloud-cli` なし { #without-fastapi-cloud-cli } + +標準の依存関係を含めつつ `fastapi-cloud-cli` を除外して FastAPI をインストールしたい場合は、`pip install "fastapi[standard-no-fastapi-cloud-cli]"` でインストールできます。 + +### 追加のオプション依存関係 { #additional-optional-dependencies } + +追加でインストールしたい依存関係があります。 + +追加のオプション Pydantic 依存関係: + +* pydantic-settings - 設定管理のため。 +* pydantic-extra-types - Pydantic で使用する追加の型のため。 + +追加のオプション FastAPI 依存関係: + +* orjson - `ORJSONResponse` を使用したい場合に必要です。 +* ujson - `UJSONResponse` を使用したい場合に必要です。 + +## ライセンス { #license } + +このプロジェクトは MIT ライセンスの条項の下でライセンスされています。 diff --git a/docs/ja/docs/learn/index.md b/docs/ja/docs/learn/index.md index 2f24c670a..bcdb1e37e 100644 --- a/docs/ja/docs/learn/index.md +++ b/docs/ja/docs/learn/index.md @@ -1,5 +1,5 @@ -# 学習 +# 学習 { #learn } ここでは、**FastAPI** を学習するための入門セクションとチュートリアルを紹介します。 -これは、FastAPIを学習するにあたっての**書籍**や**コース**であり、**公式**かつ推奨される方法とみなすことができます 😎 +これは、**書籍**や**コース**、FastAPIを学習するための**公式**かつ推奨される方法とみなすことができます。😎 diff --git a/docs/ja/docs/project-generation.md b/docs/ja/docs/project-generation.md index daef52efa..c930fb557 100644 --- a/docs/ja/docs/project-generation.md +++ b/docs/ja/docs/project-generation.md @@ -1,84 +1,28 @@ -# プロジェクト生成 - テンプレート +# Full Stack FastAPI テンプレート { #full-stack-fastapi-template } -プロジェクトジェネレーターは、初期設定、セキュリティ、データベース、初期APIエンドポイントなどの多くが含まれているため、プロジェクトの開始に利用できます。 +テンプレートは通常、特定のセットアップが含まれていますが、柔軟でカスタマイズできるように設計されています。これにより、プロジェクトの要件に合わせて変更・適応でき、優れた出発点になります。🏁 -プロジェクトジェネレーターは常に非常に意見が分かれる設定がされており、ニーズに合わせて更新および調整する必要があります。しかしきっと、プロジェクトの良い出発点となるでしょう。 +このテンプレートを使って開始できます。初期セットアップ、セキュリティ、データベース、いくつかのAPIエンドポイントがすでに用意されています。 -## フルスタック FastAPI PostgreSQL +GitHubリポジトリ: Full Stack FastAPI Template -GitHub: https://github.com/tiangolo/full-stack-fastapi-postgresql +## Full Stack FastAPI テンプレート - 技術スタックと機能 { #full-stack-fastapi-template-technology-stack-and-features } -### フルスタック FastAPI PostgreSQL - 機能 - -* 完全な**Docker**インテグレーション (Dockerベース)。 -* Docker Swarm モードデプロイ。 -* ローカル開発環境向けの**Docker Compose**インテグレーションと最適化。 -* UvicornとGunicornを使用した**リリース可能な** Python web サーバ。 -* Python **FastAPI** バックエンド: - * **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげ)。 - * **直感的**: 素晴らしいエディタのサポートや 補完。 デバッグ時間の短縮。 - * **簡単**: 簡単に利用、習得できるようなデザイン。ドキュメントを読む時間を削減。 - * **短い**: コードの重複を最小限に。パラメータ宣言による複数の機能。 - * **堅牢性**: 自動対話ドキュメントを使用した、本番環境で使用できるコード。 - * **標準規格準拠**: API のオープンスタンダードに基く、完全な互換性: OpenAPIJSON スキーマ。 - * 自動バリデーション、シリアライゼーション、対話的なドキュメント、OAuth2 JWTトークンを用いた認証などを含む、**その他多くの機能**。 -* **セキュアなパスワード** ハッシュ化 (デフォルトで)。 -* **JWTトークン** 認証。 -* **SQLAlchemy** モデル (Flask用の拡張と独立しているので、Celeryワーカーと直接的に併用できます)。 -* 基本的なユーザーモデル (任意の修正や削除が可能)。 -* **Alembic** マイグレーション。 -* **CORS** (Cross Origin Resource Sharing (オリジン間リソース共有))。 -* **Celery** ワーカー。バックエンドの残りの部分からモデルとコードを選択的にインポートし、使用可能。 -* Dockerと統合された**Pytest**ベースのRESTバックエンドテスト。データベースに依存せずに、全てのAPIをテスト可能。Docker上で動作するので、毎回ゼロから新たなデータストアを構築可能。(ElasticSearch、MongoDB、CouchDBなどを使用して、APIの動作をテスト可能) -* Atom HydrogenやVisual Studio Code Jupyterなどの拡張機能を使用した、リモートまたはDocker開発用の**Jupyterカーネル**との簡単なPython統合。 -* **Vue** フロントエンド: - * Vue CLIにより生成。 - * **JWT認証**の処理。 - * ログインビュー。 - * ログイン後の、メインダッシュボードビュー。 - * メインダッシュボードでのユーザー作成と編集。 - * セルフユーザー版 - * **Vuex**。 - * **Vue-router**。 - * 美しいマテリアルデザインコンポーネントのための**Vuetify**。 - * **TypeScript**。 - * **Nginx**ベースのDockerサーバ (Vue-routerとうまく協調する構成)。 - * Dockerマルチステージビルド。コンパイルされたコードの保存やコミットが不要。 - * ビルド時にフロントエンドテスト実行 (無効化も可能)。 - * 可能な限りモジュール化されているのでそのまま使用できますが、Vue CLIで再生成したり、必要に応じて作成したりして、必要なものを再利用可能。 -* PostgreSQLデータベースのための**PGAdmin**。(PHPMyAdminとMySQLを使用できるように簡単に変更可能) -* Celeryジョブ監視のための**Flower**。 -* **Traefik**を使用してフロントエンドとバックエンド間をロードバランシング。同一ドメインに配置しパスで区切る、ただし、異なるコンテナで処理。 -* Traefik統合。Let's Encrypt **HTTPS**証明書の自動生成を含む。 -* GitLab **CI** (継続的インテグレーション)。フロントエンドおよびバックエンドテストを含む。 - -## フルスタック FastAPI Couchbase - -GitHub: https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **警告** ⚠️ - -ゼロから新規プロジェクトを始める場合は、ここで代替案を確認してください。 - -例えば、フルスタック FastAPI PostgreSQLのプロジェクトジェネレーターは、積極的にメンテナンスされ、利用されているのでより良い代替案かもしれません。また、すべての新機能と改善点が含まれています。 - -Couchbaseベースのジェネレーターは今も無償提供されています。恐らく正常に動作するでしょう。また、すでにそのジェネレーターで生成されたプロジェクトが存在する場合でも (ニーズに合わせてアップデートしているかもしれません)、同様に正常に動作するはずです。 - -詳細はレポジトリのドキュメントを参照して下さい。 - -## フルスタック FastAPI MongoDB - -...時間の都合等によっては、今後作成されるかもしれません。😅 🎉 - -## spaCyとFastAPIを使用した機械学習モデル - -GitHub: https://github.com/microsoft/cookiecutter-spacy-fastapi - -### spaCyとFastAPIを使用した機械学習モデル - 機能 - -* **spaCy** のNERモデルの統合。 -* **Azure Cognitive Search** のリクエストフォーマットを搭載。 -* **リリース可能な** UvicornとGunicornを使用したPythonウェブサーバ。 -* **Azure DevOps** のKubernetes (AKS) CI/CD デプロイを搭載。 -* **多言語** プロジェクトのために、セットアップ時に言語を容易に選択可能 (spaCyに組み込まれている言語の中から)。 -* **簡単に拡張可能**。spaCyだけでなく、他のモデルフレームワーク (Pytorch、Tensorflow) へ。 +- ⚡ PythonバックエンドAPI向けの [**FastAPI**](https://fastapi.tiangolo.com/ja)。 + - 🧰 PythonのSQLデータベース操作(ORM)向けの [SQLModel](https://sqlmodel.tiangolo.com)。 + - 🔍 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)。 + - 🤖 自動生成されたフロントエンドクライアント。 + - 🧪 End-to-Endテスト向けの [Playwright](https://playwright.dev)。 + - 🦇 ダークモードのサポート。 +- 🐋 開発および本番向けの [Docker Compose](https://www.docker.com)。 +- 🔒 デフォルトでの安全なパスワードハッシュ化。 +- 🔑 JWT(JSON Web Token)認証。 +- 📫 メールベースのパスワードリカバリ。 +- ✅ [Pytest](https://pytest.org) によるテスト。 +- 📞 リバースプロキシ / ロードバランサとしての [Traefik](https://traefik.io)。 +- 🚢 Docker Composeを使用したデプロイ手順(自動HTTPS証明書を処理するフロントエンドTraefikプロキシのセットアップ方法を含む)。 +- 🏭 GitHub Actionsに基づくCI(continuous integration)とCD(continuous deployment)。 diff --git a/docs/ja/docs/python-types.md b/docs/ja/docs/python-types.md index a847ce5d5..26a9e2193 100644 --- a/docs/ja/docs/python-types.md +++ b/docs/ja/docs/python-types.md @@ -1,16 +1,16 @@ -# Pythonの型の紹介 +# Pythonの型の紹介 { #python-types-intro } -**Python 3.6以降** では「型ヒント」オプションがサポートされています。 +Pythonではオプションの「型ヒント」(「型アノテーション」とも呼ばれます)がサポートされています。 -これらの **"型ヒント"** は変数のを宣言することができる新しい構文です。(Python 3.6以降) +これらの **「型ヒント」** またはアノテーションは、変数のを宣言できる特別な構文です。 -変数に型を宣言することでエディターやツールがより良いサポートを提供することができます。 +変数に型を宣言することで、エディターやツールがより良いサポートを提供できます。 -ここではPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** で、**FastAPI**でそれらを使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 +これはPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** にすぎません。**FastAPI** で使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 **FastAPI** はすべてこれらの型ヒントに基づいており、多くの強みと利点を与えてくれます。 -しかしたとえまったく **FastAPI** を使用しない場合でも、それらについて少し学ぶことで利点を得ることができるでしょう。 +しかし、たとえ **FastAPI** をまったく使用しない場合でも、それらについて少し学ぶことで利点を得られます。 /// note | 備考 @@ -18,14 +18,13 @@ /// -## 動機 +## 動機 { #motivation } 簡単な例から始めてみましょう: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -このプログラムを実行すると以下が出力されます: +このプログラムを呼び出すと、以下が出力されます: ``` John Doe @@ -35,12 +34,11 @@ John Doe * `first_name`と`last_name`を取得します。 * `title()`を用いて、それぞれの最初の文字を大文字に変換します。 -* 真ん中にスペースを入れて連結します。 +* 真ん中にスペースを入れて連結します。 -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} - -### 編集 +### 編集 { #edit-it } これはとても簡単なプログラムです。 @@ -50,7 +48,7 @@ John Doe しかし、そうすると「最初の文字を大文字に変換するあのメソッド」を呼び出す必要があります。 -それは`upper`でしたか?`uppercase`でしたか?それとも`first_uppercase`?または`capitalize`? +それは`upper`でしたか?`uppercase`でしたか?`first_uppercase`?`capitalize`? そして、古くからプログラマーの友人であるエディタで自動補完を試してみます。 @@ -58,13 +56,13 @@ John Doe しかし、悲しいことに、これはなんの役にも立ちません: - + -### 型の追加 +### 型の追加 { #add-types } 先ほどのコードから一行変更してみましょう。 -以下の関数のパラメータ部分を: +関数のパラメータである次の断片を、以下から: ```Python first_name, last_name @@ -80,8 +78,7 @@ John Doe それが「型ヒント」です: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} - +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} これは、以下のようにデフォルト値を宣言するのと同じではありません: @@ -95,41 +92,39 @@ John Doe そして、通常、型ヒントを追加しても、それらがない状態と起こることは何も変わりません。 -しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみて下さい。 +しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみてください。 同じタイミングで`Ctrl+Space`で自動補完を実行すると、以下のようになります: - + -これであれば、あなたは「ベルを鳴らす」一つを見つけるまで、オプションを見て、スクロールすることができます: +これであれば、あなたは「ベルを鳴らす」ものを見つけるまで、オプションを見てスクロールできます: - + -## より強い動機 +## より強い動機 { #more-motivation } この関数を見てください。すでに型ヒントを持っています: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} +エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます: -エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます。 - - + これで`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 } -`str`だけでなく、Pythonの標準的な型すべてを宣言することができます。 +`str`だけでなく、Pythonの標準的な型すべてを宣言できます。 例えば、以下を使用可能です: @@ -138,40 +133,47 @@ John Doe * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### 型パラメータを持つジェネリック型 { #generic-types-with-type-parameters } -### 型パラメータを持つジェネリック型 +データ構造の中には、`dict`、`list`、`set`、`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 -データ構造の中には、`dict`、`list`、`set`、そして`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 +内部の型を持つこれらの型は「**generic**」型と呼ばれます。そして、内部の型も含めて宣言することが可能です。 -これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用します。 +これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用できます。これらの型ヒントをサポートするために特別に存在しています。 -これらの型ヒントをサポートするために特別に存在しています。 +#### 新しいPythonバージョン { #newer-versions-of-python } -#### `List` +`typing`を使う構文は、Python 3.6から最新バージョンまで(Python 3.9、Python 3.10などを含む)すべてのバージョンと **互換性** があります。 + +Pythonが進化するにつれ、**新しいバージョン** ではこれらの型アノテーションへのサポートが改善され、多くの場合、型アノテーションを宣言するために`typing`モジュールをインポートして使う必要すらなくなります。 + +プロジェクトでより新しいPythonバージョンを選べるなら、その追加のシンプルさを活用できます。 + +ドキュメント全体で、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 } 例えば、`str`の`list`の変数を定義してみましょう。 -`typing`から`List`をインポートします(大文字の`L`を含む): +同じコロン(`:`)の構文で変数を宣言します。 -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +型として、`list`を指定します。 +リストはいくつかの内部の型を含む型なので、それらを角括弧で囲みます: -同じようにコロン(`:`)の構文で変数を宣言します。 +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -型として、`List`を入力します。 - -リストはいくつかの内部の型を含む型なので、それらを角括弧で囲んでいます。 - -{* ../../docs_src/python_types/tutorial006.py hl[4] *} - - -/// tip | 豆知識 +/// info | 情報 角括弧内の内部の型は「型パラメータ」と呼ばれています。 -この場合、`str`は`List`に渡される型パラメータです。 +この場合、`str`は`list`に渡される型パラメータです。 /// @@ -179,86 +181,203 @@ John Doe そうすることで、エディタはリストの項目を処理している間にもサポートを提供できます。 - + -タイプがなければ、それはほぼ不可能です。 +型がなければ、それはほぼ不可能です。 変数`item`はリスト`items`の要素の一つであることに注意してください。 それでも、エディタはそれが`str`であることを知っていて、そのためのサポートを提供しています。 -#### `Tuple` と `Set` +#### Tuple と Set { #tuple-and-set } `tuple`と`set`の宣言も同様です: -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} - +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} つまり: -* 変数`items_t`は`int`、`int`、`str`の3つの項目を持つ`tuple`です +* 変数`items_t`は`int`、別の`int`、`str`の3つの項目を持つ`tuple`です。 +* 変数`items_s`は`set`であり、その各項目は`bytes`型です。 -* 変数`items_s`はそれぞれの項目が`bytes`型である`set`です。 +#### Dict { #dict } -#### `Dict` - -`dict`を宣言するためには、カンマ区切りで2つの型パラメータを渡します。 +`dict`を定義するには、カンマ区切りで2つの型パラメータを渡します。 最初の型パラメータは`dict`のキーです。 -2番目の型パラメータは`dict`の値です。 - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} +2番目の型パラメータは`dict`の値です: +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} つまり: -* 変数`prices`は`dict`であり: - * この`dict`のキーは`str`型です。(つまり、各項目の名前) - * この`dict`の値は`float`型です。(つまり、各項目の価格) +* 変数`prices`は`dict`です: + * この`dict`のキーは`str`型です(例えば、各項目の名前)。 + * この`dict`の値は`float`型です(例えば、各項目の価格)。 -#### `Optional` +#### Union { #union } -また、`Optional`を使用して、変数が`str`のような型を持つことを宣言することもできますが、それは「オプション」であり、`None`にすることもできます。 +変数が**複数の型のいずれか**になり得ることを宣言できます。例えば、`int`または`str`です。 -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +Python 3.6以上(Python 3.10を含む)では、`typing`の`Union`型を使い、角括弧の中に受け付ける可能性のある型を入れられます。 + +Python 3.10では、受け付ける可能性のある型を縦棒(`|`)で区切って書ける **新しい構文** もあります。 + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} ``` -ただの`str`の代わりに`Optional[str]`を使用することで、エディタは値が常に`str`であると仮定している場合に実際には`None`である可能性があるエラーを検出するのに役立ちます。 +//// -#### ジェネリック型 +//// tab | Python 3.9+ -以下のように角括弧で型パラメータを取る型を: +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` -* `List` -* `Tuple` -* `Set` -* `Dict` +//// + +どちらの場合も、`item`は`int`または`str`になり得ることを意味します。 + +#### `None`の可能性 { #possibly-none } + +値が`str`のような型を持つ可能性がある一方で、`None`にもなり得ることを宣言できます。 + +Python 3.6以上(Python 3.10を含む)では、`typing`モジュールから`Optional`をインポートして使うことで宣言できます。 + +```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`より`Union`をおすすめします。というのも「**optional**」という単語は値がオプションであることを示唆するように見えますが、実際には「`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-types } + +角括弧で型パラメータを取るこれらの型は、例えば次のように **Generic types** または **Generics** と呼ばれます: + +//// tab | Python 3.10+ + +同じ組み込み型をジェネリクスとして(角括弧と内部の型で)使えます: + +* `list` +* `tuple` +* `set` +* `dict` + +また、これまでのPythonバージョンと同様に、`typing`モジュールから: + +* `Union` * `Optional` -* ...など +* ...and others. -**ジェネリック型** または **ジェネリクス** と呼びます。 +Python 3.10では、ジェネリクスの`Union`や`Optional`を使う代替として、型のユニオンを宣言するために縦棒(`|`)を使えます。これはずっと良く、よりシンプルです。 -### 型としてのクラス +//// + +//// tab | Python 3.9+ + +同じ組み込み型をジェネリクスとして(角括弧と内部の型で)使えます: + +* `list` +* `tuple` +* `set` +* `dict` + +そして`typing`モジュールのジェネリクス: + +* `Union` +* `Optional` +* ...and others. + +//// + +### 型としてのクラス { #classes-as-types } 変数の型としてクラスを宣言することもできます。 -例えば、`Person`クラスという名前のクラスがあるとしましょう: +名前を持つ`Person`クラスがあるとしましょう: -{* ../../docs_src/python_types/tutorial010.py hl[1,2,3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} +変数を`Person`型として宣言できます: -変数の型を`Person`として宣言することができます: - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} そして、再び、すべてのエディタのサポートを得ることができます: - + -## Pydanticのモデル +これは「`one_person`はクラス`Person`の**インスタンス**である」ことを意味します。 + +「`one_person`は`Person`という名前の**クラス**である」という意味ではありません。 + +## Pydanticのモデル { #pydantic-models } Pydantic はデータ検証を行うためのPythonライブラリです。 @@ -266,18 +385,17 @@ John Doe そして、それぞれの属性は型を持ちます。 -さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)全てのデータを持つオブジェクトを提供してくれます。 +さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)すべてのデータを持つオブジェクトを提供してくれます。 また、その結果のオブジェクトですべてのエディタのサポートを受けることができます。 -Pydanticの公式ドキュメントから引用: - -{* ../../docs_src/python_types/tutorial011.py *} +Pydanticの公式ドキュメントからの例: +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | 情報 -Pydanticについてより学びたい方はドキュメントを参照してください. +Pydanticの詳細はドキュメントを参照してください。 /// @@ -285,30 +403,62 @@ Pydanticについてより学びたい方はRequired Optional fieldsを参照してください。 + +/// + +## メタデータアノテーション付き型ヒント { #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** では型ヒントを使って型パラメータを宣言すると以下のものが得られます: +**FastAPI** では型ヒントを使ってパラメータを宣言すると以下のものが得られます: -* **エディタサポート**. -* **型チェック**. +* **エディタサポート**。 +* **型チェック**。 -...そして **FastAPI** は同じように宣言をすると、以下のことを行います: +...そして **FastAPI** は同じ宣言を使って、以下のことを行います: -* **要件の定義**: リクエストパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 -* **データの変換**: リクエストのデータを必要な型に変換します。 -* **データの検証**: リクエストごとに: +* **要件の定義**: リクエストのパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 +* **データの変換**: リクエストから必要な型にデータを変換します。 +* **データの検証**: 各リクエストから来るデータについて: * データが無効な場合にクライアントに返される **自動エラー** を生成します。 -* **ドキュメント** OpenAPIを使用したAPI: - * 自動的に対話型ドキュメントのユーザーインターフェイスで使用されます。 +* OpenAPIを使用してAPIを**ドキュメント化**します: + * これは自動の対話型ドキュメントのユーザーインターフェイスで使われます。 すべてが抽象的に聞こえるかもしれません。心配しないでください。 この全ての動作は [チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で見ることができます。 -重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 +重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 /// info | 情報 -すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、`mypy`のチートシートを参照してください +すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、良いリソースとして`mypy`の「チートシート」があります。 /// diff --git a/docs/ja/docs/tutorial/background-tasks.md b/docs/ja/docs/tutorial/background-tasks.md index b289faf12..0ed41ce11 100644 --- a/docs/ja/docs/tutorial/background-tasks.md +++ b/docs/ja/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# バックグラウンドタスク +# バックグラウンドタスク { #background-tasks } レスポンスを返した *後に* 実行されるバックグラウンドタスクを定義できます。 @@ -9,17 +9,17 @@ * 作業実行後のメール通知: * メールサーバーへの接続とメールの送信は「遅い」(数秒) 傾向があるため、すぐにレスポンスを返し、バックグラウンドでメール通知ができます。 * データ処理: - * たとえば、時間のかかる処理を必要とするファイル受信時には、「受信済み」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 + * たとえば、時間のかかる処理を必要とするファイル受信時には、「Accepted」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 -## `BackgroundTasks` の使用 +## `BackgroundTasks` の使用 { #using-backgroundtasks } -まず初めに、`BackgroundTasks` をインポートし、` BackgroundTasks` の型宣言と共に、*path operation 関数* のパラメーターを定義します: +まず初めに、`BackgroundTasks` をインポートし、`BackgroundTasks` の型宣言と共に、*path operation function* のパラメーターを定義します: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** は、`BackgroundTasks` 型のオブジェクトを作成し、そのパラメーターに渡します。 -## タスク関数の作成 +## タスク関数の作成 { #create-a-task-function } バックグラウンドタスクとして実行される関数を作成します。 @@ -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 } -*path operations 関数* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 +*path operation function* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` は以下の引数を受け取ります: @@ -45,40 +45,42 @@ * タスク関数に順番に渡す必要のある引数の列 (`email`)。 * タスク関数に渡す必要のあるキーワード引数 (`message="some notification"`)。 -## 依存性注入 +## 依存性注入 { #dependency-injection } -`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operations 関数*、依存性 (依存可能性)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 +`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operation function*、依存性 (dependable)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 -**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます。 +**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます: + + +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} この例では、レスポンスが送信された *後* にメッセージが `log.txt` ファイルに書き込まれます。 リクエストにクエリがあった場合、バックグラウンドタスクでログに書き込まれます。 -そして、*path operations 関数* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 +そして、*path operation function* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 -## 技術的な詳細 +## 技術的な詳細 { #technical-details } `BackgroundTasks` クラスは、`starlette.background`から直接取得されます。 これは、FastAPI に直接インポート/インクルードされるため、`fastapi` からインポートできる上に、`starlette.background`から別の `BackgroundTask` (末尾に `s` がない) を誤ってインポートすることを回避できます。 -`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operations 関数* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 +`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operation function* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 それでも、FastAPI で `BackgroundTask` を単独で使用することは可能ですが、コード内でオブジェクトを作成し、それを含むStarlette `Response` を返す必要があります。 -詳細については、バックグラウンドタスクに関する Starlette の公式ドキュメントを参照して下さい。 +詳細については、Starlette のバックグラウンドタスクに関する公式ドキュメントを参照して下さい。 -## 警告 +## 注意 { #caveat } -大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、Celery のようなより大きな他のツールを使用するとメリットがあるかもしれません。 +大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、Celery のようなより大きな他のツールを使用するとメリットがあるかもしれません。 これらは、より複雑な構成、RabbitMQ や Redis などのメッセージ/ジョブキューマネージャーを必要とする傾向がありますが、複数のプロセス、特に複数のサーバーでバックグラウンドタスクを実行できます。 ただし、同じ **FastAPI** アプリから変数とオブジェクトにアクセスする必要がある場合、または小さなバックグラウンドタスク (電子メール通知の送信など) を実行する必要がある場合は、単に `BackgroundTasks` を使用できます。 -## まとめ +## まとめ { #recap } -`BackgroundTasks` をインポートして、*path operations 関数* や依存関係のパラメータに `BackgroundTasks`を使用し、バックグラウンドタスクを追加して下さい。 +*path operation functions* と依存性のパラメータで `BackgroundTasks`をインポートして使用し、バックグラウンドタスクを追加して下さい。 diff --git a/docs/ja/docs/tutorial/body-fields.md b/docs/ja/docs/tutorial/body-fields.md index ce5630351..234c99d52 100644 --- a/docs/ja/docs/tutorial/body-fields.md +++ b/docs/ja/docs/tutorial/body-fields.md @@ -1,12 +1,13 @@ -# ボディ - フィールド +# ボディ - フィールド { #body-fields } `Query`や`Path`、`Body`を使って *path operation関数* のパラメータに追加のバリデーションやメタデータを宣言するのと同じように、Pydanticの`Field`を使ってPydanticモデルの内部でバリデーションやメタデータを宣言することができます。 -## `Field`のインポート +## `Field`のインポート { #import-field } まず、以下のようにインポートします: -{* ../../docs_src/body_fields/tutorial001.py hl[4] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} + /// warning | 注意 @@ -14,11 +15,11 @@ /// -## モデルの属性の宣言 +## モデルの属性の宣言 { #declare-model-attributes } 以下のように`Field`をモデルの属性として使用することができます: -{* ../../docs_src/body_fields/tutorial001.py hl[11,12,13,14] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} `Field`は`Query`や`Path`、`Body`と同じように動作し、全く同様のパラメータなどを持ちます。 @@ -40,13 +41,20 @@ /// -## 追加情報の追加 +## 追加情報の追加 { #add-extra-information } 追加情報は`Field`や`Query`、`Body`などで宣言することができます。そしてそれは生成されたJSONスキーマに含まれます。 後に例を用いて宣言を学ぶ際に、追加情報を追加する方法を学べます。 -## まとめ +/// warning | 注意 + +`Field`に渡された追加のキーは、結果として生成されるアプリケーションのOpenAPIスキーマにも含まれます。 +これらのキーは必ずしもOpenAPI仕様の一部であるとは限らないため、例えば[OpenAPI validator](https://validator.swagger.io/)などの一部のOpenAPIツールは、生成されたスキーマでは動作しない場合があります。 + +/// + +## まとめ { #recap } Pydanticの`Field`を使用して、モデルの属性に追加のバリデーションやメタデータを宣言することができます。 diff --git a/docs/ja/docs/tutorial/body-multiple-params.md b/docs/ja/docs/tutorial/body-multiple-params.md index cbfdda4b2..4ce77cc0d 100644 --- a/docs/ja/docs/tutorial/body-multiple-params.md +++ b/docs/ja/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# ボディ - 複数のパラメータ +# ボディ - 複数のパラメータ { #body-multiple-parameters } -これまで`Path`と`Query`をどう使うかを見てきましたが、リクエストボディの宣言のより高度な使い方を見てみましょう。 +これまで`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.py hl[19,20,21] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | 備考 -この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値は`None`です。 +この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値が`None`になっているためです。 /// -## 複数のボディパラメータ +## 複数のボディパラメータ { #multiple-body-parameters } -上述の例では、*path operations*は`item`の属性を持つ以下のようなJSONボディを期待していました: +上述の例では、*path operations*は`Item`の属性を持つ以下のようなJSONボディを期待していました: ```JSON { @@ -31,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モデルである2つのパラメータ)があることに気付きます。 -そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待しています: +この場合、**FastAPI**は関数内に複数のボディパラメータがあることに気付きます(Pydanticモデルである2つのパラメータがあります)。 + +そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待します: ```JSON { @@ -62,7 +63,7 @@ 複合データの検証を行い、OpenAPIスキーマや自動ドキュメントのように文書化してくれます。 -## ボディ内の単数値 +## ボディ内の単数値 { #singular-values-in-body } クエリとパスパラメータの追加データを定義するための `Query` と `Path` があるのと同じように、 **FastAPI** は同等の `Body` を提供します。 @@ -72,12 +73,11 @@ しかし、`Body`を使用して、**FastAPI** に別のボディキーとして扱うように指示することができます: +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -{* ../../docs_src/body_multiple_params/tutorial003.py hl[23] *} この場合、**FastAPI** は以下のようなボディを期待します: - ```JSON { "item": { @@ -96,41 +96,48 @@ 繰り返しになりますが、データ型の変換、検証、文書化などを行います。 -## 複数のボディパラメータとクエリ +## 複数のボディパラメータとクエリ { #multiple-body-params-and-query } もちろん、ボディパラメータに加えて、必要に応じて追加のクエリパラメータを宣言することもできます。 -デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はありません。 +デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はなく、次のようにできます: ```Python -q: str = None +q: str | None = None ``` -以下において: +またはPython 3.10以上では: -{* ../../docs_src/body_multiple_params/tutorial004.py hl[27] *} +```Python +q: Union[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`を1つだけ持っているとしましょう。 +Pydanticモデル`Item`の単一の`item`ボディパラメータしかないとしましょう。 デフォルトでは、**FastAPI**はそのボディを直接期待します。 -しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON とその中のモデルの内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: +しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON と、その中のモデル内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: ```Python -item: Item = Body(..., embed=True) +item: Item = Body(embed=True) ``` 以下において: -{* ../../docs_src/body_multiple_params/tutorial005.py hl[17] *} +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + この場合、**FastAPI** は以下のようなボディを期待します: @@ -156,9 +163,9 @@ item: Item = Body(..., embed=True) } ``` -## まとめ +## まとめ { #recap } -リクエストが単一のボディしか持てない場合でも、*path operation関数*に複数のボディパラメータを追加することができます。 +リクエストが単一のボディしか持てない場合でも、*path operation function*に複数のボディパラメータを追加することができます。 しかし、**FastAPI** はそれを処理し、関数内の正しいデータを与え、*path operation*内の正しいスキーマを検証し、文書化します。 diff --git a/docs/ja/docs/tutorial/body-nested-models.md b/docs/ja/docs/tutorial/body-nested-models.md index a1680d10f..24eb30208 100644 --- a/docs/ja/docs/tutorial/body-nested-models.md +++ b/docs/ja/docs/tutorial/body-nested-models.md @@ -1,36 +1,26 @@ -# ボディ - ネストされたモデル +# ボディ - ネストされたモデル { #body-nested-models } **FastAPI** を使用すると、深くネストされた任意のモデルを定義、検証、文書化、使用することができます(Pydanticのおかげです)。 -## リストのフィールド +## リストのフィールド { #list-fields } -属性をサブタイプとして定義することができます。例えば、Pythonの`list`は以下のように定義できます: +属性をサブタイプとして定義することができます。例えば、Pythonの`list`: -{* ../../docs_src/body_nested_models/tutorial001.py hl[12] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} -これにより、各項目の型は宣言されていませんが、`tags`はある項目のリストになります。 +これにより、各項目の型は宣言されていませんが、`tags`はリストになります。 -## タイプパラメータを持つリストのフィールド +## タイプパラメータを持つリストのフィールド { #list-fields-with-type-parameter } -しかし、Pythonには型や「タイプパラメータ」を使ってリストを宣言する方法があります: +しかし、Pythonには内部の型、または「タイプパラメータ」を使ってリストを宣言するための特定の方法があります: -### typingの`List`をインポート +### タイプパラメータを持つ`list`の宣言 { #declare-a-list-with-a-type-parameter } -まず、Pythonの標準の`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] ``` 型宣言の標準的なPythonの構文はこれだけです。 @@ -39,17 +29,17 @@ 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 } しかし、よく考えてみると、タグは繰り返すべきではなく、おそらくユニークな文字列になるのではないかと気付いたとします。 そして、Pythonにはユニークな項目のセットのための特別なデータ型`set`があります。 -そのため、以下のように、`Set`をインポートして`str`の`set`として`tags`を宣言することができます: +そして、`tags`を文字列のセットとして宣言できます: -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} これを使えば、データが重複しているリクエストを受けた場合でも、ユニークな項目のセットに変換されます。 @@ -57,27 +47,27 @@ my_list: List[str] また、それに応じて注釈をつけたり、文書化したりします。 -## ネストされたモデル +## ネストされたモデル { #nested-models } Pydanticモデルの各属性には型があります。 しかし、その型はそれ自体が別のPydanticモデルである可能性があります。 -そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON`object`を宣言することができます。 +そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON「オブジェクト」を宣言することができます。 すべては、任意のネストにされています。 -### サブモデルの定義 +### サブモデルの定義 { #define-a-submodel } 例えば、`Image`モデルを定義することができます: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9,10,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** が以下のようなボディを期待することを意味します: @@ -102,23 +92,23 @@ Pydanticモデルの各属性には型があります。 * データの検証 * 自動文書化 -## 特殊な型とバリデーション +## 特殊な型とバリデーション { #special-types-and-validation } -`str`や`int`、`float`のような通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 +`str`や`int`、`float`などの通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 -すべてのオプションをみるには、Pydanticのエキゾチック な型のドキュメントを確認してください。次の章でいくつかの例をみることができます。 +すべてのオプションをみるには、Pydanticの型の概要を確認してください。次の章でいくつかの例をみることができます。 -例えば、`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 } Pydanticモデルを`list`や`set`などのサブタイプとして使用することもできます: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} これは、次のようなJSONボディを期待します(変換、検証、ドキュメントなど): @@ -152,59 +142,59 @@ Pydanticモデルを`list`や`set`などのサブタイプとして使用する /// -## 深くネストされたモデル +## 深くネストされたモデル { #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`は`Item`のリストであり、オプションの`Image`のリストを持っていることに注目してください。 +`Offer`は`Item`のリストであり、それらがさらにオプションの`Image`のリストを持っていることに注目してください。 /// -## 純粋なリストのボディ +## 純粋なリストのボディ { #bodies-of-pure-lists } 期待するJSONボディのトップレベルの値がJSON`array`(Pythonの`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`を直接使用している場合はこのようなエディタのサポートは得られません。 -しかし、それらについて心配する必要はありません。入力された辞書は自動的に変換され、出力も自動的にJSONに変換されます。 +しかし、それらについて心配する必要はありません。入力されたdictは自動的に変換され、出力も自動的にJSONに変換されます。 -## 任意の`dict`のボディ +## 任意の`dict`のボディ { #bodies-of-arbitrary-dicts } また、ある型のキーと別の型の値を持つ`dict`としてボディを宣言することもできます。 -有効なフィールド・属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 +この方法で、有効なフィールド/属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 -これは、まだ知らないキーを受け取りたいときに便利だと思います。 +これは、まだ知らないキーを受け取りたいときに便利です。 --- -他にも、`int`のように他の型のキーを持ちたい場合などに便利です。 +もうひとつ便利なケースは、別の型(例: `int`)のキーを持ちたい場合です。 -それをここで見ていきましょう。 +それをここで見ていきます。 この場合、`int`のキーと`float`の値を持つものであれば、どんな`dict`でも受け入れることができます: -{* ../../docs_src/body_nested_models/tutorial009.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | 豆知識 @@ -218,14 +208,14 @@ JSONはキーとして`str`しかサポートしていないことに注意し /// -## まとめ +## まとめ { #recap } **FastAPI** を使用すると、Pydanticモデルが提供する最大限の柔軟性を持ちながら、コードをシンプルに短く、エレガントに保つことができます。 -以下のような利点があります: +しかし、以下のような利点があります: * エディタのサポート(どこでも補完!) -* データ変換(別名:構文解析・シリアライズ) +* データ変換(別名:構文解析 / シリアライズ) * データの検証 * スキーマ文書 -* 自動文書化 +* 自動ドキュメント diff --git a/docs/ja/docs/tutorial/body-updates.md b/docs/ja/docs/tutorial/body-updates.md index ffbe52e1d..e888d5a0d 100644 --- a/docs/ja/docs/tutorial/body-updates.md +++ b/docs/ja/docs/tutorial/body-updates.md @@ -1,16 +1,16 @@ -# ボディ - 更新 +# ボディ - 更新 { #body-updates } -## `PUT`による置換での更新 +## `PUT`による置換での更新 { #update-replacing-with-put } 項目を更新するにはHTTPの`PUT`操作を使用することができます。 -`jsonable_encoder`を用いて、入力データをJSON形式で保存できるデータに変換することができます(例:NoSQLデータベース)。例えば、`datetime`を`str`に変換します。 +`jsonable_encoder`を用いて、入力データをJSONとして保存できるデータに変換することができます(例:NoSQLデータベース)。例えば、`datetime`を`str`に変換します。 -{* ../../docs_src/body_updates/tutorial001.py hl[30,31,32,33,34,35] *} +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} -既存のデータを置き換えるべきデータを受け取るために`PUT`は使用されます。 +`PUT`は、既存のデータを置き換えるべきデータを受け取るために使用されます。 -### 置換についての注意 +### 置換についての注意 { #warning-about-replacing } つまり、`PUT`を使用して以下のボディで項目`bar`を更新したい場合は: @@ -22,11 +22,11 @@ } ``` -すでに格納されている属性`"tax": 20.2`を含まないため、入力モデルのデフォルト値は`"tax": 10.5`です。 +すでに格納されている属性`"tax": 20.2`を含まないため、入力モデルは`"tax": 10.5`のデフォルト値を取ります。 そして、データはその「新しい」`10.5`の`tax`と共に保存されます。 -## `PATCH`による部分的な更新 +## `PATCH`による部分的な更新 { #partial-updates-with-patch } また、HTTPの`PATCH`操作でデータを*部分的に*更新することもできます。 @@ -44,27 +44,27 @@ /// -### Pydanticの`exclude_unset`パラメータの使用 +### Pydanticの`exclude_unset`パラメータの使用 { #using-pydantics-exclude-unset-parameter } -部分的な更新を受け取りたい場合は、Pydanticモデルの`.dict()`の`exclude_unset`パラメータを使用すると非常に便利です。 +部分的な更新を受け取りたい場合は、Pydanticモデルの`.model_dump()`の`exclude_unset`パラメータを使用すると非常に便利です。 -`item.dict(exclude_unset=True)`のように。 +`item.model_dump(exclude_unset=True)`のように。 これにより、`item`モデルの作成時に設定されたデータのみを持つ`dict`が生成され、デフォルト値は除外されます。 これを使うことで、デフォルト値を省略して、設定された(リクエストで送られた)データのみを含む`dict`を生成することができます: -{* ../../docs_src/body_updates/tutorial002.py hl[34] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Pydanticの`update`パラメータ +### Pydanticの`update`パラメータの使用 { #using-pydantics-update-parameter } -ここで、`.copy()`を用いて既存のモデルのコピーを作成し、`update`パラメータに更新するデータを含む`dict`を渡すことができます。 +ここで、`.model_copy()`を用いて既存のモデルのコピーを作成し、`update`パラメータに更新するデータを含む`dict`を渡すことができます。 -`stored_item_model.copy(update=update_data)`のように: +`stored_item_model.model_copy(update=update_data)`のように: -{* ../../docs_src/body_updates/tutorial002.py hl[35] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### 部分的更新のまとめ +### 部分的更新のまとめ { #partial-updates-recap } まとめると、部分的な更新を適用するには、次のようにします: @@ -75,11 +75,11 @@ * この方法では、モデル内のデフォルト値ですでに保存されている値を上書きするのではなく、ユーザーが実際に設定した値のみを更新することができます。 * 保存されているモデルのコピーを作成し、受け取った部分的な更新で属性を更新します(`update`パラメータを使用します)。 * コピーしたモデルをDBに保存できるものに変換します(例えば、`jsonable_encoder`を使用します)。 - * これはモデルの`.dict()`メソッドを再度利用することに匹敵しますが、値をJSONに変換できるデータ型、例えば`datetime`を`str`に変換します。 + * これはモデルの`.model_dump()`メソッドを再度利用することに匹敵しますが、値をJSONに変換できるデータ型になるようにし(変換し)、例えば`datetime`を`str`に変換します。 * データをDBに保存します。 * 更新されたモデルを返します。 -{* ../../docs_src/body_updates/tutorial002.py hl[30,31,32,33,34,35,36,37] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} /// tip | 豆知識 diff --git a/docs/ja/docs/tutorial/body.md b/docs/ja/docs/tutorial/body.md index 1298eec7e..a219faed0 100644 --- a/docs/ja/docs/tutorial/body.md +++ b/docs/ja/docs/tutorial/body.md @@ -1,40 +1,41 @@ -# リクエストボディ +# リクエストボディ { #request-body } -クライアント (ブラウザなど) からAPIにデータを送信する必要があるとき、データを **リクエストボディ (request body)** として送ります。 +クライアント(例えばブラウザ)からAPIにデータを送信する必要がある場合、**リクエストボディ**として送信します。 -**リクエスト** ボディはクライアントによってAPIへ送られます。**レスポンス** ボディはAPIがクライアントに送るデータです。 +**リクエスト**ボディは、クライアントからAPIへ送信されるデータです。**レスポンス**ボディは、APIがクライアントに送信するデータです。 -APIはほとんどの場合 **レスポンス** ボディを送らなければなりません。しかし、クライアントは必ずしも **リクエスト** ボディを送らなければいけないわけではありません。 +APIはほとんどの場合 **レスポンス** ボディを送信する必要があります。しかしクライアントは、常に **リクエストボディ** を送信する必要があるとは限りません。場合によっては、クエリパラメータ付きのパスだけをリクエストして、ボディを送信しないこともあります。 -**リクエスト** ボディを宣言するために Pydantic モデルを使用します。そして、その全てのパワーとメリットを利用します。 +**リクエスト**ボディを宣言するには、Pydantic モデルを使用し、その強力な機能とメリットをすべて利用します。 /// info | 情報 -データを送るには、`POST` (もっともよく使われる)、`PUT`、`DELETE` または `PATCH` を使うべきです。 +データを送信するには、`POST`(より一般的)、`PUT`、`DELETE`、`PATCH` のいずれかを使用すべきです。 -GET リクエストでボディを送信することは、仕様では未定義の動作ですが、FastAPI でサポートされており、非常に複雑な(極端な)ユースケースにのみ対応しています。 +`GET` リクエストでボディを送信することは仕様上は未定義の動作ですが、それでもFastAPIではサポートされています。ただし、非常に複雑/極端なユースケースのためだけです。 -非推奨なので、Swagger UIを使った対話型のドキュメントにはGETのボディ情報は表示されません。さらに、中継するプロキシが対応していない可能性があります。 +推奨されないため、Swagger UIによる対話的ドキュメントでは `GET` 使用時のボディのドキュメントは表示されず、途中のプロキシが対応していない可能性もあります。 /// -## Pydanticの `BaseModel` をインポート +## Pydanticの `BaseModel` をインポート { #import-pydantics-basemodel } -ます初めに、 `pydantic` から `BaseModel` をインポートする必要があります: +まず、`pydantic` から `BaseModel` をインポートする必要があります: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## データモデルの作成 +## データモデルの作成 { #create-your-data-model } -そして、`BaseModel` を継承したクラスとしてデータモデルを宣言します。 +次に、`BaseModel` を継承するクラスとしてデータモデルを宣言します。 -すべての属性にpython標準の型を使用します: +すべての属性に標準のPython型を使用します: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -クエリパラメータの宣言と同様に、モデル属性がデフォルト値をもつとき、必須な属性ではなくなります。それ以外は必須になります。オプショナルな属性にしたい場合は `None` を使用してください。 -例えば、上記のモデルは以下の様なJSON「`オブジェクト`」(もしくはPythonの `dict` ) を宣言しています: +クエリパラメータの宣言と同様に、モデル属性がデフォルト値を持つ場合は必須ではありません。そうでなければ必須です。単にオプションにするには `None` を使用してください。 + +例えば、上記のモデルは次のようなJSON「`object`」(またはPythonの `dict`)を宣言します: ```JSON { @@ -45,7 +46,7 @@ GET リクエストでボディを送信することは、仕様では未定義 } ``` -...`description` と `tax` はオプショナル (デフォルト値は `None`) なので、以下のJSON「`オブジェクト`」も有効です: +...`description` と `tax` はオプション(デフォルト値が `None`)なので、このJSON「`object`」も有効です: ```JSON { @@ -54,109 +55,112 @@ GET リクエストでボディを送信することは、仕様では未定義 } ``` -## パラメータとして宣言 +## パラメータとして宣言 { #declare-it-as-a-parameter } -*パスオペレーション* に加えるために、パスパラメータやクエリパラメータと同じ様に宣言します: +*path operation* に追加するには、パスパラメータやクエリパラメータを宣言したのと同じ方法で宣言します: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...そして、作成したモデル `Item` で型を宣言します。 +...そして、作成したモデル `Item` を型として宣言します。 -## 結果 +## 結果 { #results } -そのPythonの型宣言だけで **FastAPI** は以下のことを行います: +そのPythonの型宣言だけで **FastAPI** は以下を行います: -* リクエストボディをJSONとして読み取ります。 -* 適当な型に変換します(必要な場合)。 +* リクエストのボディをJSONとして読み取ります。 +* 対応する型に変換します(必要な場合)。 * データを検証します。 - * データが無効な場合は、明確なエラーが返され、どこが不正なデータであったかを示します。 -* 受け取ったデータをパラメータ `item` に変換します。 - * 関数内で `Item` 型であると宣言したので、すべての属性とその型に対するエディタサポート(補完など)をすべて使用できます。 -* モデルのJSONスキーマ定義を生成し、好きな場所で使用することができます。 -* これらのスキーマは、生成されたOpenAPIスキーマの一部となり、自動ドキュメントのUIに使用されます。 + * データが無効な場合は、どこで何が不正なデータだったのかを正確に示す、分かりやすい明確なエラーを返します。 +* 受け取ったデータをパラメータ `item` に渡します。 + * 関数内で `Item` 型として宣言したため、すべての属性とその型について、エディタサポート(補完など)も利用できます。 +* モデル向けの JSON Schema 定義を生成します。プロジェクトにとって意味があるなら、他の場所でも好きなように利用できます。 +* それらのスキーマは生成されるOpenAPIスキーマの一部となり、自動ドキュメントの UIs で使用されます。 -## 自動ドキュメント生成 +## 自動ドキュメント { #automatic-docs } -モデルのJSONスキーマはOpenAPIで生成されたスキーマの一部になり、対話的なAPIドキュメントに表示されます: +モデルのJSON Schemaは、OpenAPIで生成されたスキーマの一部になり、対話的なAPIドキュメントに表示されます: -そして、それらが使われる *パスオペレーション* のそれぞれのAPIドキュメントにも表示されます: +また、それらが必要な各 *path operation* 内のAPIドキュメントでも使用されます: -## エディターサポート +## エディタサポート { #editor-support } -エディターによる型ヒントと補完が関数内で利用できます (Pydanticモデルではなく `dict` を受け取ると、同じサポートは受けられません): +エディタ上で、関数内のあらゆる場所で型ヒントと補完が得られます(Pydanticモデルの代わりに `dict` を受け取った場合は起きません): -型によるエラーチェックも可能です: +不正な型操作に対するエラーチェックも得られます: -これは偶然ではなく、このデザインに基づいてフレームワークが作られています。 +これは偶然ではなく、フレームワーク全体がその設計を中心に構築されています。 -全てのエディターで機能することを確認するために、実装前の設計時に徹底的にテストしました。 +そして、すべてのエディタで動作することを確実にするために、実装前の設計フェーズで徹底的にテストされました。 -これをサポートするためにPydantic自体にもいくつかの変更がありました。 +これをサポートするために、Pydantic自体にもいくつかの変更が加えられました。 -上記のスクリーンショットはVisual Studio Codeを撮ったものです。 +前述のスクリーンショットは Visual Studio Code で撮影されたものです。 -しかし、PyCharmやほとんどのPythonエディタでも同様なエディターサポートを受けられます: +ただし、PyCharm や、他のほとんどのPythonエディタでも同じエディタサポートを得られます: /// tip | 豆知識 -PyCharmエディタを使用している場合は、Pydantic PyCharm Pluginが使用可能です。 +エディタとして PyCharm を使用している場合、Pydantic PyCharm Plugin を使用できます。 -以下のエディターサポートが強化されます: +以下により、Pydanticモデルに対するエディタサポートが改善されます: -* 自動補完 -* 型チェック -* リファクタリング -* 検索 -* インスペクション +* auto-completion +* type checks +* refactoring +* searching +* inspections /// -## モデルの使用 +## モデルを使用する { #use-the-model } -関数内部で、モデルの全ての属性に直接アクセスできます: +関数内では、モデルオブジェクトのすべての属性に直接アクセスできます: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## リクエストボディ + パスパラメータ +## リクエストボディ + パスパラメータ { #request-body-path-parameters } パスパラメータとリクエストボディを同時に宣言できます。 -**FastAPI** はパスパラメータである関数パラメータは**パスから受け取り**、Pydanticモデルによって宣言された関数パラメータは**リクエストボディから受け取る**ということを認識します。 +**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** はそれぞれを認識し、適切な場所からデータを取得します。 +**body**、**path**、**query** パラメータもすべて同時に宣言できます。 -{* ../../docs_src/body/tutorial004.py hl[18] *} +**FastAPI** はそれぞれを認識し、正しい場所からデータを取得します。 -関数パラメータは以下の様に認識されます: +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} -* パラメータが**パス**で宣言されている場合は、優先的にパスパラメータとして扱われます。 -* パラメータが**単数型** (`int`、`float`、`str`、`bool` など)の場合は**クエリ**パラメータとして解釈されます。 -* パラメータが **Pydantic モデル**型で宣言された場合、リクエスト**ボディ**として解釈されます。 +関数パラメータは以下のように認識されます: + +* パラメータが **path** でも宣言されている場合、パスパラメータとして使用されます。 +* パラメータが **単数型**(`int`、`float`、`str`、`bool` など)の場合、**query** パラメータとして解釈されます。 +* パラメータが **Pydanticモデル** の型として宣言されている場合、リクエスト **body** として解釈されます。 /// note | 備考 -FastAPIは、`= None`があるおかげで、`q`がオプショナルだとわかります。 +FastAPIは、デフォルト値 `= None` があるため、`q` の値が必須ではないことを認識します。 -`Optional[str]` の`Optional` はFastAPIでは使用されていません(FastAPIは`str`の部分のみ使用します)。しかし、`Optional[str]` はエディタがコードのエラーを見つけるのを助けてくれます。 +`str | None`(Python 3.10+)や `Union[str, None]`(Python 3.9+)の `Union` は、値が必須ではないことを判断するためにFastAPIでは使用されません。`= None` というデフォルト値があるため、必須ではないことを認識します。 + +しかし、型アノテーションを追加すると、エディタがより良いサポートを提供し、エラーを検出できるようになります。 /// -## Pydanticを使わない方法 +## Pydanticを使わない方法 { #without-pydantic } -もしPydanticモデルを使用したくない場合は、**Body**パラメータが利用できます。[Body - Multiple Parameters: Singular values in 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/ja/docs/tutorial/cookie-param-models.md b/docs/ja/docs/tutorial/cookie-param-models.md index 8285f44ef..10ffb2566 100644 --- a/docs/ja/docs/tutorial/cookie-param-models.md +++ b/docs/ja/docs/tutorial/cookie-param-models.md @@ -1,8 +1,8 @@ -# クッキーパラメータモデル +# クッキーパラメータモデル { #cookie-parameter-models } もし関連する**複数のクッキー**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。🍪 -こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのクッキーパラメータに対して一度に宣言できます。😎 +こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのパラメータに対して一度に宣言できます。😎 /// note | 備考 @@ -16,15 +16,15 @@ /// -## クッキーにPydanticモデルを使用する +## Pydanticモデルを使用したクッキー { #cookies-with-a-pydantic-model } -必要な複数の**クッキー**パラメータを**Pydanticモデル**で宣言し、さらに、それを `Cookie` として宣言しましょう: +必要な複数の**クッキー**パラメータを**Pydanticモデル**で宣言し、さらに、パラメータを `Cookie` として宣言しましょう: {* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} -**FastAPI**は、リクエストの**クッキー**から**それぞれのフィールド**のデータを**抽出**し、定義された**Pydanticモデル**を提供します。 +**FastAPI**は、リクエストで受け取った**クッキー**から**それぞれのフィールド**のデータを**抽出**し、定義したPydanticモデルを提供します。 -## ドキュメントの確認 +## ドキュメントの確認 { #check-the-docs } 対話的APIドキュメントUI `/docs` で、定義されているクッキーを確認できます: @@ -32,18 +32,17 @@
-/// info | 備考 - +/// info | 情報 **ブラウザがクッキーを処理し**ていますが、特別な方法で内部的に処理を行っているために、**JavaScript**からは簡単に操作**できない**ことに留意してください。 -**対話的APIドキュメントUI** `/docs` にアクセスすれば、*パスオペレーション*に関するクッキーの**ドキュメンテーション**を確認できます。 +**APIドキュメントUI** `/docs` にアクセスすれば、*path operation*に関するクッキーの**ドキュメンテーション**を確認できます。 -しかし、たとえ**クッキーデータを入力して**「Execute」をクリックしても、対話的APIドキュメントUIは**JavaScript**で動作しているためクッキーは送信されず、まるで値を入力しなかったかのような**エラー**メッセージが表示されます。 +しかし、たとえ**データを入力して**「Execute」をクリックしても、ドキュメントUIは**JavaScript**で動作しているためクッキーは送信されず、まるで値を入力しなかったかのような**エラー**メッセージが表示されます。 /// -## 余分なクッキーを禁止する +## 余分なクッキーを禁止する { #forbid-extra-cookies } 特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクッキーを**制限**する必要があるかもしれません。 @@ -51,7 +50,7 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} もしクライアントが**余分なクッキー**を送ろうとすると、**エラー**レスポンスが返されます。 @@ -72,6 +71,6 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールド } ``` -## まとめ +## まとめ { #summary } **FastAPI**では、**クッキー**を宣言するために、**Pydanticモデル**を使用できます。😎 diff --git a/docs/ja/docs/tutorial/cookie-params.md b/docs/ja/docs/tutorial/cookie-params.md index 13af6d3c7..1e5a0d3cf 100644 --- a/docs/ja/docs/tutorial/cookie-params.md +++ b/docs/ja/docs/tutorial/cookie-params.md @@ -1,20 +1,20 @@ -# クッキーのパラメータ +# クッキーのパラメータ { #cookie-parameters } クッキーのパラメータは、`Query`や`Path`のパラメータを定義するのと同じ方法で定義できます。 -## `Cookie`をインポート +## `Cookie`をインポート { #import-cookie } まず、`Cookie`をインポートします: -{* ../../docs_src/cookie_params/tutorial001.py hl[3] *} +{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## `Cookie`のパラメータを宣言 +## `Cookie`のパラメータを宣言 { #declare-cookie-parameters } 次に、`Path`や`Query`と同じ構造を使ってクッキーのパラメータを宣言します。 最初の値がデフォルト値で、追加の検証パラメータや注釈パラメータをすべて渡すことができます: -{* ../../docs_src/cookie_params/tutorial001.py hl[9] *} +{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | 技術詳細 @@ -30,6 +30,16 @@ /// -## まとめ +/// info | 情報 -クッキーは`Cookie`を使って宣言し、`Query`や`Path`と同じパターンを使用する。 +**ブラウザがクッキーを**特殊な方法で裏側で扱うため、**JavaScript** から簡単には触れられないことを念頭に置いてください。 + +`/docs` の **API docs UI** に移動すると、*path operation* のクッキーに関する **documentation** を確認できます。 + +しかし、データを **入力** して「Execute」をクリックしても、docs UI は **JavaScript** で動作するためクッキーは送信されず、値を何も書かなかったかのような **error** メッセージが表示されます。 + +/// + +## まとめ { #recap } + +クッキーは`Cookie`を使って宣言し、`Query`や`Path`と同じ共通のパターンを使用する。 diff --git a/docs/ja/docs/tutorial/cors.md b/docs/ja/docs/tutorial/cors.md index f7bd59b70..a1dfe8e62 100644 --- a/docs/ja/docs/tutorial/cors.md +++ b/docs/ja/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (オリジン間リソース共有) +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } -CORSまたは「オリジン間リソース共有」 は、ブラウザで実行されているフロントエンドにバックエンドと通信するJavaScriptコードがあり、そのバックエンドがフロントエンドとは異なる「オリジン」にある状況を指します。 +CORSまたは「Cross-Origin Resource Sharing」 は、ブラウザで実行されているフロントエンドにバックエンドと通信するJavaScriptコードがあり、そのバックエンドがフロントエンドとは異なる「オリジン」にある状況を指します。 -## オリジン +## オリジン { #origin } オリジンはプロトコル (`http`、`https`) とドメイン (`myapp.com`、`localhost`、`localhost.tiangolo.com`) とポート (`80`、`443`、`8080`) の組み合わせです。 @@ -14,25 +14,25 @@ すべて `localhost` であっても、異なるプロトコルやポートを使用するので、異なる「オリジン」です。 -## ステップ +## ステップ { #steps } そして、ブラウザ上で実行されているフロントエンド (`http://localhost:8080`) があり、そのJavaScriptが `http://localhost` で実行されているバックエンドと通信するとします。(ポートを指定していないので、ブラウザはデフォルトの`80`ポートを使用します) -次に、ブラウザはHTTPの `OPTIONS` リクエストをバックエンドに送信します。そして、バックエンドがこの異なるオリジン (`http://localhost:8080`) からの通信を許可する適切なヘッダーを送信すると、ブラウザはフロントエンドのJavaScriptにバックエンドへのリクエストを送信させます。 +次に、ブラウザはHTTPの `OPTIONS` リクエストを `:80` のバックエンドに送信します。そして、バックエンドがこの異なるオリジン (`http://localhost:8080`) からの通信を許可する適切なヘッダーを送信すると、`:8080` のブラウザはフロントエンドのJavaScriptに `:80` のバックエンドへのリクエストを送信させます。 -これを実現するには、バックエンドに「許可されたオリジン」のリストがなければなりません。 +これを実現するには、`:80` のバックエンドに「許可されたオリジン」のリストがなければなりません。 -この場合、フロントエンドを正しく機能させるには、そのリストに `http://localhost:8080` を含める必要があります。 +この場合、`:8080` のフロントエンドを正しく機能させるには、そのリストに `http://localhost:8080` を含める必要があります。 -## ワイルドカード +## ワイルドカード { #wildcards } -リストを `"*"` (ワイルドカード) と宣言して、すべてを許可することもできます。 +リストを `"*"` (「ワイルドカード」) と宣言して、すべてを許可することもできます。 -ただし、Bearer Tokenで使用されるような認証ヘッダーやCookieなどのクレデンシャル情報に関するものを除いて、特定の種類の通信のみが許可されます。 +ただし、クレデンシャル情報に関するもの、つまりCookie、Bearer Tokenで使用されるようなAuthorizationヘッダーなどを含むものは除外され、特定の種類の通信のみが許可されます。 したがって、すべてを正しく機能させるために、許可されたオリジンの明示的な指定をお勧めします。 -## `CORSMiddleware` の使用 +## `CORSMiddleware` の使用 { #use-corsmiddleware } **FastAPI** アプリケーションでは `CORSMiddleware` を使用して、CORSに関する設定ができます。 @@ -42,39 +42,43 @@ 以下も、バックエンドに許可させるかどうか指定できます: -* クレデンシャル情報 (認証ヘッダー、Cookieなど) 。 +* クレデンシャル情報 (Authorizationヘッダー、Cookieなど) 。 * 特定の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` 実装のデフォルトのパラメータはCORSに関して制限を与えるものになっているので、ブラウザにドメインを跨いで特定のオリジン、メソッド、またはヘッダーを使用可能にするためには、それらを明示的に有効にする必要があります + +`CORSMiddleware` 実装で使用されるデフォルトのパラメータはデフォルトで制限が厳しいため、ブラウザがクロスドメインのコンテキストでそれらを使用できるようにするには、特定のオリジン、メソッド、またはヘッダーを明示的に有効にする必要があります。 以下の引数がサポートされています: * `allow_origins` - オリジン間リクエストを許可するオリジンのリスト。例えば、`['https://example.org', 'https://www.example.org']`。`['*']`を使用して任意のオリジンを許可できます。 * `allow_origin_regex` - オリジン間リクエストを許可するオリジンの正規表現文字列。例えば、`'https://.*\.example\.org'`。 * `allow_methods` - オリジン間リクエストで許可するHTTPメソッドのリスト。デフォルトは `['GET']` です。`['*']`を使用してすべての標準メソッドを許可できます。 -* `allow_headers` - オリジン間リクエストでサポートするHTTPリクエストヘッダーのリスト。デフォルトは `[]` です。`['*']`を使用して、すべてのヘッダーを許可できます。CORSリクエストでは、 `Accept` 、 `Accept-Language` 、 `Content-Language` 、 `Content-Type` ヘッダーが常に許可されます。 +* `allow_headers` - オリジン間リクエストでサポートするHTTPリクエストヘッダーのリスト。デフォルトは `[]` です。`['*']`を使用して、すべてのヘッダーを許可できます。シンプルなCORSリクエストでは、 `Accept` 、 `Accept-Language` 、 `Content-Language` 、 `Content-Type` ヘッダーが常に許可されます。 * `allow_credentials` - オリジン間リクエストでCookieをサポートする必要があることを示します。デフォルトは `False` です。 + + `allow_credentials` が `True` に設定されている場合、`allow_origins`、`allow_methods`、`allow_headers` のいずれも `['*']` に設定できません。これらはすべて明示的に指定する必要があります。 + * `expose_headers` - ブラウザからアクセスできるようにするレスポンスヘッダーを示します。デフォルトは `[]` です。 * `max_age` - ブラウザがCORSレスポンスをキャッシュする最大時間を秒単位で設定します。デフォルトは `600` です。 このミドルウェアは2種類のHTTPリクエストに応答します... -### CORSプリフライトリクエスト +### CORSプリフライトリクエスト { #cors-preflight-requests } これらは、 `Origin` ヘッダーと `Access-Control-Request-Method` ヘッダーを持つ `OPTIONS` リクエストです。 この場合、ミドルウェアはリクエストを横取りし、適切なCORSヘッダーと共に情報提供のために `200` または `400` のレスポンスを返します。 -### シンプルなリクエスト +### シンプルなリクエスト { #simple-requests } `Origin` ヘッダーのあるリクエスト。この場合、ミドルウェアは通常どおりリクエストに何もしないですが、レスポンスに適切なCORSヘッダーを加えます。 -## より詳しい情報 +## より詳しい情報 { #more-info } -CORSについてより詳しい情報は、Mozilla CORS documentation を参照して下さい。 +CORSについてより詳しい情報は、Mozilla CORS documentation を参照して下さい。 /// note | 技術詳細 diff --git a/docs/ja/docs/tutorial/debugging.md b/docs/ja/docs/tutorial/debugging.md index 6c29679ef..8fe5b2d5d 100644 --- a/docs/ja/docs/tutorial/debugging.md +++ b/docs/ja/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# デバッグ +# デバッグ { #debugging } Visual Studio CodeやPyCharmなどを使用して、エディター上でデバッガーと連携できます。 -## `uvicorn` の実行 +## `uvicorn` を呼び出す { #call-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__"` を持ちません。 したがって、以下の行: @@ -78,7 +78,7 @@ from myapp import app /// -## デバッガーでコードを実行 +## デバッガーでコードを実行 { #run-your-code-with-your-debugger } コードから直接Uvicornサーバーを実行しているため、デバッガーから直接Pythonプログラム (FastAPIアプリケーション) を呼び出せます。 diff --git a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md index 80153529e..3cb1fe73d 100644 --- a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,12 +1,12 @@ -# 依存関係としてのクラス +# 依存関係としてのクラス { #classes-as-dependencies } **依存性注入** システムを深く掘り下げる前に、先ほどの例をアップグレードしてみましょう。 -## 前の例の`dict` +## 前の例の`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] *} しかし、*path operation関数*のパラメータ`commons`に`dict`が含まれています。 @@ -14,7 +14,7 @@ もっとうまくやれるはずです...。 -## 依存関係を作るもの +## 依存関係を作るもの { #what-makes-a-dependency } これまでは、依存関係が関数として宣言されているのを見てきました。 @@ -38,7 +38,7 @@ something(some_argument, some_keyword_argument="foo") これを「呼び出し可能」なものと呼びます。 -## 依存関係としてのクラス +## 依存関係としてのクラス { #classes-as-dependencies_1 } Pythonのクラスのインスタンスを作成する際に、同じ構文を使用していることに気づくかもしれません。 @@ -67,48 +67,66 @@ FastAPIが実際にチェックしているのは、それが「呼び出し可 それは、パラメータが全くない呼び出し可能なものにも適用されます。パラメータのない*path operation関数*と同じように。 -そこで、上で紹介した依存関係の`common_parameters`を`CommonQueryParams`クラスに変更します: +そこで、上で紹介した依存関係の"dependable" `common_parameters`を`CommonQueryParams`クラスに変更します: -{* ../../docs_src/dependencies/tutorial002.py hl[11,12,13,14,15] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} クラスのインスタンスを作成するために使用される`__init__`メソッドに注目してください: -{* ../../docs_src/dependencies/tutorial002.py hl[12] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} ...以前の`common_parameters`と同じパラメータを持っています: -{* ../../docs_src/dependencies/tutorial001.py hl[8] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} これらのパラメータは **FastAPI** が依存関係を「解決」するために使用するものです。 どちらの場合も以下を持っています: -* オプショナルの`q`クエリパラメータ。 -* `skip`クエリパラメータ、デフォルトは`0`。 -* `limit`クエリパラメータ、デフォルトは`100`。 +* `str`であるオプショナルの`q`クエリパラメータ。 +* デフォルトが`0`である`int`の`skip`クエリパラメータ。 +* デフォルトが`100`である`int`の`limit`クエリパラメータ。 どちらの場合も、データは変換され、検証され、OpenAPIスキーマなどで文書化されます。 -## 使用 +## 使用 { #use-it } これで、このクラスを使用して依存関係を宣言することができます。 -{* ../../docs_src/dependencies/tutorial002.py hl[19] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} **FastAPI** は`CommonQueryParams`クラスを呼び出します。これにより、そのクラスの「インスタンス」が作成され、インスタンスはパラメータ`commons`として関数に渡されます。 -## 型注釈と`Depends` +## 型注釈と`Depends` { #type-annotation-vs-depends } 上のコードでは`CommonQueryParams`を2回書いていることに注目してください: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + 以下にある最後の`CommonQueryParams`: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...は、**FastAPI** が依存関係を知るために実際に使用するものです。 @@ -119,55 +137,145 @@ commons: CommonQueryParams = Depends(CommonQueryParams) この場合、以下にある最初の`CommonQueryParams`: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams ... ``` -...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`= Depends(CommonQueryParams)`を使用しています)。 +//// + +...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`Depends(CommonQueryParams)`を使用しています)。 実際には以下のように書けばいいだけです: +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons = Depends(CommonQueryParams) ``` +//// + 以下にあるように: -{* ../../docs_src/dependencies/tutorial003.py hl[19] *} +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} しかし、型を宣言することは推奨されています。そうすれば、エディタは`commons`のパラメータとして何が渡されるかを知ることができ、コードの補完や型チェックなどを行うのに役立ちます: - + -## ショートカット +## ショートカット { #shortcut } しかし、ここでは`CommonQueryParams`を2回書くというコードの繰り返しが発生していることがわかります: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + 依存関係が、クラス自体のインスタンスを作成するために**FastAPI**が「呼び出す」*特定の*クラスである場合、**FastAPI** はこれらのケースのショートカットを提供しています。 それらの具体的なケースについては以下のようにします: 以下のように書く代わりに: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + ...以下のように書きます: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends() ``` +//// + パラメータの型として依存関係を宣言し、`Depends()`の中でパラメータを指定せず、`Depends()`をその関数のパラメータの「デフォルト」値(`=`のあとの値)として使用することで、`Depends(CommonQueryParams)`の中でクラス全体を*もう一度*書かなくてもよくなります。 同じ例では以下のようになります: -{* ../../docs_src/dependencies/tutorial004.py hl[19] *} +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} ...そして **FastAPI** は何をすべきか知っています。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 0fb15ae02..2051afc05 100644 --- a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,57 +1,69 @@ -# path operationデコレータの依存関係 +# path operation デコレータの依存関係 { #dependencies-in-path-operation-decorators } -場合によっては*path operation関数*の中で依存関係の戻り値を本当に必要としないこともあります。 +場合によっては、*path operation 関数*の中で依存関係の戻り値を実際には必要としないことがあります。 -もしくは、依存関係が値を返さない場合もあります。 +または、依存関係が値を返さない場合もあります。 -しかし、それでも実行・解決する必要があります。 +しかし、それでも実行・解決される必要があります。 -このような場合、*path operation関数*のパラメータを`Depends`で宣言する代わりに、*path operation decorator*に`dependencies`の`list`を追加することができます。 +そのような場合、`Depends` で *path operation 関数* のパラメータを宣言する代わりに、*path operation デコレータ*に `dependencies` の `list` を追加できます。 -## *path operationデコレータ*への`dependencies`の追加 +## *path operation デコレータ*に`dependencies`を追加 { #add-dependencies-to-the-path-operation-decorator } -*path operationデコレータ*はオプショナルの引数`dependencies`を受け取ります。 +*path operation デコレータ*はオプション引数`dependencies`を受け取ります。 それは`Depends()`の`list`であるべきです: -{* ../../docs_src/dependencies/tutorial006.py hl[17] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *} -これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation関数*には渡されません。 +これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation 関数*には渡されません。 /// tip | 豆知識 -エディタによっては、未使用の関数パラメータをチェックしてエラーとして表示するものもあります。 +一部のエディタは、未使用の関数パラメータをチェックしてエラーとして表示します。 -`dependencies`を`path operationデコレータ`で使用することで、エディタやツールのエラーを回避しながら確実に実行することができます。 +これらの`dependencies`を*path operation デコレータ*で使用することで、エディタ/ツールのエラーを回避しつつ、確実に実行されるようにできます。 -また、コードの未使用のパラメータがあるのを見て、それが不要だと思ってしまうような新しい開発者の混乱を避けるのにも役立つかもしれません。 +また、コード内の未使用のパラメータを見た新しい開発者が、それを不要だと思って混乱するのを避ける助けにもなるかもしれません。 /// -## 依存関係のエラーと戻り値 +/// info | 情報 -通常使用している依存関係の*関数*と同じものを使用することができます。 +この例では、架空のカスタムヘッダー `X-Key` と `X-Token` を使用しています。 -### 依存関係の要件 +しかし実際のケースでセキュリティを実装する際は、統合された[Security utilities(次の章)](../security/index.md){.internal-link target=_blank}を使うことで、より多くの利点を得られます。 -これらはリクエストの要件(ヘッダのようなもの)やその他のサブ依存関係を宣言することができます: +/// -{* ../../docs_src/dependencies/tutorial006.py hl[6,11] *} +## 依存関係のエラーと戻り値 { #dependencies-errors-and-return-values } -### 例外の発生 +通常使用している依存関係の*関数*と同じものを使用できます。 -これらの依存関係は通常の依存関係と同じように、例外を`raise`発生させることができます: +### 依存関係の要件 { #dependency-requirements } -{* ../../docs_src/dependencies/tutorial006.py hl[8,13] *} +これらはリクエストの要件(ヘッダーのようなもの)やその他のサブ依存関係を宣言できます: -### 戻り値 +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} + +### 例外の発生 { #raise-exceptions } + +これらの依存関係は、通常の依存関係と同じように例外を`raise`できます: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} + +### 戻り値 { #return-values } そして、値を返すことも返さないこともできますが、値は使われません。 -つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用することができ、値は使われなくても依存関係は実行されます: +つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用でき、値は使われなくても依存関係は実行されます: -{* ../../docs_src/dependencies/tutorial006.py hl[9,14] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## *path operations*のグループに対する依存関係 +## *path operation*のグループに対する依存関係 { #dependencies-for-a-group-of-path-operations } -後で、より大きなアプリケーションの構造([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読む時に、おそらく複数のファイルを使用して、*path operations*のグループに対して単一の`dependencies`パラメータを宣言する方法を学ぶでしょう。 +後で、より大きなアプリケーションを(おそらく複数ファイルで)構造化する方法([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読むときに、*path operation*のグループに対して単一の`dependencies`パラメータを宣言する方法を学びます。 + +## グローバル依存関係 { #global-dependencies } + +次に、`FastAPI`アプリケーション全体に依存関係を追加して、各*path operation*に適用する方法を見ていきます。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md index 35a69de0d..8095114c3 100644 --- a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,24 +1,12 @@ -# yieldを持つ依存関係 +# `yield`を持つ依存関係 { #dependencies-with-yield } -FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。 +FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。 -これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップを書きます。 +これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップ(コード)を書きます。 /// tip | 豆知識 -`yield`は必ず一度だけ使用するようにしてください。 - -/// - -/// info | 情報 - -これを動作させるには、**Python 3.7** 以上を使用するか、**Python 3.6** では"backports"をインストールする必要があります: - -``` -pip install async-exit-stack async-generator -``` - -これによりasync-exit-stackasync-generatorがインストールされます。 +`yield`は必ず依存関係ごとに1回だけ使用するようにしてください。 /// @@ -35,21 +23,21 @@ pip install async-exit-stack async-generator /// -## `yield`を持つデータベースの依存関係 +## `yield`を持つデータベースの依存関係 { #a-database-dependency-with-yield } 例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。 -レスポンスを送信する前に`yield`文を含む前のコードのみが実行されます。 +レスポンスを作成する前に、`yield`文より前のコード(および`yield`文を含む)が実行されます: -{* ../../docs_src/dependencies/tutorial007.py hl[2,3,4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} 生成された値は、*path operations*や他の依存関係に注入されるものです: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} -`yield`文に続くコードは、レスポンスが送信された後に実行されます: +`yield`文に続くコードは、レスポンスの後に実行されます: -{* ../../docs_src/dependencies/tutorial007.py hl[5,6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | 豆知識 @@ -59,27 +47,27 @@ pip install async-exit-stack async-generator /// -## `yield`と`try`を持つ依存関係 +## `yield`と`try`を持つ依存関係 { #a-dependency-with-yield-and-try } -`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。 +`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際にスローされたあらゆる例外を受け取ることになります。 -例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 +例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他の例外を作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 そのため、依存関係の中にある特定の例外を`except SomeException`で探すことができます。 同様に、`finally`を用いて例外があったかどうかにかかわらず、終了ステップを確実に実行することができます。 -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -## `yield`を持つサブ依存関係 +## `yield`を持つサブ依存関係 { #sub-dependencies-with-yield } 任意の大きさや形のサブ依存関係やサブ依存関係の「ツリー」を持つことができ、その中で`yield`を使用することができます。 **FastAPI** は、`yield`を持つ各依存関係の「終了コード」が正しい順番で実行されていることを確認します。 -例えば、`dependency_c`は`dependency_b`と`dependency_b`に依存する`dependency_a`に、依存することができます: +例えば、`dependency_c`は`dependency_b`に、そして`dependency_b`は`dependency_a`に依存することができます: -{* ../../docs_src/dependencies/tutorial008.py hl[4,12,20] *} +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} そして、それらはすべて`yield`を使用することができます。 @@ -87,11 +75,11 @@ pip install async-exit-stack async-generator そして、`dependency_b`は`dependency_a`(ここでは`dep_a`という名前)の値を終了コードで利用できるようにする必要があります。 -{* ../../docs_src/dependencies/tutorial008.py hl[16,17,24,25] *} +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} -同様に、`yield`と`return`が混在した依存関係を持つこともできます。 +同様に、`yield`を持つ依存関係と`return`を持つ他の依存関係をいくつか持ち、それらの一部が他の一部に依存するようにもできます。 -また、単一の依存関係を持っていて、`yield`などの他の依存関係をいくつか必要とすることもできます。 +また、単一の依存関係を持っていて、`yield`を持つ他の依存関係をいくつか必要とすることもできます。 依存関係の組み合わせは自由です。 @@ -105,32 +93,46 @@ pip install async-exit-stack async-generator /// -## `yield`と`HTTPException`を持つ依存関係 +## `yield`と`HTTPException`を持つ依存関係 { #dependencies-with-yield-and-httpexception } -`yield`と例外をキャッチする`try`ブロックを持つことができる依存関係を使用することができることがわかりました。 +`yield`を持つ依存関係を使い、何らかのコードを実行し、その後に`finally`の後で終了コードを実行しようとする`try`ブロックを持てることが分かりました。 -`yield`の後の終了コードで`HTTPException`などを発生させたくなるかもしれません。しかし**それはうまくいきません** +また、`except`を使って発生した例外をキャッチし、それに対して何かをすることもできます。 -`yield`を持つ依存関係の終了コードは[例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}の*後に*実行されます。依存関係によって投げられた例外を終了コード(`yield`の後)でキャッチするものはなにもありません。 - -つまり、`yield`の後に`HTTPException`を発生させた場合、`HTTTPException`をキャッチしてHTTP 400のレスポンスを返すデフォルトの(あるいは任意のカスタムの)例外ハンドラは、その例外をキャッチすることができなくなります。 - -これは、依存関係に設定されているもの(例えば、DBセッション)を、例えば、バックグラウンドタスクで使用できるようにするものです。 - -バックグラウンドタスクはレスポンスが送信された*後*に実行されます。そのため、*すでに送信されている*レスポンスを変更する方法すらないので、`HTTPException`を発生させる方法はありません。 - -しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくとも`yield`で依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。 - -例外が発生する可能性があるコードがある場合は、最も普通の「Python流」なことをして、コードのその部分に`try`ブロックを追加してください。 - -レスポンスを返したり、レスポンスを変更したり、`HTTPException`を発生させたりする*前に*処理したいカスタム例外がある場合は、[カスタム例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}を作成してください。 +例えば、`HTTPException`のように別の例外を発生させることができます。 /// tip | 豆知識 -`HTTPException`を含む例外は、`yield`の*前*でも発生させることができます。ただし、後ではできません。 +これはやや高度なテクニックで、ほとんどの場合は本当に必要にはなりません。例えば、*path operation 関数*など、アプリケーションコードの他の場所から(`HTTPException`を含む)例外を発生させられるためです。 + +ただし必要であれば使えます。 🤓 /// +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} + +例外をキャッチして、それに基づいてカスタムレスポンスを作成したい場合は、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成してください。 + +## `yield`と`except`を持つ依存関係 { #dependencies-with-yield-and-except } + +`yield`を持つ依存関係で`except`を使って例外をキャッチし、それを再度raiseしない(または新しい例外をraiseしない)場合、通常のPythonと同じように、FastAPIは例外があったことに気づけません: + +{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} + +この場合、(`HTTPException`やそれに類するものをraiseしていないため)クライアントには適切に*HTTP 500 Internal Server Error*レスポンスが返りますが、サーバーには**ログが一切残らず**、何がエラーだったのかを示す他の手がかりもありません。 😱 + +### `yield`と`except`を持つ依存関係では常に`raise`する { #always-raise-in-dependencies-with-yield-and-except } + +`yield`を持つ依存関係で例外をキャッチした場合、別の`HTTPException`などをraiseするのでない限り、**元の例外を再raiseすべきです**。 + +`raise`を使うと同じ例外を再raiseできます: + +{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} + +これでクライアントは同じ*HTTP 500 Internal Server Error*レスポンスを受け取りますが、サーバーのログにはカスタムの`InternalError`が残ります。 😎 + +## `yield`を持つ依存関係の実行 { #execution-of-dependencies-with-yield } + 実行の順序は多かれ少なかれ以下の図のようになります。時間は上から下へと流れていきます。そして、各列はコードを相互作用させたり、実行したりしている部分の一つです。 ```mermaid @@ -142,32 +144,29 @@ 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 + Note over client,operation: Can raise exceptions, including HTTPException client ->> dep: Start request Note over dep: Run code up to yield - opt raise - dep -->> handler: Raise HTTPException + opt raise Exception + dep -->> handler: Raise Exception handler -->> client: HTTP error response - dep -->> dep: Raise other exception end dep ->> operation: Run dependency, e.g. DB session opt raise - operation -->> handler: Raise HTTPException + operation -->> dep: Raise Exception (e.g. HTTPException) + opt handle + dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception + end handler -->> client: HTTP error response - operation -->> dep: Raise other 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. + tasks -->> tasks: Handle exceptions in the background task code end ``` @@ -181,15 +180,63 @@ participant tasks as Background tasks /// tip | 豆知識 -この図は`HTTPException`を示していますが、[カスタム例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}を作成することで、他の例外を発生させることもできます。そして、その例外は依存関係の終了コードではなく、そのカスタム例外ハンドラによって処理されます。 - -しかし例外ハンドラで処理されない例外を発生させた場合は、依存関係の終了コードで処理されます。 +*path operation 関数*のコードで例外をraiseした場合、`HTTPException`を含め、それはyieldを持つ依存関係に渡されます。ほとんどの場合、その例外が正しく処理されるように、`yield`を持つ依存関係から同じ例外、または新しい例外を再raiseしたくなるでしょう。 /// -## コンテキストマネージャ +## 早期終了と`scope` { #early-exit-and-scope } -### 「コンテキストマネージャ」とは +通常、`yield`を持つ依存関係の終了コードは、クライアントに**レスポンスが送信された後**に実行されます。 + +しかし、*path operation 関数*からreturnした後に依存関係を使う必要がないと分かっている場合は、`Depends(scope="function")`を使って、**レスポンスが送信される前**に、*path operation 関数*のreturn後に依存関係を閉じるべきだとFastAPIに伝えられます。 + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()`は、以下のいずれかを取る`scope`パラメータを受け取ります: + +* `"function"`: リクエストを処理する*path operation 関数*の前に依存関係を開始し、*path operation 関数*の終了後に依存関係を終了しますが、クライアントにレスポンスが返される**前**に終了します。つまり、依存関係関数は*path operation 関数*の**周囲**で実行されます。 +* `"request"`: リクエストを処理する*path operation 関数*の前に依存関係を開始し(`"function"`を使用する場合と同様)、クライアントにレスポンスが返された**後**に終了します。つまり、依存関係関数は**リクエスト**とレスポンスのサイクルの**周囲**で実行されます。 + +指定されておらず、依存関係に`yield`がある場合、デフォルトで`scope`は`"request"`になります。 + +### サブ依存関係の`scope` { #scope-for-sub-dependencies } + +`scope="request"`(デフォルト)を持つ依存関係を宣言する場合、どのサブ依存関係も`"request"`の`scope`を持つ必要があります。 + +しかし、`"function"`の`scope`を持つ依存関係は、`"function"`と`"request"`の`scope`を持つ依存関係を持てます。 + +これは、いずれの依存関係も、サブ依存関係より前に終了コードを実行できる必要があるためです(終了コードの実行中にサブ依存関係をまだ使う必要がある可能性があるためです)。 + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> 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 +``` + +## `yield`、`HTTPException`、`except`、バックグラウンドタスクを持つ依存関係 { #dependencies-with-yield-httpexception-except-and-background-tasks } + +`yield`を持つ依存関係は、さまざまなユースケースをカバーし、いくつかの問題を修正するために、時間とともに進化してきました。 + +FastAPIの異なるバージョンで何が変わったのかを知りたい場合は、上級ガイドの[上級の依存関係 - `yield`、`HTTPException`、`except`、バックグラウンドタスクを持つ依存関係](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}で詳しく読めます。 +## コンテキストマネージャ { #context-managers } + +### 「コンテキストマネージャ」とは { #what-are-context-managers } 「コンテキストマネージャ」とは、`with`文の中で使用できるPythonオブジェクトのことです。 @@ -205,9 +252,9 @@ with open("./somefile.txt") as f: `with`ブロックが終了すると、例外があったとしてもファイルを確かに閉じます。 -`yield`を依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 +`yield`を持つ依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 -### `yield`を持つ依存関係でのコンテキストマネージャの使用 +### `yield`を持つ依存関係でのコンテキストマネージャの使用 { #using-context-managers-in-dependencies-with-yield } /// warning | 注意 @@ -221,7 +268,7 @@ Pythonでは、依存性注入** システムを持っています。 +**FastAPI** は非常に強力でありながら直感的な **Dependency Injection** システムを持っています。 それは非常にシンプルに使用できるように設計されており、開発者が他のコンポーネント **FastAPI** と統合するのが非常に簡単になるように設計されています。 -## 「依存性注入」とは +## 「Dependency Injection」とは { #what-is-dependency-injection } -**「依存性注入」** とは、プログラミングにおいて、コード(この場合は、*path operation関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: +**「Dependency Injection」** とは、プログラミングにおいて、コード(この場合は、*path operation 関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: そして、そのシステム(この場合は、**FastAPI**)は、必要な依存関係をコードに提供するために必要なことは何でも行います(依存関係を「注入」します)。 @@ -19,27 +19,27 @@ これらすべてを、コードの繰り返しを最小限に抑えながら行います。 -## 最初のステップ +## 最初のステップ { #first-steps } 非常にシンプルな例を見てみましょう。あまりにもシンプルなので、今のところはあまり参考にならないでしょう。 -しかし、この方法では **依存性注入** システムがどのように機能するかに焦点を当てることができます。 +しかし、この方法では **Dependency Injection** システムがどのように機能するかに焦点を当てることができます。 -### 依存関係の作成 +### 依存関係(「dependable」)の作成 { #create-a-dependency-or-dependable } まずは依存関係に注目してみましょう。 -以下のように、*path operation関数*と同じパラメータを全て取ることができる関数にすぎません: +以下のように、*path operation 関数*と同じパラメータを全て取ることができる関数にすぎません: -{* ../../docs_src/dependencies/tutorial001.py hl[8,9] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} これだけです。 **2行**。 -そして、それはすべての*path operation関数*が持っているのと同じ形と構造を持っています。 +そして、それはすべての*path operation 関数*が持っているのと同じ形と構造を持っています。 -「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation関数*と考えることもできます。 +「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation 関数*と考えることもできます。 そして何でも返すことができます。 @@ -51,15 +51,25 @@ そして、これらの値を含む`dict`を返します。 -### `Depends`のインポート +/// info | 情報 -{* ../../docs_src/dependencies/tutorial001.py hl[3] *} +FastAPI はバージョン 0.95.0 で `Annotated` のサポートを追加し(そして推奨し始めました)。 -### "dependant"での依存関係の宣言 +古いバージョンを使用している場合、`Annotated` を使おうとするとエラーになります。 -*path operation関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: +`Annotated` を使用する前に、少なくとも 0.95.1 まで [FastAPI のバージョンをアップグレード](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} してください。 -{* ../../docs_src/dependencies/tutorial001.py hl[13,18] *} +/// + +### `Depends`のインポート { #import-depends } + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} + +### 「dependant」での依存関係の宣言 { #declare-the-dependency-in-the-dependant } + +*path operation 関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} 関数のパラメータに`Depends`を使用するのは`Body`や`Query`などと同じですが、`Depends`の動作は少し異なります。 @@ -67,7 +77,9 @@ このパラメータは関数のようなものである必要があります。 -そして、その関数は、*path operation関数*が行うのと同じ方法でパラメータを取ります。 +直接**呼び出しません**(末尾に括弧を付けません)。`Depends()` のパラメータとして渡すだけです。 + +そして、その関数は、*path operation 関数*が行うのと同じ方法でパラメータを取ります。 /// tip | 豆知識 @@ -79,7 +91,7 @@ * 依存関係("dependable")関数を正しいパラメータで呼び出します。 * 関数の結果を取得します。 -* *path operation関数*のパラメータにその結果を代入してください。 +* *path operation 関数*のパラメータにその結果を代入してください。 ```mermaid graph TB @@ -92,7 +104,7 @@ common_parameters --> read_items common_parameters --> read_users ``` -この方法では、共有されるコードを一度書き、**FastAPI** が*path operations*のための呼び出しを行います。 +この方法では、共有されるコードを一度書き、**FastAPI** が*path operation*のための呼び出しを行います。 /// check | 確認 @@ -102,59 +114,85 @@ common_parameters --> read_users /// -## `async`にするかどうか +## `Annotated` 依存関係の共有 { #share-annotated-dependencies } -依存関係は **FastAPI**(*path operation関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 +上の例では、ほんの少し **コードの重複** があることがわかります。 + +`common_parameters()` 依存関係を使う必要があるときは、型アノテーションと `Depends()` を含むパラメータ全体を書く必要があります: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +しかし、`Annotated` を使用しているので、その `Annotated` 値を変数に格納して複数箇所で使えます: + +{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} + +/// tip | 豆知識 + +これはただの標準 Python で、「type alias」と呼ばれ、**FastAPI** 固有のものではありません。 + +しかし **FastAPI** は `Annotated` を含む Python 標準に基づいているため、このテクニックをコードで使えます。 😎 + +/// + +依存関係は期待どおりに動作し続け、**一番良い点** は **型情報が保持される** ことです。つまり、エディタは **自動補完**、**インラインエラー** などを提供し続けられます。`mypy` のような他のツールでも同様です。 + +これは **大規模なコードベース** で、**同じ依存関係** を **多くの *path operation*** で何度も使う場合に特に役立ちます。 + +## `async`にするかどうか { #to-async-or-not-to-async } + +依存関係は **FastAPI**(*path operation 関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 `async def`や通常の`def`を使用することができます。 -また、通常の`def`*path operation関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 +また、通常の`def`*path operation 関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation 関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 それは重要ではありません。**FastAPI** は何をすべきかを知っています。 /// note | 備考 -わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 +わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md#in-a-hurry){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 /// -## OpenAPIとの統合 +## OpenAPIとの統合 { #integrated-with-openapi } 依存関係(およびサブ依存関係)のすべてのリクエスト宣言、検証、および要件は、同じOpenAPIスキーマに統合されます。 つまり、対話型ドキュメントにはこれらの依存関係から得られる全ての情報も含まれているということです: - + -## 簡単な使い方 +## 簡単な使い方 { #simple-usage } -見てみると、*path*と*operation*が一致した時に*path operation関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 +見てみると、*path*と*operation*が一致した時に*path operation 関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 実は、すべての(あるいはほとんどの)Webフレームワークは、このように動作します。 これらの関数を直接呼び出すことはありません。これらの関数はフレームワーク(この場合は、**FastAPI**)によって呼び出されます。 -依存性注入システムでは、**FastAPI** に*path operation*もまた、*path operation関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 +Dependency Injection システムでは、**FastAPI** に*path operation 関数*もまた、*path operation 関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 -他にも、「依存性注入」と同じような考えの一般的な用語があります: +他にも、「dependency injection」と同じような考えの一般的な用語があります: -* リソース -* プロバイダ -* サービス -* インジェクタブル -* コンポーネント +* resources +* providers +* services +* injectables +* components -## **FastAPI** プラグイン +## **FastAPI** プラグイン { #fastapi-plug-ins } -統合や「プラグイン」は **依存性注入** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが**path operation関数*で利用可能になるからです。 +統合や「プラグイン」は **Dependency Injection** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが*path operation 関数*で利用可能になるからです。 依存関係は非常にシンプルで直感的な方法で作成することができ、必要なPythonパッケージをインポートするだけで、*文字通り*数行のコードでAPI関数と統合することができます。 次の章では、リレーショナルデータベースやNoSQLデータベース、セキュリティなどについて、その例を見ていきます。 -## **FastAPI** 互換性 +## **FastAPI** 互換性 { #fastapi-compatibility } -依存性注入システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: +dependency injection システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: * すべてのリレーショナルデータベース * NoSQLデータベース @@ -165,15 +203,15 @@ common_parameters --> read_users * レスポンスデータ注入システム * など。 -## シンプルでパワフル +## シンプルでパワフル { #simple-and-powerful } -階層依存性注入システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 +階層的な dependency injection システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 -依存関係事態を定義する依存関係を定義することができます。 +依存関係が、さらに依存関係を定義することもできます。 -最終的には、依存関係の階層ツリーが構築され、**依存性注入**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 +最終的には、依存関係の階層ツリーが構築され、**Dependency Injection**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 -例えば、4つのAPIエンドポイント(*path operations*)があるとします: +例えば、4つのAPIエンドポイント(*path operation*)があるとします: * `/items/public/` * `/items/private/` @@ -205,8 +243,8 @@ admin_user --> activate_user paying_user --> pro_items ``` -## **OpenAPI** との統合 +## **OpenAPI** との統合 { #integrated-with-openapi_1 } -これら全ての依存関係は、要件を宣言すると同時に、*path operations*にパラメータやバリデーションを追加します。 +これら全ての依存関係は、要件を宣言すると同時に、*path operation*にパラメータやバリデーションを追加します。 **FastAPI** はそれをすべてOpenAPIスキーマに追加して、対話型のドキュメントシステムに表示されるようにします。 diff --git a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md index 211a86a0a..007c320f3 100644 --- a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# サブ依存関係 +# サブ依存関係 { #sub-dependencies } **サブ依存関係** を持つ依存関係を作成することができます。 @@ -6,21 +6,21 @@ **FastAPI** はそれらを解決してくれます。 -### 最初の依存関係「依存可能なもの」 +## 最初の依存関係「依存可能なもの」 { #first-dependency-dependable } 以下のような最初の依存関係(「依存可能なもの」)を作成することができます: -{* ../../docs_src/dependencies/tutorial005.py hl[8,9] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} これはオプショナルのクエリパラメータ`q`を`str`として宣言し、それを返すだけです。 これは非常にシンプルです(あまり便利ではありません)が、サブ依存関係がどのように機能するかに焦点を当てるのに役立ちます。 -### 第二の依存関係 「依存可能なもの」と「依存」 +## 第二の依存関係 「依存可能なもの」と「依存」 { #second-dependency-dependable-and-dependant } そして、別の依存関数(「依存可能なもの」)を作成して、同時にそれ自身の依存関係を宣言することができます(つまりそれ自身も「依存」です): -{* ../../docs_src/dependencies/tutorial005.py hl[13] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} 宣言されたパラメータに注目してみましょう: @@ -29,15 +29,15 @@ * また、オプショナルの`last_query`クッキーを`str`として宣言します。 * ユーザーがクエリ`q`を提供しなかった場合、クッキーに保存していた最後に使用したクエリを使用します。 -### 依存関係の使用 +## 依存関係の使用 { #use-the-dependency } 以下のように依存関係を使用することができます: -{* ../../docs_src/dependencies/tutorial005.py hl[21] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} /// info | 情報 -*path operation関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 +*path operation 関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 しかし、**FastAPI** は`query_extractor`を最初に解決し、その結果を`query_or_cookie_extractor`を呼び出す時に渡す必要があることを知っています。 @@ -54,24 +54,43 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## 同じ依存関係の複数回の使用 +## 同じ依存関係の複数回の使用 { #using-the-same-dependency-multiple-times } -依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 +依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 -そして、返された値を「キャッシュ」に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、特定のリクエストでそれを必要とする全ての「依存関係」に渡すことになります。 +そして、返された値を「キャッシュ」に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、その特定のリクエストでそれを必要とする全ての「依存」に渡すことになります。 -高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depens`を使用する際に、`use_cache=False`というパラメータを設定することができます。 +高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depends`を使用する際に、`use_cache=False`というパラメータを設定することができます: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ 非Annotated + +/// tip | 豆知識 + +可能であれば`Annotated`版を使うことを推奨します。 + +/// ```Python hl_lines="1" async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): return {"fresh_value": fresh_value} ``` -## まとめ +//// -ここで使われている派手な言葉は別にして、**依存性注入** システムは非常にシンプルです。 +## まとめ { #recap } -*path operation関数*と同じように見えるただの関数です。 +ここで使われている派手な言葉は別にして、**Dependency Injection** システムは非常にシンプルです。 + +*path operation 関数*と同じように見えるただの関数です。 しかし、それでも非常に強力で、任意の深くネストされた依存関係「グラフ」(ツリー)を宣言することができます。 diff --git a/docs/ja/docs/tutorial/encoder.md b/docs/ja/docs/tutorial/encoder.md index 309cf8857..33cc6ae48 100644 --- a/docs/ja/docs/tutorial/encoder.md +++ b/docs/ja/docs/tutorial/encoder.md @@ -1,16 +1,16 @@ -# JSON互換エンコーダ +# JSON互換エンコーダ { #json-compatible-encoder } -データ型(Pydanticモデルのような)をJSONと互換性のあるもの(`dict`や`list`など)に変更する必要がある場合があります。 +データ型(Pydanticモデルのような)をJSONと互換性のあるもの(`dict`や`list`など)に変換する必要があるケースがあります。 例えば、データベースに保存する必要がある場合です。 そのために、**FastAPI** は`jsonable_encoder()`関数を提供しています。 -## `jsonable_encoder`の使用 +## `jsonable_encoder`の使用 { #using-the-jsonable-encoder } JSON互換のデータのみを受信するデータベース`fake_db`があるとしましょう。 -例えば、`datetime`オブジェクトはJSONと互換性がないので、このデーターベースには受け取られません。 +例えば、`datetime`オブジェクトはJSONと互換性がないので、受け取られません。 そのため、`datetime`オブジェクトはISO形式のデータを含む`str`に変換されなければなりません。 @@ -20,7 +20,7 @@ JSON互換のデータのみを受信するデータベース`fake_db`がある Pydanticモデルのようなオブジェクトを受け取り、JSON互換版を返します: -{* ../../docs_src/encoder/tutorial001.py hl[5,22] *} +{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} この例では、Pydanticモデルを`dict`に、`datetime`を`str`に変換します。 diff --git a/docs/ja/docs/tutorial/extra-data-types.md b/docs/ja/docs/tutorial/extra-data-types.md index 1be1c3f92..4ed84e86f 100644 --- a/docs/ja/docs/tutorial/extra-data-types.md +++ b/docs/ja/docs/tutorial/extra-data-types.md @@ -1,6 +1,6 @@ -# 追加データ型 +# 追加データ型 { #extra-data-types } -今までは、以下のような一般的なデータ型を使用してきました: +今まで、以下のような一般的なデータ型を使用してきました: * `int` * `float` @@ -11,13 +11,13 @@ そして、今まで見てきたのと同じ機能を持つことになります: -* 素晴らしいエディタのサポート -* 受信したリクエストからのデータ変換 -* レスポンスデータのデータ変換 -* データの検証 -* 自動注釈と文書化 +* 素晴らしいエディタのサポート。 +* 受信したリクエストからのデータ変換。 +* レスポンスデータのデータ変換。 +* データの検証。 +* 自動注釈と文書化。 -## 他のデータ型 +## 他のデータ型 { #other-data-types } ここでは、使用できる追加のデータ型のいくつかを紹介します: @@ -26,17 +26,17 @@ * リクエストとレスポンスでは`str`として表現されます。 * `datetime.datetime`: * Pythonの`datetime.datetime`です。 - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15T15:53:00+05:00` + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `2008-09-15T15:53:00+05:00`)。 * `datetime.date`: - * Pythonの`datetime.date`です。 - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15` + * Python `datetime.date`。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `2008-09-15`)。 * `datetime.time`: - * Pythonの`datetime.time`. - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `14:23:55.003` + * Pythonの`datetime.time`。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `14:23:55.003`)。 * `datetime.timedelta`: * Pythonの`datetime.timedelta`です。 * リクエストとレスポンスでは合計秒数の`float`で表現されます。 - * Pydanticでは「ISO 8601 time diff encoding」として表現することも可能です。詳細はドキュメントを参照してください。 + * Pydanticでは「ISO 8601 time diff encoding」として表現することも可能です。詳細はドキュメントを参照してください。 * `frozenset`: * リクエストとレスポンスでは`set`と同じように扱われます: * リクエストでは、リストが読み込まれ、重複を排除して`set`に変換されます。 @@ -45,18 +45,18 @@ * `bytes`: * Pythonの標準的な`bytes`です。 * リクエストとレスポンスでは`str`として扱われます。 - * 生成されたスキーマは`str`で`binary`の「フォーマット」持つことを指定します。 + * 生成されたスキーマは`str`で`binary`の「フォーマット」を持つことを指定します。 * `Decimal`: * Pythonの標準的な`Decimal`です。 - * リクエストやレスポンスでは`float`と同じように扱います。 + * リクエストとレスポンスでは`float`と同じように扱われます。 +* Pydanticの全ての有効な型はこちらで確認できます: Pydantic data types。 -* Pydanticの全ての有効な型はこちらで確認できます: Pydantic data types。 -## 例 +## 例 { #example } ここでは、上記の型のいくつかを使用したパラメータを持つ*path operation*の例を示します。 -{* ../../docs_src/extra_data_types/tutorial001.py hl[1,2,12:16] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} -関数内のパラメータは自然なデータ型を持っていることに注意してください。そして、以下のように通常の日付操作を行うことができます: +関数内のパラメータは自然なデータ型を持っていることに注意してください。そして、例えば、以下のように通常の日付操作を行うことができます: -{* ../../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/ja/docs/tutorial/extra-models.md b/docs/ja/docs/tutorial/extra-models.md index b7e215409..05e267818 100644 --- a/docs/ja/docs/tutorial/extra-models.md +++ b/docs/ja/docs/tutorial/extra-models.md @@ -1,6 +1,6 @@ -# モデル - より詳しく +# Extra Models { #extra-models } -先ほどの例に続き、複数の関連モデルを持つことが一般的です。 +先ほどの例に続き、複数の関連モデルを持つことは一般的です。 これはユーザーモデルの場合は特にそうです。なぜなら: @@ -8,27 +8,27 @@ * **出力モデル**はパスワードをもつべきではありません。 * **データベースモデル**はおそらくハッシュ化されたパスワードが必要になるでしょう。 -/// danger | 危険 +/// danger -ユーザーの平文のパスワードは絶対に保存しないでください。常に認証に利用可能な「安全なハッシュ」を保存してください。 +ユーザーの平文のパスワードは絶対に保存しないでください。常に検証できる「安全なハッシュ」を保存してください。 知らない方は、[セキュリティの章](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}で「パスワードハッシュ」とは何かを学ぶことができます。 /// -## 複数のモデル +## Multiple models { #multiple-models } ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します: -{* ../../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] *} -### `**user_in.dict()`について +### About `**user_in.model_dump()` { #about-user-in-model-dump } -#### Pydanticの`.dict()` +#### Pydanticの`.model_dump()` { #pydantics-model-dump } `user_in`は`UserIn`クラスのPydanticモデルです。 -Pydanticモデルには、モデルのデータを含む`dict`を返す`.dict()`メソッドがあります。 +Pydanticモデルには、モデルのデータを含む`dict`を返す`.model_dump()`メソッドがあります。 そこで、以下のようなPydanticオブジェクト`user_in`を作成すると: @@ -39,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com そして呼び出すと: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` これで変数`user_dict`のデータを持つ`dict`ができました。(これはPydanticモデルのオブジェクトの代わりに`dict`です)。 @@ -61,7 +61,7 @@ print(user_dict) } ``` -#### `dict`の展開 +#### `dict`の展開 { #unpacking-a-dict } `user_dict`のような`dict`を受け取り、それを`**user_dict`を持つ関数(またはクラス)に渡すと、Pythonはそれを「展開」します。これは`user_dict`のキーと値を直接キー・バリューの引数として渡します。 @@ -93,31 +93,31 @@ UserInDB( ) ``` -#### 別のモデルからつくるPydanticモデル +#### 別のモデルの内容からつくるPydanticモデル { #a-pydantic-model-from-the-contents-of-another } -上述の例では`user_in.dict()`から`user_dict`をこのコードのように取得していますが: +上述の例では`user_in.model_dump()`から`user_dict`をこのコードのように取得していますが: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` これは以下と同等です: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...なぜなら`user_in.dict()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 +...なぜなら`user_in.model_dump()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 そこで、別のPydanticモデルのデータからPydanticモデルを取得します。 -#### `dict`の展開と追加引数 +#### `dict`の展開と追加キーワード { #unpacking-a-dict-and-extra-keywords } そして、追加のキーワード引数`hashed_password=hashed_password`を以下のように追加すると: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...以下のようになります: @@ -132,13 +132,13 @@ UserInDB( ) ``` -/// warning | 注意 +/// warning -サポートしている追加機能は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 +追加のサポート関数`fake_password_hasher`と`fake_save_user`は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 /// -## 重複の削減 +## Reduce duplication { #reduce-duplication } コードの重複を減らすことは、**FastAPI**の中核的なアイデアの1つです。 @@ -152,40 +152,60 @@ UserInDB( データの変換、検証、文書化などはすべて通常通りに動作します。 -このようにして、モデル間の違いだけを宣言することができます: +このようにして、モデル間の違いだけを宣言することができます(平文の`password`、`hashed_password`、パスワードなし): -{* ../../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`または`anyOf` +## `Union` or `anyOf` { #union-or-anyof } -レスポンスを2つの型の`Union`として宣言することができます。 +レスポンスを2つ以上の型の`Union`として宣言できます。つまり、そのレスポンスはそれらのいずれかになります。 OpenAPIでは`anyOf`で定義されます。 そのためには、標準的なPythonの型ヒント`typing.Union`を使用します: -{* ../../docs_src/extra_models/tutorial003.py hl[1,14,15,18,19,20,33] *} +/// note | 備考 -## モデルのリスト +`Union`を定義する場合は、最も具体的な型を先に、その後により具体性の低い型を含めてください。以下の例では、より具体的な`PlaneItem`が`Union[PlaneItem, CarItem]`内で`CarItem`より前に来ています。 -同じように、オブジェクトのリストのレスポンスを宣言することができます。 +/// -そのためには、標準のPythonの`typing.List`を使用する: +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} -{* ../../docs_src/extra_models/tutorial004.py hl[1,20] *} +### Python 3.10の`Union` { #union-in-python-3-10 } -## 任意の`dict`を持つレスポンス +この例では、引数`response_model`の値として`Union[PlaneItem, CarItem]`を渡しています。 + +**型アノテーション**に書くのではなく、**引数の値**として渡しているため、Python 3.10でも`Union`を使う必要があります。 + +型アノテーションであれば、次のように縦棒を使用できました: + +```Python +some_variable: PlaneItem | CarItem +``` + +しかし、これを代入で`response_model=PlaneItem | CarItem`のように書くと、Pythonはそれを型アノテーションとして解釈するのではなく、`PlaneItem`と`CarItem`の間で**無効な操作**を行おうとしてしまうため、エラーになります。 + +## List of models { #list-of-models } + +同じように、オブジェクトのリストのレスポンスを宣言できます。 + +そのためには、標準のPythonの`typing.List`(またはPython 3.9以降では単に`list`)を使用します: + +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} + +## Response with arbitrary `dict` { #response-with-arbitrary-dict } また、Pydanticモデルを使用せずに、キーと値の型だけを定義した任意の`dict`を使ってレスポンスを宣言することもできます。 これは、有効なフィールド・属性名(Pydanticモデルに必要なもの)を事前に知らない場合に便利です。 -この場合、`typing.Dict`を使用することができます: +この場合、`typing.Dict`(またはPython 3.9以降では単に`dict`)を使用できます: -{* ../../docs_src/extra_models/tutorial005.py hl[1,8] *} +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} -## まとめ +## Recap { #recap } 複数のPydanticモデルを使用し、ケースごとに自由に継承します。 -エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password` や `password_hash` やパスワードなしなどのいくつかの「状態」をもつユーザー「エンティティ」の場合の様にすれば良いです。 +エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password`、`password_hash`、パスワードなしを含む状態を持つユーザー「エンティティ」の場合と同様です。 diff --git a/docs/ja/docs/tutorial/first-steps.md b/docs/ja/docs/tutorial/first-steps.md index 77f9cba43..ecad2f6ff 100644 --- a/docs/ja/docs/tutorial/first-steps.md +++ b/docs/ja/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 最初のステップ +# 最初のステップ { #first-steps } 最もシンプルなFastAPIファイルは以下のようになります: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} これを`main.py`にコピーします。 @@ -11,27 +11,43 @@
```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. ```
-/// note | 備考 - -`uvicorn main:app`は以下を示します: - -* `main`: `main.py`ファイル (Python "module")。 -* `app`: `main.py`内部で作られるobject(`app = FastAPI()`のように記述される)。 -* `--reload`: コードの変更時にサーバーを再起動させる。開発用。 - -/// - 出力には次のような行があります: ```hl_lines="4" @@ -40,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) この行はローカルマシンでアプリが提供されているURLを示しています。 -### チェック +### チェック { #check-it } ブラウザでhttp://127.0.0.1:8000を開きます。 @@ -50,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 対話的APIドキュメント +### 対話的APIドキュメント { #interactive-api-docs } 次に、http://127.0.0.1:8000/docsにアクセスします。 @@ -58,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![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にアクセスします。 @@ -66,31 +82,31 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } **FastAPI**は、APIを定義するための**OpenAPI**標準規格を使用して、すべてのAPIの「スキーマ」を生成します。 -#### 「スキーマ」 +#### 「スキーマ」 { #schema } 「スキーマ」は定義または説明です。実装コードではなく、単なる抽象的な説明です。 -#### API「スキーマ」 +#### API「スキーマ」 { #api-schema } ここでは、OpenAPIはAPIのスキーマ定義の方法を規定する仕様です。 このスキーマ定義はAPIパス、受け取り可能なパラメータなどが含まれます。 -#### データ「スキーマ」 +#### データ「スキーマ」 { #data-schema } 「スキーマ」という用語は、JSONコンテンツなどの一部のデータの形状を指す場合もあります。 そのような場合、スキーマはJSON属性とそれらが持つデータ型などを意味します。 -#### OpenAPIおよびJSONスキーマ +#### OpenAPIおよびJSONスキーマ { #openapi-and-json-schema } OpenAPIはAPIのためのAPIスキーマを定義します。そして、そのスキーマは**JSONデータスキーマ**の標準規格に準拠したJSONスキーマを利用するAPIによって送受されるデータの定義(または「スキーマ」)を含んでいます。 -#### `openapi.json`を確認 +#### `openapi.json`を確認 { #check-the-openapi-json } 素のOpenAPIスキーマがどのようなものか興味がある場合、FastAPIはすべてのAPIの説明を含むJSON(スキーマ)を自動的に生成します。 @@ -100,7 +116,7 @@ OpenAPIはAPIのためのAPIスキーマを定義します。そして、その ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -119,7 +135,7 @@ OpenAPIはAPIのためのAPIスキーマを定義します。そして、その ... ``` -#### OpenAPIの目的 +#### OpenAPIの目的 { #what-is-openapi-for } OpenAPIスキーマは、FastAPIに含まれている2つのインタラクティブなドキュメントシステムの動力源です。 @@ -127,11 +143,47 @@ OpenAPIスキーマは、FastAPIに含まれている2つのインタラクテ また、APIと通信するクライアント用のコードを自動的に生成するために使用することもできます。たとえば、フロントエンド、モバイル、またはIoTアプリケーションです。 -## ステップ毎の要約 +### アプリをデプロイ(任意) { #deploy-your-app-optional } -### Step 1: `FastAPI`をインポート +任意でFastAPIアプリをFastAPI Cloudにデプロイできます。まだなら、待機リストに登録してください。 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +すでに**FastAPI Cloud**アカウントがある場合(待機リストから招待済みの場合😉)、1コマンドでアプリケーションをデプロイできます。 + +デプロイする前に、ログインしていることを確認してください: + +
+ +```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 } + +### Step 1: `FastAPI`をインポート { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI`は、APIのすべての機能を提供するPythonクラスです。 @@ -143,44 +195,16 @@ OpenAPIスキーマは、FastAPIに含まれている2つのインタラクテ /// -### Step 2: `FastAPI`の「インスタンス」を生成 +### Step 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`クラスの「インスタンス」になります。 これが、すべてのAPIを作成するための主要なポイントになります。 -この`app`はコマンドで`uvicorn`が参照するものと同じです: +### Step 3: *path operation*を作成 { #step-3-create-a-path-operation } -
- -```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) -``` - -
- -### Step 3: *path operation*を作成 - -#### パス +#### パス { #path } ここでの「パス」とは、最初の`/`から始まるURLの最後の部分を指します。 @@ -204,7 +228,7 @@ https://example.com/items/foo APIを構築する際、「パス」は「関心事」と「リソース」を分離するための主要な方法です。 -#### Operation +#### Operation { #operation } ここでの「オペレーション」とは、HTTPの「メソッド」の1つを指します。 @@ -239,15 +263,16 @@ APIを構築するときは、通常、これらの特定のHTTPメソッドを 「**オペレーションズ**」とも呼ぶことにします。 -#### *パスオペレーションデコレータ*を定義 +#### *path operation デコレータ*を定義 { #define-a-path-operation-decorator } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} `@app.get("/")`は直下の関数が下記のリクエストの処理を担当することを**FastAPI**に伝えます: * パス `/` * get オペレーション -/// info | `@decorator` について +/// info | `@decorator` Info Pythonにおける`@something`シンタックスはデコレータと呼ばれます。 @@ -255,9 +280,9 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ 「デコレータ」は直下の関数を受け取り、それを使って何かを行います。 -私たちの場合、このデコレーターは直下の関数が**オペレーション** `get`を使用した**パス**` / `に対応することを**FastAPI** に通知します。 +私たちの場合、このデコレーターは直下の関数が**オペレーション** `get`を使用した**パス** `/`に対応することを**FastAPI** に通知します。 -これが「*パスオペレーションデコレータ*」です。 +これが「*path operation デコレータ*」です。 /// @@ -286,15 +311,15 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ /// -### Step 4: **パスオペレーション**を定義 +### Step 4: **path operation 関数**を定義 { #step-4-define-the-path-operation-function } -以下は「**パスオペレーション関数**」です: +以下は「**path operation 関数**」です: * **パス**: は`/`です。 * **オペレーション**: は`get`です。 * **関数**: 「デコレータ」の直下にある関数 (`@app.get("/")`の直下) です。 -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} これは、Pythonの関数です。 @@ -306,28 +331,49 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ `async def`の代わりに通常の関数として定義することもできます: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | 備考 -違いが分からない場合は、[Async: *"急いでいますか?"*](../async.md#_1){.internal-link target=_blank}を確認してください。 +違いが分からない場合は、[Async: *"急いでいますか?"*](../async.md#in-a-hurry){.internal-link target=_blank}を確認してください。 /// -### Step 5: コンテンツの返信 +### Step 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など)。 お気に入りのものを使ってみてください。すでにサポートされている可能性が高いです。 -## まとめ +### Step 6: デプロイする { #step-6-deploy-it } -* `FastAPI`をインポート -* `app`インスタンスを生成 -* **パスオペレーションデコレータ**を記述 (`@app.get("/")`) -* **パスオペレーション関数**を定義 (上記の`def root(): ...`のように) -* 開発サーバーを起動 (`uvicorn main:app --reload`) +**FastAPI Cloud**に1コマンドでアプリをデプロイします: `fastapi deploy`. 🎉 + +#### FastAPI Cloudについて { #about-fastapi-cloud } + +**FastAPI Cloud**は、**FastAPI**の作者とそのチームによって開発されています。 + +最小限の労力でAPIの**構築**、**デプロイ**、**アクセス**を行うプロセスを合理化します。 + +FastAPIでアプリを構築するのと同じ**開発体験**を、クラウドへの**デプロイ**にもたらします。 🎉 + +FastAPI Cloudは、*FastAPI and friends*のオープンソースプロジェクトに対する主要スポンサーであり、資金提供元です。 ✨ + +#### 他のクラウドプロバイダにデプロイする { #deploy-to-other-cloud-providers } + +FastAPIはオープンソースで、標準に基づいています。選択した任意のクラウドプロバイダにFastAPIアプリをデプロイできます。 + +クラウドプロバイダのガイドに従って、FastAPIアプリをデプロイしてください。 🤓 + +## まとめ { #recap } + +* `FastAPI`をインポートします。 +* `app`インスタンスを生成します。 +* `@app.get("/")`のようなデコレータを使用して、**path operation デコレータ**を記述します。 +* **path operation 関数**を定義します。例: `def root(): ...`。 +* `fastapi dev`コマンドで開発サーバーを起動します。 +* 任意で`fastapi deploy`を使ってアプリをデプロイします。 diff --git a/docs/ja/docs/tutorial/handling-errors.md b/docs/ja/docs/tutorial/handling-errors.md index 8578ca335..945fe0777 100644 --- a/docs/ja/docs/tutorial/handling-errors.md +++ b/docs/ja/docs/tutorial/handling-errors.md @@ -1,4 +1,4 @@ -# エラーハンドリング +# エラーハンドリング { #handling-errors } APIを使用しているクライアントにエラーを通知する必要がある状況はたくさんあります。 @@ -19,15 +19,15 @@ APIを使用しているクライアントにエラーを通知する必要が **"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に関連するデータを追加したものです。 @@ -39,9 +39,9 @@ Pythonの例外なので、`return`ではなく、`raise`です。 この例では、クライアントが存在しない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"`)をリクエストすると、HTTPステータスコードが200で、以下のJSONレスポンスが返されます: @@ -69,7 +69,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 /// -## カスタムヘッダーの追加 +## カスタムヘッダーの追加 { #add-custom-headers } 例えば、いくつかのタイプのセキュリティのために、HTTPエラーにカスタムヘッダを追加できると便利な状況がいくつかあります。 @@ -77,9 +77,9 @@ Pythonの例外なので、`return`ではなく、`raise`です。 しかし、高度なシナリオのために必要な場合には、カスタムヘッダーを追加することができます: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## カスタム例外ハンドラのインストール +## カスタム例外ハンドラのインストール { #install-custom-exception-handlers } カスタム例外ハンドラはStarletteと同じ例外ユーティリティを使用して追加することができます。 @@ -89,7 +89,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 カスタム例外ハンドラを`@app.exception_handler()`で追加することができます: -{* ../../docs_src/handling_errors/tutorial003.py hl[5,6,7,13,14,15,16,17,18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} ここで、`/unicorns/yolo`をリクエストすると、*path operation*は`UnicornException`を`raise`します。 @@ -109,7 +109,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 /// -## デフォルトの例外ハンドラのオーバーライド +## デフォルトの例外ハンドラのオーバーライド { #override-the-default-exception-handlers } **FastAPI** にはいくつかのデフォルトの例外ハンドラがあります。 @@ -117,7 +117,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 これらの例外ハンドラを独自のものでオーバーライドすることができます。 -### リクエスト検証の例外のオーバーライド +### リクエスト検証の例外のオーバーライド { #override-request-validation-exceptions } リクエストに無効なデータが含まれている場合、**FastAPI** は内部的に`RequestValidationError`を発生させます。 @@ -125,11 +125,11 @@ Pythonの例外なので、`return`ではなく、`raise`です。 これをオーバーライドするには`RequestValidationError`をインポートして`@app.exception_handler(RequestValidationError)`と一緒に使用して例外ハンドラをデコレートします。 -この例外ハンドラは`Requset`と例外を受け取ります。 +この例外ハンドラは`Request`と例外を受け取ります。 -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14,15,16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} -これで、`/items/foo`にアクセスすると、デフォルトのJSONエラーの代わりに以下が返されます: +これで、`/items/foo`にアクセスすると、以下のデフォルトのJSONエラーの代わりに: ```JSON { @@ -146,39 +146,20 @@ Pythonの例外なので、`return`ではなく、`raise`です。 } ``` -以下のようなテキスト版を取得します: +以下のテキスト版を取得します: ``` -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` - -/// warning | 注意 - -これらは今のあなたにとって重要でない場合は省略しても良い技術的な詳細です。 - -/// - -`RequestValidationError`はPydanticの`ValidationError`のサブクラスです。 - -**FastAPI** は`response_model`でPydanticモデルを使用していて、データにエラーがあった場合、ログにエラーが表示されるようにこれを使用しています。 - -しかし、クライアントやユーザーはそれを見ることはありません。その代わりに、クライアントはHTTPステータスコード`500`の「Internal Server Error」を受け取ります。 - -*レスポンス*やコードのどこか(クライアントの*リクエスト*ではなく)にPydanticの`ValidationError`がある場合、それは実際にはコードのバグなのでこのようにすべきです。 - -また、あなたがそれを修正している間は、セキュリティの脆弱性が露呈する場合があるため、クライアントやユーザーがエラーに関する内部情報にアクセスできないようにしてください。 - -### エラーハンドラ`HTTPException`のオーバーライド +### `HTTPException`エラーハンドラのオーバーライド { #override-the-httpexception-error-handler } 同様に、`HTTPException`ハンドラをオーバーライドすることもできます。 例えば、これらのエラーに対しては、JSONではなくプレーンテキストを返すようにすることができます: -{* ../../docs_src/handling_errors/tutorial004.py hl[3,4,9,10,11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | 技術詳細 @@ -188,13 +169,21 @@ path -> item_id /// -### `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] *} ここで、以下のような無効な項目を送信してみてください: @@ -207,7 +196,7 @@ path -> item_id 受信したボディを含むデータが無効であることを示すレスポンスが表示されます: -```JSON hl_lines="12 13 14 15" +```JSON hl_lines="12-15" { "detail": [ { @@ -226,36 +215,30 @@ path -> item_id } ``` -#### FastAPIの`HTTPException`とStarletteの`HTTPException` +#### FastAPIの`HTTPException`とStarletteの`HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI**は独自の`HTTPException`を持っています。 -また、 **FastAPI**のエラークラス`HTTPException`はStarletteのエラークラス`HTTPException`を継承しています。 +また、 **FastAPI**の`HTTPException`エラークラスはStarletteの`HTTPException`エラークラスを継承しています。 -唯一の違いは、**FastAPI** の`HTTPException`はレスポンスに含まれるヘッダを追加できることです。 - -これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。 +唯一の違いは、**FastAPI** の`HTTPException`は`detail`フィールドにJSONに変換可能な任意のデータを受け付けるのに対し、Starletteの`HTTPException`は文字列のみを受け付けることです。 そのため、コード内では通常通り **FastAPI** の`HTTPException`を発生させ続けることができます。 -しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`を登録しておく必要があります。 +しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`に対して登録しておく必要があります。 -これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部が`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** から同じデフォルトの例外ハンドラと一緒に例外を使用したい場合は、`fastapi.exception_handlers`からデフォルトの例外ハンドラをインポートして再利用できます: -デフォルトの例外ハンドラを`fastapi.exception_handlers`からインポートして再利用することができます: +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} -{* ../../docs_src/handling_errors/tutorial006.py hl[2,3,4,5,15,21] *} - -この例では、非常に表現力のあるメッセージでエラーを`print`しています。 - -しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。 +この例では、非常に表現力のあるメッセージでエラーを`print`しているだけですが、要点は理解できるはずです。例外を使用し、その後デフォルトの例外ハンドラを再利用できます。 diff --git a/docs/ja/docs/tutorial/header-params.md b/docs/ja/docs/tutorial/header-params.md index ac89afbdb..1916baf61 100644 --- a/docs/ja/docs/tutorial/header-params.md +++ b/docs/ja/docs/tutorial/header-params.md @@ -1,20 +1,20 @@ -# ヘッダーのパラメータ +# ヘッダーのパラメータ { #header-parameters } ヘッダーのパラメータは、`Query`や`Path`、`Cookie`のパラメータを定義するのと同じように定義できます。 -## `Header`をインポート +## `Header`をインポート { #import-header } まず、`Header`をインポートします: -{* ../../docs_src/header_params/tutorial001.py hl[3] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## `Header`のパラメータの宣言 +## `Header`のパラメータの宣言 { #declare-header-parameters } 次に、`Path`や`Query`、`Cookie`と同じ構造を用いてヘッダーのパラメータを宣言します。 -最初の値がデフォルト値で、追加の検証パラメータや注釈パラメータをすべて渡すことができます。 +デフォルト値に加えて、追加の検証パラメータや注釈パラメータをすべて定義できます: -{* ../../docs_src/header_params/tutorial001.py hl[9] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} /// note | 技術詳細 @@ -30,23 +30,23 @@ /// -## 自動変換 +## 自動変換 { #automatic-conversion } `Header`は`Path`や`Query`、`Cookie`が提供する機能に加え、少しだけ追加の機能を持っています。 -ほとんどの標準ヘッダーは、「マイナス記号」(`-`)としても知られる「ハイフン」で区切られています。 +ほとんどの標準ヘッダーは、「マイナス記号」(`-`)としても知られる「ハイフン」文字で区切られています。 しかし、`user-agent`のような変数はPythonでは無効です。 -そのため、デフォルトでは、`Header`はパラメータの文字をアンダースコア(`_`)からハイフン(`-`)に変換して、ヘッダーを抽出して文書化します。 +そのため、デフォルトでは、`Header`はパラメータ名の文字をアンダースコア(`_`)からハイフン(`-`)に変換して、ヘッダーを抽出して文書化します。 また、HTTPヘッダは大文字小文字を区別しないので、Pythonの標準スタイル(別名「スネークケース」)で宣言することができます。 そのため、`User_Agent`などのように最初の文字を大文字にする必要はなく、通常のPythonコードと同じように`user_agent`を使用することができます。 -もしなんらかの理由でアンダースコアからハイフンへの自動変換を無効にする必要がある場合は、`Header`の`convert_underscores`に`False`を設定してください: +もしなんらかの理由でアンダースコアからハイフンへの自動変換を無効にする必要がある場合は、`Header`のパラメータ`convert_underscores`を`False`に設定してください: -{* ../../docs_src/header_params/tutorial002.py hl[9] *} +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} /// warning | 注意 @@ -54,26 +54,26 @@ /// -## ヘッダーの重複 +## ヘッダーの重複 { #duplicate-headers } 受信したヘッダーが重複することがあります。つまり、同じヘッダーで複数の値を持つということです。 -これらの場合、リストの型宣言を使用して定義することができます。 +これらの場合、型宣言でリストを使用して定義することができます。 重複したヘッダーのすべての値をPythonの`list`として受け取ることができます。 例えば、複数回出現する可能性のある`X-Token`のヘッダを定義するには、以下のように書くことができます: -{* ../../docs_src/header_params/tutorial003.py hl[9] *} +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -もし、その*path operation*で通信する場合は、次のように2つのHTTPヘッダーを送信します: +その*path operation*と通信する際に、次のように2つのHTTPヘッダーを送信する場合: ``` X-Token: foo X-Token: bar ``` -このレスポンスは以下のようになります: +レスポンスは以下のようになります: ```JSON { @@ -84,8 +84,8 @@ X-Token: bar } ``` -## まとめ +## まとめ { #recap } -ヘッダーは`Header`で宣言し、`Query`や`Path`、`Cookie`と同じパターンを使用する。 +ヘッダーは`Header`で宣言し、`Query`や`Path`、`Cookie`と同じ共通パターンを使用します。 また、変数のアンダースコアを気にする必要はありません。**FastAPI** がそれらの変換をすべて取り持ってくれます。 diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index 87d3751fd..d298abc62 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -1,83 +1,95 @@ -# チュートリアル - ユーザーガイド +# チュートリアル - ユーザーガイド { #tutorial-user-guide } -このチュートリアルは**FastAPI**のほぼすべての機能の使い方を段階的に紹介します。 +このチュートリアルでは、**FastAPI**のほとんどの機能を使う方法を段階的に紹介します。 -各セクションは前のセクションを踏まえた内容になっています。しかし、トピックごとに分割されているので、特定のAPIの要求を満たすようなトピックに直接たどり着けるようになっています。 +各セクションは前のセクションを踏まえた内容になっています。しかし、トピックごとに分割されているので、特定の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. ```
-コードを記述またはコピーし、編集してローカルで実行することを**強くお勧めします**。 +コードを記述またはコピーし、編集してローカルで実行することを**強く推奨します**。 -また、エディターで使用することで、書く必要のあるコードの少なさ、すべての型チェック、自動補完などのFastAPIの利点を実感できます。 +エディターで使用することで、書く必要のあるコードの少なさ、すべての型チェック、自動補完など、FastAPIの利点を本当に実感できます。 --- -## 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 | 備考 -パーツ毎にインストールすることも可能です。 +`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 } -**高度なユーザーガイド**もあり、**チュートリアル - ユーザーガイド**の後で読むことができます。 +この **チュートリアル - ユーザーガイド** の後で、後から読める **高度なユーザーガイド** もあります。 -**高度なユーザーガイド**は**チュートリアル - ユーザーガイド**に基づいており、同じ概念を使用し、いくつかの追加機能を紹介しています。 +**高度なユーザーガイド** は本チュートリアルをベースにしており、同じ概念を使用し、いくつかの追加機能を教えます。 -ただし、最初に**チュートリアル - ユーザーガイド**(現在読んでいる内容)をお読みください。 +ただし、最初に **チュートリアル - ユーザーガイド**(今読んでいる内容)をお読みください。 -**チュートリアル-ユーザーガイド**だけで完全なアプリケーションを構築できるように設計されています。加えて、**高度なユーザーガイド**の中からニーズに応じたアイデアを使用して、様々な拡張が可能です。 +**チュートリアル - ユーザーガイド** だけで完全なアプリケーションを構築できるように設計されており、その後ニーズに応じて、**高度なユーザーガイド** の追加のアイデアのいくつかを使って、さまざまな方法で拡張できます。 diff --git a/docs/ja/docs/tutorial/metadata.md b/docs/ja/docs/tutorial/metadata.md index b93dedcb9..0ffb8f350 100644 --- a/docs/ja/docs/tutorial/metadata.md +++ b/docs/ja/docs/tutorial/metadata.md @@ -1,47 +1,66 @@ -# メタデータとドキュメントのURL +# メタデータとドキュメントのURL { #metadata-and-docs-urls } -**FastAPI** アプリケーションのいくつかのメタデータの設定をカスタマイズできます。 +**FastAPI** アプリケーションのいくつかのメタデータ設定をカスタマイズできます。 -## タイトル、説明文、バージョン +## APIのメタデータ { #metadata-for-api } -以下を設定できます: +OpenAPI仕様および自動APIドキュメントUIで使用される次のフィールドを設定できます: -* **タイトル**: OpenAPIおよび自動APIドキュメントUIでAPIのタイトル/名前として使用される。 -* **説明文**: OpenAPIおよび自動APIドキュメントUIでのAPIの説明文。 -* **バージョン**: APIのバージョン。例: `v2` または `2.5.0`。 - *たとえば、以前のバージョンのアプリケーションがあり、OpenAPIも使用している場合に便利です。 +| パラメータ | 型 | 説明 | +|------------|------|-------------| +| `title` | `str` | APIのタイトルです。 | +| `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` | APIの利用規約へのURLです。指定する場合、URLである必要があります。 | +| `contact` | `dict` | 公開されるAPIの連絡先情報です。複数のフィールドを含められます。
contact fields
ParameterTypeDescription
namestr連絡先の個人/組織を識別する名前です。
urlstr連絡先情報を指すURLです。URL形式である必要があります。
emailstr連絡先の個人/組織のメールアドレスです。メールアドレス形式である必要があります。
| +| `license_info` | `dict` | 公開されるAPIのライセンス情報です。複数のフィールドを含められます。
license_info fields
ParameterTypeDescription
namestr必須license_info が設定されている場合)。APIに使用されるライセンス名です。
identifierstrAPIの SPDX ライセンス式です。identifier フィールドは url フィールドと同時に指定できません。 OpenAPI 3.1.0、FastAPI 0.99.0 以降で利用できます。
urlstrAPIに使用されるライセンスへのURLです。URL形式である必要があります。
| -これらを設定するには、パラメータ `title`、`description`、`version` を使用します: +以下のように設定できます: -{* ../../docs_src/metadata/tutorial001.py hl[4:6] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -この設定では、自動APIドキュメントは以下の様になります: +/// tip | 豆知識 + +`description` フィールドにはMarkdownを書けて、出力ではレンダリングされます。 + +/// + +この設定では、自動APIドキュメントは以下のようになります: -## タグのためのメタデータ +## ライセンス識別子 { #license-identifier } -さらに、パラメータ `openapi_tags` を使うと、path operations をグループ分けするための複数のタグに関するメタデータを追加できます。 +OpenAPI 3.1.0 および FastAPI 0.99.0 以降では、`license_info` を `url` の代わりに `identifier` で設定することもできます。 -それぞれのタグ毎にひとつの辞書を含むリストをとります。 +例: -それぞれの辞書は以下をもつことができます: +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -* `name` (**必須**): *path operations* および `APIRouter` の `tags` パラメーターで使用するのと同じタグ名である `str`。 -* `description`: タグの簡単な説明文である `str`。 Markdownで記述でき、ドキュメントUIに表示されます。 -* `externalDocs`: 外部ドキュメントを説明するための `dict`: - * `description`: 外部ドキュメントの簡単な説明文である `str`。 - * `url` (**必須**): 外部ドキュメントのURLである `str`。 +## タグのメタデータ { #metadata-for-tags } -### タグのためのメタデータの作成 +パラメータ `openapi_tags` を使うと、path operation をグループ分けするために使用する各タグに追加のメタデータを追加できます。 -`users` と `items` のタグを使った例でメタデータの追加を試してみましょう。 +それぞれのタグごとに1つの辞書を含むリストを取ります。 -タグのためのメタデータを作成し、それを `openapi_tags` パラメータに渡します。 +それぞれの辞書は以下を含められます: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +* `name` (**必須**): *path operation* および `APIRouter` の `tags` パラメータで使用するのと同じタグ名の `str`。 +* `description`: タグの短い説明の `str`。Markdownを含められ、ドキュメントUIに表示されます。 +* `externalDocs`: 外部ドキュメントを説明する `dict`。以下を含みます: + * `description`: 外部ドキュメントの短い説明の `str`。 + * `url` (**必須**): 外部ドキュメントのURLの `str`。 -説明文 (description) の中で Markdown を使用できることに注意してください。たとえば、「login」は太字 (**login**) で表示され、「fancy」は斜体 (_fancy_) で表示されます。 +### タグのメタデータの作成 { #create-metadata-for-tags } + +`users` と `items` のタグを使った例で試してみましょう。 + +タグのメタデータを作成し、それを `openapi_tags` パラメータに渡します: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} + +説明の中でMarkdownを使用できることに注意してください。たとえば「login」は太字 (**login**) で表示され、「fancy」は斜体 (_fancy_) で表示されます。 /// tip | 豆知識 @@ -49,31 +68,31 @@ /// -### 自作タグの使用 +### タグの使用 { #use-your-tags } -`tags` パラメーターを使用して、それぞれの *path operations* (および `APIRouter`) を異なるタグに割り当てます: +*path operation*(および `APIRouter`)の `tags` パラメータを使用して、それらを異なるタグに割り当てます: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | 情報 -タグのより詳しい説明を知りたい場合は [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank} を参照して下さい。 +タグの詳細は [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank} を参照してください。 /// -### ドキュメントの確認 +### ドキュメントの確認 { #check-the-docs } -ここで、ドキュメントを確認すると、追加したメタデータがすべて表示されます: +ここでドキュメントを確認すると、追加したメタデータがすべて表示されます: -### タグの順番 +### タグの順番 { #order-of-tags } タグのメタデータ辞書の順序は、ドキュメントUIに表示される順序の定義にもなります。 -たとえば、`users` はアルファベット順では `items` の後に続きます。しかし、リストの最初に `users` のメタデータ辞書を追加したため、ドキュメントUIでは `users` が先に表示されます。 +たとえば、`users` はアルファベット順では `items` の後に続きますが、リストの最初の辞書としてメタデータを追加したため、それより前に表示されます。 -## OpenAPI URL +## OpenAPI URL { #openapi-url } デフォルトでは、OpenAPIスキーマは `/openapi.json` で提供されます。 @@ -81,21 +100,21 @@ たとえば、`/api/v1/openapi.json` で提供されるように設定するには: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} OpenAPIスキーマを完全に無効にする場合は、`openapi_url=None` を設定できます。これにより、それを使用するドキュメントUIも無効になります。 -## ドキュメントのURL +## ドキュメントのURL { #docs-urls } -以下の2つのドキュメントUIを構築できます: +含まれている2つのドキュメントUIを設定できます: * **Swagger UI**: `/docs` で提供されます。 - * URL はパラメータ `docs_url` で設定できます。 - * `docs_url=None` を設定することで無効にできます。 -* ReDoc: `/redoc` で提供されます。 - * URL はパラメータ `redoc_url` で設定できます。 - * `redoc_url=None` を設定することで無効にできます。 + * URL はパラメータ `docs_url` で設定できます。 + * `docs_url=None` を設定することで無効にできます。 +* **ReDoc**: `/redoc` で提供されます。 + * URL はパラメータ `redoc_url` で設定できます。 + * `redoc_url=None` を設定することで無効にできます。 たとえば、`/documentation` でSwagger UIが提供されるように設定し、ReDocを無効にするには: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/ja/docs/tutorial/middleware.md b/docs/ja/docs/tutorial/middleware.md index 539ec5b8c..12fb57a64 100644 --- a/docs/ja/docs/tutorial/middleware.md +++ b/docs/ja/docs/tutorial/middleware.md @@ -1,4 +1,4 @@ -# ミドルウェア +# ミドルウェア { #middleware } **FastAPI** アプリケーションにミドルウェアを追加できます。 @@ -15,11 +15,11 @@ `yield` を使った依存関係をもつ場合は、終了コードはミドルウェアの *後に* 実行されます。 -バックグラウンドタスク (後述) がある場合は、それらは全てのミドルウェアの *後に* 実行されます。 +バックグラウンドタスク ([バックグラウンドタスク](background-tasks.md){.internal-link target=_blank} セクションで説明します。後で確認できます) がある場合は、それらは全てのミドルウェアの *後に* 実行されます。 /// -## ミドルウェアの作成 +## ミドルウェアの作成 { #create-a-middleware } ミドルウェアを作成するには、関数の上部でデコレータ `@app.middleware("http")` を使用します。 @@ -31,13 +31,13 @@ * 次に、対応する*path operation*によって生成された `response` を返します。 * その後、`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-` プレフィックスを使用して追加できる点に注意してください。 -ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) +ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})。 /// @@ -49,7 +49,7 @@ /// -### `response` の前後 +### `response` の前後 { #before-and-after-the-response } *path operation* が `request` を受け取る前に、 `request` とともに実行されるコードを追加できます。 @@ -57,9 +57,38 @@ 例えば、リクエストの処理とレスポンスの生成にかかった秒数を含むカスタムヘッダー `X-Process-Time` を追加できます: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} -## その他のミドルウェア +/// tip | 豆知識 + +ここでは、これらのユースケースに対してより正確になり得るため、`time.time()` の代わりに `time.perf_counter()` を使用しています。 🤓 + +/// + +## 複数ミドルウェアの実行順序 { #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}を参照してください。 diff --git a/docs/ja/docs/tutorial/path-operation-configuration.md b/docs/ja/docs/tutorial/path-operation-configuration.md index 0cc38cb25..eb6b6b11a 100644 --- a/docs/ja/docs/tutorial/path-operation-configuration.md +++ b/docs/ja/docs/tutorial/path-operation-configuration.md @@ -1,14 +1,14 @@ -# Path Operationの設定 +# Path Operationの設定 { #path-operation-configuration } *path operationデコレータ*を設定するためのパラメータがいくつかあります。 /// warning | 注意 -これらのパラメータは*path operation関数*ではなく、*path operationデコレータ*に直接渡されることに注意してください。 +これらのパラメータは*path operationデコレータ*に直接渡され、*path operation関数*に渡されないことに注意してください。 /// -## レスポンスステータスコード +## レスポンスステータスコード { #response-status-code } *path operation*のレスポンスで使用する(HTTP)`status_code`を定義することができます。 @@ -16,55 +16,65 @@ しかし、それぞれの番号コードが何のためのものか覚えていない場合は、`status`のショートカット定数を使用することができます: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} そのステータスコードはレスポンスで使用され、OpenAPIスキーマに追加されます。 /// note | 技術詳細 -また、`from starlette import status`を使用することもできます。 +`from starlette import status`を使用することもできます。 **FastAPI** は開発者の利便性を考慮して、`fastapi.status`と同じ`starlette.status`を提供しています。しかし、これはStarletteから直接提供されています。 /// -## タグ +## タグ { #tags } `tags`パラメータを`str`の`list`(通常は1つの`str`)と一緒に渡すと、*path operation*にタグを追加できます: -{* ../../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 } + +大きなアプリケーションの場合、**複数のタグ**が蓄積されていき、関連する*path operations*に対して常に**同じタグ**を使っていることを確認したくなるかもしれません。 + +このような場合、タグを`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[17:18] *} -## docstringを用いた説明 +## docstringを用いた説明 { #description-from-docstring } -説明文は長くて複数行におよぶ傾向があるので、関数docstring内に*path operation*の説明文を宣言できます。すると、**FastAPI** は説明文を読み込んでくれます。 +説明文は長くて複数行におよぶ傾向があるので、関数docstring内に*path operation*の説明文を宣言できます。すると、**FastAPI** は説明文を読み込んでくれます。 docstringにMarkdownを記述すれば、正しく解釈されて表示されます。(docstringのインデントを考慮して) -{* ../../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[18] *} /// info | 情報 -`respnse_description`は具体的にレスポンスを参照し、`description`は*path operation*全般を参照していることに注意してください。 +`response_description`は具体的にレスポンスを参照し、`description`は*path operation*全般を参照していることに注意してください。 /// @@ -76,22 +86,22 @@ OpenAPIは*path operation*ごとにレスポンスの説明を必要としてい /// - + -## 非推奨の*path operation* +## *path operation*を非推奨にする { #deprecate-a-path-operation } -*path operation*をdeprecatedとしてマークする必要があるが、それを削除しない場合は、`deprecated`パラメータを渡します: +*path operation*をdeprecatedとしてマークする必要があるが、それを削除しない場合は、`deprecated`パラメータを渡します: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} 対話的ドキュメントでは非推奨と明記されます: - + *path operations*が非推奨である場合とそうでない場合でどのように見えるかを確認してください: - + -## まとめ +## まとめ { #recap } *path operationデコレータ*にパラメータを渡すことで、*path operations*のメタデータを簡単に設定・追加することができます。 diff --git a/docs/ja/docs/tutorial/path-params-numeric-validations.md b/docs/ja/docs/tutorial/path-params-numeric-validations.md index a1810ae37..6a9ecc4e7 100644 --- a/docs/ja/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ja/docs/tutorial/path-params-numeric-validations.md @@ -1,40 +1,52 @@ -# パスパラメータと数値の検証 +# パスパラメータと数値の検証 { #path-parameters-and-numeric-validations } クエリパラメータに対して`Query`でより多くのバリデーションとメタデータを宣言できるのと同じように、パスパラメータに対しても`Path`で同じ種類のバリデーションとメタデータを宣言することができます。 -## Pathのインポート +## `Path`のインポート { #import-path } -まず初めに、`fastapi`から`Path`をインポートします: +まず初めに、`fastapi`から`Path`をインポートし、`Annotated`もインポートします: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[1] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## メタデータの宣言 +/// info | 情報 + +FastAPI はバージョン 0.95.0 で`Annotated`のサポートを追加し(そして推奨し始めました)。 + +古いバージョンの場合、`Annotated`を使おうとするとエラーになります。 + +`Annotated`を使用する前に、FastAPI のバージョンを少なくとも 0.95.1 まで[アップグレードしてください](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}。 + +/// + +## メタデータの宣言 { #declare-metadata } パラメータは`Query`と同じものを宣言することができます。 例えば、パスパラメータ`item_id`に対して`title`のメタデータを宣言するには以下のようにします: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} /// note | 備考 -パスの一部でなければならないので、パスパラメータは常に必須です。 - -そのため、`...`を使用して必須と示す必要があります。 - -それでも、`None`で宣言しても、デフォルト値を設定しても、何の影響もなく、常に必要とされていることに変わりはありません。 +パスパラメータはパスの一部でなければならないので、常に必須です。`None`で宣言したりデフォルト値を設定したりしても何も影響せず、常に必須のままです。 /// -## 必要に応じてパラメータを並び替える +## 必要に応じてパラメータを並び替える { #order-the-parameters-as-you-need } + +/// tip | 豆知識 + +`Annotated`を使う場合、これはおそらくそれほど重要でも必要でもありません。 + +/// クエリパラメータ`q`を必須の`str`として宣言したいとしましょう。 また、このパラメータには何も宣言する必要がないので、`Query`を使う必要はありません。 -しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。 +しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。そして何らかの理由で`Annotated`を使いたくないとします。 -Pythonは「デフォルト」を持たない値の前に「デフォルト」を持つ値を置くことができません。 +Pythonは「デフォルト」を持つ値を「デフォルト」を持たない値の前に置くとエラーになります。 しかし、それらを並び替えることができ、デフォルト値を持たない値(クエリパラメータ`q`)を最初に持つことができます。 @@ -42,63 +54,88 @@ Pythonは「デフォルト」を持たない値の前に「デフォルト」 そのため、以下のように関数を宣言することができます: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -## 必要に応じてパラメータを並び替えるトリック +ただし、`Annotated`を使う場合はこの問題は起きないことを覚えておいてください。`Query()`や`Path()`に関数パラメータのデフォルト値を使わないためです。 -クエリパラメータ`q`を`Query`やデフォルト値なしで宣言し、パスパラメータ`item_id`を`Path`を用いて宣言し、それらを別の順番に並びたい場合、Pythonには少し特殊な構文が用意されています。 +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## 必要に応じてパラメータを並び替えるトリック { #order-the-parameters-as-you-need-tricks } + +/// tip | 豆知識 + +`Annotated`を使う場合、これはおそらくそれほど重要でも必要でもありません。 + +/// + +これは**小さなトリック**で、便利な場合がありますが、頻繁に必要になることはありません。 + +次のことをしたい場合: + +* `q`クエリパラメータを`Query`もデフォルト値もなしで宣言する +* パスパラメータ`item_id`を`Path`を使って宣言する +* それらを別の順番にする +* `Annotated`を使わない + +...Pythonにはそのための少し特殊な構文があります。 関数の最初のパラメータとして`*`を渡します。 Pythonはその`*`で何かをすることはありませんが、それ以降のすべてのパラメータがキーワード引数(キーと値のペア)として呼ばれるべきものであると知っているでしょう。それはkwargsとしても知られています。たとえデフォルト値がなくても。 -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -## 数値の検証: 以上 +### `Annotated`のほうがよい { #better-with-annotated } -`Query`と`Path`(、そして後述する他のもの)を用いて、文字列の制約を宣言することができますが、数値の制約も同様に宣言できます。 +`Annotated`を使う場合は、関数パラメータのデフォルト値を使わないため、この問題は起きず、おそらく`*`を使う必要もありません。 -ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなれけばなりません。 +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +## 数値の検証: 以上 { #number-validations-greater-than-or-equal } -## 数値の検証: より大きいと小なりイコール +`Query`と`Path`(、そして後述する他のもの)を用いて、数値の制約を宣言できます。 + +ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなければなりません。 + +{* ../../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`の値に対しても有効です。 -ここで重要になってくるのはgtだけでなくgeも宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 +ここで重要になってくるのはgtだけでなくgeも宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 したがって、`0.5`は有効な値ですが、`0.0`や`0`はそうではありません。 -これはltも同じです。 +これは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`クラスのサブクラスです。 -そして、それらすべては、これまで見てきた追加のバリデーションとメタデータと同じパラメータを共有しています。 +それらはすべて、これまで見てきた追加のバリデーションとメタデータの同じパラメータを共有しています。 /// diff --git a/docs/ja/docs/tutorial/path-params.md b/docs/ja/docs/tutorial/path-params.md index 1893ec12f..96a1fe9d1 100644 --- a/docs/ja/docs/tutorial/path-params.md +++ b/docs/ja/docs/tutorial/path-params.md @@ -1,22 +1,22 @@ -# パスパラメータ +# パスパラメータ { #path-parameters } Pythonのformat文字列と同様のシンタックスで「パスパラメータ」や「パス変数」を宣言できます: -{* ../../docs_src/path_params/tutorial001.py hl[6,7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} パスパラメータ `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-parameters-with-types } 標準のPythonの型アノテーションを使用して、関数内のパスパラメータの型を宣言できます: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} ここでは、 `item_id` は `int` として宣言されています。 @@ -26,7 +26,7 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -## データ変換 +## データ変換 { #data-conversion } この例を実行し、ブラウザで http://127.0.0.1:8000/items/3 を開くと、次のレスポンスが表示されます: @@ -38,68 +38,69 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー 関数が受け取った(および返した)値は、文字列の `"3"` ではなく、Pythonの `int` としての `3` であることに注意してください。 -したがって、型宣言を使用すると、**FastAPI**は自動リクエスト "解析" を行います。 +したがって、その型宣言を使うと、**FastAPI**は自動リクエスト "解析" を行います。 /// -## データバリデーション +## データバリデーション { #data-validation } しかしブラウザで 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"` だからです。 -http://127.0.0.1:8000/items/4.2 で見られるように、intのかわりに `float` が与えられた場合にも同様なエラーが表示されます。 +http://127.0.0.1:8000/items/4.2 で見られるように、`int` のかわりに `float` が与えられた場合にも同様なエラーが表示されます。 /// check | 確認 -したがって、Pythonの型宣言を使用することで、**FastAPI**はデータのバリデーションを行います。 +したがって、同じPythonの型宣言を使用することで、**FastAPI**はデータのバリデーションを行います。 -表示されたエラーには問題のある箇所が明確に指摘されていることに注意してください。 +表示されたエラーには、バリデーションが通らなかった箇所が明確に示されていることに注意してください。 -これは、APIに関連するコードの開発およびデバッグに非常に役立ちます。 +これは、APIとやり取りするコードを開発・デバッグする際に非常に役立ちます。 /// -## ドキュメント +## ドキュメント { #documentation } -そしてブラウザで http://127.0.0.1:8000/docs を開くと、以下の様な自動的に生成された対話的なドキュメントが表示されます。 +そしてブラウザで http://127.0.0.1:8000/docs を開くと、以下の様な自動的に生成された対話的なAPIドキュメントが表示されます。 /// check | 確認 -繰り返しになりますが、Python型宣言を使用するだけで、**FastAPI**は対話的なAPIドキュメントを自動的に生成します(Swagger UIを統合)。 +繰り返しになりますが、同じPython型宣言を使用するだけで、**FastAPI**は対話的なドキュメントを自動的に生成します(Swagger UIを統合)。 パスパラメータが整数として宣言されていることに注意してください。 /// -## 標準であることのメリット、ドキュメンテーションの代替物 +## 標準ベースのメリット、ドキュメンテーションの代替物 { #standards-based-benefits-alternative-documentation } -また、生成されたスキーマが OpenAPI 標準に従っているので、互換性のあるツールが多数あります。 +また、生成されたスキーマが OpenAPI 標準に従っているので、互換性のあるツールが多数あります。 このため、**FastAPI**自体が代替のAPIドキュメントを提供します(ReDocを使用)。これは、 http://127.0.0.1:8000/redoc にアクセスすると確認できます。 -同様に、互換性のあるツールが多数あります(多くの言語用のコード生成ツールを含む)。 +同様に、互換性のあるツールが多数あります。多くの言語用のコード生成ツールを含みます。 -## Pydantic +## Pydantic { #pydantic } すべてのデータバリデーションは Pydantic によって内部で実行されるため、Pydanticの全てのメリットが得られます。そして、安心して利用することができます。 @@ -107,7 +108,7 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー これらのいくつかについては、チュートリアルの次の章で説明します。 -## 順序の問題 +## 順序の問題 { #order-matters } *path operations* を作成する際、固定パスをもつ状況があり得ます。 @@ -117,29 +118,29 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー *path operations* は順に評価されるので、 `/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/{users_id}` は `/users/me` としてもマッチします。値が「"me"」であるパラメータ `user_id` を受け取ると「考え」ます。 +それ以外の場合、 `/users/{user_id}` は `/users/me` としてもマッチします。値が `"me"` であるパラメータ `user_id` を受け取ると「考え」ます。 -## 定義済みの値 +同様に、path operation を再定義することはできません: -*パスパラメータ*を受け取る *path operation* をもち、有効な*パスパラメータ*の値を事前に定義したい場合は、標準のPython `Enum` を利用できます。 +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### `Enum` クラスの作成 +パスは最初にマッチしたものが常に使われるため、最初のものが常に使用されます。 + +## 定義済みの値 { #predefined-values } + +*パスパラメータ*を受け取る *path operation* をもち、有効な*パスパラメータ*の値を事前に定義したい場合は、標準のPython `Enum` を利用できます。 + +### `Enum` クラスの作成 { #create-an-enum-class } `Enum` をインポートし、 `str` と `Enum` を継承したサブクラスを作成します。 -`str` を継承することで、APIドキュメントは値が `文字列` でなければいけないことを知り、正確にレンダリングできるようになります。 +`str` を継承することで、APIドキュメントは値が `string` 型でなければいけないことを知り、正確にレンダリングできるようになります。 そして、固定値のクラス属性を作ります。すると、その値が使用可能な値となります: -{* ../../docs_src/path_params/tutorial005.py hl[1,6,7,8,9] *} - -/// info | 情報 - -Enumerations (もしくは、enums)はPython 3.4以降で利用できます。 - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | 豆知識 @@ -147,33 +148,33 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -### *パスパラメータ*の宣言 +### *パスパラメータ*の宣言 { #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 } これは、作成した列挙型 `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,13 +182,13 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -#### *列挙型メンバ*の返却 +#### *列挙型メンバ*の返却 { #return-enumeration-members } -*path operation* から*列挙型メンバ*を返すことができます。JSONボディ(`dict` など)でネストすることもできます。 +*path operation* から*列挙型メンバ*を返すことができます。JSONボディ(例: `dict`)でネストすることもできます。 それらはクライアントに返される前に適切な値 (この場合は文字列) に変換されます。 -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} クライアントは以下の様なJSONレスポンスを得ます: @@ -198,23 +199,23 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー } ``` -## パスを含んだパスパラメータ +## パスを含んだパスパラメータ { #path-parameters-containing-paths } パス `/files/{file_path}` となる *path operation* を持っているとしましょう。 ただし、 `home/johndoe/myfile.txt` のような*パス*を含んだ `file_path` が必要です。 -したがって、URLは `/files/home/johndoe/myfile.txt` の様になります。 +したがって、そのファイルのURLは `/files/home/johndoe/myfile.txt` の様になります。 -### OpenAPIサポート +### OpenAPIサポート { #openapi-support } OpenAPIはテストや定義が困難なシナリオにつながる可能性があるため、内部に*パス*を含む*パスパラメータ*の宣言をサポートしていません。 それにも関わらず、Starletteの内部ツールのひとつを使用することで、**FastAPI**はそれが実現できます。 -そして、パラメータがパスを含むべきであることを明示的にドキュメントに追加することなく、機能します。 +そして、パラメータがパスを含むべきであることを示すドキュメントを追加しなくても、ドキュメントは動作します。 -### パス変換 +### パスコンバーター { #path-convertor } Starletteのオプションを直接使用することで、以下のURLの様な*パス*を含んだ、*パスパラメータ*の宣言ができます: @@ -226,17 +227,17 @@ Starletteのオプションを直接使用することで、以下のURLの様 したがって、以下の様に使用できます: -{* ../../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` の間にダブルスラッシュ (`//`) のある、 `/files//home/johndoe/myfile.txt` になります。 /// -## まとめ +## まとめ { #recap } 簡潔で、本質的で、標準的なPythonの型宣言を使用することで、**FastAPI**は以下を行います: diff --git a/docs/ja/docs/tutorial/query-param-models.md b/docs/ja/docs/tutorial/query-param-models.md index 053d0740b..d892a57d2 100644 --- a/docs/ja/docs/tutorial/query-param-models.md +++ b/docs/ja/docs/tutorial/query-param-models.md @@ -1,8 +1,8 @@ -# クエリパラメータモデル +# クエリパラメータモデル { #query-parameter-models } もし関連する**複数のクエリパラメータ**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。 -こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのクエリパラメータに対して一度に宣言できます。😎 +こうすることで、**複数の場所**で**そのモデルを再利用**でき、バリデーションやメタデータを、すべてのパラメータに対して一度に宣言できます。😎 /// note | 備考 @@ -10,15 +10,15 @@ /// -## クエリパラメータに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 } 対話的APIドキュメント `/docs` でクエリパラメータを確認できます: @@ -26,11 +26,11 @@ -## 余分なクエリパラメータを禁止する +## 余分なクエリパラメータを禁止する { #forbid-extra-query-parameters } -特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクエリパラメータを**制限**する必要があるかもしれません。 +特定の(あまり一般的ではないかもしれない)ユースケースで、受け取りたいクエリパラメータを**制限**したい場合があります。 -Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 +Pydanticのモデル設定を使って、あらゆる `extra` フィールドを `forbid` にできます。 {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} @@ -42,7 +42,7 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールド https://example.com/items/?limit=10&tool=plumbus ``` -クエリパラメータ `tool` が許可されていないことを通知する**エラー**レスポンスが返されます。 +クエリパラメータ `tool` が許可されていないことを伝える**エラー**レスポンスが返されます。 ```json { @@ -57,7 +57,7 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## まとめ +## まとめ { #summary } **FastAPI**では、**クエリパラメータ**を宣言するために、**Pydanticモデル**を使用できます。😎 diff --git a/docs/ja/docs/tutorial/query-params-str-validations.md b/docs/ja/docs/tutorial/query-params-str-validations.md index 22b89e452..e230ef29a 100644 --- a/docs/ja/docs/tutorial/query-params-str-validations.md +++ b/docs/ja/docs/tutorial/query-params-str-validations.md @@ -1,120 +1,228 @@ -# クエリパラメータと文字列の検証 +# クエリパラメータと文字列の検証 { #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]` 型で、`None` を許容する `str` 型を意味しており、デフォルトは `None` です。そのため、FastAPIはそれが必須ではないと理解します。 +クエリパラメータ `q` は `str | None` 型で、`str` 型ですが `None` にもなり得ることを意味し、実際にデフォルト値は `None` なので、FastAPIはそれが必須ではないと理解します。 /// note | 備考 -FastAPIは、 `q` はデフォルト値が `=None` であるため、必須ではないと理解します。 +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] *} -パラメータのデフォルト値として使用し、パラメータ`max_length`を50に設定します: +/// info | 情報 -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI はバージョン 0.95.0 で `Annotated` のサポートを追加し(推奨し始め)ました。 -デフォルト値`None`を`Query(default=None)`に置き換える必要があるので、`Query`の最初の引数はデフォルト値を定義するのと同じです。 +古いバージョンの場合、`Annotated` を使おうとするとエラーになります。 + +`Annotated` を使う前に、FastAPI のバージョンを少なくとも 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 スキーマの *path operation* にパラメータを **ドキュメント化** する(その結果、**自動ドキュメント UI** に表示されます) + +## 代替(古い方法): デフォルト値としての `Query` { #alternative-old-query-as-the-default-value } + +FastAPI の以前のバージョン(0.95.0 より前)では、パラメータのデフォルト値として `Query` を使う必要があり、`Annotated` の中に入れるのではありませんでした。これを使ったコードを見かける可能性が高いので、説明します。 + +/// 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(default=None) +q: str | None = Query(default=None) ``` -...を以下と同じようにパラメータをオプションにします: +...はデフォルト値 `None` を持つオプショナルなパラメータになり、以下と同じです: + ```Python -q: Optional[str] = None +q: str | None = None ``` -しかし、これはクエリパラメータとして明示的に宣言しています。 - -/// info | 情報 - -FastAPIは以下の部分を気にすることを覚えておいてください: - -```Python -= None -``` - -もしくは: - -```Python -= Query(default=None) -``` - -そして、 `None` を利用することでクエリパラメータが必須ではないと検知します。 - -`Optional` の部分は、エディターによるより良いサポートを可能にします。 - -/// +ただし `Query` のバージョンでは、クエリパラメータであることを明示的に宣言しています。 そして、さらに多くのパラメータを`Query`に渡すことができます。この場合、文字列に適用される、`max_length`パラメータを指定します。 ```Python -q: Union[str, None] = Query(default=None, max_length=50) +q: str | None = Query(default=None, max_length=50) ``` これにより、データを検証し、データが有効でない場合は明確なエラーを表示し、OpenAPIスキーマの *path operation* にパラメータを記載します。 -## バリデーションをさらに追加する +### デフォルト値としての `Query` または `Annotated` 内の `Query` { #query-as-the-default-value-or-in-annotated } + +`Annotated` の中で `Query` を使う場合、`Query` の `default` パラメータは使えないことに注意してください。 + +その代わりに、関数パラメータの実際のデフォルト値を使います。そうしないと整合性が取れなくなります。 + +例えば、これは許可されません: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...なぜなら、デフォルト値が `"rick"` なのか `"morty"` なのかが不明確だからです。 + +そのため、(できれば)次のようにします: + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...または、古いコードベースでは次のようなものが見つかるでしょう: + +```Python +q: str = Query(default="rick") +``` + +### `Annotated` の利点 { #advantages-of-annotated } + +関数パラメータのデフォルト値スタイルではなく、**`Annotated` を使うことが推奨** されます。複数の理由で **より良い** からです。 🤓 + +**関数パラメータ** の **デフォルト値** は **実際のデフォルト値** であり、Python 全般としてより直感的です。 😌 + +FastAPI なしで同じ関数を **別の場所** から **呼び出しても**、**期待どおりに動作** します。**必須** パラメータ(デフォルト値がない)があれば、**エディター** がエラーで知らせてくれますし、**Python** も必須パラメータを渡さずに実行すると文句を言います。 + +`Annotated` を使わずに **(古い)デフォルト値スタイル** を使う場合、FastAPI なしでその関数を **別の場所** で呼び出すとき、正しく動かすために関数へ引数を渡すことを **覚えておく** 必要があります。そうしないと値が期待と異なります(例えば `str` の代わりに `QueryInfo` か、それに類するものになります)。また、エディターも警告せず、Python もその関数の実行で文句を言いません。内部の処理がエラーになるときに初めて問題が出ます。 + +`Annotated` は複数のメタデータアノテーションを持てるので、Typer のような別ツールと同じ関数を使うこともできます。 🚀 + +## バリデーションをさらに追加する { #add-more-validations } パラメータ`min_length`も追加することができます: -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## 正規表現の追加 +## 正規表現の追加 { #add-regular-expressions } -パラメータが一致するべき正規表現を定義することができます: +パラメータが一致するべき 正規表現 `pattern` を定義することができます: -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *} +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} -この特定の正規表現は受け取ったパラメータの値をチェックします: +この特定の正規表現パターンは受け取ったパラメータの値をチェックします: * `^`: は、これ以降の文字で始まり、これより以前には文字はありません。 * `fixedquery`: は、正確な`fixedquery`を持っています. * `$`: で終わる場合、`fixedquery`以降には文字はありません. -もしこれらすべての **正規表現**のアイデアについて迷っていても、心配しないでください。多くの人にとって難しい話題です。正規表現を必要としなくても、まだ、多くのことができます。 +もしこれらすべての **「正規表現」** のアイデアについて迷っていても、心配しないでください。多くの人にとって難しい話題です。正規表現を必要としなくても、まだ、多くのことができます。 -しかし、あなたがそれらを必要とし、学ぶときにはすでに、 **FastAPI**で直接それらを使用することができます。 +これで、必要になったときにはいつでも **FastAPI** で使えることが分かりました。 -## デフォルト値 +## デフォルト値 { #default-values } -第一引数に`None`を渡して、デフォルト値として使用するのと同じように、他の値を渡すこともできます。 +もちろん、`None` 以外のデフォルト値も使えます。 -クエリパラメータ`q`の`min_length`を`3`とし、デフォルト値を`fixedquery`としてみましょう: +クエリパラメータ `q` の `min_length` を `3` とし、デフォルト値を `"fixedquery"` として宣言したいとします: -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} /// note | 備考 -デフォルト値を指定すると、パラメータは任意になります。 +`None` を含む任意の型のデフォルト値があると、パラメータはオプショナル(必須ではない)になります。 /// -## 必須にする +## 必須パラメータ { #required-parameters } -これ以上、バリデーションやメタデータを宣言する必要のない場合は、デフォルト値を指定しないだけでクエリパラメータ`q`を必須にすることができます。以下のように: +これ以上、バリデーションやメタデータを宣言する必要がない場合は、デフォルト値を宣言しないだけでクエリパラメータ `q` を必須にできます。以下のように: ```Python q: str @@ -123,42 +231,42 @@ q: str 以下の代わりに: ```Python -q: Union[str, None] = None +q: str | None = None ``` -現在は以下の例のように`Query`で宣言しています: +しかし今は、例えば次のように `Query` で宣言しています: ```Python -q: Union[str, None] = Query(default=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 } -これまで`...`を見たことがない方へ: これは特殊な単一値です。Pythonの一部であり、"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`を宣言するには以下のように書きます: -{* ../../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`)を*path operation関数*内で*関数パラメータ*`q`としてPythonの`list`を受け取ることになります。 +*path operation function* 内の *function parameter* `q` で、複数の `q` *query parameters'* 値(`foo` と `bar`)を Python の `list` として受け取ります。 そのため、このURLのレスポンスは以下のようになります: @@ -179,15 +287,15 @@ http://localhost:8000/items/?q=foo&q=bar 対話的APIドキュメントは複数の値を許可するために自動的に更新されます。 - + -### デフォルト値を持つ、クエリパラメータのリスト / 複数の値 +### デフォルト値を持つ、クエリパラメータのリスト / 複数の値 { #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] *} -以下のURLを開くと: +以下にアクセスすると: ``` http://localhost:8000/items/ @@ -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はリストの内容をチェックしないことを覚えておいてください。 -例えば`List[int]`はリストの内容が整数であるかどうかをチェックします(そして、文書化します)。しかし`list`だけではそうしません。 +例えば`list[int]`はリストの内容が整数であるかどうかをチェックします(そして、文書化します)。しかし`list`だけではそうしません。 /// -## より多くのメタデータを宣言する +## より多くのメタデータを宣言する { #declare-more-metadata } パラメータに関する情報をさらに追加することができます。 @@ -234,13 +342,13 @@ http://localhost:8000/items/ `title`を追加できます: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[9] *} +{* ../../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`を指定するとします. @@ -258,23 +366,91 @@ http://127.0.0.1:8000/items/?item-query=foobaritems それならば、`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と明記しておきたいです。 その場合、`Query`にパラメータ`deprecated=True`を渡します: -{* ../../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` などもあります。 🤓 + +/// + +例えば、このカスタムバリデータは、ISBN の書籍番号なら item ID が `isbn-` で始まること、IMDB の movie 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** を使うべきです。これについては後で学びます。 + +これらのカスタムバリデータは、リクエストで提供された **同じデータのみ** でチェックできるもの向けです。 + +/// + +### そのコードを理解する { #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()` で、辞書の各アイテムのキーと値を含むタプルを持つ 反復可能オブジェクト を取得します。 + +この反復可能オブジェクトを `list(data.items())` で適切な `list` に変換します。 + +そして `random.choice()` でその `list` から **ランダムな値** を取得するので、`(id, name)` のタプルを得ます。これは `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")` のようになります。 + +次に、そのタプルの **2つの値を代入** して、変数 `id` と `name` に入れます。 + +つまり、ユーザーが item ID を提供しなかった場合でも、ランダムな提案を受け取ります。 + +...これを **単一のシンプルな1行** で行っています。 🤯 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` -この例では、`str`の値のバリデーションを宣言する方法を見てきました。 +`AfterValidator` を使ったカスタムバリデーション。 + +この例では、`str` の値のバリデーションを宣言する方法を見てきました。 数値のような他の型のバリデーションを宣言する方法は次の章を参照してください。 diff --git a/docs/ja/docs/tutorial/query-params.md b/docs/ja/docs/tutorial/query-params.md index 74e455579..41e51ed78 100644 --- a/docs/ja/docs/tutorial/query-params.md +++ b/docs/ja/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 @@ http://127.0.0.1:8000/items/?skip=0&limit=10 パスパラメータに適用される処理と完全に同様な処理がクエリパラメータにも施されます: * エディターサポート (明らかに) -* データ「解析」 +* データ 「解析」 * データバリデーション * 自動ドキュメント生成 -## デフォルト +## デフォルト { #defaults } クエリパラメータはパスの固定部分ではないので、オプショナルとしたり、デフォルト値をもつことができます。 @@ -55,13 +55,13 @@ http://127.0.0.1:8000/items/?skip=20 関数内のパラメータの値は以下の様になります: * `skip=20`: URL内にセットしたため -* `limit=10`: デフォルト値 +* `limit=10`: デフォルト値だったため -## オプショナルなパラメータ +## オプショナルなパラメータ { #optional-parameters } 同様に、デフォルト値を `None` とすることで、オプショナルなクエリパラメータを宣言できます: -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} この場合、関数パラメータ `q` はオプショナルとなり、デフォルトでは `None` になります。 @@ -71,11 +71,11 @@ http://127.0.0.1:8000/items/?skip=20 /// -## クエリパラメータの型変換 +## クエリパラメータの型変換 { #query-parameter-type-conversion } -`bool` 型も宣言できます。これは以下の様に変換されます: +`bool` 型も宣言でき、変換されます: -{* ../../docs_src/query_params/tutorial003.py hl[9] *} +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} この場合、以下にアクセスすると: @@ -109,27 +109,28 @@ http://127.0.0.1:8000/items/foo?short=yes もしくは、他の大文字小文字のバリエーション (アッパーケース、最初の文字だけアッパーケース、など)で、関数は `short` パラメータを `True` な `bool` 値として扱います。それ以外は `False` になります。 -## 複数のパスパラメータとクエリパラメータ -複数のパスパラメータとクエリパラメータを同時に宣言できます。**FastAPI**は互いを区別できます。 +## 複数のパスパラメータとクエリパラメータ { #multiple-path-and-query-parameters } + +複数のパスパラメータとクエリパラメータを同時に宣言できます。**FastAPI**はどれがどれかを把握しています。 そして特定の順序で宣言しなくてもよいです。 名前で判別されます: -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## 必須のクエリパラメータ +## 必須のクエリパラメータ { #required-query-parameters } -パスパラメータ以外のパラメータ (今のところ、クエリパラメータのみ説明しました) のデフォルト値を宣言した場合、そのパラメータは必須ではなくなります。 +パスパラメータ以外のパラメータ (今のところ、クエリパラメータのみ見てきました) のデフォルト値を宣言した場合、そのパラメータは必須ではなくなります。 特定の値を与えずにただオプショナルにしたい場合はデフォルト値を `None` にして下さい。 しかしクエリパラメータを必須にしたい場合は、ただデフォルト値を宣言しなければよいです: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -ここで、クエリパラメータ `needy` は `str` 型の必須のクエリパラメータです +ここで、クエリパラメータ `needy` は `str` 型の必須のクエリパラメータです。 以下のURLをブラウザで開くと: @@ -141,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 + } + ] } ``` @@ -169,9 +171,9 @@ 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つのクエリパラメータがあります。: @@ -181,6 +183,6 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy /// tip | 豆知識 -[パスパラメータ](path-params.md#_8){.internal-link target=_blank}と同様に `Enum` を使用できます。 +[パスパラメータ](path-params.md#predefined-values){.internal-link target=_blank}と同様に `Enum` を使用できます。 /// diff --git a/docs/ja/docs/tutorial/request-forms-and-files.md b/docs/ja/docs/tutorial/request-forms-and-files.md index 110e3106a..09e1277c8 100644 --- a/docs/ja/docs/tutorial/request-forms-and-files.md +++ b/docs/ja/docs/tutorial/request-forms-and-files.md @@ -1,24 +1,28 @@ -# リクエストフォームとファイル +# リクエストフォームとファイル { #request-forms-and-files } `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] *} ファイルとフォームフィールドがフォームデータとしてアップロードされ、ファイルとフォームフィールドを受け取ります。 @@ -32,6 +36,6 @@ /// -## まとめ +## まとめ { #recap } 同じリクエストでデータやファイルを受け取る必要がある場合は、`File` と`Form`を一緒に使用します。 diff --git a/docs/ja/docs/tutorial/request-forms.md b/docs/ja/docs/tutorial/request-forms.md index eca2cd6dc..1bdc28670 100644 --- a/docs/ja/docs/tutorial/request-forms.md +++ b/docs/ja/docs/tutorial/request-forms.md @@ -1,4 +1,4 @@ -# フォームデータ +# フォームデータ { #form-data } JSONの代わりにフィールドを受け取る場合は、`Form`を使用します。 @@ -6,27 +6,31 @@ JSONの代わりにフィールドを受け取る場合は、`Form`を使用し フォームを使うためには、まず`python-multipart`をインストールします。 -たとえば、`pip install python-multipart`のように。 +必ず[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成して有効化してから、例えば次のようにインストールしてください: + +```console +$ pip install python-multipart +``` /// -## `Form`のインポート +## `Form`のインポート { #import-form } `fastapi`から`Form`をインポートします: -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## `Form`のパラメータの定義 +## `Form`のパラメータの定義 { #define-form-parameters } `Body`や`Query`の場合と同じようにフォームパラメータを作成します: -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} 例えば、OAuth2仕様が使用できる方法の1つ(「パスワードフロー」と呼ばれる)では、フォームフィールドとして`username`と`password`を送信する必要があります。 -仕様では、フィールドの名前が`username`と`password`であることと、JSONではなくフォームフィールドとして送信されることを要求しています。 +specでは、フィールドの名前が`username`と`password`であることと、JSONではなくフォームフィールドとして送信されることを要求しています。 -`Form`では`Body`(および`Query`や`Path`、`Cookie`)と同じメタデータとバリデーションを宣言することができます。 +`Form`では`Body`(および`Query`や`Path`、`Cookie`)と同じ設定を宣言することができます。これには、バリデーション、例、エイリアス(例えば`username`の代わりに`user-name`)などが含まれます。 /// info | 情報 @@ -40,7 +44,7 @@ JSONの代わりにフィールドを受け取る場合は、`Form`を使用し /// -## 「フォームフィールド」について +## 「フォームフィールド」について { #about-form-fields } HTMLフォーム(`
`)がサーバにデータを送信する方法は、通常、そのデータに「特別な」エンコーディングを使用していますが、これはJSONとは異なります。 @@ -64,6 +68,6 @@ HTMLフォーム(`
`)がサーバにデータを送信する方 /// -## まとめ +## まとめ { #recap } フォームデータの入力パラメータを宣言するには、`Form`を使用する。 diff --git a/docs/ja/docs/tutorial/response-model.md b/docs/ja/docs/tutorial/response-model.md index b8464a4c7..258eac8e6 100644 --- a/docs/ja/docs/tutorial/response-model.md +++ b/docs/ja/docs/tutorial/response-model.md @@ -1,6 +1,35 @@ -# レスポンスモデル +# レスポンスモデル - 戻り値の型 { #response-model-return-type } -*path operations* のいずれにおいても、`response_model`パラメータを使用して、レスポンスのモデルを宣言することができます: +*path operation 関数*の**戻り値の型**にアノテーションを付けることで、レスポンスに使用される型を宣言できます。 + +関数**パラメータ**の入力データと同じように **型アノテーション** を使用できます。Pydanticモデル、リスト、辞書、整数や真偽値などのスカラー値を使用できます。 + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPIはこの戻り値の型を使って以下を行います: + +* 返却データを**検証**します。 + * データが不正(例: フィールドが欠けている)であれば、それは*あなた*のアプリコードが壊れていて、返すべきものを返していないことを意味し、不正なデータを返す代わりにサーバーエラーを返します。これにより、あなたとクライアントは、期待されるデータとデータ形状を受け取れることを確実にできます。 +* OpenAPIの *path operation* に、レスポンス用の **JSON Schema** を追加します。 + * これは**自動ドキュメント**で使用されます。 + * 自動クライアントコード生成ツールでも使用されます。 + +しかし、最も重要なのは: + +* 戻り値の型で定義された内容に合わせて、出力データを**制限しフィルタリング**します。 + * これは**セキュリティ**の観点で特に重要です。以下で詳しく見ていきます。 + +## `response_model`パラメータ { #response-model-parameter } + +型が宣言している内容とまったく同じではないデータを返す必要がある、またはそうしたいケースがあります。 + +例えば、**辞書を返す**、またはデータベースオブジェクトを返したいが、**Pydanticモデルとして宣言**したい場合があります。こうすることで、Pydanticモデルが返したオブジェクト(例: 辞書やデータベースオブジェクト)のドキュメント化、バリデーションなどをすべて行ってくれます。 + +戻り値の型アノテーションを追加すると、ツールやエディタが(正しく)エラーとして、関数が宣言した型(例: Pydanticモデル)とは異なる型(例: dict)を返していると警告します。 + +そのような場合、戻り値の型の代わりに、*path operation デコレータ*のパラメータ `response_model` を使用できます。 + +`response_model`パラメータは、いずれの *path operation* でも使用できます: * `@app.get()` * `@app.post()` @@ -8,104 +37,211 @@ * `@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`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数* のパラメータではありません。 +`response_model`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation 関数* のパラメータではありません。 /// -Pydanticモデルの属性に対して宣言するのと同じ型を受け取るので、Pydanticモデルになることもできますが、例えば、`List[Item]`のようなPydanticモデルの`list`になることもできます。 +`response_model`は、Pydanticモデルのフィールドで宣言するのと同じ型を受け取ります。そのため、Pydanticモデルにもできますし、例えば `List[Item]` のように、Pydanticモデルの `list` にもできます。 -FastAPIは`response_model`を使って以下のことをします: +FastAPIはこの `response_model` を使って、データのドキュメント化や検証などを行い、さらに出力データを型宣言に合わせて**変換・フィルタリング**します。 -* 出力データを型宣言に変換します。 -* データを検証します。 -* OpenAPIの *path operation* で、レスポンス用のJSON Schemaを追加します。 -* 自動ドキュメントシステムで使用されます。 +/// 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` を使用して、その*path operation*のレスポンスモデル生成を無効化することもできます。これは、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がレスポンスで同じパスワードを返すようになりました。 -この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。 +この場合、同じユーザーがパスワードを送信しているので問題ないかもしれません。 -しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまうことになります。 +しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまう可能性があります。 -/// danger | 危険 +/// 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] *} -ここでは、*path operation関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: +ここでは、*path operation 関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: -{* ../../docs_src/response_model/tutorial003.py hl[24] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} -...`response_model`を`UserOut`と宣言したことで、パスワードが含まれていません: +...`response_model`を、パスワードを含まない `UserOut` モデルとして宣言しました: -{* ../../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 Schemaを持っていることが確認できます。 +このケースでは2つのモデルが異なるため、関数の戻り値の型を `UserOut` としてアノテーションすると、エディタやツールは、異なるクラスなので不正な型を返していると警告します。 - +そのため、この例では `response_model` パラメータで宣言する必要があります。 -そして、両方のモデルは、対話型のAPIドキュメントに使用されます: +...しかし、これを解決する方法を以下で確認しましょう。 - +## 戻り値の型とデータフィルタリング { #return-type-and-data-filtering } -## レスポンスモデルのエンコーディングパラメータ +前の例から続けます。**関数に1つの型をアノテーション**したい一方で、関数からは実際には**より多くのデータ**を含むものを返せるようにしたいとします。 -レスポンスモデルにはデフォルト値を設定することができます: +FastAPIにはレスポンスモデルを使用してデータを**フィルタリング**し続けてほしいです。つまり、関数がより多くのデータを返しても、レスポンスにはレスポンスモデルで宣言されたフィールドのみが含まれます。 -{* ../../docs_src/response_model/tutorial004.py hl[11,13,14] *} +前の例ではクラスが異なるため `response_model` パラメータを使う必要がありました。しかしそれは、エディタやツールによる関数の戻り値の型チェックのサポートを受けられないことも意味します。 -* `description: str = None`は`None`がデフォルト値です。 -* `tax: float = 10.5`は`10.5`がデフォルト値です。 -* `tags: List[str] = []` は空のリスト(`[]`)がデフォルト値です。 +しかし、このようなことが必要になる多くのケースでは、この例のようにモデルでデータの一部を**フィルタ/削除**したいだけです。 -しかし、実際に保存されていない場合には結果からそれらを省略した方が良いかもしれません。 +そのような場合、クラスと継承を利用して関数の**型アノテーション**を活用し、エディタやツールのサポートを改善しつつ、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などのツールはこれに文句を言いません。typingの観点では、`UserIn` は `BaseUser` のサブクラスであり、期待されるものが `BaseUser` であれば `UserIn` は*有効*な型だからです。 + +### FastAPIのデータフィルタリング { #fastapi-data-filtering } + +一方FastAPIでは、戻り値の型を見て、返す内容にその型で宣言されたフィールド**だけ**が含まれることを確認します。 + +FastAPIは、返却データのフィルタリングにクラス継承の同じルールが使われてしまわないようにするため、内部でPydanticを使っていくつかの処理を行っています。そうでないと、期待以上に多くのデータを返してしまう可能性があります。 + +この方法で、**ツール支援**付きの型アノテーションと**データフィルタリング**の両方という、いいとこ取りができます。 + +## ドキュメントを見る { #see-it-in-the-docs } + +自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます: + + + +そして、両方のモデルは対話型のAPIドキュメントに使用されます: + + + +## その他の戻り値の型アノテーション { #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レスポンスモデルを作成しようとして失敗します。 + +同様に、unionのように、複数の型のうち1つ以上がPydantic型として有効でないものを含む場合も起こります。例えば次は失敗します 💥: + +{* ../../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_py310.py hl[9,11:12] *} + +* `description: Union[str, None] = None`(またはPython 3.10では `str | None = None`)はデフォルトが `None` です。 +* `tax: float = 10.5` はデフォルトが `10.5` です。 +* `tags: List[str] = []` はデフォルトが空のリスト `[]` です。 + +ただし、それらが実際には保存されていない場合、結果から省略したいことがあります。 例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合です。 -### `response_model_exclude_unset`パラメータの使用 +### `response_model_exclude_unset`パラメータの使用 { #use-the-response-model-exclude-unset-parameter } -*path operation デコレータ*に`response_model_exclude_unset=True`パラメータを設定することができます: +*path operation デコレータ*のパラメータ `response_model_exclude_unset=True` を設定できます: -{* ../../docs_src/response_model/tutorial004.py hl[24] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -そして、これらのデフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 +そうすると、デフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 -そのため、*path operation*にID`foo`が設定されたitemのリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): +そのため、ID `foo` のitemに対してその *path operation* へリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): ```JSON { @@ -116,26 +252,20 @@ FastAPIは`response_model`を使って以下のことをします: /// info | 情報 -FastAPIはこれをするために、Pydanticモデルの`.dict()`をその`exclude_unset`パラメータで使用しています。 - -/// - -/// info | 情報 - -以下も使用することができます: +以下も使用できます: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -`exclude_defaults`と`exclude_none`については、Pydanticのドキュメントで説明されている通りです。 +`exclude_defaults` と `exclude_none` については、Pydanticのドキュメントで説明されている通りです。 /// -#### デフォルト値を持つフィールドの値を持つデータ +#### デフォルト値を持つフィールドに値があるデータ { #data-with-values-for-fields-with-defaults } -しかし、ID`bar`のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: +しかし、ID `bar` のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: -```Python hl_lines="3 5" +```Python hl_lines="3 5" { "name": "Bar", "description": "The bartenders", @@ -146,11 +276,11 @@ FastAPIはこれをするために、Pydanticモデルの`.dict()`を + /// note | 備考 @@ -39,7 +39,7 @@ FastAPIはこれを知っていて、レスポンスボディがないというO /// -## HTTPステータスコードについて +## HTTPステータスコードについて { #about-http-status-codes } /// note | 備考 @@ -47,34 +47,34 @@ FastAPIはこれを知っていて、レスポンスボディがないというO /// -HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 +HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 これらのステータスコードは、それらを認識するために関連付けられた名前を持っていますが、重要な部分は番号です。 つまり: -* `100`以上は「情報」のためのものです。。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 -* **`200`** 以上は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 +* `100 - 199` は「情報」のためのものです。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 +* **`200 - 299`** は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 * `200`はデフォルトのステータスコードで、すべてが「OK」であったことを意味します。 * 別の例としては、`201`(Created)があります。これはデータベースに新しいレコードを作成した後によく使用されます。 - * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されます。そしてこのレスポンスはボディを持つことはできません。 -* **`300`** 以上は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。 -* **`400`** 以上は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 + * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されるため、レスポンスはボディを持ってはいけません。 +* **`300 - 399`** は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。`304`はボディを持ってはいけません。 +* **`400 - 499`** は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 * 例えば、`404`は「Not Found」レスポンスです。 * クライアントからの一般的なエラーについては、`400`を使用することができます。 -* `500`以上はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 +* `500 - 599` はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 /// tip | 豆知識 -それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細はMDN HTTP レスポンスステータスコードについてのドキュメントを参照してください。 +それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細はMDN documentation about HTTP status codesを参照してください。 /// -## 名前を覚えるための近道 +## 名前を覚えるための近道 { #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`は「作成完了」のためのステータスコードです。 @@ -82,11 +82,11 @@ HTTPでは、レスポンスの一部として3桁の数字のステータス `fastapi.status`の便利な変数を利用することができます。 -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -それらは便利です。それらは同じ番号を保持しており、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 +それらは単なる便利なものであり、同じ番号を保持しています。しかし、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 - + /// note | 技術詳細 @@ -96,6 +96,6 @@ HTTPでは、レスポンスの一部として3桁の数字のステータス /// -## デフォルトの変更 +## デフォルトの変更 { #changing-the-default } 後に、[高度なユーザーガイド](../advanced/response-change-status-code.md){.internal-link target=_blank}で、ここで宣言しているデフォルトとは異なるステータスコードを返す方法を見ていきます。 diff --git a/docs/ja/docs/tutorial/schema-extra-example.md b/docs/ja/docs/tutorial/schema-extra-example.md index 1834e67b2..e526685c2 100644 --- a/docs/ja/docs/tutorial/schema-extra-example.md +++ b/docs/ja/docs/tutorial/schema-extra-example.md @@ -1,55 +1,202 @@ -# スキーマの追加 - 例 +# リクエストのExampleデータの宣言 { #declare-request-example-data } -JSON Schemaに追加する情報を定義することができます。 +アプリが受け取れるデータの例を宣言できます。 -一般的なユースケースはこのドキュメントで示されているように`example`を追加することです。 +ここでは、それを行ういくつかの方法を紹介します。 -JSON Schemaの追加情報を宣言する方法はいくつかあります。 +## Pydanticモデルでの追加JSON Schemaデータ { #extra-json-schema-data-in-pydantic-models } -## Pydanticの`schema_extra` +生成されるJSON Schemaに追加されるPydanticモデルの`examples`を宣言できます。 -Pydanticのドキュメント: スキーマのカスタマイズで説明されているように、`Config`と`schema_extra`を使ってPydanticモデルの例を宣言することができます: +{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -{* ../../docs_src/schema_extra_example/tutorial001.py hl[15,16,17,18,19,20,21,22,23] *} +その追加情報は、そのモデルの出力**JSON Schema**にそのまま追加され、APIドキュメントで使用されます。 -その追加情報はそのまま出力され、JSON Schemaに追加されます。 +Pydanticのドキュメント: Configurationで説明されているように、`dict`を受け取る属性`model_config`を使用できます。 -## `Field`の追加引数 +生成されるJSON Schemaに表示したい追加データ(`examples`を含む)を含む`dict`を使って、`"json_schema_extra"`を設定できます。 -後述する`Field`、`Path`、`Query`、`Body`などでは、任意の引数を関数に渡すことでJSON Schemaの追加情報を宣言することもできます: +/// tip | 豆知識 -{* ../../docs_src/schema_extra_example/tutorial002.py hl[4,10,11,12,13] *} +同じ手法を使ってJSON Schemaを拡張し、独自のカスタム追加情報を追加できます。 -/// warning | 注意 - -これらの追加引数が渡されても、文書化のためのバリデーションは追加されず、注釈だけが追加されることを覚えておいてください。 +例えば、フロントエンドのユーザーインターフェースのためのメタデータを追加する、などに使えます。 /// -## `Body`の追加引数 +/// info | 情報 -追加情報を`Field`に渡すのと同じように、`Path`、`Query`、`Body`などでも同じことができます。 +OpenAPI 3.1.0(FastAPI 0.99.0以降で使用)では、**JSON Schema**標準の一部である`examples`がサポートされました。 -例えば、`Body`にボディリクエストの`example`を渡すことができます: +それ以前は、単一の例を持つキーワード`example`のみがサポートされていました。これはOpenAPI 3.1.0でも引き続きサポートされていますが、非推奨であり、JSON Schema標準の一部ではありません。そのため、`example`から`examples`への移行が推奨されます。🤓 -{* ../../docs_src/schema_extra_example/tutorial003.py hl[21,22,23,24,25,26] *} +詳細はこのページの最後で読めます。 -## ドキュメントのUIの例 +/// + +## `Field`の追加引数 { #field-additional-arguments } + +Pydanticモデルで`Field()`を使う場合、追加の`examples`も宣言できます: + +{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} + +## JSON Schema内の`examples` - OpenAPI { #examples-in-json-schema-openapi } + +以下のいずれかを使用する場合: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +追加情報を含む`examples`のグループを宣言でき、それらは**OpenAPI**内のそれぞれの**JSON Schemas**に追加されます。 + +### `examples`を使う`Body` { #body-with-examples } + +ここでは、`Body()`で期待されるデータの例を1つ含む`examples`を渡します: + +{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} + +### ドキュメントUIでの例 { #example-in-the-docs-ui } 上記のいずれの方法でも、`/docs`の中では以下のようになります: - + -## 技術詳細 +### 複数の`examples`を使う`Body` { #body-with-multiple-examples } -`example` と `examples`について... +もちろん、複数の`examples`を渡すこともできます: -JSON Schemaの最新バージョンでは`examples`というフィールドを定義していますが、OpenAPIは`examples`を持たない古いバージョンのJSON Schemaをベースにしています。 +{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -そのため、OpenAPIでは同じ目的のために`example`を独自に定義しており(`examples`ではなく`example`として)、それがdocs UI(Swagger UIを使用)で使用されています。 +この場合、examplesはそのボディデータの内部**JSON Schema**の一部になります。 -つまり、`example`はJSON Schemaの一部ではありませんが、OpenAPIの一部であり、それがdocs UIで使用されることになります。 +それでも、執筆時点では、ドキュメントUIの表示を担当するツールであるSwagger UIは、**JSON Schema**内のデータに対して複数の例を表示することをサポートしていません。しかし、回避策については以下を読んでください。 -## その他の情報 +### OpenAPI固有の`examples` { #openapi-specific-examples } -同じように、フロントエンドのユーザーインターフェースなどをカスタマイズするために、各モデルのJSON Schemaに追加される独自の追加情報を追加することができます。 +**JSON Schema**が`examples`をサポートする前から、OpenAPIは同じく`examples`という別のフィールドをサポートしていました。 + +この**OpenAPI固有**の`examples`は、OpenAPI仕様の別のセクションに入ります。各JSON Schemaの中ではなく、**各*path operation*の詳細**に入ります。 + +そしてSwagger UIは、この特定の`examples`フィールドを以前からサポートしています。そのため、これを使って**ドキュメントUIに異なる例を表示**できます。 + +このOpenAPI固有フィールド`examples`の形は**複数の例**(`list`ではなく)を持つ`dict`であり、それぞれに追加情報が含まれ、その追加情報は**OpenAPI**にも追加されます。 + +これはOpenAPIに含まれる各JSON Schemaの中には入らず、外側の、*path operation*に直接入ります。 + +### `openapi_examples`パラメータの使用 { #using-the-openapi-examples-parameter } + +FastAPIでは、以下に対してパラメータ`openapi_examples`を使って、OpenAPI固有の`examples`を宣言できます: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +`dict`のキーは各例を識別し、各値は別の`dict`です。 + +`examples`内の各特定の例`dict`には、次の内容を含められます: + +* `summary`: 例の短い説明。 +* `description`: Markdownテキストを含められる長い説明。 +* `value`: 実際に表示される例(例: `dict`)。 +* `externalValue`: `value`の代替で、例を指すURLです。ただし、`value`ほど多くのツールでサポートされていない可能性があります。 + +次のように使えます: + +{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} + +### ドキュメントUIのOpenAPI Examples { #openapi-examples-in-the-docs-ui } + +`Body()`に`openapi_examples`を追加すると、`/docs`は次のようになります: + + + +## 技術詳細 { #technical-details } + +/// tip | 豆知識 + +すでに**FastAPI**バージョン**0.99.0以上**を使用している場合、おそらくこれらの詳細は**スキップ**できます。 + +これらは、OpenAPI 3.1.0が利用可能になる前の古いバージョンにより関連します。 + +これは簡単なOpenAPIとJSON Schemaの**歴史の授業**だと考えられます。🤓 + +/// + +/// warning | 注意 + +ここでは、標準である**JSON Schema**と**OpenAPI**についての非常に技術的な詳細を扱います。 + +上のアイデアがすでにうまく動いているなら、それで十分かもしれませんし、おそらくこの詳細は不要です。気軽にスキップしてください。 + +/// + +OpenAPI 3.1.0より前は、OpenAPIは古く改変されたバージョンの**JSON Schema**を使用していました。 + +JSON Schemaには`examples`がなかったため、OpenAPIは自身が改変したバージョンに独自の`example`フィールドを追加しました。 + +OpenAPIは、仕様の他の部分にも`example`と`examples`フィールドを追加しました: + +* `Parameter Object`(仕様内)。FastAPIの以下で使用されました: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`。仕様内の`Media Type Object`の`content`フィールド(仕様内)。FastAPIの以下で使用されました: + * `Body()` + * `File()` + * `Form()` + +/// info | 情報 + +この古いOpenAPI固有の`examples`パラメータは、FastAPI `0.103.0`以降は`openapi_examples`になりました。 + +/// + +### JSON Schemaの`examples`フィールド { #json-schemas-examples-field } + +しかしその後、JSON Schemaは新しいバージョンの仕様に`examples`フィールドを追加しました。 + +そして、新しいOpenAPI 3.1.0は、この新しいフィールド`examples`を含む最新バージョン(JSON Schema 2020-12)に基づくようになりました。 + +そして現在、この新しい`examples`フィールドは、古い単一(かつカスタム)の`example`フィールドより優先され、`example`は現在非推奨です。 + +JSON Schemaのこの新しい`examples`フィールドは、OpenAPIの他の場所(上で説明)にあるような追加メタデータを持つdictではなく、**単なる例の`list`**です。 + +/// info | 情報 + +OpenAPI 3.1.0がこのJSON Schemaとの新しいよりシンプルな統合とともにリリースされた後も、しばらくの間、自動ドキュメントを提供するツールであるSwagger UIはOpenAPI 3.1.0をサポートしていませんでした(バージョン5.0.0からサポートされています🎉)。 + +そのため、FastAPI 0.99.0より前のバージョンは、OpenAPI 3.1.0より低いバージョンのOpenAPIをまだ使用していました。 + +/// + +### PydanticとFastAPIの`examples` { #pydantic-and-fastapi-examples } + +Pydanticモデル内で、`schema_extra`または`Field(examples=["something"])`を使って`examples`を追加すると、その例はそのPydanticモデルの**JSON Schema**に追加されます。 + +そしてそのPydanticモデルの**JSON Schema**はAPIの**OpenAPI**に含まれ、ドキュメントUIで使用されます。 + +FastAPI 0.99.0より前のバージョン(0.99.0以上は新しいOpenAPI 3.1.0を使用)では、他のユーティリティ(`Query()`、`Body()`など)で`example`または`examples`を使っても、それらの例はそのデータを説明するJSON Schema(OpenAPI独自版のJSON Schemaでさえ)には追加されず、OpenAPI内の*path operation*宣言に直接追加されていました(JSON Schemaを使用するOpenAPIの部分の外側)。 + +しかし、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-and-openapi-specific-examples } + +Swagger UIは複数のJSON Schema examplesをサポートしていなかった(2023-08-26時点)ため、ユーザーはドキュメントで複数の例を表示する手段がありませんでした。 + +それを解決するため、FastAPI `0.103.0`は、新しいパラメータ`openapi_examples`で、同じ古い**OpenAPI固有**の`examples`フィールドを宣言するための**サポートを追加**しました。🤓 + +### まとめ { #summary } + +昔は歴史があまり好きではないと言っていました...が、今の私は「技術の歴史」の授業をしています。😅 + +要するに、**FastAPI 0.99.0以上にアップグレード**してください。そうすれば、物事はもっと**シンプルで一貫性があり直感的**になり、これらの歴史的詳細を知る必要もありません。😎 diff --git a/docs/ja/docs/tutorial/security/first-steps.md b/docs/ja/docs/tutorial/security/first-steps.md index 0ce0f929b..76ef04db8 100644 --- a/docs/ja/docs/tutorial/security/first-steps.md +++ b/docs/ja/docs/tutorial/security/first-steps.md @@ -1,4 +1,4 @@ -# セキュリティ - 最初の一歩 +# セキュリティ - 最初の一歩 { #security-first-steps } あるドメインに、**バックエンド** APIを持っているとしましょう。 @@ -12,25 +12,31 @@ **FastAPI**が提供するツールを使って、セキュリティを制御してみましょう。 -## どう見えるか +## どう見えるか { #how-it-looks } まずはこのコードを使って、どう動くか観察します。その後で、何が起こっているのか理解しましょう。 -## `main.py`を作成 +## `main.py`を作成 { #create-main-py } `main.py`に、下記の例をコピーします: -{* ../../docs_src/security/tutorial001.py *} +{* ../../docs_src/security/tutorial001_an_py39.py *} -## 実行 +## 実行 { #run-it } /// info | 情報 -まず`python-multipart`をインストールします。 +`python-multipart` パッケージは、`pip install "fastapi[standard]"` コマンドを実行すると **FastAPI** と一緒に自動的にインストールされます。 -例えば、`pip install python-multipart`。 +しかし、`pip install fastapi` コマンドを使用する場合、`python-multipart` パッケージはデフォルトでは含まれません。 -これは、**OAuth2**が `ユーザー名` や `パスワード` を送信するために、「フォームデータ」を使うからです。 +手動でインストールするには、[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成して有効化し、次のコマンドでインストールしてください: + +```console +$ pip install python-multipart +``` + +これは、**OAuth2**が `username` と `password` を送信するために、「フォームデータ」を使うからです。 /// @@ -39,14 +45,14 @@
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-## 確認 +## 確認 { #check-it } 次のインタラクティブなドキュメントにアクセスしてください: http://127.0.0.1:8000/docs。 @@ -62,7 +68,7 @@ $ uvicorn main:app --reload /// -それをクリックすると、`ユーザー名`と`パスワード` (およびその他のオプションフィールド) を入力する小さな認証フォームが表示されます: +それをクリックすると、`username` と `password`(およびその他のオプションフィールド)を入力する小さな認可フォームが表示されます: @@ -80,11 +86,11 @@ $ uvicorn main:app --reload また、同じアプリケーションのデバッグ、チェック、テストのためにも利用できます。 -## `パスワード` フロー +## `password` フロー { #the-password-flow } では、少し話を戻して、どうなっているか理解しましょう。 -`パスワード`の「フロー」は、OAuth2で定義されているセキュリティと認証を扱う方法 (「フロー」) の1つです。 +`password`の「フロー」は、OAuth2で定義されているセキュリティと認証を扱う方法 (「フロー」) の1つです。 OAuth2は、バックエンドやAPIがユーザーを認証するサーバーから独立したものとして設計されていました。 @@ -92,9 +98,9 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー そこで、簡略化した箇所から見直してみましょう: -* ユーザーはフロントエンドで`ユーザー名`と`パスワード`を入力し、`Enter`を押します。 -* フロントエンド (ユーザーのブラウザで実行中) は、`ユーザー名`と`パスワード`をAPIの特定のURL (`tokenUrl="token"`で宣言された) に送信します。 -* APIは`ユーザー名`と`パスワード`をチェックし、「トークン」を返却します (まだ実装していません)。 +* ユーザーはフロントエンドで`username`と`password`を入力し、`Enter`を押します。 +* フロントエンド (ユーザーのブラウザで実行中) は、`username`と`password`をAPIの特定のURL (`tokenUrl="token"`で宣言された) に送信します。 +* APIは`username`と`password`をチェックし、「トークン」を返却します (まだ実装していません)。 * 「トークン」はただの文字列であり、あとでこのユーザーを検証するために使用します。 * 通常、トークンは時間が経つと期限切れになるように設定されています。 * トークンが期限切れの場合は、再度ログインする必要があります。 @@ -106,11 +112,11 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー * したがって、APIで認証するため、HTTPヘッダー`Authorization`に`Bearer`の文字列とトークンを加えた値を送信します。 * トークンに`foobar`が含まれている場合、`Authorization`ヘッダーの内容は次のようになります: `Bearer foobar`。 -## **FastAPI**の`OAuth2PasswordBearer` +## **FastAPI**の`OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer } **FastAPI**は、これらのセキュリティ機能を実装するために、抽象度の異なる複数のツールを提供しています。 -この例では、**Bearer**トークンを使用して**OAuth2**を**パスワード**フローで使用します。これには`OAuth2PasswordBearer`クラスを使用します。 +この例では、**Bearer**トークンを使用して**OAuth2**を**Password**フローで使用します。これには`OAuth2PasswordBearer`クラスを使用します。 /// info | 情報 @@ -124,9 +130,9 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー /// -`OAuth2PasswordBearer` クラスのインスタンスを作成する時に、パラメーター`tokenUrl`を渡します。このパラメーターには、クライアント (ユーザーのブラウザで動作するフロントエンド) がトークンを取得するために`ユーザー名`と`パスワード`を送信するURLを指定します。 +`OAuth2PasswordBearer` クラスのインスタンスを作成する時に、パラメーター`tokenUrl`を渡します。このパラメーターには、クライアント (ユーザーのブラウザで動作するフロントエンド) がトークンを取得するために`username`と`password`を送信するURLを指定します。 -{* ../../docs_src/security/tutorial001.py hl[6] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} /// tip | 豆知識 @@ -134,13 +140,13 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー 相対URLを使っているので、APIが`https://example.com/`にある場合、`https://example.com/token`を参照します。しかし、APIが`https://example.com/api/v1/`にある場合は`https://example.com/api/v1/token`を参照することになります。 -相対 URL を使うことは、[プロキシと接続](../../advanced/behind-a-proxy.md){.internal-link target=_blank}のような高度なユースケースでもアプリケーションを動作させ続けるために重要です。 +相対 URL を使うことは、[プロキシの背後](../../advanced/behind-a-proxy.md){.internal-link target=_blank}のような高度なユースケースでもアプリケーションを動作させ続けるために重要です。 /// このパラメーターはエンドポイント/ *path operation*を作成しません。しかし、URL`/token`はクライアントがトークンを取得するために使用するものであると宣言します。この情報は OpenAPI やインタラクティブな API ドキュメントシステムで使われます。 -実際のpath operationもすぐに作ります。 +実際の path operation もすぐに作ります。 /// info | 情報 @@ -160,13 +166,13 @@ oauth2_scheme(some, parameters) そのため、`Depends`と一緒に使うことができます。 -### 使い方 +### 使い方 { #use-it } これで`oauth2_scheme`を`Depends`で依存関係に渡すことができます。 -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -この依存関係は、*path operation function*のパラメーター`token`に代入される`str`を提供します。 +この依存関係は、*path operation 関数*のパラメーター`token`に代入される`str`を提供します。 **FastAPI**は、この依存関係を使用してOpenAPIスキーマ (および自動APIドキュメント) で「セキュリティスキーム」を定義できることを知っています。 @@ -178,13 +184,13 @@ OpenAPIと統合するセキュリティユーティリティ (および自動AP /// -## どのように動作するか +## 何をするか { #what-it-does } -リクエストの中に`Authorization`ヘッダーを探しに行き、その値が`Bearer`と何らかのトークンを含んでいるかどうかをチェックし、そのトークンを`str`として返します。 +リクエストの中に`Authorization`ヘッダーを探しに行き、その値が`Bearer `と何らかのトークンを含んでいるかどうかをチェックし、そのトークンを`str`として返します。 -もし`Authorization`ヘッダーが見つからなかったり、値が`Bearer`トークンを持っていなかったりすると、401 ステータスコードエラー (`UNAUTHORIZED`) で直接応答します。 +もし`Authorization`ヘッダーが見つからなかったり、値が`Bearer `トークンを持っていなかったりすると、401 ステータスコードエラー (`UNAUTHORIZED`) で直接応答します。 -トークンが存在するかどうかをチェックしてエラーを返す必要はありません。関数が実行された場合、そのトークンに`str`が含まれているか確認できます。 +トークンが存在するかどうかをチェックしてエラーを返す必要はありません。関数が実行された場合、そのトークンに`str`が含まれていることを確信できます。 インタラクティブなドキュメントですでに試すことができます: @@ -192,6 +198,6 @@ OpenAPIと統合するセキュリティユーティリティ (および自動AP まだトークンの有効性を検証しているわけではありませんが、これはもう始まっています。 -## まとめ +## まとめ { #recap } つまり、たった3~4行の追加で、すでに何らかの基礎的なセキュリティの形になっています。 diff --git a/docs/ja/docs/tutorial/security/get-current-user.md b/docs/ja/docs/tutorial/security/get-current-user.md index 9fc46c07c..39b97cca5 100644 --- a/docs/ja/docs/tutorial/security/get-current-user.md +++ b/docs/ja/docs/tutorial/security/get-current-user.md @@ -1,22 +1,22 @@ -# 現在のユーザーの取得 +# 現在のユーザーの取得 { #get-current-user } -一つ前の章では、(依存性注入システムに基づいた)セキュリティシステムは、 *path operation関数* に `str` として `token` を与えていました: +一つ前の章では、(依存性注入システムに基づいた)セキュリティシステムは、 *path operation 関数* に `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を別のどんなところでも使うことができます: -{* ../../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` を作ってみましょう。 @@ -24,21 +24,21 @@ `get_current_user` は前に作成した `oauth2_scheme` と同じ依存関係を持ちます。 -以前直接 *path operation* の中でしていたのと同じように、新しい依存関係である `get_current_user` は `str` として `token` を受け取るようになります: +以前直接 *path operation* の中でしていたのと同じように、新しい依存関係である `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` モデルを返却します: -{* ../../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` に対して同様に *path operation* の中で `Depends` を利用できます。 -{* ../../docs_src/security/tutorial002.py hl[31] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} Pydanticモデルの `User` として、 `current_user` の型を宣言することに注意してください。 @@ -54,15 +54,15 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ /// check | 確認 -依存関係システムがこのように設計されているおかげで、 `User` モデルを返却する別の依存関係(別の"dependables")を持つことができます。 +依存関係システムがこのように設計されているおかげで、 `User` モデルを返却する別の依存関係(別の「dependables」)を持つことができます。 同じデータ型を返却する依存関係は一つだけしか持てない、という制約が入ることはないのです。 /// -## 別のモデル +## 別のモデル { #other-models } -これで、*path operation関数* の中で現在のユーザーを直接取得し、`Depends` を使って、 **依存性注入** レベルでセキュリティメカニズムを処理できるようになりました。 +これで、*path operation 関数* の中で現在のユーザーを直接取得し、`Depends` を使って、 **依存性注入** レベルでセキュリティメカニズムを処理できるようになりました。 そして、セキュリティ要件のためにどんなモデルやデータでも利用することができます。(この場合は、 Pydanticモデルの `User`) @@ -76,10 +76,9 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ あなたのアプリケーションに必要なのがどんな種類のモデル、どんな種類のクラス、どんな種類のデータベースであったとしても、 **FastAPI** は依存性注入システムでカバーしてくれます。 +## コードサイズ { #code-size } -## コードサイズ - -この例は冗長に見えるかもしれません。セキュリティとデータモデルユーティリティ関数および *path operations* が同じファイルに混在しているということを覚えておいてください。 +この例は冗長に見えるかもしれません。セキュリティとデータモデルユーティリティ関数および *path operation* が同じファイルに混在しているということを覚えておいてください。 しかし、ここに重要なポイントがあります。 @@ -87,20 +86,20 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ そして、それは好きなだけ複雑にすることができます。それでも、一箇所に、一度だけ書くのです。すべての柔軟性を備えます。 -しかし、同じセキュリティシステムを使って何千ものエンドポイント(*path operations*)を持つことができます。 +しかし、同じセキュリティシステムを使って何千ものエンドポイント(*path operation*)を持つことができます。 そして、それらエンドポイントのすべて(必要な、どの部分でも)がこうした依存関係や、あなたが作成する別の依存関係を再利用する利点を享受できるのです。 -さらに、こうした何千もの *path operations* は、たった3行で表現できるのです: +さらに、こうした何千もの *path operation* は、たった3行で表現できるのです: -{* ../../docs_src/security/tutorial002.py hl[30:32] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} -## まとめ +## まとめ { #recap } -これで、 *path operation関数* の中で直接現在のユーザーを取得できるようになりました。 +これで、 *path operation 関数* の中で直接現在のユーザーを取得できるようになりました。 既に半分のところまで来ています。 -あとは、 `username` と `password` を実際にそのユーザーやクライアントに送る、 *path operation* を追加する必要があるだけです。 +あとは、ユーザー/クライアントが実際に `username` と `password` を送信するための *path operation* を追加する必要があるだけです。 次はそれを説明します。 diff --git a/docs/ja/docs/tutorial/security/oauth2-jwt.md b/docs/ja/docs/tutorial/security/oauth2-jwt.md index 599fc7b06..186936f1b 100644 --- a/docs/ja/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ja/docs/tutorial/security/oauth2-jwt.md @@ -1,4 +1,4 @@ -# パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer +# パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } これでセキュリティの流れが全てわかったので、JWTトークンと安全なパスワードのハッシュ化を使用して、実際にアプリケーションを安全にしてみましょう。 @@ -6,7 +6,7 @@ 本章では、前章の続きから始めて、コードをアップデートしていきます。 -## JWT について +## JWT について { #about-jwt } JWTとは「JSON Web Tokens」の略称です。 @@ -26,33 +26,31 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 JWT トークンを使って遊んでみたいという方は、https://jwt.io をチェックしてください。 -## `python-jose` のインストール +## `PyJWT` のインストール { #install-pyjwt } -PythonでJWTトークンの生成と検証を行うために、`python-jose`をインストールする必要があります: +PythonでJWTトークンの生成と検証を行うために、`PyJWT`をインストールする必要があります。 + +[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成し、アクティベートしてから、`pyjwt`をインストールしてください。
```console -$ pip install python-jose[cryptography] +$ pip install pyjwt ---> 100% ```
-また、Python-joseだけではなく、暗号を扱うためのパッケージを追加で必要とします。 +/// info | 情報 -ここでは、推奨されているものを使用します:pyca/cryptography。 +RSAやECDSAのようなデジタル署名アルゴリズムを使用する予定がある場合は、cryptographyライブラリの依存関係`pyjwt[crypto]`をインストールしてください。 -/// tip | 豆知識 - -このチュートリアルでは以前、PyJWTを使用していました。 - -しかし、Python-joseは、PyJWTのすべての機能に加えて、後に他のツールと統合して構築する際におそらく必要となる可能性のあるいくつかの追加機能を提供しています。そのため、代わりにPython-joseを使用するように更新されました。 +詳細はPyJWT Installation docsで確認できます。 /// -## パスワードのハッシュ化 +## パスワードのハッシュ化 { #password-hashing } 「ハッシュ化」とは、あるコンテンツ(ここではパスワード)を、規則性のないバイト列(単なる文字列)に変換することです。 @@ -60,26 +58,26 @@ $ pip install python-jose[cryptography] しかし、規則性のないバイト列から元のパスワードに戻すことはできません。 -### パスワードのハッシュ化を使う理由 +### パスワードのハッシュ化を使う理由 { #why-use-password-hashing } データベースが盗まれても、ユーザーの平文のパスワードは盗まれず、ハッシュ値だけが盗まれます。 したがって、泥棒はそのパスワードを別のシステムで使えません(多くのユーザーはどこでも同じパスワードを使用しているため、危険性があります)。 -## `passlib` のインストール +## `pwdlib` のインストール { #install-pwdlib } -PassLib は、パスワードのハッシュを処理するための優れたPythonパッケージです。 +pwdlib は、パスワードのハッシュを処理するための優れたPythonパッケージです。 このパッケージは、多くの安全なハッシュアルゴリズムとユーティリティをサポートします。 -推奨されるアルゴリズムは「Bcrypt」です。 +推奨されるアルゴリズムは「Argon2」です。 -そのため、Bcryptを指定してPassLibをインストールします: +[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成し、アクティベートしてから、Argon2付きでpwdlibをインストールしてください。
```console -$ pip install passlib[bcrypt] +$ pip install "pwdlib[argon2]" ---> 100% ``` @@ -88,7 +86,7 @@ $ pip install passlib[bcrypt] /// tip | 豆知識 -`passlib`を使用すると、**Django**や**Flask**のセキュリティプラグインなどで作成されたパスワードを読み取れるように設定できます。 +`pwdlib`を使用すると、**Django**や**Flask**のセキュリティプラグインなどで作成されたパスワードを読み取れるように設定できます。 例えば、Djangoアプリケーションからデータベース内の同じデータをFastAPIアプリケーションと共有できるだけではなく、同じデータベースを使用してDjangoアプリケーションを徐々に移行することもできます。 @@ -96,17 +94,17 @@ $ pip install passlib[bcrypt] /// -## パスワードのハッシュ化と検証 +## パスワードのハッシュ化と検証 { #hash-and-verify-the-passwords } -必要なツールを `passlib`からインポートします。 +必要なツールを `pwdlib`からインポートします。 -PassLib の「context」を作成します。これは、パスワードのハッシュ化と検証に使用されるものです。 +推奨設定でPasswordHashインスタンスを作成します。これは、パスワードのハッシュ化と検証に使用されます。 /// tip | 豆知識 -PassLibのcontextには、検証だけが許された非推奨の古いハッシュアルゴリズムを含む、様々なハッシュアルゴリズムを使用した検証機能もあります。 +pwdlibはbcryptハッシュアルゴリズムもサポートしていますが、レガシーアルゴリズムは含みません。古いハッシュを扱うには、passlibライブラリを使用することが推奨されます。 -例えば、この機能を使用して、別のシステム(Djangoなど)によって生成されたパスワードを読み取って検証し、Bcryptなどの別のアルゴリズムを使用して新しいパスワードをハッシュするといったことができます。 +例えば、この機能を使用して、別のシステム(Djangoなど)によって生成されたパスワードを読み取って検証し、Argon2やBcryptなどの別のアルゴリズムを使用して新しいパスワードをハッシュするといったことができます。 そして、同時にそれらはすべてに互換性があります。 @@ -118,15 +116,15 @@ PassLibのcontextには、検証だけが許された非推奨の古いハッシ さらに、ユーザーを認証して返す関数も作成します。 -{* ../../docs_src/security/tutorial004.py hl[7,48,55:56,59:60,69:75] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} /// 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 } インストールした複数のモジュールをインポートします。 @@ -148,33 +146,33 @@ $ openssl rand -hex 32 JWTトークンの署名に使用するアルゴリズム`"HS256"`を指定した変数`ALGORITHM`を作成します。 -トークンの有効期限を指定した変数`ACCESS_TOKEN_EXPIRE_MINUTES`を作成します。 +トークンの有効期限を指定した変数を作成します。 レスポンスのトークンエンドポイントで使用するPydanticモデルを定義します。 新しいアクセストークンを生成するユーティリティ関数を作成します。 -{* ../../docs_src/security/tutorial004.py hl[6,12:14,28:30,78:86] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## 依存関係の更新 +## 依存関係の更新 { #update-the-dependencies } `get_current_user`を更新して、先ほどと同じトークンを受け取るようにしますが、今回はJWTトークンを使用します。 -受け取ったトークンを復号して検証し、現在のユーザーを返します。 +受け取ったトークンをデコードして検証し、現在のユーザーを返します。 トークンが無効な場合は、すぐにHTTPエラーを返します。 -{* ../../docs_src/security/tutorial004.py hl[89:106] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## `/token` パスオペレーションの更新 +## `/token` *path operation* の更新 { #update-the-token-path-operation } トークンの有効期限を表す`timedelta`を作成します。 -JWTアクセストークンを作成し、それを返します。 +実際のJWTアクセストークンを作成し、それを返します。 -{* ../../docs_src/security/tutorial004.py hl[115:130] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### JWTの"subject" `sub` についての技術的な詳細 +### JWTの「subject」`sub` についての技術的な詳細 { #technical-details-about-the-jwt-subject-sub } JWTの仕様では、トークンのsubjectを表すキー`sub`があるとされています。 @@ -192,13 +190,13 @@ JWTは、ユーザーを識別して、そのユーザーがAPI上で直接操 しかしながら、それらのエンティティのいくつかが同じIDを持つ可能性があります。例えば、`foo`(ユーザー`foo`、車 `foo`、ブログ投稿`foo`)などです。 -IDの衝突を回避するために、ユーザーのJWTトークンを作成するとき、subキーの値にプレフィックスを付けることができます(例えば、`username:`)。したがって、この例では、`sub`の値は次のようになっている可能性があります:`username:johndoe` +IDの衝突を回避するために、ユーザーのJWTトークンを作成するとき、subキーの値にプレフィックスを付けることができます(例えば、`username:`)。したがって、この例では、`sub`の値は次のようになっている可能性があります:`username:johndoe`。 覚えておくべき重要なことは、`sub`キーはアプリケーション全体で一意の識別子を持ち、文字列である必要があるということです。 -## 確認 +## 確認 { #check-it } -サーバーを実行し、ドキュメントに移動します:http://127.0.0.1:8000/docs +サーバーを実行し、ドキュメントに移動します:http://127.0.0.1:8000/docs。 次のようなユーザーインターフェイスが表示されます: @@ -232,17 +230,17 @@ Password: `secret` -開発者ツールを開くと、送信されるデータにはトークンだけが含まれており、パスワードはユーザーを認証してアクセストークンを取得する最初のリクエストでのみ送信され、その後は送信されないことがわかります。 +開発者ツールを開くと、送信されるデータにはトークンだけが含まれており、パスワードはユーザーを認証してアクセストークンを取得する最初のリクエストでのみ送信され、その後は送信されないことがわかります: /// note | 備考 -ヘッダーの`Authorization`には、`Bearer`で始まる値があります。 +ヘッダーの`Authorization`には、`Bearer `で始まる値があります。 /// -## `scopes` を使った高度なユースケース +## `scopes` を使った高度なユースケース { #advanced-usage-with-scopes } OAuth2には、「スコープ」という概念があります。 @@ -252,7 +250,7 @@ OAuth2には、「スコープ」という概念があります。 これらの使用方法や**FastAPI**への統合方法については、**高度なユーザーガイド**で後ほど説明します。 -## まとめ +## まとめ { #recap } ここまでの説明で、OAuth2やJWTなどの規格を使った安全な**FastAPI**アプリケーションを設定することができます。 @@ -266,7 +264,7 @@ OAuth2には、「スコープ」という概念があります。 そのため、プロジェクトに合わせて自由に選択することができます。 -また、**FastAPI**は外部パッケージを統合するために複雑な仕組みを必要としないため、`passlib`や`python-jose`のようなよく整備され広く使われている多くのパッケージを直接使用することができます。 +また、**FastAPI**は外部パッケージを統合するために複雑な仕組みを必要としないため、`pwdlib`や`PyJWT`のようなよく整備され広く使われている多くのパッケージを直接使用することができます。 しかし、柔軟性、堅牢性、セキュリティを損なうことなく、可能な限りプロセスを簡素化するためのツールを提供します。 diff --git a/docs/ja/docs/tutorial/static-files.md b/docs/ja/docs/tutorial/static-files.md index f910d7e36..c79789494 100644 --- a/docs/ja/docs/tutorial/static-files.md +++ b/docs/ja/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,15 +17,15 @@ /// -### 「マウント」とは +### 「マウント」とは { #what-is-mounting } 「マウント」とは、特定のパスに完全な「独立した」アプリケーションを追加することを意味します。これにより、すべてのサブパスの処理がなされます。 これは、マウントされたアプリケーションが完全に独立しているため、`APIRouter` とは異なります。メインアプリケーションのOpenAPIとドキュメントには、マウントされたアプリケーションの内容などは含まれません。 -これについて詳しくは、**高度なユーザーガイド** をご覧ください。 +これについて詳しくは、[高度なユーザーガイド](../advanced/index.md){.internal-link target=_blank} をご覧ください。 -## 詳細 +## 詳細 { #details } 最初の `"/static"` は、この「サブアプリケーション」が「マウント」されるサブパスを指します。したがって、`"/static"` から始まるパスはすべてサブアプリケーションによって処理されます。 @@ -33,8 +33,8 @@ `name="static"` は、**FastAPI** が内部で使用できる名前を付けます。 -これらのパラメータはすべて「`静的`」とは異なる場合があり、独自のアプリケーションのニーズと詳細に合わせて調整します。 +これらのパラメータはすべて「`static`」とは異なる場合があり、独自のアプリケーションのニーズと詳細に合わせて調整します。 -## より詳しい情報 +## より詳しい情報 { #more-info } 詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 4e8ad4f7c..0ec6250f3 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -1,12 +1,24 @@ -# テスト +# テスト { #testing } Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 -HTTPX がベースなので、非常に使いやすく直感的です。 +HTTPX がベースで、さらにその設計は Requests をベースにしているため、とても馴染みがあり直感的です。 これを使用すると、**FastAPI** と共に pytest を直接利用できます。 -## `TestClient` を使用 +## `TestClient` を使用 { #using-testclient } + +/// info | 情報 + +`TestClient` を使用するには、まず `httpx` をインストールします。 + +[仮想環境](../virtual-environments.md){.internal-link target=_blank} を作成し、それを有効化してから、例えば以下のようにインストールしてください: + +```console +$ pip install httpx +``` + +/// `TestClient` をインポートします。 @@ -16,9 +28,9 @@ `httpx` と同じ様に `TestClient` オブジェクトを使用します。 -チェックしたい Python の標準的な式と共に、シンプルに `assert` 文を記述します。 +チェックしたい Python の標準的な式と共に、シンプルに `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 | 豆知識 @@ -44,48 +56,81 @@ FastAPIアプリケーションへのリクエストの送信とは別に、テ /// -## テストの分離 +## テストの分離 { #separating-tests } 実際のアプリケーションでは、おそらくテストを別のファイルに保存します。 また、**FastAPI** アプリケーションは、複数のファイル/モジュールなどで構成されている場合もあります。 -### **FastAPI** アプリファイル +### **FastAPI** アプリファイル { #fastapi-app-file } -**FastAPI** アプリに `main.py` ファイルがあるとします: +[Bigger Applications](bigger-applications.md){.internal-link target=_blank} で説明されている、次のようなファイル構成があるとします: -{* ../../docs_src/app_testing/main.py *} +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` -### テストファイル +ファイル `main.py` に **FastAPI** アプリがあります: -次に、テストを含む `test_main.py` ファイルを作成し、`main` モジュール (`main.py`) から `app` をインポートします: -{* ../../docs_src/app_testing/test_main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} -## テスト: 例の拡張 +### テストファイル { #testing-file } + +次に、テストを含む `test_main.py` ファイルを用意できます。これは同じ Python パッケージ (`__init__.py` ファイルがある同じディレクトリ) に置けます: + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +このファイルは同じパッケージ内にあるため、相対インポートを使って `main` モジュール (`main.py`) からオブジェクト `app` をインポートできます: + +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} + + +...そして、これまでと同じようにテストコードを書けます。 + +## テスト: 例の拡張 { #testing-extended-example } 次に、この例を拡張し、詳細を追加して、さまざまなパーツをテストする方法を確認しましょう。 +### 拡張版 **FastAPI** アプリファイル { #extended-fastapi-app-file } -### 拡張版 **FastAPI** アプリファイル +先ほどと同じファイル構成で続けます: -**FastAPI** アプリに `main_b.py` ファイルがあるとします。 +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` -そのファイルには、エラーを返す可能性のある `GET` オペレーションがあります。 +ここで、**FastAPI** アプリがある `main.py` ファイルには、他の path operation があります。 -また、いくつかのエラーを返す可能性のある `POST` オペレーションもあります。 +エラーを返す可能性のある `GET` オペレーションがあります。 -これらの *path operation* には `X-Token` ヘッダーが必要です。 +いくつかのエラーを返す可能性のある `POST` オペレーションもあります。 -{* ../../docs_src/app_testing/app_b_py310/main.py *} +両方の *path operation* には `X-Token` ヘッダーが必要です。 -### 拡張版テストファイル +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -次に、先程のものに拡張版のテストを加えた、`test_main_b.py` を作成します。 +### 拡張版テストファイル { #extended-testing-file } -{* ../../docs_src/app_testing/app_b/test_main.py *} +次に、拡張版のテストで `test_main.py` を更新できます: -リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、`httpx` での実現方法を検索 (Google) できます。 +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} + + +リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、`httpx` での実現方法、あるいは HTTPX の設計が Requests の設計をベースにしているため `requests` での実現方法を検索 (Google) できます。 テストでも同じことを行います。 @@ -107,9 +152,11 @@ FastAPIアプリケーションへのリクエストの送信とは別に、テ /// -## 実行 +## 実行 { #run-it } -後は、`pytest` をインストールするだけです: +その後、`pytest` をインストールするだけです。 + +[仮想環境](../virtual-environments.md){.internal-link target=_blank} を作成し、それを有効化してから、例えば以下のようにインストールしてください:
@@ -121,7 +168,7 @@ $ pip install pytest
-ファイルを検知し、自動テストを実行し、結果のレポートを返します。 +ファイルとテストを自動的に検出し、実行して、結果のレポートを返します。 以下でテストを実行します: diff --git a/docs/ja/docs/virtual-environments.md b/docs/ja/docs/virtual-environments.md index 791cf64a8..672323063 100644 --- a/docs/ja/docs/virtual-environments.md +++ b/docs/ja/docs/virtual-environments.md @@ -1,10 +1,10 @@ -# 仮想環境 +# 仮想環境 { #virtual-environments } Pythonプロジェクトの作業では、**仮想環境**(または類似の仕組み)を使用し、プロジェクトごとにインストールするパッケージを分離するべきでしょう。 /// info | 情報 -もし、仮想環境の概要や作成方法、使用方法について既にご存知なら、このセクションをスキップすることができます。🤓 +もし、仮想環境の概要や作成方法、使用方法について既にご存知なら、このセクションをスキップした方がよいかもしれません。🤓 /// @@ -25,7 +25,7 @@ Pythonプロジェクトの作業では、**仮想環境**(または類似の /// -## プロジェクトの作成 +## プロジェクトの作成 { #create-a-project } まず、プロジェクト用のディレクトリを作成します。 @@ -48,9 +48,9 @@ $ cd awesome-project
-## 仮想環境の作成 +## 仮想環境の作成 { #create-a-virtual-environment } -Pythonプロジェクトでの**初めての**作業を開始する際には、**プロジェクト内**に仮想環境を作成してください。 +Pythonプロジェクトでの**初めての**作業を開始する際には、**プロジェクト内**に仮想環境を作成してください。 /// tip | 豆知識 @@ -72,10 +72,10 @@ $ python -m venv .venv /// details | このコマンドの意味 -- `python` : `python` というプログラムを呼び出します -- `-m` : モジュールをスクリプトとして呼び出します。どのモジュールを呼び出すのか、この次に指定します -- `venv` : 通常Pythonに付随してインストールされる `venv`モジュールを使用します -- `.venv` : 仮想環境を`.venv`という新しいディレクトリに作成します +* `python`: `python` というプログラムを呼び出します +* `-m`: モジュールをスクリプトとして呼び出します。どのモジュールを呼び出すのか、この次に指定します +* `venv`: 通常Pythonに付随してインストールされる `venv`モジュールを使用します +* `.venv`: 仮想環境を`.venv`という新しいディレクトリに作成します /// @@ -111,13 +111,13 @@ $ uv venv /// -## 仮想環境の有効化 +## 仮想環境の有効化 { #activate-the-virtual-environment } 実行されるPythonコマンドやインストールされるパッケージが新しく作成した仮想環境を使用するよう、その仮想環境を有効化しましょう。 /// tip | 豆知識 -そのプロジェクトの作業で**新しいターミナルセッション**を開始する際には、**毎回**有効化が必要です。 +そのプロジェクトの作業で**新しいターミナルセッション**を開始する際には、**毎回**有効化してください。 /// @@ -161,13 +161,13 @@ $ source .venv/Scripts/activate /// tip | 豆知識 -**新しいパッケージ**を仮想環境にインストールするときには、再度**有効化**してください。 +**新しいパッケージ**を仮想環境にインストールするたびに、環境をもう一度**有効化**してください。 こうすることで、そのパッケージがインストールした**ターミナル(CLI)プログラム**を使用する場合に、仮想環境内のものが確実に使われ、グローバル環境にインストールされている別のもの(おそらく必要なものとは異なるバージョン)を誤って使用することを防ぎます。 /// -## 仮想環境が有効であることを確認する +## 仮想環境が有効であることを確認する { #check-the-virtual-environment-is-active } 仮想環境が有効である(前のコマンドが正常に機能した)ことを確認します。 @@ -197,7 +197,7 @@ $ which python
-``` console +```console $ Get-Command python C:\Users\user\code\awesome-project\.venv\Scripts\python @@ -209,7 +209,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -## `pip` をアップグレードする +## `pip` をアップグレードする { #upgrade-pip } /// tip | 豆知識 @@ -239,7 +239,27 @@ $ python -m pip install --upgrade pip
-## `.gitignore` を追加する +/// tip | 豆知識 + +ときどき、pip をアップグレードしようとすると **`No module named pip`** エラーが表示されることがあります。 + +その場合は、以下のコマンドで pip をインストールしてアップグレードしてください: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +このコマンドは、pip がまだインストールされていなければ pip をインストールし、また、インストールされる pip のバージョンが `ensurepip` で利用可能なもの以上に新しいことも保証します。 + +/// + +## `.gitignore` を追加する { #add-gitignore } **Git**を使用している場合(使用するべきでしょう)、 `.gitignore` ファイルを追加して、 `.venv` 内のあらゆるファイルをGitの管理対象から除外します。 @@ -265,9 +285,9 @@ $ echo "*" > .venv/.gitignore /// details | このコマンドの意味 -- `echo "*"` : ターミナルに `*` というテキストを「表示」しようとします。(次の部分によってその動作が少し変わります) -- `>` : `>` の左側のコマンドがターミナルに表示しようとする内容を、ターミナルには表示せず、 `>` の右側のファイルに書き込みます。 -- `.gitignore` : `*` を書き込むファイル名。 +* `echo "*"`: ターミナルに `*` というテキストを「表示」しようとします。(次の部分によってその動作が少し変わります) +* `>`: `>` の左側のコマンドがターミナルに表示しようとする内容を、ターミナルには表示せず、 `>` の右側のファイルに書き込みます。 +* `.gitignore`: `*` を書き込むファイル名。 ここで、Gitにおける `*` は「すべて」を意味するので、このコマンドによって `.venv` ディレクトリ内のすべてがGitに無視されるようになります。 @@ -279,7 +299,7 @@ $ echo "*" > .venv/.gitignore /// -## パッケージのインストール +## パッケージのインストール { #install-packages } 仮想環境を有効化した後、その中でパッケージをインストールできます。 @@ -291,7 +311,7 @@ $ echo "*" > .venv/.gitignore /// -### パッケージを直接インストールする +### パッケージを直接インストールする { #install-packages-directly } 急いでいて、プロジェクトのパッケージ要件を宣言するファイルを使いたくない場合、パッケージを直接インストールできます。 @@ -330,7 +350,7 @@ $ uv pip install "fastapi[standard]" //// -### `requirements.txt` からインストールする +### `requirements.txt` からインストールする { #install-from-requirements-txt } もし `requirements.txt` があるなら、パッケージのインストールに使用できます。 @@ -373,7 +393,7 @@ pydantic==2.8.0 /// -## プログラムを実行する +## プログラムを実行する { #run-your-program } 仮想環境を有効化した後、プログラムを実行できます。この際、仮想環境内のPythonと、そこにインストールしたパッケージが使用されます。 @@ -387,7 +407,7 @@ Hello World -## エディタの設定 +## エディタの設定 { #configure-your-editor } プロジェクトではおそらくエディタを使用するでしょう。コード補完やインラインエラーの表示ができるように、作成した仮想環境をエディタでも使えるよう設定してください。(多くの場合、自動検出されます) @@ -402,7 +422,7 @@ Hello World /// -## 仮想環境の無効化 +## 仮想環境の無効化 { #deactivate-the-virtual-environment } プロジェクトの作業が終了したら、その仮想環境を**無効化**できます。 @@ -416,9 +436,11 @@ $ deactivate これにより、 `python` コマンドを実行しても、そのプロジェクト用(のパッケージがインストールされた)仮想環境から `python` プログラムを呼び出そうとはしなくなります。 -## 作業準備完了 +## 作業準備完了 { #ready-to-work } + +これで、プロジェクトの作業を始める準備が整いました。 + -ここまでで、プロジェクトの作業を始める準備が整いました。 /// tip | 豆知識 @@ -428,9 +450,9 @@ $ deactivate /// -## なぜ仮想環境? +## なぜ仮想環境? { #why-virtual-environments } -FastAPIを使った作業をするには、 [Python](https://www.python.org/) のインストールが必要です。 +FastAPIを使った作業をするには、Python のインストールが必要です。 それから、FastAPIや、使用したいその他の**パッケージ**を**インストール**する必要があります。 @@ -438,7 +460,7 @@ FastAPIを使った作業をするには、 [Python](https://www.python.org/) ただし、`pip` を直接使用すると、パッケージは**グローバルなPython環境**(OS全体にインストールされたPython環境)にインストールされます。 -### 問題点 +### 問題点 { #the-problem } では、グローバルPython環境にパッケージをインストールすることの問題点は何でしょうか? @@ -521,7 +543,7 @@ Pythonのパッケージでは、**新しいバージョン**で**互換性を また、使用しているOS(Linux、Windows、macOS など)によっては、Pythonがすでにインストールされていることがあります。この場合、特定のバージョンのパッケージが**OSの動作に必要である**ことがあります。グローバル環境にパッケージをインストールすると、OSに付属するプログラムを**壊してしまう**可能性があります。 -## パッケージのインストール先 +## パッケージのインストール先 { #where-are-packages-installed } Pythonをインストールしたとき、ファイルを含んだいくつかのディレクトリが作成されます。 @@ -539,7 +561,7 @@ $ pip install "fastapi[standard]" -FastAPIのコードを含む圧縮ファイルが、通常は [PyPI](https://pypi.org/project/fastapi/) からダウンロードされます。 +FastAPIのコードを含む圧縮ファイルが、通常は PyPI からダウンロードされます。 また、FastAPIが依存する他のパッケージも**ダウンロード**されます。 @@ -547,7 +569,7 @@ FastAPIのコードを含む圧縮ファイルが、通常は [PyPI](https://pyp デフォルトでは、これらのファイルはPythonのインストール時に作成されるディレクトリ、つまり**グローバル環境**に配置されます。 -## 仮想環境とは +## 仮想環境とは { #what-are-virtual-environments } すべてのパッケージをグローバル環境に配置することによって生じる問題の解決策は、作業する**プロジェクトごとの仮想環境**を使用することです。 @@ -572,7 +594,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 仮想環境の有効化とは +## 仮想環境の有効化とは { #what-does-activating-a-virtual-environment-mean } 仮想環境を有効にしたとき、例えば次のコマンドを実行した場合を考えます: @@ -620,7 +642,7 @@ $ source .venv/Scripts/activate /// tip | 豆知識 -`PATH` 変数についての詳細は [環境変数](environment-variables.md#path環境変数){.internal-link target=_blank} を参照してください。 +`PATH` 変数についての詳細は [環境変数](environment-variables.md#path-environment-variable){.internal-link target=_blank} を参照してください。 /// @@ -701,7 +723,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 仮想環境を有効にして変更されることは他にもありますが、これが最も重要な変更のひとつです。 -## 仮想環境の確認 +## 仮想環境の確認 { #checking-a-virtual-environment } 仮想環境が有効かどうか、例えば次のように確認できます。: @@ -753,7 +775,7 @@ LinuxやmacOSでは `which` を、Windows PowerShellでは `Get-Command` を使 /// -## なぜ仮想環境を無効化するのか +## なぜ仮想環境を無効化するのか { #why-deactivate-a-virtual-environment } 例えば、`philosophers-stone` (賢者の石)というプロジェクトで作業をしていて、**その仮想環境を有効にし**、必要なパッケージをインストールしてその環境内で作業を進めているとします。 @@ -786,7 +808,7 @@ Traceback (most recent call last): -しかし、その仮想環境を無効化し、 `prisoner-of-azkaban` (アズカバンの囚人)のための新しい仮想環境を有効にすれば、 `python` を実行したときに `prisoner-of-azkaban` (アズカバンの囚人)の仮想環境の Python が使用されるようになります。 +しかし、その仮想環境を無効化し、 `prisoner-of-askaban` のための新しい仮想環境を有効にすれば、 `python` を実行したときに `prisoner-of-azkaban` (アズカバンの囚人)の仮想環境の Python が使用されるようになります。
@@ -807,7 +829,7 @@ I solemnly swear 🐺
-## 代替手段 +## 代替手段 { #alternatives } これは、あらゆる仕組みを**根本から**学ぶためのシンプルな入門ガイドです。 @@ -824,7 +846,7 @@ I solemnly swear 🐺 * パッケージとそのバージョンの、依存関係を含めた**厳密な**組み合わせを保持し、これによって、本番環境で、開発環境と全く同じようにプロジェクトを実行できる(これは**locking**と呼ばれます) * その他のさまざまな機能 -## まとめ +## まとめ { #conclusion } ここまで読みすべて理解したなら、世間の多くの開発者と比べて、仮想環境について**あなたはより多くのことを知っています**。🤓 diff --git a/docs/ja/llm-prompt.md b/docs/ja/llm-prompt.md index 18909cd59..de2616746 100644 --- a/docs/ja/llm-prompt.md +++ b/docs/ja/llm-prompt.md @@ -30,8 +30,7 @@ Use the following preferred translations when they apply in documentation prose: - request (HTTP): リクエスト - response (HTTP): レスポンス -- path operation: パスオペレーション -- path operation function: パスオペレーション関数 +- path operation: path operation (do not translate) ### `///` admonitions diff --git a/docs/ko/docs/_llm-test.md b/docs/ko/docs/_llm-test.md index 1b828c663..272af763a 100644 --- a/docs/ko/docs/_llm-test.md +++ b/docs/ko/docs/_llm-test.md @@ -132,7 +132,7 @@ works(foo="bar") # 이건 동작합니다 🎉 일부 텍스트 /// -/// note Technical details | 기술 세부사항 +/// note | 기술 세부사항 일부 텍스트 /// @@ -202,11 +202,6 @@ works(foo="bar") # 이건 동작합니다 🎉 * XWT * PSGI -### abbr가 설명을 제공 { #the-abbr-gives-an-explanation } - -* cluster -* Deep Learning - ### abbr가 전체 문구와 설명을 제공 { #the-abbr-gives-a-full-phrase-and-an-explanation } * MDN @@ -224,6 +219,11 @@ works(foo="bar") # 이건 동작합니다 🎉 //// +## HTML "dfn" 요소 { #html-dfn-elements } + +* 클러스터 +* 딥 러닝 + ## 제목 { #headings } //// tab | 테스트 diff --git a/docs/ko/docs/advanced/advanced-dependencies.md b/docs/ko/docs/advanced/advanced-dependencies.md index 04e557d15..fe1606258 100644 --- a/docs/ko/docs/advanced/advanced-dependencies.md +++ b/docs/ko/docs/advanced/advanced-dependencies.md @@ -79,13 +79,13 @@ checker(q="somequery") ### `yield`와 `scope`가 있는 의존성 { #dependencies-with-yield-and-scope } -0.121.0 버전에서 FastAPI는 `yield`가 있는 의존성에 대해 `Depends(scope="function")` 지원을 추가했습니다. +0.121.0 버전에서 FastAPI는 `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`가 있는 의존성 - 조기 종료와 `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) 문서를 참고하세요. ### `yield`가 있는 의존성과 `StreamingResponse`, 기술 세부사항 { #dependencies-with-yield-and-streamingresponse-technical-details } @@ -133,7 +133,7 @@ SQLModel(또는 SQLAlchemy)을 사용하면서 이런 특정 사용 사례가 그러면 세션이 데이터베이스 연결을 해제하여, 다른 요청들이 이를 사용할 수 있게 됩니다. -`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 GitHub Discussion Question을 생성해 주세요. +`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 GitHub Discussions 질문을 생성해 주세요. `yield`가 있는 의존성에서 조기 종료에 대한 설득력 있는 사용 사례가 있다면, 조기 종료를 선택적으로 활성화할 수 있는 새로운 방법을 추가하는 것을 고려하겠습니다. @@ -145,7 +145,7 @@ FastAPI 0.110.0 이전에는 `yield`가 있는 의존성을 사용한 다음 그 ### 백그라운드 태스크와 `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}가 이미 실행된 뒤였습니다. +FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성의 종료 코드는 응답이 전송된 *후에* 실행되었기 때문에, [예외 핸들러](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 뒤였습니다. 이는 주로 백그라운드 태스크 안에서 의존성이 "yield"한 동일한 객체들을 사용할 수 있게 하기 위한 설계였습니다. 백그라운드 태스크가 끝난 뒤에 종료 코드가 실행되었기 때문입니다. diff --git a/docs/ko/docs/advanced/wsgi.md b/docs/ko/docs/advanced/wsgi.md index 89cf57cfe..5e0e87c5e 100644 --- a/docs/ko/docs/advanced/wsgi.md +++ b/docs/ko/docs/advanced/wsgi.md @@ -1,32 +1,48 @@ -# WSGI 포함하기 - Flask, Django 그 외 { #including-wsgi-flask-django-others } +# 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` 사용하기 { #using-wsgimiddleware } -`WSGIMiddleware`를 불러와야 합니다. +/// info | 정보 -그런 다음, WSGI(예: Flask) 응용 프로그램을 미들웨어로 포장합니다. +이를 사용하려면 `a2wsgi`를 설치해야 합니다. 예: `pip install a2wsgi` -그 후, 해당 경로에 마운트합니다. +/// -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +`a2wsgi`에서 `WSGIMiddleware`를 import 해야 합니다. + +그런 다음, WSGI(예: Flask) 애플리케이션을 미들웨어로 감쌉니다. + +그리고 해당 경로에 마운트합니다. + +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | 참고 + +이전에 `fastapi.middleware.wsgi`의 `WSGIMiddleware` 사용을 권장했지만 이제는 더 이상 권장되지 않습니다. + +대신 `a2wsgi` 패키지 사용을 권장합니다. 사용 방법은 동일합니다. + +단, `a2wsgi` 패키지가 설치되어 있고 `a2wsgi`에서 `WSGIMiddleware`를 올바르게 import 하는지만 확인하세요. + +/// ## 확인하기 { #check-it } -이제 `/v1/` 경로에 있는 모든 요청은 Flask 응용 프로그램에서 처리됩니다. +이제 `/v1/` 경로에 있는 모든 요청은 Flask 애플리케이션에서 처리됩니다. 그리고 나머지는 **FastAPI**에 의해 처리됩니다. -실행하면 http://localhost:8000/v1/으로 이동해서 Flask의 응답을 볼 수 있습니다: +실행하고 http://localhost:8000/v1/로 이동하면 Flask의 응답을 볼 수 있습니다: ```txt Hello, World from Flask! ``` -그리고 다음으로 이동하면 http://localhost:8000/v2 **FastAPI**의 응답을 볼 수 있습니다: +그리고 http://localhost:8000/v2로 이동하면 **FastAPI**의 응답을 볼 수 있습니다: ```JSON { diff --git a/docs/ko/docs/deployment/docker.md b/docs/ko/docs/deployment/docker.md index be04c923a..20e341c26 100644 --- a/docs/ko/docs/deployment/docker.md +++ b/docs/ko/docs/deployment/docker.md @@ -145,8 +145,6 @@ Successfully installed fastapi pydantic * 다음 내용으로 `main.py` 파일을 만듭니다: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -245,14 +243,14 @@ Docker 지시어 cluster를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. +**Kubernetes**, Docker Swarm Mode, Nomad 등의 복잡한 시스템으로 여러 머신에 분산된 컨테이너를 관리하는 클러스터를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. Kubernetes 같은 분산 컨테이너 관리 시스템은 보통 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서도, **컨테이너 복제**를 처리하는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서요. @@ -580,7 +578,7 @@ Kubernetes를 사용한다면, 이는 아마도 자동완성. 적은 디버깅 시간. +* **직관적**: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. * **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. @@ -127,7 +127,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 -웹 API 대신 터미널에서 사용할 CLI 앱을 만들고 있다면, **Typer**를 확인해 보십시오. +웹 API 대신 터미널에서 사용할 CLI 앱을 만들고 있다면, **Typer**를 확인해 보십시오. **Typer**는 FastAPI의 동생입니다. 그리고 **CLI를 위한 FastAPI**가 되기 위해 생겼습니다. ⌨️ 🚀 @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" 다음 내용으로 `main.py` 파일을 만드십시오: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): 여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Pydantic 덕분에 표준 Python 타입을 사용해 본문을 선언합니다. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -374,7 +368,7 @@ item: Item * 데이터 검증: * 데이터가 유효하지 않을 때 자동으로 생성하는 명확한 에러. * 깊이 중첩된 JSON 객체에 대한 유효성 검사. -* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: +* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: * JSON. * 경로 매개변수. * 쿼리 매개변수. @@ -382,7 +376,7 @@ item: Item * 헤더. * 폼(Forms). * 파일. -* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): +* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): * 파이썬 타입 변환 (`str`, `int`, `float`, `bool`, `list`, 등). * `datetime` 객체. * `UUID` 객체. @@ -445,7 +439,7 @@ item: Item * 서로 다른 장소에서 **매개변수** 선언: **헤더**, **쿠키**, **폼 필드** 그리고 **파일**. * `maximum_length` 또는 `regex`처럼 **유효성 제약**하는 방법. -* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. +* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. * **OAuth2** 지원을 포함한 **JWT tokens** 및 **HTTP Basic**을 갖는 보안과 인증. * (Pydantic 덕분에) **깊은 중첩 JSON 모델**을 선언하는데 더 진보한 (하지만 마찬가지로 쉬운) 기술. * Strawberry 및 기타 라이브러리와의 **GraphQL** 통합. diff --git a/docs/ko/docs/tutorial/body-multiple-params.md b/docs/ko/docs/tutorial/body-multiple-params.md index 701351e63..bebdffed8 100644 --- a/docs/ko/docs/tutorial/body-multiple-params.md +++ b/docs/ko/docs/tutorial/body-multiple-params.md @@ -102,15 +102,16 @@ 기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요 없이 이렇게 하면 됩니다: +```Python +q: str | None = None +``` + +또는 Python 3.9에서는: + ```Python q: Union[str, None] = None ``` -또는 Python 3.10 이상에서는: - -```Python -q: str | None = None -``` 예를 들어: diff --git a/docs/ko/docs/tutorial/path-operation-configuration.md b/docs/ko/docs/tutorial/path-operation-configuration.md index b8a87667a..baef3fb40 100644 --- a/docs/ko/docs/tutorial/path-operation-configuration.md +++ b/docs/ko/docs/tutorial/path-operation-configuration.md @@ -52,11 +52,11 @@ `summary`와 `description`을 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## 독스트링으로 만든 설명 { #description-from-docstring } -설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 docstring에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. +설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 docstring에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. 독스트링에는 Markdown을 작성할 수 있으며, (독스트링의 들여쓰기를 고려하여) 올바르게 해석되고 표시됩니다. @@ -70,7 +70,7 @@ `response_description` 매개변수로 응답에 관한 설명을 명시할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | 정보 @@ -90,7 +90,7 @@ OpenAPI는 각 *경로 처리*가 응답에 관한 설명을 요구할 것을 ## *경로 처리* 지원중단하기 { #deprecate-a-path-operation } -*경로 처리*를 제거하지 않고 deprecated로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: +*경로 처리*를 제거하지 않고 deprecated로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: {* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} diff --git a/docs/pt/docs/_llm-test.md b/docs/pt/docs/_llm-test.md index b59292f47..3a2801fdd 100644 --- a/docs/pt/docs/_llm-test.md +++ b/docs/pt/docs/_llm-test.md @@ -197,15 +197,10 @@ Aqui estão algumas coisas envolvidas em elementos HTML "abbr" (algumas são inv ### 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 +* GTD +* lt +* XWT +* PSGI ### O abbr fornece uma frase completa e uma explicação { #the-abbr-gives-a-full-phrase-and-an-explanation } @@ -224,6 +219,11 @@ Veja a seção `### HTML abbr elements` no prompt geral em `scripts/translate.py //// +## Elementos HTML "dfn" { #html-dfn-elements } + +* cluster +* Deep Learning + ## Títulos { #headings } //// tab | Teste diff --git a/docs/pt/docs/advanced/advanced-dependencies.md b/docs/pt/docs/advanced/advanced-dependencies.md index 1ad9ea617..959a7f3c2 100644 --- a/docs/pt/docs/advanced/advanced-dependencies.md +++ b/docs/pt/docs/advanced/advanced-dependencies.md @@ -148,7 +148,7 @@ Antes do FastAPI 0.106.0, lançar exceções após o `yield` não era possível, 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. +Isso foi alterado no FastAPI 0.106.0 com a intenção de não manter recursos enquanto se espera a response percorrer a rede. /// tip | Dica diff --git a/docs/pt/docs/advanced/wsgi.md b/docs/pt/docs/advanced/wsgi.md index c3c21d430..99b783cdb 100644 --- a/docs/pt/docs/advanced/wsgi.md +++ b/docs/pt/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ Para isso, você pode utilizar o `WSGIMiddleware` para encapsular a sua aplicaç ## Usando `WSGIMiddleware` { #using-wsgimiddleware } -Você precisa importar o `WSGIMiddleware`. +/// info | Informação + +Isso requer instalar `a2wsgi`, por exemplo com `pip install a2wsgi`. + +/// + +Você precisa importar o `WSGIMiddleware` de `a2wsgi`. Em seguida, encapsule a aplicação WSGI (e.g. Flask) com o middleware. E então monte isso sob um path. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Nota + +Anteriormente, recomendava-se usar `WSGIMiddleware` de `fastapi.middleware.wsgi`, mas agora está descontinuado. + +É aconselhável usar o pacote `a2wsgi` em seu lugar. O uso permanece o mesmo. + +Apenas certifique-se de que o pacote `a2wsgi` está instalado e importe `WSGIMiddleware` corretamente de `a2wsgi`. + +/// ## Confira { #check-it } diff --git a/docs/pt/docs/deployment/docker.md b/docs/pt/docs/deployment/docker.md index b26a69b54..d47a15394 100644 --- a/docs/pt/docs/deployment/docker.md +++ b/docs/pt/docs/deployment/docker.md @@ -145,8 +145,6 @@ Há outros formatos e ferramentas para definir e instalar dependências de pacot * Crie um arquivo `main.py` com: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -572,7 +570,7 @@ Se você tiver uma configuração simples, com um **único contêiner** que ent ### Imagem Docker base { #base-docker-image } -Antes havia uma imagem oficial do FastAPI para Docker: tiangolo/uvicorn-gunicorn-fastapi. Mas agora ela está descontinuada. ⛔️ +Antes havia uma imagem oficial do FastAPI para Docker: tiangolo/uvicorn-gunicorn-fastapi-docker. Mas agora ela está descontinuada. ⛔️ Você provavelmente **não** deve usar essa imagem base do Docker (ou qualquer outra semelhante). diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 4e3be586d..e61cc2e2f 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -33,7 +33,7 @@ --- -FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python, baseado nos _type hints_ padrões do Python. +FastAPI é um moderno e rápido (alta performance) framework web para construção de APIs com Python, baseado nos type hints padrões do Python. Os recursos chave são: @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Crie um arquivo `main.py` com: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Se seu código utiliza `async` / `await`, use `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Agora modifique o arquivo `main.py` para receber um corpo de uma requisição `P Declare o corpo utilizando tipos padrão Python, graças ao Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -411,7 +405,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: * Documentar tudo com OpenAPI, que poderá ser usado por: * Sistemas de documentação interativos. * Sistemas de clientes de geração de código automáticos, para muitas linguagens. -* Fornecer diretamente 2 interfaces _web_ de documentação interativa. +* Fornecer diretamente 2 interfaces web de documentação interativa. --- @@ -510,7 +504,7 @@ Siga os tutoriais do seu provedor de nuvem para implantar aplicações FastAPI c ## 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). (*) +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 isso, veja a seção Comparações. @@ -530,7 +524,7 @@ 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: diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index 3cba04912..24f35b210 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Obviamente, você também pode declarar parâmetros de consulta assim que você Dado que, por padrão, valores singulares são interpretados como parâmetros de consulta, você não precisa explicitamente adicionar uma `Query`, você pode somente: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Ou como em Python 3.10 e versões superiores: +Ou em Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Por exemplo: diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md index 84eebc128..2527c2892 100644 --- a/docs/pt/docs/tutorial/path-operation-configuration.md +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. Você pode adicionar um `summary` e uma `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Descrição do docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Ela será usada nas documentações interativas: Você pode especificar a descrição da resposta com o parâmetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Informação diff --git a/docs/ru/docs/_llm-test.md b/docs/ru/docs/_llm-test.md index 6a0272f3a..247d3a964 100644 --- a/docs/ru/docs/_llm-test.md +++ b/docs/ru/docs/_llm-test.md @@ -202,11 +202,6 @@ works(foo="bar") # Это работает 🎉 * XWT * PSGI -### abbr даёт объяснение { #the-abbr-gives-an-explanation } - -* кластер -* Глубокое обучение - ### abbr даёт полную расшифровку и объяснение { #the-abbr-gives-a-full-phrase-and-an-explanation } * MDN @@ -224,6 +219,11 @@ works(foo="bar") # Это работает 🎉 //// +## HTML-элементы "dfn" { #html-dfn-elements } + +* кластер +* Глубокое обучение + ## Заголовки { #headings } //// tab | Тест diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md index cc6691b30..fb2643cd5 100644 --- a/docs/ru/docs/advanced/advanced-dependencies.md +++ b/docs/ru/docs/advanced/advanced-dependencies.md @@ -48,7 +48,7 @@ checker(q="somequery") ``` -…и передаст возвращённое значение как значение зависимости в нашу *функцию-обработчике пути* в параметр `fixed_content_included`: +…и передаст возвращённое значение как значение зависимости в параметр `fixed_content_included` нашей *функции-обработчика пути*: {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} diff --git a/docs/ru/docs/advanced/wsgi.md b/docs/ru/docs/advanced/wsgi.md index 64d7c7a28..41d3a169c 100644 --- a/docs/ru/docs/advanced/wsgi.md +++ b/docs/ru/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ ## Использование `WSGIMiddleware` { #using-wsgimiddleware } -Нужно импортировать `WSGIMiddleware`. +/// info | Информация + +Для этого требуется установить `a2wsgi`, например с помощью `pip install a2wsgi`. + +/// + +Нужно импортировать `WSGIMiddleware` из `a2wsgi`. Затем оберните WSGI‑приложение (например, Flask) в middleware (Промежуточный слой). После этого смонтируйте его на путь. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Примечание + +Ранее рекомендовалось использовать `WSGIMiddleware` из `fastapi.middleware.wsgi`, но теперь он помечен как устаревший. + +Вместо него рекомендуется использовать пакет `a2wsgi`. Использование остаётся таким же. + +Просто убедитесь, что пакет `a2wsgi` установлен, и импортируйте `WSGIMiddleware` из `a2wsgi`. + +/// ## Проверьте { #check-it } diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md index 3937b0165..9e8562be7 100644 --- a/docs/ru/docs/deployment/docker.md +++ b/docs/ru/docs/deployment/docker.md @@ -145,8 +145,6 @@ Successfully installed fastapi pydantic * Создайте файл `main.py` со следующим содержимым: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 02b1c9a28..b1a0c9a2e 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Создайте файл `main.py` со следующим содержимым: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Если ваш код использует `async` / `await`, используйте `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/ru/docs/tutorial/body-multiple-params.md b/docs/ru/docs/tutorial/body-multiple-params.md index 9ae57a311..9d9400494 100644 --- a/docs/ru/docs/tutorial/body-multiple-params.md +++ b/docs/ru/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Или в Python 3.10 и выше: +Или в Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Например: @@ -116,7 +116,7 @@ q: str | None = None /// info | Информация -`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже. +`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`, `Path` и других, которые вы увидите позже. /// diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md index 96a54ffea..112a1efca 100644 --- a/docs/ru/docs/tutorial/path-operation-configuration.md +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ Вы можете добавить параметры `summary` и `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Описание из строк документации { #description-from-docstring } @@ -70,7 +70,7 @@ Вы можете указать описание ответа с помощью параметра `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Дополнительная информация @@ -78,7 +78,7 @@ /// -/// check +/// check | Проверка OpenAPI указывает, что каждой *операции пути* необходимо описание ответа. diff --git a/docs/tr/docs/_llm-test.md b/docs/tr/docs/_llm-test.md new file mode 100644 index 000000000..0ca218e6e --- /dev/null +++ b/docs/tr/docs/_llm-test.md @@ -0,0 +1,503 @@ +# LLM test dosyası { #llm-test-file } + +Bu doküman, dokümantasyonu çeviren LLM'nin `scripts/translate.py` içindeki `general_prompt`'u ve `docs/{language code}/llm-prompt.md` içindeki dile özel prompt'u anlayıp anlamadığını test eder. Dile özel prompt, `general_prompt`'a eklenir. + +Buraya eklenen testler, dile özel prompt'ları tasarlayan herkes tarafından görülecektir. + +Şu şekilde kullanın: + +* Dile özel bir prompt bulundurun: `docs/{language code}/llm-prompt.md`. +* Bu dokümanın hedeflediğiniz dile sıfırdan yeni bir çevirisini yapın (örneğin `translate.py` içindeki `translate-page` komutu). Bu, çeviriyi `docs/{language code}/docs/_llm-test.md` altında oluşturur. +* Çeviride her şeyin yolunda olup olmadığını kontrol edin. +* Gerekirse dile özel prompt'u, genel prompt'u veya İngilizce dokümanı iyileştirin. +* Ardından çeviride kalan sorunları elle düzeltin; böylece iyi bir çeviri elde edin. +* İyi çeviri yerindeyken yeniden çeviri yapın. İdeal sonuç, LLM'nin artık çeviride hiçbir değişiklik yapmamasıdır. Bu da genel prompt'un ve dile özel prompt'un olabilecek en iyi hâle geldiği anlamına gelir (bazen rastgele gibi görünen birkaç değişiklik yapabilir; çünkü LLM'ler deterministik algoritmalar değildir). + +Testler: + +## Code snippets { #code-snippets } + +//// tab | Test + +Bu bir code snippet: `foo`. Bu da başka bir code snippet: `bar`. Bir tane daha: `baz quux`. + +//// + +//// tab | Bilgi + +Code snippet'lerin içeriği olduğu gibi bırakılmalıdır. + +`script/translate.py` içindeki genel prompt'ta `### Content of code snippets` bölümüne bakın. + +//// + +## Alıntılar { #quotes } + +//// tab | Test + +Dün bir arkadaşım şunu yazdı: "If you spell incorrectly correctly, you have spelled it incorrectly". Ben de şunu yanıtladım: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'". + +/// note | Not + +LLM muhtemelen bunu yanlış çevirecektir. Yeniden çeviri yapıldığında düzeltilmiş çeviriyi koruyup korumadığı önemlidir. + +/// + +//// + +//// tab | Bilgi + +Prompt tasarlayan kişi, düz tırnakları tipografik tırnaklara dönüştürüp dönüştürmemeyi seçebilir. Olduğu gibi bırakmak da uygundur. + +Örneğin `docs/de/llm-prompt.md` içindeki `### Quotes` bölümüne bakın. + +//// + +## Code snippet'lerde alıntılar { #quotes-in-code-snippets } + +//// tab | Test + +`pip install "foo[bar]"` + +Code snippet'lerde string literal örnekleri: `"this"`, `'that'`. + +Code snippet'lerde string literal için zor bir örnek: `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 | Bilgi + +... Ancak code snippet'lerin içindeki tırnaklar olduğu gibi kalmalıdır. + +//// + +## Code block'lar { #code-blocks } + +//// tab | Test + +Bir Bash code örneği... + +```bash +# Evrene bir selam yazdır +echo "Hello universe" +``` + +...ve bir console code örneği... + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +...ve bir başka console code örneği... + +```console +// "Code" adında bir dizin oluştur +$ mkdir code +// O dizine geç +$ cd code +``` + +...ve bir Python code örneği... + +```Python +wont_work() # This won't work 😱 +works(foo="bar") # This works 🎉 +``` + +...ve hepsi bu. + +//// + +//// tab | Bilgi + +Code block'ların içindeki code değiştirilmemelidir; tek istisna yorumlardır (comments). + +`script/translate.py` içindeki genel prompt'ta `### Content of code blocks` bölümüne bakın. + +//// + +## Sekmeler ve renkli kutular { #tabs-and-colored-boxes } + +//// tab | Test + +/// info | Bilgi +Some text +/// + +/// note | Not +Some text +/// + +/// note | Teknik Detaylar +Some text +/// + +/// check | Ek bilgi +Some text +/// + +/// tip | İpucu +Some text +/// + +/// warning | Uyarı +Some text +/// + +/// danger | Tehlike +Some text +/// + +//// + +//// tab | Bilgi + +Sekmelerin ve `Info`/`Note`/`Warning`/vb. blokların başlığı, dikey çizgiden (`|`) sonra çeviri olarak eklenmelidir. + +`script/translate.py` içindeki genel prompt'ta `### Special blocks` ve `### Tab blocks` bölümlerine bakın. + +//// + +## Web ve internal link'ler { #web-and-internal-links } + +//// tab | Test + +Link metni çevrilmelidir, link adresi değişmeden kalmalıdır: + +* [Yukarıdaki başlığa link](#code-snippets) +* [Internal link](index.md#installation){.internal-link target=_blank} +* External link +* Link to a style +* Link to a script +* Link to an image + +Link metni çevrilmelidir, link adresi çeviriye işaret etmelidir: + +* FastAPI link + +//// + +//// tab | Bilgi + +Link'ler çevrilmelidir, ancak adresleri değişmeden kalmalıdır. Bir istisna, FastAPI dokümantasyonunun sayfalarına verilen mutlak link'lerdir. Bu durumda link, çeviriye işaret etmelidir. + +`script/translate.py` içindeki genel prompt'ta `### Links` bölümüne bakın. + +//// + +## HTML "abbr" öğeleri { #html-abbr-elements } + +//// tab | Test + +Burada HTML "abbr" öğeleriyle sarılmış bazı şeyler var (bazıları uydurma): + +### abbr tam bir ifade verir { #the-abbr-gives-a-full-phrase } + +* GTD +* lt +* XWT +* PSGI + +### abbr bir açıklama verir { #the-abbr-gives-an-explanation } + +* cluster +* Deep Learning + +### abbr tam bir ifade ve bir açıklama verir { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* MDN +* I/O. + +//// + +//// tab | Bilgi + +"abbr" öğelerinin "title" attribute'ları belirli talimatlara göre çevrilir. + +Çeviriler, LLM'nin kaldırmaması gereken kendi "abbr" öğelerini ekleyebilir. Örneğin İngilizce kelimeleri açıklamak için. + +`script/translate.py` içindeki genel prompt'ta `### HTML abbr elements` bölümüne bakın. + +//// + +## Başlıklar { #headings } + +//// tab | Test + +### Bir web uygulaması geliştirin - bir öğretici { #develop-a-webapp-a-tutorial } + +Merhaba. + +### Type hint'ler ve -annotation'lar { #type-hints-and-annotations } + +Tekrar merhaba. + +### Super- ve subclass'lar { #super-and-subclasses } + +Tekrar merhaba. + +//// + +//// tab | Bilgi + +Başlıklarla ilgili tek katı kural, LLM'nin süslü parantezler içindeki hash kısmını değiştirmemesidir; böylece link'ler bozulmaz. + +`script/translate.py` içindeki genel prompt'ta `### Headings` bölümüne bakın. + +Dile özel bazı talimatlar için örneğin `docs/de/llm-prompt.md` içindeki `### Headings` bölümüne bakın. + +//// + +## Dokümanlarda kullanılan terimler { #terms-used-in-the-docs } + +//// tab | Test + +* siz +* sizin + +* örn. +* vb. + +* `foo` bir `int` olarak +* `bar` bir `str` olarak +* `baz` bir `list` olarak + +* Tutorial - Kullanıcı kılavuzu +* İleri Düzey Kullanıcı Kılavuzu +* SQLModel dokümanları +* API dokümanları +* otomatik dokümanlar + +* Veri Bilimi +* Deep Learning +* Machine Learning +* Dependency Injection +* HTTP Basic authentication +* HTTP Digest +* ISO formatı +* JSON Schema standardı +* JSON schema +* schema tanımı +* Password Flow +* Mobil + +* deprecated +* designed +* invalid +* on the fly +* standard +* default +* case-sensitive +* case-insensitive + +* uygulamayı serve etmek +* sayfayı serve etmek + +* app +* application + +* request +* response +* error response + +* path operation +* path operation decorator +* path operation function + +* body +* request body +* response body +* JSON body +* form body +* file body +* function body + +* parameter +* body parameter +* path parameter +* query parameter +* cookie parameter +* header parameter +* form parameter +* function parameter + +* event +* startup event +* server'ın startup'ı +* shutdown event +* lifespan event + +* handler +* event handler +* exception handler +* handle etmek + +* model +* Pydantic model +* data model +* database model +* form model +* model object + +* class +* base class +* parent class +* subclass +* child class +* sibling class +* class method + +* header +* headers +* authorization header +* `Authorization` header +* forwarded header + +* dependency injection system +* dependency +* dependable +* dependant + +* I/O bound +* CPU bound +* concurrency +* parallelism +* multiprocessing + +* env var +* environment variable +* `PATH` +* `PATH` variable + +* authentication +* authentication provider +* authorization +* authorization form +* authorization provider +* kullanıcı authenticate olur +* sistem kullanıcıyı authenticate eder + +* CLI +* command line interface + +* server +* client + +* cloud provider +* cloud service + +* geliştirme +* geliştirme aşamaları + +* dict +* dictionary +* enumeration +* enum +* enum member + +* encoder +* decoder +* encode etmek +* decode etmek + +* exception +* raise etmek + +* expression +* statement + +* frontend +* backend + +* GitHub discussion +* GitHub issue + +* performance +* performance optimization + +* return type +* return value + +* security +* security scheme + +* task +* background task +* task function + +* template +* template engine + +* type annotation +* type hint + +* server worker +* Uvicorn worker +* Gunicorn Worker +* worker process +* worker class +* workload + +* deployment +* deploy etmek + +* SDK +* software development kit + +* `APIRouter` +* `requirements.txt` +* Bearer Token +* breaking change +* bug +* button +* callable +* code +* commit +* context manager +* coroutine +* database session +* disk +* domain +* engine +* fake X +* HTTP GET method +* item +* library +* lifespan +* lock +* middleware +* mobile application +* module +* mounting +* network +* origin +* override +* payload +* processor +* property +* proxy +* pull request +* query +* RAM +* remote machine +* status code +* string +* tag +* web framework +* wildcard +* return etmek +* validate etmek + +//// + +//// tab | Bilgi + +Bu, dokümanlarda görülen (çoğunlukla) teknik terimlerin eksiksiz ve normatif olmayan bir listesidir. Prompt tasarlayan kişi için, LLM'nin hangi terimlerde desteğe ihtiyaç duyduğunu anlamada yardımcı olabilir. Örneğin iyi bir çeviriyi sürekli daha zayıf bir çeviriye geri alıyorsa. Ya da sizin dilinizde bir terimi çekimlemekte (conjugating/declinating) zorlanıyorsa. + +Örneğin `docs/de/llm-prompt.md` içindeki `### List of English terms and their preferred German translations` bölümüne bakın. + +//// diff --git a/docs/tr/docs/advanced/additional-responses.md b/docs/tr/docs/advanced/additional-responses.md new file mode 100644 index 000000000..c8e372775 --- /dev/null +++ b/docs/tr/docs/advanced/additional-responses.md @@ -0,0 +1,247 @@ +# OpenAPI'de Ek Response'lar { #additional-responses-in-openapi } + +/// warning | Uyarı + +Bu konu oldukça ileri seviye bir konudur. + +**FastAPI**'ye yeni başlıyorsanız buna ihtiyaç duymayabilirsiniz. + +/// + +Ek status code'lar, media type'lar, açıklamalar vb. ile ek response'lar tanımlayabilirsiniz. + +Bu ek response'lar OpenAPI şemasına dahil edilir; dolayısıyla API dokümanlarında da görünürler. + +Ancak bu ek response'lar için, status code'unuzu ve içeriğinizi vererek `JSONResponse` gibi bir `Response`'u doğrudan döndürdüğünüzden emin olmanız gerekir. + +## `model` ile Ek Response { #additional-response-with-model } + +*Path operation decorator*'larınıza `responses` adlı bir parametre geçebilirsiniz. + +Bu parametre bir `dict` alır: anahtarlar her response için status code'lardır (`200` gibi), değerler ise her birine ait bilgileri içeren başka `dict`'lerdir. + +Bu response `dict`'lerinin her birinde, `response_model`'e benzer şekilde bir Pydantic model içeren `model` anahtarı olabilir. + +**FastAPI** bu modeli alır, JSON Schema'sını üretir ve OpenAPI'de doğru yere ekler. + +Örneğin, `404` status code'u ve `Message` Pydantic model'i ile başka bir response tanımlamak için şunu yazabilirsiniz: + +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} + +/// note | Not + +`JSONResponse`'u doğrudan döndürmeniz gerektiğini unutmayın. + +/// + +/// info | Bilgi + +`model` anahtarı OpenAPI'nin bir parçası değildir. + +**FastAPI** buradaki Pydantic model'i alır, JSON Schema'yı üretir ve doğru yere yerleştirir. + +Doğru yer şurasıdır: + +* Değeri başka bir JSON nesnesi (`dict`) olan `content` anahtarının içinde: + * Media type anahtarı (örn. `application/json`) bulunur; bunun değeri başka bir JSON nesnesidir ve onun içinde: + * Değeri model'den gelen JSON Schema olan `schema` anahtarı vardır; doğru yer burasıdır. + * **FastAPI** bunu doğrudan gömmek yerine OpenAPI'deki başka bir yerde bulunan global JSON Schema'lara bir referans ekler. Böylece diğer uygulamalar ve client'lar bu JSON Schema'ları doğrudan kullanabilir, daha iyi code generation araçları sağlayabilir, vb. + +/// + +Bu *path operation* için OpenAPI'de üretilen response'lar şöyle olur: + +```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" + } + } + } + } + } +} +``` + +Schema'lar OpenAPI şemasının içinde başka bir yere referanslanır: + +```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" + } + } + } + } + } + } +} +``` + +## Ana Response İçin Ek Media Type'lar { #additional-media-types-for-the-main-response } + +Aynı `responses` parametresini, aynı ana response için farklı media type'lar eklemek amacıyla da kullanabilirsiniz. + +Örneğin, `image/png` için ek bir media type ekleyerek, *path operation*'ınızın bir JSON nesnesi (media type `application/json`) ya da bir PNG görseli döndürebildiğini belirtebilirsiniz: + +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} + +/// note | Not + +Görseli `FileResponse` kullanarak doğrudan döndürmeniz gerektiğine dikkat edin. + +/// + +/// info | Bilgi + +`responses` parametrenizde açıkça farklı bir media type belirtmediğiniz sürece FastAPI, response'un ana response class'ı ile aynı media type'a sahip olduğunu varsayar (varsayılan `application/json`). + +Ancak media type'ı `None` olan özel bir response class belirttiyseniz, FastAPI ilişkili bir model'i olan tüm ek response'lar için `application/json` kullanır. + +/// + +## Bilgileri Birleştirme { #combining-information } + +`response_model`, `status_code` ve `responses` parametreleri dahil olmak üzere, response bilgilerini birden fazla yerden birleştirebilirsiniz. + +Varsayılan `200` status code'unu (ya da gerekiyorsa özel bir tane) kullanarak bir `response_model` tanımlayabilir, ardından aynı response için ek bilgileri `responses` içinde, doğrudan OpenAPI şemasına ekleyebilirsiniz. + +**FastAPI**, `responses` içindeki ek bilgileri korur ve model'inizin JSON Schema'sı ile birleştirir. + +Örneğin, Pydantic model kullanan ve özel bir `description` içeren `404` status code'lu bir response tanımlayabilirsiniz. + +Ayrıca `response_model`'inizi kullanan, ancak özel bir `example` içeren `200` status code'lu bir response da tanımlayabilirsiniz: + +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} + +Bunların hepsi OpenAPI'nize birleştirilerek dahil edilir ve API dokümanlarında gösterilir: + + + +## Ön Tanımlı Response'ları Özel Olanlarla Birleştirme { #combine-predefined-responses-and-custom-ones } + +Birçok *path operation* için geçerli olacak bazı ön tanımlı response'larınız olabilir; ancak bunları her *path operation*'ın ihtiyaç duyduğu özel response'larla birleştirmek isteyebilirsiniz. + +Bu durumlarda, Python'daki bir `dict`'i `**dict_to_unpack` ile "unpacking" tekniğini kullanabilirsiniz: + +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +Burada `new_dict`, `old_dict` içindeki tüm key-value çiftlerini ve buna ek olarak yeni key-value çiftini içerir: + +```Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +Bu tekniği, *path operation*'larınızda bazı ön tanımlı response'ları yeniden kullanmak ve bunları ek özel response'larla birleştirmek için kullanabilirsiniz. + +Örneğin: + +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} + +## OpenAPI Response'ları Hakkında Daha Fazla Bilgi { #more-information-about-openapi-responses } + +Response'ların içine tam olarak neleri dahil edebileceğinizi görmek için OpenAPI spesifikasyonundaki şu bölümlere bakabilirsiniz: + +* OpenAPI Responses Object, `Response Object`'i içerir. +* OpenAPI Response Object, buradaki her şeyi `responses` parametreniz içinde, her bir response'un içine doğrudan ekleyebilirsiniz. Buna `description`, `headers`, `content` (bunun içinde farklı media type'lar ve JSON Schema'lar tanımlarsınız) ve `links` dahildir. diff --git a/docs/tr/docs/advanced/additional-status-codes.md b/docs/tr/docs/advanced/additional-status-codes.md new file mode 100644 index 000000000..710a6459f --- /dev/null +++ b/docs/tr/docs/advanced/additional-status-codes.md @@ -0,0 +1,41 @@ +# Ek Status Code'ları { #additional-status-codes } + +Varsayılan olarak **FastAPI**, response'ları bir `JSONResponse` kullanarak döndürür; *path operation*'ınızdan döndürdüğünüz içeriği bu `JSONResponse`'un içine yerleştirir. + +Varsayılan status code'u veya *path operation* içinde sizin belirlediğiniz status code'u kullanır. + +## Ek status code'ları { #additional-status-codes_1 } + +Ana status code'a ek olarak başka status code'lar da döndürmek istiyorsanız, `JSONResponse` gibi bir `Response`'u doğrudan döndürerek bunu yapabilirsiniz ve ek status code'u doğrudan orada ayarlarsınız. + +Örneğin, item'ları güncellemeye izin veren bir *path operation*'ınız olduğunu düşünelim; başarılı olduğunda 200 "OK" HTTP status code'unu döndürüyor olsun. + +Ancak yeni item'ları da kabul etmesini istiyorsunuz. Ve item daha önce yoksa, onu oluşturup 201 "Created" HTTP status code'unu döndürsün. + +Bunu yapmak için `JSONResponse` import edin ve içeriğinizi doğrudan onunla döndürün; istediğiniz `status_code`'u da ayarlayın: + +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} + +/// warning | Uyarı + +Yukarıdaki örnekte olduğu gibi bir `Response`'u doğrudan döndürdüğünüzde, response aynen olduğu gibi döndürülür. + +Bir model ile serialize edilmez, vb. + +İçinde olmasını istediğiniz veriyi taşıdığından emin olun ve değerlerin geçerli JSON olduğundan emin olun (eğer `JSONResponse` kullanıyorsanız). + +/// + +/// note | Teknik Detaylar + +`from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içindekileri `fastapi.responses` altında da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. `status` için de aynı durum geçerlidir. + +/// + +## OpenAPI ve API docs { #openapi-and-api-docs } + +Ek status code'ları ve response'ları doğrudan döndürürseniz, FastAPI sizin ne döndüreceğinizi önceden bilemeyeceği için bunlar OpenAPI şemasına (API docs) dahil edilmez. + +Ancak bunu kodunuzda şu şekilde dokümante edebilirsiniz: [Ek Response'lar](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/advanced/advanced-dependencies.md b/docs/tr/docs/advanced/advanced-dependencies.md new file mode 100644 index 000000000..8afb544bd --- /dev/null +++ b/docs/tr/docs/advanced/advanced-dependencies.md @@ -0,0 +1,163 @@ +# Gelişmiş Dependency'ler { #advanced-dependencies } + +## Parametreli dependency'ler { #parameterized-dependencies } + +Şimdiye kadar gördüğümüz tüm dependency'ler sabit bir function ya da class idi. + +Ancak, birçok farklı function veya class tanımlamak zorunda kalmadan, dependency üzerinde bazı parametreler ayarlamak isteyebileceğiniz durumlar olabilir. + +Örneğin, query parametresi `q`'nun belirli bir sabit içeriği barındırıp barındırmadığını kontrol eden bir dependency istediğimizi düşünelim. + +Ama bu sabit içeriği parametreleştirebilmek istiyoruz. + +## "Callable" bir instance { #a-callable-instance } + +Python'da bir class'ın instance'ını "callable" yapmanın bir yolu vardır. + +Class'ın kendisini değil (zaten callable'dır), o class'ın bir instance'ını. + +Bunu yapmak için `__call__` adında bir method tanımlarız: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *} + +Bu durumda, ek parametreleri ve alt-dependency'leri kontrol etmek için **FastAPI**'nin kullanacağı şey bu `__call__` olacaktır; ayrıca daha sonra *path operation function* içindeki parametreye bir değer geçmek için çağrılacak olan da budur. + +## Instance'ı parametreleştirme { #parameterize-the-instance } + +Ve şimdi, dependency'yi "parametreleştirmek" için kullanacağımız instance parametrelerini tanımlamak üzere `__init__` kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[9] *} + +Bu durumda **FastAPI**, `__init__` ile asla uğraşmaz veya onu önemsemez; onu doğrudan kendi kodumuzda kullanırız. + +## Bir instance oluşturma { #create-an-instance } + +Bu class'tan bir instance'ı şöyle oluşturabiliriz: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[18] *} + +Böylece dependency'mizi "parametreleştirmiş" oluruz; artık içinde `"bar"` vardır ve bu değer `checker.fixed_content` attribute'u olarak durur. + +## Instance'ı dependency olarak kullanma { #use-the-instance-as-a-dependency } + +Sonra `Depends(FixedContentQueryChecker)` yerine `Depends(checker)` içinde bu `checker`'ı kullanabiliriz. Çünkü dependency, class'ın kendisi değil, `checker` instance'ıdır. + +Ve dependency çözülürken **FastAPI** bu `checker`'ı şöyle çağırır: + +```Python +checker(q="somequery") +``` + +...ve bunun döndürdüğü her şeyi, *path operation function* içinde `fixed_content_included` parametresine dependency değeri olarak geçirir: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} + +/// tip | İpucu + +Bunların tamamı biraz yapay görünebilir. Ayrıca bunun nasıl faydalı olduğu da henüz çok net olmayabilir. + +Bu örnekler bilerek basit tutuldu; ama mekanizmanın nasıl çalıştığını gösteriyor. + +Security bölümlerinde, aynı şekilde implement edilmiş yardımcı function'lar bulunuyor. + +Buradaki her şeyi anladıysanız, security için kullanılan bu yardımcı araçların arka planda nasıl çalıştığını da zaten biliyorsunuz demektir. + +/// + +## `yield`, `HTTPException`, `except` ve Background Tasks ile Dependency'ler { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | Uyarı + +Büyük ihtimalle bu teknik detaylara ihtiyacınız yok. + +Bu detaylar, özellikle 0.121.0'dan eski bir FastAPI uygulamanız varsa ve `yield` kullanan dependency'lerle ilgili sorunlar yaşıyorsanız faydalıdır. + +/// + +`yield` kullanan dependency'ler; farklı kullanım senaryolarını kapsamak ve bazı sorunları düzeltmek için zaman içinde evrildi. Aşağıda nelerin değiştiğinin bir özeti var. + +### `yield` ve `scope` ile dependency'ler { #dependencies-with-yield-and-scope } + +0.121.0 sürümünde FastAPI, `Depends(scope="function")` desteğini ekledi. + +`Depends(scope="function")` kullanıldığında, `yield` sonrasındaki çıkış kodu, *path operation function* biter bitmez, response client'a geri gönderilmeden önce çalıştırılır. + +`Depends(scope="request")` (varsayılan) kullanıldığında ise `yield` sonrasındaki çıkış kodu, response gönderildikten sonra çalıştırılır. + +Daha fazlasını [`yield` ile Dependency'ler - Erken çıkış ve `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) dokümantasyonunda okuyabilirsiniz. + +### `yield` ve `StreamingResponse` ile dependency'ler, Teknik Detaylar { #dependencies-with-yield-and-streamingresponse-technical-details } + +FastAPI 0.118.0 öncesinde, `yield` kullanan bir dependency kullanırsanız, *path operation function* döndükten sonra ama response gönderilmeden hemen önce `yield` sonrasındaki çıkış kodu çalıştırılırdı. + +Amaç, response'un ağ üzerinden taşınmasını beklerken gereğinden uzun süre resource tutmaktan kaçınmaktı. + +Bu değişiklik aynı zamanda şunu da ifade ediyordu: `StreamingResponse` döndürürseniz, `yield` kullanan dependency'nin çıkış kodu zaten çalışmış olurdu. + +Örneğin, `yield` kullanan bir dependency içinde bir veritabanı session'ınız varsa, `StreamingResponse` veri stream ederken bu session'ı kullanamazdı; çünkü `yield` sonrasındaki çıkış kodunda session zaten kapatılmış olurdu. + +Bu davranış 0.118.0'da geri alındı ve `yield` sonrasındaki çıkış kodunun, response gönderildikten sonra çalıştırılması sağlandı. + +/// info | Bilgi + +Aşağıda göreceğiniz gibi, bu davranış 0.106.0 sürümünden önceki davranışa oldukça benzer; ancak köşe durumlar için çeşitli iyileştirmeler ve bug fix'ler içerir. + +/// + +#### Erken Çıkış Kodu için Kullanım Senaryoları { #use-cases-with-early-exit-code } + +Bazı özel koşullardaki kullanım senaryoları, response gönderilmeden önce `yield` kullanan dependency'lerin çıkış kodunun çalıştırıldığı eski davranıştan fayda görebilir. + +Örneğin, `yield` kullanan bir dependency içinde yalnızca bir kullanıcıyı doğrulamak için veritabanı session'ı kullanan bir kodunuz olduğunu düşünün; ama bu session *path operation function* içinde bir daha hiç kullanılmıyor, yalnızca dependency içinde kullanılıyor **ve** response'un gönderilmesi uzun sürüyor. Mesela veriyi yavaş gönderen bir `StreamingResponse` var, ama herhangi bir nedenle veritabanını kullanmıyor. + +Bu durumda veritabanı session'ı, response tamamen gönderilene kadar elde tutulur. Ancak session kullanılmıyorsa, bunu elde tutmak gerekli değildir. + +Şöyle görünebilir: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +`Session`'ın otomatik kapatılması olan çıkış kodu şurada: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...yavaş veri gönderen response'un gönderimi bittikten sonra çalıştırılır: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +Ama `generate_stream()` veritabanı session'ını kullanmadığı için, response gönderilirken session'ı açık tutmak aslında gerekli değildir. + +SQLModel (veya SQLAlchemy) kullanarak bu spesifik senaryoya sahipseniz, session'a artık ihtiyacınız kalmadıktan sonra session'ı açıkça kapatabilirsiniz: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +Böylece session veritabanı bağlantısını serbest bırakır ve diğer request'ler bunu kullanabilir. + +`yield` kullanan bir dependency'den erken çıkış gerektiren farklı bir kullanım senaryonuz varsa, lütfen kullanım senaryonuzla birlikte ve `yield` kullanan dependency'ler için erken kapatmadan neden fayda göreceğinizi açıklayarak bir GitHub Discussion Sorusu oluşturun. + +`yield` kullanan dependency'lerde erken kapatma için ikna edici kullanım senaryoları varsa, erken kapatmayı seçmeli (opt-in) hale getiren yeni bir yöntem eklemeyi düşünebilirim. + +### `yield` ve `except` ile dependency'ler, Teknik Detaylar { #dependencies-with-yield-and-except-technical-details } + +FastAPI 0.110.0 öncesinde, `yield` kullanan bir dependency kullanır, sonra o dependency içinde `except` ile bir exception yakalar ve exception'ı tekrar raise etmezseniz; exception otomatik olarak herhangi bir exception handler'a veya internal server error handler'a raise/forward edilirdi. + +Bu davranış 0.110.0 sürümünde değiştirildi. Amaç, handler olmayan (internal server errors) forward edilmiş exception'ların yönetilmemesinden kaynaklanan bellek tüketimini düzeltmek ve bunu normal Python kodunun davranışıyla tutarlı hale getirmekti. + +### Background Tasks ve `yield` ile dependency'ler, Teknik Detaylar { #background-tasks-and-dependencies-with-yield-technical-details } + +FastAPI 0.106.0 öncesinde, `yield` sonrasında exception raise etmek mümkün değildi; çünkü `yield` kullanan dependency'lerdeki çıkış kodu response gönderildikten *sonra* çalıştırılıyordu. Bu nedenle [Exception Handler'ları](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} zaten çalışmış olurdu. + +Bu tasarımın ana sebeplerinden biri, background task'lerin içinde dependency'lerin "yield ettiği" aynı objeleri kullanmaya izin vermekti; çünkü çıkış kodu, background task'ler bittikten sonra çalıştırılıyordu. + +Bu davranış FastAPI 0.106.0'da, response'un ağ üzerinde taşınmasını beklerken resource tutmamak amacıyla değiştirildi. + +/// tip | İpucu + +Ek olarak, bir background task normalde ayrı ele alınması gereken bağımsız bir mantık setidir ve kendi resource'larına sahip olmalıdır (ör. kendi veritabanı bağlantısı). + +Bu şekilde muhtemelen daha temiz bir kod elde edersiniz. + +/// + +Bu davranışa güvenerek kod yazdıysanız, artık background task'ler için resource'ları background task'in içinde oluşturmalı ve içeride yalnızca `yield` kullanan dependency'lerin resource'larına bağlı olmayan verileri kullanmalısınız. + +Örneğin, aynı veritabanı session'ını kullanmak yerine background task içinde yeni bir veritabanı session'ı oluşturur ve veritabanındaki objeleri bu yeni session ile alırsınız. Ardından, background task function'ına veritabanından gelen objeyi parametre olarak geçirmek yerine, o objenin ID'sini geçirir ve objeyi background task function'ı içinde yeniden elde edersiniz. diff --git a/docs/tr/docs/advanced/async-tests.md b/docs/tr/docs/advanced/async-tests.md new file mode 100644 index 000000000..82349bbec --- /dev/null +++ b/docs/tr/docs/advanced/async-tests.md @@ -0,0 +1,99 @@ +# Async Testler { #async-tests } + +Sağlanan `TestClient` ile **FastAPI** uygulamalarınızı nasıl test edeceğinizi zaten gördünüz. Şimdiye kadar yalnızca senkron testler yazdık, yani `async` fonksiyonlar kullanmadan. + +Testlerinizde asenkron fonksiyonlar kullanabilmek faydalı olabilir; örneğin veritabanınızı asenkron olarak sorguluyorsanız. Diyelim ki FastAPI uygulamanıza request gönderilmesini test etmek ve ardından async bir veritabanı kütüphanesi kullanırken backend'in doğru veriyi veritabanına başarıyla yazdığını doğrulamak istiyorsunuz. + +Bunu nasıl çalıştırabileceğimize bir bakalım. + +## pytest.mark.anyio { #pytest-mark-anyio } + +Testlerimizde asenkron fonksiyonlar çağırmak istiyorsak, test fonksiyonlarımızın da asenkron olması gerekir. AnyIO bunun için güzel bir plugin sağlar; böylece bazı test fonksiyonlarının asenkron olarak çağrılacağını belirtebiliriz. + +## HTTPX { #httpx } + +**FastAPI** uygulamanız `async def` yerine normal `def` fonksiyonları kullanıyor olsa bile, altta yatan yapı hâlâ bir `async` uygulamadır. + +`TestClient`, standart pytest kullanarak normal `def` test fonksiyonlarınızın içinden asenkron FastAPI uygulamasını çağırmak için içeride bazı “sihirli” işlemler yapar. Ancak bu sihir, onu asenkron fonksiyonların içinde kullandığımızda artık çalışmaz. Testlerimizi asenkron çalıştırdığımızda, test fonksiyonlarımızın içinde `TestClient` kullanamayız. + +`TestClient`, HTTPX tabanlıdır ve neyse ki API'yi test etmek için HTTPX'i doğrudan kullanabiliriz. + +## Örnek { #example } + +Basit bir örnek için, [Bigger Applications](../tutorial/bigger-applications.md){.internal-link target=_blank} ve [Testing](../tutorial/testing.md){.internal-link target=_blank} bölümlerinde anlatılana benzer bir dosya yapısı düşünelim: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +`main.py` dosyası şöyle olur: + +{* ../../docs_src/async_tests/app_a_py39/main.py *} + +`test_main.py` dosyasında `main.py` için testler yer alır, artık şöyle görünebilir: + +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} + +## Çalıştırma { #run-it } + +Testlerinizi her zamanki gibi şu şekilde çalıştırabilirsiniz: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## Detaylı Anlatım { #in-detail } + +`@pytest.mark.anyio` marker'ı, pytest'e bu test fonksiyonunun asenkron olarak çağrılması gerektiğini söyler: + +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} + +/// tip | İpucu + +Test fonksiyonu artık `TestClient` kullanırken eskiden olduğu gibi sadece `def` değil, `async def`. + +/// + +Ardından app ile bir `AsyncClient` oluşturup `await` kullanarak ona async request'ler gönderebiliriz. + +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} + +Bu, şu kullanıma denktir: + +```Python +response = client.get('/') +``` + +...ki daha önce request'leri `TestClient` ile bu şekilde gönderiyorduk. + +/// tip | İpucu + +Yeni `AsyncClient` ile async/await kullandığımızı unutmayın; request asenkron çalışır. + +/// + +/// warning | Uyarı + +Uygulamanız lifespan event'lerine dayanıyorsa, `AsyncClient` bu event'leri tetiklemez. Tetiklendiklerinden emin olmak için florimondmanca/asgi-lifespan paketindeki `LifespanManager`'ı kullanın. + +/// + +## Diğer Asenkron Fonksiyon Çağrıları { #other-asynchronous-function-calls } + +Test fonksiyonu artık asenkron olduğundan, testlerinizde FastAPI uygulamanıza request göndermenin yanında başka `async` fonksiyonları da (çağırıp `await` ederek) kodunuzun başka yerlerinde yaptığınız gibi aynı şekilde kullanabilirsiniz. + +/// tip | İpucu + +Testlerinize asenkron fonksiyon çağrıları entegre ederken `RuntimeError: Task attached to a different loop` hatasıyla karşılaşırsanız (ör. MongoDB'nin MotorClient kullanımı), event loop gerektiren nesneleri yalnızca async fonksiyonların içinde oluşturmanız gerektiğini unutmayın; örneğin bir `@app.on_event("startup")` callback'i içinde. + +/// diff --git a/docs/tr/docs/advanced/behind-a-proxy.md b/docs/tr/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..e70b16960 --- /dev/null +++ b/docs/tr/docs/advanced/behind-a-proxy.md @@ -0,0 +1,466 @@ +# Proxy Arkasında Çalıştırma { #behind-a-proxy } + +Birçok durumda, FastAPI uygulamanızın önünde Traefik veya Nginx gibi bir **proxy** kullanırsınız. + +Bu proxy'ler HTTPS sertifikalarını ve diğer bazı işleri üstlenebilir. + +## Proxy Forwarded Header'ları { #proxy-forwarded-headers } + +Uygulamanızın önündeki bir **proxy**, request'leri **server**'ınıza göndermeden önce genelde bazı header'ları dinamik olarak ayarlar. Böylece server, request'in proxy tarafından **forward** edildiğini; domain dahil orijinal (public) URL'yi, HTTPS kullanıldığını vb. bilgileri anlayabilir. + +**Server** programı (örneğin **FastAPI CLI** üzerinden **Uvicorn**) bu header'ları yorumlayabilir ve ardından bu bilgiyi uygulamanıza aktarabilir. + +Ancak güvenlik nedeniyle, server güvenilir bir proxy arkasında olduğunu bilmediği için bu header'ları yorumlamaz. + +/// note | Teknik Detaylar + +Proxy header'ları şunlardır: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +### Proxy Forwarded Header'larını Etkinleştirme { #enable-proxy-forwarded-headers } + +FastAPI CLI'yi `--forwarded-allow-ips` *CLI Option*'ı ile başlatıp, bu forwarded header'ları okumada güvenilecek IP adreslerini verebilirsiniz. + +Bunu `--forwarded-allow-ips="*"` olarak ayarlarsanız, gelen tüm IP'lere güvenir. + +**Server**'ınız güvenilir bir **proxy** arkasındaysa ve onunla sadece proxy konuşuyorsa, bu ayar server'ın o **proxy**'nin IP'si her neyse onu kabul etmesini sağlar. + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### HTTPS ile Redirect'ler { #redirects-with-https } + +Örneğin `/items/` adında bir *path operation* tanımladığınızı düşünelim: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +Client `/items`'a gitmeye çalışırsa, varsayılan olarak `/items/`'a redirect edilir. + +Ancak *CLI Option* `--forwarded-allow-ips` ayarlanmadan önce, `http://localhost:8000/items/`'a redirect edebilir. + +Oysa uygulamanız `https://mysuperapp.com` üzerinde host ediliyor olabilir ve redirect'in `https://mysuperapp.com/items/` olması gerekir. + +Artık `--proxy-headers` ayarını yaparak FastAPI'nin doğru adrese redirect edebilmesini sağlarsınız. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | İpucu + +HTTPS hakkında daha fazla bilgi için [HTTPS Hakkında](../deployment/https.md){.internal-link target=_blank} rehberine bakın. + +/// + +### Proxy Forwarded Header'ları Nasıl Çalışır { #how-proxy-forwarded-headers-work } + +**Proxy**'nin, client ile **application server** arasında forwarded header'ları nasıl eklediğini gösteren görsel bir temsil: + +```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 +``` + +**Proxy**, orijinal client request'ini araya girerek (intercept) alır ve request'i **application server**'a iletmeden önce özel *forwarded* header'ları (`X-Forwarded-*`) ekler. + +Bu header'lar, aksi halde kaybolacak olan orijinal request bilgilerini korur: + +* **X-Forwarded-For**: Orijinal client'ın IP adresi +* **X-Forwarded-Proto**: Orijinal protokol (`https`) +* **X-Forwarded-Host**: Orijinal host (`mysuperapp.com`) + +**FastAPI CLI** `--forwarded-allow-ips` ile yapılandırıldığında bu header'lara güvenir ve örneğin redirect'lerde doğru URL'leri üretmek için bunları kullanır. + +## Path Prefix'i Kırpılan (Stripped) Bir Proxy { #proxy-with-a-stripped-path-prefix } + +Uygulamanıza bir path prefix ekleyen bir proxy'niz olabilir. + +Bu durumlarda uygulamanızı yapılandırmak için `root_path` kullanabilirsiniz. + +`root_path`, FastAPI'nin (Starlette üzerinden) üzerine kurulduğu ASGI spesifikasyonunun sağladığı bir mekanizmadır. + +`root_path` bu özel senaryoları yönetmek için kullanılır. + +Ayrıca sub-application mount ederken de içeride kullanılır. + +Path prefix'i kırpılan bir proxy kullanmak, şu anlama gelir: Kodunuzda `/app` altında bir path tanımlarsınız; ancak üstte bir katman (proxy) ekleyip **FastAPI** uygulamanızı `/api/v1` gibi bir path'in altına koyarsınız. + +Bu durumda, orijinal `/app` path'i aslında `/api/v1/app` altında servis edilir. + +Kodunuzun tamamı sadece `/app` varmış gibi yazılmış olsa bile. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} + +Proxy, request'i app server'a (muhtemelen FastAPI CLI üzerinden Uvicorn) iletmeden önce **path prefix**'i anlık olarak **"kırpar"** (strip). Böylece uygulamanız hâlâ `/app` altında servis ediliyormuş gibi davranır ve tüm kodunuzu `/api/v1` prefix'ini içerecek şekilde güncellemeniz gerekmez. + +Buraya kadar her şey normal çalışır. + +Ancak entegre doküman arayüzünü (frontend) açtığınızda, OpenAPI şemasını `/api/v1/openapi.json` yerine `/openapi.json` üzerinden almayı bekler. + +Dolayısıyla tarayıcıda çalışan frontend `/openapi.json`'a erişmeye çalışır ve OpenAPI şemasını alamaz. + +Çünkü uygulamamız proxy arkasında `/api/v1` path prefix'i ile çalışmaktadır; frontend'in OpenAPI şemasını `/api/v1/openapi.json` üzerinden çekmesi gerekir. + +```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 | İpucu + +`0.0.0.0` IP'si, genelde programın ilgili makine/server üzerindeki tüm kullanılabilir IP'lerde dinlediği anlamına gelir. + +/// + +Docs UI'nin, bu API `server`'ının (proxy arkasında) `/api/v1` altında bulunduğunu belirtmek için OpenAPI şemasına da ihtiyacı olur. Örneğin: + +```JSON hl_lines="4-8" +{ + "openapi": "3.1.0", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +Bu örnekte "Proxy", **Traefik** gibi bir şey olabilir. Server da FastAPI uygulamanızı çalıştıran (Uvicorn'lu) FastAPI CLI olabilir. + +### `root_path` Sağlama { #providing-the-root-path } + +Bunu yapmak için `--root-path` komut satırı seçeneğini şöyle kullanabilirsiniz: + +
+ +```console +$ 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) +``` + +
+ +Hypercorn kullanıyorsanız, onda da `--root-path` seçeneği vardır. + +/// note | Teknik Detaylar + +ASGI spesifikasyonu bu kullanım senaryosu için bir `root_path` tanımlar. + +`--root-path` komut satırı seçeneği de bu `root_path`'i sağlar. + +/// + +### Mevcut `root_path`'i Kontrol Etme { #checking-the-current-root-path } + +Uygulamanızın her request için kullandığı mevcut `root_path` değerini alabilirsiniz; bu değer ASGI spesifikasyonunun bir parçası olan `scope` dict'inin içindedir. + +Burada sadece göstermek için bunu mesaja dahil ediyoruz. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} + +Ardından Uvicorn'u şu şekilde başlatırsanız: + +
+ +```console +$ 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) +``` + +
+ +Response şöyle bir şey olur: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### FastAPI Uygulamasında `root_path` Ayarlama { #setting-the-root-path-in-the-fastapi-app } + +Alternatif olarak, `--root-path` gibi bir komut satırı seçeneği (veya muadili) sağlayamıyorsanız, FastAPI uygulamanızı oluştururken `root_path` parametresini ayarlayabilirsiniz: + +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} + +`FastAPI`'ye `root_path` vermek, Uvicorn veya Hypercorn'a `--root-path` komut satırı seçeneğini vermekle eşdeğerdir. + +### `root_path` Hakkında { #about-root-path } + +Şunu unutmayın: Server (Uvicorn) bu `root_path`'i, uygulamaya iletmek dışında başka bir amaçla kullanmaz. + +Ancak tarayıcınızla http://127.0.0.1:8000/app adresine giderseniz normal response'u görürsünüz: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +Yani `http://127.0.0.1:8000/api/v1/app` üzerinden erişilmeyi beklemez. + +Uvicorn, proxy'nin Uvicorn'a `http://127.0.0.1:8000/app` üzerinden erişmesini bekler; bunun üstüne ekstra `/api/v1` prefix'ini eklemek proxy'nin sorumluluğudur. + +## Stripped Path Prefix Kullanan Proxy'ler Hakkında { #about-proxies-with-a-stripped-path-prefix } + +Stripped path prefix kullanan bir proxy, yapılandırma yöntemlerinden yalnızca biridir. + +Birçok durumda varsayılan davranış, proxy'nin stripped path prefix kullanmaması olacaktır. + +Böyle bir durumda (stripped path prefix olmadan), proxy `https://myawesomeapp.com` gibi bir yerde dinler; tarayıcı `https://myawesomeapp.com/api/v1/app`'e giderse ve sizin server'ınız (ör. Uvicorn) `http://127.0.0.1:8000` üzerinde dinliyorsa, proxy (stripped path prefix olmadan) Uvicorn'a aynı path ile erişir: `http://127.0.0.1:8000/api/v1/app`. + +## Traefik ile Local Olarak Test Etme { #testing-locally-with-traefik } + +Traefik kullanarak, stripped path prefix'li deneyi local'de kolayca çalıştırabilirsiniz. + +Traefik'i indirin; tek bir binary'dir, sıkıştırılmış dosyayı çıkarıp doğrudan terminalden çalıştırabilirsiniz. + +Ardından `traefik.toml` adında bir dosya oluşturup şunu yazın: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +Bu, Traefik'e 9999 portunda dinlemesini ve `routes.toml` adlı başka bir dosyayı kullanmasını söyler. + +/// tip | İpucu + +Standart HTTP portu 80 yerine 9999 portunu kullanıyoruz; böylece admin (`sudo`) yetkileriyle çalıştırmanız gerekmez. + +/// + +Şimdi diğer dosyayı, `routes.toml`'u oluşturun: + +```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" +``` + +Bu dosya, Traefik'i `/api/v1` path prefix'ini kullanacak şekilde yapılandırır. + +Ardından Traefik, request'leri `http://127.0.0.1:8000` üzerinde çalışan Uvicorn'unuza yönlendirir. + +Şimdi Traefik'i başlatın: + +
+ +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
+ +Ve şimdi uygulamanızı `--root-path` seçeneğiyle başlatın: + +
+ +```console +$ 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) +``` + +
+ +### Response'ları Kontrol Edin { #check-the-responses } + +Şimdi Uvicorn'un portundaki URL'ye giderseniz: http://127.0.0.1:8000/app, normal response'u görürsünüz: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +/// tip | İpucu + +`http://127.0.0.1:8000/app` üzerinden erişiyor olsanız bile, `root_path` değerinin `--root-path` seçeneğinden alınıp `/api/v1` olarak gösterildiğine dikkat edin. + +/// + +Şimdi de Traefik'in portundaki URL'yi, path prefix ile birlikte açın: http://127.0.0.1:9999/api/v1/app. + +Aynı response'u alırız: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +ama bu sefer proxy'nin sağladığı prefix path olan `/api/v1` ile gelen URL'de. + +Elbette buradaki fikir, herkesin uygulamaya proxy üzerinden erişmesidir; dolayısıyla `/api/v1` path prefix'li sürüm "doğru" olandır. + +Uvicorn'un doğrudan sunduğu, path prefix olmayan sürüm (`http://127.0.0.1:8000/app`) ise sadece _proxy_'nin (Traefik) erişmesi için kullanılmalıdır. + +Bu da Proxy'nin (Traefik) path prefix'i nasıl kullandığını ve server'ın (Uvicorn) `--root-path` seçeneğinden gelen `root_path`'i nasıl kullandığını gösterir. + +### Docs UI'yi Kontrol Edin { #check-the-docs-ui } + +Şimdi işin eğlenceli kısmı. ✨ + +Uygulamaya erişmenin "resmi" yolu, tanımladığımız path prefix ile proxy üzerinden erişmektir. Bu yüzden beklendiği gibi, Uvicorn'un doğrudan servis ettiği docs UI'yi URL'de path prefix olmadan açarsanız çalışmaz; çünkü proxy üzerinden erişileceğini varsayar. + +Şuradan kontrol edebilirsiniz: http://127.0.0.1:8000/docs: + + + +Ancak docs UI'yi proxy üzerinden, `9999` portuyla, `/api/v1/docs` altında "resmi" URL'den açarsak doğru çalışır! 🎉 + +Şuradan kontrol edebilirsiniz: http://127.0.0.1:9999/api/v1/docs: + + + +Tam istediğimiz gibi. ✔️ + +Bunun nedeni, FastAPI'nin OpenAPI içinde varsayılan `server`'ı, `root_path` tarafından verilen URL ile oluşturmak için bu `root_path`'i kullanmasıdır. + +## Ek `server`'lar { #additional-servers } + +/// warning | Uyarı + +Bu daha ileri seviye bir kullanım senaryosudur. İsterseniz atlayabilirsiniz. + +/// + +Varsayılan olarak **FastAPI**, OpenAPI şemasında `root_path` için bir `server` oluşturur. + +Ancak başka alternatif `servers` da sağlayabilirsiniz; örneğin *aynı* docs UI'nin hem staging hem de production ortamıyla etkileşime girmesini istiyorsanız. + +Özel bir `servers` listesi verirseniz ve bir `root_path` varsa (çünkü API'niz proxy arkasındadır), **FastAPI** bu `root_path` ile bir "server"ı listenin başına ekler. + +Örneğin: + +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} + +Şöyle bir OpenAPI şeması üretir: + +```JSON hl_lines="5-7" +{ + "openapi": "3.1.0", + // 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 | İpucu + +`url` değeri `/api/v1` olan, `root_path`'ten alınmış otomatik üretilen server'a dikkat edin. + +/// + +Docs UI'de, http://127.0.0.1:9999/api/v1/docs adresinde şöyle görünür: + + + +/// tip | İpucu + +Docs UI, seçtiğiniz server ile etkileşime girer. + +/// + +/// note | Teknik Detaylar + +OpenAPI spesifikasyonunda `servers` özelliği opsiyoneldir. + +`servers` parametresini belirtmezseniz ve `root_path` `/` ile aynıysa, üretilen OpenAPI şemasında `servers` özelliği varsayılan olarak tamamen çıkarılır; bu da `url` değeri `/` olan tek bir server ile eşdeğerdir. + +/// + +### `root_path`'ten Otomatik `server` Eklenmesini Kapatma { #disable-automatic-server-from-root-path } + +**FastAPI**'nin `root_path` kullanarak otomatik bir server eklemesini istemiyorsanız, `root_path_in_servers=False` parametresini kullanabilirsiniz: + +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} + +Böylece OpenAPI şemasına dahil etmez. + +## Bir Sub-Application Mount Etme { #mounting-a-sub-application } + +Bir sub-application'ı ( [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank} bölümünde anlatıldığı gibi) mount etmeniz gerekiyorsa ve aynı zamanda `root_path` ile bir proxy kullanıyorsanız, bunu beklendiği gibi normal şekilde yapabilirsiniz. + +FastAPI içeride `root_path`'i akıllıca kullanır; dolayısıyla doğrudan çalışır. ✨ diff --git a/docs/tr/docs/advanced/custom-response.md b/docs/tr/docs/advanced/custom-response.md new file mode 100644 index 000000000..c5148f428 --- /dev/null +++ b/docs/tr/docs/advanced/custom-response.md @@ -0,0 +1,312 @@ +# Özel Response - HTML, Stream, File ve Diğerleri { #custom-response-html-stream-file-others } + +Varsayılan olarak **FastAPI**, response'ları `JSONResponse` kullanarak döndürür. + +Bunu, [Doğrudan bir Response döndür](response-directly.md){.internal-link target=_blank} bölümünde gördüğünüz gibi doğrudan bir `Response` döndürerek geçersiz kılabilirsiniz. + +Ancak doğrudan bir `Response` döndürürseniz (veya `JSONResponse` gibi herhangi bir alt sınıfını), veri otomatik olarak dönüştürülmez (bir `response_model` tanımlamış olsanız bile) ve dokümantasyon da otomatik üretilmez (örneğin, üretilen OpenAPI’nin parçası olarak HTTP header `Content-Type` içindeki ilgili "media type" dahil edilmez). + +Bununla birlikte, *path operation decorator* içinde `response_class` parametresini kullanarak hangi `Response`’un (örn. herhangi bir `Response` alt sınıfı) kullanılacağını da ilan edebilirsiniz. + +*path operation function*’ınızdan döndürdüğünüz içerik, o `Response`’un içine yerleştirilir. + +Ve eğer bu `Response` ( `JSONResponse` ve `UJSONResponse`’ta olduğu gibi) bir JSON media type’a (`application/json`) sahipse, döndürdüğünüz veri; *path operation decorator* içinde tanımladığınız herhangi bir Pydantic `response_model` ile otomatik olarak dönüştürülür (ve filtrelenir). + +/// note | Not + +Media type’ı olmayan bir response class kullanırsanız, FastAPI response’unuzun content içermediğini varsayar; bu yüzden ürettiği OpenAPI dokümanında response formatını dokümante etmez. + +/// + +## `ORJSONResponse` Kullan { #use-orjsonresponse } + +Örneğin performansı sıkıştırmaya çalışıyorsanız, `orjson` kurup kullanabilir ve response’u `ORJSONResponse` olarak ayarlayabilirsiniz. + +Kullanmak istediğiniz `Response` class’ını (alt sınıfını) import edin ve *path operation decorator* içinde tanımlayın. + +Büyük response'larda, doğrudan bir `Response` döndürmek bir dictionary döndürmekten çok daha hızlıdır. + +Çünkü varsayılan olarak FastAPI, içindeki her item’ı inceleyip JSON olarak serialize edilebilir olduğundan emin olur; tutorial’da anlatılan aynı [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} mekanizmasını kullanır. Bu da örneğin veritabanı modelleri gibi **keyfi objeleri** döndürebilmenizi sağlar. + +Ancak döndürdüğünüz içeriğin **JSON ile serialize edilebilir** olduğundan eminseniz, onu doğrudan response class’ına verebilir ve FastAPI’nin response class’ına vermeden önce dönüş içeriğinizi `jsonable_encoder` içinden geçirirken oluşturacağı ek yükten kaçınabilirsiniz. + +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} + +/// info | Bilgi + +`response_class` parametresi, response’un "media type"’ını tanımlamak için de kullanılır. + +Bu durumda HTTP header `Content-Type`, `application/json` olarak ayarlanır. + +Ve OpenAPI’de de bu şekilde dokümante edilir. + +/// + +/// tip | İpucu + +`ORJSONResponse` yalnızca FastAPI’de vardır, Starlette’te yoktur. + +/// + +## HTML Response { #html-response } + +**FastAPI**’den doğrudan HTML içeren bir response döndürmek için `HTMLResponse` kullanın. + +* `HTMLResponse` import edin. +* *path operation decorator*’ınızın `response_class` parametresi olarak `HTMLResponse` verin. + +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} + +/// info | Bilgi + +`response_class` parametresi, response’un "media type"’ını tanımlamak için de kullanılır. + +Bu durumda HTTP header `Content-Type`, `text/html` olarak ayarlanır. + +Ve OpenAPI’de de bu şekilde dokümante edilir. + +/// + +### Bir `Response` Döndür { #return-a-response } + +[Doğrudan bir Response döndür](response-directly.md){.internal-link target=_blank} bölümünde görüldüğü gibi, *path operation* içinde doğrudan bir response döndürerek response’u override edebilirsiniz. + +Yukarıdaki örneğin aynısı, bu sefer bir `HTMLResponse` döndürerek, şöyle görünebilir: + +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} + +/// warning | Uyarı + +*path operation function*’ınızın doğrudan döndürdüğü bir `Response`, OpenAPI’de dokümante edilmez (örneğin `Content-Type` dokümante edilmez) ve otomatik interaktif dokümanlarda görünmez. + +/// + +/// info | Bilgi + +Elbette gerçek `Content-Type` header’ı, status code vb. değerler, döndürdüğünüz `Response` objesinden gelir. + +/// + +### OpenAPI’de Dokümante Et ve `Response`’u Override Et { #document-in-openapi-and-override-response } + +Response’u fonksiyonun içinden override etmek ama aynı zamanda OpenAPI’de "media type"’ı dokümante etmek istiyorsanız, `response_class` parametresini kullanıp ayrıca bir `Response` objesi döndürebilirsiniz. + +Bu durumda `response_class` sadece OpenAPI *path operation*’ını dokümante etmek için kullanılır; sizin `Response`’unuz ise olduğu gibi kullanılır. + +#### Doğrudan bir `HTMLResponse` Döndür { #return-an-htmlresponse-directly } + +Örneğin şöyle bir şey olabilir: + +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} + +Bu örnekte `generate_html_response()` fonksiyonu, HTML’i bir `str` olarak döndürmek yerine zaten bir `Response` üretip döndürmektedir. + +`generate_html_response()` çağrısının sonucunu döndürerek, varsayılan **FastAPI** davranışını override edecek bir `Response` döndürmüş olursunuz. + +Ama `response_class` içinde `HTMLResponse` da verdiğiniz için **FastAPI**, bunu OpenAPI’de ve interaktif dokümanlarda `text/html` ile HTML olarak nasıl dokümante edeceğini bilir: + + + +## Mevcut Response'lar { #available-responses } + +Mevcut response'lardan bazıları aşağıdadır. + +Unutmayın: `Response` ile başka herhangi bir şeyi döndürebilir, hatta özel bir alt sınıf da oluşturabilirsiniz. + +/// note | Teknik Detaylar + +`from starlette.responses import HTMLResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici için kolaylık olsun diye `starlette.responses` içindekileri `fastapi.responses` olarak da sağlar. Ancak mevcut response'ların çoğu doğrudan Starlette’ten gelir. + +/// + +### `Response` { #response } + +Ana `Response` class’ıdır; diğer tüm response'lar bundan türetilir. + +Bunu doğrudan döndürebilirsiniz. + +Şu parametreleri kabul eder: + +* `content` - Bir `str` veya `bytes`. +* `status_code` - Bir `int` HTTP status code. +* `headers` - String’lerden oluşan bir `dict`. +* `media_type` - Media type’ı veren bir `str`. Örn. `"text/html"`. + +FastAPI (aslında Starlette) otomatik olarak bir Content-Length header’ı ekler. Ayrıca `media_type`’a göre bir Content-Type header’ı ekler ve text türleri için sona bir charset ekler. + +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +### `HTMLResponse` { #htmlresponse } + +Yukarıda okuduğunuz gibi, bir miktar text veya bytes alır ve HTML response döndürür. + +### `PlainTextResponse` { #plaintextresponse } + +Bir miktar text veya bytes alır ve düz metin response döndürür. + +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} + +### `JSONResponse` { #jsonresponse } + +Bir miktar veri alır ve `application/json` olarak encode edilmiş bir response döndürür. + +Yukarıda okuduğunuz gibi, **FastAPI**’de varsayılan response budur. + +### `ORJSONResponse` { #orjsonresponse } + +Yukarıda okuduğunuz gibi `orjson` kullanan hızlı bir alternatif JSON response. + +/// info | Bilgi + +Bunun için `orjson` kurulmalıdır; örneğin `pip install orjson`. + +/// + +### `UJSONResponse` { #ujsonresponse } + +`ujson` kullanan alternatif bir JSON response. + +/// info | Bilgi + +Bunun için `ujson` kurulmalıdır; örneğin `pip install ujson`. + +/// + +/// warning | Uyarı + +`ujson`, bazı edge-case’leri ele alma konusunda Python’un built-in implementasyonu kadar dikkatli değildir. + +/// + +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} + +/// tip | İpucu + +`ORJSONResponse` daha hızlı bir alternatif olabilir. + +/// + +### `RedirectResponse` { #redirectresponse } + +HTTP redirect döndürür. Varsayılan olarak 307 status code (Temporary Redirect) kullanır. + +`RedirectResponse`’u doğrudan döndürebilirsiniz: + +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} + +--- + +Veya `response_class` parametresi içinde kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} + +Bunu yaparsanız, *path operation* function’ınızdan doğrudan URL döndürebilirsiniz. + +Bu durumda kullanılan `status_code`, `RedirectResponse` için varsayılan olan `307` olur. + +--- + +Ayrıca `status_code` parametresini `response_class` parametresiyle birlikte kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} + +### `StreamingResponse` { #streamingresponse } + +Bir async generator veya normal generator/iterator alır ve response body’yi stream eder. + +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} + +#### `StreamingResponse`’u file-like objelerle kullanma { #using-streamingresponse-with-file-like-objects } + +Bir file-like objeniz varsa (örn. `open()`’ın döndürdüğü obje), o file-like obje üzerinde iterate eden bir generator function oluşturabilirsiniz. + +Böylece önce hepsini memory’ye okumak zorunda kalmazsınız; bu generator function’ı `StreamingResponse`’a verip döndürebilirsiniz. + +Buna cloud storage ile etkileşime giren, video işleyen ve benzeri birçok kütüphane dahildir. + +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} + +1. Bu generator function’dır. İçinde `yield` ifadeleri olduğu için "generator function" denir. +2. Bir `with` bloğu kullanarak, generator function bittiğinde file-like objenin kapandığından emin oluruz. Yani response göndermeyi bitirdikten sonra kapanır. +3. Bu `yield from`, fonksiyona `file_like` isimli şeyi iterate etmesini söyler. Ardından iterate edilen her parça için, o parçayı bu generator function’dan (`iterfile`) geliyormuş gibi yield eder. + + Yani, içerdeki "üretme" (generating) işini başka bir şeye devreden bir generator function’dır. + + Bunu bu şekilde yaptığımızda `with` bloğu içinde tutabilir ve böylece iş bitince file-like objenin kapanmasını garanti edebiliriz. + +/// tip | İpucu + +Burada `async` ve `await` desteklemeyen standart `open()` kullandığımız için path operation’ı normal `def` ile tanımlarız. + +/// + +### `FileResponse` { #fileresponse } + +Asenkron olarak bir dosyayı response olarak stream eder. + +Diğer response türlerine göre instantiate ederken farklı argümanlar alır: + +* `path` - Stream edilecek dosyanın dosya path'i. +* `headers` - Eklenecek özel header’lar; dictionary olarak. +* `media_type` - Media type’ı veren string. Ayarlanmazsa, dosya adı veya path kullanılarak media type tahmin edilir. +* `filename` - Ayarlanırsa response içindeki `Content-Disposition`’a dahil edilir. + +File response'ları uygun `Content-Length`, `Last-Modified` ve `ETag` header’larını içerir. + +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} + +`response_class` parametresini de kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} + +Bu durumda *path operation* function’ınızdan doğrudan dosya path'ini döndürebilirsiniz. + +## Özel response class { #custom-response-class } + +`Response`’dan türeterek kendi özel response class’ınızı oluşturabilir ve kullanabilirsiniz. + +Örneğin, dahil gelen `ORJSONResponse` class’ında kullanılmayan bazı özel ayarlarla `orjson` kullanmak istediğinizi varsayalım. + +Diyelim ki girintili ve biçimlendirilmiş JSON döndürmek istiyorsunuz; bunun için `orjson.OPT_INDENT_2` seçeneğini kullanmak istiyorsunuz. + +Bir `CustomORJSONResponse` oluşturabilirsiniz. Burada yapmanız gereken temel şey, content’i `bytes` olarak döndüren bir `Response.render(content)` metodu yazmaktır: + +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} + +Artık şunu döndürmek yerine: + +```json +{"message": "Hello World"} +``` + +...bu response şunu döndürür: + +```json +{ + "message": "Hello World" +} +``` + +Elbette JSON’u formatlamaktan çok daha iyi şekillerde bundan faydalanabilirsiniz. 😉 + +## Varsayılan response class { #default-response-class } + +Bir **FastAPI** class instance’ı veya bir `APIRouter` oluştururken, varsayılan olarak hangi response class’ının kullanılacağını belirtebilirsiniz. + +Bunu tanımlayan parametre `default_response_class`’tır. + +Aşağıdaki örnekte **FastAPI**, tüm *path operations* için varsayılan olarak `JSONResponse` yerine `ORJSONResponse` kullanır. + +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} + +/// tip | İpucu + +Daha önce olduğu gibi, *path operations* içinde `response_class`’ı yine override edebilirsiniz. + +/// + +## Ek dokümantasyon { #additional-documentation } + +OpenAPI’de media type’ı ve daha birçok detayı `responses` kullanarak da tanımlayabilirsiniz: [OpenAPI’de Ek Response'lar](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/advanced/dataclasses.md b/docs/tr/docs/advanced/dataclasses.md new file mode 100644 index 000000000..263976007 --- /dev/null +++ b/docs/tr/docs/advanced/dataclasses.md @@ -0,0 +1,95 @@ +# Dataclass Kullanımı { #using-dataclasses } + +FastAPI, **Pydantic** üzerine inşa edilmiştir ve request/response tanımlamak için Pydantic model'lerini nasıl kullanacağınızı gösteriyordum. + +Ancak FastAPI, `dataclasses` kullanmayı da aynı şekilde destekler: + +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} + +Bu destek hâlâ **Pydantic** sayesinde vardır; çünkü Pydantic, `dataclasses` için dahili destek sunar. + +Yani yukarıdaki kod Pydantic'i doğrudan kullanmasa bile, FastAPI bu standart dataclass'ları Pydantic'in kendi dataclass biçimine dönüştürmek için Pydantic'i kullanmaktadır. + +Ve elbette aynı özellikleri destekler: + +* veri doğrulama (data validation) +* veri serileştirme (data serialization) +* veri dokümantasyonu (data documentation), vb. + +Bu, Pydantic model'lerinde olduğu gibi çalışır. Aslında arka planda da aynı şekilde, Pydantic kullanılarak yapılır. + +/// info | Bilgi + +Dataclass'ların, Pydantic model'lerinin yapabildiği her şeyi yapamadığını unutmayın. + +Bu yüzden yine de Pydantic model'lerini kullanmanız gerekebilir. + +Ancak elinizde zaten bir sürü dataclass varsa, bunları FastAPI ile bir web API'yi beslemek için kullanmak güzel bir numaradır. 🤓 + +/// + +## `response_model` İçinde Dataclass'lar { #dataclasses-in-response-model } + +`response_model` parametresinde `dataclasses` da kullanabilirsiniz: + +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} + +Dataclass otomatik olarak bir Pydantic dataclass'ına dönüştürülür. + +Bu sayede şeması API docs kullanıcı arayüzünde görünür: + + + +## İç İçe Veri Yapılarında Dataclass'lar { #dataclasses-in-nested-data-structures } + +İç içe veri yapıları oluşturmak için `dataclasses` ile diğer type annotation'ları da birleştirebilirsiniz. + +Bazı durumlarda yine de Pydantic'in `dataclasses` sürümünü kullanmanız gerekebilir. Örneğin, otomatik oluşturulan API dokümantasyonunda hata alıyorsanız. + +Bu durumda standart `dataclasses` yerine, drop-in replacement olan `pydantic.dataclasses` kullanabilirsiniz: + +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} + +1. `field` hâlâ standart `dataclasses` içinden import edilir. + +2. `pydantic.dataclasses`, `dataclasses` için bir drop-in replacement'tır. + +3. `Author` dataclass'ı, `Item` dataclass'larından oluşan bir liste içerir. + +4. `Author` dataclass'ı, `response_model` parametresi olarak kullanılır. + +5. Request body olarak dataclass'larla birlikte diğer standart type annotation'ları da kullanabilirsiniz. + + Bu örnekte, `Item` dataclass'larından oluşan bir listedir. + +6. Burada `items` içeren bir dictionary döndürüyoruz; `items` bir dataclass listesi. + + FastAPI, veriyi JSON'a serializing etmeyi yine başarır. + +7. Burada `response_model`, `Author` dataclass'larından oluşan bir listenin type annotation'ını kullanıyor. + + Yine `dataclasses` ile standart type annotation'ları birleştirebilirsiniz. + +8. Bu *path operation function*, `async def` yerine normal `def` kullanıyor. + + Her zaman olduğu gibi, FastAPI'de ihtiyaca göre `def` ve `async def`’i birlikte kullanabilirsiniz. + + Hangisini ne zaman kullanmanız gerektiğine dair hızlı bir hatırlatma isterseniz, [`async` ve `await`](../async.md#in-a-hurry){.internal-link target=_blank} dokümanındaki _"In a hurry?"_ bölümüne bakın. + +9. Bu *path operation function* dataclass döndürmüyor (isterse döndürebilir), onun yerine dahili verilerle bir dictionary listesi döndürüyor. + + FastAPI, response'u dönüştürmek için (dataclass'ları içeren) `response_model` parametresini kullanacaktır. + +Karmaşık veri yapıları oluşturmak için `dataclasses` ile diğer type annotation'ları pek çok farklı kombinasyonda birleştirebilirsiniz. + +Daha spesifik ayrıntılar için yukarıdaki kod içi annotation ipuçlarına bakın. + +## Daha Fazla Öğrenin { #learn-more } + +`dataclasses`'ı diğer Pydantic model'leriyle de birleştirebilir, onlardan kalıtım alabilir, kendi model'lerinize dahil edebilirsiniz, vb. + +Daha fazlası için Pydantic'in dataclasses dokümantasyonuna bakın. + +## Sürüm { #version } + +Bu özellik FastAPI `0.67.0` sürümünden beri mevcuttur. 🔖 diff --git a/docs/tr/docs/advanced/events.md b/docs/tr/docs/advanced/events.md new file mode 100644 index 000000000..257b952f9 --- /dev/null +++ b/docs/tr/docs/advanced/events.md @@ -0,0 +1,165 @@ +# Lifespan Olayları { #lifespan-events } + +Uygulama **başlamadan** önce çalıştırılması gereken mantığı (kodu) tanımlayabilirsiniz. Bu, bu kodun **bir kez**, uygulama **request almaya başlamadan önce** çalıştırılacağı anlamına gelir. + +Benzer şekilde, uygulama **kapanırken** çalıştırılması gereken mantığı (kodu) da tanımlayabilirsiniz. Bu durumda bu kod, muhtemelen **çok sayıda request** işlendi **sonra**, **bir kez** çalıştırılır. + +Bu kod, uygulama request almaya **başlamadan** önce ve request’leri işlemeyi **bitirdikten** hemen sonra çalıştığı için, uygulamanın tüm **lifespan**’ını (birazdan "lifespan" kelimesi önemli olacak 😉) kapsar. + +Bu yaklaşım, tüm uygulama boyunca kullanacağınız ve request’ler arasında **paylaşılan** **resource**’ları kurmak ve/veya sonrasında bunları **temizlemek** için çok faydalıdır. Örneğin bir veritabanı connection pool’u ya da paylaşılan bir machine learning modelini yüklemek gibi. + +## Kullanım Senaryosu { #use-case } + +Önce bir **kullanım senaryosu** örneğiyle başlayalım, sonra bunu bununla nasıl çözeceğimize bakalım. + +Request’leri işlemek için kullanmak istediğiniz bazı **machine learning modelleriniz** olduğunu hayal edelim. 🤖 + +Aynı modeller request’ler arasında paylaşılır; yani request başına bir model, kullanıcı başına bir model vb. gibi değil. + +Modeli yüklemenin, diskten çok fazla **data** okunması gerektiği için **oldukça uzun sürebildiğini** düşünelim. Dolayısıyla bunu her request için yapmak istemezsiniz. + +Modeli modülün/dosyanın en üst seviyesinde yükleyebilirdiniz; ancak bu, basit bir otomatik test çalıştırdığınızda bile **modelin yükleneceği** anlamına gelir. Böyle olunca test, kodun bağımsız bir kısmını çalıştırabilmek için önce modelin yüklenmesini beklemek zorunda kalır ve **yavaş** olur. + +Burada çözeceğimiz şey bu: modeli request’ler işlenmeden önce yükleyelim, ama kod yüklenirken değil; yalnızca uygulama request almaya başlamadan hemen önce. + +## Lifespan { #lifespan } + +Bu *startup* ve *shutdown* mantığını, `FastAPI` uygulamasının `lifespan` parametresi ve bir "context manager" kullanarak tanımlayabilirsiniz (bunun ne olduğunu birazdan göstereceğim). + +Önce bir örnekle başlayıp sonra ayrıntılarına bakalım. + +Aşağıdaki gibi `yield` kullanan async bir `lifespan()` fonksiyonu oluşturuyoruz: + +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} + +Burada, `yield` öncesinde (sahte) model fonksiyonunu machine learning modellerini içeren dictionary’e koyarak, modeli yükleme gibi maliyetli bir *startup* işlemini simüle ediyoruz. Bu kod, *startup* sırasında, uygulama **request almaya başlamadan önce** çalıştırılır. + +Ardından `yield`’den hemen sonra modeli bellekten kaldırıyoruz (unload). Bu kod, uygulama **request’leri işlemeyi bitirdikten sonra**, *shutdown*’dan hemen önce çalıştırılır. Örneğin memory veya GPU gibi resource’ları serbest bırakabilir. + +/// tip | İpucu + +`shutdown`, uygulamayı **durdurduğunuzda** gerçekleşir. + +Belki yeni bir sürüm başlatmanız gerekiyordur, ya da çalıştırmaktan sıkılmışsınızdır. 🤷 + +/// + +### Lifespan fonksiyonu { #lifespan-function } + +Dikkat edilmesi gereken ilk şey, `yield` içeren async bir fonksiyon tanımlıyor olmamız. Bu, `yield` kullanan Dependencies’e oldukça benzer. + +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} + +Fonksiyonun `yield`’den önceki kısmı, uygulama başlamadan **önce** çalışır. + +`yield`’den sonraki kısım ise, uygulama işini bitirdikten **sonra** çalışır. + +### Async Context Manager { #async-context-manager } + +Bakarsanız, fonksiyon `@asynccontextmanager` ile dekore edilmiş. + +Bu da fonksiyonu "**async context manager**" denen şeye dönüştürür. + +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} + +Python’da **context manager**, `with` ifadesi içinde kullanabildiğiniz bir yapıdır. Örneğin `open()` bir context manager olarak kullanılabilir: + +```Python +with open("file.txt") as file: + file.read() +``` + +Python’ın güncel sürümlerinde bir de **async context manager** vardır. Bunu `async with` ile kullanırsınız: + +```Python +async with lifespan(app): + await do_stuff() +``` + +Yukarıdaki gibi bir context manager veya async context manager oluşturduğunuzda, yaptığı şey şudur: `with` bloğuna girmeden önce `yield`’den önceki kodu çalıştırır, `with` bloğundan çıktıktan sonra da `yield`’den sonraki kodu çalıştırır. + +Yukarıdaki kod örneğimizde bunu doğrudan kullanmıyoruz; bunun yerine FastAPI’ye veriyoruz ki o kullansın. + +`FastAPI` uygulamasının `lifespan` parametresi bir **async context manager** alır; dolayısıyla oluşturduğumuz yeni `lifespan` async context manager’ını buraya geçebiliriz. + +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} + +## Alternatif Events (kullanımdan kaldırıldı) { #alternative-events-deprecated } + +/// warning | Uyarı + +*startup* ve *shutdown* işlemlerini yönetmenin önerilen yolu, yukarıda anlatıldığı gibi `FastAPI` uygulamasının `lifespan` parametresini kullanmaktır. Bir `lifespan` parametresi sağlarsanız, `startup` ve `shutdown` event handler’ları artık çağrılmaz. Ya tamamen `lifespan` ya da tamamen events; ikisi birden değil. + +Muhtemelen bu bölümü atlayabilirsiniz. + +/// + +*startup* ve *shutdown* sırasında çalıştırılacak bu mantığı tanımlamanın alternatif bir yolu daha vardır. + +Uygulama başlamadan önce veya uygulama kapanırken çalıştırılması gereken event handler’ları (fonksiyonları) tanımlayabilirsiniz. + +Bu fonksiyonlar `async def` ile veya normal `def` ile tanımlanabilir. + +### `startup` eventi { #startup-event } + +Uygulama başlamadan önce çalıştırılacak bir fonksiyon eklemek için, `"startup"` event’i ile tanımlayın: + +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} + +Bu durumda `startup` event handler fonksiyonu, "database" öğesini (sadece bir `dict`) bazı değerlerle başlatır. + +Birden fazla event handler fonksiyonu ekleyebilirsiniz. + +Ve tüm `startup` event handler’ları tamamlanmadan uygulamanız request almaya başlamaz. + +### `shutdown` eventi { #shutdown-event } + +Uygulama kapanırken çalıştırılacak bir fonksiyon eklemek için, `"shutdown"` event’i ile tanımlayın: + +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} + +Burada `shutdown` event handler fonksiyonu, `log.txt` dosyasına `"Application shutdown"` satırını yazar. + +/// info | Bilgi + +`open()` fonksiyonunda `mode="a"` "append" anlamına gelir; yani satır, önceki içeriği silmeden dosyada ne varsa onun sonuna eklenir. + +/// + +/// tip | İpucu + +Dikkat edin, bu örnekte bir dosyayla etkileşen standart Python `open()` fonksiyonunu kullanıyoruz. + +Dolayısıyla disk’e yazılmasını beklemeyi gerektiren I/O (input/output) söz konusu. + +Ancak `open()` `async` ve `await` kullanmaz. + +Bu yüzden event handler fonksiyonunu `async def` yerine standart `def` ile tanımlarız. + +/// + +### `startup` ve `shutdown` birlikte { #startup-and-shutdown-together } + +*startup* ve *shutdown* mantığınızın birbiriyle bağlantılı olma ihtimali yüksektir; bir şeyi başlatıp sonra bitirmek, bir resource edinip sonra serbest bırakmak vb. isteyebilirsiniz. + +Bunu, ortak mantık veya değişken paylaşmayan ayrı fonksiyonlarda yapmak daha zordur; çünkü değerleri global değişkenlerde tutmanız veya benzer numaralar yapmanız gerekir. + +Bu nedenle artık bunun yerine, yukarıda açıklandığı gibi `lifespan` kullanmanız önerilmektedir. + +## Teknik Detaylar { #technical-details } + +Meraklı nerd’ler için küçük bir teknik detay. 🤓 + +Altta, ASGI teknik spesifikasyonunda bu, Lifespan Protocol’ün bir parçasıdır ve `startup` ile `shutdown` adında event’ler tanımlar. + +/// info | Bilgi + +Starlette `lifespan` handler’ları hakkında daha fazlasını Starlette's Lifespan docs içinde okuyabilirsiniz. + +Ayrıca kodunuzun başka bölgelerinde de kullanılabilecek lifespan state’i nasıl yöneteceğinizi de kapsar. + +/// + +## Alt Uygulamalar { #sub-applications } + +🚨 Unutmayın: Bu lifespan event’leri (`startup` ve `shutdown`) yalnızca ana uygulama için çalıştırılır; [Alt Uygulamalar - Mounts](sub-applications.md){.internal-link target=_blank} için çalıştırılmaz. diff --git a/docs/tr/docs/advanced/generate-clients.md b/docs/tr/docs/advanced/generate-clients.md new file mode 100644 index 000000000..af278f2fe --- /dev/null +++ b/docs/tr/docs/advanced/generate-clients.md @@ -0,0 +1,208 @@ +# SDK Üretme { #generating-sdks } + +**FastAPI**, **OpenAPI** spesifikasyonunu temel aldığı için API'leri birçok aracın anlayabildiği standart bir formatta tanımlanabilir. + +Bu sayede güncel **dokümantasyon**, birden fazla dilde istemci kütüphaneleri (**SDKs**) ve kodunuzla senkron kalan **test** veya **otomasyon iş akışları** üretmek kolaylaşır. + +Bu rehberde, FastAPI backend'iniz için bir **TypeScript SDK** üretmeyi öğreneceksiniz. + +## Açık Kaynak SDK Üreteçleri { #open-source-sdk-generators } + +Esnek bir seçenek olan OpenAPI Generator, **birçok programlama dilini** destekler ve OpenAPI spesifikasyonunuzdan SDK üretebilir. + +**TypeScript client**'lar için Hey API, TypeScript ekosistemi için özel olarak tasarlanmış, optimize bir deneyim sunan bir çözümdür. + +Daha fazla SDK üretecini OpenAPI.Tools üzerinde keşfedebilirsiniz. + +/// tip | İpucu + +FastAPI otomatik olarak **OpenAPI 3.1** spesifikasyonları üretir; bu yüzden kullanacağınız aracın bu sürümü desteklemesi gerekir. + +/// + +## FastAPI Sponsorlarından SDK Üreteçleri { #sdk-generators-from-fastapi-sponsors } + +Bu bölüm, FastAPI'yi sponsorlayan şirketlerin sunduğu **yatırım destekli** ve **şirket destekli** çözümleri öne çıkarır. Bu ürünler, yüksek kaliteli üretilen SDK'ların üzerine **ek özellikler** ve **entegrasyonlar** sağlar. + +✨ [**FastAPI'ye sponsor olarak**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ bu şirketler, framework'ün ve **ekosisteminin** sağlıklı ve **sürdürülebilir** kalmasına yardımcı olur. + +Sponsor olmaları aynı zamanda FastAPI **topluluğuna** (size) güçlü bir bağlılığı da gösterir; yalnızca **iyi bir hizmet** sunmayı değil, aynı zamanda **güçlü ve gelişen bir framework** olan FastAPI'yi desteklemeyi de önemsediklerini gösterir. 🙇 + +Örneğin şunları deneyebilirsiniz: + +* Speakeasy +* Stainless +* liblab + +Bu çözümlerin bazıları açık kaynak olabilir veya ücretsiz katman sunabilir; yani finansal bir taahhüt olmadan deneyebilirsiniz. Başka ticari SDK üreteçleri de vardır ve internette bulunabilir. 🤓 + +## TypeScript SDK Oluşturma { #create-a-typescript-sdk } + +Basit bir FastAPI uygulamasıyla başlayalım: + +{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} + +*Path operation*'ların, request payload ve response payload için kullandıkları modelleri `Item` ve `ResponseMessage` modelleriyle tanımladıklarına dikkat edin. + +### API Dokümanları { #api-docs } + +`/docs` adresine giderseniz, request'lerde gönderilecek ve response'larda alınacak veriler için **schema**'ları içerdiğini görürsünüz: + + + +Bu schema'ları görebilirsiniz, çünkü uygulamada modellerle birlikte tanımlandılar. + +Bu bilgi uygulamanın **OpenAPI schema**'sında bulunur ve sonrasında API dokümanlarında gösterilir. + +OpenAPI'ye dahil edilen, modellerden gelen bu bilginin aynısı **client code üretmek** için kullanılabilir. + +### Hey API { #hey-api } + +Modelleri olan bir FastAPI uygulamamız olduğunda, Hey API ile bir TypeScript client üretebiliriz. Bunu yapmanın en hızlı yolu npx kullanmaktır. + +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client +``` + +Bu komut `./src/client` içine bir TypeScript SDK üretecektir. + +Web sitelerinde `@hey-api/openapi-ts` kurulumunu öğrenebilir ve üretilen çıktıyı inceleyebilirsiniz. + +### SDK'yı Kullanma { #using-the-sdk } + +Artık client code'u import edip kullanabilirsiniz. Şuna benzer görünebilir; method'lar için otomatik tamamlama aldığınıza dikkat edin: + + + +Ayrıca gönderilecek payload için de otomatik tamamlama alırsınız: + + + +/// tip | İpucu + +`name` ve `price` için otomatik tamamlamaya dikkat edin; bunlar FastAPI uygulamasında, `Item` modelinde tanımlanmıştı. + +/// + +Gönderdiğiniz veriler için satır içi hatalar (inline errors) da alırsınız: + + + +Response objesi de otomatik tamamlama sunacaktır: + + + +## Tag'lerle FastAPI Uygulaması { #fastapi-app-with-tags } + +Birçok durumda FastAPI uygulamanız daha büyük olacaktır ve farklı *path operation* gruplarını ayırmak için muhtemelen tag'leri kullanacaksınız. + +Örneğin **items** için bir bölüm, **users** için başka bir bölüm olabilir ve bunları tag'lerle ayırabilirsiniz: + +{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} + +### Tag'lerle TypeScript Client Üretme { #generate-a-typescript-client-with-tags } + +Tag'leri kullanan bir FastAPI uygulaması için client ürettiğinizde, genelde client code da tag'lere göre ayrılır. + +Bu sayede client code tarafında her şey doğru şekilde sıralanır ve gruplandırılır: + + + +Bu örnekte şunlar var: + +* `ItemsService` +* `UsersService` + +### Client Method İsimleri { #client-method-names } + +Şu an üretilen `createItemItemsPost` gibi method isimleri çok temiz görünmüyor: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...çünkü client üreteci, her *path operation* için OpenAPI'nin dahili **operation ID** değerini kullanır. + +OpenAPI, her operation ID'nin tüm *path operation*'lar arasında benzersiz olmasını ister. Bu yüzden FastAPI; operation ID'yi benzersiz tutabilmek için **function adı**, **path** ve **HTTP method/operation** bilgilerini birleştirerek üretir. + +Ancak bunu bir sonraki adımda nasıl iyileştirebileceğinizi göstereceğim. 🤓 + +## Özel Operation ID'ler ve Daha İyi Method İsimleri { #custom-operation-ids-and-better-method-names } + +Bu operation ID'lerin **üretilme** şeklini **değiştirerek**, client'larda daha basit **method isimleri** elde edebilirsiniz. + +Bu durumda, her operation ID'nin **benzersiz** olduğundan başka bir şekilde emin olmanız gerekir. + +Örneğin, her *path operation*'ın bir tag'i olmasını sağlayabilir ve operation ID'yi **tag** ve *path operation* **adı**na (function adı) göre üretebilirsiniz. + +### Benzersiz ID Üreten Özel Fonksiyon { #custom-generate-unique-id-function } + +FastAPI, her *path operation* için bir **unique ID** kullanır. Bu ID, **operation ID** için ve ayrıca request/response'lar için gerekebilecek özel model isimleri için de kullanılır. + +Bu fonksiyonu özelleştirebilirsiniz. Bir `APIRoute` alır ve string döndürür. + +Örneğin burada ilk tag'i (muhtemelen tek tag'iniz olur) ve *path operation* adını (function adı) kullanıyor. + +Sonrasında bu özel fonksiyonu `generate_unique_id_function` parametresiyle **FastAPI**'ye geçebilirsiniz: + +{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} + +### Özel Operation ID'lerle TypeScript Client Üretme { #generate-a-typescript-client-with-custom-operation-ids } + +Artık client'ı tekrar üretirseniz, geliştirilmiş method isimlerini göreceksiniz: + + + +Gördüğünüz gibi method isimleri artık önce tag'i, sonra function adını içeriyor; URL path'i ve HTTP operation bilgisini artık taşımıyor. + +### Client Üretecine Vermeden Önce OpenAPI Spesifikasyonunu Ön İşlemek { #preprocess-the-openapi-specification-for-the-client-generator } + +Üretilen kodda hâlâ bazı **tekrarlanan bilgiler** var. + +Bu method'un **items** ile ilişkili olduğunu zaten biliyoruz; çünkü bu kelime `ItemsService` içinde var (tag'den geliyor). Ama method adında da tag adı önek olarak duruyor. 😕 + +OpenAPI genelinde muhtemelen bunu korumak isteriz; çünkü operation ID'lerin **benzersiz** olmasını sağlar. + +Ancak üretilen client için, client'ları üretmeden hemen önce OpenAPI operation ID'lerini **değiştirip**, method isimlerini daha hoş ve **temiz** hale getirebiliriz. + +OpenAPI JSON'u `openapi.json` diye bir dosyaya indirip, şu tarz bir script ile **öndeki tag'i kaldırabiliriz**: + +{* ../../docs_src/generate_clients/tutorial004_py39.py *} + +//// tab | Node.js + +```Javascript +{!> ../../docs_src/generate_clients/tutorial004.js!} +``` + +//// + +Bununla operation ID'ler `items-get_items` gibi değerlerden sadece `get_items` olacak şekilde yeniden adlandırılır; böylece client üreteci daha basit method isimleri üretebilir. + +### Ön İşlenmiş OpenAPI ile TypeScript Client Üretme { #generate-a-typescript-client-with-the-preprocessed-openapi } + +Sonuç artık bir `openapi.json` dosyasında olduğuna göre, input konumunu güncellemeniz gerekir: + +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client +``` + +Yeni client'ı ürettikten sonra, tüm **otomatik tamamlama**, **satır içi hatalar**, vb. ile birlikte **temiz method isimleri** elde edersiniz: + + + +## Faydalar { #benefits } + +Otomatik üretilen client'ları kullanınca şu alanlarda **otomatik tamamlama** elde edersiniz: + +* Method'lar. +* Body'deki request payload'ları, query parametreleri, vb. +* Response payload'ları. + +Ayrıca her şey için **satır içi hatalar** (inline errors) da olur. + +Backend kodunu her güncellediğinizde ve frontend'i **yeniden ürettiğinizde**, yeni *path operation*'lar method olarak eklenir, eskileri kaldırılır ve diğer değişiklikler de üretilen koda yansır. 🤓 + +Bu, bir şey değiştiğinde client code'a otomatik olarak **yansıyacağı** anlamına gelir. Ayrıca client'ı **build** ettiğinizde, kullanılan verilerde bir **uyuşmazlık** (mismatch) varsa hata alırsınız. + +Böylece üretimde son kullanıcılara hata yansımasını beklemek ve sonra sorunun nerede olduğunu debug etmeye çalışmak yerine, geliştirme sürecinin çok erken aşamalarında **birçok hatayı tespit edersiniz**. ✨ diff --git a/docs/tr/docs/advanced/middleware.md b/docs/tr/docs/advanced/middleware.md new file mode 100644 index 000000000..a22644a09 --- /dev/null +++ b/docs/tr/docs/advanced/middleware.md @@ -0,0 +1,97 @@ +# İleri Seviye Middleware { #advanced-middleware } + +Ana tutorial'da uygulamanıza [Özel Middleware](../tutorial/middleware.md){.internal-link target=_blank} eklemeyi gördünüz. + +Ardından [`CORSMiddleware` ile CORS'u yönetmeyi](../tutorial/cors.md){.internal-link target=_blank} de okudunuz. + +Bu bölümde diğer middleware'leri nasıl kullanacağımıza bakacağız. + +## ASGI middleware'leri ekleme { #adding-asgi-middlewares } + +**FastAPI**, Starlette üzerine kurulu olduğu ve ASGI spesifikasyonunu uyguladığı için, herhangi bir ASGI middleware'ini kullanabilirsiniz. + +Bir middleware'in çalışması için özellikle FastAPI ya da Starlette için yazılmış olması gerekmez; ASGI spec'ine uyduğu sürece yeterlidir. + +Genel olarak ASGI middleware'leri, ilk argüman olarak bir ASGI app almayı bekleyen class'lar olur. + +Dolayısıyla üçüncü taraf ASGI middleware'lerinin dokümantasyonunda muhtemelen şöyle bir şey yapmanızı söylerler: + +```Python +from unicorn import UnicornMiddleware + +app = SomeASGIApp() + +new_app = UnicornMiddleware(app, some_config="rainbow") +``` + +Ancak FastAPI (aslında Starlette) bunu yapmanın daha basit bir yolunu sunar; böylece dahili middleware'ler server hatalarını doğru şekilde ele alır ve özel exception handler'lar düzgün çalışır. + +Bunun için `app.add_middleware()` kullanırsınız (CORS örneğindeki gibi). + +```Python +from fastapi import FastAPI +from unicorn import UnicornMiddleware + +app = FastAPI() + +app.add_middleware(UnicornMiddleware, some_config="rainbow") +``` + +`app.add_middleware()` ilk argüman olarak bir middleware class'ı alır ve middleware'e aktarılacak ek argümanları da kabul eder. + +## Entegre middleware'ler { #integrated-middlewares } + +**FastAPI**, yaygın kullanım senaryoları için birkaç middleware içerir; şimdi bunları nasıl kullanacağımıza bakacağız. + +/// note | Teknik Detaylar + +Bir sonraki örneklerde `from starlette.middleware.something import SomethingMiddleware` kullanmanız da mümkündür. + +**FastAPI**, size (geliştirici olarak) kolaylık olsun diye `fastapi.middleware` içinde bazı middleware'leri sağlar. Ancak mevcut middleware'lerin çoğu doğrudan Starlette'ten gelir. + +/// + +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } + +Gelen tüm request'lerin `https` veya `wss` olmasını zorunlu kılar. + +`http` veya `ws` olarak gelen herhangi bir request, bunun yerine güvenli şemaya redirect edilir. + +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} + +## `TrustedHostMiddleware` { #trustedhostmiddleware } + +HTTP Host Header saldırılarına karşı korunmak için, gelen tüm request'lerde `Host` header'ının doğru ayarlanmış olmasını zorunlu kılar. + +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} + +Aşağıdaki argümanlar desteklenir: + +* `allowed_hosts` - Hostname olarak izin verilmesi gereken domain adlarının listesi. `*.example.com` gibi wildcard domain'ler subdomain eşleştirmesi için desteklenir. Herhangi bir hostname'e izin vermek için `allowed_hosts=["*"]` kullanın veya middleware'i hiç eklemeyin. +* `www_redirect` - True olarak ayarlanırsa, izin verilen host'ların www olmayan sürümlerine gelen request'ler www sürümlerine redirect edilir. Varsayılanı `True`'dur. + +Gelen bir request doğru şekilde doğrulanmazsa `400` response gönderilir. + +## `GZipMiddleware` { #gzipmiddleware } + +`Accept-Encoding` header'ında `"gzip"` içeren herhangi bir request için GZip response'larını yönetir. + +Middleware hem standart hem de streaming response'ları ele alır. + +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} + +Aşağıdaki argümanlar desteklenir: + +* `minimum_size` - Bayt cinsinden bu minimum boyuttan küçük response'lara GZip uygulama. Varsayılanı `500`'dür. +* `compresslevel` - GZip sıkıştırması sırasında kullanılır. 1 ile 9 arasında bir tamsayıdır. Varsayılanı `9`'dur. Daha düşük değer daha hızlı sıkıştırma ama daha büyük dosya boyutları üretir; daha yüksek değer daha yavaş sıkıştırma ama daha küçük dosya boyutları üretir. + +## Diğer middleware'ler { #other-middlewares } + +Başka birçok ASGI middleware'i vardır. + +Örneğin: + +* Uvicorn'un `ProxyHeadersMiddleware`'i +* MessagePack + +Diğer mevcut middleware'leri görmek için Starlette'in Middleware dokümanlarına ve ASGI Awesome List listesine bakın. diff --git a/docs/tr/docs/advanced/openapi-callbacks.md b/docs/tr/docs/advanced/openapi-callbacks.md new file mode 100644 index 000000000..61135b7e0 --- /dev/null +++ b/docs/tr/docs/advanced/openapi-callbacks.md @@ -0,0 +1,186 @@ +# OpenAPI Callback'leri { #openapi-callbacks } + +Başka biri tarafından (muhtemelen API'nizi *kullanacak* olan aynı geliştirici tarafından) oluşturulmuş bir *external API*'ye request tetikleyebilen bir *path operation* ile bir API oluşturabilirsiniz. + +API uygulamanızın *external API*'yi çağırdığı sırada gerçekleşen sürece "callback" denir. Çünkü dış geliştiricinin yazdığı yazılım API'nize bir request gönderir ve ardından API'niz *geri çağrı* yaparak (*call back*), bir *external API*'ye request gönderir (muhtemelen aynı geliştiricinin oluşturduğu). + +Bu durumda, o external API'nin nasıl görünmesi *gerektiğini* dokümante etmek isteyebilirsiniz. Hangi *path operation*'a sahip olmalı, hangi body'yi beklemeli, hangi response'u döndürmeli, vb. + +## Callback'leri olan bir uygulama { #an-app-with-callbacks } + +Bunların hepsine bir örnekle bakalım. + +Fatura oluşturmayı sağlayan bir uygulama geliştirdiğinizi düşünün. + +Bu faturaların `id`, `title` (opsiyonel), `customer` ve `total` alanları olacak. + +API'nizin kullanıcısı (external bir geliştirici) API'nizde bir POST request ile fatura oluşturacak. + +Sonra API'niz (varsayalım ki): + +* Faturayı external geliştiricinin bir müşterisine gönderir. +* Parayı tahsil eder. +* API kullanıcısına (external geliştiriciye) tekrar bir bildirim gönderir. + * Bu, external geliştiricinin sağladığı bir *external API*'ye (*sizin API'nizden*) bir POST request gönderilerek yapılır (işte bu "callback"tir). + +## Normal **FastAPI** uygulaması { #the-normal-fastapi-app } + +Önce callback eklemeden önce normal API uygulamasının nasıl görüneceğine bakalım. + +Bir `Invoice` body alacak bir *path operation*'ı ve callback için URL'yi taşıyacak `callback_url` adlı bir query parametresi olacak. + +Bu kısım oldukça standart; kodun çoğu muhtemelen size zaten tanıdık gelecektir: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} + +/// tip | İpucu + +`callback_url` query parametresi, Pydantic'in Url tipini kullanır. + +/// + +Tek yeni şey, *path operation decorator*'ına argüman olarak verilen `callbacks=invoices_callback_router.routes`. Bunun ne olduğuna şimdi bakacağız. + +## Callback'i dokümante etmek { #documenting-the-callback } + +Callback'in gerçek kodu, büyük ölçüde sizin API uygulamanıza bağlıdır. + +Ve bir uygulamadan diğerine oldukça değişebilir. + +Sadece bir-iki satır kod bile olabilir, örneğin: + +```Python +callback_url = "https://example.com/api/v1/invoices/events/" +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) +``` + +Ancak callback'in belki de en önemli kısmı, API'nizin kullanıcısının (external geliştiricinin) *external API*'yi doğru şekilde uyguladığından emin olmaktır; çünkü *sizin API'niz* callback'in request body'sinde belirli veriler gönderecektir, vb. + +Dolayısıyla sıradaki adım olarak, *sizin API'nizden* callback almak için o *external API*'nin nasıl görünmesi gerektiğini dokümante eden kodu ekleyeceğiz. + +Bu dokümantasyon, API'nizde `/docs` altındaki Swagger UI'da görünecek ve external geliştiricilere *external API*'yi nasıl inşa edeceklerini gösterecek. + +Bu örnek callback'in kendisini implemente etmiyor (o zaten tek satır kod olabilir), sadece dokümantasyon kısmını ekliyor. + +/// tip | İpucu + +Gerçek callback, sadece bir HTTP request'tir. + +Callback'i kendiniz implemente ederken HTTPX veya Requests gibi bir şey kullanabilirsiniz. + +/// + +## Callback dokümantasyon kodunu yazın { #write-the-callback-documentation-code } + +Bu kod uygulamanızda çalıştırılmayacak; sadece o *external API*'nin nasıl görünmesi gerektiğini *dokümante etmek* için gerekiyor. + +Ancak **FastAPI** ile bir API için otomatik dokümantasyonu kolayca nasıl üreteceğinizi zaten biliyorsunuz. + +O halde aynı bilgiyi kullanarak, *external API*'nin nasıl görünmesi gerektiğini dokümante edeceğiz... external API'nin implemente etmesi gereken *path operation*'ları oluşturarak (API'nizin çağıracağı olanlar). + +/// tip | İpucu + +Bir callback'i dokümante eden kodu yazarken, kendinizi *external geliştirici* olarak hayal etmek faydalı olabilir. Ve şu anda *sizin API'nizi* değil, *external API*'yi implemente ettiğinizi düşünün. + +Bu bakış açısını (external geliştiricinin bakış açısını) geçici olarak benimsemek; parametreleri nereye koyacağınızı, body için Pydantic modelini, response için modelini vb. external API tarafında nasıl tasarlayacağınızı daha net hale getirebilir. + +/// + +### Bir callback `APIRouter` oluşturun { #create-a-callback-apirouter } + +Önce bir veya daha fazla callback içerecek yeni bir `APIRouter` oluşturun. + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} + +### Callback *path operation*'ını oluşturun { #create-the-callback-path-operation } + +Callback *path operation*'ını oluşturmak için, yukarıda oluşturduğunuz aynı `APIRouter`'ı kullanın. + +Normal bir FastAPI *path operation*'ı gibi görünmelidir: + +* Muhtemelen alması gereken body'nin bir deklarasyonu olmalı, örn. `body: InvoiceEvent`. +* Ayrıca döndürmesi gereken response'un deklarasyonu da olabilir, örn. `response_model=InvoiceEventReceived`. + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} + +Normal bir *path operation*'dan 2 temel farkı vardır: + +* Gerçek bir koda ihtiyaç duymaz; çünkü uygulamanız bu kodu asla çağırmayacak. Bu yalnızca *external API*'yi dokümante etmek için kullanılır. Yani fonksiyon sadece `pass` içerebilir. +* *path*, bir OpenAPI 3 expression (aşağıda daha fazlası) içerebilir; böylece parametreler ve *sizin API'nize* gönderilen orijinal request'in bazı parçalarıyla değişkenler kullanılabilir. + +### Callback path ifadesi { #the-callback-path-expression } + +Callback *path*'i, *sizin API'nize* gönderilen orijinal request'in bazı parçalarını içerebilen bir OpenAPI 3 expression barındırabilir. + +Bu örnekte, bu bir `str`: + +```Python +"{$callback_url}/invoices/{$request.body.id}" +``` + +Yani API'nizin kullanıcısı (external geliştirici) *sizin API'nize* şu adrese bir request gönderirse: + +``` +https://yourapi.com/invoices/?callback_url=https://www.external.org/events +``` + +ve JSON body şu şekilde olursa: + +```JSON +{ + "id": "2expen51ve", + "customer": "Mr. Richie Rich", + "total": "9999" +} +``` + +o zaman *sizin API'niz* faturayı işleyecek ve daha sonra bir noktada `callback_url`'ye (yani *external API*'ye) bir callback request gönderecek: + +``` +https://www.external.org/events/invoices/2expen51ve +``` + +ve JSON body yaklaşık şöyle bir şey içerecek: + +```JSON +{ + "description": "Payment celebration", + "paid": true +} +``` + +ve o *external API*'den şu gibi bir JSON body içeren response bekleyecek: + +```JSON +{ + "ok": true +} +``` + +/// tip | İpucu + +Callback URL'sinin, `callback_url` içindeki query parametresi olarak alınan URL'yi (`https://www.external.org/events`) ve ayrıca JSON body'nin içindeki fatura `id`'sini (`2expen51ve`) birlikte kullandığına dikkat edin. + +/// + +### Callback router'ını ekleyin { #add-the-callback-router } + +Bu noktada, yukarıda oluşturduğunuz callback router'ında gerekli callback *path operation*'ları (external geliştiricinin *external API*'de implemente etmesi gerekenler) hazır. + +Şimdi *sizin API'nizin path operation decorator*'ında `callbacks` parametresini kullanarak, callback router'ının `.routes` attribute'unu (bu aslında route/*path operation*'lardan oluşan bir `list`) geçin: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} + +/// tip | İpucu + +`callback=` içine router'ın kendisini (`invoices_callback_router`) değil, `invoices_callback_router.routes` şeklinde `.routes` attribute'unu verdiğinize dikkat edin. + +/// + +### Dokümanları kontrol edin { #check-the-docs } + +Artık uygulamanızı başlatıp http://127.0.0.1:8000/docs adresine gidebilirsiniz. + +*Path operation*'ınız için, *external API*'nin nasıl görünmesi gerektiğini gösteren bir "Callbacks" bölümünü içeren dokümanları göreceksiniz: + + diff --git a/docs/tr/docs/advanced/openapi-webhooks.md b/docs/tr/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..dd9e9bbe7 --- /dev/null +++ b/docs/tr/docs/advanced/openapi-webhooks.md @@ -0,0 +1,55 @@ +# OpenAPI Webhook'lar { #openapi-webhooks } + +Bazı durumlarda, API'nizi kullanan **kullanıcılara** uygulamanızın *onların* uygulamasını (request göndererek) bazı verilerle çağırabileceğini; genellikle bir tür **event** hakkında **bildirim** yapmak için kullanacağını söylemek istersiniz. + +Bu da şunu ifade eder: Kullanıcılarınızın API'nize request göndermesi şeklindeki normal akış yerine, request'i **sizin API'niz** (veya uygulamanız) **onların sistemine** (onların API'sine, onların uygulamasına) **gönderebilir**. + +Buna genellikle **webhook** denir. + +## Webhook adımları { #webhooks-steps } + +Süreç genellikle şöyledir: Kodunuzda göndereceğiniz mesajın ne olduğunu, yani request'in **body**'sini **siz tanımlarsınız**. + +Ayrıca uygulamanızın bu request'leri veya event'leri hangi **anlarda** göndereceğini de bir şekilde tanımlarsınız. + +Ve **kullanıcılarınız** da bir şekilde (örneğin bir web dashboard üzerinden) uygulamanızın bu request'leri göndermesi gereken **URL**'yi tanımlar. + +Webhook'lar için URL'lerin nasıl kaydedileceğine dair tüm **mantık** ve bu request'leri gerçekten gönderen kod tamamen size bağlıdır. Bunu **kendi kodunuzda** istediğiniz gibi yazarsınız. + +## **FastAPI** ve OpenAPI ile webhook'ları dokümante etmek { #documenting-webhooks-with-fastapi-and-openapi } + +**FastAPI** ile OpenAPI kullanarak bu webhook'ların adlarını, uygulamanızın gönderebileceği HTTP operation türlerini (örn. `POST`, `PUT`, vb.) ve uygulamanızın göndereceği request **body**'lerini tanımlayabilirsiniz. + +Bu, kullanıcılarınızın **webhook** request'lerinizi alacak şekilde **API'lerini implement etmesini** çok daha kolaylaştırabilir; hatta kendi API kodlarının bir kısmını otomatik üretebilirler. + +/// info | Bilgi + +Webhook'lar OpenAPI 3.1.0 ve üzeri sürümlerde mevcuttur; FastAPI `0.99.0` ve üzeri tarafından desteklenir. + +/// + +## Webhook'ları olan bir uygulama { #an-app-with-webhooks } + +Bir **FastAPI** uygulaması oluşturduğunuzda, *webhook*'ları tanımlamak için kullanabileceğiniz bir `webhooks` attribute'u vardır; *path operation* tanımlar gibi, örneğin `@app.webhooks.post()` ile. + +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} + +Tanımladığınız webhook'lar **OpenAPI** şemasında ve otomatik **docs UI**'da yer alır. + +/// info | Bilgi + +`app.webhooks` nesnesi aslında sadece bir `APIRouter`'dır; uygulamanızı birden fazla dosya ile yapılandırırken kullanacağınız türün aynısıdır. + +/// + +Dikkat edin: Webhook'larda aslında bir *path* (ör. `/items/`) deklare etmiyorsunuz; oraya verdiğiniz metin sadece webhook'un bir **identifier**'ıdır (event'in adı). Örneğin `@app.webhooks.post("new-subscription")` içinde webhook adı `new-subscription`'dır. + +Bunun nedeni, webhook request'ini almak istedikleri gerçek **URL path**'i **kullanıcılarınızın** başka bir şekilde (örn. bir web dashboard üzerinden) tanımlamasının beklenmesidir. + +### Dokümanları kontrol edin { #check-the-docs } + +Şimdi uygulamanızı başlatıp http://127.0.0.1:8000/docs adresine gidin. + +Dokümanlarınızda normal *path operation*'ları ve artık bazı **webhook**'ları da göreceksiniz: + + diff --git a/docs/tr/docs/advanced/path-operation-advanced-configuration.md b/docs/tr/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 000000000..e326842d6 --- /dev/null +++ b/docs/tr/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,172 @@ +# Path Operation İleri Düzey Yapılandırma { #path-operation-advanced-configuration } + +## OpenAPI operationId { #openapi-operationid } + +/// warning | Uyarı + +OpenAPI konusunda "uzman" değilseniz, muhtemelen buna ihtiyacınız yok. + +/// + +*path operation*’ınızda kullanılacak OpenAPI `operationId` değerini `operation_id` parametresiyle ayarlayabilirsiniz. + +Bunun her operation için benzersiz olduğundan emin olmanız gerekir. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} + +### operationId olarak *path operation function* adını kullanma { #using-the-path-operation-function-name-as-the-operationid } + +API’lerinizin function adlarını `operationId` olarak kullanmak istiyorsanız, hepsini dolaşıp her *path operation*’ın `operation_id` değerini `APIRoute.name` ile override edebilirsiniz. + +Bunu, tüm *path operation*’ları ekledikten sonra yapmalısınız. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} + +/// tip | İpucu + +`app.openapi()` fonksiyonunu manuel olarak çağırıyorsanız, bunu yapmadan önce `operationId`’leri güncellemelisiniz. + +/// + +/// warning | Uyarı + +Bunu yaparsanız, her bir *path operation function*’ın adının benzersiz olduğundan emin olmanız gerekir. + +Farklı modüllerde (Python dosyalarında) olsalar bile. + +/// + +## OpenAPI’den Hariç Tutma { #exclude-from-openapi } + +Bir *path operation*’ı üretilen OpenAPI şemasından (dolayısıyla otomatik dokümantasyon sistemlerinden) hariç tutmak için `include_in_schema` parametresini kullanın ve `False` yapın: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} + +## Docstring’den İleri Düzey Açıklama { #advanced-description-from-docstring } + +OpenAPI için, bir *path operation function*’ın docstring’inden kullanılacak satırları sınırlandırabilirsiniz. + +Bir `\f` (escape edilmiş "form feed" karakteri) eklerseniz, **FastAPI** OpenAPI için kullanılan çıktıyı bu noktada **keser**. + +Dokümantasyonda görünmez, ancak diğer araçlar (Sphinx gibi) geri kalan kısmı kullanabilir. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} + +## Ek Responses { #additional-responses } + +Muhtemelen bir *path operation* için `response_model` ve `status_code` tanımlamayı görmüşsünüzdür. + +Bu, bir *path operation*’ın ana response’u ile ilgili metadata’yı tanımlar. + +Ek response’ları; modelleri, status code’ları vb. ile birlikte ayrıca da tanımlayabilirsiniz. + +Dokümantasyonda bununla ilgili ayrı bir bölüm var; [OpenAPI’de Ek Responses](additional-responses.md){.internal-link target=_blank} sayfasından okuyabilirsiniz. + +## OpenAPI Extra { #openapi-extra } + +Uygulamanızda bir *path operation* tanımladığınızda, **FastAPI** OpenAPI şemasına dahil edilmek üzere o *path operation* ile ilgili metadata’yı otomatik olarak üretir. + +/// note | Teknik Detaylar + +OpenAPI spesifikasyonunda buna Operation Object denir. + +/// + +Bu, *path operation* hakkında tüm bilgileri içerir ve otomatik dokümantasyonu üretmek için kullanılır. + +`tags`, `parameters`, `requestBody`, `responses` vb. alanları içerir. + +Bu *path operation*’a özel OpenAPI şeması normalde **FastAPI** tarafından otomatik üretilir; ancak siz bunu genişletebilirsiniz. + +/// tip | İpucu + +Bu, düşük seviyeli bir genişletme noktasıdır. + +Yalnızca ek response’lar tanımlamanız gerekiyorsa, bunu yapmanın daha pratik yolu [OpenAPI’de Ek Responses](additional-responses.md){.internal-link target=_blank} kullanmaktır. + +/// + +Bir *path operation* için OpenAPI şemasını `openapi_extra` parametresiyle genişletebilirsiniz. + +### OpenAPI Extensions { #openapi-extensions } + +Örneğin bu `openapi_extra`, [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) tanımlamak için faydalı olabilir: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +Otomatik API dokümanlarını açtığınızda, extension’ınız ilgili *path operation*’ın en altında görünür. + + + +Ayrıca ortaya çıkan OpenAPI’yi (API’nizde `/openapi.json`) görüntülerseniz, extension’ınızı ilgili *path operation*’ın bir parçası olarak orada da görürsünüz: + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### Özel OpenAPI *path operation* şeması { #custom-openapi-path-operation-schema } + +`openapi_extra` içindeki dictionary, *path operation* için otomatik üretilen OpenAPI şemasıyla derinlemesine (deep) birleştirilir. + +Böylece otomatik üretilen şemaya ek veri ekleyebilirsiniz. + +Örneğin, Pydantic ile FastAPI’nin otomatik özelliklerini kullanmadan request’i kendi kodunuzla okuyup doğrulamaya karar verebilirsiniz; ancak yine de OpenAPI şemasında request’i tanımlamak isteyebilirsiniz. + +Bunu `openapi_extra` ile yapabilirsiniz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +Bu örnekte herhangi bir Pydantic model tanımlamadık. Hatta request body JSON olarak parsed bile edilmiyor; doğrudan `bytes` olarak okunuyor ve `magic_data_reader()` fonksiyonu bunu bir şekilde parse etmekten sorumlu oluyor. + +Buna rağmen, request body için beklenen şemayı tanımlayabiliriz. + +### Özel OpenAPI content type { #custom-openapi-content-type } + +Aynı yöntemi kullanarak, Pydantic model ile JSON Schema’yı tanımlayıp bunu *path operation* için özel OpenAPI şeması bölümüne dahil edebilirsiniz. + +Ve bunu, request içindeki veri tipi JSON olmasa bile yapabilirsiniz. + +Örneğin bu uygulamada, FastAPI’nin Pydantic modellerinden JSON Schema çıkarmaya yönelik entegre işlevselliğini ve JSON için otomatik doğrulamayı kullanmıyoruz. Hatta request content type’ını JSON değil, YAML olarak tanımlıyoruz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +Buna rağmen, varsayılan entegre işlevselliği kullanmasak da, YAML olarak almak istediğimiz veri için JSON Schema’yı manuel üretmek üzere bir Pydantic model kullanmaya devam ediyoruz. + +Ardından request’i doğrudan kullanıp body’yi `bytes` olarak çıkarıyoruz. Bu da FastAPI’nin request payload’ını JSON olarak parse etmeye çalışmayacağı anlamına gelir. + +Sonrasında kodumuzda bu YAML içeriğini doğrudan parse ediyor, ardından YAML içeriğini doğrulamak için yine aynı Pydantic modeli kullanıyoruz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip | İpucu + +Burada aynı Pydantic modeli tekrar kullanıyoruz. + +Aynı şekilde, başka bir yöntemle de doğrulama yapabilirdik. + +/// diff --git a/docs/tr/docs/advanced/response-change-status-code.md b/docs/tr/docs/advanced/response-change-status-code.md new file mode 100644 index 000000000..239c0dddd --- /dev/null +++ b/docs/tr/docs/advanced/response-change-status-code.md @@ -0,0 +1,31 @@ +# Response - Status Code Değiştirme { #response-change-status-code } + +Muhtemelen daha önce varsayılan bir [Response Status Code](../tutorial/response-status-code.md){.internal-link target=_blank} ayarlayabileceğinizi okumuşsunuzdur. + +Ancak bazı durumlarda, varsayılandan farklı bir status code döndürmeniz gerekir. + +## Kullanım senaryosu { #use-case } + +Örneğin, varsayılan olarak "OK" `200` HTTP status code'u döndürmek istediğinizi düşünün. + +Ama veri mevcut değilse onu oluşturmak ve "CREATED" `201` HTTP status code'u döndürmek istiyorsunuz. + +Aynı zamanda, döndürdüğünüz veriyi bir `response_model` ile filtreleyip dönüştürebilmeyi de sürdürmek istiyorsunuz. + +Bu tür durumlarda bir `Response` parametresi kullanabilirsiniz. + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde `Response` tipinde bir parametre tanımlayabilirsiniz (cookie ve header'lar için yapabildiğiniz gibi). + +Ardından bu *geçici (temporal)* `Response` nesnesi üzerinde `status_code` değerini ayarlayabilirsiniz. + +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} + +Sonrasında, normalde yaptığınız gibi ihtiyacınız olan herhangi bir nesneyi döndürebilirsiniz (`dict`, bir veritabanı modeli, vb.). + +Ve eğer bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, status code'u (ayrıca cookie ve header'ları) bu *geçici (temporal)* response'tan alır ve `response_model` ile filtrelenmiş, sizin döndürdüğünüz değeri içeren nihai response'a yerleştirir. + +Ayrıca `Response` parametresini dependency'lerde de tanımlayıp status code'u orada ayarlayabilirsiniz. Ancak unutmayın, en son ayarlanan değer geçerli olur. diff --git a/docs/tr/docs/advanced/response-cookies.md b/docs/tr/docs/advanced/response-cookies.md new file mode 100644 index 000000000..d00bfc4cd --- /dev/null +++ b/docs/tr/docs/advanced/response-cookies.md @@ -0,0 +1,51 @@ +# Response Cookie'leri { #response-cookies } + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde `Response` tipinde bir parametre tanımlayabilirsiniz. + +Ardından bu *geçici* response nesnesi üzerinde cookie'leri set edebilirsiniz. + +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} + +Sonrasında normalde yaptığınız gibi ihtiyaç duyduğunuz herhangi bir nesneyi döndürebilirsiniz (bir `dict`, bir veritabanı modeli vb.). + +Ayrıca bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, bu *geçici* response'u cookie'leri (ayrıca header'ları ve status code'u) çıkarmak için kullanır ve bunları, döndürdüğünüz değeri içeren nihai response'a ekler. Döndürdüğünüz değer, varsa `response_model` ile filtrelenmiş olur. + +`Response` parametresini dependency'lerde de tanımlayıp, onların içinde cookie (ve header) set edebilirsiniz. + +## Doğrudan bir `Response` döndürün { #return-a-response-directly } + +Kodunuzda doğrudan bir `Response` döndürürken de cookie oluşturabilirsiniz. + +Bunu yapmak için, [Doğrudan Response Döndürme](response-directly.md){.internal-link target=_blank} bölümünde anlatıldığı gibi bir response oluşturabilirsiniz. + +Sonra bunun içinde Cookie'leri set edin ve response'u döndürün: + +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} + +/// tip | İpucu + +`Response` parametresini kullanmak yerine doğrudan bir response döndürürseniz, FastAPI onu olduğu gibi (doğrudan) döndürür. + +Bu yüzden, verinizin doğru tipte olduğundan emin olmanız gerekir. Örneğin `JSONResponse` döndürüyorsanız, verinin JSON ile uyumlu olması gerekir. + +Ayrıca `response_model` tarafından filtrelenmesi gereken bir veriyi göndermediğinizden de emin olun. + +/// + +### Daha fazla bilgi { #more-info } + +/// note | Teknik Detaylar + +`from starlette.responses import Response` veya `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `fastapi.responses` içinde `starlette.responses` ile aynı response sınıflarını sunar. Ancak mevcut response'ların büyük kısmı doğrudan Starlette'ten gelir. + +Ve `Response`, header ve cookie set etmek için sık kullanıldığından, **FastAPI** bunu `fastapi.Response` olarak da sağlar. + +/// + +Mevcut tüm parametreleri ve seçenekleri görmek için Starlette dokümantasyonuna bakın. diff --git a/docs/tr/docs/advanced/response-directly.md b/docs/tr/docs/advanced/response-directly.md new file mode 100644 index 000000000..332f1224f --- /dev/null +++ b/docs/tr/docs/advanced/response-directly.md @@ -0,0 +1,65 @@ +# Doğrudan Bir Response Döndürme { #return-a-response-directly } + +**FastAPI** ile bir *path operation* oluşturduğunuzda, normalde ondan herhangi bir veri döndürebilirsiniz: bir `dict`, bir `list`, bir Pydantic model, bir veritabanı modeli vb. + +Varsayılan olarak **FastAPI**, döndürdüğünüz bu değeri [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} bölümünde anlatılan `jsonable_encoder` ile otomatik olarak JSON'a çevirir. + +Ardından perde arkasında, JSON-uyumlu bu veriyi (ör. bir `dict`) client'a response göndermek için kullanılacak bir `JSONResponse` içine yerleştirir. + +Ancak *path operation*'larınızdan doğrudan bir `JSONResponse` döndürebilirsiniz. + +Bu, örneğin özel header'lar veya cookie'ler döndürmek istediğinizde faydalı olabilir. + +## Bir `Response` Döndürme { #return-a-response } + +Aslında herhangi bir `Response` veya onun herhangi bir alt sınıfını döndürebilirsiniz. + +/// tip | İpucu + +`JSONResponse` zaten `Response`'un bir alt sınıfıdır. + +/// + +Bir `Response` döndürdüğünüzde, **FastAPI** bunu olduğu gibi doğrudan iletir. + +Pydantic model'leriyle herhangi bir veri dönüşümü yapmaz, içeriği başka bir tipe çevirmez vb. + +Bu size ciddi bir esneklik sağlar. Herhangi bir veri türü döndürebilir, herhangi bir veri deklarasyonunu veya validasyonunu override edebilirsiniz. + +## Bir `Response` İçinde `jsonable_encoder` Kullanma { #using-the-jsonable-encoder-in-a-response } + +**FastAPI**, sizin döndürdüğünüz `Response` üzerinde hiçbir değişiklik yapmadığı için, içeriğinin gönderilmeye hazır olduğundan emin olmanız gerekir. + +Örneğin, bir Pydantic model'i, önce JSON-uyumlu tiplere çevrilmeden (`datetime`, `UUID` vb.) doğrudan bir `JSONResponse` içine koyamazsınız. Önce tüm veri tipleri JSON-uyumlu hale gelecek şekilde `dict`'e çevrilmesi gerekir. + +Bu gibi durumlarda, response'a vermeden önce verinizi dönüştürmek için `jsonable_encoder` kullanabilirsiniz: + +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} + +/// note | Teknik Detaylar + +`from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `starlette.responses` içeriğini `fastapi.responses` üzerinden de sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'tan gelir. + +/// + +## Özel Bir `Response` Döndürme { #returning-a-custom-response } + +Yukarıdaki örnek ihtiyaç duyduğunuz tüm parçaları gösteriyor, ancak henüz çok kullanışlı değil. Çünkü `item`'ı zaten doğrudan döndürebilirdiniz ve **FastAPI** varsayılan olarak onu sizin için bir `JSONResponse` içine koyup `dict`'e çevirirdi vb. + +Şimdi bunu kullanarak nasıl özel bir response döndürebileceğinize bakalım. + +Diyelim ki XML response döndürmek istiyorsunuz. + +XML içeriğinizi bir string içine koyabilir, onu bir `Response` içine yerleştirip döndürebilirsiniz: + +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +## Notlar { #notes } + +Bir `Response`'u doğrudan döndürdüğünüzde, verisi otomatik olarak validate edilmez, dönüştürülmez (serialize edilmez) veya dokümante edilmez. + +Ancak yine de [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank} bölümünde anlatıldığı şekilde dokümante edebilirsiniz. + +İlerleyen bölümlerde, otomatik veri dönüşümü, dokümantasyon vb. özellikleri korurken bu özel `Response`'ları nasıl kullanıp declare edebileceğinizi göreceksiniz. diff --git a/docs/tr/docs/advanced/response-headers.md b/docs/tr/docs/advanced/response-headers.md new file mode 100644 index 000000000..85b0799d3 --- /dev/null +++ b/docs/tr/docs/advanced/response-headers.md @@ -0,0 +1,41 @@ +# Response Header'ları { #response-headers } + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde (cookie'lerde yapabildiğiniz gibi) tipi `Response` olan bir parametre tanımlayabilirsiniz. + +Sonra da bu *geçici* response nesnesi üzerinde header'ları ayarlayabilirsiniz. + +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} + +Ardından normalde yaptığınız gibi ihtiyacınız olan herhangi bir nesneyi döndürebilirsiniz (bir `dict`, bir veritabanı modeli vb.). + +Eğer bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, header'ları (aynı şekilde cookie'leri ve status code'u) bu *geçici* response'dan alır ve döndürdüğünüz değeri (varsa bir `response_model` ile filtrelenmiş hâliyle) içeren nihai response'a ekler. + +`Response` parametresini dependency'lerde de tanımlayıp, onların içinde header (ve cookie) ayarlayabilirsiniz. + +## Doğrudan bir `Response` döndürün { #return-a-response-directly } + +Doğrudan bir `Response` döndürdüğünüzde de header ekleyebilirsiniz. + +[Bir Response'u Doğrudan Döndürün](response-directly.md){.internal-link target=_blank} bölümünde anlatıldığı gibi bir response oluşturun ve header'ları ek bir parametre olarak geçin: + +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} + +/// note | Teknik Detaylar + +`from starlette.responses import Response` veya `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. + +Ayrıca `Response` header ve cookie ayarlamak için sık kullanıldığından, **FastAPI** bunu `fastapi.Response` altında da sağlar. + +/// + +## Özel Header'lar { #custom-headers } + +Özel/proprietary header'ların `X-` prefix'i kullanılarak eklenebileceğini unutmayın. + +Ancak tarayıcıdaki bir client'ın görebilmesini istediğiniz özel header'larınız varsa, bunları CORS ayarlarınıza eklemeniz gerekir ([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank} bölümünde daha fazla bilgi), bunun için Starlette'in CORS dokümanında açıklanan `expose_headers` parametresini kullanın. diff --git a/docs/tr/docs/advanced/security/http-basic-auth.md b/docs/tr/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..b194c763e --- /dev/null +++ b/docs/tr/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,107 @@ +# HTTP Basic Auth { #http-basic-auth } + +En basit senaryolarda HTTP Basic Auth kullanabilirsiniz. + +HTTP Basic Auth’ta uygulama, içinde kullanıcı adı ve şifre bulunan bir header bekler. + +Eğer bunu almazsa HTTP 401 "Unauthorized" hatası döndürür. + +Ayrıca değeri `Basic` olan ve isteğe bağlı `realm` parametresi içerebilen `WWW-Authenticate` header’ını da döndürür. + +Bu da tarayıcıya, kullanıcı adı ve şifre için entegre giriş penceresini göstermesini söyler. + +Ardından kullanıcı adı ve şifreyi yazdığınızda tarayıcı bunları otomatik olarak header içinde gönderir. + +## Basit HTTP Basic Auth { #simple-http-basic-auth } + +* `HTTPBasic` ve `HTTPBasicCredentials` import edin. +* `HTTPBasic` kullanarak bir "`security` scheme" oluşturun. +* *path operation*’ınızda bir dependency ile bu `security`’yi kullanın. +* Bu, `HTTPBasicCredentials` tipinde bir nesne döndürür: + * İçinde gönderilen `username` ve `password` bulunur. + +{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *} + +URL’yi ilk kez açmaya çalıştığınızda (veya dokümanlardaki "Execute" butonuna tıkladığınızda) tarayıcı sizden kullanıcı adınızı ve şifrenizi ister: + + + +## Kullanıcı adını kontrol edin { #check-the-username } + +Daha kapsamlı bir örneğe bakalım. + +Kullanıcı adı ve şifrenin doğru olup olmadığını kontrol etmek için bir dependency kullanın. + +Bunun için kullanıcı adı ve şifreyi kontrol ederken Python standart modülü olan `secrets`’i kullanın. + +`secrets.compare_digest()`; `bytes` ya da yalnızca ASCII karakterleri (İngilizce’deki karakterler) içeren bir `str` almalıdır. Bu da `Sebastián` içindeki `á` gibi karakterlerle çalışmayacağı anlamına gelir. + +Bunu yönetmek için önce `username` ve `password` değerlerini UTF-8 ile encode ederek `bytes`’a dönüştürürüz. + +Sonra `secrets.compare_digest()` kullanarak `credentials.username`’in `"stanleyjobson"` ve `credentials.password`’ün `"swordfish"` olduğundan emin olabiliriz. + +{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *} + +Bu, kabaca şuna benzer olurdu: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +Ancak `secrets.compare_digest()` kullanarak, "timing attacks" denilen bir saldırı türüne karşı güvenli olursunuz. + +### Timing Attacks { #timing-attacks } + +Peki "timing attack" nedir? + +Bazı saldırganların kullanıcı adı ve şifreyi tahmin etmeye çalıştığını düşünelim. + +Ve `johndoe` kullanıcı adı ve `love123` şifresi ile bir request gönderiyorlar. + +Uygulamanızdaki Python kodu o zaman kabaca şuna denk olur: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Ancak Python, `johndoe` içindeki ilk `j` ile `stanleyjobson` içindeki ilk `s`’i karşılaştırdığı anda `False` döndürür; çünkü iki string’in aynı olmadığını zaten anlar ve "kalan harfleri karşılaştırmak için daha fazla hesaplama yapmaya gerek yok" diye düşünür. Uygulamanız da "Incorrect username or password" der. + +Sonra saldırganlar bu sefer `stanleyjobsox` kullanıcı adı ve `love123` şifresi ile dener. + +Uygulama kodunuz da şuna benzer bir şey yapar: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Bu kez Python, iki string’in aynı olmadığını fark etmeden önce hem `stanleyjobsox` hem de `stanleyjobson` içinde `stanleyjobso` kısmının tamamını karşılaştırmak zorunda kalır. Bu nedenle "Incorrect username or password" yanıtını vermesi birkaç mikro saniye daha uzun sürer. + +#### Yanıt süresi saldırganlara yardımcı olur { #the-time-to-answer-helps-the-attackers } + +Bu noktada saldırganlar, server’ın "Incorrect username or password" response’unu göndermesinin birkaç mikro saniye daha uzun sürdüğünü fark ederek _bir şeyleri_ doğru yaptıklarını anlar; yani başlangıçtaki bazı harfler doğrudur. + +Sonra tekrar denerken, bunun `johndoe`’dan ziyade `stanleyjobsox`’a daha yakın bir şey olması gerektiğini bilerek devam edebilirler. + +#### "Profesyonel" bir saldırı { #a-professional-attack } + +Elbette saldırganlar bunu elle tek tek denemez; bunu yapan bir program yazarlar. Muhtemelen saniyede binlerce ya da milyonlarca test yaparlar ve her seferinde yalnızca bir doğru harf daha elde ederler. + +Böylece birkaç dakika ya da birkaç saat içinde doğru kullanıcı adı ve şifreyi, yanıt süresini kullanarak ve uygulamamızın "yardımıyla" tahmin etmiş olurlar. + +#### `secrets.compare_digest()` ile düzeltin { #fix-it-with-secrets-compare-digest } + +Ancak bizim kodumuzda `secrets.compare_digest()` kullanıyoruz. + +Kısacası, `stanleyjobsox` ile `stanleyjobson`’u karşılaştırmak için geçen süre, `johndoe` ile `stanleyjobson`’u karşılaştırmak için geçen süreyle aynı olur. Şifre için de aynı şekilde. + +Bu sayede uygulama kodunuzda `secrets.compare_digest()` kullanarak bu güvenlik saldırıları ailesine karşı güvenli olursunuz. + +### Hatayı döndürün { #return-the-error } + +Credential’ların hatalı olduğunu tespit ettikten sonra, 401 status code ile (credential verilmediğinde dönenle aynı) bir `HTTPException` döndürün ve tarayıcının giriş penceresini yeniden göstermesi için `WWW-Authenticate` header’ını ekleyin: + +{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *} diff --git a/docs/tr/docs/advanced/security/oauth2-scopes.md b/docs/tr/docs/advanced/security/oauth2-scopes.md new file mode 100644 index 000000000..ecba7851b --- /dev/null +++ b/docs/tr/docs/advanced/security/oauth2-scopes.md @@ -0,0 +1,274 @@ +# OAuth2 scope'ları { #oauth2-scopes } + +OAuth2 scope'larını **FastAPI** ile doğrudan kullanabilirsiniz; sorunsuz çalışacak şekilde entegre edilmiştir. + +Bu sayede OAuth2 standardını takip eden, daha ince taneli bir izin sistemini OpenAPI uygulamanıza (ve API dokümanlarınıza) entegre edebilirsiniz. + +Scope'lu OAuth2; Facebook, Google, GitHub, Microsoft, X (Twitter) vb. birçok büyük kimlik doğrulama sağlayıcısının kullandığı mekanizmadır. Kullanıcı ve uygulamalara belirli izinler vermek için bunu kullanırlar. + +Facebook, Google, GitHub, Microsoft, X (Twitter) ile "giriş yaptığınızda", o uygulama scope'lu OAuth2 kullanıyor demektir. + +Bu bölümde, **FastAPI** uygulamanızda aynı scope'lu OAuth2 ile authentication ve authorization'ı nasıl yöneteceğinizi göreceksiniz. + +/// warning | Uyarı + +Bu bölüm az çok ileri seviye sayılır. Yeni başlıyorsanız atlayabilirsiniz. + +OAuth2 scope'larına mutlaka ihtiyacınız yok; authentication ve authorization'ı istediğiniz şekilde ele alabilirsiniz. + +Ancak scope'lu OAuth2, API'nize (OpenAPI ile) ve API dokümanlarınıza güzel biçimde entegre edilebilir. + +Buna rağmen, bu scope'ları (veya başka herhangi bir security/authorization gereksinimini) kodunuzda ihtiyaç duyduğunuz şekilde yine siz zorunlu kılarsınız. + +Birçok durumda scope'lu OAuth2 gereğinden fazla (overkill) olabilir. + +Ama ihtiyacınız olduğunu biliyorsanız ya da merak ediyorsanız okumaya devam edin. + +/// + +## OAuth2 scope'ları ve OpenAPI { #oauth2-scopes-and-openapi } + +OAuth2 spesifikasyonu, "scope"ları boşluklarla ayrılmış string'lerden oluşan bir liste olarak tanımlar. + +Bu string'lerin her birinin içeriği herhangi bir formatta olabilir, ancak boşluk içermemelidir. + +Bu scope'lar "izinleri" temsil eder. + +OpenAPI'de (ör. API dokümanlarında) "security scheme" tanımlayabilirsiniz. + +Bu security scheme'lerden biri OAuth2 kullanıyorsa, scope'ları da tanımlayıp kullanabilirsiniz. + +Her bir "scope" sadece bir string'dir (boşluksuz). + +Genellikle belirli güvenlik izinlerini tanımlamak için kullanılır, örneğin: + +* `users:read` veya `users:write` sık görülen örneklerdir. +* `instagram_basic` Facebook / Instagram tarafından kullanılır. +* `https://www.googleapis.com/auth/drive` Google tarafından kullanılır. + +/// info | Bilgi + +OAuth2'de "scope", gereken belirli bir izni bildiren bir string'den ibarettir. + +`:` gibi başka karakterler içermesi ya da bir URL olması önemli değildir. + +Bu detaylar implementasyon'a bağlıdır. + +OAuth2 için bunlar sadece string'dir. + +/// + +## Genel görünüm { #global-view } + +Önce, ana **Tutorial - User Guide** içindeki [Password (ve hashing) ile OAuth2, JWT token'lı Bearer](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank} örneklerinden, OAuth2 scope'larına geçince hangi kısımların değiştiğine hızlıca bakalım: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} + +Şimdi bu değişiklikleri adım adım inceleyelim. + +## OAuth2 Security scheme { #oauth2-security-scheme } + +İlk değişiklik, artık OAuth2 security scheme'ini iki adet kullanılabilir scope ile tanımlamamız: `me` ve `items`. + +`scopes` parametresi; her scope'un key, açıklamasının ise value olduğu bir `dict` alır: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *} + +Bu scope'ları tanımladığımız için, login/authorize yaptığınızda API dokümanlarında görünecekler. + +Ve hangi scope'lara erişim vermek istediğinizi seçebileceksiniz: `me` ve `items`. + +Bu, Facebook/Google/GitHub vb. ile giriş yaparken izin verdiğinizde kullanılan mekanizmanın aynısıdır: + + + +## Scope'lu JWT token { #jwt-token-with-scopes } + +Şimdi token *path operation*'ını, istenen scope'ları döndürecek şekilde değiştirin. + +Hâlâ aynı `OAuth2PasswordRequestForm` kullanılıyor. Bu form, request'te aldığı her scope için `str`'lerden oluşan bir `list` içeren `scopes` özelliğine sahiptir. + +Ve scope'ları JWT token'ın bir parçası olarak döndürüyoruz. + +/// danger | Uyarı + +Basitlik için burada, gelen scope'ları doğrudan token'a ekliyoruz. + +Ama uygulamanızda güvenlik açısından, yalnızca kullanıcının gerçekten sahip olabileceği scope'ları (veya sizin önceden tanımladıklarınızı) eklediğinizden emin olmalısınız. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} + +## *Path operation*'larda ve dependency'lerde scope tanımlama { #declare-scopes-in-path-operations-and-dependencies } + +Artık `/users/me/items/` için olan *path operation*'ın `items` scope'unu gerektirdiğini tanımlıyoruz. + +Bunun için `fastapi` içinden `Security` import edip kullanıyoruz. + +Dependency'leri (`Depends` gibi) tanımlamak için `Security` kullanabilirsiniz; fakat `Security`, ayrıca string'lerden oluşan bir scope listesi alan `scopes` parametresini de alır. + +Bu durumda `Security`'ye dependency fonksiyonu olarak `get_current_active_user` veriyoruz (`Depends` ile yaptığımız gibi). + +Ama ayrıca bir `list` olarak scope'ları da veriyoruz; burada tek bir scope var: `items` (daha fazla da olabilir). + +Ve `get_current_active_user` dependency fonksiyonu, sadece `Depends` ile değil `Security` ile de alt-dependency'ler tanımlayabilir. Kendi alt-dependency fonksiyonunu (`get_current_user`) ve daha fazla scope gereksinimini tanımlar. + +Bu örnekte `me` scope'unu gerektiriyor (birden fazla scope da isteyebilirdi). + +/// note | Not + +Farklı yerlerde farklı scope'lar eklemek zorunda değilsiniz. + +Burada, **FastAPI**'nin farklı seviyelerde tanımlanan scope'ları nasıl ele aldığını göstermek için böyle yapıyoruz. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} + +/// info | Teknik Detaylar + +`Security` aslında `Depends`'in bir alt sınıfıdır ve sadece birazdan göreceğimiz bir ek parametreye sahiptir. + +Ancak `Depends` yerine `Security` kullanınca **FastAPI**, security scope'larının tanımlanabileceğini bilir, bunları içeride kullanır ve API'yi OpenAPI ile dokümante eder. + +Fakat `fastapi` içinden `Query`, `Path`, `Depends`, `Security` vb. import ettiğiniz şeyler, aslında özel sınıflar döndüren fonksiyonlardır. + +/// + +## `SecurityScopes` kullanımı { #use-securityscopes } + +Şimdi `get_current_user` dependency'sini güncelleyelim. + +Bu fonksiyon, yukarıdaki dependency'ler tarafından kullanılıyor. + +Burada, daha önce oluşturduğumuz aynı OAuth2 scheme'i dependency olarak tanımlıyoruz: `oauth2_scheme`. + +Bu dependency fonksiyonunun kendi içinde bir scope gereksinimi olmadığı için, `oauth2_scheme` ile `Depends` kullanabiliriz; security scope'larını belirtmemiz gerekmiyorsa `Security` kullanmak zorunda değiliz. + +Ayrıca `fastapi.security` içinden import edilen, `SecurityScopes` tipinde özel bir parametre tanımlıyoruz. + +Bu `SecurityScopes` sınıfı, `Request`'e benzer (`Request`, request nesnesini doğrudan almak için kullanılmıştı). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} + +## `scopes`'ları kullanma { #use-the-scopes } + +`security_scopes` parametresi `SecurityScopes` tipinde olacaktır. + +Bu nesnenin `scopes` adlı bir özelliği vardır; bu liste, kendisinin ve bunu alt-dependency olarak kullanan tüm dependency'lerin gerektirdiği tüm scope'ları içerir. Yani tüm "dependant"lar... kafa karıştırıcı gelebilir; aşağıda tekrar açıklanıyor. + +`security_scopes` nesnesi (`SecurityScopes` sınıfından) ayrıca, bu scope'ları boşluklarla ayrılmış tek bir string olarak veren `scope_str` attribute'una sahiptir (bunu kullanacağız). + +Sonrasında birkaç farklı noktada tekrar kullanabileceğimiz (`raise` edebileceğimiz) bir `HTTPException` oluşturuyoruz. + +Bu exception içinde, gerekiyorsa, gerekli scope'ları boşlukla ayrılmış bir string olarak (`scope_str` ile) ekliyoruz. Bu scope'ları içeren string'i `WWW-Authenticate` header'ına koyuyoruz (spesifikasyonun bir parçası). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} + +## `username` ve veri şeklinin doğrulanması { #verify-the-username-and-data-shape } + +Bir `username` aldığımızı doğruluyoruz ve scope'ları çıkarıyoruz. + +Ardından bu veriyi Pydantic model'i ile doğruluyoruz (`ValidationError` exception'ını yakalayarak). JWT token'ı okurken veya Pydantic ile veriyi doğrularken bir hata olursa, daha önce oluşturduğumuz `HTTPException`'ı fırlatıyoruz. + +Bunun için Pydantic model'i `TokenData`'yı, `scopes` adlı yeni bir özellik ekleyerek güncelliyoruz. + +Veriyi Pydantic ile doğrulayarak örneğin scope'ların tam olarak `str`'lerden oluşan bir `list` olduğunu ve `username`'in bir `str` olduğunu garanti edebiliriz. + +Aksi halde, örneğin bir `dict` veya başka bir şey gelebilir; bu da daha sonra uygulamanın bir yerinde kırılmaya yol açıp güvenlik riski oluşturabilir. + +Ayrıca bu `username` ile bir kullanıcı olduğunu doğruluyoruz; yoksa yine aynı exception'ı fırlatıyoruz. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} + +## `scopes`'ların doğrulanması { #verify-the-scopes } + +Şimdi bu dependency'nin ve tüm dependant'ların ( *path operation*'lar dahil) gerektirdiği tüm scope'ların, alınan token'da sağlanan scope'lar içinde olup olmadığını doğruluyoruz; değilse `HTTPException` fırlatıyoruz. + +Bunun için, tüm bu scope'ları `str` olarak içeren bir `list` olan `security_scopes.scopes` kullanılır. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} + +## Dependency ağacı ve scope'lar { #dependency-tree-and-scopes } + +Bu dependency ağacını ve scope'ları tekrar gözden geçirelim. + +`get_current_active_user` dependency'si, alt-dependency olarak `get_current_user`'ı kullandığı için, `get_current_active_user` üzerinde tanımlanan `"me"` scope'u, `get_current_user`'a geçirilen `security_scopes.scopes` içindeki gerekli scope listesine dahil edilir. + +*Path operation*'ın kendisi de `"items"` scope'unu tanımlar; bu da `get_current_user`'a geçirilen `security_scopes.scopes` listesinde yer alır. + +Dependency'lerin ve scope'ların hiyerarşisi şöyle görünür: + +* *Path operation* `read_own_items` şunlara sahiptir: + * Dependency ile gerekli scope'lar `["items"]`: + * `get_current_active_user`: + * `get_current_active_user` dependency fonksiyonu şunlara sahiptir: + * Dependency ile gerekli scope'lar `["me"]`: + * `get_current_user`: + * `get_current_user` dependency fonksiyonu şunlara sahiptir: + * Kendisinin gerektirdiği scope yok. + * `oauth2_scheme` kullanan bir dependency. + * `SecurityScopes` tipinde bir `security_scopes` parametresi: + * Bu `security_scopes` parametresinin `scopes` adlı bir özelliği vardır ve yukarıda tanımlanan tüm scope'ları içeren bir `list` taşır, yani: + * *Path operation* `read_own_items` için `security_scopes.scopes` `["me", "items"]` içerir. + * *Path operation* `read_users_me` için `security_scopes.scopes` `["me"]` içerir; çünkü bu scope `get_current_active_user` dependency'sinde tanımlanmıştır. + * *Path operation* `read_system_status` için `security_scopes.scopes` `[]` (boş) olur; çünkü herhangi bir `Security` ile `scopes` tanımlamamıştır ve dependency'si olan `get_current_user` da `scopes` tanımlamaz. + +/// tip | İpucu + +Buradaki önemli ve "sihirli" nokta şu: `get_current_user`, her *path operation* için kontrol etmesi gereken farklı bir `scopes` listesi alır. + +Bu, belirli bir *path operation* için dependency ağacındaki her *path operation* ve her dependency üzerinde tanımlanan `scopes`'lara bağlıdır. + +/// + +## `SecurityScopes` hakkında daha fazla detay { #more-details-about-securityscopes } + +`SecurityScopes`'u herhangi bir noktada ve birden fazla yerde kullanabilirsiniz; mutlaka "kök" dependency'de olmak zorunda değildir. + +Her zaman, **o spesifik** *path operation* ve **o spesifik** dependency ağacı için, mevcut `Security` dependency'lerinde ve tüm dependant'larda tanımlanan security scope'larını içerir. + +`SecurityScopes`, dependant'ların tanımladığı tüm scope'ları barındırdığı için, gereken scope'ların token'da olup olmadığını merkezi bir dependency fonksiyonunda doğrulayıp, farklı *path operation*'larda farklı scope gereksinimleri tanımlayabilirsiniz. + +Bu kontroller her *path operation* için bağımsız yapılır. + +## Deneyin { #check-it } + +API dokümanlarını açarsanız, authenticate olup hangi scope'ları authorize etmek istediğinizi seçebilirsiniz. + + + +Hiç scope seçmezseniz "authenticated" olursunuz; ancak `/users/me/` veya `/users/me/items/`'e erişmeye çalıştığınızda, yeterli izniniz olmadığını söyleyen bir hata alırsınız. Yine de `/status/`'a erişebilirsiniz. + +`me` scope'unu seçip `items` scope'unu seçmezseniz `/users/me/`'a erişebilirsiniz ama `/users/me/items/`'e erişemezsiniz. + +Bu, bir üçüncü taraf uygulamanın, bir kullanıcı tarafından sağlanan token ile bu *path operation*'lardan birine erişmeye çalıştığında; kullanıcının uygulamaya kaç izin verdiğine bağlı olarak yaşayacağı durumdur. + +## Üçüncü taraf entegrasyonları hakkında { #about-third-party-integrations } + +Bu örnekte OAuth2 "password" flow'unu kullanıyoruz. + +Bu, kendi uygulamamıza giriş yaptığımız durumlar için uygundur; muhtemelen kendi frontend'imiz vardır. + +Çünkü `username` ve `password` alacağını bildiğimiz frontend'i biz kontrol ediyoruz, dolayısıyla güvenebiliriz. + +Ancak başkalarının bağlanacağı bir OAuth2 uygulaması geliştiriyorsanız (yani Facebook, Google, GitHub vb. gibi bir authentication provider muadili geliştiriyorsanız) diğer flow'lardan birini kullanmalısınız. + +En yaygını implicit flow'dur. + +En güvenlisi code flow'dur; ancak daha fazla adım gerektirdiği için implementasyonu daha karmaşıktır. Daha karmaşık olduğundan, birçok sağlayıcı implicit flow'yu önermeye yönelir. + +/// note | Not + +Her authentication provider'ın flow'ları markasının bir parçası yapmak için farklı şekilde adlandırması yaygındır. + +Ama sonuçta aynı OAuth2 standardını implement ediyorlar. + +/// + +**FastAPI**, bu OAuth2 authentication flow'larının tamamı için `fastapi.security.oauth2` içinde yardımcı araçlar sunar. + +## Decorator `dependencies` içinde `Security` { #security-in-decorator-dependencies } + +Decorator'ın `dependencies` parametresinde bir `list` `Depends` tanımlayabildiğiniz gibi ( [Path operation decorator'larında Dependencies](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} bölümünde açıklandığı üzere), burada `scopes` ile birlikte `Security` de kullanabilirsiniz. diff --git a/docs/tr/docs/advanced/settings.md b/docs/tr/docs/advanced/settings.md new file mode 100644 index 000000000..e3bcaac61 --- /dev/null +++ b/docs/tr/docs/advanced/settings.md @@ -0,0 +1,302 @@ +# Ayarlar ve Ortam Değişkenleri { #settings-and-environment-variables } + +Birçok durumda uygulamanızın bazı harici ayarlara veya konfigürasyonlara ihtiyacı olabilir; örneğin secret key'ler, veritabanı kimlik bilgileri, e-posta servisleri için kimlik bilgileri vb. + +Bu ayarların çoğu değişkendir (değişebilir); örneğin veritabanı URL'leri. Ayrıca birçoğu hassas olabilir; örneğin secret'lar. + +Bu nedenle bunları, uygulama tarafından okunan environment variable'lar ile sağlamak yaygındır. + +/// tip | İpucu + +Environment variable'ları anlamak için [Environment Variables](../environment-variables.md){.internal-link target=_blank} dokümanını okuyabilirsiniz. + +/// + +## Tipler ve doğrulama { #types-and-validation } + +Bu environment variable'lar yalnızca metin (string) taşıyabilir; çünkü Python'ın dışındadırlar ve diğer programlarla ve sistemin geri kalanıyla uyumlu olmaları gerekir (hatta Linux, Windows, macOS gibi farklı işletim sistemleriyle de). + +Bu da, Python içinde bir environment variable'dan okunan herhangi bir değerin `str` olacağı anlamına gelir; farklı bir tipe dönüştürme veya herhangi bir doğrulama işlemi kod içinde yapılmalıdır. + +## Pydantic `Settings` { #pydantic-settings } + +Neyse ki Pydantic, environment variable'lardan gelen bu ayarları yönetmek için Pydantic: Settings management ile çok iyi bir yardımcı araç sunar. + +### `pydantic-settings`'i kurun { #install-pydantic-settings } + +Önce, [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, aktive ettiğinizden emin olun ve ardından `pydantic-settings` paketini kurun: + +
+ +```console +$ pip install pydantic-settings +---> 100% +``` + +
+ +Ayrıca `all` extras'ını şu şekilde kurduğunuzda da dahil gelir: + +
+ +```console +$ pip install "fastapi[all]" +---> 100% +``` + +
+ +### `Settings` nesnesini oluşturun { #create-the-settings-object } + +Pydantic'ten `BaseSettings` import edin ve bir alt sınıf (sub-class) oluşturun; tıpkı bir Pydantic model'inde olduğu gibi. + +Pydantic model'lerinde olduğu gibi, type annotation'larla (ve gerekirse default değerlerle) class attribute'ları tanımlarsınız. + +Pydantic model'lerinde kullandığınız aynı doğrulama özelliklerini ve araçlarını burada da kullanabilirsiniz; örneğin farklı veri tipleri ve `Field()` ile ek doğrulamalar. + +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} + +/// tip | İpucu + +Hızlıca kopyalayıp yapıştırmak istiyorsanız bu örneği kullanmayın; aşağıdaki son örneği kullanın. + +/// + +Ardından, bu `Settings` sınıfının bir instance'ını oluşturduğunuzda (bu örnekte `settings` nesnesi), Pydantic environment variable'ları büyük/küçük harfe duyarsız şekilde okur; yani büyük harfli `APP_NAME` değişkeni, yine de `app_name` attribute'u için okunur. + +Sonrasında veriyi dönüştürür ve doğrular. Böylece `settings` nesnesini kullandığınızda, tanımladığınız tiplerde verilere sahip olursunuz (örn. `items_per_user` bir `int` olur). + +### `settings`'i kullanın { #use-the-settings } + +Daha sonra uygulamanızda yeni `settings` nesnesini kullanabilirsiniz: + +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} + +### Server'ı çalıştırın { #run-the-server } + +Sonraki adımda server'ı çalıştırırken konfigürasyonları environment variable olarak geçersiniz; örneğin `ADMIN_EMAIL` ve `APP_NAME` şu şekilde ayarlanabilir: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +/// tip | İpucu + +Tek bir komut için birden fazla env var ayarlamak istiyorsanız aralarına boşluk koyun ve hepsini komuttan önce yazın. + +/// + +Böylece `admin_email` ayarı `"deadpool@example.com"` olur. + +`app_name` `"ChimichangApp"` olur. + +`items_per_user` ise default değeri olan `50` olarak kalır. + +## Ayarları başka bir module'de tutma { #settings-in-another-module } + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, bu ayarları başka bir module dosyasına koyabilirsiniz. + +Örneğin `config.py` adında bir dosyanız şu şekilde olabilir: + +{* ../../docs_src/settings/app01_py39/config.py *} + +Ve ardından bunu `main.py` dosyasında kullanabilirsiniz: + +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} + +/// tip | İpucu + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, ayrıca bir `__init__.py` dosyasına da ihtiyacınız olacak. + +/// + +## Dependency içinde ayarlar { #settings-in-a-dependency } + +Bazı durumlarda, her yerde kullanılan global bir `settings` nesnesi yerine ayarları bir dependency üzerinden sağlamak faydalı olabilir. + +Bu özellikle test sırasında çok işe yarar; çünkü bir dependency'yi kendi özel ayarlarınızla override etmek çok kolaydır. + +### Config dosyası { #the-config-file } + +Bir önceki örnekten devam edersek, `config.py` dosyanız şöyle görünebilir: + +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} + +Dikkat edin, artık default bir instance `settings = Settings()` oluşturmuyoruz. + +### Ana uygulama dosyası { #the-main-app-file } + +Şimdi, yeni bir `config.Settings()` döndüren bir dependency oluşturuyoruz. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} + +/// tip | İpucu + +`@lru_cache` konusunu birazdan ele alacağız. + +Şimdilik `get_settings()`'in normal bir fonksiyon olduğunu varsayabilirsiniz. + +/// + +Sonra bunu dependency olarak *path operation function*'dan talep edebilir ve ihtiyaç duyduğumuz her yerde kullanabiliriz. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} + +### Ayarlar ve test { #settings-and-testing } + +Ardından, `get_settings` için bir dependency override oluşturarak test sırasında farklı bir settings nesnesi sağlamak çok kolay olur: + +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} + +Dependency override içinde, yeni `Settings` nesnesini oluştururken `admin_email` için yeni bir değer ayarlarız ve sonra bu yeni nesneyi döndürürüz. + +Sonrasında bunun kullanıldığını test edebiliriz. + +## `.env` dosyası okuma { #reading-a-env-file } + +Çok sayıda ayarınız varsa ve bunlar farklı ortamlarda sık sık değişiyorsa, bunları bir dosyaya koyup, sanki environment variable'mış gibi o dosyadan okumak faydalı olabilir. + +Bu yaklaşım oldukça yaygındır ve bir adı vardır: Bu environment variable'lar genellikle `.env` adlı bir dosyaya konur ve bu dosyaya "dotenv" denir. + +/// tip | İpucu + +Nokta (`.`) ile başlayan dosyalar, Linux ve macOS gibi Unix-benzeri sistemlerde gizli dosyadır. + +Ancak dotenv dosyasının mutlaka bu dosya adına sahip olması gerekmez. + +/// + +Pydantic, harici bir kütüphane kullanarak bu tür dosyalardan okuma desteğine sahiptir. Daha fazlası için: Pydantic Settings: Dotenv (.env) support. + +/// tip | İpucu + +Bunun çalışması için `pip install python-dotenv` yapmanız gerekir. + +/// + +### `.env` dosyası { #the-env-file } + +Şöyle bir `.env` dosyanız olabilir: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### Ayarları `.env`'den okuyun { #read-settings-from-env } + +Ardından `config.py` dosyanızı şöyle güncelleyin: + +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} + +/// tip | İpucu + +`model_config` attribute'u yalnızca Pydantic konfigürasyonu içindir. Daha fazlası için Pydantic: Concepts: Configuration. + +/// + +Burada, Pydantic `Settings` sınıfınızın içinde `env_file` konfigürasyonunu tanımlar ve değer olarak kullanmak istediğimiz dotenv dosyasının dosya adını veririz. + +### `lru_cache` ile `Settings`'i yalnızca bir kez oluşturma { #creating-the-settings-only-once-with-lru-cache } + +Diskten dosya okumak normalde maliyetli (yavaş) bir işlemdir; bu yüzden muhtemelen bunu yalnızca bir kez yapıp aynı settings nesnesini tekrar kullanmak istersiniz. Her request için yeniden okumak istemezsiniz. + +Ancak her seferinde şunu yaptığımızda: + +```Python +Settings() +``` + +yeni bir `Settings` nesnesi oluşturulur ve oluşturulurken `.env` dosyasını yeniden okur. + +Dependency fonksiyonu sadece şöyle olsaydı: + +```Python +def get_settings(): + return Settings() +``` + +bu nesneyi her request için oluştururduk ve `.env` dosyasını her request'te okurduk. ⚠️ + +Fakat en üstte `@lru_cache` decorator'ünü kullandığımız için `Settings` nesnesi yalnızca bir kez, ilk çağrıldığı anda oluşturulur. ✔️ + +{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} + +Sonraki request'lerde dependency'ler içinden `get_settings()` çağrıldığında, `get_settings()`'in iç kodu tekrar çalıştırılıp yeni bir `Settings` nesnesi yaratılmak yerine, ilk çağrıda döndürülen aynı nesne tekrar tekrar döndürülür. + +#### `lru_cache` Teknik Detayları { #lru-cache-technical-details } + +`@lru_cache`, decorator olarak uygulandığı fonksiyonu, her seferinde tekrar hesaplamak yerine ilk seferde döndürdüğü değeri döndürecek şekilde değiştirir; yani fonksiyon kodunu her çağrıda yeniden çalıştırmaz. + +Bu nedenle altındaki fonksiyon, argüman kombinasyonlarının her biri için bir kez çalıştırılır. Sonra bu argüman kombinasyonlarının her biri için döndürülmüş değerler, fonksiyon aynı argüman kombinasyonuyla çağrıldıkça tekrar tekrar kullanılır. + +Örneğin, şöyle bir fonksiyonunuz varsa: + +```Python +@lru_cache +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +programınız şu şekilde çalışabilir: + +```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 +``` + +Bizim `get_settings()` dependency'miz özelinde ise fonksiyon hiç argüman almaz; dolayısıyla her zaman aynı değeri döndürür. + +Bu şekilde, neredeyse global bir değişken gibi davranır. Ancak bir dependency fonksiyonu kullandığı için testte kolayca override edebiliriz. + +`@lru_cache`, Python standart kütüphanesinin bir parçası olan `functools` içindedir. Daha fazla bilgi için: Python docs for `@lru_cache`. + +## Özet { #recap } + +Uygulamanızın ayarlarını veya konfigürasyonlarını yönetmek için, Pydantic model'lerinin tüm gücüyle birlikte Pydantic Settings'i kullanabilirsiniz. + +* Dependency kullanarak test etmeyi basitleştirebilirsiniz. +* Bununla `.env` dosyalarını kullanabilirsiniz. +* `@lru_cache` kullanmak, dotenv dosyasını her request için tekrar tekrar okumayı engellerken, test sırasında override etmenize de izin verir. diff --git a/docs/tr/docs/advanced/sub-applications.md b/docs/tr/docs/advanced/sub-applications.md new file mode 100644 index 000000000..4773ba200 --- /dev/null +++ b/docs/tr/docs/advanced/sub-applications.md @@ -0,0 +1,67 @@ +# Alt Uygulamalar - Mount İşlemi { #sub-applications-mounts } + +Kendi bağımsız OpenAPI şemaları ve kendi dokümantasyon arayüzleri olan iki bağımsız FastAPI uygulamasına ihtiyacınız varsa, bir ana uygulama oluşturup bir (veya daha fazla) alt uygulamayı "mount" edebilirsiniz. + +## Bir **FastAPI** uygulamasını mount etmek { #mounting-a-fastapi-application } + +"Mount" etmek, belirli bir path altında tamamen "bağımsız" bir uygulamayı eklemek anlamına gelir. Ardından o path’in altındaki her şeyi, alt uygulamada tanımlanan _path operation_’lar ile o alt uygulama yönetir. + +### Üst seviye uygulama { #top-level-application } + +Önce ana, üst seviye **FastAPI** uygulamasını ve onun *path operation*’larını oluşturun: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} + +### Alt uygulama { #sub-application } + +Sonra alt uygulamanızı ve onun *path operation*’larını oluşturun. + +Bu alt uygulama da standart bir FastAPI uygulamasıdır; ancak "mount" edilecek olan budur: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} + +### Alt uygulamayı mount edin { #mount-the-sub-application } + +Üst seviye uygulamanızda (`app`), alt uygulama `subapi`’yi mount edin. + +Bu örnekte `/subapi` path’ine mount edilecektir: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} + +### Otomatik API dokümanlarını kontrol edin { #check-the-automatic-api-docs } + +Şimdi dosyanızla birlikte `fastapi` komutunu çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Ardından http://127.0.0.1:8000/docs adresinden dokümanları açın. + +Ana uygulama için otomatik API dokümanlarını göreceksiniz; yalnızca onun kendi _path operation_’larını içerir: + + + +Sonra alt uygulamanın dokümanlarını http://127.0.0.1:8000/subapi/docs adresinden açın. + +Alt uygulama için otomatik API dokümanlarını göreceksiniz; yalnızca onun kendi _path operation_’larını içerir ve hepsi doğru alt-path öneki `/subapi` altında yer alır: + + + +İki arayüzden herhangi biriyle etkileşime girmeyi denerseniz doğru şekilde çalıştıklarını görürsünüz; çünkü tarayıcı her bir uygulama ya da alt uygulama ile ayrı ayrı iletişim kurabilir. + +### Teknik Detaylar: `root_path` { #technical-details-root-path } + +Yukarıda anlatıldığı gibi bir alt uygulamayı mount ettiğinizde FastAPI, ASGI spesifikasyonundaki `root_path` adlı bir mekanizmayı kullanarak alt uygulamaya mount path’ini iletmeyi otomatik olarak yönetir. + +Bu sayede alt uygulama, dokümantasyon arayüzü için o path önekini kullanması gerektiğini bilir. + +Ayrıca alt uygulamanın kendi mount edilmiş alt uygulamaları da olabilir; FastAPI tüm bu `root_path`’leri otomatik olarak yönettiği için her şey doğru şekilde çalışır. + +`root_path` hakkında daha fazlasını ve bunu açıkça nasıl kullanacağınızı [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümünde öğreneceksiniz. diff --git a/docs/tr/docs/advanced/templates.md b/docs/tr/docs/advanced/templates.md new file mode 100644 index 000000000..b91e0a2a8 --- /dev/null +++ b/docs/tr/docs/advanced/templates.md @@ -0,0 +1,126 @@ +# Şablonlar { #templates } + +**FastAPI** ile istediğiniz herhangi bir template engine'i kullanabilirsiniz. + +Yaygın bir tercih, Flask ve diğer araçların da kullandığı Jinja2'dir. + +Bunu kolayca yapılandırmak için, doğrudan **FastAPI** uygulamanızda kullanabileceğiniz yardımcı araçlar vardır (Starlette tarafından sağlanır). + +## Bağımlılıkları Yükleme { #install-dependencies } + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, etkinleştirdiğinizden ve `jinja2`'yi yüklediğinizden emin olun: + +
+ +```console +$ pip install jinja2 + +---> 100% +``` + +
+ +## `Jinja2Templates` Kullanımı { #using-jinja2templates } + +* `Jinja2Templates`'ı içe aktarın. +* Daha sonra tekrar kullanabileceğiniz bir `templates` nesnesi oluşturun. +* Template döndürecek *path operation* içinde bir `Request` parametresi tanımlayın. +* Oluşturduğunuz `templates` nesnesini kullanarak bir `TemplateResponse` render edip döndürün; template'in adını, request nesnesini ve Jinja2 template'i içinde kullanılacak anahtar-değer çiftlerini içeren bir "context" sözlüğünü (dict) iletin. + +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} + +/// note | Not + +FastAPI 0.108.0 ve Starlette 0.29.0 öncesinde, ilk parametre `name` idi. + +Ayrıca, daha önceki sürümlerde `request` nesnesi, Jinja2 için context içindeki anahtar-değer çiftlerinin bir parçası olarak geçirilirdi. + +/// + +/// tip | İpucu + +`response_class=HTMLResponse` olarak tanımlarsanız doküman arayüzü (docs UI) response'un HTML olacağını anlayabilir. + +/// + +/// note | Teknik Detaylar + +`from starlette.templating import Jinja2Templates` da kullanabilirsiniz. + +**FastAPI**, geliştirici için kolaylık olması adına `starlette.templating` içeriğini `fastapi.templating` olarak da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. `Request` ve `StaticFiles` için de aynı durum geçerlidir. + +/// + +## Template Yazma { #writing-templates } + +Ardından örneğin `templates/item.html` konumunda bir template yazabilirsiniz: + +```jinja hl_lines="7" +{!../../docs_src/templates/templates/item.html!} +``` + +### Template Context Değerleri { #template-context-values } + +Şu HTML içeriğinde: + +{% raw %} + +```jinja +Item ID: {{ id }} +``` + +{% endraw %} + +...gösterilecek olan `id`, sizin "context" olarak ilettiğiniz `dict` içinden alınır: + +```Python +{"id": id} +``` + +Örneğin ID değeri `42` ise, şu şekilde render edilir: + +```html +Item ID: 42 +``` + +### Template `url_for` Argümanları { #template-url-for-arguments } + +Template içinde `url_for()` da kullanabilirsiniz; argüman olarak, *path operation function*'ınızın kullandığı argümanların aynısını alır. + +Dolayısıyla şu bölüm: + +{% raw %} + +```jinja + +``` + +{% endraw %} + +...*path operation function* olan `read_item(id=id)` tarafından handle edilecek URL'nin aynısına bir link üretir. + +Örneğin ID değeri `42` ise, şu şekilde render edilir: + +```html + +``` + +## Template'ler ve statik dosyalar { #templates-and-static-files } + +Template içinde `url_for()` kullanabilir ve örneğin `name="static"` ile mount ettiğiniz `StaticFiles` ile birlikte kullanabilirsiniz. + +```jinja hl_lines="4" +{!../../docs_src/templates/templates/item.html!} +``` + +Bu örnekte, şu şekilde `static/styles.css` konumundaki bir CSS dosyasına link verir: + +```CSS hl_lines="4" +{!../../docs_src/templates/static/styles.css!} +``` + +Ve `StaticFiles` kullandığınız için, bu CSS dosyası **FastAPI** uygulamanız tarafından `/static/styles.css` URL'sinde otomatik olarak servis edilir. + +## Daha fazla detay { #more-details } + +Template'leri nasıl test edeceğiniz dahil daha fazla detay için Starlette'in template dokümantasyonuna bakın. diff --git a/docs/tr/docs/advanced/testing-dependencies.md b/docs/tr/docs/advanced/testing-dependencies.md new file mode 100644 index 000000000..3cad63776 --- /dev/null +++ b/docs/tr/docs/advanced/testing-dependencies.md @@ -0,0 +1,53 @@ +# Override Kullanarak Dependency'leri Test Etme { #testing-dependencies-with-overrides } + +## Test Sırasında Dependency Override Etme { #overriding-dependencies-during-testing } + +Test yazarken bazı durumlarda bir dependency'yi override etmek isteyebilirsiniz. + +Orijinal dependency'nin (ve varsa tüm alt dependency'lerinin) çalışmasını istemezsiniz. + +Bunun yerine, yalnızca testler sırasında (hatta belki sadece belirli bazı testlerde) kullanılacak farklı bir dependency sağlarsınız; böylece orijinal dependency'nin ürettiği değerin kullanıldığı yerde, test için üretilen değeri kullanabilirsiniz. + +### Kullanım Senaryoları: Harici Servis { #use-cases-external-service } + +Örneğin, çağırmanız gereken harici bir authentication provider'ınız olabilir. + +Ona bir token gönderirsiniz ve o da authenticated bir user döndürür. + +Bu provider request başına ücret alıyor olabilir ve onu çağırmak, testlerde sabit bir mock user kullanmaya kıyasla daha fazla zaman alabilir. + +Muhtemelen harici provider'ı bir kez test etmek istersiniz; ancak çalışan her testte onu çağırmanız şart değildir. + +Bu durumda, o provider'ı çağıran dependency'yi override edebilir ve yalnızca testleriniz için mock user döndüren özel bir dependency kullanabilirsiniz. + +### `app.dependency_overrides` Attribute'ünü Kullanın { #use-the-app-dependency-overrides-attribute } + +Bu tür durumlar için **FastAPI** uygulamanızda `app.dependency_overrides` adında bir attribute bulunur; bu basit bir `dict`'tir. + +Test için bir dependency'yi override etmek istediğinizde, key olarak orijinal dependency'yi (bir function), value olarak da override edecek dependency'nizi (başka bir function) verirsiniz. + +Böylece **FastAPI**, orijinal dependency yerine bu override'ı çağırır. + +{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *} + +/// tip | İpucu + +**FastAPI** uygulamanızın herhangi bir yerinde kullanılan bir dependency için override tanımlayabilirsiniz. + +Orijinal dependency bir *path operation function* içinde, bir *path operation decorator* içinde (return value kullanmadığınız durumlarda), bir `.include_router()` çağrısında, vb. kullanılıyor olabilir. + +FastAPI yine de onu override edebilir. + +/// + +Sonrasında override'larınızı (yani kaldırıp sıfırlamayı) `app.dependency_overrides` değerini boş bir `dict` yaparak gerçekleştirebilirsiniz: + +```Python +app.dependency_overrides = {} +``` + +/// tip | İpucu + +Bir dependency'yi yalnızca bazı testler sırasında override etmek istiyorsanız, override'ı testin başında (test function'ının içinde) ayarlayıp testin sonunda (yine test function'ının sonunda) sıfırlayabilirsiniz. + +/// diff --git a/docs/tr/docs/advanced/testing-events.md b/docs/tr/docs/advanced/testing-events.md new file mode 100644 index 000000000..f12aef988 --- /dev/null +++ b/docs/tr/docs/advanced/testing-events.md @@ -0,0 +1,12 @@ +# Event'leri Test Etme: lifespan ve startup - shutdown { #testing-events-lifespan-and-startup-shutdown } + +Test'lerinizde `lifespan`'ın çalışması gerektiğinde, `TestClient`'ı bir `with` ifadesiyle kullanabilirsiniz: + +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + + +Bu konuda daha fazla ayrıntıyı resmi Starlette dokümantasyon sitesindeki ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) bölümünde okuyabilirsiniz. + +Kullanımdan kaldırılmış `startup` ve `shutdown` event'leri için ise `TestClient`'ı aşağıdaki gibi kullanabilirsiniz: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/tr/docs/advanced/using-request-directly.md b/docs/tr/docs/advanced/using-request-directly.md new file mode 100644 index 000000000..3efdafb03 --- /dev/null +++ b/docs/tr/docs/advanced/using-request-directly.md @@ -0,0 +1,56 @@ +# Request'i Doğrudan Kullanmak { #using-the-request-directly } + +Şu ana kadar, ihtiyacınız olan request parçalarını tipleriyle birlikte tanımlıyordunuz. + +Verileri şuradan alarak: + +* path'ten parameter olarak. +* Header'lardan. +* Cookie'lerden. +* vb. + +Bunu yaptığınızda **FastAPI**, bu verileri doğrular (validate eder), dönüştürür ve API'niz için dokümantasyonu otomatik olarak üretir. + +Ancak bazı durumlarda `Request` nesnesine doğrudan erişmeniz gerekebilir. + +## `Request` nesnesi hakkında detaylar { #details-about-the-request-object } + +**FastAPI** aslında altta **Starlette** çalıştırır ve üstüne çeşitli araçlardan oluşan bir katman ekler. Bu yüzden gerektiğinde Starlette'in `Request` nesnesini doğrudan kullanabilirsiniz. + +Bu ayrıca şu anlama gelir: `Request` nesnesinden veriyi doğrudan alırsanız (örneğin body'yi okursanız) FastAPI bu veriyi doğrulamaz, dönüştürmez veya dokümante etmez (otomatik API arayüzü için OpenAPI ile). + +Buna rağmen normal şekilde tanımladığınız diğer herhangi bir parameter (örneğin Pydantic model ile body) yine doğrulanır, dönüştürülür, annotate edilir, vb. + +Ama bazı özel durumlarda `Request` nesnesini almak faydalıdır. + +## `Request` nesnesini doğrudan kullanın { #use-the-request-object-directly } + +*Path operation function* içinde client'ın IP adresini/host'unu almak istediğinizi düşünelim. + +Bunun için request'e doğrudan erişmeniz gerekir. + +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} + +Tipi `Request` olan bir *path operation function* parameter'ı tanımladığınızda **FastAPI**, o parameter'a `Request` nesnesini geçmesi gerektiğini anlar. + +/// tip | İpucu + +Bu örnekte, request parameter'ının yanında bir path parameter'ı da tanımladığımıza dikkat edin. + +Dolayısıyla path parameter'ı çıkarılır, doğrulanır, belirtilen tipe dönüştürülür ve OpenAPI ile annotate edilir. + +Aynı şekilde, diğer parameter'ları normal biçimde tanımlamaya devam edip buna ek olarak `Request` de alabilirsiniz. + +/// + +## `Request` dokümantasyonu { #request-documentation } + +Resmi Starlette dokümantasyon sitesinde `Request` nesnesiyle ilgili daha fazla detayı okuyabilirsiniz. + +/// note | Teknik Detaylar + +`from starlette.requests import Request` de kullanabilirsiniz. + +**FastAPI** bunu size (geliştiriciye) kolaylık olsun diye doğrudan sunar. Ancak kendisi doğrudan Starlette'ten gelir. + +/// diff --git a/docs/tr/docs/advanced/websockets.md b/docs/tr/docs/advanced/websockets.md new file mode 100644 index 000000000..775d7cc25 --- /dev/null +++ b/docs/tr/docs/advanced/websockets.md @@ -0,0 +1,186 @@ +# WebSockets { #websockets } + +**FastAPI** ile WebSockets kullanabilirsiniz. + +## `websockets` Kurulumu { #install-websockets } + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden ve `websockets`'i ("WebSocket" protokolünü kullanmayı kolaylaştıran bir Python kütüphanesi) kurduğunuzdan emin olun: + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## WebSockets client { #websockets-client } + +### Production'da { #in-production } + +Production sisteminizde muhtemelen React, Vue.js veya Angular gibi modern bir framework ile oluşturulmuş bir frontend vardır. + +WebSockets kullanarak backend'inizle iletişim kurmak için de büyük ihtimalle frontend'inizin sağladığı yardımcı araçları kullanırsınız. + +Ya da native kod ile doğrudan WebSocket backend'inizle iletişim kuran native bir mobil uygulamanız olabilir. + +Veya WebSocket endpoint'i ile iletişim kurmak için başka herhangi bir yönteminizi de kullanıyor olabilirsiniz. + +--- + +Ancak bu örnek için, tamamı uzun bir string içinde olacak şekilde biraz JavaScript içeren çok basit bir HTML dokümanı kullanacağız. + +Elbette bu optimal değil ve production için kullanmazsınız. + +Production'da yukarıdaki seçeneklerden birini kullanırsınız. + +Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek için en basit yol bu: + +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} + +## Bir `websocket` Oluşturun { #create-a-websocket } + +**FastAPI** uygulamanızda bir `websocket` oluşturun: + +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} + +/// note | Teknik Detaylar + +`from starlette.websockets import WebSocket` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak işinizi kolaylaştırmak için aynı `WebSocket`'i doğrudan sağlar. Ancak aslında doğrudan Starlette'ten gelir. + +/// + +## Mesajları `await` Edin ve Mesaj Gönderin { #await-for-messages-and-send-messages } + +WebSocket route'unuzda mesajları `await` edebilir ve mesaj gönderebilirsiniz. + +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} + +Binary, text ve JSON verisi alıp gönderebilirsiniz. + +## Deneyin { #try-it } + +Dosyanızın adı `main.py` ise uygulamanızı şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Şuna benzer basit bir sayfa göreceksiniz: + + + +Input kutusuna mesaj yazıp gönderebilirsiniz: + + + +Ve WebSockets kullanan **FastAPI** uygulamanız yanıt döndürecektir: + + + +Birçok mesaj gönderebilir (ve alabilirsiniz): + + + +Ve hepsinde aynı WebSocket bağlantısı kullanılacaktır. + +## `Depends` ve Diğerlerini Kullanma { #using-depends-and-others } + +WebSocket endpoint'lerinde `fastapi` içinden import edip şunları kullanabilirsiniz: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +Diğer FastAPI endpoint'leri/*path operations* ile aynı şekilde çalışırlar: + +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} + +/// info | Bilgi + +Bu bir WebSocket olduğu için `HTTPException` raise etmek pek anlamlı değildir; bunun yerine `WebSocketException` raise ederiz. + +Spesifikasyonda tanımlanan geçerli kodlar arasından bir kapatma kodu kullanabilirsiniz. + +/// + +### Dependency'lerle WebSockets'i Deneyin { #try-the-websockets-with-dependencies } + +Dosyanızın adı `main.py` ise uygulamanızı şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Burada şunları ayarlayabilirsiniz: + +* path'te kullanılan "Item ID". +* query parametresi olarak kullanılan "Token". + +/// tip | İpucu + +query'deki `token` değerinin bir dependency tarafından ele alınacağına dikkat edin. + +/// + +Bununla WebSocket'e bağlanabilir, ardından mesaj gönderip alabilirsiniz: + + + +## Bağlantı Kopmalarını ve Birden Fazla Client'ı Yönetme { #handling-disconnections-and-multiple-clients } + +Bir WebSocket bağlantısı kapandığında, `await websocket.receive_text()` bir `WebSocketDisconnect` exception'ı raise eder; ardından bunu bu örnekteki gibi yakalayıp (catch) yönetebilirsiniz. + +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} + +Denemek için: + +* Uygulamayı birden fazla tarayıcı sekmesiyle açın. +* Bu sekmelerden mesaj yazın. +* Sonra sekmelerden birini kapatın. + +Bu, `WebSocketDisconnect` exception'ını raise eder ve diğer tüm client'lar şuna benzer bir mesaj alır: + +``` +Client #1596980209979 left the chat +``` + +/// tip | İpucu + +Yukarıdaki uygulama, birden fazla WebSocket bağlantısına mesajları nasıl yönetip broadcast edeceğinizi göstermek için minimal ve basit bir örnektir. + +Ancak her şey memory'de, tek bir list içinde yönetildiği için yalnızca process çalıştığı sürece ve yalnızca tek bir process ile çalışacaktır. + +FastAPI ile kolay entegre olan ama Redis, PostgreSQL vb. tarafından desteklenen daha sağlam bir şeye ihtiyacınız varsa encode/broadcaster'a göz atın. + +/// + +## Daha Fazla Bilgi { #more-info } + +Seçenekler hakkında daha fazlasını öğrenmek için Starlette dokümantasyonunda şunlara bakın: + +* `WebSocket` class'ı. +* Class tabanlı WebSocket yönetimi. diff --git a/docs/tr/docs/advanced/wsgi.md b/docs/tr/docs/advanced/wsgi.md index 442f83a59..6f6b10b68 100644 --- a/docs/tr/docs/advanced/wsgi.md +++ b/docs/tr/docs/advanced/wsgi.md @@ -1,18 +1,34 @@ # WSGI'yi Dahil Etme - Flask, Django ve Diğerleri { #including-wsgi-flask-django-others } -WSGI uygulamalarını [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi mount edebilirsiniz. +WSGI uygulamalarını [Alt Uygulamalar - Mount Etme](sub-applications.md){.internal-link target=_blank}, [Bir Proxy Arkasında](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi mount edebilirsiniz. Bunun için `WSGIMiddleware`'ı kullanabilir ve bunu WSGI uygulamanızı (örneğin Flask, Django vb.) sarmalamak için kullanabilirsiniz. ## `WSGIMiddleware` Kullanımı { #using-wsgimiddleware } -`WSGIMiddleware`'ı import etmeniz gerekir. +/// info | Bilgi + +Bunun için `a2wsgi` kurulmalıdır; örneğin `pip install a2wsgi` ile. + +/// + +`WSGIMiddleware`'ı `a2wsgi` paketinden import etmeniz gerekir. Ardından WSGI (örn. Flask) uygulamasını middleware ile sarmalayın. Ve sonra bunu bir path'in altına mount edin. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Not + +Önceden, `fastapi.middleware.wsgi` içindeki `WSGIMiddleware`'ın kullanılması öneriliyordu, ancak artık kullanımdan kaldırıldı. + +Bunun yerine `a2wsgi` paketini kullanmanız önerilir. Kullanım aynıdır. + +Sadece `a2wsgi` paketinin kurulu olduğundan emin olun ve `WSGIMiddleware`'ı `a2wsgi` içinden doğru şekilde import edin. + +/// ## Kontrol Edelim { #check-it } diff --git a/docs/tr/docs/alternatives.md b/docs/tr/docs/alternatives.md index 9b603ea81..afc1a45ef 100644 --- a/docs/tr/docs/alternatives.md +++ b/docs/tr/docs/alternatives.md @@ -1,483 +1,483 @@ -# Alternatifler, İlham Kaynakları ve Karşılaştırmalar +# Alternatifler, İlham Kaynakları ve Karşılaştırmalar { #alternatives-inspiration-and-comparisons } -**FastAPI**'ya neler ilham verdi? Diğer alternatiflerle karşılaştırıldığında farkları neler? **FastAPI** diğer alternatiflerinden neler öğrendi? +**FastAPI**'a nelerin ilham verdiği, alternatiflerle nasıl karşılaştırıldığı ve onlardan neler öğrendiği. -## Giriş +## Giriş { #intro } Başkalarının daha önceki çalışmaları olmasaydı, **FastAPI** var olmazdı. -Geçmişte oluşturulan pek çok araç **FastAPI**'a ilham kaynağı olmuştur. +Önceden oluşturulan birçok araç, ortaya çıkışına ilham verdi. -Yıllardır yeni bir framework oluşturmaktan kaçınıyordum. Başlangıçta **FastAPI**'ın çözdüğü sorunları çözebilmek için pek çok farklı framework, eklenti ve araç kullanmayı denedim. +Yıllarca yeni bir framework oluşturmaktan kaçındım. Önce **FastAPI**’ın bugün kapsadığı özelliklerin tamamını, birçok farklı framework, eklenti ve araçla çözmeyi denedim. -Ancak bir noktada, geçmişteki diğer araçlardan en iyi fikirleri alarak bütün bu çözümleri kapsayan, ayrıca bütün bunları Python'ın daha önce mevcut olmayan özelliklerini (Python 3.6+ ile gelen tip belirteçleri) kullanarak yapan bir şey üretmekten başka seçenek kalmamıştı. +Ancak bir noktada, geçmişteki araçlardan en iyi fikirleri alıp, mümkün olan en iyi şekilde birleştiren ve daha önce mevcut olmayan dil özelliklerini (Python 3.6+ tip belirteçleri) kullanarak tüm bu özellikleri sağlayan bir şey geliştirmekten başka seçenek kalmadı. -## Daha Önce Geliştirilen Araçlar +## Daha Önce Geliştirilen Araçlar { #previous-tools } -### Django +### Django { #django } -Django geniş çapta güvenilen, Python ekosistemindeki en popüler web framework'üdür. Instagram gibi sistemleri geliştirmede kullanılmıştır. +Python ekosistemindeki en popüler ve yaygın olarak güvenilen web framework’üdür. Instagram gibi sistemleri geliştirmede kullanılmıştır. -MySQL ve PostgreSQL gibi ilişkisel veritabanlarıyla nispeten sıkı bir şekilde bağlantılıdır. Bu nedenle Couchbase, MongoDB ve Cassandra gibi NoSQL veritabanlarını ana veritabanı motoru olarak kullanmak pek de kolay değil. +MySQL veya PostgreSQL gibi ilişkisel veritabanlarıyla nispeten sıkı bağlıdır, bu nedenle Couchbase, MongoDB, Cassandra vb. gibi bir NoSQL veritabanını ana depolama motoru olarak kullanmak pek kolay değildir. -Modern ön uçlarda (React, Vue.js ve Angular gibi) veya diğer sistemler (örneğin nesnelerin interneti cihazları) tarafından kullanılan API'lar yerine arka uçta HTML üretmek için oluşturuldu. +Modern bir ön uç (React, Vue.js, Angular gibi) veya onunla haberleşen diğer sistemler (ör. IoT cihazları) tarafından tüketilen API’lar üretmekten ziyade, arka uçta HTML üretmek için oluşturulmuştur. -### Django REST Framework +### Django REST Framework { #django-rest-framework } -Django REST framework'ü, Django'nun API kabiliyetlerini arttırmak için arka planda Django kullanan esnek bir araç grubu olarak oluşturuldu. +Django REST Framework, Django üzerine kurulu esnek bir araç takımı olarak, Web API’lar geliştirmeyi ve Django’nun API kabiliyetlerini artırmayı hedefler. -Üstelik Mozilla, Red Hat ve Eventbrite gibi pek çok şirket tarafından kullanılıyor. +Mozilla, Red Hat ve Eventbrite gibi birçok şirket tarafından kullanılmaktadır. -**Otomatik API dökümantasyonu**nun ilk örneklerinden biri olduğu için, **FastAPI** arayışına ilham veren ilk fikirlerden biri oldu. +**Otomatik API dökümantasyonu**nun ilk örneklerinden biriydi ve bu, “**FastAPI** arayışına” ilham veren ilk fikirlerden biriydi. /// note | Not -Django REST Framework'ü, aynı zamanda **FastAPI**'ın dayandığı Starlette ve Uvicorn'un da yaratıcısı olan Tom Christie tarafından geliştirildi. +Django REST Framework, **FastAPI**'ın üzerine inşa edildiği Starlette ve Uvicorn'un da yaratıcısı olan Tom Christie tarafından geliştirildi. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Kullanıcılar için otomatik API dökümantasyonu sunan bir web arayüzüne sahip olmalı. +Otomatik API dökümantasyonu sağlayan bir web arayüzü sunmak. /// -### Flask +### Flask { #flask } -Flask bir mikro framework olduğundan Django gibi framework'lerin aksine veritabanı entegrasyonu gibi Django ile gelen pek çok özelliği direkt barındırmaz. +Flask bir “mikroframework”tür, Django’da varsayılan gelen pek çok özelliği (veritabanı entegrasyonları vb.) içermez. -Sağladığı basitlik ve esneklik NoSQL veritabanlarını ana veritabanı sistemi olarak kullanmak gibi şeyler yapmaya olanak sağlar. +Bu basitlik ve esneklik, NoSQL veritabanlarını ana veri depolama sistemi olarak kullanmak gibi şeyleri mümkün kılar. -Yapısı oldukça basit olduğundan öğrenmesi de nispeten basittir, tabii dökümantasyonu bazı noktalarda biraz teknik hale geliyor. +Çok basit olduğu için öğrenmesi nispeten sezgiseldir, ancak dökümantasyon bazı noktalarda biraz teknikleşebilir. -Ayrıca Django ile birlikte gelen veritabanı, kullanıcı yönetimi ve diğer pek çok özelliğe ihtiyaç duymayan uygulamalarda da yaygın olarak kullanılıyor. Ancak bu tür özelliklerin pek çoğu eklentiler ile eklenebiliyor. +Ayrıca veritabanı, kullanıcı yönetimi veya Django’da önceden gelen pek çok özelliğe ihtiyaç duymayan uygulamalar için de yaygın olarak kullanılır. Yine de bu özelliklerin çoğu eklentilerle eklenebilir. -Uygulama parçalarının böyle ayrılıyor oluşu ve istenilen özelliklerle genişletilebilecek bir mikro framework olmak tam da benim istediğim bir özellikti. +Bileşenlerin ayrık olması ve gerekeni tam olarak kapsayacak şekilde genişletilebilen bir “mikroframework” olması, özellikle korumak istediğim bir özelliktir. -Flask'ın basitliği göz önünde bulundurulduğu zaman, API geliştirmek için iyi bir seçim gibi görünüyordu. Sıradaki şey ise Flask için bir "Django REST Framework"! +Flask’ın sadeliği göz önüne alındığında, API geliştirmek için iyi bir aday gibi görünüyordu. Sırada, Flask için bir “Django REST Framework” bulmak vardı. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Gereken araçları ve parçaları birleştirip eşleştirmeyi kolaylaştıracak bir mikro framework olmalı. +Gereken araç ve parçaları kolayca eşleştirip birleştirmeyi sağlayan bir mikroframework olmak. -Basit ve kullanması kolay bir yönlendirme sistemine sahip olmalı. +Basit ve kullanımı kolay bir yönlendirme (routing) sistemine sahip olmak. /// -### Requests +### Requests { #requests } -**FastAPI** aslında **Requests**'in bir alternatifi değil. İkisininde kapsamı oldukça farklı. +**FastAPI** aslında **Requests**’in bir alternatifi değildir. Kapsamları çok farklıdır. -Aslında Requests'i bir FastAPI uygulamasının *içinde* kullanmak daha olağan olurdu. +Hatta bir FastAPI uygulamasının içinde Requests kullanmak yaygındır. -Ama yine de, FastAPI, Requests'ten oldukça ilham aldı. +Yine de FastAPI, Requests’ten epey ilham almıştır. -**Requests**, API'lar ile bir istemci olarak *etkileşime geçmeyi* sağlayan bir kütüphaneyken **FastAPI** bir sunucu olarak API'lar oluşturmaya yarar. +**Requests** bir kütüphane olarak API’larla (istemci olarak) etkileşime geçmeye yararken, **FastAPI** API’lar (sunucu olarak) geliştirmeye yarar. -Öyle ya da böyle zıt uçlarda olmalarına rağmen birbirlerini tamamlıyorlar. +Yani daha çok zıt uçlardadırlar ama birbirlerini tamamlarlar. -Requests oldukça basit ve sezgisel bir tasarıma sahip, kullanması da mantıklı varsayılan değerlerle oldukça kolay. Ama aynı zamanda çok güçlü ve gayet özelleştirilebilir. +Requests çok basit ve sezgisel bir tasarıma sahiptir, mantıklı varsayılanlarla kullanımı çok kolaydır. Aynı zamanda çok güçlü ve özelleştirilebilirdir. -Bu yüzden resmi web sitede de söylendiği gibi: +Bu yüzden resmi web sitesinde de söylendiği gibi: -> Requests, tüm zamanların en çok indirilen Python paketlerinden biridir. +> Requests, tüm zamanların en çok indirilen Python paketlerinden biridir -Kullanım şekli oldukça basit. Örneğin bir `GET` isteği yapmak için aşağıdaki yeterli: +Kullanımı çok basittir. Örneğin bir `GET` isteği yapmak için: ```Python response = requests.get("http://example.com/some/url") ``` -Bunun FastAPI'deki API *yol işlemi* şöyle görünür: +Buna karşılık bir FastAPI API *path operation*’ı şöyle olabilir: ```Python hl_lines="1" @app.get("/some/url") def read_url(): - return {"message": "Hello World!"} + return {"message": "Hello World"} ``` `requests.get(...)` ile `@app.get(...)` arasındaki benzerliklere bakın. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -* Basit ve sezgisel bir API'ya sahip olmalı. -* HTTP metot isimlerini (işlemlerini) anlaşılır olacak bir şekilde, direkt kullanmalı. -* Mantıklı varsayılan değerlere ve buna rağmen güçlü bir özelleştirme desteğine sahip olmalı. +* Basit ve sezgisel bir API’ya sahip olmak. +* HTTP metot isimlerini (işlemlerini) doğrudan, anlaşılır ve sezgisel bir şekilde kullanmak. +* Mantıklı varsayılanlara sahip olmak ama güçlü özelleştirmeler de sunmak. /// -### Swagger / OpenAPI +### Swagger / OpenAPI { #swagger-openapi } -Benim Django REST Framework'ünden istediğim ana özellik otomatik API dökümantasyonuydu. +Django REST Framework’ünden istediğim ana özellik otomatik API dökümantasyonuydu. -Daha sonra API'ları dökümanlamak için Swagger adında JSON (veya JSON'un bir uzantısı olan YAML'ı) kullanan bir standart olduğunu buldum. +Sonra API’ları JSON (veya JSON’un bir uzantısı olan YAML) kullanarak dökümante etmek için Swagger adlı bir standart olduğunu gördüm. -Üstelik Swagger API'ları için zaten halihazırda oluşturulmuş bir web arayüzü vardı. Yani, bir API için Swagger dökümantasyonu oluşturmak bu arayüzü otomatik olarak kullanabilmek demekti. +Ve Swagger API’ları için zaten oluşturulmuş bir web arayüzü vardı. Yani bir API için Swagger dökümantasyonu üretebilmek, bu web arayüzünü otomatik kullanabilmek demekti. -Swagger bir noktada Linux Foundation'a verildi ve adı OpenAPI olarak değiştirildi. +Bir noktada Swagger, Linux Foundation’a devredildi ve OpenAPI olarak yeniden adlandırıldı. -İşte bu yüzden versiyon 2.0 hakkında konuşurken "Swagger", versiyon 3 ve üzeri için ise "OpenAPI" adını kullanmak daha yaygın. +Bu yüzden, 2.0 sürümü söz konusu olduğunda “Swagger”, 3+ sürümler için ise “OpenAPI” denmesi yaygındır. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -API spesifikasyonları için özel bir şema yerine bir açık standart benimseyip kullanmalı. +API spesifikasyonları için özel bir şema yerine açık bir standart benimsemek ve kullanmak. -Ayrıca standarda bağlı kullanıcı arayüzü araçlarını entegre etmeli: +Ve standartlara dayalı kullanıcı arayüzü araçlarını entegre etmek: * Swagger UI * ReDoc -Yukarıdaki ikisi oldukça popüler ve istikrarlı olduğu için seçildi, ancak hızlı bir araştırma yaparak **FastAPI** ile kullanabileceğiniz pek çok OpenAPI alternatifi arayüz bulabilirsiniz. +Bu ikisi oldukça popüler ve istikrarlı oldukları için seçildi; hızlı bir aramayla OpenAPI için onlarca alternatif kullanıcı arayüzü bulabilirsiniz (**FastAPI** ile de kullanabilirsiniz). /// -### Flask REST framework'leri +### Flask REST framework’leri { #flask-rest-frameworks } -Pek çok Flask REST framework'ü var, fakat bunları biraz araştırdıktan sonra pek çoğunun artık geliştirilmediğini ve göze batan bazı sorunlarının olduğunu gördüm. +Birçok Flask REST framework’ü var; ancak zaman ayırıp inceledikten sonra çoğunun artık sürdürülmediğini veya bazı kritik sorunlar nedeniyle uygun olmadıklarını gördüm. -### Marshmallow +### Marshmallow { #marshmallow } -API sistemlerine gereken ana özelliklerden biri de koddan veriyi alıp ağ üzerinde gönderilebilecek bir şeye çevirmek, yani veri dönüşümü. Bu işleme veritabanındaki veriyi içeren bir objeyi JSON objesine çevirmek, `datetime` objelerini metinlere çevirmek gibi örnekler verilebilir. +API sistemlerinin ihtiyaç duyduğu temel özelliklerden biri, koddan (Python) veriyi alıp ağ üzerinden gönderilebilecek bir şeye dönüştürmek, yani veri “dönüşüm”üdür. Örneğin, bir veritabanından gelen verileri içeren bir objeyi JSON objesine dönüştürmek, `datetime` objelerini string’e çevirmek vb. -API'lara gereken bir diğer büyük özellik ise veri doğrulamadır, yani verinin çeşitli parametrelere bağlı olarak doğru ve tutarlı olduğundan emin olmaktır. Örneğin bir alanın `int` olmasına karar verdiniz, daha sonra rastgele bir metin değeri almasını istemezsiniz. Bu özellikle sisteme dışarıdan gelen veri için kullanışlı bir özellik oluyor. +API’ların ihtiyaç duyduğu bir diğer önemli özellik, veri doğrulamadır; belirli parametreler göz önüne alındığında verinin geçerli olduğundan emin olmak. Örneğin, bir alanın `int` olması ve rastgele bir metin olmaması. Bu özellikle dışarıdan gelen veriler için kullanışlıdır. -Bir veri doğrulama sistemi olmazsa bütün bu kontrolleri koda dökerek kendiniz yapmak zorunda kalırdınız. +Bir veri doğrulama sistemi olmadan, tüm bu kontrolleri kod içinde el ile yapmanız gerekir. -Marshmallow bu özellikleri sağlamak için geliştirilmişti. Benim de geçmişte oldukça sık kullandığım harika bir kütüphanedir. +Marshmallow, bu özellikleri sağlamak için inşa edildi. Harika bir kütüphanedir ve geçmişte çok kullandım. -Ama... Python'un tip belirteçleri gelmeden önce oluşturulmuştu. Yani her şemayı tanımlamak için Marshmallow'un sunduğu spesifik araçları ve sınıfları kullanmanız gerekiyordu. +Ancak Python tip belirteçlerinden önce yazılmıştır. Dolayısıyla her şemayı tanımlamak için Marshmallow’un sağladığı belirli yardımcılar ve sınıflar kullanılır. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Kod kullanarak otomatik olarak veri tipini ve veri doğrulamayı belirten "şemalar" tanımlamalı. +Kodla, veri tiplerini ve doğrulamayı otomatik sağlayan “şemalar” tanımlamak. /// -### Webargs +### Webargs { #webargs } -API'ların ihtiyacı olan bir diğer önemli özellik ise gelen isteklerdeki verileri Python objelerine ayrıştırabilmektir. +API’ların ihtiyaç duyduğu bir diğer büyük özellik, gelen isteklerden veriyi ayrıştırmadır. -Webargs, Flask gibi bir kaç framework'ün üzerinde bunu sağlamak için geliştirilen bir araçtır. +Webargs, Flask dahil birkaç framework’ün üzerinde bunu sağlamak için geliştirilmiş bir araçtır. -Veri doğrulama için arka planda Marshmallow kullanıyor, hatta aynı geliştiriciler tarafından oluşturuldu. +Veri doğrulama için arka planda Marshmallow’u kullanır. Aynı geliştiriciler tarafından yazılmıştır. -Webargs da harika bir araç ve onu da geçmişte henüz **FastAPI** yokken çok kullandım. +**FastAPI**’dan önce benim de çok kullandığım harika bir araçtır. /// info | Bilgi -Webargs aynı Marshmallow geliştirileri tarafından oluşturuldu. +Webargs, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Gelen istek verisi için otomatik veri doğrulamaya sahip olmalı. +Gelen istek verisini otomatik doğrulamak. /// -### APISpec +### APISpec { #apispec } -Marshmallow ve Webargs eklentiler olarak; veri doğrulama, ayrıştırma ve dönüştürmeyi sağlıyor. +Marshmallow ve Webargs; doğrulama, ayrıştırma ve dönüşümü eklenti olarak sağlar. -Ancak dökümantasyondan hala ses seda yok. Daha sonrasında ise APISpec geldi. +Ama dökümantasyon eksikti. Sonra APISpec geliştirildi. -APISpec pek çok framework için bir eklenti olarak kullanılıyor (Starlette için de bir eklentisi var). +Birçok framework için bir eklentidir (Starlette için de bir eklenti vardır). -Şemanın tanımını yol'a istek geldiğinde çalışan her bir fonksiyonun döküman dizesinin içine YAML formatında olacak şekilde yazıyorsunuz o da OpenAPI şemaları üretiyor. +Çalışma şekli: Her bir route’u işleyen fonksiyonun docstring’i içine YAML formatında şema tanımı yazarsınız. -Flask, Starlette, Responder ve benzerlerinde bu şekilde çalışıyor. +Ve OpenAPI şemaları üretir. -Fakat sonrasında yine mikro sözdizimi problemiyle karşılaşıyoruz. Python metinlerinin içinde koskoca bir YAML oluyor. +Flask, Starlette, Responder vb. için çalışma şekli böyledir. -Editör bu konuda pek yardımcı olamaz. Üstelik eğer parametreleri ya da Marshmallow şemalarını değiştirip YAML kodunu güncellemeyi unutursak artık döküman geçerliliğini yitiriyor. +Ancak yine, Python metni içinde (kocaman bir YAML) mikro bir söz dizimi sorunu ortaya çıkar. + +Editör bu konuda pek yardımcı olamaz. Parametreleri veya Marshmallow şemalarını değiştirip docstring’teki YAML’ı güncellemeyi unutursak, üretilen şema geçerliliğini yitirir. /// info | Bilgi -APISpec de aynı Marshmallow geliştiricileri tarafından oluşturuldu. +APISpec, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -API'lar için açık standart desteği olmalı (OpenAPI gibi). +API’lar için açık standart olan OpenAPI’ı desteklemek. /// -### Flask-apispec +### Flask-apispec { #flask-apispec } -Flask-apispec ise Webargs, Marshmallow ve APISpec'i birbirine bağlayan bir Flask eklentisi. +Webargs, Marshmallow ve APISpec’i bir araya getiren bir Flask eklentisidir. -Webargs ve Marshmallow'daki bilgiyi APISpec ile otomatik OpenAPI şemaları üretmek için kullanıyor. +Webargs ve Marshmallow’dan aldığı bilgiyi kullanarak, APISpec ile otomatik OpenAPI şemaları üretir. -Hak ettiği değeri görmeyen, harika bir araç. Piyasadaki çoğu Flask eklentisinden çok daha popüler olmalı. Hak ettiği değeri görmüyor oluşunun sebebi ise dökümantasyonun çok kısa ve soyut olması olabilir. +Harika ama yeterince değer görmeyen bir araçtır. Mevcut birçok Flask eklentisinden çok daha popüler olmalıydı. Muhtemelen dökümantasyonunun fazla kısa ve soyut olmasından kaynaklanıyor olabilir. -Böylece Flask-apispec, Python döküman dizilerine YAML gibi farklı bir syntax yazma sorununu çözmüş oldu. +Python docstring’leri içine YAML (farklı bir söz dizimi) yazma ihtiyacını ortadan kaldırdı. -**FastAPI**'ı geliştirene dek benim favori arka uç kombinasyonum Flask'in yanında Marshmallow ve Webargs ile birlikte Flask-apispec idi. +**FastAPI**’yı inşa edene kadar, Flask + Flask-apispec + Marshmallow + Webargs kombinasyonu benim favori arka uç stack’imdi. -Bunu kullanmak, bir kaç full-stack Flask projesi oluşturucusunun yaratılmasına yol açtı. Bunlar benim (ve bir kaç harici ekibin de) şimdiye kadar kullandığı asıl stackler: +Bunu kullanmak, birkaç Flask full‑stack üreticisinin ortaya çıkmasına yol açtı. Şu ana kadar benim (ve birkaç harici ekibin) kullandığı ana stack’ler: * https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase * https://github.com/tiangolo/full-stack-flask-couchdb -Aynı full-stack üreticiler [**FastAPI** Proje Üreticileri](project-generation.md){.internal-link target=_blank}'nin de temelini oluşturdu. +Aynı full‑stack üreticiler, [**FastAPI** Proje Üreticileri](project-generation.md){.internal-link target=_blank}’nin de temelini oluşturdu. /// info | Bilgi -Flask-apispec de aynı Marshmallow geliştiricileri tarafından üretildi. +Flask-apispec, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham oldu? +/// check | **FastAPI**'a ilham olan -Veri dönüşümü ve veri doğrulamayı tanımlayan kodu kullanarak otomatik olarak OpenAPI şeması oluşturmalı. +Veri dönüşümü ve doğrulamayı tanımlayan aynı koddan, OpenAPI şemasını otomatik üretmek. /// -### NestJS (and Angular) +### NestJS (ve Angular) { #nestjs-and-angular } -Bu Python teknolojisi bile değil. NestJS, Angulardan ilham almış olan bir JavaScript (TypeScript) NodeJS framework'ü. +Bu Python bile değil; NestJS, Angular’dan ilham alan bir JavaScript (TypeScript) NodeJS framework’üdür. -Flask-apispec ile yapılabileceklere nispeten benzeyen bir şey elde ediyor. +Flask-apispec ile yapılabilene kısmen benzer bir şey başarır. -Angular 2'den ilham alan, içine gömülü bir bağımlılık enjeksiyonu sistemi var. Bildiğim diğer tüm bağımlılık enjeksiyonu sistemlerinde olduğu gibi"bağımlılık"ları önceden kaydetmenizi gerektiriyor. Böylece projeyi daha detaylı hale getiriyor ve kod tekrarını da arttırıyor. +Angular 2’den esinlenen, entegre bir bağımlılık enjeksiyonu sistemi vardır. “Injectable”ları önceden kaydetmeyi gerektirir (bildiğim diğer bağımlılık enjeksiyonu sistemlerinde olduğu gibi), bu da ayrıntıyı ve kod tekrarını artırır. -Parametreler TypeScript tipleri (Python tip belirteçlerine benzer) ile açıklandığından editör desteği oldukça iyi. +Parametreler TypeScript tipleriyle (Python tip belirteçlerine benzer) açıklandığından, editör desteği oldukça iyidir. -Ama TypeScript verileri kod JavaScript'e derlendikten sonra korunmadığından, bunlara dayanarak aynı anda veri doğrulaması, veri dönüşümü ve dökümantasyon tanımlanamıyor. Bundan ve bazı tasarım tercihlerinden dolayı veri doğrulaması, dönüşümü ve otomatik şema üretimi için pek çok yere dekorator eklemek gerekiyor. Bu da projeyi oldukça detaylandırıyor. +Ancak TypeScript tip bilgisi JavaScript’e derlemeden sonra korunmadığından, aynı anda tiplere dayanarak doğrulama, dönüşüm ve dökümantasyon tanımlanamaz. Bu ve bazı tasarım kararları nedeniyle doğrulama, dönüşüm ve otomatik şema üretimi için birçok yere dekoratör eklemek gerekir; proje oldukça ayrıntılı hâle gelir. -İç içe geçen derin modelleri pek iyi işleyemiyor. Yani eğer istekteki JSON gövdesi derin bir JSON objesiyse düzgün bir şekilde dökümante edilip doğrulanamıyor. +İçiçe modelleri çok iyi işleyemez. Yani istek gövdesindeki JSON, içinde başka alanları ve onlar da içiçe JSON objelerini içeriyorsa, doğru şekilde dökümante edilip doğrulanamaz. -/// check | **FastAPI**'a nasıl ilham oldu? +/// check | **FastAPI**'a ilham olan -Güzel bir editör desteği için Python tiplerini kullanmalı. +Harika editör desteği için Python tiplerini kullanmak. -Güçlü bir bağımlılık enjeksiyon sistemine sahip olmalı. Kod tekrarını minimuma indirecek bir yol bulmalı. +Güçlü bir bağımlılık enjeksiyonu sistemine sahip olmak. Kod tekrarını en aza indirmenin bir yolunu bulmak. /// -### Sanic +### Sanic { #sanic } -Sanic, `asyncio`'ya dayanan son derece hızlı Python kütüphanelerinden biriydi. Flask'a epey benzeyecek şekilde geliştirilmişti. - -/// note | Teknik detaylar - -İçerisinde standart Python `asyncio` döngüsü yerine `uvloop` kullanıldı. Hızının asıl kaynağı buydu. - -Uvicorn ve Starlette'e ilham kaynağı olduğu oldukça açık, şu anda ikisi de açık karşılaştırmalarda Sanicten daha hızlı gözüküyor. - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Uçuk performans sağlayacak bir yol bulmalı. - -Tam da bu yüzden **FastAPI** Starlette'e dayanıyor, çünkü Starlette şu anda kullanılabilir en hızlı framework. (üçüncü parti karşılaştırmalı testlerine göre) - -/// - -### Falcon - -Falcon ise bir diğer yüksek performanslı Python framework'ü. Minimal olacak şekilde Hug gibi diğer framework'lerin temeli olabilmek için tasarlandı. - -İki parametre kabul eden fonksiyonlar şeklinde tasarlandı, biri "istek" ve diğeri ise "cevap". Sonra isteğin çeşitli kısımlarını **okuyor**, cevaba ise bir şeyler **yazıyorsunuz**. Bu tasarımdan dolayı istek parametrelerini ve gövdelerini standart Python tip belirteçlerini kullanarak fonksiyon parametreleriyle belirtmek mümkün değil. - -Yani veri doğrulama, veri dönüştürme ve dökümantasyonun hepsi kodda yer almalı, otomatik halledemiyoruz. Ya da Falcon üzerine bir framework olarak uygulanmaları gerekiyor, aynı Hug'da olduğu gibi. Bu ayrım Falcon'un tasarımından esinlenen, istek ve cevap objelerini parametre olarak işleyen diğer kütüphanelerde de yer alıyor. - -/// check | **FastAPI**'a nasıl ilham oldu? - -Harika bir performans'a sahip olmanın yollarını bulmalı. - -Hug ile birlikte (Hug zaten Falcon'a dayandığından) **FastAPI**'ın fonksiyonlarda `cevap` parametresi belirtmesinde ilham kaynağı oldu. - -FastAPI'da opsiyonel olmasına rağmen, daha çok header'lar, çerezler ve alternatif durum kodları belirlemede kullanılıyor. - -/// - -### Molten - -**FastAPI**'ı geliştirmenin ilk aşamalarında Molten'ı keşfettim. Pek çok ortak fikrimiz vardı: - -* Python'daki tip belirteçlerini baz alıyordu. -* Bunlara bağlı olarak veri doğrulaması ve dökümantasyon sağlıyordu. -* Bir bağımlılık enjeksiyonu sistemi vardı. - -Veri doğrulama, veri dönüştürme ve dökümantasyon için Pydantic gibi bir üçüncü parti kütüphane kullanmıyor, kendi içerisinde bunlara sahip. Yani bu veri tipi tanımlarını tekrar kullanmak pek de kolay değil. - -Biraz daha detaylı ayarlamalara gerek duyuyor. Ayrıca ASGI yerine WSGI'a dayanıyor. Yani Uvicorn, Starlette ve Sanic gibi araçların yüksek performansından faydalanacak şekilde tasarlanmamış. - -Bağımlılık enjeksiyonu sistemi bağımlılıkların önceden kaydedilmesini ve sonrasında belirlenen veri tiplerine göre çözülmesini gerektiriyor. Yani spesifik bir tip, birden fazla bileşen ile belirlenemiyor. - -Yol'lar fonksiyonun üstünde endpoint'i işleyen dekoratörler yerine farklı yerlerde tanımlanan fonksiyonlarla belirlenir. Bu Flask (ve Starlette) yerine daha çok Django'nun yaklaşımına daha yakın bir metot. Bu, kodda nispeten birbiriyle sıkı ilişkili olan şeyleri ayırmaya sebep oluyor. - -/// check | **FastAPI**'a nasıl ilham oldu? - -Model özelliklerinin "standart" değerlerini kullanarak veri tipleri için ekstra veri doğrulama koşulları tanımlamalı. Bu editör desteğini geliştiriyor ve daha önceden Pydantic'te yoktu. - -Bu aslında Pydantic'in de aynı doğrulama stiline geçmesinde ilham kaynağı oldu. Şu anda bütün bu özellikler Pydantic'in yapısında yer alıyor. - -/// - -### Hug - -Hug, Python tip belirteçlerini kullanarak API parametrelerinin tipini belirlemeyi uygulayan ilk framework'lerdendi. Bu, diğer araçlara da ilham kaynağı olan harika bir fikirdi. - -Tip belirlerken standart Python veri tipleri yerine kendi özel tiplerini kullandı, yine de bu ileriye dönük devasa bir adımdı. - -Hug ayrıca tüm API'ı JSON ile ifade eden özel bir şema oluşturan ilk framework'lerdendir. - -OpenAPI veya JSON Şeması gibi bir standarda bağlı değildi. Yani Swagger UI gibi diğer araçlarla entegre etmek kolay olmazdı. Ama yine de, bu oldukça yenilikçi bir fikirdi. - -Ayrıca ilginç ve çok rastlanmayan bir özelliği vardı: aynı framework'ü kullanarak hem API'lar hem de CLI'lar oluşturmak mümkündü. - -Senkron çalışan Python web framework'lerinin standardına (WSGI) dayandığından dolayı Websocket'leri ve diğer şeyleri işleyemiyor, ancak yine de yüksek performansa sahip. - -/// info | Bilgi - -Hug, Python dosyalarında bulunan dahil etme satırlarını otomatik olarak sıralayan harika bir araç olan `isort`'un geliştiricisi Timothy Crosley tarafından geliştirildi. - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Hug, APIStar'ın çeşitli kısımlarında esin kaynağı oldu ve APIStar'la birlikte en umut verici bulduğum araçlardan biriydi. - -**FastAPI**, Python tip belirteçlerini kullanarak parametre belirlemede ve API'ı otomatık tanımlayan bir şema üretmede de Hug'a esinlendi. - -**FastAPI**'ın header ve çerez tanımlamak için fonksiyonlarda `response` parametresini belirtmesinde de Hug'dan ilham alındı. - -/// - -### APIStar (<= 0.5) - -**FastAPI**'ı geliştirmeye başlamadan hemen önce **APIStar** sunucusunu buldum. Benim aradığım şeylerin neredeyse hepsine sahipti ve harika bir tasarımı vardı. - -Benim şimdiye kadar gördüğüm Python tip belirteçlerini kullanarak parametre ve istekler belirlemeyi uygulayan ilk framework'lerdendi (Molten ve NestJS'den önce). APIStar'ı da aşağı yukarı Hug ile aynı zamanlarda buldum. Fakat APIStar OpenAPI standardını kullanıyordu. - -Farklı yerlerdeki tip belirteçlerine bağlı olarak otomatik veri doğrulama, veri dönüştürme ve OpenAPI şeması oluşturma desteği sunuyordu. - -Gövde şema tanımları Pydantic ile aynı Python tip belirteçlerini kullanmıyordu, biraz daha Marsmallow'a benziyordu. Dolayısıyla editör desteği de o kadar iyi olmazdı ama APIStar eldeki en iyi seçenekti. - -O dönemlerde karşılaştırmalarda en iyi performansa sahipti (yalnızca Starlette'e kaybediyordu). - -Başlangıçta otomatik API dökümantasyonu sunan bir web arayüzü yoktu, ama ben ona Swagger UI ekleyebileceğimi biliyordum. - -Bağımlılık enjeksiyon sistemi vardı. Yukarıda bahsettiğim diğer araçlar gibi bundaki sistem de bileşenlerin önceden kaydedilmesini gerektiriyordu. Yine de harika bir özellikti. - -Güvenlik entegrasyonu olmadığından dolayı APIStar'ı hiç bir zaman tam bir projede kullanamadım. Bu yüzden Flask-apispec'e bağlı full-stack proje üreticilerde sahip olduğum özellikleri tamamen değiştiremedim. Bu güvenlik entegrasyonunu ekleyen bir PR oluşturmak da projelerim arasında yer alıyordu. - -Sonrasında ise projenin odağı değişti. - -Geliştiricinin Starlette'e odaklanması gerekince proje de artık bir API web framework'ü olmayı bıraktı. - -Artık APIStar, OpenAPI özelliklerini doğrulamak için bir dizi araç sunan bir proje haline geldi. - -/// info | Bilgi - -APIStar, aşağıdaki projeleri de üreten Tom Christie tarafından geliştirildi: - -* Django REST Framework -* **FastAPI**'ın da dayandığı Starlette -* Starlette ve **FastAPI** tarafından da kullanılan Uvicorn - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Var oldu. - -Aynı Python veri tipleriyle birden fazla şeyi belirleme (veri doğrulama, veri dönüştürme ve dökümantasyon), bir yandan da harika bir editör desteği sunma, benim muhteşem bulduğum bir fikirdi. - -Uzunca bir süre boyunca benzer bir framework arayıp pek çok farklı alternatifi denedikten sonra, APIStar en iyi seçenekti. - -Sonra APIStar bir sunucu olmayı bıraktı ve Starlette oluşturuldu. Starlette, böyle bir sunucu sistemi için daha iyi bir temel sunuyordu. Bu da **FastAPI**'ın son esin kaynağıydı. - -Ben bu önceki araçlardan öğrendiklerime dayanarak **FastAPI**'ın özelliklerini arttırıp geliştiriyor, tip desteği sistemi ve diğer kısımları iyileştiriyorum ancak yine de **FastAPI**'ı APIStar'ın "ruhani varisi" olarak görüyorum. - -/// - -## **FastAPI** Tarafından Kullanılanlar - -### Pydantic - -Pydantic Python tip belirteçlerine dayanan; veri doğrulama, veri dönüştürme ve dökümantasyon tanımlamak (JSON Şema kullanarak) için bir kütüphanedir. - -Tip belirteçleri kullanıyor olması onu aşırı sezgisel yapıyor. - -Marshmallow ile karşılaştırılabilir. Ancak karşılaştırmalarda Marshmallowdan daha hızlı görünüyor. Aynı Python tip belirteçlerine dayanıyor ve editör desteği de harika. - -/// check | **FastAPI** nerede kullanıyor? - -Bütün veri doğrulama, veri dönüştürme ve JSON Şemasına bağlı otomatik model dökümantasyonunu halletmek için! - -**FastAPI** yaptığı her şeyin yanı sıra bu JSON Şema verisini alıp daha sonra OpenAPI'ya yerleştiriyor. - -/// - -### Starlette - -Starlette hafif bir ASGI framework'ü ve yüksek performanslı asyncio servisleri oluşturmak için ideal. - -Kullanımı çok kolay ve sezgisel, kolaylıkla genişletilebilecek ve modüler bileşenlere sahip olacak şekilde tasarlandı. - -Sahip olduğu bir kaç özellik: - -* Cidden etkileyici bir performans. -* WebSocket desteği. -* İşlem-içi arka plan görevleri. -* Başlatma ve kapatma olayları. -* HTTPX ile geliştirilmiş bir test istemcisi. -* CORS, GZip, Static Files ve Streaming cevapları desteği. -* Session ve çerez desteği. -* Kodun %100'ü test kapsamında. -* Kodun %100'ü tip belirteçleriyle desteklenmiştir. -* Yalnızca bir kaç zorunlu bağımlılığa sahip. - -Starlette şu anda test edilen en hızlı Python framework'ü. Yalnızca bir sunucu olan Uvicorn'a yeniliyor, o da zaten bir framework değil. - -Starlette bütün temel web mikro framework işlevselliğini sağlıyor. - -Ancak otomatik veri doğrulama, veri dönüştürme ve dökümantasyon sağlamyor. - -Bu, **FastAPI**'ın onun üzerine tamamen Python tip belirteçlerine bağlı olarak eklediği (Pydantic ile) ana şeylerden biri. **FastAPI** bunun yanında artı olarak bağımlılık enjeksiyonu sistemi, güvenlik araçları, OpenAPI şema üretimi ve benzeri özellikler de ekliyor. +`asyncio` tabanlı, son derece hızlı ilk Python framework’lerinden biriydi. Flask’a oldukça benzer olacak şekilde geliştirilmişti. /// note | Teknik Detaylar -ASGI, Django'nun ana ekibi tarafından geliştirilen yeni bir "standart". Bir "Python standardı" (PEP) olma sürecinde fakat henüz bir standart değil. +Varsayılan Python `asyncio` döngüsü yerine `uvloop` kullanır; hızını esasen bu sağlar. -Bununla birlikte, halihazırda birçok araç tarafından bir "standart" olarak kullanılmakta. Bu, Uvicorn'u farklı ASGI sunucularıyla (Daphne veya Hypercorn gibi) değiştirebileceğiniz veya `python-socketio` gibi ASGI ile uyumlu araçları ekleyebileciğiniz için birlikte çalışılabilirliği büyük ölçüde arttırıyor. +Açık kıyaslamalarda, bugün Uvicorn ve Starlette’in Sanic’ten daha hızlı olduğu görülür; Sanic bu ikisine ilham vermiştir. /// -/// check | **FastAPI** nerede kullanıyor? +/// check | **FastAPI**'a ilham olan -Tüm temel web kısımlarında üzerine özellikler eklenerek kullanılmakta. +Çok yüksek performans elde etmenin bir yolunu bulmak. -`FastAPI` sınıfının kendisi direkt olarak `Starlette` sınıfını miras alıyor! - -Yani, Starlette ile yapabileceğiniz her şeyi, Starlette'in bir nevi güçlendirilmiş hali olan **FastAPI** ile doğrudan yapabilirsiniz. +Bu yüzden **FastAPI**, en hızlı framework olduğu için (üçüncü parti kıyaslamalara göre) Starlette üzerine kuruludur. /// -### Uvicorn +### Falcon { #falcon } -Uvicorn, uvlook ile httptools üzerine kurulu ışık hzında bir ASGI sunucusudur. +Falcon, başka bir yüksek performanslı Python framework’üdür; minimal olacak şekilde tasarlanmış ve Hug gibi diğer framework’lere temel olmuştur. -Bir web framework'ünden ziyade bir sunucudur, yani yollara bağlı yönlendirme yapmanızı sağlayan araçları yoktur. Bu daha çok Starlette (ya da **FastAPI**) gibi bir framework'ün sunucuya ek olarak sağladığı bir şeydir. +İki parametre alan fonksiyonlar etrafında tasarlanmıştır: “request” ve “response”. İstekten parçalar “okur”, cevaba parçalar “yazarsınız”. Bu tasarım nedeniyle, fonksiyon parametreleriyle standart Python tip belirteçlerini kullanarak istek parametrelerini ve gövdelerini ilan etmek mümkün değildir. -Starlette ve **FastAPI** için tavsiye edilen sunucu Uvicorndur. +Dolayısıyla veri doğrulama, dönüşüm ve dökümantasyon kodda yapılmalı; otomatik olmaz. Ya da Hug’da olduğu gibi Falcon’un üzerine bir framework olarak uygulanmalıdır. Falcon’un tasarımından etkilenen ve tek bir request objesi ile response objesini parametre olarak alan diğer framework’lerde de aynı ayrım vardır. -/// check | **FastAPI** neden tavsiye ediyor? +/// check | **FastAPI**'a ilham olan -**FastAPI** uygulamalarını çalıştırmak için ana web sunucusu Uvicorn! +Harika performans elde etmenin yollarını bulmak. -Gunicorn ile birleştirdiğinizde asenkron ve çoklu işlem destekleyen bir sunucu elde ediyorsunuz! - -Daha fazla detay için [Deployment](deployment/index.md){.internal-link target=_blank} bölümünü inceleyebilirsiniz. +Hug ile birlikte (Hug, Falcon’a dayanır) **FastAPI**’da fonksiyonlarda opsiyonel bir `response` parametresi ilan edilmesi fikrine ilham vermek. FastAPI’da bu parametre çoğunlukla header, cookie ve alternatif durum kodlarını ayarlamak için kullanılır. /// -## Karşılaştırma ve Hız +### Molten { #molten } -Uvicorn, Starlette ve FastAPI arasındakı farkı daha iyi anlamak ve karşılaştırma yapmak için [Kıyaslamalar](benchmarks.md){.internal-link target=_blank} bölümüne bakın! +**FastAPI**’ı geliştirmenin ilk aşamalarında Molten’ı keşfettim. Oldukça benzer fikirleri vardı: + +* Python tip belirteçlerine dayanır. +* Bu tiplere bağlı doğrulama ve dökümantasyon sağlar. +* Bağımlılık enjeksiyonu sistemi vardır. + +Pydantic gibi doğrulama, dönüşüm ve dökümantasyon için üçüncü parti bir kütüphane kullanmaz; kendi içinde sağlar. Bu yüzden bu veri tipi tanımlarını tekrar kullanmak o kadar kolay olmaz. + +Biraz daha ayrıntılı yapılandırma ister. Ve ASGI yerine WSGI tabanlı olduğundan, Uvicorn, Starlette ve Sanic gibi araçların yüksek performansından faydalanmaya yönelik tasarlanmamıştır. + +Bağımlılık enjeksiyonu sistemi, bağımlılıkların önceden kaydedilmesini ve tiplerine göre çözülmesini gerektirir. Yani belirli bir tipi sağlayan birden fazla “bileşen” tanımlanamaz. + +Route’lar, endpoint’i işleyen fonksiyonun üstüne konan dekoratörlerle değil, tek bir yerde, farklı yerlerde tanımlanmış fonksiyonlar kullanılarak ilan edilir. Bu yaklaşım, Flask (ve Starlette) yerine Django’ya daha yakındır; kodda aslında birbirine sıkı bağlı olan şeyleri ayırır. + +/// check | **FastAPI**'a ilham olan + +Model özelliklerinin “varsayılan” değerlerini kullanarak veri tiplerine ekstra doğrulamalar tanımlamak. Bu, editör desteğini iyileştirir ve Pydantic’te daha önce yoktu. + +Bu yaklaşım, Pydantic’te de aynı doğrulama beyan stilinin desteklenmesine ilham verdi (bu işlevselliklerin tamamı artık Pydantic’te mevcut). + +/// + +### Hug { #hug } + +Hug, Python tip belirteçlerini kullanarak API parametre tiplerini ilan etmeyi uygulayan ilk framework’lerden biriydi. Diğer araçlara da ilham veren harika bir fikirdi. + +Standart Python tipleri yerine kendi özel tiplerini kullansa da büyük bir adımdı. + +JSON ile tüm API’ı beyan eden özel bir şema üreten ilk framework’lerden biriydi. + +OpenAPI veya JSON Schema gibi bir standarda dayanmadığı için Swagger UI gibi diğer araçlarla doğrudan entegre edilemezdi. Yine de oldukça yenilikçiydi. + +Nadir bir özelliği daha vardı: aynı framework ile hem API’lar hem de CLI’lar oluşturmak mümkündü. + +Senkron Python web framework’leri için önceki standart olan WSGI’ye dayandığından, WebSocket vb. şeyleri işleyemez, ancak yine de yüksek performansa sahiptir. + +/// info | Bilgi + +Hug, Python dosyalarındaki import’ları otomatik sıralayan harika bir araç olan `isort`’un geliştiricisi Timothy Crosley tarafından geliştirildi. + +/// + +/// check | **FastAPI**'a ilham olan fikirler + +Hug, APIStar’ın bazı kısımlarına ilham verdi ve APIStar ile birlikte en umut verici bulduğum araçlardandı. + +**FastAPI**, parametreleri ilan etmek ve API’ı otomatik tanımlayan bir şema üretmek için Python tip belirteçlerini kullanma fikrini Hug’dan ilhamla benimsedi. + +Ayrıca header ve cookie ayarlamak için fonksiyonlarda `response` parametresi ilan etme fikrine de Hug ilham verdi. + +/// + +### APIStar (<= 0.5) { #apistar-0-5 } + +**FastAPI**’yi inşa etmeye karar vermeden hemen önce **APIStar** sunucusunu buldum. Aradığım şeylerin neredeyse hepsine sahipti ve harika bir tasarımı vardı. + +Python tip belirteçleriyle parametreleri ve istekleri ilan eden bir framework’ün gördüğüm ilk örneklerindendi (NestJS ve Molten’dan önce). Aşağı yukarı Hug ile aynı zamanlarda buldum; ancak APIStar, OpenAPI standardını kullanıyordu. + +Farklı yerlerdeki aynı tip belirteçlerine dayanarak otomatik veri doğrulama, veri dönüşümü ve OpenAPI şeması üretimi vardı. + +Gövde şema tanımları Pydantic’tekiyle aynı Python tip belirteçlerini kullanmıyordu; biraz daha Marshmallow’a benziyordu. Bu yüzden editör desteği o kadar iyi olmazdı; yine de APIStar mevcut en iyi seçenekti. + +O dönem kıyaslamalarda en iyi performansa sahipti (sadece Starlette tarafından geçiliyordu). + +Başta otomatik API dökümantasyonu sunan bir web arayüzü yoktu ama Swagger UI ekleyebileceğimi biliyordum. + +Bağımlılık enjeksiyonu sistemi vardı. Diğer araçlarda olduğu gibi bileşenlerin önceden kaydedilmesini gerektiriyordu. Yine de harika bir özellikti. + +Güvenlik entegrasyonu olmadığından tam bir projede kullanamadım; bu yüzden Flask-apispec tabanlı full‑stack üreticilerle sahip olduğum özelliklerin tamamını ikame edemedim. Bu işlevi ekleyen bir pull request’i yapılacaklar listeme almıştım. + +Sonra projenin odağı değişti. + +Artık bir API web framework’ü değildi; geliştirici Starlette’e odaklanmak zorundaydı. + +Şimdi APIStar, bir web framework’ü değil, OpenAPI spesifikasyonlarını doğrulamak için araçlar takımından ibaret. + +/// info | Bilgi + +APIStar, aşağıdakilerin de yaratıcısı olan Tom Christie tarafından geliştirildi: + +* Django REST Framework +* **FastAPI**’ın üzerine kurulu Starlette +* Starlette ve **FastAPI** tarafından kullanılan Uvicorn + +/// + +/// check | **FastAPI**'a ilham olan + +Var olmak. + +Aynı Python tipleriyle (hem veri doğrulama, dönüşüm ve dökümantasyon) birden çok şeyi ilan etmek ve aynı anda harika editör desteği sağlamak, bence dahiyane bir fikirdi. + +Uzun süre benzer bir framework arayıp birçok alternatifi denedikten sonra, APIStar mevcut en iyi seçenekti. + +Sonra APIStar bir sunucu olarak var olmaktan çıktı ve Starlette oluşturuldu; böyle bir sistem için daha iyi bir temel oldu. Bu, **FastAPI**’yi inşa etmek için son ilhamdı. + +Önceki bu araçlardan edinilen deneyimler üzerine özellikleri, tip sistemi ve diğer kısımları geliştirip artırırken, **FastAPI**’yi APIStar’ın “ruhani varisi” olarak görüyorum. + +/// + +## **FastAPI** Tarafından Kullanılanlar { #used-by-fastapi } + +### Pydantic { #pydantic } + +Pydantic, Python tip belirteçlerine dayalı olarak veri doğrulama, dönüşüm ve dökümantasyon (JSON Schema kullanarak) tanımlamak için bir kütüphanedir. + +Bu onu aşırı sezgisel kılar. + +Marshmallow ile karşılaştırılabilir. Kıyaslamalarda Marshmallow’dan daha hızlıdır. Aynı Python tip belirteçlerine dayandığı için editör desteği harikadır. + +/// check | **FastAPI** bunu şurada kullanır + +Tüm veri doğrulama, veri dönüşümü ve JSON Schema tabanlı otomatik model dökümantasyonunu halletmekte. + +**FastAPI** daha sonra bu JSON Schema verisini alır ve (yaptığı diğer şeylerin yanı sıra) OpenAPI içine yerleştirir. + +/// + +### Starlette { #starlette } + +Starlette, yüksek performanslı asyncio servisleri oluşturmak için ideal, hafif bir ASGI framework’ü/araç takımıdır. + +Çok basit ve sezgiseldir. Kolayca genişletilebilir ve modüler bileşenlere sahip olacak şekilde tasarlanmıştır. + +Şunlara sahiptir: + +* Cidden etkileyici performans. +* WebSocket desteği. +* Süreç içi arka plan görevleri. +* Başlatma ve kapatma olayları. +* HTTPX üzerinde geliştirilmiş test istemcisi. +* CORS, GZip, Statik Dosyalar, Streaming cevaplar. +* Oturum (Session) ve Cookie desteği. +* %100 test kapsamı. +* %100 tip anotasyonlu kod tabanı. +* Az sayıda zorunlu bağımlılık. + +Starlette, şu anda test edilen en hızlı Python framework’üdür. Yalnızca bir framework değil, bir sunucu olan Uvicorn tarafından geçilir. + +Starlette, temel web mikroframework işlevselliğinin tamamını sağlar. + +Ancak otomatik veri doğrulama, dönüşüm veya dökümantasyon sağlamaz. + +**FastAPI**’nin bunun üzerine eklediği ana şeylerden biri, Pydantic kullanarak, bütünüyle Python tip belirteçlerine dayalı bu özelliklerdir. Buna ek olarak bağımlılık enjeksiyonu sistemi, güvenlik yardımcıları, OpenAPI şema üretimi vb. gelir. + +/// note | Teknik Detaylar + +ASGI, Django çekirdek ekip üyeleri tarafından geliştirilen yeni bir “standart”tır. Hâlâ resmi bir “Python standardı” (PEP) değildir, ancak bu süreç üzerindedirler. + +Buna rağmen, şimdiden birçok araç tarafından bir “standart” olarak kullanılmaktadır. Bu, birlikte çalışabilirliği büyük ölçüde artırır; örneğin Uvicorn’u başka bir ASGI sunucusuyla (Daphne veya Hypercorn gibi) değiştirebilir ya da `python-socketio` gibi ASGI uyumlu araçlar ekleyebilirsiniz. + +/// + +/// check | **FastAPI** bunu şurada kullanır + +Tüm temel web kısımlarını ele almak; üzerine özellikler eklemek. + +`FastAPI` sınıfı, doğrudan `Starlette` sınıfından miras alır. + +Dolayısıyla Starlette ile yapabildiğiniz her şeyi, adeta “turbo şarjlı Starlette” olan **FastAPI** ile de doğrudan yapabilirsiniz. + +/// + +### Uvicorn { #uvicorn } + +Uvicorn, uvloop ve httptools üzerinde inşa edilmiş, ışık hızında bir ASGI sunucusudur. + +Bir web framework’ü değil, bir sunucudur. Örneğin path’lere göre yönlendirme araçları sağlamaz; bunu Starlette (veya **FastAPI**) gibi bir framework üstte sağlar. + +Starlette ve **FastAPI** için önerilen sunucudur. + +/// check | **FastAPI** bunu şöyle önerir + +**FastAPI** uygulamalarını çalıştırmak için ana web sunucusu. + +Komut satırında `--workers` seçeneğini kullanarak asenkron çok süreçli (multi‑process) bir sunucu da elde edebilirsiniz. + +Daha fazla detay için [Dağıtım](deployment/index.md){.internal-link target=_blank} bölümüne bakın. + +/// + +## Kıyaslamalar ve Hız { #benchmarks-and-speed } + +Uvicorn, Starlette ve FastAPI arasındaki farkı anlamak ve karşılaştırmak için [Kıyaslamalar](benchmarks.md){.internal-link target=_blank} bölümüne göz atın. diff --git a/docs/tr/docs/deployment/concepts.md b/docs/tr/docs/deployment/concepts.md new file mode 100644 index 000000000..d0f568146 --- /dev/null +++ b/docs/tr/docs/deployment/concepts.md @@ -0,0 +1,321 @@ +# Deployment Kavramları { #deployments-concepts } + +Bir **FastAPI** uygulamasını (hatta genel olarak herhangi bir web API'yi) deploy ederken, muhtemelen önemseyeceğiniz bazı kavramlar vardır. Bu kavramları kullanarak, **uygulamanızı deploy etmek** için **en uygun** yöntemi bulabilirsiniz. + +Önemli kavramlardan bazıları şunlardır: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Bunların **deployment**'ları nasıl etkilediğine bakalım. + +Nihai hedef, **API client**'larınıza **güvenli** bir şekilde hizmet verebilmek, **kesintileri** önlemek ve **hesaplama kaynaklarını** (ör. uzak server'lar/sanal makineler) olabildiğince verimli kullanmaktır. + +Burada bu **kavramlar** hakkında biraz daha bilgi vereceğim. Böylece, çok farklı ortamlarda—hatta bugün var olmayan **gelecekteki** ortamlarda bile—API'nizi nasıl deploy edeceğinize karar verirken ihtiyaç duyacağınız **sezgiyi** kazanmış olursunuz. + +Bu kavramları dikkate alarak, **kendi API**'leriniz için en iyi deployment yaklaşımını **değerlendirebilir ve tasarlayabilirsiniz**. + +Sonraki bölümlerde, FastAPI uygulamalarını deploy etmek için daha **somut tarifler** (recipes) paylaşacağım. + +Ama şimdilik, bu önemli **kavramsal fikirleri** inceleyelim. Bu kavramlar diğer tüm web API türleri için de geçerlidir. + +## Güvenlik - HTTPS { #security-https } + +[HTTPS hakkındaki önceki bölümde](https.md){.internal-link target=_blank} HTTPS'in API'niz için nasıl şifreleme sağladığını öğrenmiştik. + +Ayrıca HTTPS'in genellikle uygulama server'ınızın **dışında** yer alan bir bileşen tarafından sağlandığını, yani bir **TLS Termination Proxy** ile yapıldığını da görmüştük. + +Ve **HTTPS sertifikalarını yenilemekten** sorumlu bir şey olmalıdır; bu aynı bileşen olabileceği gibi farklı bir bileşen de olabilir. + +### HTTPS için Örnek Araçlar { #example-tools-for-https } + +TLS Termination Proxy olarak kullanabileceğiniz bazı araçlar: + +* Traefik + * Sertifika yenilemelerini otomatik yönetir +* Caddy + * Sertifika yenilemelerini otomatik yönetir +* Nginx + * Sertifika yenilemeleri için Certbot gibi harici bir bileşenle +* HAProxy + * Sertifika yenilemeleri için Certbot gibi harici bir bileşenle +* Nginx gibi bir Ingress Controller ile Kubernetes + * Sertifika yenilemeleri için cert-manager gibi harici bir bileşenle +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi (aşağıyı okuyun) + +Bir diğer seçenek de, HTTPS kurulumunu da dahil olmak üzere işin daha büyük kısmını yapan bir **cloud service** kullanmaktır. Bunun bazı kısıtları olabilir veya daha pahalı olabilir vb. Ancak bu durumda TLS Termination Proxy'yi kendiniz kurmak zorunda kalmazsınız. + +Sonraki bölümlerde bazı somut örnekler göstereceğim. + +--- + +Sonraki kavramlar, gerçek API'nizi çalıştıran programla (ör. Uvicorn) ilgilidir. + +## Program ve Process { #program-and-process } + +Çalışan "**process**" hakkında çok konuşacağız. Bu yüzden ne anlama geldiğini ve "**program**" kelimesinden farkının ne olduğunu netleştirmek faydalı. + +### Program Nedir { #what-is-a-program } + +**Program** kelimesi günlük kullanımda birçok şeyi anlatmak için kullanılır: + +* Yazdığınız **code**, yani **Python dosyaları**. +* İşletim sistemi tarafından **çalıştırılabilen** **dosya**, örn: `python`, `python.exe` veya `uvicorn`. +* İşletim sistemi üzerinde **çalışır durumdayken** CPU kullanan ve bellekte veri tutan belirli bir program. Buna **process** de denir. + +### Process Nedir { #what-is-a-process } + +**Process** kelimesi genellikle daha spesifik kullanılır; yalnızca işletim sistemi üzerinde çalışan şeye (yukarıdaki son madde gibi) işaret eder: + +* İşletim sistemi üzerinde **çalışır durumda** olan belirli bir program. + * Bu; dosyayı ya da code'u değil, işletim sistemi tarafından **çalıştırılan** ve yönetilen şeyi ifade eder. +* Herhangi bir program, herhangi bir code, **yalnızca çalıştırılırken** bir şey yapabilir. Yani bir **process çalışıyorken**. +* Process siz tarafından veya işletim sistemi tarafından **sonlandırılabilir** (ya da "killed" edilebilir). O anda çalışması/çalıştırılması durur ve artık **hiçbir şey yapamaz**. +* Bilgisayarınızda çalışan her uygulamanın arkasında bir process vardır; çalışan her program, her pencere vb. Bilgisayar açıkken normalde **aynı anda** birçok process çalışır. +* Aynı anda **aynı programın birden fazla process**'i çalışabilir. + +İşletim sisteminizdeki "task manager" veya "system monitor" (ya da benzeri araçlar) ile bu process'lerin birçoğunu çalışır halde görebilirsiniz. + +Örneğin muhtemelen aynı browser programını (Firefox, Chrome, Edge vb.) çalıştıran birden fazla process göreceksiniz. Genelde her tab için bir process, üstüne bazı ek process'ler çalıştırırlar. + + + +--- + +Artık **process** ve **program** arasındaki farkı bildiğimize göre, deployment konusuna devam edelim. + +## Startup'ta Çalıştırma { #running-on-startup } + +Çoğu durumda bir web API oluşturduğunuzda, client'larınızın her zaman erişebilmesi için API'nizin kesintisiz şekilde **sürekli çalışıyor** olmasını istersiniz. Elbette sadece belirli durumlarda çalışmasını istemenizin özel bir sebebi olabilir; ancak çoğunlukla onu sürekli açık ve **kullanılabilir** halde tutarsınız. + +### Uzak Bir Server'da { #in-a-remote-server } + +Uzak bir server (cloud server, sanal makine vb.) kurduğunuzda, yapabileceğiniz en basit şey; local geliştirme sırasında yaptığınız gibi, manuel olarak `fastapi run` (Uvicorn'u kullanır) veya benzeri bir komutla çalıştırmaktır. + +Bu yöntem çalışır ve **geliştirme sırasında** faydalıdır. + +Ancak server'a olan bağlantınız koparsa, **çalışan process** muhtemelen ölür. + +Ve server yeniden başlatılırsa (örneğin update'lerden sonra ya da cloud provider'ın migration'larından sonra) bunu muhtemelen **fark etmezsiniz**. Dolayısıyla process'i manuel yeniden başlatmanız gerektiğini de bilmezsiniz. Sonuçta API'niz ölü kalır. + +### Startup'ta Otomatik Çalıştırma { #run-automatically-on-startup } + +Genellikle server programının (ör. Uvicorn) server açılışında otomatik başlamasını ve herhangi bir **insan müdahalesi** gerektirmeden API'nizi çalıştıran bir process'in sürekli ayakta olmasını istersiniz (ör. FastAPI uygulamanızı çalıştıran Uvicorn). + +### Ayrı Bir Program { #separate-program } + +Bunu sağlamak için genellikle startup'ta uygulamanızın çalıştığından emin olacak **ayrı bir program** kullanırsınız. Pek çok durumda bu program, örneğin bir veritabanı gibi diğer bileşenlerin/uygulamaların da çalıştığından emin olur. + +### Startup'ta Çalıştırmak için Örnek Araçlar { #example-tools-to-run-at-startup } + +Bu işi yapabilen araçlara örnekler: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi +* Diğerleri... + +Sonraki bölümlerde daha somut örnekler vereceğim. + +## Yeniden Başlatmalar { #restarts } + +Uygulamanızın startup'ta çalıştığından emin olmaya benzer şekilde, hatalardan sonra **yeniden başlatıldığından** da emin olmak istersiniz. + +### Hata Yaparız { #we-make-mistakes } + +Biz insanlar sürekli **hata** yaparız. Yazılımın neredeyse *her zaman* farklı yerlerinde gizli **bug**'lar vardır. + +Ve biz geliştiriciler bu bug'ları buldukça ve yeni özellikler ekledikçe code'u iyileştiririz (muhtemelen yeni bug'lar da ekleyerek). + +### Küçük Hatalar Otomatik Yönetilir { #small-errors-automatically-handled } + +FastAPI ile web API geliştirirken, code'umuzda bir hata olursa FastAPI genellikle bunu hatayı tetikleyen tek request ile sınırlar. + +Client o request için **500 Internal Server Error** alır; ancak uygulama tamamen çöküp durmak yerine sonraki request'ler için çalışmaya devam eder. + +### Daha Büyük Hatalar - Çökmeler { #bigger-errors-crashes } + +Yine de bazı durumlarda, yazdığımız bir code **tüm uygulamayı çökertip** Uvicorn ve Python'ın crash olmasına neden olabilir. + +Böyle bir durumda, tek bir noktadaki hata yüzünden uygulamanın ölü kalmasını istemezsiniz; bozuk olmayan *path operations* en azından çalışmaya devam etsin istersiniz. + +### Crash Sonrası Yeniden Başlatma { #restart-after-crash } + +Ancak çalışan **process**'i çökerten gerçekten kötü hatalarda, process'i **yeniden başlatmaktan** sorumlu harici bir bileşen istersiniz; en azından birkaç kez... + +/// tip | İpucu + +...Yine de uygulama **hemen crash oluyorsa**, onu sonsuza kadar yeniden başlatmaya çalışmanın pek anlamı yoktur. Böyle durumları büyük ihtimalle geliştirme sırasında ya da en geç deploy'dan hemen sonra fark edersiniz. + +O yüzden ana senaryoya odaklanalım: Gelecekte bazı özel durumlarda tamamen çökebilir ve yine de yeniden başlatmak mantıklıdır. + +/// + +Uygulamanızı yeniden başlatmakla görevli bileşenin **harici bir bileşen** olmasını istersiniz. Çünkü o noktada Uvicorn ve Python ile birlikte aynı uygulama zaten crash olmuştur; aynı app'in içindeki aynı code'un bunu düzeltmek için yapabileceği bir şey kalmaz. + +### Otomatik Yeniden Başlatma için Örnek Araçlar { #example-tools-to-restart-automatically } + +Çoğu durumda, **startup'ta programı çalıştırmak** için kullanılan aracın aynısı otomatik **restart**'ları yönetmek için de kullanılır. + +Örneğin bu şunlarla yönetilebilir: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi +* Diğerleri... + +## Replikasyon - Process'ler ve Bellek { #replication-processes-and-memory } + +FastAPI uygulamasında, Uvicorn'u çalıştıran `fastapi` komutu gibi bir server programı kullanırken, uygulamayı **tek bir process** içinde bir kez çalıştırmak bile aynı anda birden fazla client'a hizmet verebilir. + +Ancak birçok durumda, aynı anda birden fazla worker process çalıştırmak istersiniz. + +### Birden Fazla Process - Worker'lar { #multiple-processes-workers } + +Tek bir process'in karşılayabileceğinden daha fazla client'ınız varsa (örneğin sanal makine çok büyük değilse) ve server CPU'sunda **birden fazla core** varsa, aynı uygulamayla **birden fazla process** çalıştırıp tüm request'leri bunlara dağıtabilirsiniz. + +Aynı API programının **birden fazla process**'ini çalıştırdığınızda, bunlara genellikle **worker** denir. + +### Worker Process'ler ve Port'lar { #worker-processes-and-ports } + +[HTTPS hakkındaki dokümanda](https.md){.internal-link target=_blank} bir server'da aynı port ve IP adresi kombinasyonunu yalnızca tek bir process'in dinleyebileceğini hatırlıyor musunuz? + +Bu hâlâ geçerli. + +Dolayısıyla **aynı anda birden fazla process** çalıştırabilmek için, **port** üzerinde dinleyen **tek bir process** olmalı ve bu process iletişimi bir şekilde worker process'lere aktarmalıdır. + +### Process Başına Bellek { #memory-per-process } + +Program belleğe bir şeyler yüklediğinde—örneğin bir değişkende bir machine learning modelini veya büyük bir dosyanın içeriğini tutmak gibi—bunların hepsi server'ın **belleğini (RAM)** tüketir. + +Ve birden fazla process normalde **belleği paylaşmaz**. Yani her çalışan process'in kendi verileri, değişkenleri ve belleği vardır. Code'unuz çok bellek tüketiyorsa, **her process** buna denk bir miktar bellek tüketir. + +### Server Belleği { #server-memory } + +Örneğin code'unuz **1 GB** boyutunda bir Machine Learning modelini yüklüyorsa, API'niz tek process ile çalışırken en az 1 GB RAM tüketir. **4 process** (4 worker) başlatırsanız her biri 1 GB RAM tüketir. Yani toplamda API'niz **4 GB RAM** tüketir. + +Uzak server'ınız veya sanal makineniz yalnızca 3 GB RAM'e sahipse, 4 GB'tan fazla RAM yüklemeye çalışmak sorun çıkarır. + +### Birden Fazla Process - Bir Örnek { #multiple-processes-an-example } + +Bu örnekte, iki adet **Worker Process** başlatıp kontrol eden bir **Manager Process** vardır. + +Bu Manager Process büyük ihtimalle IP üzerindeki **port**'u dinleyen süreçtir ve tüm iletişimi worker process'lere aktarır. + +Worker process'ler uygulamanızı çalıştıran process'lerdir; bir **request** alıp bir **response** döndürmek için asıl hesaplamaları yaparlar ve sizin RAM'de değişkenlere koyduğunuz her şeyi yüklerler. + + + +Elbette aynı makinede, uygulamanız dışında da muhtemelen **başka process**'ler çalışır. + +İlginç bir detay: Her process'in kullandığı **CPU** yüzdesi zaman içinde çok **değişken** olabilir; ancak **bellek (RAM)** genellikle az çok **stabil** kalır. + +Eğer API'niz her seferinde benzer miktarda hesaplama yapıyorsa ve çok sayıda client'ınız varsa, **CPU kullanımı** da muhtemelen *stabil olur* (hızlı hızlı sürekli yükselip alçalmak yerine). + +### Replikasyon Araçları ve Stratejileri Örnekleri { #examples-of-replication-tools-and-strategies } + +Bunu başarmak için farklı yaklaşımlar olabilir. Sonraki bölümlerde, örneğin Docker ve container'lar konuşurken, belirli stratejileri daha detaylı anlatacağım. + +Dikkate almanız gereken ana kısıt şudur: **public IP** üzerindeki **port**'u yöneten **tek** bir bileşen olmalı. Sonrasında bu bileşenin, replikasyonla çoğaltılmış **process/worker**'lara iletişimi **aktarmanın** bir yoluna sahip olması gerekir. + +Olası kombinasyonlar ve stratejiler: + +* `--workers` ile **Uvicorn** + * Bir Uvicorn **process manager** **IP** ve **port** üzerinde dinler ve **birden fazla Uvicorn worker process** başlatır. +* **Kubernetes** ve diğer dağıtık **container sistemleri** + * **Kubernetes** katmanında bir şey **IP** ve **port** üzerinde dinler. Replikasyon, her birinde **tek bir Uvicorn process** çalışan **birden fazla container** ile yapılır. +* Bunu sizin yerinize yapan **cloud service**'ler + * Cloud service muhtemelen **replikasyonu sizin yerinize yönetir**. Size çalıştırılacak **bir process** veya kullanılacak bir **container image** tanımlama imkânı verebilir; her durumda büyük ihtimalle **tek bir Uvicorn process** olur ve bunu çoğaltmaktan cloud service sorumlu olur. + +/// tip | İpucu + +**Container**, Docker veya Kubernetes ile ilgili bazı maddeler şimdilik çok anlamlı gelmiyorsa dert etmeyin. + +Container image'ları, Docker, Kubernetes vb. konuları ilerideki bir bölümde daha detaylı anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## Başlatmadan Önceki Adımlar { #previous-steps-before-starting } + +Uygulamanızı **başlatmadan önce** bazı adımlar yapmak isteyeceğiniz birçok durum vardır. + +Örneğin **database migrations** çalıştırmak isteyebilirsiniz. + +Ancak çoğu durumda, bu adımları yalnızca **bir kez** çalıştırmak istersiniz. + +Bu yüzden, uygulamayı başlatmadan önce bu **ön adımları** çalıştıracak **tek bir process** olmasını istersiniz. + +Ve daha sonra uygulamanın kendisi için **birden fazla process** (birden fazla worker) başlatsanız bile, bu ön adımları çalıştıranın *yine* tek process olduğundan emin olmalısınız. Bu adımlar **birden fazla process** tarafından çalıştırılsaydı, işi **paralel** şekilde tekrarlarlardı. Adımlar database migration gibi hassas bir şeyse, birbirleriyle çakışıp çatışma çıkarabilirler. + +Elbette bazı durumlarda ön adımları birden fazla kez çalıştırmak sorun değildir; bu durumda yönetmesi çok daha kolay olur. + +/// tip | İpucu + +Ayrıca, kurulumunuza bağlı olarak bazı durumlarda uygulamanızı başlatmadan önce **hiç ön adıma ihtiyaç duymayabilirsiniz**. + +Bu durumda bunların hiçbirini düşünmeniz gerekmez. + +/// + +### Ön Adımlar için Strateji Örnekleri { #examples-of-previous-steps-strategies } + +Bu konu, **sisteminizi nasıl deploy ettiğinize** çok bağlıdır ve muhtemelen programları nasıl başlattığınız, restart'ları nasıl yönettiğiniz vb. ile bağlantılıdır. + +Bazı olası fikirler: + +* Kubernetes'te, app container'ınızdan önce çalışan bir "Init Container" +* Ön adımları çalıştırıp sonra uygulamanızı başlatan bir bash script + * Yine de o bash script'i başlatmak/restart etmek, hataları tespit etmek vb. için bir mekanizmaya ihtiyacınız olur. + +/// tip | İpucu + +Bunu container'larla nasıl yapabileceğinize dair daha somut örnekleri ilerideki bir bölümde anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## Kaynak Kullanımı { #resource-utilization } + +Server(lar)ınız bir **kaynaktır**. Programlarınızla CPU'lardaki hesaplama zamanını ve mevcut RAM belleğini tüketebilir veya **kullanabilirsiniz**. + +Sistem kaynaklarının ne kadarını tüketmek/kullanmak istersiniz? "Az" demek kolaydır; ancak pratikte hedef genellikle **çökmeden mümkün olduğunca fazla** kullanmaktır. + +3 server için para ödüyor ama onların RAM ve CPU'sunun yalnızca küçük bir kısmını kullanıyorsanız, muhtemelen **para israf ediyorsunuz** ve muhtemelen **elektrik tüketimini** de gereksiz yere artırıyorsunuz vb. + +Bu durumda 2 server ile devam edip onların kaynaklarını (CPU, bellek, disk, ağ bant genişliği vb.) daha yüksek oranlarda kullanmak daha iyi olabilir. + +Öte yandan, 2 server'ınız var ve CPU ile RAM'in **%100**'ünü kullanıyorsanız, bir noktada bir process daha fazla bellek ister; server diski "bellek" gibi kullanmak zorunda kalır (binlerce kat daha yavaş olabilir) ya da hatta **crash** edebilir. Ya da bir process bir hesaplama yapmak ister ve CPU tekrar boşalana kadar beklemek zorunda kalır. + +Bu senaryoda **bir server daha** eklemek ve bazı process'leri orada çalıştırmak daha iyi olur; böylece hepsinin **yeterli RAM'i ve CPU zamanı** olur. + +Ayrıca, herhangi bir sebeple API'nizde bir kullanım **spike**'ı olma ihtimali de vardır. Belki viral olur, belki başka servisler veya bot'lar kullanmaya başlar. Bu durumlarda güvende olmak için ekstra kaynak isteyebilirsiniz. + +Hedef olarak **keyfi bir sayı** belirleyebilirsiniz; örneğin kaynak kullanımını **%50 ile %90 arasında** tutmak gibi. Önemli olan, bunların muhtemelen ölçmek isteyeceğiniz ve deployment'larınızı ayarlamak için kullanacağınız ana metrikler olmasıdır. + +Server'ınızda CPU ve RAM kullanımını veya her process'in ne kadar kullandığını görmek için `htop` gibi basit araçları kullanabilirsiniz. Ya da server'lar arasında dağıtık çalışan daha karmaşık monitoring araçları kullanabilirsiniz. + +## Özet { #recap } + +Uygulamanızı nasıl deploy edeceğinize karar verirken aklınızda tutmanız gereken ana kavramların bazılarını okudunuz: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Bu fikirleri ve nasıl uygulayacağınızı anlamak, deployment'larınızı yapılandırırken ve ince ayar yaparken ihtiyaç duyacağınız sezgiyi kazanmanızı sağlamalıdır. + +Sonraki bölümlerde, izleyebileceğiniz stratejilere dair daha somut örnekler paylaşacağım. diff --git a/docs/tr/docs/deployment/docker.md b/docs/tr/docs/deployment/docker.md new file mode 100644 index 000000000..6c8f74c77 --- /dev/null +++ b/docs/tr/docs/deployment/docker.md @@ -0,0 +1,618 @@ +# Container'larda FastAPI - Docker { #fastapi-in-containers-docker } + +FastAPI uygulamalarını deploy ederken yaygın bir yaklaşım, bir **Linux container image** oluşturmaktır. Bu genellikle **Docker** kullanılarak yapılır. Ardından bu container image'ı birkaç farklı yöntemden biriyle deploy edebilirsiniz. + +Linux container'ları kullanmanın **güvenlik**, **tekrarlanabilirlik**, **basitlik** gibi birçok avantajı vardır. + +/// tip | İpucu + +Aceleniz var ve bunları zaten biliyor musunuz? Aşağıdaki [`Dockerfile`'a atlayın 👇](#build-a-docker-image-for-fastapi). + +/// + +
+Dockerfile Önizleme 👀 + +```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 ["fastapi", "run", "app/main.py", "--port", "80"] + +# If running behind a proxy like Nginx or Traefik add --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] +``` + +
+ +## Container Nedir { #what-is-a-container } + +Container'lar (özellikle Linux container'ları), bir uygulamayı tüm bağımlılıkları ve gerekli dosyalarıyla birlikte paketlemenin, aynı sistemdeki diğer container'lardan (diğer uygulama ya da bileşenlerden) izole tutarken yapılan, çok **hafif** bir yoludur. + +Linux container'ları, host'un (makine, sanal makine, cloud server vb.) aynı Linux kernel'ini kullanarak çalışır. Bu da, tüm bir işletim sistemini emüle eden tam sanal makinelere kıyasla çok daha hafif oldukları anlamına gelir. + +Bu sayede container'lar **az kaynak** tüketir; süreçleri doğrudan çalıştırmaya benzer bir seviyede (bir sanal makine çok daha fazla tüketirdi). + +Container'ların ayrıca kendi **izole** çalışan process'leri (çoğunlukla tek bir process), dosya sistemi ve ağı vardır. Bu da deployment, güvenlik, geliştirme vb. süreçleri kolaylaştırır. + +## Container Image Nedir { #what-is-a-container-image } + +Bir **container**, bir **container image**'dan çalıştırılır. + +Container image; bir container içinde bulunması gereken tüm dosyaların, environment variable'ların ve varsayılan komut/programın **statik** bir sürümüdür. Buradaki **statik**, container **image**'ının çalışmadığı, execute edilmediği; sadece paketlenmiş dosyalar ve metadata olduğu anlamına gelir. + +Depolanmış statik içerik olan "**container image**"ın aksine, "**container**" normalde çalışan instance'ı, yani **execute edilen** şeyi ifade eder. + +**Container** başlatılıp çalıştığında (bir **container image**'dan başlatılır), dosyalar oluşturabilir/değiştirebilir, environment variable'ları değiştirebilir vb. Bu değişiklikler sadece o container içinde kalır; alttaki container image'da kalıcı olmaz (diske kaydedilmez). + +Bir container image, **program** dosyası ve içeriklerine benzetilebilir; örn. `python` ve `main.py` gibi bir dosya. + +Ve **container**'ın kendisi (container image'a karşıt olarak) image'ın gerçek çalışan instance'ıdır; bir **process**'e benzer. Hatta bir container, yalnızca içinde **çalışan bir process** varken çalışır (ve genelde tek process olur). İçinde çalışan process kalmayınca container durur. + +## Container Image'lar { #container-images } + +Docker, **container image** ve **container** oluşturup yönetmek için kullanılan başlıca araçlardan biri olmuştur. + +Ayrıca birçok araç, ortam, veritabanı ve uygulama için önceden hazırlanmış **resmi container image**'ların bulunduğu herkese açık bir Docker Hub vardır. + +Örneğin, resmi bir Python Image bulunur. + +Ve veritabanları gibi farklı şeyler için de birçok image vardır; örneğin: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, vb. + +Hazır bir container image kullanarak farklı araçları **birleştirmek** ve birlikte kullanmak çok kolaydır. Örneğin yeni bir veritabanını denemek için. Çoğu durumda **resmi image**'ları kullanıp sadece environment variable'lar ile yapılandırmanız yeterlidir. + +Bu şekilde, çoğu zaman container'lar ve Docker hakkında öğrendiklerinizi farklı araç ve bileşenlerde tekrar kullanabilirsiniz. + +Dolayısıyla; veritabanı, Python uygulaması, React frontend uygulaması olan bir web server gibi farklı şeyler için **birden fazla container** çalıştırır ve bunları internal network üzerinden birbirine bağlarsınız. + +Docker veya Kubernetes gibi tüm container yönetim sistemlerinde bu ağ özellikleri entegre olarak bulunur. + +## Container'lar ve Process'ler { #containers-and-processes } + +Bir **container image** normalde metadata içinde, **container** başlatıldığında çalıştırılacak varsayılan program/komutu ve o programa geçirilecek parametreleri içerir. Bu, komut satırında yazacağınız şeye çok benzer. + +Bir **container** başlatıldığında bu komutu/programı çalıştırır (ancak isterseniz bunu override edip başka bir komut/program çalıştırabilirsiniz). + +Bir container, **ana process** (komut/program) çalıştığı sürece çalışır. + +Container'larda normalde **tek bir process** olur. Ancak ana process içinden subprocess'ler başlatmak da mümkündür; böylece aynı container içinde **birden fazla process** olur. + +Ama **en az bir çalışan process olmadan** çalışan bir container olamaz. Ana process durursa container da durur. + +## FastAPI için Docker Image Oluşturalım { #build-a-docker-image-for-fastapi } + +Tamam, şimdi bir şeyler inşa edelim! 🚀 + +Resmi **Python** image'ını temel alarak, FastAPI için **sıfırdan** bir **Docker image** nasıl oluşturulur göstereceğim. + +Bu, örneğin şu durumlarda **çoğu zaman** yapmak isteyeceğiniz şeydir: + +* **Kubernetes** veya benzeri araçlar kullanırken +* **Raspberry Pi** üzerinde çalıştırırken +* Container image'ınızı sizin için çalıştıran bir cloud servisi kullanırken, vb. + +### Paket Gereksinimleri { #package-requirements } + +Uygulamanızın **paket gereksinimleri** genelde bir dosyada yer alır. + +Bu, gereksinimleri **yüklemek** için kullandığınız araca göre değişir. + +En yaygın yöntem, paket adları ve versiyonlarının satır satır yazıldığı bir `requirements.txt` dosyasına sahip olmaktır. + +Versiyon aralıklarını belirlemek için elbette [FastAPI sürümleri hakkında](versions.md){.internal-link target=_blank} bölümünde okuduğunuz fikirleri kullanırsınız. + +Örneğin `requirements.txt` şöyle görünebilir: + +``` +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 +``` + +Ve bu bağımlılıkları normalde `pip` ile yüklersiniz, örneğin: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic +``` + +
+ +/// info | Bilgi + +Paket bağımlılıklarını tanımlamak ve yüklemek için başka formatlar ve araçlar da vardır. + +/// + +### **FastAPI** Kodunu Oluşturun { #create-the-fastapi-code } + +* Bir `app` dizini oluşturun ve içine girin. +* Boş bir `__init__.py` dosyası oluşturun. +* Aşağıdakilerle bir `main.py` dosyası oluşturun: + +```Python +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: str | None = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile { #dockerfile } + +Şimdi aynı proje dizininde `Dockerfile` adlı bir dosya oluşturun ve içine şunları yazın: + +```{ .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 ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +1. Resmi Python base image'ından başlayın. + +2. Geçerli çalışma dizinini `/code` olarak ayarlayın. + + `requirements.txt` dosyasını ve `app` dizinini buraya koyacağız. + +3. Gereksinimleri içeren dosyayı `/code` dizinine kopyalayın. + + Önce kodun tamamını değil, **sadece** gereksinim dosyasını kopyalayın. + + Bu dosya **çok sık değişmediği** için Docker bunu tespit eder ve bu adımda **cache** kullanır; böylece bir sonraki adım için de cache devreye girer. + +4. Gereksinim dosyasındaki paket bağımlılıklarını yükleyin. + + `--no-cache-dir` seçeneği, indirilen paketlerin yerel olarak kaydedilmemesini `pip`'e söyler. Bu kayıt, `pip` aynı paketleri tekrar yüklemek için yeniden çalıştırılacaksa işe yarar; ancak container'larla çalışırken genelde bu durum geçerli değildir. + + /// note | Not + + `--no-cache-dir` yalnızca `pip` ile ilgilidir; Docker veya container'larla ilgili değildir. + + /// + + `--upgrade` seçeneği, paketler zaten yüklüyse `pip`'e onları yükseltmesini söyler. + + Bir önceki adım (dosyayı kopyalama) **Docker cache** tarafından tespit edilebildiği için, bu adım da uygun olduğunda **Docker cache'i kullanır**. + + Bu adımda cache kullanmak, geliştirme sırasında image'ı tekrar tekrar build ederken size çok **zaman** kazandırır; her seferinde bağımlılıkları **indirip yüklemek** zorunda kalmazsınız. + +5. `./app` dizinini `/code` dizininin içine kopyalayın. + + Burada en sık değişen şey olan kodun tamamı bulunduğundan, bu adım (ve genelde bundan sonraki adımlar) için Docker **cache**'i kolay kolay kullanılamaz. + + Bu yüzden, container image build sürelerini optimize etmek için bunu `Dockerfile`'ın **sonlarına yakın** koymak önemlidir. + +6. Altta Uvicorn kullanan `fastapi run` komutunu **command** olarak ayarlayın. + + `CMD` bir string listesi alır; bu string'lerin her biri komut satırında boşlukla ayrılmış şekilde yazacağınız parçaları temsil eder. + + Bu komut, yukarıda `WORKDIR /code` ile ayarladığınız `/code` dizininden çalıştırılır. + +/// tip | İpucu + +Kod içindeki her numara balonuna tıklayarak her satırın ne yaptığını gözden geçirin. 👆 + +/// + +/// warning | Uyarı + +Aşağıda açıklandığı gibi `CMD` talimatının **her zaman** **exec form**'unu kullandığınızdan emin olun. + +/// + +#### `CMD` Kullanımı - Exec Form { #use-cmd-exec-form } + +`CMD` Docker talimatı iki formda yazılabilir: + +✅ **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'nin düzgün şekilde kapanabilmesi ve [lifespan event](../advanced/events.md){.internal-link target=_blank}'lerinin tetiklenmesi için her zaman **exec** formunu kullanın. + +Detaylar için shell ve exec form için Docker dokümanlarına bakabilirsiniz. + +Bu durum `docker compose` kullanırken oldukça belirgin olabilir. Daha teknik detaylar için şu Docker Compose FAQ bölümüne bakın: Hizmetlerimin yeniden oluşturulması veya durması neden 10 saniye sürüyor?. + +#### Dizin Yapısı { #directory-structure } + +Artık dizin yapınız şöyle olmalı: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### TLS Termination Proxy Arkasında { #behind-a-tls-termination-proxy } + +Container'ınızı Nginx veya Traefik gibi bir TLS Termination Proxy (load balancer) arkasında çalıştırıyorsanız `--proxy-headers` seçeneğini ekleyin. Bu, Uvicorn'a (FastAPI CLI üzerinden) uygulamanın HTTPS arkasında çalıştığını söyleyen proxy header'larına güvenmesini söyler. + +```Dockerfile +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] +``` + +#### Docker Cache { #docker-cache } + +Bu `Dockerfile` içinde önemli bir numara var: önce kodun geri kalanını değil, **sadece bağımlılık dosyasını** kopyalıyoruz. Nedenini anlatayım. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker ve benzeri araçlar bu container image'larını **artımlı (incremental)** olarak **build** eder; `Dockerfile`'ın en üstünden başlayıp her talimatın oluşturduğu dosyaları ekleyerek **katman katman (layer)** ilerler. + +Docker ve benzeri araçlar image build ederken ayrıca bir **internal cache** kullanır. Son build'den beri bir dosya değişmediyse, dosyayı tekrar kopyalayıp sıfırdan yeni bir layer oluşturmak yerine, daha önce oluşturulan **aynı layer**'ı yeniden kullanır. + +Sadece dosya kopyalamayı azaltmak her zaman büyük fark yaratmaz. Ancak o adımda cache kullanıldığı için, **bir sonraki adımda da cache kullanılabilir**. Örneğin bağımlılıkları yükleyen şu talimat için: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Paket gereksinimleri dosyası **sık sık değişmez**. Bu yüzden sadece bu dosyayı kopyalayınca, Docker bu adımda **cache** kullanabilir. + +Sonra Docker, bağımlılıkları indirip yükleyen **bir sonraki adımda** da cache kullanabilir. Asıl **çok zaman kazandığımız** yer de burasıdır. ✨ ...ve beklerken sıkılmayı engeller. 😪😆 + +Bağımlılıkları indirip yüklemek **dakikalar sürebilir**, fakat **cache** kullanmak en fazla **saniyeler** alır. + +Geliştirme sırasında kod değişikliklerinizin çalıştığını kontrol etmek için container image'ı tekrar tekrar build edeceğinizden, bu ciddi birikimli zaman kazancı sağlar. + +Sonra `Dockerfile`'ın sonlarına doğru tüm kodu kopyalarız. En sık değişen kısım bu olduğu için sona koyarız; çünkü neredeyse her zaman bu adımdan sonra gelen adımlar cache kullanamaz. + +```Dockerfile +COPY ./app /code/app +``` + +### Docker Image'ını Build Edin { #build-the-docker-image } + +Tüm dosyalar hazır olduğuna göre container image'ı build edelim. + +* Proje dizinine gidin (`Dockerfile`'ınızın olduğu ve `app` dizininizi içeren dizin). +* FastAPI image'ınızı build edin: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +/// tip | İpucu + +Sondaki `.` ifadesine dikkat edin; `./` ile aynı anlama gelir ve Docker'a container image build etmek için hangi dizini kullanacağını söyler. + +Bu örnekte, mevcut dizindir (`.`). + +/// + +### Docker Container'ını Başlatın { #start-the-docker-container } + +* Image'ınızdan bir container çalıştırın: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## Kontrol Edin { #check-it } + +Docker container'ınızın URL'inden kontrol edebilmelisiniz. Örneğin: http://192.168.99.100/items/5?q=somequery veya http://127.0.0.1/items/5?q=somequery (ya da Docker host'unuzu kullanarak eşdeğeri). + +Şuna benzer bir şey görürsünüz: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## Etkileşimli API Dokümanları { #interactive-api-docs } + +Şimdi http://192.168.99.100/docs veya http://127.0.0.1/docs adresine gidebilirsiniz (ya da Docker host'unuzla eşdeğeri). + +Otomatik etkileşimli API dokümantasyonunu görürsünüz ( Swagger UI tarafından sağlanır): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## Alternatif API Dokümanları { #alternative-api-docs } + +Ayrıca http://192.168.99.100/redoc veya http://127.0.0.1/redoc adresine de gidebilirsiniz (ya da Docker host'unuzla eşdeğeri). + +Alternatif otomatik dokümantasyonu görürsünüz (ReDoc tarafından sağlanır): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Tek Dosyalık FastAPI ile Docker Image Oluşturma { #build-a-docker-image-with-a-single-file-fastapi } + +FastAPI uygulamanız tek bir dosyaysa; örneğin `./app` dizini olmadan sadece `main.py` varsa, dosya yapınız şöyle olabilir: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +Bu durumda `Dockerfile` içinde dosyayı kopyaladığınız path'leri buna göre değiştirmeniz yeterlidir: + +```{ .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 ["fastapi", "run", "main.py", "--port", "80"] +``` + +1. `main.py` dosyasını doğrudan `/code` dizinine kopyalayın (herhangi bir `./app` dizini olmadan). + +2. Tek dosya olan `main.py` içindeki uygulamanızı sunmak için `fastapi run` kullanın. + +Dosyayı `fastapi run`'a verdiğinizde, bunun bir package'ın parçası değil tek bir dosya olduğunu otomatik olarak algılar; nasıl import edip FastAPI uygulamanızı nasıl serve edeceğini bilir. 😎 + +## Deployment Kavramları { #deployment-concepts } + +Aynı [Deployment Kavramları](concepts.md){.internal-link target=_blank}nı bu kez container'lar açısından tekrar konuşalım. + +Container'lar, bir uygulamayı **build etme ve deploy etme** sürecini basitleştiren bir araçtır. Ancak bu **deployment kavramları**nı ele almak için belirli bir yaklaşımı zorunlu kılmazlar; birkaç farklı strateji mümkündür. + +**İyi haber** şu: Hangi stratejiyi seçerseniz seçin, deployment kavramlarının tamamını kapsayacak bir yol vardır. 🎉 + +Bu **deployment kavramları**nı container'lar açısından gözden geçirelim: + +* HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +## HTTPS { #https } + +Bir FastAPI uygulamasının sadece **container image**'ına (ve sonra çalışan **container**'a) odaklanırsak, HTTPS genellikle **haricen** başka bir araçla ele alınır. + +Örneğin Traefik kullanan başka bir container olabilir; **HTTPS** ve **sertifika**ların **otomatik** alınmasını o yönetebilir. + +/// tip | İpucu + +Traefik; Docker, Kubernetes ve diğerleriyle entegre çalışır. Bu sayede container'larınız için HTTPS'i kurup yapılandırmak oldukça kolaydır. + +/// + +Alternatif olarak HTTPS, bir cloud provider'ın sunduğu servislerden biri tarafından da yönetilebilir (uygulama yine container içinde çalışırken). + +## Startup'ta Çalıştırma ve Restart'lar { #running-on-startup-and-restarts } + +Container'ınızı **başlatıp çalıştırmaktan** sorumlu genellikle başka bir araç olur. + +Bu; doğrudan **Docker**, **Docker Compose**, **Kubernetes**, bir **cloud service** vb. olabilir. + +Çoğu (veya tüm) durumda, container'ı startup'ta çalıştırmayı ve hata durumlarında restart'ları etkinleştirmeyi sağlayan basit bir seçenek vardır. Örneğin Docker'da bu, `--restart` komut satırı seçeneğidir. + +Container kullanmadan, uygulamaları startup'ta çalıştırmak ve restart mekanizması eklemek zahmetli ve zor olabilir. Ancak **container'larla çalışırken** çoğu zaman bu işlevler varsayılan olarak hazır gelir. ✨ + +## Replication - Process Sayısı { #replication-number-of-processes } + +Kubernetes, Docker Swarm Mode, Nomad veya benzeri, birden fazla makinede dağıtık container'ları yöneten karmaşık bir sistemle kurulmuş bir cluster'ınız varsa, replication'ı her container içinde bir **process manager** (ör. worker'lı Uvicorn) kullanarak yönetmek yerine, muhtemelen **cluster seviyesinde** ele almak istersiniz. + +Kubernetes gibi dağıtık container yönetim sistemleri, gelen request'ler için **load balancing** desteği sunarken aynı zamanda **container replication**'ını yönetmek için entegre mekanizmalara sahiptir. Hepsi **cluster seviyesinde**. + +Bu tür durumlarda, yukarıda [anlatıldığı gibi](#dockerfile) bağımlılıkları yükleyip **sıfırdan bir Docker image** build etmek ve birden fazla Uvicorn worker kullanmak yerine **tek bir Uvicorn process** çalıştırmak istersiniz. + +### Load Balancer { #load-balancer } + +Container'lar kullanırken, genellikle ana port'ta dinleyen bir bileşen olur. Bu, **HTTPS**'i ele almak için bir **TLS Termination Proxy** olan başka bir container da olabilir ya da benzeri bir araç olabilir. + +Bu bileşen request'lerin **yükünü** alıp worker'lar arasında (umarım) **dengeli** şekilde dağıttığı için yaygın olarak **Load Balancer** diye de adlandırılır. + +/// tip | İpucu + +HTTPS için kullanılan aynı **TLS Termination Proxy** bileşeni muhtemelen bir **Load Balancer** olarak da çalışır. + +/// + +Container'larla çalışırken, onları başlatıp yönettiğiniz sistem; bu **load balancer**'dan (aynı zamanda **TLS Termination Proxy** de olabilir) uygulamanızın bulunduğu container(lar)a **network communication**'ı (ör. HTTP request'leri) iletmek için zaten dahili araçlar sunar. + +### Tek Load Balancer - Çoklu Worker Container { #one-load-balancer-multiple-worker-containers } + +**Kubernetes** veya benzeri dağıtık container yönetim sistemleriyle çalışırken, dahili ağ mekanizmaları sayesinde ana **port**'u dinleyen tek bir **load balancer**, uygulamanızı çalıştıran muhtemelen **birden fazla container**'a request'leri iletebilir. + +Uygulamanızı çalıştıran bu container'ların her birinde normalde **tek bir process** olur (ör. FastAPI uygulamanızı çalıştıran bir Uvicorn process). Hepsi aynı şeyi çalıştıran **özdeş container**'lardır; ancak her birinin kendi process'i, memory'si vb. vardır. Böylece CPU'nun **farklı core**'larında, hatta **farklı makinelerde** paralelleştirmeden yararlanırsınız. + +Load balancer'lı dağıtık sistem, request'leri uygulamanızın bulunduğu container'ların her birine sırayla **dağıtır**. Böylece her request, uygulamanızın birden fazla **replicated container**'ından biri tarafından işlenebilir. + +Ve bu **load balancer** normalde cluster'ınızdaki *diğer* uygulamalara giden request'leri de (ör. farklı bir domain ya da farklı bir URL path prefix altında) yönetebilir ve iletişimi o *diğer* uygulamanın doğru container'larına iletir. + +### Container Başına Tek Process { #one-process-per-container } + +Bu senaryoda, replication'ı zaten cluster seviyesinde yaptığınız için, muhtemelen **container başına tek bir (Uvicorn) process** istersiniz. + +Dolayısıyla bu durumda container içinde `--workers` gibi bir komut satırı seçeneğiyle çoklu worker istemezsiniz. Container başına sadece **tek bir Uvicorn process** istersiniz (ama muhtemelen birden fazla container). + +Container içine ekstra bir process manager koymak (çoklu worker gibi) çoğu zaman zaten cluster sisteminizle çözdüğünüz şeye ek **gereksiz karmaşıklık** katar. + +### Birden Fazla Process'li Container'lar ve Özel Durumlar { #containers-with-multiple-processes-and-special-cases } + +Elbette bazı **özel durumlarda** bir container içinde birden fazla **Uvicorn worker process** çalıştırmak isteyebilirsiniz. + +Bu durumlarda çalıştırmak istediğiniz worker sayısını `--workers` komut satırı seçeneğiyle ayarlayabilirsiniz: + +```{ .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. Burada worker sayısını 4 yapmak için `--workers` komut satırı seçeneğini kullanıyoruz. + +Bunun mantıklı olabileceği birkaç örnek: + +#### Basit Bir Uygulama { #a-simple-app } + +Uygulamanız tek bir server üzerinde (cluster değil) çalışacak kadar **basitse**, container içinde bir process manager isteyebilirsiniz. + +#### Docker Compose { #docker-compose } + +**Docker Compose** ile **tek bir server**'a (cluster değil) deploy ediyor olabilirsiniz. Bu durumda, paylaşılan ağı ve **load balancing**'i koruyarak container replication'ını (Docker Compose ile) yönetmenin kolay bir yolu olmayabilir. + +Bu durumda, tek bir container içinde **bir process manager** ile **birden fazla worker process** başlatmak isteyebilirsiniz. + +--- + +Ana fikir şu: Bunların **hiçbiri** körü körüne uymanız gereken **değişmez kurallar** değildir. Bu fikirleri, kendi kullanım senaryonuzu **değerlendirmek** ve sisteminiz için en iyi yaklaşımı seçmek için kullanabilirsiniz. Şu kavramları nasıl yöneteceğinize bakarak karar verin: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +## Memory { #memory } + +**Container başına tek process** çalıştırırsanız, her container'ın tüketeceği memory miktarı aşağı yukarı tanımlı, stabil ve sınırlı olur (replication varsa birden fazla container için). + +Sonra aynı memory limit ve gereksinimlerini container yönetim sisteminizin (ör. **Kubernetes**) konfigürasyonlarında belirleyebilirsiniz. Böylece sistem; ihtiyaç duyulan memory miktarını ve cluster'daki makinelerde mevcut memory'yi dikkate alarak **uygun makinelerde container'ları replicate edebilir**. + +Uygulamanız **basitse**, muhtemelen bu **bir sorun olmaz** ve katı memory limitleri belirlemeniz gerekmeyebilir. Ancak **çok memory kullanıyorsanız** (ör. **machine learning** modelleriyle), ne kadar memory tükettiğinizi kontrol edip **her makinede** çalışacak **container sayısını** ayarlamalısınız (ve gerekirse cluster'a daha fazla makine eklemelisiniz). + +**Container başına birden fazla process** çalıştırırsanız, başlatılan process sayısının mevcut olandan **fazla memory tüketmediğinden** emin olmanız gerekir. + +## Başlatmadan Önceki Adımlar ve Container'lar { #previous-steps-before-starting-and-containers } + +Container kullanıyorsanız (örn. Docker, Kubernetes), temelde iki yaklaşım vardır. + +### Birden Fazla Container { #multiple-containers } + +**Birden fazla container**'ınız varsa ve muhtemelen her biri **tek process** çalıştırıyorsa (ör. bir **Kubernetes** cluster'ında), replication yapılan worker container'lar çalışmadan **önce**, **başlatmadan önceki adımlar**ın işini yapan **ayrı bir container** kullanmak isteyebilirsiniz (tek container, tek process). + +/// info | Bilgi + +Kubernetes kullanıyorsanız, bu muhtemelen bir Init Container olur. + +/// + +Kullanım senaryonuzda bu adımları **paralel olarak birden fazla kez** çalıştırmak sorun değilse (örneğin veritabanı migration çalıştırmıyor, sadece veritabanı hazır mı diye kontrol ediyorsanız), o zaman her container'da ana process başlamadan hemen önce de çalıştırabilirsiniz. + +### Tek Container { #single-container } + +Basit bir kurulumda; **tek bir container** olup onun içinde birden fazla **worker process** (ya da sadece bir process) başlatıyorsanız, bu adımları aynı container içinde, uygulama process'ini başlatmadan hemen önce çalıştırabilirsiniz. + +### Base Docker Image { #base-docker-image } + +Eskiden resmi bir FastAPI Docker image'ı vardı: tiangolo/uvicorn-gunicorn-fastapi. Ancak artık kullanımdan kaldırıldı (deprecated). ⛔️ + +Muhtemelen bu base Docker image'ını (veya benzeri başka bir image'ı) kullanmamalısınız. + +**Kubernetes** (veya diğerleri) kullanıyor ve cluster seviyesinde birden fazla **container** ile **replication** ayarlıyorsanız, bu durumda yukarıda anlatıldığı gibi **sıfırdan bir image build etmek** daha iyi olur: [FastAPI için Docker Image Oluşturalım](#build-a-docker-image-for-fastapi). + +Ve birden fazla worker gerekiyorsa, sadece `--workers` komut satırı seçeneğini kullanabilirsiniz. + +/// note | Teknik Detaylar + +Bu Docker image, Uvicorn dead worker'ları yönetmeyi ve yeniden başlatmayı desteklemediği dönemde oluşturulmuştu. Bu yüzden Uvicorn ile birlikte Gunicorn kullanmak gerekiyordu; sırf Gunicorn, Uvicorn worker process'lerini yönetip yeniden başlatsın diye oldukça fazla karmaşıklık ekleniyordu. + +Ancak artık Uvicorn (ve `fastapi` komutu) `--workers` kullanımını desteklediğine göre, kendi image'ınızı build etmek yerine bir base Docker image kullanmanın bir nedeni kalmadı (kod miktarı da hemen hemen aynı 😅). + +/// + +## Container Image'ı Deploy Etme { #deploy-the-container-image } + +Bir Container (Docker) Image'ınız olduktan sonra bunu deploy etmenin birkaç yolu vardır. + +Örneğin: + +* Tek bir server'da **Docker Compose** ile +* Bir **Kubernetes** cluster'ı ile +* Docker Swarm Mode cluster'ı ile +* Nomad gibi başka bir araçla +* Container image'ınızı alıp deploy eden bir cloud servisiyle + +## `uv` ile Docker Image { #docker-image-with-uv } + +Projenizi yüklemek ve yönetmek için uv kullanıyorsanız, onların uv Docker rehberini takip edebilirsiniz. + +## Özet { #recap } + +Container sistemleri (örn. **Docker** ve **Kubernetes** ile) kullanınca tüm **deployment kavramları**nı ele almak oldukça kolaylaşır: + +* HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +Çoğu durumda bir base image kullanmak istemezsiniz; bunun yerine resmi Python Docker image'ını temel alarak **sıfırdan bir container image** build edersiniz. + +`Dockerfile` içindeki talimatların **sırasına** ve **Docker cache**'ine dikkat ederek **build sürelerini minimize edebilir**, üretkenliğinizi artırabilirsiniz (ve beklerken sıkılmayı önlersiniz). 😎 diff --git a/docs/tr/docs/deployment/fastapicloud.md b/docs/tr/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..bb861273b --- /dev/null +++ b/docs/tr/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +FastAPI uygulamanızı FastAPI Cloud'a **tek bir komutla** deploy edebilirsiniz. Henüz yapmadıysanız gidip bekleme listesine katılın. 🚀 + +## Giriş Yapma { #login } + +Önceden bir **FastAPI Cloud** hesabınız olduğundan emin olun (sizi bekleme listesinden davet ettik 😉). + +Ardından giriş yapın: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deploy { #deploy } + +Şimdi uygulamanızı **tek bir komutla** deploy edin: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Hepsi bu! Artık uygulamanıza o URL üzerinden erişebilirsiniz. ✨ + +## FastAPI Cloud Hakkında { #about-fastapi-cloud } + +**FastAPI Cloud**, **FastAPI**'nin arkasındaki aynı yazar ve ekip tarafından geliştirilmiştir. + +Bir API'yi minimum eforla **geliştirme**, **deploy etme** ve **erişilebilir kılma** sürecini sadeleştirir. + +FastAPI ile uygulama geliştirirken elde ettiğiniz aynı **developer experience**'ı, onları buluta **deploy etmeye** de taşır. 🎉 + +Ayrıca bir uygulamayı deploy ederken ihtiyaç duyacağınız pek çok şeyi de sizin için halleder; örneğin: + +* HTTPS +* Replication (çoğaltma), request'lere göre autoscaling ile +* vb. + +FastAPI Cloud, *FastAPI and friends* açık kaynak projelerinin birincil sponsoru ve finansman sağlayıcısıdır. ✨ + +## Diğer cloud sağlayıcılarına deploy etme { #deploy-to-other-cloud-providers } + +FastAPI açık kaynaklıdır ve standartlara dayanır. FastAPI uygulamalarını seçtiğiniz herhangi bir cloud sağlayıcısına deploy edebilirsiniz. + +FastAPI uygulamalarını deploy etmek için cloud sağlayıcınızın kendi kılavuzlarını takip edin. 🤓 + +## Kendi server'ınıza deploy etme { #deploy-your-own-server } + +Bu **Deployment** kılavuzunun ilerleyen bölümlerinde tüm detayları da ele alacağız; böylece neler olduğunu, nelerin gerçekleşmesi gerektiğini ve FastAPI uygulamalarını kendi başınıza (kendi server'larınızla da) nasıl deploy edebileceğinizi anlayacaksınız. 🤓 diff --git a/docs/tr/docs/deployment/https.md b/docs/tr/docs/deployment/https.md new file mode 100644 index 000000000..bb70883aa --- /dev/null +++ b/docs/tr/docs/deployment/https.md @@ -0,0 +1,231 @@ +# HTTPS Hakkında { #about-https } + +HTTPS’in sadece "açık" ya da "kapalı" olan bir şey olduğunu düşünmek kolaydır. + +Ancak bundan çok daha karmaşıktır. + +/// tip | İpucu + +Aceleniz varsa veya çok da önemsemiyorsanız, her şeyi farklı tekniklerle adım adım kurmak için sonraki bölümlere geçin. + +/// + +Bir kullanıcı gözüyle **HTTPS’in temellerini öğrenmek** için https://howhttps.works/ adresine bakın. + +Şimdi de **geliştirici perspektifinden**, HTTPS hakkında düşünürken akılda tutulması gereken birkaç nokta: + +* HTTPS için **server**’ın, **üçüncü bir taraf** tarafından verilen **"sertifikalara"** sahip olması gerekir. + * Bu sertifikalar aslında üçüncü tarafça "üretilmez", üçüncü taraftan **temin edilir**. +* Sertifikaların bir **geçerlilik süresi** vardır. + * Süresi **dolar**. + * Sonrasında **yenilenmeleri**, üçüncü taraftan **yeniden temin edilmeleri** gerekir. +* Bağlantının şifrelenmesi **TCP seviyesinde** gerçekleşir. + * Bu, **HTTP’nin bir katman altıdır**. + * Dolayısıyla **sertifika ve şifreleme** işlemleri **HTTP’den önce** yapılır. +* **TCP "domain"leri bilmez**. Yalnızca IP adreslerini bilir. + * İstenen **spesifik domain** bilgisi **HTTP verisinin** içindedir. +* **HTTPS sertifikaları** belirli bir **domain**’i "sertifikalandırır"; ancak protokol ve şifreleme TCP seviyesinde, hangi domain ile çalışıldığı **henüz bilinmeden** gerçekleşir. +* **Varsayılan olarak** bu, IP adresi başına yalnızca **bir HTTPS sertifikası** olabileceği anlamına gelir. + * Server’ınız ne kadar büyük olursa olsun ya da üzerindeki her uygulama ne kadar küçük olursa olsun. + * Ancak bunun bir **çözümü** vardır. +* **TLS** protokolüne (TCP seviyesinde, HTTP’den önce şifrelemeyi yapan) eklenen **SNI** adlı bir **extension** vardır. + * Bu SNI extension’ı, tek bir server’ın (tek bir **IP adresiyle**) **birden fazla HTTPS sertifikası** kullanmasına ve **birden fazla HTTPS domain/uygulama** sunmasına izin verir. + * Bunun çalışması için server üzerinde, **public IP adresini** dinleyen tek bir bileşenin (programın) server’daki **tüm HTTPS sertifikalarına** sahip olması gerekir. +* Güvenli bir bağlantı elde edildikten **sonra**, iletişim protokolü **hâlâ HTTP**’dir. + * İçerikler, **HTTP protokolü** ile gönderiliyor olsa bile **şifrelenmiştir**. + +Yaygın yaklaşım, server’da (makine, host vb.) çalışan **tek bir program/HTTP server** bulundurup **HTTPS ile ilgili tüm kısımları** yönetmektir: **şifreli HTTPS request**’leri almak, aynı server’da çalışan gerçek HTTP uygulamasına (bu örnekte **FastAPI** uygulaması) **şifresi çözülmüş HTTP request**’leri iletmek, uygulamadan gelen **HTTP response**’u almak, uygun **HTTPS sertifikası** ile **şifrelemek** ve **HTTPS** ile client’a geri göndermek. Bu server’a çoğu zaman **TLS Termination Proxy** denir. + +TLS Termination Proxy olarak kullanabileceğiniz seçeneklerden bazıları: + +* Traefik (sertifika yenilemelerini de yönetebilir) +* Caddy (sertifika yenilemelerini de yönetebilir) +* Nginx +* HAProxy + +## Let's Encrypt { #lets-encrypt } + +Let's Encrypt’ten önce bu **HTTPS sertifikaları**, güvenilen üçüncü taraflar tarafından satılırdı. + +Bu sertifikalardan birini temin etme süreci zahmetliydi, epey evrak işi gerektirirdi ve sertifikalar oldukça pahalıydı. + +Sonra **Let's Encrypt** ortaya çıktı. + +Linux Foundation’ın bir projesidir. **HTTPS sertifikalarını ücretsiz** ve otomatik bir şekilde sağlar. Bu sertifikalar tüm standart kriptografik güvenliği kullanır ve kısa ömürlüdür (yaklaşık 3 ay). Bu yüzden, ömürleri kısa olduğu için **güvenlik aslında daha iyidir**. + +Domain’ler güvenli şekilde doğrulanır ve sertifikalar otomatik üretilir. Bu sayede sertifikaların yenilenmesini otomatikleştirmek de mümkün olur. + +Amaç, bu sertifikaların temin edilmesi ve yenilenmesini otomatikleştirerek **ücretsiz, kalıcı olarak güvenli HTTPS** sağlamaktır. + +## Geliştiriciler İçin HTTPS { #https-for-developers } + +Burada, bir HTTPS API’nin adım adım nasıl görünebileceğine dair, özellikle geliştiriciler için önemli fikirlere odaklanan bir örnek var. + +### Domain Adı { #domain-name } + +Muhtemelen her şey, bir **domain adı** **temin etmenizle** başlar. Sonra bunu bir DNS server’ında (muhtemelen aynı cloud provider’ınızda) yapılandırırsınız. + +Muhtemelen bir cloud server (virtual machine) ya da benzeri bir şey alırsınız ve bunun fixed bir **public IP adresi** olur. + +DNS server(lar)ında, bir kaydı ("`A record`") **domain**’inizi server’ınızın **public IP adresine** yönlendirecek şekilde yapılandırırsınız. + +Bunu büyük olasılıkla ilk kurulumda, sadece bir kez yaparsınız. + +/// tip | İpucu + +Bu Domain Adı kısmı HTTPS’ten çok daha önce gelir. Ancak her şey domain ve IP adresine bağlı olduğu için burada bahsetmeye değer. + +/// + +### DNS { #dns } + +Şimdi gerçek HTTPS parçalarına odaklanalım. + +Önce tarayıcı, bu örnekte `someapp.example.com` olan domain için **IP**’nin ne olduğunu **DNS server**’larına sorar. + +DNS server’ları tarayıcıya belirli bir **IP adresini** kullanmasını söyler. Bu, DNS server’larında yapılandırdığınız ve server’ınızın kullandığı public IP adresidir. + + + +### TLS Handshake Başlangıcı { #tls-handshake-start } + +Tarayıcı daha sonra bu IP adresiyle **443 portu** (HTTPS portu) üzerinden iletişim kurar. + +İletişimin ilk kısmı, client ile server arasında bağlantıyı kurmak ve hangi kriptografik anahtarların kullanılacağına karar vermek vb. içindir. + + + +Client ile server arasındaki, TLS bağlantısını kurmaya yönelik bu etkileşime **TLS handshake** denir. + +### SNI Extension’ı ile TLS { #tls-with-sni-extension } + +Server’da, belirli bir **IP adresindeki** belirli bir **portu** dinleyen **yalnızca bir process** olabilir. Aynı IP adresinde başka portları dinleyen başka process’ler olabilir, ancak IP+port kombinasyonu başına yalnızca bir tane olur. + +TLS (HTTPS) varsayılan olarak `443` portunu kullanır. Yani ihtiyaç duyacağımız port budur. + +Bu portu yalnızca bir process dinleyebileceği için, bunu yapacak process **TLS Termination Proxy** olur. + +TLS Termination Proxy, bir ya da daha fazla **TLS sertifikasına** (HTTPS sertifikası) erişebilir. + +Yukarıda bahsettiğimiz **SNI extension**’ını kullanarak TLS Termination Proxy, bu bağlantı için elindeki TLS (HTTPS) sertifikalarından hangisini kullanacağını kontrol eder; client’ın beklediği domain ile eşleşen sertifikayı seçer. + +Bu örnekte `someapp.example.com` sertifikasını kullanır. + + + +Client, bu TLS sertifikasını üreten kuruluşa zaten **güvenir** (bu örnekte Let's Encrypt; birazdan ona da geleceğiz). Bu sayede sertifikanın geçerli olduğunu **doğrulayabilir**. + +Ardından client ve TLS Termination Proxy, sertifikayı kullanarak **TCP iletişiminin geri kalanını nasıl şifreleyeceklerine** karar verir. Böylece **TLS Handshake** kısmı tamamlanır. + +Bundan sonra client ve server arasında **şifreli bir TCP bağlantısı** vardır; TLS’in sağladığı şey budur. Sonra bu bağlantıyı kullanarak gerçek **HTTP iletişimini** başlatabilirler. + +Ve **HTTPS** de tam olarak budur: şifrelenmemiş bir TCP bağlantısı yerine, **güvenli bir TLS bağlantısının içinde** düz **HTTP**’dir. + +/// tip | İpucu + +Şifrelemenin HTTP seviyesinde değil, **TCP seviyesinde** gerçekleştiğine dikkat edin. + +/// + +### HTTPS Request { #https-request } + +Artık client ile server (özellikle tarayıcı ile TLS Termination Proxy) arasında **şifreli bir TCP bağlantısı** olduğuna göre, **HTTP iletişimi** başlayabilir. + +Dolayısıyla client bir **HTTPS request** gönderir. Bu, şifreli bir TLS bağlantısı üzerinden giden bir HTTP request’tir. + + + +### Request’in Şifresini Çözme { #decrypt-the-request } + +TLS Termination Proxy, üzerinde anlaşılan şifrelemeyi kullanarak **request’in şifresini çözer** ve **düz (şifresi çözülmüş) HTTP request**’i uygulamayı çalıştıran process’e iletir (ör. FastAPI uygulamasını çalıştıran Uvicorn process’i). + + + +### HTTP Response { #http-response } + +Uygulama request’i işler ve TLS Termination Proxy’ye **düz (şifrelenmemiş) bir HTTP response** gönderir. + + + +### HTTPS Response { #https-response } + +TLS Termination Proxy daha sonra response’u, daha önce üzerinde anlaşılan kriptografi ile (başlangıcı `someapp.example.com` sertifikasına dayanan) **şifreler** ve tarayıcıya geri gönderir. + +Sonrasında tarayıcı response’un geçerli olduğunu ve doğru kriptografik anahtarla şifrelendiğini doğrular vb. Ardından **response’un şifresini çözer** ve işler. + + + +Client (tarayıcı), response’un doğru server’dan geldiğini bilir; çünkü daha önce **HTTPS sertifikası** ile üzerinde anlaştıkları kriptografiyi kullanmaktadır. + +### Birden Fazla Uygulama { #multiple-applications } + +Aynı server’da (veya server’larda) örneğin başka API programları ya da bir veritabanı gibi **birden fazla uygulama** olabilir. + +Belirli IP ve port kombinasyonunu yalnızca bir process yönetebilir (örneğimizde TLS Termination Proxy). Ancak diğer uygulamalar/process’ler, aynı **public IP + port kombinasyonunu** kullanmaya çalışmadıkları sürece server(lar)da çalışabilir. + + + +Bu şekilde TLS Termination Proxy, birden fazla uygulama için **birden fazla domain**’in HTTPS ve sertifika işlerini yönetebilir ve her durumda request’leri doğru uygulamaya iletebilir. + +### Sertifika Yenileme { #certificate-renewal } + +Gelecekte bir noktada, her sertifikanın süresi **dolar** (temin edildikten yaklaşık 3 ay sonra). + +Ardından başka bir program (bazı durumlarda ayrı bir programdır, bazı durumlarda aynı TLS Termination Proxy olabilir) Let's Encrypt ile konuşup sertifika(ları) yeniler. + + + +**TLS sertifikaları** bir IP adresiyle değil, **domain adıyla ilişkilidir**. + +Bu yüzden sertifikaları yenilemek için, yenileme programı otoriteye (Let's Encrypt) gerçekten o domain’i **"sahiplendiğini" ve kontrol ettiğini** **kanıtlamalıdır**. + +Bunu yapmak ve farklı uygulama ihtiyaçlarını karşılamak için birden fazla yöntem vardır. Yaygın yöntemlerden bazıları: + +* Bazı **DNS kayıtlarını değiştirmek**. + * Bunun için yenileme programının DNS provider API’lerini desteklemesi gerekir. Dolayısıyla kullandığınız DNS provider’a bağlı olarak bu seçenek mümkün de olabilir, olmayabilir de. +* Domain ile ilişkili public IP adresinde **server olarak çalışmak** (en azından sertifika temin sürecinde). + * Yukarıda söylediğimiz gibi, belirli bir IP ve portu yalnızca bir process dinleyebilir. + * Bu, aynı TLS Termination Proxy’nin sertifika yenileme sürecini de yönetmesinin neden çok faydalı olduğunun sebeplerinden biridir. + * Aksi halde TLS Termination Proxy’yi kısa süreliğine durdurmanız, sertifikaları temin etmek için yenileme programını başlatmanız, sonra bunları TLS Termination Proxy ile yapılandırmanız ve ardından TLS Termination Proxy’yi tekrar başlatmanız gerekebilir. Bu ideal değildir; çünkü TLS Termination Proxy kapalıyken uygulama(lar)ınıza erişilemez. + +Uygulamayı servis etmeye devam ederken tüm bu yenileme sürecini yönetebilmek, TLS sertifikalarını doğrudan uygulama server’ıyla (örn. Uvicorn) kullanmak yerine, TLS Termination Proxy ile HTTPS’i yönetecek **ayrı bir sistem** istemenizin başlıca nedenlerinden biridir. + +## Proxy Forwarded Headers { #proxy-forwarded-headers } + +HTTPS’i bir proxy ile yönetirken, **application server**’ınız (örneğin FastAPI CLI üzerinden Uvicorn) HTTPS süreci hakkında hiçbir şey bilmez; **TLS Termination Proxy** ile düz HTTP üzerinden iletişim kurar. + +Bu **proxy** normalde request’i **application server**’a iletmeden önce, request’in proxy tarafından **forward** edildiğini application server’a bildirmek için bazı HTTP header’larını anlık olarak ekler. + +/// note | Teknik Detaylar + +Proxy header’ları şunlardır: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +Buna rağmen **application server**, güvenilen bir **proxy** arkasında olduğunu bilmediği için varsayılan olarak bu header’lara güvenmez. + +Ancak **application server**’ı, **proxy**’nin gönderdiği *forwarded* header’larına güvenecek şekilde yapılandırabilirsiniz. FastAPI CLI kullanıyorsanız, hangi IP’lerden gelen *forwarded* header’lara güvenmesi gerektiğini söylemek için *CLI Option* `--forwarded-allow-ips` seçeneğini kullanabilirsiniz. + +Örneğin **application server** yalnızca güvenilen **proxy**’den iletişim alıyorsa, yalnızca **proxy**’nin kullandığı IP’den request alacağı için `--forwarded-allow-ips="*"` ayarlayıp gelen tüm IP’lere güvenmesini sağlayabilirsiniz. + +Bu sayede uygulama kendi public URL’inin ne olduğunu, HTTPS kullanıp kullanmadığını, domain’i vb. bilebilir. + +Bu, örneğin redirect’leri doğru şekilde yönetmek için faydalıdır. + +/// tip | İpucu + +Bununla ilgili daha fazlasını [Behind a Proxy - Enable Proxy Forwarded Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} dokümantasyonunda öğrenebilirsiniz. + +/// + +## Özet { #recap } + +**HTTPS** kullanmak çok önemlidir ve çoğu durumda oldukça **kritiktir**. Geliştirici olarak HTTPS etrafında harcadığınız çabanın büyük kısmı, aslında **bu kavramları** ve nasıl çalıştıklarını **anlamaktır**. + +Ancak **geliştiriciler için HTTPS**’in temel bilgilerini öğrendikten sonra, her şeyi basitçe yönetmek için farklı araçları kolayca birleştirip yapılandırabilirsiniz. + +Sonraki bölümlerin bazılarında, **FastAPI** uygulamaları için **HTTPS**’i nasıl kuracağınıza dair birkaç somut örnek göstereceğim. 🔒 diff --git a/docs/tr/docs/deployment/manually.md b/docs/tr/docs/deployment/manually.md new file mode 100644 index 000000000..561ba8677 --- /dev/null +++ b/docs/tr/docs/deployment/manually.md @@ -0,0 +1,157 @@ +# Bir Sunucuyu Manuel Olarak Çalıştırın { #run-a-server-manually } + +## `fastapi run` Komutunu Kullanın { #use-the-fastapi-run-command } + +Kısacası, FastAPI uygulamanızı sunmak için `fastapi run` kullanın: + +
+ +```console +$ fastapi run main.py + + 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 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) +``` + +
+ +Bu, çoğu durumda işinizi görür. 😎 + +Örneğin bu komutu, **FastAPI** app'inizi bir container içinde, bir sunucuda vb. başlatmak için kullanabilirsiniz. + +## ASGI Sunucuları { #asgi-servers } + +Şimdi biraz daha detaya inelim. + +FastAPI, Python web framework'leri ve sunucularını inşa etmek için kullanılan ASGI adlı bir standardı kullanır. FastAPI bir ASGI web framework'üdür. + +Uzak bir sunucu makinesinde **FastAPI** uygulamasını (veya herhangi bir ASGI uygulamasını) çalıştırmak için gereken ana şey, **Uvicorn** gibi bir ASGI server programıdır. `fastapi` komutuyla varsayılan olarak gelen de budur. + +Buna alternatif birkaç seçenek daha vardır, örneğin: + +* Uvicorn: yüksek performanslı bir ASGI server. +* Hypercorn: diğer özelliklerin yanında HTTP/2 ve Trio ile uyumlu bir ASGI server. +* Daphne: Django Channels için geliştirilmiş ASGI server. +* Granian: Python uygulamaları için bir Rust HTTP server. +* NGINX Unit: NGINX Unit, hafif ve çok yönlü bir web uygulaması runtime'ıdır. + +## Sunucu Makinesi ve Sunucu Programı { #server-machine-and-server-program } + +İsimlendirme konusunda akılda tutulması gereken küçük bir detay var. 💡 + +"**server**" kelimesi yaygın olarak hem uzak/bulut bilgisayarı (fiziksel veya sanal makine) hem de o makinede çalışan programı (ör. Uvicorn) ifade etmek için kullanılır. + +Dolayısıyla genel olarak "server" dendiğinde, bu iki şeyden birini kast ediyor olabilir. + +Uzak makineden bahsederken genelde **server** denir; ayrıca **machine**, **VM** (virtual machine), **node** ifadeleri de kullanılır. Bunların hepsi, genellikle Linux çalıştıran ve üzerinde programlarınızı çalıştırdığınız bir tür uzak makineyi ifade eder. + +## Sunucu Programını Yükleyin { #install-the-server-program } + +FastAPI'yi kurduğunuzda, production sunucusu olarak Uvicorn da beraberinde gelir ve bunu `fastapi run` komutuyla başlatabilirsiniz. + +Ancak bir ASGI server'ı manuel olarak da kurabilirsiniz. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, etkinleştirdiğinizden emin olun; ardından server uygulamasını kurabilirsiniz. + +Örneğin Uvicorn'u kurmak için: + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +Benzer bir süreç, diğer ASGI server programlarının tamamı için de geçerlidir. + +/// tip | İpucu + +`standard` eklediğinizde Uvicorn, önerilen bazı ek bağımlılıkları kurar ve kullanır. + +Bunlara, `asyncio` için yüksek performanslı bir drop-in replacement olan ve concurrency performansını ciddi şekilde artıran `uvloop` da dahildir. + +FastAPI'yi `pip install "fastapi[standard]"` gibi bir şekilde kurduğunuzda `uvicorn[standard]` da zaten kurulmuş olur. + +/// + +## Sunucu Programını Çalıştırın { #run-the-server-program } + +Bir ASGI server'ı manuel olarak kurduysanız, FastAPI uygulamanızı import edebilmesi için genellikle özel bir formatta bir import string geçirmeniz gerekir: + +
+ +```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) +``` + +
+ +/// note | Not + +`uvicorn main:app` komutu şunları ifade eder: + +* `main`: `main.py` dosyası (Python "module"). +* `app`: `main.py` içinde `app = FastAPI()` satırıyla oluşturulan nesne. + +Şununla eşdeğerdir: + +```Python +from main import app +``` + +/// + +Her alternatif ASGI server programı için benzer bir komut bulunur; daha fazlası için ilgili dokümantasyonlarına bakabilirsiniz. + +/// warning | Uyarı + +Uvicorn ve diğer sunucular, geliştirme sırasında faydalı olan `--reload` seçeneğini destekler. + +`--reload` seçeneği çok daha fazla kaynak tüketir, daha kararsızdır vb. + +**Geliştirme** sırasında çok yardımcı olur, ancak **production** ortamında kullanmamalısınız. + +/// + +## Deployment Kavramları { #deployment-concepts } + +Bu örnekler server programını (ör. Uvicorn) çalıştırır; **tek bir process** başlatır, tüm IP'lerde (`0.0.0.0`) ve önceden belirlenmiş bir port'ta (ör. `80`) dinler. + +Temel fikir budur. Ancak muhtemelen şunlar gibi bazı ek konularla da ilgilenmek isteyeceksiniz: + +* Güvenlik - HTTPS +* Açılışta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Sonraki bölümlerde bu kavramların her birini nasıl düşünmeniz gerektiğini ve bunlarla başa çıkmak için kullanabileceğiniz somut örnekleri/stratejileri anlatacağım. 🚀 diff --git a/docs/tr/docs/deployment/server-workers.md b/docs/tr/docs/deployment/server-workers.md new file mode 100644 index 000000000..faae4ef92 --- /dev/null +++ b/docs/tr/docs/deployment/server-workers.md @@ -0,0 +1,139 @@ +# Server Workers - Worker'larla Uvicorn { #server-workers-uvicorn-with-workers } + +Önceki bölümlerde bahsettiğimiz deployment kavramlarına tekrar bakalım: + +* Güvenlik - HTTPS +* Başlangıçta çalıştırma +* Yeniden başlatmalar +* **Replikasyon (çalışan process sayısı)** +* Bellek +* Başlatmadan önceki adımlar + +Bu noktaya kadar, dokümantasyondaki tüm tutorial'larla muhtemelen bir **server programı** çalıştırıyordunuz; örneğin Uvicorn'u çalıştıran `fastapi` komutunu kullanarak ve **tek bir process** ile. + +Uygulamaları deploy ederken, **çok çekirdekten (multiple cores)** faydalanmak ve daha fazla request'i karşılayabilmek için büyük olasılıkla **process replikasyonu** (birden fazla process) isteyeceksiniz. + +[Daha önceki Deployment Concepts](concepts.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, kullanabileceğiniz birden fazla strateji var. + +Burada, `fastapi` komutunu kullanarak ya da `uvicorn` komutunu doğrudan çalıştırarak **worker process**'lerle **Uvicorn**'u nasıl kullanacağınızı göstereceğim. + +/// info | Bilgi + +Container kullanıyorsanız (örneğin Docker veya Kubernetes ile), bununla ilgili daha fazlasını bir sonraki bölümde anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +Özellikle **Kubernetes** üzerinde çalıştırırken, büyük olasılıkla worker kullanmak **istemeyeceksiniz**; bunun yerine **container başına tek bir Uvicorn process** çalıştırmak daha uygundur. Ancak bunu da o bölümde detaylandıracağım. + +/// + +## Birden Fazla Worker { #multiple-workers } + +Komut satırında `--workers` seçeneğiyle birden fazla worker başlatabilirsiniz: + +//// tab | `fastapi` + +`fastapi` komutunu kullanıyorsanız: + +
+ +```console +$ fastapi run --workers 4 main.py + + 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. +``` + +
+ +//// + +//// tab | `uvicorn` + +`uvicorn` komutunu doğrudan kullanmayı tercih ederseniz: + +
+ +```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. +``` + +
+ +//// + +Buradaki tek yeni seçenek `--workers`; bu seçenek Uvicorn'a 4 adet worker process başlatmasını söyler. + +Ayrıca her process'in **PID**'inin gösterildiğini de görebilirsiniz: parent process için `27365` (bu **process manager**), her worker process için de bir PID: `27368`, `27369`, `27370` ve `27367`. + +## Deployment Kavramları { #deployment-concepts } + +Burada, uygulamanın çalışmasını **paralelleştirmek**, CPU'daki **çok çekirdekten** yararlanmak ve **daha fazla request** karşılayabilmek için birden fazla **worker**'ı nasıl kullanacağınızı gördünüz. + +Yukarıdaki deployment kavramları listesinden, worker kullanımı ağırlıklı olarak **replikasyon** kısmına yardımcı olur, ayrıca **yeniden başlatmalar** konusunda da az da olsa katkı sağlar. Ancak diğerlerini yine sizin yönetmeniz gerekir: + +* **Güvenlik - HTTPS** +* **Başlangıçta çalıştırma** +* ***Yeniden başlatmalar*** +* Replikasyon (çalışan process sayısı) +* **Bellek** +* **Başlatmadan önceki adımlar** + +## Container'lar ve Docker { #containers-and-docker } + +Bir sonraki bölümde, [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank} üzerinden diğer **deployment kavramlarını** ele almak için kullanabileceğiniz bazı stratejileri anlatacağım. + +Tek bir Uvicorn process çalıştıracak şekilde **sıfırdan kendi image'ınızı oluşturmayı** göstereceğim. Bu oldukça basit bir süreçtir ve **Kubernetes** gibi dağıtık bir container yönetim sistemi kullanırken büyük olasılıkla yapmak isteyeceğiniz şey de budur. + +## Özet { #recap } + +**Çok çekirdekli CPU**'lardan faydalanmak ve **birden fazla process'i paralel** çalıştırmak için `fastapi` veya `uvicorn` komutlarıyla `--workers` CLI seçeneğini kullanarak birden fazla worker process çalıştırabilirsiniz. + +Diğer deployment kavramlarını da kendiniz ele alarak **kendi deployment sisteminizi** kuruyorsanız, bu araçları ve fikirleri kullanabilirsiniz. + +Container'larla (örn. Docker ve Kubernetes) **FastAPI**'yi öğrenmek için bir sonraki bölüme göz atın. Bu araçların, diğer **deployment kavramlarını** çözmek için de basit yöntemleri olduğunu göreceksiniz. ✨ diff --git a/docs/tr/docs/deployment/versions.md b/docs/tr/docs/deployment/versions.md new file mode 100644 index 000000000..c3fb5d9bd --- /dev/null +++ b/docs/tr/docs/deployment/versions.md @@ -0,0 +1,93 @@ +# FastAPI Sürümleri Hakkında { #about-fastapi-versions } + +**FastAPI** hâlihazırda birçok uygulama ve sistemde production ortamında kullanılmaktadır. Ayrıca test kapsamı %100 seviyesinde tutulmaktadır. Ancak geliştirme süreci hâlâ hızlı şekilde ilerlemektedir. + +Yeni özellikler sık sık eklenir, bug'lar düzenli olarak düzeltilir ve kod sürekli iyileştirilmektedir. + +Bu yüzden mevcut sürümler hâlâ `0.x.x` şeklindedir; bu da her sürümde breaking change olma ihtimalini yansıtır. Bu yaklaşım Semantic Versioning kurallarını takip eder. + +Şu anda **FastAPI** ile production uygulamaları geliştirebilirsiniz (muhtemelen bir süredir yapıyorsunuz da); sadece kodunuzun geri kalanıyla doğru çalışan bir sürüm kullandığınızdan emin olmanız gerekir. + +## `fastapi` sürümünü sabitleyin { #pin-your-fastapi-version } + +İlk yapmanız gereken, kullandığınız **FastAPI** sürümünü uygulamanızla doğru çalıştığını bildiğiniz belirli bir güncel sürüme "sabitlemek" (pinlemek) olmalı. + +Örneğin, uygulamanızda `0.112.0` sürümünü kullandığınızı varsayalım. + +`requirements.txt` dosyası kullanıyorsanız sürümü şöyle belirtebilirsiniz: + +```txt +fastapi[standard]==0.112.0 +``` + +Bu, tam olarak `0.112.0` sürümünü kullanacağınız anlamına gelir. + +Ya da şu şekilde de sabitleyebilirsiniz: + +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` + +Bu da `0.112.0` ve üzeri, ama `0.113.0` altındaki sürümleri kullanacağınız anlamına gelir; örneğin `0.112.2` gibi bir sürüm de kabul edilir. + +Kurulumları yönetmek için `uv`, Poetry, Pipenv gibi başka bir araç (veya benzerleri) kullanıyorsanız, bunların hepsinde paketler için belirli sürümler tanımlamanın bir yolu vardır. + +## Mevcut sürümler { #available-versions } + +Mevcut sürümleri (ör. en güncel son sürümün hangisi olduğunu kontrol etmek için) [Release Notes](../release-notes.md){.internal-link target=_blank} sayfasında görebilirsiniz. + +## Sürümler Hakkında { #about-versions } + +Semantic Versioning kurallarına göre, `1.0.0` altındaki herhangi bir sürüm breaking change içerebilir. + +FastAPI ayrıca "PATCH" sürüm değişikliklerinin bug fix'ler ve breaking olmayan değişiklikler için kullanılması kuralını da takip eder. + +/// tip | İpucu + +"PATCH" son sayıdır. Örneğin `0.2.3` içinde PATCH sürümü `3`'tür. + +/// + +Dolayısıyla şu şekilde bir sürüme sabitleyebilmelisiniz: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +Breaking change'ler ve yeni özellikler "MINOR" sürümlerde eklenir. + +/// tip | İpucu + +"MINOR" ortadaki sayıdır. Örneğin `0.2.3` içinde MINOR sürümü `2`'dir. + +/// + +## FastAPI Sürümlerini Yükseltme { #upgrading-the-fastapi-versions } + +Uygulamanız için test'ler eklemelisiniz. + +**FastAPI** ile bu çok kolaydır (Starlette sayesinde). Dokümantasyona bakın: [Testing](../tutorial/testing.md){.internal-link target=_blank} + +Test'leriniz olduktan sonra **FastAPI** sürümünü daha yeni bir sürüme yükseltebilir ve test'lerinizi çalıştırarak tüm kodunuzun doğru çalıştığından emin olabilirsiniz. + +Her şey çalışıyorsa (ya da gerekli değişiklikleri yaptıktan sonra) ve tüm test'leriniz geçiyorsa, `fastapi` sürümünü o yeni sürüme sabitleyebilirsiniz. + +## Starlette Hakkında { #about-starlette } + +`starlette` sürümünü sabitlememelisiniz. + +**FastAPI**'nin farklı sürümleri, Starlette'in belirli (daha yeni) bir sürümünü kullanır. + +Bu yüzden **FastAPI**'nin doğru Starlette sürümünü kullanmasına izin verebilirsiniz. + +## Pydantic Hakkında { #about-pydantic } + +Pydantic, **FastAPI** için olan test'leri kendi test'lerinin içine dahil eder; bu yüzden Pydantic'in yeni sürümleri (`1.0.0` üzeri) her zaman FastAPI ile uyumludur. + +Pydantic'i sizin için çalışan `1.0.0` üzerindeki herhangi bir sürüme sabitleyebilirsiniz. + +Örneğin: + +```txt +pydantic>=2.7.0,<3.0.0 +``` diff --git a/docs/tr/docs/environment-variables.md b/docs/tr/docs/environment-variables.md new file mode 100644 index 000000000..e4f769a16 --- /dev/null +++ b/docs/tr/docs/environment-variables.md @@ -0,0 +1,298 @@ +# Ortam Değişkenleri { #environment-variables } + +/// tip | İpucu + +"Ortam değişkenleri"nin ne olduğunu ve nasıl kullanılacağını zaten biliyorsanız, bu bölümü atlayabilirsiniz. + +/// + +Ortam değişkeni (genelde "**env var**" olarak da anılır), Python kodunun **dışında**, **işletim sistemi** seviyesinde bulunan ve Python kodunuz (veya diğer programlar) tarafından okunabilen bir değişkendir. + +Ortam değişkenleri; uygulama **ayarları**nı yönetmek, Python’un **kurulumu**nun bir parçası olarak konfigürasyon yapmak vb. durumlarda işe yarar. + +## Env Var Oluşturma ve Kullanma { #create-and-use-env-vars } + +Python’a ihtiyaç duymadan, **shell (terminal)** içinde ortam değişkenleri **oluşturabilir** ve kullanabilirsiniz: + +//// tab | Linux, macOS, Windows Bash + +
+ +```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 | Windows PowerShell + +
+ +```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 +``` + +
+ +//// + +## Python’da env var Okuma { #read-env-vars-in-python } + +Ortam değişkenlerini Python’un **dışında** (terminalde veya başka bir yöntemle) oluşturup daha sonra **Python’da okuyabilirsiniz**. + +Örneğin `main.py` adında bir dosyanız şöyle olabilir: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip | İpucu + +`os.getenv()` fonksiyonunun ikinci argümanı, bulunamadığında döndürülecek varsayılan (default) değerdir. + +Verilmezse varsayılan olarak `None` olur; burada varsayılan değer olarak `"World"` verdik. + +/// + +Sonrasında bu Python programını çalıştırabilirsiniz: + +//// tab | Linux, macOS, Windows Bash + +
+ +```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 +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```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 +$ $Env: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 +``` + +
+ +//// + +Ortam değişkenleri kodun dışında ayarlanabildiği, ama kod tarafından okunabildiği ve dosyalarla birlikte saklanmasının (ör. `git`’e commit edilmesinin) gerekmediği için, konfigürasyon veya **ayarlar** için sıkça kullanılır. + +Ayrıca, bir ortam değişkenini yalnızca **belirli bir program çalıştırımı** için oluşturabilirsiniz; bu değişken sadece o program tarafından, sadece o çalıştırma süresince kullanılabilir. + +Bunu yapmak için, program komutunun hemen öncesinde ve aynı satırda tanımlayın: + +
+ +```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 | İpucu + +Bu konuyla ilgili daha fazlasını The Twelve-Factor App: Config bölümünde okuyabilirsiniz. + +/// + +## Tipler ve Doğrulama { #types-and-validation } + +Bu ortam değişkenleri yalnızca **metin string**’lerini taşıyabilir. Çünkü Python’un dışındadırlar ve diğer programlarla, sistemin geri kalanıyla (hatta Linux, Windows, macOS gibi farklı işletim sistemleriyle) uyumlu olmak zorundadırlar. + +Bu, Python’da bir ortam değişkeninden okunan **her değerin `str` olacağı** anlamına gelir. Farklı bir tipe dönüştürme veya doğrulama işlemleri kod içinde yapılmalıdır. + +Uygulama **ayarları**nı yönetmek için ortam değişkenlerini kullanmayı, [İleri Seviye Kullanıcı Rehberi - Ayarlar ve Ortam Değişkenleri](./advanced/settings.md){.internal-link target=_blank} bölümünde daha detaylı öğreneceksiniz. + +## `PATH` Ortam Değişkeni { #path-environment-variable } + +İşletim sistemlerinin (Linux, macOS, Windows) çalıştırılacak programları bulmak için kullandığı **özel** bir ortam değişkeni vardır: **`PATH`**. + +`PATH` değişkeninin değeri uzun bir string’dir; Linux ve macOS’te dizinler iki nokta üst üste `:` ile, Windows’ta ise noktalı virgül `;` ile ayrılır. + +Örneğin `PATH` ortam değişkeni şöyle görünebilir: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin şu dizinlerde program araması gerektiği anlamına gelir: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +Bu, sistemin şu dizinlerde program araması gerektiği anlamına gelir: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +Terminalde bir **komut** yazdığınızda, işletim sistemi `PATH` ortam değişkeninde listelenen **bu dizinlerin her birinde** programı **arar**. + +Örneğin terminalde `python` yazdığınızda, işletim sistemi bu listedeki **ilk dizinde** `python` adlı bir program arar. + +Bulursa **onu kullanır**. Bulamazsa **diğer dizinlerde** aramaya devam eder. + +### Python Kurulumu ve `PATH`’in Güncellenmesi { #installing-python-and-updating-the-path } + +Python’u kurarken, `PATH` ortam değişkenini güncellemek isteyip istemediğiniz sorulabilir. + +//// tab | Linux, macOS + +Diyelim ki Python’u kurdunuz ve `/opt/custompython/bin` dizinine yüklendi. + +`PATH` ortam değişkenini güncellemeyi seçerseniz, kurulum aracı `/opt/custompython/bin` yolunu `PATH` ortam değişkenine ekler. + +Şöyle görünebilir: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +Böylece terminalde `python` yazdığınızda, sistem `/opt/custompython/bin` (son dizin) içindeki Python programını bulur ve onu kullanır. + +//// + +//// tab | Windows + +Diyelim ki Python’u kurdunuz ve `C:\opt\custompython\bin` dizinine yüklendi. + +`PATH` ortam değişkenini güncellemeyi seçerseniz, kurulum aracı `C:\opt\custompython\bin` yolunu `PATH` ortam değişkenine ekler. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +Böylece terminalde `python` yazdığınızda, sistem `C:\opt\custompython\bin` (son dizin) içindeki Python programını bulur ve onu kullanır. + +//// + +Yani şunu yazarsanız: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +Sistem `python` programını `/opt/custompython/bin` içinde **bulur** ve çalıştırır. + +Bu, kabaca şunu yazmaya denktir: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +Sistem `python` programını `C:\opt\custompython\bin\python` içinde **bulur** ve çalıştırır. + +Bu, kabaca şunu yazmaya denktir: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +Bu bilgiler, [Virtual Environments](virtual-environments.md){.internal-link target=_blank} konusunu öğrenirken işinize yarayacak. + +## Sonuç { #conclusion } + +Buraya kadar **ortam değişkenleri**nin ne olduğuna ve Python’da nasıl kullanılacağına dair temel bir fikir edinmiş olmalısınız. + +Ayrıca Wikipedia for Environment Variable sayfasından daha fazlasını da okuyabilirsiniz. + +Çoğu zaman ortam değişkenlerinin hemen nasıl işe yarayacağı ilk bakışta çok net olmayabilir. Ancak geliştirme yaparken birçok farklı senaryoda tekrar tekrar karşınıza çıkarlar; bu yüzden bunları bilmek faydalıdır. + +Örneğin bir sonraki bölümde, [Virtual Environments](virtual-environments.md) konusunda bu bilgilere ihtiyaç duyacaksınız. diff --git a/docs/tr/docs/fastapi-cli.md b/docs/tr/docs/fastapi-cli.md new file mode 100644 index 000000000..4680d4bb6 --- /dev/null +++ b/docs/tr/docs/fastapi-cli.md @@ -0,0 +1,75 @@ +# FastAPI CLI { #fastapi-cli } + +**FastAPI CLI**, FastAPI uygulamanızı servis etmek, FastAPI projenizi yönetmek ve daha fazlası için kullanabileceğiniz bir komut satırı programıdır. + +FastAPI'yi kurduğunuzda (ör. `pip install "fastapi[standard]"`), beraberinde `fastapi-cli` adlı bir paket de gelir; bu paket terminalde `fastapi` komutunu sağlar. + +FastAPI uygulamanızı geliştirme için çalıştırmak üzere `fastapi dev` komutunu kullanabilirsiniz: + +
+ +```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. +``` + +
+ +`fastapi` adlı bu komut satırı programı, **FastAPI CLI**'dır. + +FastAPI CLI, Python programınızın path'ini (ör. `main.py`) alır; `FastAPI` instance'ını (genellikle `app` olarak adlandırılır) otomatik olarak tespit eder, doğru import sürecini belirler ve ardından uygulamayı servis eder. + +Production için bunun yerine `fastapi run` kullanırsınız. 🚀 + +İçeride **FastAPI CLI**, yüksek performanslı, production'a hazır bir ASGI server olan Uvicorn'u kullanır. 😎 + +## `fastapi dev` { #fastapi-dev } + +`fastapi dev` çalıştırmak, geliştirme modunu başlatır. + +Varsayılan olarak **auto-reload** etkindir; kodunuzda değişiklik yaptığınızda server'ı otomatik olarak yeniden yükler. Bu, kaynak tüketimi yüksek bir özelliktir ve kapalı olduğuna kıyasla daha az stabil olabilir. Sadece geliştirme sırasında kullanmalısınız. Ayrıca yalnızca `127.0.0.1` IP adresini dinler; bu, makinenizin sadece kendisiyle iletişim kurması için kullanılan IP'dir (`localhost`). + +## `fastapi run` { #fastapi-run } + +`fastapi run` çalıştırmak, varsayılan olarak FastAPI'yi production modunda başlatır. + +Varsayılan olarak **auto-reload** kapalıdır. Ayrıca `0.0.0.0` IP adresini dinler; bu, kullanılabilir tüm IP adresleri anlamına gelir. Böylece makineyle iletişim kurabilen herkes tarafından genel erişime açık olur. Bu, normalde production'da çalıştırma şeklidir; örneğin bir container içinde. + +Çoğu durumda (ve genellikle yapmanız gereken şekilde) üst tarafta sizin yerinize HTTPS'i yöneten bir "termination proxy" bulunur. Bu, uygulamanızı nasıl deploy ettiğinize bağlıdır; sağlayıcınız bunu sizin için yapabilir ya da sizin ayrıca kurmanız gerekebilir. + +/// tip | İpucu + +Bununla ilgili daha fazla bilgiyi [deployment dokümantasyonunda](deployment/index.md){.internal-link target=_blank} bulabilirsiniz. + +/// diff --git a/docs/tr/docs/help-fastapi.md b/docs/tr/docs/help-fastapi.md new file mode 100644 index 000000000..785c0ae0d --- /dev/null +++ b/docs/tr/docs/help-fastapi.md @@ -0,0 +1,256 @@ +# FastAPI'ye Yardım Et - Yardım Al { #help-fastapi-get-help } + +**FastAPI**'yi seviyor musunuz? + +FastAPI'ye, diğer kullanıcılara ve yazara yardım etmek ister misiniz? + +Yoksa **FastAPI** ile ilgili yardım mı almak istiyorsunuz? + +Yardım etmenin çok basit yolları var (bazıları sadece bir-iki tıklama gerektirir). + +Yardım almanın da birkaç yolu var. + +## Bültene abone olun { #subscribe-to-the-newsletter } + +Şunlardan haberdar olmak için (seyrek yayımlanan) [**FastAPI and friends** bültenine](newsletter.md){.internal-link target=_blank} abone olabilirsiniz: + +* FastAPI ve friends ile ilgili haberler 🚀 +* Rehberler 📝 +* Özellikler ✨ +* Geriye dönük uyumsuz değişiklikler 🚨 +* İpuçları ve püf noktaları ✅ + +## X (Twitter) üzerinden FastAPI'yi takip edin { #follow-fastapi-on-x-twitter } + +**FastAPI** ile ilgili en güncel haberleri almak için @fastapi hesabını **X (Twitter)** üzerinde takip edin. 🐦 + +## GitHub'da **FastAPI**'ye yıldız verin { #star-fastapi-in-github } + +GitHub'da FastAPI'ye "star" verebilirsiniz (sağ üstteki yıldız butonuna tıklayarak): https://github.com/fastapi/fastapi. ⭐️ + +Yıldız verince, diğer kullanıcılar projeyi daha kolay bulabilir ve başkaları için de faydalı olduğunu görebilir. + +## GitHub repository'sini release'ler için izleyin { #watch-the-github-repository-for-releases } + +GitHub'da FastAPI'yi "watch" edebilirsiniz (sağ üstteki "watch" butonuna tıklayarak): https://github.com/fastapi/fastapi. 👀 + +Orada "Releases only" seçebilirsiniz. + +Böylece **FastAPI**'nin bug fix'ler ve yeni özelliklerle gelen her yeni release'inde (yeni versiyonunda) email ile bildirim alırsınız. + +## Yazarla bağlantı kurun { #connect-with-the-author } + +Yazar olan benimle (Sebastián Ramírez / `tiangolo`) bağlantı kurabilirsiniz. + +Şunları yapabilirsiniz: + +* Beni **GitHub**'da takip edin. + * Size yardımcı olabilecek oluşturduğum diğer Open Source projelere göz atın. + * Yeni bir Open Source proje oluşturduğumda haberdar olmak için beni takip edin. +* Beni **X (Twitter)** üzerinde veya Mastodon'da takip edin. + * FastAPI'yi nasıl kullandığınızı anlatın (bunu duymayı seviyorum). + * Duyuru yaptığımda veya yeni araçlar yayınladığımda haberdar olun. + * Ayrıca (ayrı bir hesap olan) X (Twitter) üzerinde @fastapi hesabını da takip edebilirsiniz. +* Beni **LinkedIn**'de takip edin. + * Duyuru yaptığımda veya yeni araçlar yayınladığımda haberdar olun (gerçi X (Twitter)'ı daha sık kullanıyorum 🤷‍♂). +* **Dev.to** veya **Medium** üzerinde yazdıklarımı okuyun (ya da beni takip edin). + * Diğer fikirleri, yazıları ve oluşturduğum araçlarla ilgili içerikleri okuyun. + * Yeni bir şey yayınladığımda görmek için beni takip edin. + +## **FastAPI** hakkında tweet atın { #tweet-about-fastapi } + +**FastAPI** hakkında tweet atın ve neden sevdiğinizi bana ve diğerlerine söyleyin. 🎉 + +**FastAPI**'nin nasıl kullanıldığını, nelerini sevdiğinizi, hangi projede/şirkette kullandığınızı vb. duymayı seviyorum. + +## FastAPI için oy verin { #vote-for-fastapi } + +* Slant'ta **FastAPI** için oy verin. +* AlternativeTo'da **FastAPI** için oy verin. +* StackShare'de **FastAPI** kullandığınızı belirtin. + +## GitHub'da sorularla başkalarına yardım edin { #help-others-with-questions-in-github } + +Şuralarda insanların sorularına yardımcı olmayı deneyebilirsiniz: + +* GitHub Discussions +* GitHub Issues + +Birçok durumda bu soruların cevabını zaten biliyor olabilirsiniz. 🤓 + +Eğer insanların sorularına çok yardım ederseniz, resmi bir [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank} olabilirsiniz. 🎉 + +Şunu unutmayın: en önemli nokta, nazik olmaya çalışmak. İnsanlar çoğu zaman biriken stresle geliyor ve birçok durumda soruyu en iyi şekilde sormuyor; yine de elinizden geldiğince nazik olmaya çalışın. 🤗 + +Amaç, **FastAPI** topluluğunun nazik ve kapsayıcı olması. Aynı zamanda başkalarına zorbalık ya da saygısız davranışları da kabul etmeyin. Birbirimizi kollamalıyız. + +--- + +Sorularda (discussions veya issues içinde) başkalarına yardım etmek için şunları yapabilirsiniz: + +### Soruyu anlayın { #understand-the-question } + +* Soru soran kişinin **amacının** ve kullanım senaryosunun ne olduğunu anlayabiliyor musunuz, kontrol edin. + +* Sonra sorunun (büyük çoğunluğu soru olur) **net** olup olmadığına bakın. + +* Birçok durumda kullanıcı kafasında hayali bir çözüm kurup onu sorar; ancak **daha iyi** bir çözüm olabilir. Problemi ve kullanım senaryosunu daha iyi anladıysanız daha iyi bir **alternatif çözüm** önerebilirsiniz. + +* Soruyu anlayamıyorsanız daha fazla **detay** isteyin. + +### Problemi yeniden üretin { #reproduce-the-problem } + +Çoğu durumda ve çoğu soruda, kişinin **orijinal kodu** ile ilgili bir şey vardır. + +Birçok kişi sadece kodun bir parçasını kopyalar, ama bu **problemi yeniden üretmek** için yeterli olmaz. + +* Çalıştırıp aynı hatayı/davranışı görebileceğiniz veya kullanım senaryosunu daha iyi anlayabileceğiniz, yerelde **kopyala-yapıştır** yaparak çalıştırılabilen bir minimal, reproducible, example paylaşmalarını isteyebilirsiniz. + +* Çok cömert hissediyorsanız, problemi anlatan açıklamadan yola çıkarak kendiniz de böyle bir **örnek oluşturmayı** deneyebilirsiniz. Ancak bunun çok zaman alabileceğini unutmayın; çoğu zaman önce problemi netleştirmelerini istemek daha iyidir. + +### Çözüm önerin { #suggest-solutions } + +* Soruyu anlayabildikten sonra olası bir **cevap** verebilirsiniz. + +* Çoğu durumda, yapmak istediklerinden ziyade alttaki **asıl problemi veya kullanım senaryosunu** anlamak daha iyidir; çünkü denedikleri yöntemden daha iyi bir çözüm yolu olabilir. + +### Kapatılmasını isteyin { #ask-to-close } + +Eğer yanıt verirlerse, büyük ihtimalle problemi çözmüşsünüzdür, tebrikler, **kahramansınız**! 🦸 + +* Eğer çözüm işe yaradıysa şunları yapmalarını isteyebilirsiniz: + + * GitHub Discussions'ta: ilgili yorumu **answer** olarak işaretlemeleri. + * GitHub Issues'ta: issue'yu **close** etmeleri. + +## GitHub repository'sini izleyin { #watch-the-github-repository } + +GitHub'da FastAPI'yi "watch" edebilirsiniz (sağ üstteki "watch" butonuna tıklayarak): https://github.com/fastapi/fastapi. 👀 + +"Releases only" yerine "Watching" seçerseniz biri yeni bir issue veya soru oluşturduğunda bildirim alırsınız. Ayrıca sadece yeni issue'lar, ya da discussions, ya da PR'lar vb. için bildirim almak istediğinizi belirtebilirsiniz. + +Sonra da bu soruları çözmelerine yardımcı olmayı deneyebilirsiniz. + +## Soru Sorun { #ask-questions } + +GitHub repository'sinde örneğin şunlar için yeni bir soru oluşturabilirsiniz: + +* Bir **soru** sorun veya bir **problem** hakkında danışın. +* Yeni bir **feature** önerin. + +**Not**: Bunu yaparsanız, ben de sizden başkalarına yardım etmenizi isteyeceğim. 😉 + +## Pull Request'leri İnceleyin { #review-pull-requests } + +Başkalarının gönderdiği pull request'leri incelememde bana yardımcı olabilirsiniz. + +Yine, lütfen elinizden geldiğince nazik olmaya çalışın. 🤗 + +--- + +Bir pull request'i incelerken akılda tutmanız gerekenler: + +### Problemi anlayın { #understand-the-problem } + +* Önce, pull request'in çözmeye çalıştığı **problemi anladığınızdan** emin olun. GitHub Discussion veya issue içinde daha uzun bir tartışması olabilir. + +* Pull request'in aslında hiç gerekmiyor olma ihtimali de yüksektir; çünkü problem **farklı bir şekilde** çözülebilir. Bu durumda bunu önerebilir veya bununla ilgili soru sorabilirsiniz. + +### Style konusunda çok dert etmeyin { #dont-worry-about-style } + +* Commit message tarzı gibi şeyleri çok dert etmeyin; ben commit'leri manuel olarak düzenleyerek squash and merge yapacağım. + +* Style kuralları için de endişelenmeyin; bunları kontrol eden otomatik araçlar zaten var. + +Ek bir style veya tutarlılık ihtiyacı olursa, bunu doğrudan isterim ya da gerekli değişikliklerle üstüne commit eklerim. + +### Kodu kontrol edin { #check-the-code } + +* Kodu okuyup kontrol edin; mantıklı mı bakın, **yerelde çalıştırın** ve gerçekten problemi çözüyor mu görün. + +* Ardından bunu yaptığınızı belirten bir **yorum** yazın; böylece gerçekten kontrol ettiğinizi anlarım. + +/// info | Bilgi + +Ne yazık ki sadece birkaç onayı olan PR'lara körü körüne güvenemem. + +Defalarca, 3, 5 veya daha fazla onayı olan PR'lar oldu; muhtemelen açıklaması çekici olduğu için onay aldılar. Ama PR'lara baktığımda aslında bozuk olduklarını, bug içerdiğini veya iddia ettikleri problemi çözmediklerini gördüm. 😅 + +Bu yüzden kodu gerçekten okuyup çalıştırmanız ve bunu yorumlarda bana bildirmeniz çok önemli. 🤓 + +/// + +* PR bir şekilde basitleştirilebiliyorsa bunu isteyebilirsiniz. Ancak çok didik didik etmeye gerek yok; konuya göre birçok öznel bakış açısı olabilir (benim de olacaktır 🙈). Bu yüzden temel noktalara odaklanmak daha iyi. + +### Testler { #tests } + +* PR'da **testler** olduğunu kontrol etmemde bana yardımcı olun. + +* PR'dan önce testlerin **fail** ettiğini kontrol edin. 🚨 + +* PR'dan sonra testlerin **pass** ettiğini kontrol edin. ✅ + +* Birçok PR test içermez; test eklemelerini **hatırlatabilirsiniz** veya hatta kendiniz bazı testler **önerebilirsiniz**. Bu, en çok zaman alan işlerden biridir ve burada çok yardımcı olabilirsiniz. + +* Ayrıca neleri denediğinizi yorumlara yazın; böylece kontrol ettiğinizi anlarım. 🤓 + +## Pull Request Oluşturun { #create-a-pull-request } + +Örneğin şunlar için Pull Request'lerle kaynak koda [katkıda bulunabilirsiniz](contributing.md){.internal-link target=_blank}: + +* Dokümantasyonda bulduğunuz bir yazım hatasını düzeltmek. +* FastAPI hakkında oluşturduğunuz veya bulduğunuz bir makaleyi, videoyu ya da podcast'i bu dosyayı düzenleyerek paylaşmak. + * Link'inizi ilgili bölümün başına eklediğinizden emin olun. +* Dokümantasyonu kendi dilinize [çevirmeye yardımcı olmak](contributing.md#translations){.internal-link target=_blank}. + * Başkalarının yaptığı çevirileri gözden geçirmeye de yardımcı olabilirsiniz. +* Yeni dokümantasyon bölümleri önermek. +* Mevcut bir issue/bug'ı düzeltmek. + * Test eklediğinizden emin olun. +* Yeni bir feature eklemek. + * Test eklediğinizden emin olun. + * İlgiliyse dokümantasyon da eklediğinizden emin olun. + +## FastAPI'nin Bakımına Yardım Edin { #help-maintain-fastapi } + +**FastAPI**'nin bakımını yapmama yardımcı olun! 🤓 + +Yapılacak çok iş var ve bunların çoğunu **SİZ** yapabilirsiniz. + +Şu anda yapabileceğiniz ana işler: + +* [GitHub'da sorularla başkalarına yardım edin](#help-others-with-questions-in-github){.internal-link target=_blank} (yukarıdaki bölüme bakın). +* [Pull Request'leri inceleyin](#review-pull-requests){.internal-link target=_blank} (yukarıdaki bölüme bakın). + +Bu iki iş, **en çok zamanı alan** işlerdir. FastAPI bakımının ana yükü buradadır. + +Burada yardımcı olursanız, **FastAPI'nin bakımını yapmama yardım etmiş** ve daha **hızlı ve daha iyi ilerlemesini** sağlamış olursunuz. 🚀 + +## Sohbete katılın { #join-the-chat } + +FastAPI topluluğundan diğer kişilerle takılmak için 👥 Discord chat server 👥 sohbetine katılın. + +/// tip | İpucu + +Sorular için GitHub Discussions'a sorun; [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} tarafından yardım alma ihtimaliniz çok daha yüksektir. + +Chat'i sadece genel sohbetler için kullanın. + +/// + +### Sorular için chat'i kullanmayın { #dont-use-the-chat-for-questions } + +Chat sistemleri daha "serbest sohbet"e izin verdiği için, çok genel ve yanıtlaması daha zor sorular sormak kolaylaşır; bu nedenle cevap alamayabilirsiniz. + +GitHub'da ise şablon (template) doğru soruyu yazmanız için sizi yönlendirir; böylece daha kolay iyi bir cevap alabilir, hatta bazen sormadan önce problemi kendiniz çözebilirsiniz. Ayrıca GitHub'da (zaman alsa bile) her şeye mutlaka cevap verdiğimden emin olabilirim. Chat sistemlerinde bunu kişisel olarak yapamam. 😅 + +Chat sistemlerindeki konuşmalar GitHub kadar kolay aranabilir değildir; bu yüzden soru ve cevaplar sohbet içinde kaybolabilir. Ayrıca [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank} olmak için sadece GitHub'daki katkılar sayılır; dolayısıyla büyük olasılıkla GitHub'da daha fazla ilgi görürsünüz. + +Öte yandan chat sistemlerinde binlerce kullanıcı vardır; bu yüzden neredeyse her zaman konuşacak birini bulma ihtimaliniz yüksektir. 😄 + +## Yazara sponsor olun { #sponsor-the-author } + +Eğer **ürününüz/şirketiniz** **FastAPI**'ye bağlıysa veya onunla ilişkiliyse ve FastAPI kullanıcılarına ulaşmak istiyorsanız, GitHub sponsors üzerinden yazara (bana) sponsor olabilirsiniz. Tier'a göre dokümantasyonda bir rozet gibi ek faydalar elde edebilirsiniz. 🎁 + +--- + +Teşekkürler! 🚀 diff --git a/docs/tr/docs/history-design-future.md b/docs/tr/docs/history-design-future.md index cad290828..764a51957 100644 --- a/docs/tr/docs/history-design-future.md +++ b/docs/tr/docs/history-design-future.md @@ -1,4 +1,4 @@ -# Geçmişi, Tasarımı ve Geleceği +# Geçmişi, Tasarımı ve Geleceği { #history-design-and-future } Bir süre önce, bir **FastAPI** kullanıcısı sordu: @@ -6,7 +6,7 @@ Bir süre önce, **Pydantic**'i kullanmaya karar verdim. @@ -60,11 +60,11 @@ Sonra, JSON Schema ile tamamen uyumlu olmasını sağlamak, kısıtlama bildirim Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. -## Geliştirme +## Geliştirme { #development } **FastAPI**'ı oluşturmaya başladığımda, parçaların çoğu zaten yerindeydi, tasarım tanımlanmıştı, gereksinimler ve araçlar hazırdı, standartlar ve tanımlamalar hakkındaki bilgi net ve tazeydi. -## Gelecek +## Gelecek { #future } Şimdiye kadar, **FastAPI**'ın fikirleriyle birçok kişiye faydalı olduğu apaçık ortada. diff --git a/docs/tr/docs/how-to/authentication-error-status-code.md b/docs/tr/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..579673624 --- /dev/null +++ b/docs/tr/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Eski 403 Kimlik Doğrulama Hata Durum Kodlarını Kullanma { #use-old-403-authentication-error-status-codes } + +FastAPI `0.122.0` sürümünden önce, entegre security yardımcı araçları başarısız bir kimlik doğrulama (authentication) sonrasında client'a bir hata döndüğünde HTTP durum kodu olarak `403 Forbidden` kullanıyordu. + +FastAPI `0.122.0` sürümünden itibaren ise daha uygun olan HTTP durum kodu `401 Unauthorized` kullanılmakta ve HTTP spesifikasyonlarına uygun olarak response içinde anlamlı bir `WWW-Authenticate` header'ı döndürülmektedir: RFC 7235, RFC 9110. + +Ancak herhangi bir nedenle client'larınız eski davranışa bağlıysa, security class'larınızda `make_not_authenticated_error` metodunu override ederek eski davranışa geri dönebilirsiniz. + +Örneğin, varsayılan `401 Unauthorized` hatası yerine `403 Forbidden` hatası döndüren bir `HTTPBearer` alt sınıfı oluşturabilirsiniz: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | İpucu + +Fonksiyonun exception instance'ını döndürdüğüne dikkat edin; exception'ı raise etmiyor. Raise işlemi internal kodun geri kalan kısmında yapılıyor. + +/// diff --git a/docs/tr/docs/how-to/conditional-openapi.md b/docs/tr/docs/how-to/conditional-openapi.md new file mode 100644 index 000000000..9562637c4 --- /dev/null +++ b/docs/tr/docs/how-to/conditional-openapi.md @@ -0,0 +1,56 @@ +# Koşullu OpenAPI { #conditional-openapi } + +Gerekirse, ayarlar ve environment variable'ları kullanarak OpenAPI'yi ortama göre koşullu şekilde yapılandırabilir, hatta tamamen devre dışı bırakabilirsiniz. + +## Güvenlik, API'ler ve dokümantasyon hakkında { #about-security-apis-and-docs } + +Production ortamında dokümantasyon arayüzlerini gizlemek, API'nizi korumanın yolu *olmamalıdır*. + +Bu, API'nize ekstra bir güvenlik katmanı eklemez; *path operation*'lar bulundukları yerde yine erişilebilir olacaktır. + +Kodunuzda bir güvenlik açığı varsa, o açık yine var olmaya devam eder. + +Dokümantasyonu gizlemek, API'nizle nasıl etkileşime geçileceğini anlamayı zorlaştırır ve production'da debug etmeyi de daha zor hale getirebilir. Bu yaklaşım, basitçe Security through obscurity olarak değerlendirilebilir. + +API'nizi güvence altına almak istiyorsanız, yapabileceğiniz daha iyi birçok şey var; örneğin: + +* request body'leriniz ve response'larınız için iyi tanımlanmış Pydantic model'larına sahip olduğunuzdan emin olun. +* dependencies kullanarak gerekli izinleri ve rolleri yapılandırın. +* Asla düz metin (plaintext) şifre saklamayın, yalnızca password hash'leri saklayın. +* pwdlib ve JWT token'ları gibi, iyi bilinen kriptografik araçları uygulayın ve kullanın. +* Gerektiğinde OAuth2 scope'ları ile daha ayrıntılı izin kontrolleri ekleyin. +* ...vb. + +Yine de, bazı ortamlarda (örn. production) veya environment variable'lardan gelen konfigürasyonlara bağlı olarak API docs'u gerçekten devre dışı bırakmanız gereken çok spesifik bir use case'iniz olabilir. + +## Ayarlar ve env var'lar ile koşullu OpenAPI { #conditional-openapi-from-settings-and-env-vars } + +Üretilen OpenAPI'yi ve docs UI'larını yapılandırmak için aynı Pydantic settings'i kolayca kullanabilirsiniz. + +Örneğin: + +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} + +Burada `openapi_url` ayarını, varsayılanı `"/openapi.json"` olacak şekilde tanımlıyoruz. + +Ardından `FastAPI` app'ini oluştururken bunu kullanıyoruz. + +Sonrasında, environment variable `OPENAPI_URL`'i boş string olarak ayarlayarak OpenAPI'yi (UI docs dahil) devre dışı bırakabilirsiniz; örneğin: + +
+ +```console +$ OPENAPI_URL= uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Böylece `/openapi.json`, `/docs` veya `/redoc` URL'lerine giderseniz, aşağıdaki gibi bir `404 Not Found` hatası alırsınız: + +```JSON +{ + "detail": "Not Found" +} +``` diff --git a/docs/tr/docs/how-to/configure-swagger-ui.md b/docs/tr/docs/how-to/configure-swagger-ui.md new file mode 100644 index 000000000..6c051a121 --- /dev/null +++ b/docs/tr/docs/how-to/configure-swagger-ui.md @@ -0,0 +1,70 @@ +# Swagger UI'yi Yapılandırın { #configure-swagger-ui } + +Bazı ek Swagger UI parametrelerini yapılandırabilirsiniz. + +Bunları yapılandırmak için, `FastAPI()` uygulama nesnesini oluştururken ya da `get_swagger_ui_html()` fonksiyonuna `swagger_ui_parameters` argümanını verin. + +`swagger_ui_parameters`, Swagger UI'ye doğrudan iletilecek yapılandırmaları içeren bir `dict` alır. + +FastAPI, Swagger UI'nin ihtiyaç duyduğu şekilde JavaScript ile uyumlu olsun diye bu yapılandırmaları **JSON**'a dönüştürür. + +## Syntax Highlighting'i Devre Dışı Bırakın { #disable-syntax-highlighting } + +Örneğin, Swagger UI'de syntax highlighting'i devre dışı bırakabilirsiniz. + +Ayarları değiştirmeden bırakırsanız, syntax highlighting varsayılan olarak etkindir: + + + +Ancak `syntaxHighlight` değerini `False` yaparak devre dışı bırakabilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} + +...ve ardından Swagger UI artık syntax highlighting'i göstermeyecektir: + + + +## Temayı Değiştirin { #change-the-theme } + +Aynı şekilde, `"syntaxHighlight.theme"` anahtarıyla (ortasında bir nokta olduğuna dikkat edin) syntax highlighting temasını ayarlayabilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} + +Bu yapılandırma, syntax highlighting renk temasını değiştirir: + + + +## Varsayılan Swagger UI Parametrelerini Değiştirin { #change-default-swagger-ui-parameters } + +FastAPI, çoğu kullanım senaryosu için uygun bazı varsayılan yapılandırma parametreleriyle gelir. + +Şu varsayılan yapılandırmaları içerir: + +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} + +`swagger_ui_parameters` argümanında farklı bir değer vererek bunların herhangi birini ezebilirsiniz (override). + +Örneğin `deepLinking`'i devre dışı bırakmak için `swagger_ui_parameters`'a şu ayarları geçebilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} + +## Diğer Swagger UI Parametreleri { #other-swagger-ui-parameters } + +Kullanabileceğiniz diğer tüm olası yapılandırmaları görmek için, resmi Swagger UI parametreleri dokümantasyonunu okuyun. + +## Yalnızca JavaScript ayarları { #javascript-only-settings } + +Swagger UI ayrıca bazı yapılandırmaların **yalnızca JavaScript** nesneleri olmasına izin verir (örneğin JavaScript fonksiyonları). + +FastAPI, bu yalnızca JavaScript olan `presets` ayarlarını da içerir: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +Bunlar string değil, **JavaScript** nesneleridir; dolayısıyla bunları Python kodundan doğrudan geçemezsiniz. + +Böyle yalnızca JavaScript yapılandırmalarına ihtiyacınız varsa, yukarıdaki yöntemlerden birini kullanabilirsiniz: Swagger UI'nin tüm *path operation*'larını override edin ve ihtiyaç duyduğunuz JavaScript'i elle yazın. diff --git a/docs/tr/docs/how-to/custom-docs-ui-assets.md b/docs/tr/docs/how-to/custom-docs-ui-assets.md new file mode 100644 index 000000000..bdd2d0244 --- /dev/null +++ b/docs/tr/docs/how-to/custom-docs-ui-assets.md @@ -0,0 +1,185 @@ +# Özel Docs UI Statik Varlıkları (Self-Hosting) { #custom-docs-ui-static-assets-self-hosting } + +API dokümanları **Swagger UI** ve **ReDoc** kullanır ve bunların her biri bazı JavaScript ve CSS dosyalarına ihtiyaç duyar. + +Varsayılan olarak bu dosyalar bir CDN üzerinden servis edilir. + +Ancak bunu özelleştirmek mümkündür; belirli bir CDN ayarlayabilir veya dosyaları kendiniz servis edebilirsiniz. + +## JavaScript ve CSS için Özel CDN { #custom-cdn-for-javascript-and-css } + +Diyelim ki farklı bir CDN kullanmak istiyorsunuz; örneğin `https://unpkg.com/` kullanmak istiyorsunuz. + +Bu, örneğin bazı URL'leri kısıtlayan bir ülkede yaşıyorsanız faydalı olabilir. + +### Otomatik dokümanları devre dışı bırakın { #disable-the-automatic-docs } + +İlk adım, otomatik dokümanları devre dışı bırakmaktır; çünkü varsayılan olarak bunlar varsayılan CDN'i kullanır. + +Bunları devre dışı bırakmak için `FastAPI` uygulamanızı oluştururken URL'lerini `None` olarak ayarlayın: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} + +### Özel dokümanları ekleyin { #include-the-custom-docs } + +Şimdi özel dokümanlar için *path operation*'ları oluşturabilirsiniz. + +Dokümanlar için HTML sayfalarını üretmek üzere FastAPI'nin dahili fonksiyonlarını yeniden kullanabilir ve gerekli argümanları iletebilirsiniz: + +* `openapi_url`: Dokümanların HTML sayfasının API'niz için OpenAPI şemasını alacağı URL. Burada `app.openapi_url` niteliğini kullanabilirsiniz. +* `title`: API'nizin başlığı. +* `oauth2_redirect_url`: varsayılanı kullanmak için burada `app.swagger_ui_oauth2_redirect_url` kullanabilirsiniz. +* `swagger_js_url`: Swagger UI dokümanlarınızın HTML'inin **JavaScript** dosyasını alacağı URL. Bu, özel CDN URL'idir. +* `swagger_css_url`: Swagger UI dokümanlarınızın HTML'inin **CSS** dosyasını alacağı URL. Bu, özel CDN URL'idir. + +ReDoc için de benzer şekilde... + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} + +/// tip | İpucu + +`swagger_ui_redirect` için olan *path operation*, OAuth2 kullandığınızda yardımcı olması için vardır. + +API'nizi bir OAuth2 sağlayıcısıyla entegre ederseniz kimlik doğrulaması yapabilir, aldığınız kimlik bilgileriyle API dokümanlarına geri dönebilir ve gerçek OAuth2 kimlik doğrulamasını kullanarak onunla etkileşime geçebilirsiniz. + +Swagger UI bunu arka planda sizin için yönetir, ancak bu "redirect" yardımcısına ihtiyaç duyar. + +/// + +### Test etmek için bir *path operation* oluşturun { #create-a-path-operation-to-test-it } + +Şimdi her şeyin çalıştığını test edebilmek için bir *path operation* oluşturun: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} + +### Test edin { #test-it } + +Artık http://127.0.0.1:8000/docs adresinden dokümanlarınıza gidebilmeli ve sayfayı yenilediğinizde bu varlıkların yeni CDN'den yüklendiğini görebilmelisiniz. + +## Dokümanlar için JavaScript ve CSS'i Self-Hosting ile barındırma { #self-hosting-javascript-and-css-for-docs } + +JavaScript ve CSS'i self-hosting ile barındırmak, örneğin uygulamanızın İnternet erişimi olmadan (offline), açık İnternet olmadan veya bir lokal ağ içinde bile çalışmaya devam etmesi gerekiyorsa faydalı olabilir. + +Burada bu dosyaları aynı FastAPI uygulamasında nasıl kendiniz servis edeceğinizi ve dokümanların bunları kullanacak şekilde nasıl yapılandırılacağını göreceksiniz. + +### Proje dosya yapısı { #project-file-structure } + +Diyelim ki projenizin dosya yapısı şöyle: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +Şimdi bu statik dosyaları saklamak için bir dizin oluşturun. + +Yeni dosya yapınız şöyle olabilir: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### Dosyaları indirin { #download-the-files } + +Dokümanlar için gereken statik dosyaları indirin ve `static/` dizinine koyun. + +Muhtemelen her bir linke sağ tıklayıp "Save link as..." benzeri bir seçenek seçebilirsiniz. + +**Swagger UI** şu dosyaları kullanır: + +* `swagger-ui-bundle.js` +* `swagger-ui.css` + +**ReDoc** ise şu dosyayı kullanır: + +* `redoc.standalone.js` + +Bundan sonra dosya yapınız şöyle görünebilir: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### Statik dosyaları servis edin { #serve-the-static-files } + +* `StaticFiles` içe aktarın. +* Belirli bir path'te bir `StaticFiles()` instance'ını "mount" edin. + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} + +### Statik dosyaları test edin { #test-the-static-files } + +Uygulamanızı başlatın ve http://127.0.0.1:8000/static/redoc.standalone.js adresine gidin. + +**ReDoc** için çok uzun bir JavaScript dosyası görmelisiniz. + +Şuna benzer bir şekilde başlayabilir: + +```JavaScript +/*! For license information please see redoc.standalone.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")): +... +``` + +Bu, uygulamanızdan statik dosyaları servis edebildiğinizi ve dokümanlar için statik dosyaları doğru yere koyduğunuzu doğrular. + +Şimdi uygulamayı, dokümanlar için bu statik dosyaları kullanacak şekilde yapılandırabiliriz. + +### Statik dosyalar için otomatik dokümanları devre dışı bırakın { #disable-the-automatic-docs-for-static-files } + +Özel CDN kullanırken olduğu gibi, ilk adım otomatik dokümanları devre dışı bırakmaktır; çünkü bunlar varsayılan olarak CDN kullanır. + +Bunları devre dışı bırakmak için `FastAPI` uygulamanızı oluştururken URL'lerini `None` olarak ayarlayın: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} + +### Statik dosyalar için özel dokümanları ekleyin { #include-the-custom-docs-for-static-files } + +Özel CDN'de olduğu gibi, artık özel dokümanlar için *path operation*'ları oluşturabilirsiniz. + +Yine FastAPI'nin dahili fonksiyonlarını kullanarak dokümanlar için HTML sayfalarını oluşturabilir ve gerekli argümanları geçebilirsiniz: + +* `openapi_url`: Dokümanların HTML sayfasının API'niz için OpenAPI şemasını alacağı URL. Burada `app.openapi_url` niteliğini kullanabilirsiniz. +* `title`: API'nizin başlığı. +* `oauth2_redirect_url`: varsayılanı kullanmak için burada `app.swagger_ui_oauth2_redirect_url` kullanabilirsiniz. +* `swagger_js_url`: Swagger UI dokümanlarınızın HTML'inin **JavaScript** dosyasını alacağı URL. **Artık bunu sizin kendi uygulamanız servis ediyor**. +* `swagger_css_url`: Swagger UI dokümanlarınızın HTML'inin **CSS** dosyasını alacağı URL. **Artık bunu sizin kendi uygulamanız servis ediyor**. + +ReDoc için de benzer şekilde... + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} + +/// tip | İpucu + +`swagger_ui_redirect` için olan *path operation*, OAuth2 kullandığınızda yardımcı olması için vardır. + +API'nizi bir OAuth2 sağlayıcısıyla entegre ederseniz kimlik doğrulaması yapabilir, aldığınız kimlik bilgileriyle API dokümanlarına geri dönebilir ve gerçek OAuth2 kimlik doğrulamasını kullanarak onunla etkileşime geçebilirsiniz. + +Swagger UI bunu arka planda sizin için yönetir, ancak bu "redirect" yardımcısına ihtiyaç duyar. + +/// + +### Statik dosyaları test etmek için bir *path operation* oluşturun { #create-a-path-operation-to-test-static-files } + +Şimdi her şeyin çalıştığını test edebilmek için bir *path operation* oluşturun: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} + +### Statik Dosyalar UI'ını Test Edin { #test-static-files-ui } + +Artık WiFi bağlantınızı kesip http://127.0.0.1:8000/docs adresindeki dokümanlarınıza gidebilmeli ve sayfayı yenileyebilmelisiniz. + +Ve İnternet olmasa bile API dokümanlarınızı görebilir ve onunla etkileşime geçebilirsiniz. diff --git a/docs/tr/docs/how-to/custom-request-and-route.md b/docs/tr/docs/how-to/custom-request-and-route.md new file mode 100644 index 000000000..a4419373f --- /dev/null +++ b/docs/tr/docs/how-to/custom-request-and-route.md @@ -0,0 +1,109 @@ +# Özel Request ve APIRoute sınıfı { #custom-request-and-apiroute-class } + +Bazı durumlarda, `Request` ve `APIRoute` sınıflarının kullandığı mantığı override etmek isteyebilirsiniz. + +Özellikle bu yaklaşım, bir middleware içindeki mantığa iyi bir alternatif olabilir. + +Örneğin, request body uygulamanız tarafından işlenmeden önce okumak veya üzerinde değişiklik yapmak istiyorsanız. + +/// danger | Uyarı + +Bu "ileri seviye" bir özelliktir. + +**FastAPI**'ye yeni başlıyorsanız bu bölümü atlamak isteyebilirsiniz. + +/// + +## Kullanım senaryoları { #use-cases } + +Bazı kullanım senaryoları: + +* JSON olmayan request body'leri JSON'a dönüştürmek (örn. `msgpack`). +* gzip ile sıkıştırılmış request body'leri açmak (decompress). +* Tüm request body'lerini otomatik olarak loglamak. + +## Özel request body encoding'lerini ele alma { #handling-custom-request-body-encodings } + +Gzip request'lerini açmak için özel bir `Request` alt sınıfını nasıl kullanabileceğimize bakalım. + +Ayrıca, o özel request sınıfını kullanmak için bir `APIRoute` alt sınıfı da oluşturacağız. + +### Özel bir `GzipRequest` sınıfı oluşturun { #create-a-custom-gziprequest-class } + +/// tip | İpucu + +Bu, nasıl çalıştığını göstermek için hazırlanmış basit bir örnektir; Gzip desteğine ihtiyacınız varsa sağlanan [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank} bileşenini kullanabilirsiniz. + +/// + +Önce, uygun bir header mevcut olduğunda body'yi açmak için `Request.body()` metodunu overwrite edecek bir `GzipRequest` sınıfı oluşturuyoruz. + +Header'da `gzip` yoksa body'yi açmayı denemez. + +Böylece aynı route sınıfı, gzip ile sıkıştırılmış veya sıkıştırılmamış request'leri handle edebilir. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} + +### Özel bir `GzipRoute` sınıfı oluşturun { #create-a-custom-gziproute-class } + +Sonra, `GzipRequest`'i kullanacak `fastapi.routing.APIRoute` için özel bir alt sınıf oluşturuyoruz. + +Bu kez `APIRoute.get_route_handler()` metodunu overwrite edeceğiz. + +Bu metot bir fonksiyon döndürür. Bu fonksiyon da request'i alır ve response döndürür. + +Burada bu fonksiyonu, orijinal request'ten bir `GzipRequest` oluşturmak için kullanıyoruz. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} + +/// note | Teknik Detaylar + +Bir `Request`'in, request ile ilgili metadata'yı içeren bir Python `dict` olan `request.scope` attribute'u vardır. + +Bir `Request` ayrıca `request.receive` içerir; bu, request'in body'sini "almak" (receive etmek) için kullanılan bir fonksiyondur. + +`scope` `dict`'i ve `receive` fonksiyonu, ASGI spesifikasyonunun parçalarıdır. + +Ve bu iki şey, `scope` ve `receive`, yeni bir `Request` instance'ı oluşturmak için gerekenlerdir. + +`Request` hakkında daha fazla bilgi için Starlette'ın Request dokümantasyonuna bakın. + +/// + +`GzipRequest.get_route_handler` tarafından döndürülen fonksiyonun farklı yaptığı tek şey, `Request`'i bir `GzipRequest`'e dönüştürmektir. + +Bunu yaptığımızda `GzipRequest`, veriyi (gerekliyse) *path operations*'larımıza geçirmeden önce açma (decompress) işini üstlenir. + +Bundan sonra tüm işleme mantığı aynıdır. + +Ancak `GzipRequest.body` içindeki değişikliklerimiz sayesinde, request body gerektiğinde **FastAPI** tarafından yüklendiğinde otomatik olarak decompress edilir. + +## Bir exception handler içinde request body'ye erişme { #accessing-the-request-body-in-an-exception-handler } + +/// tip | İpucu + +Aynı problemi çözmek için, muhtemelen `RequestValidationError` için özel bir handler içinde `body` kullanmak çok daha kolaydır ([Hataları Ele Alma](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}). + +Yine de bu örnek geçerlidir ve dahili bileşenlerle nasıl etkileşime geçileceğini gösterir. + +/// + +Aynı yaklaşımı bir exception handler içinde request body'ye erişmek için de kullanabiliriz. + +Tek yapmamız gereken, request'i bir `try`/`except` bloğu içinde handle etmek: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} + +Bir exception oluşursa, `Request` instance'ı hâlâ scope içinde olacağı için, hatayı handle ederken request body'yi okuyup kullanabiliriz: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} + +## Bir router içinde özel `APIRoute` sınıfı { #custom-apiroute-class-in-a-router } + +Bir `APIRouter` için `route_class` parametresini de ayarlayabilirsiniz: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} + +Bu örnekte, `router` altındaki *path operations*'lar özel `TimedRoute` sınıfını kullanır ve response'u üretmek için geçen süreyi içeren ekstra bir `X-Response-Time` header'ı response'ta bulunur: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/tr/docs/how-to/extending-openapi.md b/docs/tr/docs/how-to/extending-openapi.md new file mode 100644 index 000000000..99691946c --- /dev/null +++ b/docs/tr/docs/how-to/extending-openapi.md @@ -0,0 +1,80 @@ +# OpenAPI'yi Genişletme { #extending-openapi } + +Oluşturulan OpenAPI şemasını değiştirmeniz gereken bazı durumlar olabilir. + +Bu bölümde bunun nasıl yapılacağını göreceksiniz. + +## Normal süreç { #the-normal-process } + +Normal (varsayılan) süreç şöyledir. + +Bir `FastAPI` uygulamasının (instance) OpenAPI şemasını döndürmesi beklenen bir `.openapi()` metodu vardır. + +Uygulama nesnesi oluşturulurken, `/openapi.json` (ya da `openapi_url` için ne ayarladıysanız o) için bir *path operation* kaydedilir. + +Bu path operation, uygulamanın `.openapi()` metodunun sonucunu içeren bir JSON response döndürür. + +Varsayılan olarak `.openapi()` metodunun yaptığı şey, `.openapi_schema` özelliğinde içerik olup olmadığını kontrol etmek ve varsa onu döndürmektir. + +Eğer yoksa, `fastapi.openapi.utils.get_openapi` konumundaki yardımcı (utility) fonksiyonu kullanarak şemayı üretir. + +Ve `get_openapi()` fonksiyonu şu parametreleri alır: + +* `title`: Dokümanlarda gösterilen OpenAPI başlığı. +* `version`: API'nizin sürümü, örn. `2.5.0`. +* `openapi_version`: Kullanılan OpenAPI specification sürümü. Varsayılan olarak en günceli: `3.1.0`. +* `summary`: API'nin kısa özeti. +* `description`: API'nizin açıklaması; markdown içerebilir ve dokümanlarda gösterilir. +* `routes`: route'ların listesi; bunların her biri kayıtlı *path operations*'lardır. `app.routes` içinden alınırlar. + +/// info | Bilgi + +`summary` parametresi OpenAPI 3.1.0 ve üzeri sürümlerde vardır; FastAPI 0.99.0 ve üzeri tarafından desteklenmektedir. + +/// + +## Varsayılanları ezme { #overriding-the-defaults } + +Yukarıdaki bilgileri kullanarak aynı yardımcı fonksiyonla OpenAPI şemasını üretebilir ve ihtiyacınız olan her parçayı override edebilirsiniz. + +Örneğin, özel bir logo eklemek için ReDoc'un OpenAPI extension'ını ekleyelim. + +### Normal **FastAPI** { #normal-fastapi } + +Önce, tüm **FastAPI** uygulamanızı her zamanki gibi yazın: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} + +### OpenAPI şemasını üretme { #generate-the-openapi-schema } + +Ardından, bir `custom_openapi()` fonksiyonunun içinde aynı yardımcı fonksiyonu kullanarak OpenAPI şemasını üretin: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} + +### OpenAPI şemasını değiştirme { #modify-the-openapi-schema } + +Artık OpenAPI şemasındaki `info` "object"'ine özel bir `x-logo` ekleyerek ReDoc extension'ını ekleyebilirsiniz: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} + +### OpenAPI şemasını cache'leme { #cache-the-openapi-schema } + +Ürettiğiniz şemayı saklamak için `.openapi_schema` özelliğini bir "cache" gibi kullanabilirsiniz. + +Böylece bir kullanıcı API docs'larınızı her açtığında uygulamanız şemayı tekrar tekrar üretmek zorunda kalmaz. + +Şema yalnızca bir kez üretilecektir; sonraki request'ler için de aynı cache'lenmiş şema kullanılacaktır. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} + +### Metodu override etme { #override-the-method } + +Şimdi `.openapi()` metodunu yeni fonksiyonunuzla değiştirebilirsiniz. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} + +### Kontrol edin { #check-it } + +http://127.0.0.1:8000/redoc adresine gittiğinizde, özel logonuzu kullandığınızı göreceksiniz (bu örnekte **FastAPI**'nin logosu): + + diff --git a/docs/tr/docs/how-to/graphql.md b/docs/tr/docs/how-to/graphql.md new file mode 100644 index 000000000..fbf018874 --- /dev/null +++ b/docs/tr/docs/how-to/graphql.md @@ -0,0 +1,60 @@ +# GraphQL { #graphql } + +**FastAPI**, **ASGI** standardını temel aldığı için ASGI ile uyumlu herhangi bir **GraphQL** kütüphanesini entegre etmek oldukça kolaydır. + +Aynı uygulama içinde normal FastAPI *path operation*'larını GraphQL ile birlikte kullanabilirsiniz. + +/// tip | İpucu + +**GraphQL** bazı çok özel kullanım senaryolarını çözer. + +Yaygın **web API**'lerle karşılaştırıldığında **avantajları** ve **dezavantajları** vardır. + +Kendi senaryonuz için **faydaların**, **olumsuz yönleri** telafi edip etmediğini mutlaka değerlendirin. 🤓 + +/// + +## GraphQL Kütüphaneleri { #graphql-libraries } + +Aşağıda **ASGI** desteği olan bazı **GraphQL** kütüphaneleri var. Bunları **FastAPI** ile kullanabilirsiniz: + +* Strawberry 🍓 + * FastAPI dokümantasyonu ile +* Ariadne + * FastAPI dokümantasyonu ile +* Tartiflette + * ASGI entegrasyonu sağlamak için Tartiflette ASGI ile +* Graphene + * starlette-graphene3 ile + +## Strawberry ile GraphQL { #graphql-with-strawberry } + +**GraphQL** ile çalışmanız gerekiyorsa ya da bunu istiyorsanız, **Strawberry** önerilen kütüphanedir; çünkü tasarımı **FastAPI**'nin tasarımına en yakındır ve her şey **type annotation**'lar üzerine kuruludur. + +Kullanım senaryonuza göre farklı bir kütüphaneyi tercih edebilirsiniz; ancak bana sorarsanız muhtemelen **Strawberry**'yi denemenizi önerirdim. + +Strawberry'yi FastAPI ile nasıl entegre edebileceğinize dair küçük bir ön izleme: + +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} + +Strawberry hakkında daha fazlasını Strawberry dokümantasyonunda öğrenebilirsiniz. + +Ayrıca FastAPI ile Strawberry dokümanlarına da göz atın. + +## Starlette'teki Eski `GraphQLApp` { #older-graphqlapp-from-starlette } + +Starlette'in önceki sürümlerinde Graphene ile entegrasyon için bir `GraphQLApp` sınıfı vardı. + +Bu sınıf Starlette'te kullanımdan kaldırıldı (deprecated). Ancak bunu kullanan bir kodunuz varsa, aynı kullanım senaryosunu kapsayan ve **neredeyse aynı bir interface** sağlayan starlette-graphene3'e kolayca **migrate** edebilirsiniz. + +/// tip | İpucu + +GraphQL'e ihtiyacınız varsa, custom class ve type'lar yerine type annotation'lara dayandığı için yine de Strawberry'yi incelemenizi öneririm. + +/// + +## Daha Fazlasını Öğrenin { #learn-more } + +**GraphQL** hakkında daha fazlasını resmi GraphQL dokümantasyonunda öğrenebilirsiniz. + +Ayrıca yukarıda bahsedilen kütüphanelerin her biri hakkında, kendi bağlantılarından daha fazla bilgi okuyabilirsiniz. diff --git a/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..275ac5fd1 --- /dev/null +++ b/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,135 @@ +# Pydantic v1'den Pydantic v2'ye Geçiş { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Eski bir FastAPI uygulamanız varsa, Pydantic'in 1. sürümünü kullanıyor olabilirsiniz. + +FastAPI 0.100.0 sürümü, Pydantic v1 veya v2 ile çalışmayı destekliyordu. Hangisi kuruluysa onu kullanıyordu. + +FastAPI 0.119.0 sürümü, v2'ye geçişi kolaylaştırmak için, Pydantic v2’nin içinden Pydantic v1’e (`pydantic.v1` olarak) kısmi destek ekledi. + +FastAPI 0.126.0 sürümü Pydantic v1 desteğini kaldırdı, ancak bir süre daha `pydantic.v1` desteğini sürdürdü. + +/// warning | Uyarı + +Pydantic ekibi, **Python 3.14** ile başlayarak Python'ın en yeni sürümleri için Pydantic v1 desteğini sonlandırdı. + +Buna `pydantic.v1` de dahildir; Python 3.14 ve üzeri sürümlerde artık desteklenmemektedir. + +Python'ın en yeni özelliklerini kullanmak istiyorsanız, Pydantic v2 kullandığınızdan emin olmanız gerekir. + +/// + +Pydantic v1 kullanan eski bir FastAPI uygulamanız varsa, burada onu Pydantic v2'ye nasıl taşıyacağınızı ve kademeli geçişi kolaylaştıran **FastAPI 0.119.0 özelliklerini** göstereceğim. + +## Resmi Kılavuz { #official-guide } + +Pydantic'in v1'den v2'ye resmi bir Migration Guide'ı vardır. + +Ayrıca nelerin değiştiğini, validasyonların artık nasıl daha doğru ve katı olduğunu, olası dikkat edilmesi gereken noktaları (caveat) vb. de içerir. + +Nelerin değiştiğini daha iyi anlamak için okuyabilirsiniz. + +## Testler { #tests } + +Uygulamanız için [testlerinizin](../tutorial/testing.md){.internal-link target=_blank} olduğundan ve bunları continuous integration (CI) üzerinde çalıştırdığınızdan emin olun. + +Bu şekilde yükseltmeyi yapabilir ve her şeyin hâlâ beklendiği gibi çalıştığını doğrulayabilirsiniz. + +## `bump-pydantic` { #bump-pydantic } + +Birçok durumda, özel özelleştirmeler olmadan standart Pydantic modelleri kullanıyorsanız, Pydantic v1'den Pydantic v2'ye geçiş sürecinin büyük kısmını otomatikleştirebilirsiniz. + +Aynı Pydantic ekibinin geliştirdiği `bump-pydantic` aracını kullanabilirsiniz. + +Bu araç, değişmesi gereken kodun büyük bir kısmını otomatik olarak dönüştürmenize yardımcı olur. + +Bundan sonra testleri çalıştırıp her şeyin çalışıp çalışmadığını kontrol edebilirsiniz. Çalışıyorsa işiniz biter. 😎 + +## v2 İçinde Pydantic v1 { #pydantic-v1-in-v2 } + +Pydantic v2, `pydantic.v1` adlı bir alt modül olarak Pydantic v1'in tamamını içerir. Ancak bu yapı, Python 3.13'ün üzerindeki sürümlerde artık desteklenmemektedir. + +Bu da şu anlama gelir: Pydantic v2'nin en güncel sürümünü kurup, bu alt modülden eski Pydantic v1 bileşenlerini import ederek, sanki eski Pydantic v1 kuruluymuş gibi kullanabilirsiniz. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### v2 İçinde Pydantic v1 için FastAPI Desteği { #fastapi-support-for-pydantic-v1-in-v2 } + +FastAPI 0.119.0'dan itibaren, v2'ye geçişi kolaylaştırmak için Pydantic v2’nin içinden Pydantic v1 kullanımına yönelik kısmi destek de vardır. + +Dolayısıyla Pydantic'i en güncel 2 sürümüne yükseltip import'ları `pydantic.v1` alt modülünü kullanacak şekilde değiştirebilirsiniz; çoğu durumda bu doğrudan çalışır. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Uyarı + +Pydantic ekibi Python 3.14'ten itibaren yeni Python sürümlerinde Pydantic v1'i artık desteklemediği için, `pydantic.v1` kullanımı da Python 3.14 ve üzeri sürümlerde desteklenmez. + +/// + +### Aynı Uygulamada Pydantic v1 ve v2 { #pydantic-v1-and-v2-on-the-same-app } + +Pydantic açısından, alanları (field) Pydantic v1 modelleriyle tanımlanmış bir Pydantic v2 modeli (ya da tersi) kullanmak **desteklenmez**. + +```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 +``` + +...ancak aynı uygulamada Pydantic v1 ve v2 kullanarak **ayrı (separated)** modeller tanımlayabilirsiniz. + +```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 +``` + +Bazı durumlarda, FastAPI uygulamanızda aynı **path operation** içinde hem Pydantic v1 hem de v2 modellerini kullanmak bile mümkündür: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +Yukarıdaki örnekte input modeli bir Pydantic v1 modelidir; output modeli ( `response_model=ItemV2` ile tanımlanan) ise bir Pydantic v2 modelidir. + +### Pydantic v1 Parametreleri { #pydantic-v1-parameters } + +Pydantic v1 modelleriyle `Body`, `Query`, `Form` vb. parametreler için FastAPI'ye özgü bazı araçları kullanmanız gerekiyorsa, Pydantic v2'ye geçişi tamamlayana kadar bunları `fastapi.temp_pydantic_v1_params` içinden import edebilirsiniz: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Adım Adım Geçiş { #migrate-in-steps } + +/// tip | İpucu + +Önce `bump-pydantic` ile deneyin; testleriniz geçerse ve bu yol çalışırsa tek komutla işi bitirmiş olursunuz. ✨ + +/// + +`bump-pydantic` sizin senaryonuz için uygun değilse, aynı uygulamada hem Pydantic v1 hem de v2 modellerini birlikte kullanma desteğinden yararlanarak Pydantic v2'ye kademeli şekilde geçebilirsiniz. + +Önce Pydantic'i en güncel 2 sürümüne yükseltip tüm modelleriniz için import'ları `pydantic.v1` kullanacak şekilde değiştirebilirsiniz. + +Ardından modellerinizi Pydantic v1'den v2'ye gruplar hâlinde, adım adım taşımaya başlayabilirsiniz. 🚶 diff --git a/docs/tr/docs/how-to/separate-openapi-schemas.md b/docs/tr/docs/how-to/separate-openapi-schemas.md new file mode 100644 index 000000000..c26411d29 --- /dev/null +++ b/docs/tr/docs/how-to/separate-openapi-schemas.md @@ -0,0 +1,102 @@ +# Input ve Output için Ayrı OpenAPI Schema'ları (Ya da Değil) { #separate-openapi-schemas-for-input-and-output-or-not } + +**Pydantic v2** yayınlandığından beri, üretilen OpenAPI eskisine göre biraz daha net ve **doğru**. 😎 + +Hatta bazı durumlarda, aynı Pydantic model için OpenAPI içinde input ve output tarafında, **default değerler** olup olmamasına bağlı olarak **iki farklı JSON Schema** bile görebilirsiniz. + +Bunun nasıl çalıştığına ve gerekirse nasıl değiştirebileceğinize bir bakalım. + +## Input ve Output için Pydantic Modelleri { #pydantic-models-for-input-and-output } + +Default değerleri olan bir Pydantic modeliniz olduğunu varsayalım; örneğin şöyle: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} + +### Input için Model { #model-for-input } + +Bu modeli şöyle input olarak kullanırsanız: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} + +...`description` alanı **zorunlu olmaz**. Çünkü `None` default değerine sahiptir. + +### Dokümanlarda Input Modeli { #input-model-in-docs } + +Bunu dokümanlarda da doğrulayabilirsiniz; `description` alanında **kırmızı yıldız** yoktur, yani required olarak işaretlenmemiştir: + +
+ +
+ +### Output için Model { #model-for-output } + +Ancak aynı modeli output olarak şöyle kullanırsanız: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *} + +...`description` default değere sahip olduğu için, o alan için **hiçbir şey döndürmeseniz** bile yine de **o default değeri** alır. + +### Output Response Verisi için Model { #model-for-output-response-data } + +Dokümanlarla etkileşip response'u kontrol ederseniz, kod `description` alanlarından birine bir şey eklememiş olsa bile, JSON response default değeri (`null`) içerir: + +
+ +
+ +Bu, alanın **her zaman bir değeri olacağı** anlamına gelir; sadece bazen bu değer `None` olabilir (JSON'da `null`). + +Dolayısıyla API'nizi kullanan client'ların bu değerin var olup olmadığını kontrol etmesine gerek yoktur; **alanın her zaman mevcut olacağını varsayabilirler**, sadece bazı durumlarda default değer olan `None` gelecektir. + +Bunu OpenAPI'de ifade etmenin yolu, bu alanı **required** olarak işaretlemektir; çünkü her zaman yer alacaktır. + +Bu nedenle, bir modelin JSON Schema'sı **input mu output mu** kullanıldığına göre farklı olabilir: + +* **input** için `description` **required olmaz** +* **output** için **required olur** (ve `None` olabilir; JSON açısından `null`) + +### Dokümanlarda Output Modeli { #model-for-output-in-docs } + +Dokümanlarda output modelini de kontrol edebilirsiniz; **hem** `name` **hem de** `description` alanları **kırmızı yıldız** ile **required** olarak işaretlenmiştir: + +
+ +
+ +### Dokümanlarda Input ve Output Modelleri { #model-for-input-and-output-in-docs } + +OpenAPI içindeki tüm kullanılabilir Schema'lara (JSON Schema'lara) bakarsanız, iki tane olduğunu göreceksiniz: biri `Item-Input`, diğeri `Item-Output`. + +`Item-Input` için `description` **required değildir**, kırmızı yıldız yoktur. + +Ama `Item-Output` için `description` **required**'dır, kırmızı yıldız vardır. + +
+ +
+ +**Pydantic v2**'nin bu özelliğiyle API dokümantasyonunuz daha **hassas** olur; ayrıca autogenerated client'lar ve SDK'lar kullanıyorsanız, onlar da daha tutarlı ve daha iyi bir **developer experience** ile daha doğru üretilir. 🎉 + +## Schema'ları Ayırma { #do-not-separate-schemas } + +Bazı durumlarda **input ve output için aynı schema'yı** kullanmak isteyebilirsiniz. + +Bunun muhtemelen en yaygın nedeni, halihazırda autogenerated client kodlarınız/SDK'larınızın olması ve henüz bunların hepsini güncellemek istememenizdir. Büyük ihtimalle bir noktada güncellemek isteyeceksiniz, ama belki şu an değil. + +Bu durumda **FastAPI**'de bu özelliği `separate_input_output_schemas=False` parametresiyle kapatabilirsiniz. + +/// info | Bilgi + +`separate_input_output_schemas` desteği FastAPI `0.102.0` sürümünde eklendi. 🤓 + +/// + +{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} + +### Dokümanlarda Input ve Output Modelleri için Aynı Schema { #same-schema-for-input-and-output-models-in-docs } + +Artık model için input ve output tarafında tek bir schema olur: sadece `Item`. Ayrıca `description` alanı **required değildir**: + +
+ +
diff --git a/docs/tr/docs/how-to/testing-database.md b/docs/tr/docs/how-to/testing-database.md new file mode 100644 index 000000000..a2062f6c2 --- /dev/null +++ b/docs/tr/docs/how-to/testing-database.md @@ -0,0 +1,7 @@ +# Bir Veritabanını Test Etmek { #testing-a-database } + +Veritabanları, SQL ve SQLModel hakkında SQLModel dokümantasyonundan öğrenebilirsiniz. 🤓 + +Ayrıca SQLModel'i FastAPI ile kullanmaya dair mini bir tutorial da var. ✨ + +Bu tutorial içinde SQL veritabanlarını test etme hakkında bir bölüm de bulunuyor. 😎 diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 9cffd4274..16c425f5d 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Şu içerikle `main.py` adında bir dosya oluşturalım: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Eğer kodunuz `async` / `await` kullanıyorsa, `async def` kullanın: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Alternatif otomatik dokümantasyonu göreceksiniz (`starlette.background`’dan gelir. + +`fastapi` üzerinden import edebilmeniz ve yanlışlıkla `starlette.background` içindeki alternatif `BackgroundTask`’i (sonunda `s` olmadan) import etmemeniz için FastAPI’nin içine doğrudan import/eklenmiştir. + +Sadece `BackgroundTasks` (ve `BackgroundTask` değil) kullanarak, bunu bir *path operation function* parametresi olarak kullanmak ve gerisini **FastAPI**’nin sizin için halletmesini sağlamak mümkündür; tıpkı `Request` objesini doğrudan kullanırken olduğu gibi. + +FastAPI’de `BackgroundTask`’i tek başına kullanmak hâlâ mümkündür; ancak bu durumda objeyi kendi kodunuzda oluşturmanız ve onu içeren bir Starlette `Response` döndürmeniz gerekir. + +Daha fazla detayı Starlette’in Background Tasks için resmi dokümantasyonunda görebilirsiniz. + +## Dikkat Edilmesi Gerekenler { #caveat } + +Yoğun arka plan hesaplamaları yapmanız gerekiyorsa ve bunun aynı process tarafından çalıştırılmasına şart yoksa (örneğin memory, değişkenler vb. paylaşmanız gerekmiyorsa), Celery gibi daha büyük araçları kullanmak size fayda sağlayabilir. + +Bunlar genellikle daha karmaşık konfigurasyonlar ve RabbitMQ veya Redis gibi bir mesaj/iş kuyruğu yöneticisi gerektirir; ancak arka plan görevlerini birden fazla process’te ve özellikle birden fazla server’da çalıştırmanıza olanak tanırlar. + +Ancak aynı **FastAPI** app’i içindeki değişkenlere ve objelere erişmeniz gerekiyorsa veya küçük arka plan görevleri (email bildirimi göndermek gibi) yapacaksanız, doğrudan `BackgroundTasks` kullanabilirsiniz. + +## Özet { #recap } + +Arka plan görevleri eklemek için *path operation function*’larda ve dependency’lerde parametre olarak `BackgroundTasks`’i import edip kullanın. diff --git a/docs/tr/docs/tutorial/bigger-applications.md b/docs/tr/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..d8a4b8208 --- /dev/null +++ b/docs/tr/docs/tutorial/bigger-applications.md @@ -0,0 +1,504 @@ +# Daha Büyük Uygulamalar - Birden Fazla Dosya { #bigger-applications-multiple-files } + +Bir uygulama veya web API geliştirirken, her şeyi tek bir dosyaya sığdırabilmek nadirdir. + +**FastAPI**, tüm esnekliği korurken uygulamanızı yapılandırmanıza yardımcı olan pratik bir araç sunar. + +/// info | Bilgi + +Flask'ten geliyorsanız, bu yapı Flask'in Blueprints'ine denk gelir. + +/// + +## Örnek Bir Dosya Yapısı { #an-example-file-structure } + +Diyelim ki şöyle bir dosya yapınız var: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +/// tip | İpucu + +Birden fazla `__init__.py` dosyası var: her dizinde veya alt dizinde bir tane. + +Bu sayede bir dosyadaki kodu diğerine import edebilirsiniz. + +Örneğin `app/main.py` içinde şöyle bir satırınız olabilir: + +``` +from app.routers import items +``` + +/// + +* `app` dizini her şeyi içerir. Ayrıca boş bir `app/__init__.py` dosyası olduğu için bir "Python package" (bir "Python module" koleksiyonu) olur: `app`. +* İçinde bir `app/main.py` dosyası vardır. Bir Python package'in (içinde `__init__.py` dosyası olan bir dizinin) içinde olduğundan, o package'in bir "module"’üdür: `app.main`. +* Benzer şekilde `app/dependencies.py` dosyası da bir "module"’dür: `app.dependencies`. +* `app/routers/` adında bir alt dizin vardır ve içinde başka bir `__init__.py` dosyası bulunur; dolayısıyla bu bir "Python subpackage"’dir: `app.routers`. +* `app/routers/items.py` dosyası `app/routers/` package’i içinde olduğundan bir submodule’dür: `app.routers.items`. +* `app/routers/users.py` için de aynı şekilde, başka bir submodule’dür: `app.routers.users`. +* `app/internal/` adında bir alt dizin daha vardır ve içinde başka bir `__init__.py` dosyası bulunur; dolayısıyla bu da bir "Python subpackage"’dir: `app.internal`. +* Ve `app/internal/admin.py` dosyası başka bir submodule’dür: `app.internal.admin`. + + + +Aynı dosya yapısı, yorumlarla birlikte: + +```bash +. +├── app # "app" bir Python package'idir +│   ├── __init__.py # bu dosya, "app"i bir "Python package" yapar +│   ├── main.py # "main" module'ü, örn. import app.main +│   ├── dependencies.py # "dependencies" module'ü, örn. import app.dependencies +│   └── routers # "routers" bir "Python subpackage"idir +│   │ ├── __init__.py # "routers"ı bir "Python subpackage" yapar +│   │ ├── items.py # "items" submodule'ü, örn. import app.routers.items +│   │ └── users.py # "users" submodule'ü, örn. import app.routers.users +│   └── internal # "internal" bir "Python subpackage"idir +│   ├── __init__.py # "internal"ı bir "Python subpackage" yapar +│   └── admin.py # "admin" submodule'ü, örn. import app.internal.admin +``` + +## `APIRouter` { #apirouter } + +Diyelim ki sadece kullanıcıları yönetmeye ayrılmış dosyanız `/app/routers/users.py` içindeki submodule olsun. + +Kullanıcılarla ilgili *path operation*’ları, kodun geri kalanından ayrı tutmak istiyorsunuz; böylece düzenli kalır. + +Ancak bu hâlâ aynı **FastAPI** uygulaması/web API’sinin bir parçasıdır (aynı "Python Package" içinde). + +Bu module için *path operation*’ları `APIRouter` kullanarak oluşturabilirsiniz. + +### `APIRouter` Import Edin { #import-apirouter } + +`FastAPI` class’ında yaptığınız gibi import edip bir "instance" oluşturursunuz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +### `APIRouter` ile *Path Operations* { #path-operations-with-apirouter } + +Sonra bunu kullanarak *path operation*’larınızı tanımlarsınız. + +`FastAPI` class’ını nasıl kullanıyorsanız aynı şekilde kullanın: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} + +`APIRouter`’ı "mini bir `FastAPI`" class’ı gibi düşünebilirsiniz. + +Aynı seçeneklerin hepsi desteklenir. + +Aynı `parameters`, `responses`, `dependencies`, `tags`, vb. + +/// tip | İpucu + +Bu örnekte değişkenin adı `router`. Ancak istediğiniz gibi adlandırabilirsiniz. + +/// + +Bu `APIRouter`’ı ana `FastAPI` uygulamasına ekleyeceğiz; ama önce dependency’lere ve bir diğer `APIRouter`’a bakalım. + +## Dependencies { #dependencies } + +Uygulamanın birden fazla yerinde kullanılacak bazı dependency’lere ihtiyacımız olacağını görüyoruz. + +Bu yüzden onları ayrı bir `dependencies` module’üne koyuyoruz (`app/dependencies.py`). + +Şimdi, özel bir `X-Token` header'ını okumak için basit bir dependency kullanalım: + +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} + +/// tip | İpucu + +Örneği basit tutmak için uydurma bir header kullanıyoruz. + +Ancak gerçek senaryolarda, entegre [Security yardımcı araçlarını](security/index.md){.internal-link target=_blank} kullanarak daha iyi sonuç alırsınız. + +/// + +## `APIRouter` ile Başka Bir Module { #another-module-with-apirouter } + +Diyelim ki uygulamanızdaki "items" ile ilgili endpoint'ler de `app/routers/items.py` module’ünde olsun. + +Şunlar için *path operation*’larınız var: + +* `/items/` +* `/items/{item_id}` + +Bu, `app/routers/users.py` ile aynı yapıdadır. + +Ancak biraz daha akıllı davranıp kodu sadeleştirmek istiyoruz. + +Bu module’deki tüm *path operation*’ların şu ortak özelliklere sahip olduğunu biliyoruz: + +* Path `prefix`: `/items`. +* `tags`: (tek bir tag: `items`). +* Ek `responses`. +* `dependencies`: hepsinin, oluşturduğumuz `X-Token` dependency’sine ihtiyacı var. + +Dolayısıyla bunları her *path operation*’a tek tek eklemek yerine `APIRouter`’a ekleyebiliriz. + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} + +Her *path operation*’ın path’i aşağıdaki gibi `/` ile başlamak zorunda olduğundan: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...prefix’in sonunda `/` olmamalıdır. + +Yani bu örnekte prefix `/items` olur. + +Ayrıca, bu router içindeki tüm *path operation*’lara uygulanacak bir `tags` listesi ve ek `responses` da ekleyebiliriz. + +Ve router’daki tüm *path operation*’lara eklenecek, her request için çalıştırılıp çözülecek bir `dependencies` listesi de ekleyebiliriz. + +/// tip | İpucu + +[ *path operation decorator*’larındaki dependency’lerde](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} olduğu gibi, *path operation function*’ınıza herhangi bir değer aktarılmayacağını unutmayın. + +/// + +Sonuç olarak item path’leri artık: + +* `/items/` +* `/items/{item_id}` + +...tam da istediğimiz gibi olur. + +* Hepsi, içinde tek bir string `"items"` bulunan bir tag listesiyle işaretlenir. + * Bu "tags", özellikle otomatik interaktif dokümantasyon sistemleri (OpenAPI) için çok faydalıdır. +* Hepsi önceden tanımlı `responses`’ları içerir. +* Bu *path operation*’ların hepsinde, öncesinde `dependencies` listesi değerlendirilip çalıştırılır. + * Ayrıca belirli bir *path operation* içinde dependency tanımlarsanız, **onlar da çalıştırılır**. + * Önce router dependency’leri, sonra decorator’daki [`dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, sonra da normal parametre dependency’leri çalışır. + * Ayrıca [`scopes` ile `Security` dependency’leri](../advanced/security/oauth2-scopes.md){.internal-link target=_blank} de ekleyebilirsiniz. + +/// tip | İpucu + +`APIRouter` içinde `dependencies` kullanmak, örneğin bir grup *path operation* için kimlik doğrulamayı zorunlu kılmakta kullanılabilir. Dependency’leri tek tek her birine eklemeseniz bile. + +/// + +/// check | Ek bilgi + +`prefix`, `tags`, `responses` ve `dependencies` parametreleri (çoğu başka örnekte olduğu gibi) kod tekrarını önlemenize yardımcı olan, **FastAPI**’nin bir özelliğidir. + +/// + +### Dependency'leri Import Edin { #import-the-dependencies } + +Bu kod `app.routers.items` module’ünde, yani `app/routers/items.py` dosyasında duruyor. + +Dependency function’ını ise `app.dependencies` module’ünden, yani `app/dependencies.py` dosyasından almamız gerekiyor. + +Bu yüzden dependency’ler için `..` ile relative import kullanıyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} + +#### Relative Import Nasıl Çalışır { #how-relative-imports-work } + +/// tip | İpucu + +Import’ların nasıl çalıştığını çok iyi biliyorsanız, bir sonraki bölüme geçin. + +/// + +Tek bir nokta `.`, örneğin: + +```Python +from .dependencies import get_token_header +``` + +şu anlama gelir: + +* Bu module’ün (yani `app/routers/items.py` dosyasının) bulunduğu package içinden başla ( `app/routers/` dizini)... +* `dependencies` module’ünü bul (`app/routers/dependencies.py` gibi hayali bir dosya)... +* ve oradan `get_token_header` function’ını import et. + +Ama o dosya yok; bizim dependency’lerimiz `app/dependencies.py` dosyasında. + +Uygulama/dosya yapımızın nasıl göründüğünü hatırlayın: + + + +--- + +İki nokta `..`, örneğin: + +```Python +from ..dependencies import get_token_header +``` + +şu anlama gelir: + +* Bu module’ün bulunduğu package içinden başla (`app/routers/` dizini)... +* üst (parent) package’e çık (`app/` dizini)... +* burada `dependencies` module’ünü bul (`app/dependencies.py` dosyası)... +* ve oradan `get_token_header` function’ını import et. + +Bu doğru şekilde çalışır! 🎉 + +--- + +Aynı şekilde, üç nokta `...` kullansaydık: + +```Python +from ...dependencies import get_token_header +``` + +şu anlama gelirdi: + +* Bu module’ün bulunduğu package içinden başla (`app/routers/` dizini)... +* üst package’e çık (`app/` dizini)... +* sonra bir üstüne daha çık (orada bir üst package yok; `app` en üst seviye 😱)... +* ve orada `dependencies` module’ünü bul (`app/dependencies.py` dosyası)... +* ve oradan `get_token_header` function’ını import et. + +Bu, `app/` dizininin üstünde, kendi `__init__.py` dosyası olan başka bir package’e işaret ederdi. Ama bizde böyle bir şey yok. Dolayısıyla bu örnekte hata verirdi. 🚨 + +Artık nasıl çalıştığını bildiğinize göre, uygulamalarınız ne kadar karmaşık olursa olsun relative import’ları kullanabilirsiniz. 🤓 + +### Özel `tags`, `responses` ve `dependencies` Ekleyin { #add-some-custom-tags-responses-and-dependencies } + +`/items` prefix’ini ya da `tags=["items"]` değerini her *path operation*’a tek tek eklemiyoruz; çünkü bunları `APIRouter`’a ekledik. + +Ama yine de belirli bir *path operation*’a uygulanacak _ek_ `tags` tanımlayabilir, ayrıca o *path operation*’a özel `responses` ekleyebiliriz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} + +/// tip | İpucu + +Bu son *path operation*’da tag kombinasyonu şöyle olur: `["items", "custom"]`. + +Ayrıca dokümantasyonda iki response da görünür: biri `404`, diğeri `403`. + +/// + +## Ana `FastAPI` { #the-main-fastapi } + +Şimdi `app/main.py` module’üne bakalım. + +Burada `FastAPI` class’ını import edip kullanırsınız. + +Bu dosya, uygulamanızda her şeyi bir araya getiren ana dosya olacak. + +Mantığın büyük kısmı artık kendi module’lerinde yaşayacağı için ana dosya oldukça basit kalır. + +### `FastAPI` Import Edin { #import-fastapi } + +Normal şekilde bir `FastAPI` class’ı oluşturursunuz. + +Hatta her `APIRouter` için olan dependency’lerle birleştirilecek [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} bile tanımlayabilirsiniz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} + +### `APIRouter` Import Edin { #import-the-apirouter } + +Şimdi `APIRouter` içeren diğer submodule’leri import ediyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} + +`app/routers/users.py` ve `app/routers/items.py` dosyaları aynı Python package’i olan `app`’in parçası olan submodule’ler olduğu için, onları "relative import" ile tek bir nokta `.` kullanarak import edebiliriz. + +### Import Nasıl Çalışır { #how-the-importing-works } + +Şu bölüm: + +```Python +from .routers import items, users +``` + +şu anlama gelir: + +* Bu module’ün (yani `app/main.py` dosyasının) bulunduğu package içinden başla (`app/` dizini)... +* `routers` subpackage’ini bul (`app/routers/` dizini)... +* ve buradan `items` submodule’ünü (`app/routers/items.py`) ve `users` submodule’ünü (`app/routers/users.py`) import et... + +`items` module’ünün içinde `router` adında bir değişken vardır (`items.router`). Bu, `app/routers/items.py` dosyasında oluşturduğumuz aynı değişkendir; bir `APIRouter` nesnesidir. + +Sonra aynı işlemi `users` module’ü için de yaparız. + +Ayrıca şöyle de import edebilirdik: + +```Python +from app.routers import items, users +``` + +/// info | Bilgi + +İlk sürüm "relative import"tur: + +```Python +from .routers import items, users +``` + +İkinci sürüm "absolute import"tur: + +```Python +from app.routers import items, users +``` + +Python Packages ve Modules hakkında daha fazlası için, Python'ın Modules ile ilgili resmi dokümantasyonunu okuyun. + +/// + +### İsim Çakışmalarını Önleyin { #avoid-name-collisions } + +`items` submodule’ünü doğrudan import ediyoruz; sadece içindeki `router` değişkenini import etmiyoruz. + +Çünkü `users` submodule’ünde de `router` adlı başka bir değişken var. + +Eğer şöyle sırayla import etseydik: + +```Python +from .routers.items import router +from .routers.users import router +``` + +`users` içindeki `router`, `items` içindeki `router`’ın üstüne yazardı ve ikisini aynı anda kullanamazdık. + +Bu yüzden ikisini de aynı dosyada kullanabilmek için submodule’leri doğrudan import ediyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} + +### `users` ve `items` için `APIRouter`’ları Dahil Edin { #include-the-apirouters-for-users-and-items } + +Şimdi `users` ve `items` submodule’lerindeki `router`’ları dahil edelim: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} + +/// info | Bilgi + +`users.router`, `app/routers/users.py` dosyasının içindeki `APIRouter`’ı içerir. + +`items.router` ise `app/routers/items.py` dosyasının içindeki `APIRouter`’ı içerir. + +/// + +`app.include_router()` ile her bir `APIRouter`’ı ana `FastAPI` uygulamasına ekleyebiliriz. + +Böylece o router içindeki tüm route’lar uygulamanın bir parçası olarak dahil edilir. + +/// note | Teknik Detaylar + +Aslında içeride, `APIRouter` içinde tanımlanan her *path operation* için bir *path operation* oluşturur. + +Yani perde arkasında, her şey tek bir uygulamaymış gibi çalışır. + +/// + +/// check | Ek bilgi + +Router’ları dahil ederken performans konusunda endişelenmeniz gerekmez. + +Bu işlem mikrosaniyeler sürer ve sadece startup sırasında olur. + +Dolayısıyla performansı etkilemez. ⚡ + +/// + +### Özel `prefix`, `tags`, `responses` ve `dependencies` ile Bir `APIRouter` Dahil Edin { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } + +Şimdi, kurumunuzun size `app/internal/admin.py` dosyasını verdiğini düşünelim. + +Bu dosyada, kurumunuzun birden fazla proje arasında paylaştığı bazı admin *path operation*’larını içeren bir `APIRouter` var. + +Bu örnekte çok basit olacak. Ancak kurum içinde başka projelerle paylaşıldığı için, bunu değiştirip `prefix`, `dependencies`, `tags` vs. doğrudan `APIRouter`’a ekleyemediğimizi varsayalım: + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Yine de bu `APIRouter`’ı dahil ederken özel bir `prefix` ayarlamak istiyoruz ki tüm *path operation*’ları `/admin` ile başlasın; ayrıca bu projede hâlihazırda kullandığımız `dependencies` ile güvene almak, `tags` ve `responses` eklemek istiyoruz. + +Orijinal `APIRouter`’ı değiştirmeden, bu parametreleri `app.include_router()`’a vererek hepsini tanımlayabiliriz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} + +Böylece orijinal `APIRouter` değişmeden kalır; yani aynı `app/internal/admin.py` dosyasını kurum içindeki diğer projelerle de paylaşmaya devam edebiliriz. + +Sonuç olarak, uygulamamızda `admin` module’ündeki her bir *path operation* şunlara sahip olur: + +* `/admin` prefix’i. +* `admin` tag’i. +* `get_token_header` dependency’si. +* `418` response’u. 🍵 + +Ancak bu sadece bizim uygulamamızdaki o `APIRouter` için geçerlidir; onu kullanan diğer kodlar için değil. + +Dolayısıyla örneğin diğer projeler aynı `APIRouter`’ı farklı bir authentication yöntemiyle kullanabilir. + +### Bir *Path Operation* Dahil Edin { #include-a-path-operation } + +*Path operation*’ları doğrudan `FastAPI` uygulamasına da ekleyebiliriz. + +Burada bunu yapıyoruz... sadece yapabildiğimizi göstermek için 🤷: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} + +ve `app.include_router()` ile eklenen diğer tüm *path operation*’larla birlikte doğru şekilde çalışır. + +/// info | Çok Teknik Detaylar + +**Not**: Bu oldukça teknik bir detay; büyük ihtimalle **direkt geçebilirsiniz**. + +--- + +`APIRouter`’lar "mount" edilmez; uygulamanın geri kalanından izole değildir. + +Çünkü *path operation*’larını OpenAPI şemasına ve kullanıcı arayüzlerine dahil etmek istiyoruz. + +Onları tamamen izole edip bağımsız şekilde "mount" edemediğimiz için, *path operation*’lar doğrudan eklenmek yerine "klonlanır" (yeniden oluşturulur). + +/// + +## Otomatik API Dokümanını Kontrol Edin { #check-the-automatic-api-docs } + +Şimdi uygulamanızı çalıştırın: + +
+ +```console +$ fastapi dev app/main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Ve dokümanları http://127.0.0.1:8000/docs adresinde açın. + +Tüm submodule’lerdeki path’leri, doğru path’ler (ve prefix’ler) ve doğru tag’lerle birlikte içeren otomatik API dokümanını göreceksiniz: + + + +## Aynı Router'ı Farklı `prefix` ile Birden Fazla Kez Dahil Edin { #include-the-same-router-multiple-times-with-different-prefix } + +`.include_router()` ile aynı router’ı farklı prefix’ler kullanarak birden fazla kez de dahil edebilirsiniz. + +Örneğin aynı API’yi `/api/v1` ve `/api/latest` gibi farklı prefix’ler altında sunmak için faydalı olabilir. + +Bu, muhtemelen ihtiyacınız olmayan ileri seviye bir kullanımdır; ancak gerekirse diye mevcut. + +## Bir `APIRouter`’ı Başka Birine Dahil Edin { #include-an-apirouter-in-another } + +Bir `APIRouter`’ı `FastAPI` uygulamasına dahil ettiğiniz gibi, bir `APIRouter`’ı başka bir `APIRouter`’a da şu şekilde dahil edebilirsiniz: + +```Python +router.include_router(other_router) +``` + +`router`’ı `FastAPI` uygulamasına dahil etmeden önce bunu yaptığınızdan emin olun; böylece `other_router` içindeki *path operation*’lar da dahil edilmiş olur. diff --git a/docs/tr/docs/tutorial/body-fields.md b/docs/tr/docs/tutorial/body-fields.md new file mode 100644 index 000000000..6a0f3314a --- /dev/null +++ b/docs/tr/docs/tutorial/body-fields.md @@ -0,0 +1,60 @@ +# Body - Alanlar { #body-fields } + +`Query`, `Path` ve `Body` ile *path operation function* parametrelerinde ek doğrulama ve metadata tanımlayabildiğiniz gibi, Pydantic modellerinin içinde de Pydantic'in `Field`'ını kullanarak doğrulama ve metadata tanımlayabilirsiniz. + +## `Field`'ı import edin { #import-field } + +Önce import etmeniz gerekir: + +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} + +/// warning | Uyarı + +`Field`'ın, diğerlerinin (`Query`, `Path`, `Body` vb.) aksine `fastapi`'den değil doğrudan `pydantic`'den import edildiğine dikkat edin. + +/// + +## Model attribute'larını tanımlayın { #declare-model-attributes } + +Ardından `Field`'ı model attribute'larıyla birlikte kullanabilirsiniz: + +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} + +`Field`, `Query`, `Path` ve `Body` ile aynı şekilde çalışır; aynı parametrelerin tamamına sahiptir, vb. + +/// note | Teknik Detaylar + +Aslında, `Query`, `Path` ve birazdan göreceğiniz diğerleri, ortak bir `Param` sınıfının alt sınıflarından nesneler oluşturur; `Param` sınıfı da Pydantic'in `FieldInfo` sınıfının bir alt sınıfıdır. + +Pydantic'in `Field`'ı da `FieldInfo`'nun bir instance'ını döndürür. + +`Body` ayrıca doğrudan `FieldInfo`'nun bir alt sınıfından nesneler döndürür. Daha sonra göreceğiniz başka bazıları da `Body` sınıfının alt sınıflarıdır. + +`fastapi`'den `Query`, `Path` ve diğerlerini import ettiğinizde, bunların aslında özel sınıflar döndüren fonksiyonlar olduğunu unutmayın. + +/// + +/// tip | İpucu + +Type, varsayılan değer ve `Field` ile tanımlanan her model attribute'unun yapısının, *path operation function* parametresiyle aynı olduğuna dikkat edin; sadece `Path`, `Query` ve `Body` yerine `Field` kullanılmıştır. + +/// + +## Ek bilgi ekleyin { #add-extra-information } + +`Field`, `Query`, `Body` vb. içinde ek bilgi tanımlayabilirsiniz. Bu bilgiler oluşturulan JSON Schema'ya dahil edilir. + +Örnek (examples) tanımlamayı öğrenirken, dokümanların ilerleyen kısımlarında ek bilgi ekleme konusunu daha ayrıntılı göreceksiniz. + +/// warning | Uyarı + +`Field`'a geçirilen ekstra key'ler, uygulamanız için üretilen OpenAPI schema'sında da yer alır. +Bu key'ler OpenAPI spesifikasyonunun bir parçası olmak zorunda olmadığından, örneğin [OpenAPI validator](https://validator.swagger.io/) gibi bazı OpenAPI araçları üretilen schema'nızla çalışmayabilir. + +/// + +## Özet { #recap } + +Model attribute'ları için ek doğrulamalar ve metadata tanımlamak üzere Pydantic'in `Field`'ını kullanabilirsiniz. + +Ayrıca, ek keyword argument'ları kullanarak JSON Schema'ya ekstra metadata da iletebilirsiniz. diff --git a/docs/tr/docs/tutorial/body-multiple-params.md b/docs/tr/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..29970ca40 --- /dev/null +++ b/docs/tr/docs/tutorial/body-multiple-params.md @@ -0,0 +1,175 @@ +# Body - Birden Fazla Parametre { #body-multiple-parameters } + +Artık `Path` ve `Query` kullanmayı gördüğümüze göre, request body bildirimlerinin daha ileri kullanım senaryolarına bakalım. + +## `Path`, `Query` ve body parametrelerini karıştırma { #mix-path-query-and-body-parameters } + +Öncelikle, elbette `Path`, `Query` ve request body parametre bildirimlerini serbestçe karıştırabilirsiniz ve **FastAPI** ne yapacağını bilir. + +Ayrıca, varsayılan değeri `None` yaparak body parametrelerini opsiyonel olarak da tanımlayabilirsiniz: + +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} + +/// note | Not + +Bu durumda body'den alınacak `item` opsiyoneldir. Çünkü varsayılan değeri `None` olarak ayarlanmıştır. + +/// + +## Birden fazla body parametresi { #multiple-body-parameters } + +Önceki örnekte, *path operation*'lar `Item`'ın özelliklerini içeren bir JSON body beklerdi, örneğin: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Ancak birden fazla body parametresi de tanımlayabilirsiniz; örneğin `item` ve `user`: + +{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} + + +Bu durumda **FastAPI**, fonksiyonda birden fazla body parametresi olduğunu fark eder (iki parametre de Pydantic modelidir). + +Bunun üzerine, body içinde anahtar (field name) olarak parametre adlarını kullanır ve şu şekilde bir body bekler: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +/// note | Not + +`item` daha öncekiyle aynı şekilde tanımlanmış olsa bile, artık body içinde `item` anahtarı altında gelmesi beklenir. + +/// + +**FastAPI**, request'ten otomatik dönüşümü yapar; böylece `item` parametresi kendi içeriğini alır, `user` için de aynı şekilde olur. + +Birleşik verinin validasyonunu yapar ve OpenAPI şeması ile otomatik dokümantasyonda da bunu bu şekilde dokümante eder. + +## Body içinde tekil değerler { #singular-values-in-body } + +Query ve path parametreleri için ek veri tanımlamak üzere `Query` ve `Path` olduğu gibi, **FastAPI** bunların karşılığı olarak `Body` de sağlar. + +Örneğin, önceki modeli genişleterek, aynı body içinde `item` ve `user` dışında bir de `importance` anahtarı olmasını isteyebilirsiniz. + +Bunu olduğu gibi tanımlarsanız, tekil bir değer olduğu için **FastAPI** bunun bir query parametresi olduğunu varsayar. + +Ama `Body` kullanarak, **FastAPI**'ye bunu body içinde başka bir anahtar olarak ele almasını söyleyebilirsiniz: + +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} + + +Bu durumda **FastAPI** şu şekilde bir body bekler: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +Yine veri tiplerini dönüştürür, validate eder, dokümante eder, vb. + +## Birden fazla body parametresi ve query { #multiple-body-params-and-query } + +Elbette ihtiyaç duyduğunuzda, body parametrelerine ek olarak query parametreleri de tanımlayabilirsiniz. + +Varsayılan olarak tekil değerler query parametresi olarak yorumlandığı için, ayrıca `Query` eklemeniz gerekmez; şöyle yazmanız yeterlidir: + +```Python +q: str | None = None +``` + +Ya da Python 3.9'da: + +```Python +q: Union[str, None] = None +``` + +Örneğin: + +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} + + +/// info | Bilgi + +`Body`, `Query`, `Path` ve daha sonra göreceğiniz diğerleriyle aynı ek validasyon ve metadata parametrelerine de sahiptir. + +/// + +## Tek bir body parametresini gömme { #embed-a-single-body-parameter } + +Diyelim ki Pydantic'teki `Item` modelinden gelen yalnızca tek bir `item` body parametreniz var. + +Varsayılan olarak **FastAPI**, body'nin doğrudan bu modelin içeriği olmasını bekler. + +Ancak, ek body parametreleri tanımladığınızda olduğu gibi, `item` anahtarı olan bir JSON ve onun içinde modelin içeriğini beklemesini istiyorsanız, `Body`'nin özel parametresi olan `embed`'i kullanabilirsiniz: + +```Python +item: Item = Body(embed=True) +``` + +yani şöyle: + +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + + +Bu durumda **FastAPI** şu şekilde bir body bekler: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +şunun yerine: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Özet { #recap } + +Bir request yalnızca tek bir body içerebilse de, *path operation function*'ınıza birden fazla body parametresi ekleyebilirsiniz. + +Ancak **FastAPI** bunu yönetir; fonksiyonunuza doğru veriyi verir ve *path operation* içinde doğru şemayı validate edip dokümante eder. + +Ayrıca tekil değerlerin body'nin bir parçası olarak alınmasını da tanımlayabilirsiniz. + +Ve yalnızca tek bir parametre tanımlanmış olsa bile, **FastAPI**'ye body'yi bir anahtarın içine gömmesini söyleyebilirsiniz. diff --git a/docs/tr/docs/tutorial/body-nested-models.md b/docs/tr/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..b4ffef3f1 --- /dev/null +++ b/docs/tr/docs/tutorial/body-nested-models.md @@ -0,0 +1,220 @@ +# Body - İç İçe Modeller { #body-nested-models } + +**FastAPI** ile (Pydantic sayesinde) istediğiniz kadar derin iç içe geçmiş modelleri tanımlayabilir, doğrulayabilir, dokümante edebilir ve kullanabilirsiniz. + +## List alanları { #list-fields } + +Bir attribute’u bir alt tipe sahip olacak şekilde tanımlayabilirsiniz. Örneğin, bir Python `list`: + +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} + +Bu, `tags`’in bir list olmasını sağlar; ancak list’in elemanlarının tipini belirtmez. + +## Tip parametresi olan list alanları { #list-fields-with-type-parameter } + +Ancak Python’da, iç tipleri olan list’leri (ya da "type parameter" içeren tipleri) tanımlamanın belirli bir yolu vardır: + +### Tip parametresiyle bir `list` tanımlayın { #declare-a-list-with-a-type-parameter } + +`list`, `dict`, `tuple` gibi type parameter (iç tip) alan tipleri tanımlamak için, iç tipi(leri) köşeli parantezlerle "type parameter" olarak verin: `[` ve `]` + +```Python +my_list: list[str] +``` + +Bu, tip tanımları için standart Python sözdizimidir. + +İç tipleri olan model attribute’ları için de aynı standart sözdizimini kullanın. + +Dolayısıyla örneğimizde, `tags`’i özel olarak bir "string list’i" yapabiliriz: + +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} + +## Set tipleri { #set-types } + +Sonra bunu düşününce, tag’lerin tekrar etmemesi gerektiğini fark ederiz; büyük ihtimalle benzersiz string’ler olmalıdır. + +Python’da benzersiz öğelerden oluşan kümeler için özel bir veri tipi vardır: `set`. + +O zaman `tags`’i string’lerden oluşan bir set olarak tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} + +Böylece duplicate veri içeren bir request alsanız bile, bu veri benzersiz öğelerden oluşan bir set’e dönüştürülür. + +Ve bu veriyi ne zaman output etseniz, kaynakta duplicate olsa bile, benzersiz öğelerden oluşan bir set olarak output edilir. + +Ayrıca buna göre annotate / dokümante edilir. + +## İç İçe Modeller { #nested-models } + +Bir Pydantic modelinin her attribute’unun bir tipi vardır. + +Ancak bu tip, kendi başına başka bir Pydantic modeli de olabilir. + +Yani belirli attribute adları, tipleri ve validation kurallarıyla derin iç içe JSON "object"leri tanımlayabilirsiniz. + +Hem de istediğiniz kadar iç içe. + +### Bir alt model tanımlayın { #define-a-submodel } + +Örneğin bir `Image` modeli tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} + +### Alt modeli tip olarak kullanın { #use-the-submodel-as-a-type } + +Ardından bunu bir attribute’un tipi olarak kullanabiliriz: + +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} + +Bu da **FastAPI**’nin aşağıdakine benzer bir body bekleyeceği anlamına gelir: + +```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" + } +} +``` + +Yine, sadece bu tanımı yaparak **FastAPI** ile şunları elde edersiniz: + +* Editör desteği (tamamlama vb.), iç içe modeller için bile +* Veri dönüştürme +* Veri doğrulama (validation) +* Otomatik dokümantasyon + +## Özel tipler ve doğrulama { #special-types-and-validation } + +`str`, `int`, `float` vb. normal tekil tiplerin yanında, `str`’den türeyen daha karmaşık tekil tipleri de kullanabilirsiniz. + +Tüm seçenekleri görmek için Pydantic Type Overview sayfasına göz atın. Sonraki bölümde bazı örnekleri göreceksiniz. + +Örneğin `Image` modelinde bir `url` alanımız olduğuna göre, bunu `str` yerine Pydantic’in `HttpUrl` tipinden bir instance olacak şekilde tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} + +String’in geçerli bir URL olup olmadığı kontrol edilir ve JSON Schema / OpenAPI’de de buna göre dokümante edilir. + +## Alt modellerden oluşan list’lere sahip attribute’lar { #attributes-with-lists-of-submodels } + +Pydantic modellerini `list`, `set` vb. tiplerin alt tipi olarak da kullanabilirsiniz: + +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} + +Bu, aşağıdaki gibi bir JSON body bekler (dönüştürür, doğrular, dokümante eder vb.): + +```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 | Bilgi + +`images` key’inin artık image object’lerinden oluşan bir list içerdiğine dikkat edin. + +/// + +## Çok derin iç içe modeller { #deeply-nested-models } + +İstediğiniz kadar derin iç içe modeller tanımlayabilirsiniz: + +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} + +/// info | Bilgi + +`Offer`’ın bir `Item` list’i olduğuna, `Item`’ların da opsiyonel bir `Image` list’ine sahip olduğuna dikkat edin. + +/// + +## Sadece list olan body’ler { #bodies-of-pure-lists } + +Beklediğiniz JSON body’nin en üst seviye değeri bir JSON `array` (Python’da `list`) ise, tipi Pydantic modellerinde olduğu gibi fonksiyonun parametresinde tanımlayabilirsiniz: + +```Python +images: list[Image] +``` + +şu örnekte olduğu gibi: + +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} + +## Her yerde editör desteği { #editor-support-everywhere } + +Ve her yerde editör desteği alırsınız. + +List içindeki öğeler için bile: + + + +Pydantic modelleri yerine doğrudan `dict` ile çalışsaydınız bu tür bir editör desteğini alamazdınız. + +Ancak bunlarla uğraşmanız da gerekmez; gelen dict’ler otomatik olarak dönüştürülür ve output’unuz da otomatik olarak JSON’a çevrilir. + +## Rastgele `dict` body’leri { #bodies-of-arbitrary-dicts } + +Body’yi, key’leri bir tipte ve value’ları başka bir tipte olan bir `dict` olarak da tanımlayabilirsiniz. + +Bu şekilde (Pydantic modellerinde olduğu gibi) geçerli field/attribute adlarının önceden ne olduğunu bilmeniz gerekmez. + +Bu, önceden bilmediğiniz key’leri almak istediğiniz durumlarda faydalıdır. + +--- + +Bir diğer faydalı durum da key’lerin başka bir tipte olmasını istediğiniz zamandır (ör. `int`). + +Burada göreceğimiz şey de bu. + +Bu durumda, `int` key’lere ve `float` value’lara sahip olduğu sürece herhangi bir `dict` kabul edersiniz: + +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} + +/// tip | İpucu + +JSON key olarak yalnızca `str` destekler, bunu unutmayın. + +Ancak Pydantic otomatik veri dönüştürme yapar. + +Yani API client’larınız key’leri sadece string olarak gönderebilse bile, bu string’ler saf tamsayı içeriyorsa Pydantic bunları dönüştürür ve doğrular. + +Ve `weights` olarak aldığınız `dict`, gerçekte `int` key’lere ve `float` value’lara sahip olur. + +/// + +## Özet { #recap } + +**FastAPI** ile Pydantic modellerinin sağladığı en yüksek esnekliği elde ederken, kodunuzu da basit, kısa ve şık tutarsınız. + +Üstelik tüm avantajlarla birlikte: + +* Editör desteği (her yerde tamamlama!) +* Veri dönüştürme (diğer adıyla parsing / serialization) +* Veri doğrulama (validation) +* Schema dokümantasyonu +* Otomatik dokümanlar diff --git a/docs/tr/docs/tutorial/body-updates.md b/docs/tr/docs/tutorial/body-updates.md new file mode 100644 index 000000000..a9ad13d2e --- /dev/null +++ b/docs/tr/docs/tutorial/body-updates.md @@ -0,0 +1,100 @@ +# Body - Güncellemeler { #body-updates } + +## `PUT` ile değiştirerek güncelleme { #update-replacing-with-put } + +Bir öğeyi güncellemek için HTTP `PUT` operasyonunu kullanabilirsiniz. + +Girdi verisini JSON olarak saklanabilecek bir formata (ör. bir NoSQL veritabanı ile) dönüştürmek için `jsonable_encoder` kullanabilirsiniz. Örneğin, `datetime` değerlerini `str`'ye çevirmek gibi. + +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} + +`PUT`, mevcut verinin yerine geçmesi gereken veriyi almak için kullanılır. + +### Değiştirerek güncelleme uyarısı { #warning-about-replacing } + +Bu, `bar` öğesini `PUT` ile, body içinde şu verilerle güncellemek isterseniz: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +zaten kayıtlı olan `"tax": 20.2` alanını içermediği için, input model `"tax": 10.5` varsayılan değerini kullanacaktır. + +Ve veri, bu "yeni" `tax` değeri olan `10.5` ile kaydedilecektir. + +## `PATCH` ile kısmi güncellemeler { #partial-updates-with-patch } + +Veriyi *kısmen* güncellemek için HTTP `PATCH` operasyonunu da kullanabilirsiniz. + +Bu, yalnızca güncellemek istediğiniz veriyi gönderip, geri kalanını olduğu gibi bırakabileceğiniz anlamına gelir. + +/// note | Not + +`PATCH`, `PUT`'a göre daha az yaygın kullanılır ve daha az bilinir. + +Hatta birçok ekip, kısmi güncellemeler için bile yalnızca `PUT` kullanır. + +Bunları nasıl isterseniz öyle kullanmakta **özgürsünüz**; **FastAPI** herhangi bir kısıtlama dayatmaz. + +Ancak bu kılavuz, aşağı yukarı, bunların nasıl kullanılması amaçlandığını gösterir. + +/// + +### Pydantic'in `exclude_unset` parametresini kullanma { #using-pydantics-exclude-unset-parameter } + +Kısmi güncellemeler almak istiyorsanız, Pydantic modelinin `.model_dump()` metodundaki `exclude_unset` parametresini kullanmak çok faydalıdır. + +Örneğin: `item.model_dump(exclude_unset=True)`. + +Bu, `item` modeli oluşturulurken set edilmiş verileri içeren; varsayılan değerleri hariç tutan bir `dict` üretir. + +Sonrasında bunu, yalnızca set edilmiş (request'te gönderilmiş) veriyi içeren; varsayılan değerleri atlayan bir `dict` üretmek için kullanabilirsiniz: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} + +### Pydantic'in `update` parametresini kullanma { #using-pydantics-update-parameter } + +Artık `.model_copy()` ile mevcut modelin bir kopyasını oluşturup, güncellenecek verileri içeren bir `dict` ile `update` parametresini geçebilirsiniz. + +Örneğin: `stored_item_model.model_copy(update=update_data)`: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} + +### Kısmi güncellemeler özeti { #partial-updates-recap } + +Özetle, kısmi güncelleme uygulamak için şunları yaparsınız: + +* (İsteğe bağlı olarak) `PUT` yerine `PATCH` kullanın. +* Kayıtlı veriyi alın. +* Bu veriyi bir Pydantic modeline koyun. +* Input modelinden, varsayılan değerler olmadan bir `dict` üretin (`exclude_unset` kullanarak). + * Bu şekilde, modelinizdeki varsayılan değerlerle daha önce saklanmış değerlerin üzerine yazmak yerine, yalnızca kullanıcının gerçekten set ettiği değerleri güncellersiniz. +* Kayıtlı modelin bir kopyasını oluşturun ve alınan kısmi güncellemeleri kullanarak attribute'larını güncelleyin (`update` parametresini kullanarak). +* Kopyalanan modeli DB'nizde saklanabilecek bir şeye dönüştürün (ör. `jsonable_encoder` kullanarak). + * Bu, modelin `.model_dump()` metodunu yeniden kullanmaya benzer; ancak değerlerin JSON'a dönüştürülebilecek veri tiplerine çevrilmesini garanti eder (ör. `datetime` -> `str`). +* Veriyi DB'nize kaydedin. +* Güncellenmiş modeli döndürün. + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} + +/// tip | İpucu + +Aynı tekniği HTTP `PUT` operasyonu ile de kullanabilirsiniz. + +Ancak buradaki örnek `PATCH` kullanıyor, çünkü bu kullanım senaryoları için tasarlanmıştır. + +/// + +/// note | Not + +Input modelin yine de doğrulandığına dikkat edin. + +Dolayısıyla, tüm attribute'ların atlanabildiği kısmi güncellemeler almak istiyorsanız, tüm attribute'ları optional olarak işaretlenmiş (varsayılan değerlerle veya `None` ile) bir modele ihtiyacınız vardır. + +**Güncelleme** için tüm değerleri optional olan modeller ile **oluşturma** için zorunlu değerlere sahip modelleri ayırmak için, [Extra Models](extra-models.md){.internal-link target=_blank} bölümünde anlatılan fikirleri kullanabilirsiniz. + +/// diff --git a/docs/tr/docs/tutorial/body.md b/docs/tr/docs/tutorial/body.md new file mode 100644 index 000000000..0557ef737 --- /dev/null +++ b/docs/tr/docs/tutorial/body.md @@ -0,0 +1,166 @@ +# Request Body { #request-body } + +Bir client'ten (örneğin bir tarayıcıdan) API'nize veri göndermeniz gerektiğinde, bunu **request body** olarak gönderirsiniz. + +Bir **request** body, client'in API'nize gönderdiği veridir. Bir **response** body ise API'nizin client'e gönderdiği veridir. + +API'niz neredeyse her zaman bir **response** body göndermek zorundadır. Ancak client'lerin her zaman **request body** göndermesi gerekmez; bazen sadece bir path isterler, belki birkaç query parametresiyle birlikte, ama body göndermezler. + +Bir **request** body tanımlamak için, tüm gücü ve avantajlarıyla Pydantic modellerini kullanırsınız. + +/// info | Bilgi + +Veri göndermek için şunlardan birini kullanmalısınız: `POST` (en yaygını), `PUT`, `DELETE` veya `PATCH`. + +`GET` request'i ile body göndermek, spesifikasyonlarda tanımsız bir davranıştır; yine de FastAPI bunu yalnızca çok karmaşık/uç kullanım senaryoları için destekler. + +Önerilmediği için Swagger UI ile etkileşimli dokümanlar, `GET` kullanırken body için dokümantasyonu göstermez ve aradaki proxy'ler bunu desteklemeyebilir. + +/// + +## Pydantic'in `BaseModel`'ini import edin { #import-pydantics-basemodel } + +Önce, `pydantic` içinden `BaseModel`'i import etmeniz gerekir: + +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} + +## Veri modelinizi oluşturun { #create-your-data-model } + +Sonra veri modelinizi, `BaseModel`'den kalıtım alan bir class olarak tanımlarsınız. + +Tüm attribute'lar için standart Python type'larını kullanın: + +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} + + +Query parametrelerini tanımlarken olduğu gibi, bir model attribute'ü default bir değere sahipse zorunlu değildir. Aksi halde zorunludur. Sadece opsiyonel yapmak için `None` kullanın. + +Örneğin, yukarıdaki model şu şekilde bir JSON "`object`" (veya Python `dict`) tanımlar: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...`description` ve `tax` opsiyonel olduğu için (default değerleri `None`), şu JSON "`object`" da geçerli olur: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Parametre olarak tanımlayın { #declare-it-as-a-parameter } + +Bunu *path operation*'ınıza eklemek için, path ve query parametrelerini tanımladığınız şekilde tanımlayın: + +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} + +...ve type'ını, oluşturduğunuz model olan `Item` olarak belirtin. + +## Sonuçlar { #results } + +Sadece bu Python type tanımıyla, **FastAPI** şunları yapar: + +* Request'in body'sini JSON olarak okur. +* İlgili type'lara dönüştürür (gerekirse). +* Veriyi doğrular (validate eder). + * Veri geçersizse, tam olarak nerede ve hangi verinin hatalı olduğunu söyleyen, anlaşılır bir hata döndürür. +* Aldığı veriyi `item` parametresi içinde size verir. + * Fonksiyonda bunun type'ını `Item` olarak tanımladığınız için, tüm attribute'lar ve type'ları için editor desteğini (tamamlama vb.) de alırsınız. +* Modeliniz için JSON Schema tanımları üretir; projeniz için anlamlıysa bunları başka yerlerde de kullanabilirsiniz. +* Bu şemalar üretilen OpenAPI şemasının bir parçası olur ve otomatik dokümantasyon UIs tarafından kullanılır. + +## Otomatik dokümanlar { #automatic-docs } + +Modellerinizin JSON Schema'ları, OpenAPI tarafından üretilen şemanın bir parçası olur ve etkileşimli API dokümanlarında gösterilir: + + + +Ayrıca, ihtiyaç duyan her *path operation* içindeki API dokümanlarında da kullanılır: + + + +## Editor desteği { #editor-support } + +Editor'ünüzde, fonksiyonunuzun içinde her yerde type hint'leri ve tamamlama (completion) alırsınız (Pydantic modeli yerine `dict` alsaydınız bu olmazdı): + + + +Yanlış type işlemleri için hata kontrolleri de alırsınız: + + + +Bu bir tesadüf değil; tüm framework bu tasarımın etrafında inşa edildi. + +Ayrıca, bunun tüm editor'lerle çalışacağından emin olmak için herhangi bir implementasyon yapılmadan önce tasarım aşamasında kapsamlı şekilde test edildi. + +Hatta bunu desteklemek için Pydantic'in kendisinde bile bazı değişiklikler yapıldı. + +Önceki ekran görüntüleri Visual Studio Code ile alınmıştır. + +Ancak PyCharm ve diğer Python editor'lerinin çoğunda da aynı editor desteğini alırsınız: + + + +/// tip | İpucu + +Editor olarak PyCharm kullanıyorsanız, Pydantic PyCharm Plugin kullanabilirsiniz. + +Pydantic modelleri için editor desteğini şu açılardan iyileştirir: + +* auto-completion +* type checks +* refactoring +* searching +* inspections + +/// + +## Modeli kullanın { #use-the-model } + +Fonksiyonun içinde model nesnesinin tüm attribute'larına doğrudan erişebilirsiniz: + +{* ../../docs_src/body/tutorial002_py310.py *} + +## Request body + path parametreleri { #request-body-path-parameters } + +Path parametrelerini ve request body'yi aynı anda tanımlayabilirsiniz. + +**FastAPI**, path parametreleriyle eşleşen fonksiyon parametrelerinin **path'ten alınması** gerektiğini ve Pydantic model olarak tanımlanan fonksiyon parametrelerinin **request body'den alınması** gerektiğini anlayacaktır. + +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} + + +## Request body + path + query parametreleri { #request-body-path-query-parameters } + +**body**, **path** ve **query** parametrelerini aynı anda da tanımlayabilirsiniz. + +**FastAPI** bunların her birini tanır ve veriyi doğru yerden alır. + +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} + +Fonksiyon parametreleri şu şekilde tanınır: + +* Parametre, **path** içinde de tanımlıysa path parametresi olarak kullanılır. +* Parametre **tekil bir type**'taysa (`int`, `float`, `str`, `bool` vb.), **query** parametresi olarak yorumlanır. +* Parametre bir **Pydantic model** type'ı olarak tanımlandıysa, request **body** olarak yorumlanır. + +/// note | Not + +FastAPI, `q` değerinin zorunlu olmadığını `= None` default değerinden anlayacaktır. + +`str | None` (Python 3.10+) veya `Union[str, None]` (Python 3.9+) içindeki `Union`, FastAPI tarafından bu değerin zorunlu olmadığını belirlemek için kullanılmaz; FastAPI bunun zorunlu olmadığını `= None` default değeri olduğu için bilir. + +Ancak type annotation'larını eklemek, editor'ünüzün size daha iyi destek vermesini ve hataları yakalamasını sağlar. + +/// + +## Pydantic olmadan { #without-pydantic } + +Pydantic modellerini kullanmak istemiyorsanız, **Body** parametrelerini de kullanabilirsiniz. [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} dokümanına bakın. diff --git a/docs/tr/docs/tutorial/cookie-param-models.md b/docs/tr/docs/tutorial/cookie-param-models.md new file mode 100644 index 000000000..a5bf51560 --- /dev/null +++ b/docs/tr/docs/tutorial/cookie-param-models.md @@ -0,0 +1,76 @@ +# Cookie Parameter Models { #cookie-parameter-models } + +Birbirleriyle ilişkili bir **cookie** grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Bu sayede **model'i yeniden kullanabilir**, **birden fazla yerde** tekrar tekrar kullanabilir ve tüm parametreler için validation ve metadata'yı tek seferde tanımlayabilirsiniz. + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. + +/// + +/// tip | İpucu + +Aynı teknik `Query`, `Cookie` ve `Header` için de geçerlidir. + +/// + +## Pydantic Model ile Cookies { #cookies-with-a-pydantic-model } + +İhtiyacınız olan **cookie** parametrelerini bir **Pydantic model** içinde tanımlayın ve ardından parametreyi `Cookie` olarak bildirin: + +{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} + +**FastAPI**, request ile gelen **cookies** içinden **her bir field** için veriyi **extract** eder ve size tanımladığınız Pydantic model'i verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Tanımlanan cookie'leri `/docs` altındaki docs UI'da görebilirsiniz: + +
+ +
+ +/// info | Bilgi + +Tarayıcıların cookie'leri özel biçimlerde ve arka planda yönetmesi nedeniyle, **JavaScript**'in cookie'lere erişmesine kolayca izin vermediğini aklınızda bulundurun. + +`/docs` altındaki **API docs UI**'a giderseniz, *path operation*'larınız için cookie'lerin **dokümantasyonunu** görebilirsiniz. + +Ancak verileri **doldurup** "Execute" düğmesine tıklasanız bile, docs UI **JavaScript** ile çalıştığı için cookie'ler gönderilmez; dolayısıyla hiç değer girmemişsiniz gibi bir **error** mesajı görürsünüz. + +/// + +## Fazladan Cookies'leri Yasaklayın { #forbid-extra-cookies } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değildir) almak istediğiniz cookie'leri **kısıtlamak** isteyebilirsiniz. + +API'niz artık kendi cookie consent'ını kontrol etme gücüne sahip. + +Pydantic'in model configuration'ını kullanarak `extra` olan herhangi bir field'ı `forbid` edebilirsiniz: + +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client **fazladan cookie** göndermeye çalışırsa, bir **error** response alır. + +Onayınızı almak için bunca çaba harcayan zavallı cookie banner'ları... API'nin bunu reddetmesi için. + +Örneğin client, değeri `good-list-please` olan bir `santa_tracker` cookie'si göndermeye çalışırsa, client `santa_tracker` cookie is not allowed diyen bir **error** response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "santa_tracker"], + "msg": "Extra inputs are not permitted", + "input": "good-list-please", + } + ] +} +``` + +## Özet { #summary } + +**FastAPI**'de **cookies** tanımlamak için **Pydantic model**'lerini kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/cors.md b/docs/tr/docs/tutorial/cors.md new file mode 100644 index 000000000..aae560022 --- /dev/null +++ b/docs/tr/docs/tutorial/cors.md @@ -0,0 +1,89 @@ +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } + +CORS veya "Cross-Origin Resource Sharing", tarayıcıda çalışan bir frontend’in JavaScript kodunun bir backend ile iletişim kurduğu ve backend’in frontend’den farklı bir "origin"de olduğu durumları ifade eder. + +## Origin { #origin } + +Origin; protocol (`http`, `https`), domain (`myapp.com`, `localhost`, `localhost.tiangolo.com`) ve port’un (`80`, `443`, `8080`) birleşimidir. + +Dolayısıyla şunların hepsi farklı origin’lerdir: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +Hepsi `localhost` üzerinde olsa bile, farklı protocol veya port kullandıkları için farklı "origin" sayılırlar. + +## Adımlar { #steps } + +Diyelim ki tarayıcınızda `http://localhost:8080` adresinde çalışan bir frontend’iniz var ve JavaScript’i, `http://localhost` adresinde çalışan bir backend ile iletişim kurmaya çalışıyor (port belirtmediğimiz için tarayıcı varsayılan port olan `80`’i kullanacaktır). + +Bu durumda tarayıcı, `:80`-backend’e bir HTTP `OPTIONS` request’i gönderir. Eğer backend, bu farklı origin’den (`http://localhost:8080`) gelen iletişimi yetkilendiren uygun header’ları gönderirse, `:8080`-tarayıcı frontend’deki JavaScript’in `:80`-backend’e request göndermesine izin verir. + +Bunu sağlayabilmek için `:80`-backend’in bir "allowed origins" listesi olmalıdır. + +Bu örnekte `:8080`-frontend’in doğru çalışması için listede `http://localhost:8080` bulunmalıdır. + +## Wildcard'lar { #wildcards } + +Listeyi `"*"` (bir "wildcard") olarak tanımlayıp, hepsine izin verildiğini söylemek de mümkündür. + +Ancak bu, credentials içeren her şeyi hariç tutarak yalnızca belirli iletişim türlerine izin verir: Cookie’ler, Bearer Token’larla kullanılanlar gibi Authorization header’ları, vb. + +Bu yüzden her şeyin düzgün çalışması için, izin verilen origin’leri açıkça belirtmek daha iyidir. + +## `CORSMiddleware` Kullanımı { #use-corsmiddleware } + +Bunu **FastAPI** uygulamanızda `CORSMiddleware` ile yapılandırabilirsiniz. + +* `CORSMiddleware`’i import edin. +* İzin verilen origin’lerden (string) oluşan bir liste oluşturun. +* Bunu **FastAPI** uygulamanıza bir "middleware" olarak ekleyin. + +Ayrıca backend’in şunlara izin verip vermediğini de belirtebilirsiniz: + +* Credentials (Authorization header’ları, Cookie’ler, vb). +* Belirli HTTP method’ları (`POST`, `PUT`) veya wildcard `"*"` ile hepsini. +* Belirli HTTP header’ları veya wildcard `"*"` ile hepsini. + +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} + + +`CORSMiddleware` implementasyonu tarafından kullanılan varsayılan parametreler kısıtlayıcıdır; bu nedenle tarayıcıların Cross-Domain bağlamında kullanmasına izin vermek için belirli origin’leri, method’ları veya header’ları açıkça etkinleştirmeniz gerekir. + +Aşağıdaki argümanlar desteklenir: + +* `allow_origins` - Cross-origin request yapmasına izin verilmesi gereken origin’lerin listesi. Örn. `['https://example.org', 'https://www.example.org']`. Herhangi bir origin’e izin vermek için `['*']` kullanabilirsiniz. +* `allow_origin_regex` - Cross-origin request yapmasına izin verilmesi gereken origin’lerle eşleşecek bir regex string’i. Örn. `'https://.*\.example\.org'`. +* `allow_methods` - Cross-origin request’lerde izin verilmesi gereken HTTP method’larının listesi. Varsayılanı `['GET']`. Tüm standart method’lara izin vermek için `['*']` kullanabilirsiniz. +* `allow_headers` - Cross-origin request’lerde desteklenmesi gereken HTTP request header’larının listesi. Varsayılanı `[]`. Tüm header’lara izin vermek için `['*']` kullanabilirsiniz. `Accept`, `Accept-Language`, `Content-Language` ve `Content-Type` header’larına basit CORS request'leri için her zaman izin verilir. +* `allow_credentials` - Cross-origin request’ler için cookie desteği olup olmayacağını belirtir. Varsayılanı `False`. + + `allow_credentials` `True` olarak ayarlanmışsa, `allow_origins`, `allow_methods` ve `allow_headers` değerlerinin hiçbiri `['*']` olamaz. Hepsinin açıkça belirtilmesi gerekir. + +* `expose_headers` - Tarayıcının erişebilmesi gereken response header’larını belirtir. Varsayılanı `[]`. +* `max_age` - Tarayıcıların CORS response’larını cache’lemesi için saniye cinsinden azami süreyi ayarlar. Varsayılanı `600`. + +Middleware iki özel HTTP request türüne yanıt verir... + +### CORS preflight request'leri { #cors-preflight-requests } + +Bunlar, `Origin` ve `Access-Control-Request-Method` header’larına sahip herhangi bir `OPTIONS` request’idir. + +Bu durumda middleware gelen request’i intercept eder ve uygun CORS header’larıyla yanıt verir; bilgilendirme amaçlı olarak da `200` veya `400` response döndürür. + +### Basit request'ler { #simple-requests } + +`Origin` header’ı olan herhangi bir request. Bu durumda middleware request’i normal şekilde geçirir, ancak response’a uygun CORS header’larını ekler. + +## Daha Fazla Bilgi { #more-info } + +CORS hakkında daha fazla bilgi için Mozilla CORS dokümantasyonuna bakın. + +/// note | Teknik Detaylar + +`from starlette.middleware.cors import CORSMiddleware` şeklinde de kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `fastapi.middleware` içinde bazı middleware’ler sağlar. Ancak mevcut middleware’lerin çoğu doğrudan Starlette’ten gelir. + +/// diff --git a/docs/tr/docs/tutorial/debugging.md b/docs/tr/docs/tutorial/debugging.md new file mode 100644 index 000000000..54d5c9252 --- /dev/null +++ b/docs/tr/docs/tutorial/debugging.md @@ -0,0 +1,113 @@ +# Debugging { #debugging } + +Visual Studio Code veya PyCharm gibi editörünüzde debugger'ı bağlayabilirsiniz. + +## `uvicorn`'ı Çağırma { #call-uvicorn } + +FastAPI uygulamanızda `uvicorn`'ı import edip doğrudan çalıştırın: + +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} + +### `__name__ == "__main__"` Hakkında { #about-name-main } + +`__name__ == "__main__"` ifadesinin temel amacı, dosyanız şu şekilde çağrıldığında çalışacak: + +
+ +```console +$ python myapp.py +``` + +
+ +ancak başka bir dosya onu import ettiğinde çalışmayacak bir kod bölümüne sahip olmaktır, örneğin: + +```Python +from myapp import app +``` + +#### Daha fazla detay { #more-details } + +Dosyanızın adının `myapp.py` olduğunu varsayalım. + +Şu şekilde çalıştırırsanız: + +
+ +```console +$ python myapp.py +``` + +
+ +Python tarafından otomatik oluşturulan, dosyanızın içindeki `__name__` adlı dahili değişkenin değeri `"__main__"` string'i olur. + +Dolayısıyla şu bölüm: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +çalışır. + +--- + +Ancak modülü (dosyayı) import ederseniz bu gerçekleşmez. + +Yani örneğin `importer.py` adında başka bir dosyanız var ve içinde şunlar bulunuyorsa: + +```Python +from myapp import app + +# Some more code +``` + +bu durumda `myapp.py` içindeki otomatik oluşturulan `__name__` değişkeni `"__main__"` değerine sahip olmaz. + +Bu yüzden şu satır: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +çalıştırılmaz. + +/// info | Bilgi + +Daha fazla bilgi için resmi Python dokümantasyonuna bakın. + +/// + +## Kodunuzu Debugger ile Çalıştırma { #run-your-code-with-your-debugger } + +Uvicorn server'ını doğrudan kodunuzdan çalıştırdığınız için, Python programınızı (FastAPI uygulamanızı) debugger'dan doğrudan başlatabilirsiniz. + +--- + +Örneğin Visual Studio Code'da şunları yapabilirsiniz: + +* "Debug" paneline gidin. +* "Add configuration..." seçin. +* "Python" seçin +* "`Python: Current File (Integrated Terminal)`" seçeneğiyle debugger'ı çalıştırın. + +Böylece server, **FastAPI** kodunuzla başlar; breakpoint'lerinizde durur vb. + +Aşağıdaki gibi görünebilir: + + + +--- + +PyCharm kullanıyorsanız şunları yapabilirsiniz: + +* "Run" menüsünü açın. +* "Debug..." seçeneğini seçin. +* Bir context menü açılır. +* Debug edilecek dosyayı seçin (bu örnekte `main.py`). + +Böylece server, **FastAPI** kodunuzla başlar; breakpoint'lerinizde durur vb. + +Aşağıdaki gibi görünebilir: + + diff --git a/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..9ee57cb29 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,288 @@ +# Dependency Olarak Class'lar { #classes-as-dependencies } + +**Dependency Injection** sistemine daha derinlemesine geçmeden önce, bir önceki örneği geliştirelim. + +## Önceki Örnekten Bir `dict` { #a-dict-from-the-previous-example } + +Önceki örnekte, dependency'mizden ("dependable") bir `dict` döndürüyorduk: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} + +Ama sonra *path operation function* içindeki `commons` parametresinde bir `dict` alıyoruz. + +Ve biliyoruz ki editor'ler `dict`'ler için çok fazla destek (ör. completion) veremez; çünkü key'lerini ve value type'larını bilemezler. + +Daha iyisini yapabiliriz... + +## Bir Şeyi Dependency Yapan Nedir { #what-makes-a-dependency } + +Şimdiye kadar dependency'leri function olarak tanımlanmış şekilde gördünüz. + +Ancak dependency tanımlamanın tek yolu bu değil (muhtemelen en yaygını bu olsa da). + +Buradaki kritik nokta, bir dependency'nin "callable" olması gerektiğidir. + +Python'da "**callable**", Python'ın bir function gibi "çağırabildiği" her şeydir. + +Yani elinizde `something` adlı bir nesne varsa (function _olmak zorunda değil_) ve onu şöyle "çağırabiliyorsanız" (çalıştırabiliyorsanız): + +```Python +something() +``` + +veya + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +o zaman bu bir "callable" demektir. + +## Dependency Olarak Class'lar { #classes-as-dependencies_1 } + +Python'da bir class'tan instance oluştururken de aynı söz dizimini kullandığınızı fark etmiş olabilirsiniz. + +Örneğin: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +Bu durumda `fluffy`, `Cat` class'ının bir instance'ıdır. + +Ve `fluffy` oluşturmak için `Cat`'i "çağırmış" olursunuz. + +Dolayısıyla bir Python class'ı da bir **callable**'dır. + +O zaman **FastAPI** içinde bir Python class'ını dependency olarak kullanabilirsiniz. + +FastAPI'nin aslında kontrol ettiği şey, bunun bir "callable" olması (function, class ya da başka bir şey) ve tanımlı parametreleridir. + +Eğer **FastAPI**'de bir dependency olarak bir "callable" verirseniz, FastAPI o "callable" için parametreleri analiz eder ve bunları *path operation function* parametreleriyle aynı şekilde işler. Sub-dependency'ler dahil. + +Bu, hiç parametresi olmayan callable'lar için de geçerlidir. Tıpkı hiç parametresi olmayan *path operation function*'larda olduğu gibi. + +O zaman yukarıdaki `common_parameters` adlı "dependable" dependency'sini `CommonQueryParams` class'ına çevirebiliriz: + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} + +Class instance'ını oluşturmak için kullanılan `__init__` metoduna dikkat edin: + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} + +...bizim önceki `common_parameters` ile aynı parametrelere sahip: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} + +Bu parametreler, dependency'yi "çözmek" için **FastAPI**'nin kullanacağı şeylerdir. + +Her iki durumda da şunlar olacak: + +* `str` olan opsiyonel bir `q` query parametresi. +* Default değeri `0` olan `int` tipinde bir `skip` query parametresi. +* Default değeri `100` olan `int` tipinde bir `limit` query parametresi. + +Her iki durumda da veriler dönüştürülecek, doğrulanacak, OpenAPI şemasında dokümante edilecek, vb. + +## Kullanalım { #use-it } + +Artık bu class'ı kullanarak dependency'nizi tanımlayabilirsiniz. + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} + +**FastAPI**, `CommonQueryParams` class'ını çağırır. Bu, o class'ın bir "instance"ını oluşturur ve bu instance, sizin function'ınıza `commons` parametresi olarak geçirilir. + +## Type Annotation vs `Depends` { #type-annotation-vs-depends } + +Yukarıdaki kodda `CommonQueryParams`'ı iki kez yazdığımıza dikkat edin: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +Şuradaki son `CommonQueryParams`: + +```Python +... Depends(CommonQueryParams) +``` + +...FastAPI'nin dependency'nin ne olduğunu anlamak için gerçekten kullandığı şeydir. + +FastAPI tanımlanan parametreleri buradan çıkarır ve aslında çağıracağı şey de budur. + +--- + +Bu durumda, şuradaki ilk `CommonQueryParams`: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams ... +``` + +//// + +...**FastAPI** için özel bir anlam taşımaz. FastAPI bunu veri dönüştürme, doğrulama vb. için kullanmaz (çünkü bunlar için `Depends(CommonQueryParams)` kullanıyor). + +Hatta şunu bile yazabilirsiniz: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons = Depends(CommonQueryParams) +``` + +//// + +...şu örnekte olduğu gibi: + +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} + +Ancak type'ı belirtmeniz önerilir; böylece editor'ünüz `commons` parametresine ne geçirileceğini bilir ve size code completion, type check'leri vb. konularda yardımcı olur: + + + +## Kısayol { #shortcut } + +Ama burada bir miktar kod tekrarımız olduğunu görüyorsunuz; `CommonQueryParams`'ı iki kez yazıyoruz: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +**FastAPI**, bu durumlar için bir kısayol sağlar: dependency'nin *özellikle* FastAPI'nin bir instance oluşturmak için "çağıracağı" bir class olduğu durumlar. + +Bu özel durumlarda şunu yapabilirsiniz: + +Şunu yazmak yerine: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +...şunu yazarsınız: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends() +``` + +//// + +Dependency'yi parametrenin type'ı olarak tanımlarsınız ve `Depends(CommonQueryParams)` içinde class'ı *yeniden* yazmak yerine, parametre vermeden `Depends()` kullanırsınız. + +Aynı örnek şu hale gelir: + +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} + +...ve **FastAPI** ne yapması gerektiğini bilir. + +/// tip | İpucu + +Bu size faydalı olmaktan çok kafa karıştırıcı geliyorsa, kullanmayın; buna *ihtiyacınız* yok. + +Bu sadece bir kısayoldur. Çünkü **FastAPI** kod tekrarını en aza indirmenize yardımcı olmayı önemser. + +/// diff --git a/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 000000000..4903aec4a --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,69 @@ +# Path Operation Decorator'lerinde Dependency'ler { #dependencies-in-path-operation-decorators } + +Bazı durumlarda bir dependency'nin döndürdüğü değere *path operation function* içinde gerçekten ihtiyacınız olmaz. + +Ya da dependency zaten bir değer döndürmüyordur. + +Ancak yine de çalıştırılmasını/çözülmesini istersiniz. + +Bu gibi durumlarda, `Depends` ile bir *path operation function* parametresi tanımlamak yerine, *path operation decorator*'üne `dependencies` adında bir `list` ekleyebilirsiniz. + +## *Path Operation Decorator*'üne `dependencies` Ekleyin { #add-dependencies-to-the-path-operation-decorator } + +*Path operation decorator*, opsiyonel bir `dependencies` argümanı alır. + +Bu, `Depends()` öğelerinden oluşan bir `list` olmalıdır: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *} + +Bu dependency'ler normal dependency'lerle aynı şekilde çalıştırılır/çözülür. Ancak (eğer bir değer döndürüyorlarsa) bu değer *path operation function*'ınıza aktarılmaz. + +/// tip | İpucu + +Bazı editörler, kullanılmayan function parametrelerini kontrol eder ve bunları hata olarak gösterebilir. + +Bu `dependencies` yaklaşımıyla, editör/araç hatalarına takılmadan dependency'lerin çalıştırılmasını sağlayabilirsiniz. + +Ayrıca kodunuzda kullanılmayan bir parametreyi gören yeni geliştiricilerin bunun gereksiz olduğunu düşünmesi gibi bir kafa karışıklığını da azaltabilir. + +/// + +/// info | Bilgi + +Bu örnekte uydurma özel header'lar olan `X-Key` ve `X-Token` kullanıyoruz. + +Ancak gerçek senaryolarda, security uygularken, entegre [Security yardımcı araçlarını (bir sonraki bölüm)](../security/index.md){.internal-link target=_blank} kullanmak size daha fazla fayda sağlar. + +/// + +## Dependency Hataları ve Return Değerleri { #dependencies-errors-and-return-values } + +Normalde kullandığınız aynı dependency *function*'larını burada da kullanabilirsiniz. + +### Dependency Gereksinimleri { #dependency-requirements } + +Request gereksinimleri (header'lar gibi) veya başka alt dependency'ler tanımlayabilirler: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} + +### Exception Fırlatmak { #raise-exceptions } + +Bu dependency'ler, normal dependency'lerde olduğu gibi `raise` ile exception fırlatabilir: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} + +### Return Değerleri { #return-values } + +Ayrıca değer döndürebilirler ya da döndürmeyebilirler; dönen değer kullanılmayacaktır. + +Yani başka bir yerde zaten kullandığınız, değer döndüren normal bir dependency'yi tekrar kullanabilirsiniz; değer kullanılmasa bile dependency çalıştırılacaktır: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} + +## Bir *Path Operation* Grubu İçin Dependency'ler { #dependencies-for-a-group-of-path-operations } + +Daha sonra, muhtemelen birden fazla dosya kullanarak daha büyük uygulamaları nasıl yapılandıracağınızı okurken ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), bir *path operation* grubu için tek bir `dependencies` parametresini nasıl tanımlayacağınızı öğreneceksiniz. + +## Global Dependency'ler { #global-dependencies } + +Sırada, dependency'leri tüm `FastAPI` uygulamasına nasıl ekleyeceğimizi göreceğiz; böylece her *path operation* için geçerli olacaklar. diff --git a/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..bd025f799 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,289 @@ +# `yield` ile Dependency'ler { #dependencies-with-yield } + +FastAPI, işini bitirdikten sonra ek adımlar çalıştıran dependency'leri destekler. + +Bunu yapmak için `return` yerine `yield` kullanın ve ek adımları (kodu) `yield` satırından sonra yazın. + +/// tip | İpucu + +Her dependency için yalnızca **bir kez** `yield` kullandığınızdan emin olun. + +/// + +/// note | Teknik Detaylar + +Şunlarla kullanılabilen herhangi bir fonksiyon: + +* `@contextlib.contextmanager` veya +* `@contextlib.asynccontextmanager` + +bir **FastAPI** dependency'si olarak kullanılabilir. + +Hatta FastAPI bu iki decorator'ı içeride (internally) kullanır. + +/// + +## `yield` ile Bir Veritabanı Dependency'si { #a-database-dependency-with-yield } + +Örneğin bunu, bir veritabanı session'ı oluşturmak ve iş bittikten sonra kapatmak için kullanabilirsiniz. + +Response oluşturulmadan önce yalnızca `yield` satırına kadar olan (ve `yield` dahil) kod çalıştırılır: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} + +`yield` edilen değer, *path operation*'lara ve diğer dependency'lere enjekte edilen (injected) değerdir: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} + +Response'dan sonra `yield` satırını takip eden kod çalıştırılır: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} + +/// tip | İpucu + +`async` ya da normal fonksiyonlar kullanabilirsiniz. + +**FastAPI**, normal dependency'lerde olduğu gibi her ikisinde de doğru şekilde davranır. + +/// + +## `yield` ve `try` ile Bir Dependency { #a-dependency-with-yield-and-try } + +`yield` kullanan bir dependency içinde bir `try` bloğu kullanırsanız, dependency kullanılırken fırlatılan (raised) herhangi bir exception'ı alırsınız. + +Örneğin, başka bir dependency'de veya bir *path operation* içinde çalışan bir kod, bir veritabanı transaction'ını "rollback" yaptıysa ya da başka bir exception oluşturduysa, o exception dependency'nizde size gelir. + +Dolayısıyla `except SomeException` ile dependency içinde o spesifik exception'ı yakalayabilirsiniz. + +Aynı şekilde, exception olsun ya da olmasın çıkış (exit) adımlarının çalıştırılmasını garanti etmek için `finally` kullanabilirsiniz. + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} + +## `yield` ile Alt Dependency'ler { #sub-dependencies-with-yield } + +Her boyutta ve şekilde alt dependency'ler ve alt dependency "ağaçları" (trees) oluşturabilirsiniz; bunların herhangi biri veya hepsi `yield` kullanabilir. + +**FastAPI**, `yield` kullanan her dependency'deki "exit code"'un doğru sırayla çalıştırılmasını sağlar. + +Örneğin, `dependency_c`, `dependency_b`'ye; `dependency_b` de `dependency_a`'ya bağlı olabilir: + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} + +Ve hepsi `yield` kullanabilir. + +Bu durumda `dependency_c`, exit code'unu çalıştırabilmek için `dependency_b`'den gelen değerin (burada `dep_b`) hâlâ erişilebilir olmasına ihtiyaç duyar. + +Aynı şekilde `dependency_b` de exit code'u için `dependency_a`'dan gelen değerin (burada `dep_a`) erişilebilir olmasına ihtiyaç duyar. + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} + +Benzer şekilde, bazı dependency'ler `yield`, bazıları `return` kullanabilir ve bunların bazıları diğerlerine bağlı olabilir. + +Ayrıca birden fazla `yield` kullanan dependency gerektiren tek bir dependency'niz de olabilir, vb. + +İstediğiniz herhangi bir dependency kombinasyonunu kullanabilirsiniz. + +**FastAPI** her şeyin doğru sırada çalışmasını sağlar. + +/// note | Teknik Detaylar + +Bu, Python'un Context Managers yapısı sayesinde çalışır. + +**FastAPI** bunu sağlamak için içeride onları kullanır. + +/// + +## `yield` ve `HTTPException` ile Dependency'ler { #dependencies-with-yield-and-httpexception } + +`yield` kullanan dependency'lerde `try` bloklarıyla bazı kodları çalıştırıp ardından `finally` sonrasında exit code çalıştırabileceğinizi gördünüz. + +Ayrıca `except` ile fırlatılan exception'ı yakalayıp onunla bir şey yapabilirsiniz. + +Örneğin `HTTPException` gibi farklı bir exception fırlatabilirsiniz. + +/// tip | İpucu + +Bu biraz ileri seviye bir tekniktir ve çoğu durumda gerçekten ihtiyaç duymazsınız; çünkü exception'ları (`HTTPException` dahil) uygulamanızın geri kalan kodundan, örneğin *path operation function* içinden fırlatabilirsiniz. + +Ama ihtiyaç duyarsanız diye burada. 🤓 + +/// + +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} + +Exception yakalayıp buna göre özel bir response oluşturmak istiyorsanız bir [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} oluşturun. + +## `yield` ve `except` ile Dependency'ler { #dependencies-with-yield-and-except } + +`yield` kullanan bir dependency içinde `except` ile bir exception yakalar ve bunu tekrar fırlatmazsanız (ya da yeni bir exception fırlatmazsanız), FastAPI normal Python'da olduğu gibi bir exception olduğunu fark edemez: + +{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} + +Bu durumda client, biz `HTTPException` veya benzeri bir şey fırlatmadığımız için olması gerektiği gibi *HTTP 500 Internal Server Error* response'u görür; ancak server **hiç log üretmez** ve hatanın ne olduğuna dair başka bir işaret de olmaz. 😱 + +### `yield` ve `except` Kullanan Dependency'lerde Her Zaman `raise` Edin { #always-raise-in-dependencies-with-yield-and-except } + +`yield` kullanan bir dependency içinde bir exception yakalarsanız, başka bir `HTTPException` veya benzeri bir şey fırlatmıyorsanız, **orijinal exception'ı tekrar raise etmelisiniz**. + +Aynı exception'ı `raise` ile tekrar fırlatabilirsiniz: + +{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} + +Artık client yine aynı *HTTP 500 Internal Server Error* response'unu alır, ama server log'larda bizim özel `InternalError`'ımızı görür. 😎 + +## `yield` Kullanan Dependency'lerin Çalıştırılması { #execution-of-dependencies-with-yield } + +Çalıştırma sırası kabaca aşağıdaki diyagramdaki gibidir. Zaman yukarıdan aşağı akar. Her sütun, etkileşime giren veya kod çalıştıran parçalardan birini temsil eder. + +```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,operation: Can raise exceptions, including HTTPException + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise Exception + dep -->> handler: Raise Exception + handler -->> client: HTTP error response + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise Exception (e.g. HTTPException) + opt handle + dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception + end + handler -->> client: HTTP error response + 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 -->> tasks: Handle exceptions in the background task code + end +``` + +/// info | Bilgi + +Client'a yalnızca **tek bir response** gönderilir. Bu, error response'lardan biri olabilir ya da *path operation*'dan dönen response olabilir. + +Bu response'lardan biri gönderildikten sonra başka bir response gönderilemez. + +/// + +/// tip | İpucu + +*Path operation function* içindeki koddan herhangi bir exception raise ederseniz, `HTTPException` dahil olmak üzere bu exception `yield` kullanan dependency'lere aktarılır. Çoğu durumda, doğru şekilde ele alındığından emin olmak için `yield` kullanan dependency'den aynı exception'ı (veya yeni bir tanesini) yeniden raise etmek istersiniz. + +/// + +## Erken Çıkış ve `scope` { #early-exit-and-scope } + +Normalde `yield` kullanan dependency'lerin exit code'u, client'a response gönderildikten **sonra** çalıştırılır. + +Ama *path operation function*'dan döndükten sonra dependency'yi kullanmayacağınızı biliyorsanız, `Depends(scope="function")` kullanarak FastAPI'ye dependency'yi *path operation function* döndükten sonra kapatmasını, ancak **response gönderilmeden önce** kapatmasını söyleyebilirsiniz. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` şu değerleri alabilen bir `scope` parametresi alır: + +* `"function"`: dependency'yi request'i işleyen *path operation function* çalışmadan önce başlat, *path operation function* bittikten sonra bitir, ancak response client'a geri gönderilmeden **önce** sonlandır. Yani dependency fonksiyonu, *path operation **function***'ın **etrafında** çalıştırılır. +* `"request"`: dependency'yi request'i işleyen *path operation function* çalışmadan önce başlat (`"function"` kullanımına benzer), ancak response client'a geri gönderildikten **sonra** sonlandır. Yani dependency fonksiyonu, **request** ve response döngüsünün **etrafında** çalıştırılır. + +Belirtilmezse ve dependency `yield` kullanıyorsa, varsayılan olarak `scope` `"request"` olur. + +### Alt dependency'ler için `scope` { #scope-for-sub-dependencies } + +`scope="request"` (varsayılan) ile bir dependency tanımladığınızda, herhangi bir alt dependency'nin `scope` değeri de `"request"` olmalıdır. + +Ancak `scope` değeri `"function"` olan bir dependency, hem `"function"` hem de `"request"` scope'una sahip dependency'lere bağlı olabilir. + +Bunun nedeni, bir dependency'nin exit code'unu alt dependency'lerden önce çalıştırabilmesi gerekmesidir; çünkü exit code sırasında hâlâ onları kullanması gerekebilir. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> 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 +``` + +## `yield`, `HTTPException`, `except` ve Background Tasks ile Dependency'ler { #dependencies-with-yield-httpexception-except-and-background-tasks } + +`yield` kullanan dependency'ler, zaman içinde farklı kullanım senaryolarını kapsamak ve bazı sorunları düzeltmek için gelişti. + +FastAPI'nin farklı sürümlerinde nelerin değiştiğini görmek isterseniz, advanced guide'da şu bölümü okuyabilirsiniz: [Advanced Dependencies - Dependencies with `yield`, `HTTPException`, `except` and Background Tasks](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}. + +## Context Managers { #context-managers } + +### "Context Managers" Nedir? { #what-are-context-managers } + +"Context Managers", `with` ifadesiyle kullanabildiğiniz Python nesneleridir. + +Örneğin, bir dosyayı okumak için `with` kullanabilirsiniz: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +Temelde `open("./somefile.txt")`, "Context Manager" olarak adlandırılan bir nesne oluşturur. + +`with` bloğu bittiğinde, exception olsa bile dosyanın kapatılmasını garanti eder. + +`yield` ile bir dependency oluşturduğunuzda, **FastAPI** içeride bunun için bir context manager oluşturur ve bazı ilgili başka araçlarla birleştirir. + +### `yield` kullanan dependency'lerde context manager kullanma { #using-context-managers-in-dependencies-with-yield } + +/// warning | Uyarı + +Bu, az çok "ileri seviye" bir fikirdir. + +**FastAPI**'ye yeni başlıyorsanız şimdilik bunu atlamak isteyebilirsiniz. + +/// + +Python'da Context Manager'ları, iki method'a sahip bir class oluşturarak: `__enter__()` ve `__exit__()` yaratabilirsiniz. + +Ayrıca dependency fonksiyonunun içinde `with` veya `async with` ifadeleri kullanarak **FastAPI**'de `yield` kullanan dependency'lerin içinde de kullanabilirsiniz: + +{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *} + +/// tip | İpucu + +Bir context manager oluşturmanın başka bir yolu da şunlardır: + +* `@contextlib.contextmanager` veya +* `@contextlib.asynccontextmanager` + +Bunları, tek bir `yield` içeren bir fonksiyonu decorate etmek için kullanabilirsiniz. + +FastAPI, `yield` kullanan dependency'ler için içeride bunu yapar. + +Ancak FastAPI dependency'leri için bu decorator'ları kullanmak zorunda değilsiniz (hatta kullanmamalısınız). + +FastAPI bunu sizin yerinize içeride yapar. + +/// diff --git a/docs/tr/docs/tutorial/dependencies/global-dependencies.md b/docs/tr/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 000000000..7f0025eaf --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,16 @@ +# Global Dependencies { #global-dependencies } + +Bazı uygulama türlerinde, tüm uygulama için dependency eklemek isteyebilirsiniz. + +[`dependencies`'i *path operation decorator*'larına ekleyebildiğiniz](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} gibi, `FastAPI` uygulamasına da ekleyebilirsiniz. + +Bu durumda, uygulamadaki tüm *path operation*'lara uygulanırlar: + +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} + + +Ve [*path operation decorator*'larına `dependencies` ekleme](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} bölümündeki tüm fikirler hâlâ geçerlidir; ancak bu sefer, uygulamadaki tüm *path operation*'lar için geçerli olur. + +## *Path operations* grupları için Dependencies { #dependencies-for-groups-of-path-operations } + +İleride, daha büyük uygulamaları nasıl yapılandıracağınızı ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}) okurken, muhtemelen birden fazla dosyayla birlikte, bir *path operations* grubu için tek bir `dependencies` parametresini nasıl tanımlayacağınızı öğreneceksiniz. diff --git a/docs/tr/docs/tutorial/dependencies/index.md b/docs/tr/docs/tutorial/dependencies/index.md new file mode 100644 index 000000000..f1e446d67 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/index.md @@ -0,0 +1,250 @@ +# Bağımlılıklar { #dependencies } + +**FastAPI**, çok güçlü ama aynı zamanda sezgisel bir **Dependency Injection** sistemine sahiptir. + +Kullanımı çok basit olacak şekilde tasarlanmıştır ve herhangi bir geliştiricinin diğer bileşenleri **FastAPI** ile entegre etmesini kolaylaştırır. + +## "Dependency Injection" Nedir? { #what-is-dependency-injection } + +Programlamada **"Dependency Injection"**, kodunuzun (bu örnekte *path operation function*'larınızın) çalışmak ve kullanmak için ihtiyaç duyduğu şeyleri: "dependencies" (bağımlılıklar) olarak beyan edebilmesi anlamına gelir. + +Ardından bu sistem (bu örnekte **FastAPI**), kodunuza gerekli bağımlılıkları sağlamak ("inject" etmek) için gereken her şeyi sizin yerinize halleder. + +Bu yaklaşım, şunlara ihtiyaç duyduğunuzda özellikle faydalıdır: + +* Paylaşılan bir mantığa sahip olmak (aynı kod mantığını tekrar tekrar kullanmak). +* Veritabanı bağlantılarını paylaşmak. +* Güvenlik, authentication, rol gereksinimleri vb. kuralları zorunlu kılmak. +* Ve daha birçok şey... + +Tüm bunları, kod tekrarını minimumda tutarak yaparsınız. + +## İlk Adımlar { #first-steps } + +Çok basit bir örneğe bakalım. Şimdilik o kadar basit olacak ki pek işe yaramayacak. + +Ama bu sayede **Dependency Injection** sisteminin nasıl çalıştığına odaklanabiliriz. + +### Bir dependency (bağımlılık) veya "dependable" Oluşturun { #create-a-dependency-or-dependable } + +Önce dependency'e odaklanalım. + +Bu, bir *path operation function*'ın alabileceği parametrelerin aynısını alabilen basit bir fonksiyondur: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} + +Bu kadar. + +**2 satır**. + +Ve tüm *path operation function*'larınızla aynı şekle ve yapıya sahiptir. + +Bunu, "decorator" olmadan (yani `@app.get("/some-path")` olmadan) yazılmış bir *path operation function* gibi düşünebilirsiniz. + +Ayrıca istediğiniz herhangi bir şeyi döndürebilir. + +Bu örnekte, bu dependency şunları bekler: + +* `str` olan, opsiyonel bir query parametresi `q`. +* `int` olan, opsiyonel bir query parametresi `skip` ve varsayılanı `0`. +* `int` olan, opsiyonel bir query parametresi `limit` ve varsayılanı `100`. + +Sonra da bu değerleri içeren bir `dict` döndürür. + +/// info | Bilgi + +FastAPI, `Annotated` desteğini 0.95.0 sürümünde ekledi (ve önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız `Annotated` kullanmaya çalıştığınızda hata alırsınız. + +`Annotated` kullanmadan önce **FastAPI** sürümünü en az 0.95.1'e yükseltmek için [FastAPI sürümünü yükseltin](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +### `Depends`'i Import Edin { #import-depends } + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} + +### "Dependant" İçinde Dependency'yi Tanımlayın { #declare-the-dependency-in-the-dependant } + +*Path operation function* parametrelerinizde `Body`, `Query` vb. kullandığınız gibi, yeni bir parametreyle `Depends` kullanın: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} + +Fonksiyon parametrelerinde `Depends`'i `Body`, `Query` vb. ile aynı şekilde kullansanız da `Depends` biraz farklı çalışır. + +`Depends`'e yalnızca tek bir parametre verirsiniz. + +Bu parametre, bir fonksiyon gibi bir şey olmalıdır. + +Onu doğrudan **çağırmazsınız** (sonuna parantez eklemezsiniz), sadece `Depends()`'e parametre olarak verirsiniz. + +Ve bu fonksiyon da, *path operation function*'lar gibi parametre alır. + +/// tip | İpucu + +Fonksiyonların dışında başka hangi "şeylerin" dependency olarak kullanılabildiğini bir sonraki bölümde göreceksiniz. + +/// + +Yeni bir request geldiğinde, **FastAPI** şunları sizin yerinize yapar: + +* Dependency ("dependable") fonksiyonunuzu doğru parametrelerle çağırır. +* Fonksiyonunuzun sonucunu alır. +* Bu sonucu *path operation function*'ınızdaki parametreye atar. + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +Bu şekilde paylaşılan kodu bir kez yazarsınız ve onu *path operation*'larda çağırma işini **FastAPI** halleder. + +/// check | Ek bilgi + +Dikkat edin: Bunu "register" etmek ya da benzeri bir şey yapmak için özel bir class oluşturup **FastAPI**'ye bir yere geçirmeniz gerekmez. + +Sadece `Depends`'e verirsiniz ve gerisini **FastAPI** nasıl yapacağını bilir. + +/// + +## `Annotated` Dependency'lerini Paylaşın { #share-annotated-dependencies } + +Yukarıdaki örneklerde, ufak bir **kod tekrarı** olduğunu görüyorsunuz. + +`common_parameters()` dependency'sini kullanmanız gerektiğinde, type annotation ve `Depends()` içeren parametrenin tamamını yazmanız gerekir: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +Ancak `Annotated` kullandığımız için bu `Annotated` değerini bir değişkende saklayıp birden fazla yerde kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} + +/// tip | İpucu + +Bu aslında standart Python'dır; buna "type alias" denir ve **FastAPI**'ye özel bir şey değildir. + +Ama **FastAPI**, `Annotated` dahil Python standartları üzerine kurulu olduğu için bu tekniği kodunuzda kullanabilirsiniz. 😎 + +/// + +Dependency'ler beklediğiniz gibi çalışmaya devam eder ve **en güzel kısmı** da şudur: **type bilgisi korunur**. Bu da editörünüzün size **autocompletion**, **inline errors** vb. sağlamaya devam edeceği anlamına gelir. `mypy` gibi diğer araçlar için de aynısı geçerlidir. + +Bu özellikle, **büyük bir kod tabanında**, aynı dependency'leri **birçok *path operation*** içinde tekrar tekrar kullandığınızda çok faydalı olacaktır. + +## `async` Olsa da Olmasa da { #to-async-or-not-to-async } + +Dependency'ler de **FastAPI** tarafından çağrılacağı için (tıpkı *path operation function*'larınız gibi), fonksiyonları tanımlarken aynı kurallar geçerlidir. + +`async def` ya da normal `def` kullanabilirsiniz. + +Ayrıca normal `def` *path operation function*'ları içinde `async def` dependency tanımlayabilir veya `async def` *path operation function*'ları içinde `def` dependency kullanabilirsiniz vb. + +Fark etmez. **FastAPI** ne yapacağını bilir. + +/// note | Not + +Eğer bilmiyorsanız, dokümanlarda `async` ve `await` için [Async: *"In a hurry?"*](../../async.md#in-a-hurry){.internal-link target=_blank} bölümüne bakın. + +/// + +## OpenAPI ile Entegre { #integrated-with-openapi } + +Dependency'lerinizin (ve alt dependency'lerin) tüm request tanımları, doğrulamaları ve gereksinimleri aynı OpenAPI şemasına entegre edilir. + +Bu nedenle interaktif dokümanlar, bu dependency'lerden gelen tüm bilgileri de içerir: + + + +## Basit Kullanım { #simple-usage } + +Şöyle düşünürseniz: *Path operation function*'lar, bir *path* ve *operation* eşleştiğinde kullanılacak şekilde tanımlanır; ardından **FastAPI** fonksiyonu doğru parametrelerle çağırır ve request'ten veriyi çıkarır. + +Aslında tüm (veya çoğu) web framework'ü de aynı şekilde çalışır. + +Bu fonksiyonları hiçbir zaman doğrudan çağırmazsınız. Onları framework'ünüz (bu örnekte **FastAPI**) çağırır. + +Dependency Injection sistemiyle, *path operation function*'ınızın, ondan önce çalıştırılması gereken başka bir şeye de "bağlı" olduğunu **FastAPI**'ye söyleyebilirsiniz; **FastAPI** bunu çalıştırır ve sonuçları "inject" eder. + +Aynı "dependency injection" fikri için kullanılan diğer yaygın terimler: + +* resources +* providers +* services +* injectables +* components + +## **FastAPI** Plug-in'leri { #fastapi-plug-ins } + +Entegrasyonlar ve "plug-in"ler **Dependency Injection** sistemi kullanılarak inşa edilebilir. Ancak aslında **"plug-in" oluşturmanıza gerek yoktur**; çünkü dependency'leri kullanarak *path operation function*'larınıza sunulabilecek sınırsız sayıda entegrasyon ve etkileşim tanımlayabilirsiniz. + +Dependency'ler, çok basit ve sezgisel bir şekilde oluşturulabilir. Böylece ihtiyacınız olan Python package'larını import edip, API fonksiyonlarınızla birkaç satır kodla *kelimenin tam anlamıyla* entegre edebilirsiniz. + +İlerleyen bölümlerde ilişkisel ve NoSQL veritabanları, güvenlik vb. konularda bunun örneklerini göreceksiniz. + +## **FastAPI** Uyumluluğu { #fastapi-compatibility } + +Dependency injection sisteminin sadeliği, **FastAPI**'yi şunlarla uyumlu hale getirir: + +* tüm ilişkisel veritabanları +* NoSQL veritabanları +* harici paketler +* harici API'ler +* authentication ve authorization sistemleri +* API kullanım izleme (monitoring) sistemleri +* response verisi injection sistemleri +* vb. + +## Basit ve Güçlü { #simple-and-powerful } + +Hiyerarşik dependency injection sistemi tanımlamak ve kullanmak çok basit olsa da, hâlâ oldukça güçlüdür. + +Kendileri de dependency tanımlayabilen dependency'ler tanımlayabilirsiniz. + +Sonuçta hiyerarşik bir dependency ağacı oluşur ve **Dependency Injection** sistemi tüm bu dependency'leri (ve alt dependency'lerini) sizin için çözer ve her adımda sonuçları sağlar ("inject" eder). + +Örneğin, 4 API endpoint'iniz (*path operation*) olduğunu varsayalım: + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +O zaman her biri için farklı izin gereksinimlerini yalnızca dependency'ler ve alt dependency'lerle ekleyebilirsiniz: + +```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 +``` + +## **OpenAPI** ile Entegre { #integrated-with-openapi_1 } + +Bu dependency'lerin tümü, gereksinimlerini beyan ederken aynı zamanda *path operation*'larınıza parametreler, doğrulamalar vb. da ekler. + +**FastAPI**, bunların hepsini OpenAPI şemasına eklemekle ilgilenir; böylece interaktif dokümantasyon sistemlerinde gösterilir. diff --git a/docs/tr/docs/tutorial/dependencies/sub-dependencies.md b/docs/tr/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 000000000..184db839b --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,105 @@ +# Alt Bağımlılıklar { #sub-dependencies } + +**Alt bağımlılıkları** olan bağımlılıklar oluşturabilirsiniz. + +İhtiyacınız olduğu kadar **derine** gidebilirler. + +Bunları çözme işini **FastAPI** üstlenir. + +## İlk bağımlılık "dependable" { #first-dependency-dependable } + +Şöyle bir ilk bağımlılık ("dependable") oluşturabilirsiniz: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} + +Burada `q` adında opsiyonel bir query parametresi `str` olarak tanımlanır ve sonra doğrudan geri döndürülür. + +Bu oldukça basit (pek faydalı değil), ama alt bağımlılıkların nasıl çalıştığına odaklanmamıza yardımcı olacak. + +## İkinci bağımlılık: "dependable" ve "dependant" { #second-dependency-dependable-and-dependant } + +Ardından, aynı zamanda kendi içinde bir bağımlılık da tanımlayan başka bir bağımlılık fonksiyonu (bir "dependable") oluşturabilirsiniz (yani o da bir "dependant" olur): + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} + +Tanımlanan parametrelere odaklanalım: + +* Bu fonksiyonun kendisi bir bağımlılık ("dependable") olmasına rağmen, ayrıca başka bir bağımlılık daha tanımlar (başka bir şeye "depends" olur). + * `query_extractor`'a bağlıdır ve ondan dönen değeri `q` parametresine atar. +* Ayrıca `last_query` adında opsiyonel bir cookie'yi `str` olarak tanımlar. + * Kullanıcı herhangi bir query `q` sağlamadıysa, daha önce cookie'ye kaydettiğimiz en son kullanılan query'yi kullanırız. + +## Bağımlılığı Kullanma { #use-the-dependency } + +Sonra bu bağımlılığı şöyle kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} + +/// info | Bilgi + +Dikkat edin, *path operation function* içinde yalnızca tek bir bağımlılık tanımlıyoruz: `query_or_cookie_extractor`. + +Ancak **FastAPI**, `query_or_cookie_extractor`'ı çağırmadan önce `query_extractor`'ı önce çözmesi gerektiğini bilir ve onun sonucunu `query_or_cookie_extractor`'a aktarır. + +/// + +```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 +``` + +## Aynı Bağımlılığı Birden Fazla Kez Kullanma { #using-the-same-dependency-multiple-times } + +Bağımlılıklarınızdan biri aynı *path operation* için birden fazla kez tanımlanırsa (örneğin birden fazla bağımlılığın ortak bir alt bağımlılığı varsa), **FastAPI** o alt bağımlılığı request başına yalnızca bir kez çağıracağını bilir. + +Dönen değeri bir "cache" içinde saklar ve aynı request içinde buna ihtiyaç duyan tüm "dependant"lara aktarır; böylece aynı request için bağımlılığı tekrar tekrar çağırmaz. + +Daha ileri bir senaryoda, "cached" değeri kullanmak yerine aynı request içinde her adımda (muhtemelen birden fazla kez) bağımlılığın çağrılması gerektiğini biliyorsanız, `Depends` kullanırken `use_cache=False` parametresini ayarlayabilirsiniz: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ Annotated olmayan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü tercih edin. + +/// + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +//// + +## Özet { #recap } + +Burada kullanılan havalı terimlerin ötesinde, **Dependency Injection** sistemi aslında oldukça basittir. + +*Path operation function*'lara benzeyen fonksiyonlardan ibarettir. + +Yine de çok güçlüdür ve keyfi derecede derin iç içe geçmiş bağımlılık "graph"ları (ağaçları) tanımlamanıza izin verir. + +/// tip | İpucu + +Bu basit örneklerle çok faydalı görünmeyebilir. + +Ancak **security** ile ilgili bölümlerde bunun ne kadar işe yaradığını göreceksiniz. + +Ayrıca size ne kadar kod kazandırdığını da göreceksiniz. + +/// diff --git a/docs/tr/docs/tutorial/encoder.md b/docs/tr/docs/tutorial/encoder.md new file mode 100644 index 000000000..e4790a032 --- /dev/null +++ b/docs/tr/docs/tutorial/encoder.md @@ -0,0 +1,35 @@ +# JSON Uyumlu Encoder { #json-compatible-encoder } + +Bazı durumlarda, bir veri tipini (örneğin bir Pydantic model) JSON ile uyumlu bir şeye (örneğin `dict`, `list` vb.) dönüştürmeniz gerekebilir. + +Örneğin, bunu bir veritabanında saklamanız gerekiyorsa. + +Bunun için **FastAPI**, `jsonable_encoder()` fonksiyonunu sağlar. + +## `jsonable_encoder` Kullanımı { #using-the-jsonable-encoder } + +Yalnızca JSON ile uyumlu veri kabul eden bir veritabanınız olduğunu düşünelim: `fake_db`. + +Örneğin bu veritabanı, JSON ile uyumlu olmadıkları için `datetime` objelerini kabul etmez. + +Dolayısıyla bir `datetime` objesinin, ISO formatında veriyi içeren bir `str`'e dönüştürülmesi gerekir. + +Aynı şekilde bu veritabanı bir Pydantic model'i (attribute'lara sahip bir obje) de kabul etmez; yalnızca bir `dict` kabul eder. + +Bunun için `jsonable_encoder` kullanabilirsiniz. + +Bir Pydantic model gibi bir obje alır ve JSON ile uyumlu bir versiyonunu döndürür: + +{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} + +Bu örnekte, Pydantic model'i bir `dict`'e, `datetime`'ı da bir `str`'e dönüştürür. + +Bu fonksiyonun çağrılmasıyla elde edilen sonuç, Python standardındaki `json.dumps()` ile encode edilebilecek bir şeydir. + +JSON formatında (string olarak) veriyi içeren büyük bir `str` döndürmez. Bunun yerine, tüm değerleri ve alt değerleri JSON ile uyumlu olacak şekilde, Python’un standart bir veri yapısını (örneğin bir `dict`) döndürür. + +/// note | Not + +`jsonable_encoder`, aslında **FastAPI** tarafından veriyi dönüştürmek için internal olarak kullanılır. Ancak birçok farklı senaryoda da oldukça faydalıdır. + +/// diff --git a/docs/tr/docs/tutorial/extra-data-types.md b/docs/tr/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..464d3a82a --- /dev/null +++ b/docs/tr/docs/tutorial/extra-data-types.md @@ -0,0 +1,62 @@ +# Ek Veri Tipleri { #extra-data-types } + +Şimdiye kadar şunlar gibi yaygın veri tiplerini kullanıyordunuz: + +* `int` +* `float` +* `str` +* `bool` + +Ancak daha karmaşık veri tiplerini de kullanabilirsiniz. + +Ve yine, şimdiye kadar gördüğünüz özelliklerin aynısına sahip olursunuz: + +* Harika editör desteği. +* Gelen request'lerden veri dönüştürme. +* response verileri için veri dönüştürme. +* Veri doğrulama. +* Otomatik annotation ve dokümantasyon. + +## Diğer veri tipleri { #other-data-types } + +Kullanabileceğiniz ek veri tiplerinden bazıları şunlardır: + +* `UUID`: + * Birçok veritabanı ve sistemde ID olarak yaygın kullanılan, standart bir "Universally Unique Identifier". + * request ve response'larda `str` olarak temsil edilir. +* `datetime.datetime`: + * Python `datetime.datetime`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Python `datetime.date`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `2008-09-15`. +* `datetime.time`: + * Python `datetime.time`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `14:23:55.003`. +* `datetime.timedelta`: + * Python `datetime.timedelta`. + * request ve response'larda toplam saniye sayısını ifade eden bir `float` olarak temsil edilir. + * Pydantic, bunu ayrıca bir "ISO 8601 time diff encoding" olarak temsil etmeye de izin verir, daha fazla bilgi için dokümanlara bakın. +* `frozenset`: + * request ve response'larda, `set` ile aynı şekilde ele alınır: + * request'lerde bir list okunur, tekrarlar kaldırılır ve `set`'e dönüştürülür. + * response'larda `set`, `list`'e dönüştürülür. + * Üretilen schema, `set` değerlerinin benzersiz olduğunu belirtir (JSON Schema'nın `uniqueItems` özelliğini kullanarak). +* `bytes`: + * Standart Python `bytes`. + * request ve response'larda `str` gibi ele alınır. + * Üretilen schema, bunun `binary` "format"ına sahip bir `str` olduğunu belirtir. +* `Decimal`: + * Standart Python `Decimal`. + * request ve response'larda `float` ile aynı şekilde işlenir. +* Geçerli tüm Pydantic veri tiplerini burada görebilirsiniz: Pydantic data types. + +## Örnek { #example } + +Yukarıdaki tiplerden bazılarını kullanan parametrelere sahip bir örnek *path operation* şöyle: + +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} + +Fonksiyonun içindeki parametrelerin doğal veri tiplerinde olduğunu unutmayın; örneğin normal tarih işlemleri yapabilirsiniz: + +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} diff --git a/docs/tr/docs/tutorial/extra-models.md b/docs/tr/docs/tutorial/extra-models.md new file mode 100644 index 000000000..9aae28e05 --- /dev/null +++ b/docs/tr/docs/tutorial/extra-models.md @@ -0,0 +1,211 @@ +# Ek Modeller { #extra-models } + +Önceki örnekten devam edersek, birbiriyle ilişkili birden fazla modelin olması oldukça yaygındır. + +Bu durum özellikle kullanıcı modellerinde sık görülür, çünkü: + +* **input modeli** bir `password` içerebilmelidir. +* **output modeli** `password` içermemelidir. +* **database modeli** büyük ihtimalle hash'lenmiş bir `password` tutmalıdır. + +/// danger + +Kullanıcının düz metin (plaintext) `password`'ünü asla saklamayın. Her zaman sonradan doğrulayabileceğiniz "güvenli bir hash" saklayın. + +Eğer bilmiyorsanız, "password hash" nedir konusunu [güvenlik bölümlerinde](security/simple-oauth2.md#password-hashing){.internal-link target=_blank} öğreneceksiniz. + +/// + +## Birden Çok Model { #multiple-models } + +`password` alanlarıyla birlikte modellerin genel olarak nasıl görünebileceğine ve nerelerde kullanılacaklarına dair bir fikir: + +{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} + +### `**user_in.model_dump()` Hakkında { #about-user-in-model-dump } + +#### Pydantic'in `.model_dump()` Metodu { #pydantics-model-dump } + +`user_in`, `UserIn` sınıfına ait bir Pydantic modelidir. + +Pydantic modellerinde, model verilerini içeren bir `dict` döndüren `.model_dump()` metodu bulunur. + +Yani, şöyle bir Pydantic nesnesi `user_in` oluşturursak: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +ve sonra şunu çağırırsak: + +```Python +user_dict = user_in.model_dump() +``` + +artık `user_dict` değişkeninde modelin verilerini içeren bir `dict` vardır (Pydantic model nesnesi yerine bir `dict` elde etmiş oluruz). + +Ve eğer şunu çağırırsak: + +```Python +print(user_dict) +``` + +şöyle bir Python `dict` elde ederiz: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Bir `dict`'i Unpack Etmek { #unpacking-a-dict } + +`user_dict` gibi bir `dict` alıp bunu bir fonksiyona (ya da sınıfa) `**user_dict` ile gönderirsek, Python bunu "unpack" eder. Yani `user_dict` içindeki key ve value'ları doğrudan key-value argümanları olarak geçirir. + +Dolayısıyla, yukarıdaki `user_dict` ile devam edersek, şunu yazmak: + +```Python +UserInDB(**user_dict) +``` + +şuna eşdeğer bir sonuç üretir: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Ya da daha net şekilde, `user_dict`'i doğrudan kullanarak, gelecekte içeriği ne olursa olsun: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Bir Pydantic Modelinden Diğerinin İçeriğiyle Pydantic Model Oluşturmak { #a-pydantic-model-from-the-contents-of-another } + +Yukarıdaki örnekte `user_dict`'i `user_in.model_dump()` ile elde ettiğimiz için, şu kod: + +```Python +user_dict = user_in.model_dump() +UserInDB(**user_dict) +``` + +şuna eşdeğerdir: + +```Python +UserInDB(**user_in.model_dump()) +``` + +...çünkü `user_in.model_dump()` bir `dict` döndürür ve biz de bunu `UserInDB`'ye `**` önekiyle vererek Python'ın "unpack" etmesini sağlarız. + +Böylece, bir Pydantic modelindeki verilerden başka bir Pydantic model üretmiş oluruz. + +#### Bir `dict`'i Unpack Etmek ve Ek Keyword'ler { #unpacking-a-dict-and-extra-keywords } + +Sonrasında, aşağıdaki gibi ek keyword argümanı `hashed_password=hashed_password` eklemek: + +```Python +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) +``` + +...şuna benzer bir sonuca dönüşür: + +```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 + +Ek destek fonksiyonları olan `fake_password_hasher` ve `fake_save_user` sadece verinin olası bir akışını göstermek içindir; elbette gerçek bir güvenlik sağlamazlar. + +/// + +## Tekrarı Azaltma { #reduce-duplication } + +Kod tekrarını azaltmak, **FastAPI**'nin temel fikirlerinden biridir. + +Kod tekrarı; bug, güvenlik problemi, kodun senkron dışına çıkması (bir yeri güncelleyip diğerlerini güncellememek) gibi sorunların olasılığını artırır. + +Bu modellerin hepsi verinin büyük bir kısmını paylaşıyor ve attribute adlarını ve type'larını tekrar ediyor. + +Daha iyisini yapabiliriz. + +Diğer modellerimiz için temel olacak bir `UserBase` modeli tanımlayabiliriz. Sonra da bu modelden türeyen (subclass) modeller oluşturup onun attribute'larını (type deklarasyonları, doğrulama vb.) miras aldırabiliriz. + +Tüm veri dönüştürme, doğrulama, dokümantasyon vb. her zamanki gibi çalışmaya devam eder. + +Bu sayede modeller arasındaki farkları (plaintext `password` olan, `hashed_password` olan ve `password` olmayan) sadece o farklılıklar olarak tanımlayabiliriz: + +{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} + +## `Union` veya `anyOf` { #union-or-anyof } + +Bir response'u iki ya da daha fazla type'ın `Union`'ı olarak tanımlayabilirsiniz; bu, response'un bunlardan herhangi biri olabileceği anlamına gelir. + +OpenAPI'de bu `anyOf` ile tanımlanır. + +Bunu yapmak için standart Python type hint'i olan `typing.Union`'ı kullanın: + +/// note + +Bir `Union` tanımlarken en spesifik type'ı önce, daha az spesifik olanı sonra ekleyin. Aşağıdaki örnekte daha spesifik olan `PlaneItem`, `Union[PlaneItem, CarItem]` içinde `CarItem`'dan önce gelir. + +/// + +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} + +### Python 3.10'da `Union` { #union-in-python-3-10 } + +Bu örnekte `Union[PlaneItem, CarItem]` değerini `response_model` argümanına veriyoruz. + +Bunu bir **type annotation** içine koymak yerine bir **argümana değer** olarak geçtiğimiz için, Python 3.10'da bile `Union` kullanmamız gerekiyor. + +Eğer bu bir type annotation içinde olsaydı, dikey çizgiyi kullanabilirdik: + +```Python +some_variable: PlaneItem | CarItem +``` + +Ancak bunu `response_model=PlaneItem | CarItem` atamasına koyarsak hata alırız; çünkü Python bunu bir type annotation olarak yorumlamak yerine `PlaneItem` ile `CarItem` arasında **geçersiz bir işlem** yapmaya çalışır. + +## Model Listesi { #list-of-models } + +Aynı şekilde, nesne listesi döndüren response'ları da tanımlayabilirsiniz. + +Bunun için standart Python `typing.List`'i (ya da Python 3.9 ve üzeri için sadece `list`) kullanın: + +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} + +## Rastgele `dict` ile Response { #response-with-arbitrary-dict } + +Bir Pydantic modeli kullanmadan, sadece key ve value type'larını belirterek düz, rastgele bir `dict` ile de response tanımlayabilirsiniz. + +Bu, geçerli field/attribute adlarını (Pydantic modeli için gerekli olurdu) önceden bilmiyorsanız kullanışlıdır. + +Bu durumda `typing.Dict` (ya da Python 3.9 ve üzeri için sadece `dict`) kullanabilirsiniz: + +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} + +## Özet { #recap } + +Her duruma göre birden fazla Pydantic modeli kullanın ve gerekirse özgürce inheritance uygulayın. + +Bir entity'nin farklı "state"lere sahip olması gerekiyorsa, o entity için tek bir veri modeli kullanmak zorunda değilsiniz. Örneğin `password` içeren, `password_hash` içeren ve `password` içermeyen state'lere sahip kullanıcı "entity"si gibi. diff --git a/docs/tr/docs/tutorial/handling-errors.md b/docs/tr/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..a4d693792 --- /dev/null +++ b/docs/tr/docs/tutorial/handling-errors.md @@ -0,0 +1,244 @@ +# Hataları Yönetme { #handling-errors } + +API’nizi kullanan bir client’a hata bildirmek zorunda olduğunuz pek çok durum vardır. + +Bu client; frontend’i olan bir tarayıcı, başka birinin yazdığı bir kod, bir IoT cihazı vb. olabilir. + +Client’a şunları söylemeniz gerekebilir: + +* Client’ın bu işlem için yeterli yetkisi yok. +* Client’ın bu kaynağa erişimi yok. +* Client’ın erişmeye çalıştığı öğe mevcut değil. +* vb. + +Bu durumlarda genellikle **400** aralığında (**400** ile **499** arası) bir **HTTP status code** döndürürsünüz. + +Bu, 200 HTTP status code’larına (200 ile 299 arası) benzer. Bu "200" status code’ları, request’in bir şekilde "başarılı" olduğunu ifade eder. + +400 aralığındaki status code’lar ise hatanın client tarafından kaynaklandığını gösterir. + +Şu meşhur **"404 Not Found"** hatalarını (ve şakalarını) hatırlıyor musunuz? + +## `HTTPException` Kullanma { #use-httpexception } + +Client’a hata içeren HTTP response’ları döndürmek için `HTTPException` kullanırsınız. + +### `HTTPException`’ı Import Etme { #import-httpexception } + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} + +### Kodunuzda Bir `HTTPException` Raise Etme { #raise-an-httpexception-in-your-code } + +`HTTPException`, API’lerle ilgili ek veriler içeren normal bir Python exception’ıdır. + +Python exception’ı olduğu için `return` etmezsiniz, `raise` edersiniz. + +Bu aynı zamanda şunu da ifade eder: *path operation function*’ınızın içinde çağırdığınız bir yardımcı (utility) fonksiyonun içindeyken `HTTPException` raise ederseniz, *path operation function* içindeki kodun geri kalanı çalışmaz; request’i hemen sonlandırır ve `HTTPException` içindeki HTTP hatasını client’a gönderir. + +Bir değer döndürmek yerine exception raise etmenin faydası, Dependencies ve Security bölümünde daha da netleşecektir. + +Bu örnekte, client var olmayan bir ID ile bir item istediğinde, `404` status code’u ile bir exception raise edelim: + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} + +### Ortaya Çıkan Response { #the-resulting-response } + +Client `http://example.com/items/foo` (bir `item_id` `"foo"`) isterse, HTTP status code olarak 200 ve şu JSON response’u alır: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +Ancak client `http://example.com/items/bar` (mevcut olmayan bir `item_id` `"bar"`) isterse, HTTP status code olarak 404 ("not found" hatası) ve şu JSON response’u alır: + +```JSON +{ + "detail": "Item not found" +} +``` + +/// tip | İpucu + +Bir `HTTPException` raise ederken, `detail` parametresine sadece `str` değil, JSON’a dönüştürülebilen herhangi bir değer geçebilirsiniz. + +Örneğin `dict`, `list` vb. geçebilirsiniz. + +Bunlar **FastAPI** tarafından otomatik olarak işlenir ve JSON’a dönüştürülür. + +/// + +## Özel Header’lar Eklemek { #add-custom-headers } + +Bazı durumlarda HTTP hata response’una özel header’lar eklemek faydalıdır. Örneğin bazı güvenlik türlerinde. + +Muhtemelen bunu doğrudan kendi kodunuzda kullanmanız gerekmeyecek. + +Ama ileri seviye bir senaryo için ihtiyaç duyarsanız, özel header’lar ekleyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} + +## Özel Exception Handler’ları Kurmak { #install-custom-exception-handlers } + +Starlette’in aynı exception yardımcı araçlarıyla özel exception handler’lar ekleyebilirsiniz. + +Diyelim ki sizin (ya da kullandığınız bir kütüphanenin) `raise` edebileceği `UnicornException` adında özel bir exception’ınız var. + +Ve bu exception’ı FastAPI ile global olarak handle etmek istiyorsunuz. + +`@app.exception_handler()` ile özel bir exception handler ekleyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} + +Burada `/unicorns/yolo` için request atarsanız, *path operation* bir `UnicornException` `raise` eder. + +Ancak bu, `unicorn_exception_handler` tarafından handle edilir. + +Böylece HTTP status code’u `418` olan, JSON içeriği şu şekilde temiz bir hata response’u alırsınız: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +/// note | Teknik Detaylar + +`from starlette.requests import Request` ve `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. `Request` için de aynısı geçerlidir. + +/// + +## Varsayılan Exception Handler’ları Override Etmek { #override-the-default-exception-handlers } + +**FastAPI** bazı varsayılan exception handler’lara sahiptir. + +Bu handler’lar, `HTTPException` `raise` ettiğinizde ve request geçersiz veri içerdiğinde varsayılan JSON response’ları döndürmekten sorumludur. + +Bu exception handler’ları kendi handler’larınızla override edebilirsiniz. + +### Request Validation Exception’larını Override Etmek { #override-request-validation-exceptions } + +Bir request geçersiz veri içerdiğinde, **FastAPI** içeride `RequestValidationError` raise eder. + +Ve bunun için varsayılan bir exception handler da içerir. + +Override etmek için `RequestValidationError`’ı import edin ve exception handler’ı `@app.exception_handler(RequestValidationError)` ile decorate edin. + +Exception handler, bir `Request` ve exception’ı alır. + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} + +Artık `/items/foo`’ya giderseniz, şu varsayılan JSON hatası yerine: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +şu şekilde bir metin (text) versiyonu alırsınız: + +``` +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer +``` + +### `HTTPException` Hata Handler’ını Override Etmek { #override-the-httpexception-error-handler } + +Benzer şekilde `HTTPException` handler’ını da override edebilirsiniz. + +Örneğin bu hatalar için JSON yerine plain text response döndürmek isteyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} + +/// note | Teknik Detaylar + +`from starlette.responses import PlainTextResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. + +/// + +/// warning | Uyarı + +`RequestValidationError`, validation hatasının gerçekleştiği dosya adı ve satır bilgilerini içerir; isterseniz bunu log’larınıza ilgili bilgilerle birlikte yazdırabilirsiniz. + +Ancak bu, eğer sadece string’e çevirip bu bilgiyi doğrudan response olarak döndürürseniz sisteminiz hakkında bir miktar bilgi sızdırabileceğiniz anlamına gelir. Bu yüzden burada kod, her bir hatayı ayrı ayrı çıkarıp gösterir. + +/// + +### `RequestValidationError` Body’sini Kullanmak { #use-the-requestvalidationerror-body } + +`RequestValidationError`, geçersiz veriyle aldığı `body`’yi içerir. + +Uygulamanızı geliştirirken body’yi log’lamak, debug etmek, kullanıcıya döndürmek vb. için bunu kullanabilirsiniz. + +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} + +Şimdi şu gibi geçersiz bir item göndermeyi deneyin: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +Aldığınız body’yi de içeren, verinin geçersiz olduğunu söyleyen bir response alırsınız: + +```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’nin `HTTPException`’ı vs Starlette’in `HTTPException`’ı { #fastapis-httpexception-vs-starlettes-httpexception } + +**FastAPI**’nin kendi `HTTPException`’ı vardır. + +Ve **FastAPI**’nin `HTTPException` hata sınıfı, Starlette’in `HTTPException` hata sınıfından kalıtım alır (inherit). + +Tek fark şudur: **FastAPI**’nin `HTTPException`’ı `detail` alanı için JSON’a çevrilebilir herhangi bir veri kabul ederken, Starlette’in `HTTPException`’ı burada sadece string kabul eder. + +Bu yüzden kodunuzda her zamanki gibi **FastAPI**’nin `HTTPException`’ını raise etmeye devam edebilirsiniz. + +Ancak bir exception handler register ederken, bunu Starlette’in `HTTPException`’ı için register etmelisiniz. + +Böylece Starlette’in internal kodunun herhangi bir bölümü ya da bir Starlette extension/plug-in’i Starlette `HTTPException` raise ederse, handler’ınız bunu yakalayıp (catch) handle edebilir. + +Bu örnekte, iki `HTTPException`’ı da aynı kodda kullanabilmek için Starlette’in exception’ı `StarletteHTTPException` olarak yeniden adlandırılıyor: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### **FastAPI**’nin Exception Handler’larını Yeniden Kullanmak { #reuse-fastapis-exception-handlers } + +Exception’ı, **FastAPI**’nin aynı varsayılan exception handler’larıyla birlikte kullanmak isterseniz, varsayılan exception handler’ları `fastapi.exception_handlers` içinden import edip yeniden kullanabilirsiniz: + +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} + +Bu örnekte sadece oldukça açıklayıcı bir mesajla hatayı yazdırıyorsunuz; ama fikir anlaşılıyor. Exception’ı kullanıp ardından varsayılan exception handler’ları olduğu gibi yeniden kullanabilirsiniz. diff --git a/docs/tr/docs/tutorial/header-param-models.md b/docs/tr/docs/tutorial/header-param-models.md new file mode 100644 index 000000000..9170515dc --- /dev/null +++ b/docs/tr/docs/tutorial/header-param-models.md @@ -0,0 +1,72 @@ +# Header Parametre Modelleri { #header-parameter-models } + +Birbiriyle ilişkili **header parametreleri**nden oluşan bir grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Bu sayede modeli **birden fazla yerde yeniden kullanabilir**, ayrıca tüm parametreler için doğrulamaları ve metadata bilgilerini tek seferde tanımlayabilirsiniz. 😎 + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. 🤓 + +/// + +## Pydantic Model ile Header Parametreleri { #header-parameters-with-a-pydantic-model } + +İhtiyacınız olan **header parametreleri**ni bir **Pydantic model** içinde tanımlayın, ardından parametreyi `Header` olarak belirtin: + +{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} + +**FastAPI**, request içindeki **headers** bölümünden **her alan** için veriyi **çıkarır** ve size tanımladığınız Pydantic model örneğini verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Gerekli header'ları `/docs` altındaki doküman arayüzünde görebilirsiniz: + +
+ +
+ +## Ek Header'ları Yasaklayın { #forbid-extra-headers } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değil), kabul etmek istediğiniz header'ları **kısıtlamak** isteyebilirsiniz. + +Pydantic'in model yapılandırmasını kullanarak `extra` alanları `forbid` edebilirsiniz: + +{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client bazı **ek header'lar** göndermeye çalışırsa, **hata** response'u alır. + +Örneğin client, değeri `plumbus` olan bir `tool` header'ı göndermeye çalışırsa, `tool` header parametresine izin verilmediğini söyleyen bir **hata** response'u alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ] +} +``` + +## Alt Çizgileri Dönüştürmeyi Kapatın { #disable-convert-underscores } + +Normal header parametrelerinde olduğu gibi, parametre adlarında alt çizgi karakterleri olduğunda bunlar **otomatik olarak tireye dönüştürülür**. + +Örneğin kodda `save_data` adlı bir header parametreniz varsa, beklenen HTTP header `save-data` olur ve dokümanlarda da bu şekilde görünür. + +Herhangi bir sebeple bu otomatik dönüşümü kapatmanız gerekiyorsa, header parametreleri için kullandığınız Pydantic model'lerde de bunu devre dışı bırakabilirsiniz. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Uyarı + +`convert_underscores` değerini `False` yapmadan önce, bazı HTTP proxy'lerinin ve server'ların alt çizgi içeren header'ların kullanımına izin vermediğini unutmayın. + +/// + +## Özet { #summary } + +**FastAPI**'de **headers** tanımlamak için **Pydantic model** kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/header-params.md b/docs/tr/docs/tutorial/header-params.md new file mode 100644 index 000000000..ca787f534 --- /dev/null +++ b/docs/tr/docs/tutorial/header-params.md @@ -0,0 +1,91 @@ +# Header Parametreleri { #header-parameters } + +`Query`, `Path` ve `Cookie` parametrelerini nasıl tanımlıyorsanız, Header parametrelerini de aynı şekilde tanımlayabilirsiniz. + +## `Header`'ı Import Edin { #import-header } + +Önce `Header`'ı import edin: + +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} + +## `Header` Parametrelerini Tanımlayın { #declare-header-parameters } + +Ardından header parametrelerini, `Path`, `Query` ve `Cookie` ile kullandığınız yapının aynısıyla tanımlayın. + +Default değeri ve ek validation ya da annotation parametrelerinin tamamını belirleyebilirsiniz: + +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} + +/// note | Teknik Detaylar + +`Header`, `Path`, `Query` ve `Cookie`'nin "kardeş" sınıfıdır. Ayrıca aynı ortak `Param` sınıfından kalıtım alır. + +Ancak şunu unutmayın: `fastapi`'den `Query`, `Path`, `Header` ve diğerlerini import ettiğinizde, bunlar aslında özel sınıfları döndüren fonksiyonlardır. + +/// + +/// info | Bilgi + +Header'ları tanımlamak için `Header` kullanmanız gerekir; aksi halde parametreler query parametreleri olarak yorumlanır. + +/// + +## Otomatik Dönüştürme { #automatic-conversion } + +`Header`, `Path`, `Query` ve `Cookie`'nin sağladıklarına ek olarak küçük bir ekstra işlevsellik sunar. + +Standart header'ların çoğu, "hyphen" karakteri (diğer adıyla "minus symbol" (`-`)) ile ayrılır. + +Ancak `user-agent` gibi bir değişken adı Python'da geçersizdir. + +Bu yüzden, default olarak `Header`, header'ları almak ve dokümante etmek için parametre adlarındaki underscore (`_`) karakterlerini hyphen (`-`) ile dönüştürür. + +Ayrıca HTTP header'ları büyük/küçük harfe duyarlı değildir; dolayısıyla onları standart Python stiliyle (diğer adıyla "snake_case") tanımlayabilirsiniz. + +Yani `User_Agent` gibi bir şey yazıp ilk harfleri büyütmeniz gerekmeden, Python kodunda normalde kullandığınız gibi `user_agent` kullanabilirsiniz. + +Herhangi bir nedenle underscore'ların hyphen'lara otomatik dönüştürülmesini kapatmanız gerekirse, `Header`'ın `convert_underscores` parametresini `False` yapın: + +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} + +/// warning | Uyarı + +`convert_underscores`'u `False` yapmadan önce, bazı HTTP proxy'lerinin ve server'ların underscore içeren header'ların kullanımına izin vermediğini unutmayın. + +/// + +## Yinelenen Header'lar { #duplicate-headers } + +Yinelenen header'lar almak mümkündür. Yani aynı header'ın birden fazla değeri olabilir. + +Bu tür durumları, type tanımında bir list kullanarak belirtebilirsiniz. + +Yinelenen header'daki tüm değerleri Python `list` olarak alırsınız. + +Örneğin, birden fazla kez gelebilen `X-Token` header'ını tanımlamak için şöyle yazabilirsiniz: + +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} + +Eğer bu *path operation* ile iki HTTP header göndererek iletişim kurarsanız: + +``` +X-Token: foo +X-Token: bar +``` + +response şöyle olur: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## Özet { #recap } + +Header'ları `Header` ile tanımlayın; `Query`, `Path` ve `Cookie` ile kullanılan ortak kalıbı burada da kullanın. + +Değişkenlerinizdeki underscore'lar konusunda endişelenmeyin, **FastAPI** bunları dönüştürmeyi halleder. diff --git a/docs/tr/docs/tutorial/index.md b/docs/tr/docs/tutorial/index.md new file mode 100644 index 000000000..f672c9e20 --- /dev/null +++ b/docs/tr/docs/tutorial/index.md @@ -0,0 +1,95 @@ +# Eğitim - Kullanıcı Rehberi { #tutorial-user-guide } + +Bu eğitim, **FastAPI**'yi özelliklerinin çoğuyla birlikte adım adım nasıl kullanacağınızı gösterir. + +Her bölüm bir öncekilerin üzerine kademeli olarak eklenir, ancak konular birbirinden ayrılacak şekilde yapılandırılmıştır; böylece API ihtiyaçlarınıza göre doğrudan belirli bir konuya gidip aradığınızı bulabilirsiniz. + +Ayrıca, ileride tekrar dönüp tam olarak ihtiyaç duyduğunuz şeyi görebileceğiniz bir referans olarak da tasarlanmıştır. + +## Kodu Çalıştırın { #run-the-code } + +Tüm code block'lar kopyalanıp doğrudan kullanılabilir (zaten test edilmiş Python dosyalarıdır). + +Örneklerden herhangi birini çalıştırmak için, kodu `main.py` adlı bir dosyaya kopyalayın ve şu komutla `fastapi dev`'i başlatın: + +
+ +```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. +``` + +
+ +Kodu yazmanız ya da kopyalayıp düzenlemeniz ve yerelinizde çalıştırmanız **şiddetle önerilir**. + +Editörünüzde kullanmak FastAPI'nin avantajlarını gerçekten gösterir: ne kadar az kod yazmanız gerektiğini, type check'leri, autocompletion'ı vb. görürsünüz. + +--- + +## FastAPI'yi Kurun { #install-fastapi } + +İlk adım FastAPI'yi kurmaktır. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, etkinleştirin ve ardından **FastAPI'yi kurun**: + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +/// note | Not + +`pip install "fastapi[standard]"` ile kurduğunuzda, bazı varsayılan opsiyonel standard bağımlılıklarla birlikte gelir. Bunlara `fastapi-cloud-cli` da dahildir; bu sayede FastAPI Cloud'a deploy edebilirsiniz. + +Bu opsiyonel bağımlılıkları istemiyorsanız bunun yerine `pip install fastapi` kurabilirsiniz. + +Standard bağımlılıkları kurmak istiyor ama `fastapi-cloud-cli` olmasın diyorsanız, `pip install "fastapi[standard-no-fastapi-cloud-cli]"` ile kurabilirsiniz. + +/// + +## İleri Düzey Kullanıcı Rehberi { #advanced-user-guide } + +Bu **Eğitim - Kullanıcı Rehberi**'ni bitirdikten sonra daha sonra okuyabileceğiniz bir **İleri Düzey Kullanıcı Rehberi** de var. + +**İleri Düzey Kullanıcı Rehberi** bunun üzerine inşa eder, aynı kavramları kullanır ve size bazı ek özellikler öğretir. + +Ancak önce **Eğitim - Kullanıcı Rehberi**'ni (şu anda okuduğunuz bölümü) okumalısınız. + +Yalnızca **Eğitim - Kullanıcı Rehberi** ile eksiksiz bir uygulama oluşturabilmeniz hedeflenmiştir; ardından ihtiyaçlarınıza göre, **İleri Düzey Kullanıcı Rehberi**'ndeki ek fikirlerden bazılarını kullanarak farklı şekillerde genişletebilirsiniz. diff --git a/docs/tr/docs/tutorial/metadata.md b/docs/tr/docs/tutorial/metadata.md new file mode 100644 index 000000000..dacd68cf5 --- /dev/null +++ b/docs/tr/docs/tutorial/metadata.md @@ -0,0 +1,120 @@ +# Metadata ve Doküman URL'leri { #metadata-and-docs-urls } + +**FastAPI** uygulamanızda çeşitli metadata yapılandırmalarını özelleştirebilirsiniz. + +## API için Metadata { #metadata-for-api } + +OpenAPI spesifikasyonunda ve otomatik API doküman arayüzlerinde kullanılan şu alanları ayarlayabilirsiniz: + +| Parametre | Tip | Açıklama | +|------------|------|-------------| +| `title` | `str` | API'nin başlığı. | +| `summary` | `str` | API'nin kısa özeti. OpenAPI 3.1.0, FastAPI 0.99.0 sürümünden itibaren mevcut. | +| `description` | `str` | API'nin kısa açıklaması. Markdown kullanabilir. | +| `version` | `string` | API'nin sürümü. Bu, OpenAPI'nin değil, kendi uygulamanızın sürümüdür. Örneğin `2.5.0`. | +| `terms_of_service` | `str` | API'nin Kullanım Koşulları (Terms of Service) için bir URL. Verilirse, URL formatında olmalıdır. | +| `contact` | `dict` | Yayınlanan API için iletişim bilgileri. Birden fazla alan içerebilir.
contact alanları
ParametreTipAçıklama
namestrİletişim kişisi/kuruluşunu tanımlayan ad.
urlstrİletişim bilgilerine işaret eden URL. URL formatında OLMALIDIR.
emailstrİletişim kişisi/kuruluşunun e-posta adresi. E-posta adresi formatında OLMALIDIR.
| +| `license_info` | `dict` | Yayınlanan API için lisans bilgileri. Birden fazla alan içerebilir.
license_info alanları
ParametreTipAçıklama
namestrZORUNLU (license_info ayarlanmışsa). API için kullanılan lisans adı.
identifierstrAPI için bir SPDX lisans ifadesi. identifier alanı, url alanıyla karşılıklı olarak dışlayıcıdır (ikisi aynı anda kullanılamaz). OpenAPI 3.1.0, FastAPI 0.99.0 sürümünden itibaren mevcut.
urlstrAPI için kullanılan lisansa ait URL. URL formatında OLMALIDIR.
| + +Şu şekilde ayarlayabilirsiniz: + +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} + +/// tip | İpucu + +`description` alanına Markdown yazabilirsiniz; çıktı tarafında render edilir. + +/// + +Bu yapılandırmayla otomatik API dokümanları şöyle görünür: + + + +## License identifier { #license-identifier } + +OpenAPI 3.1.0 ve FastAPI 0.99.0 sürümünden itibaren, `license_info` içinde `url` yerine bir `identifier` da ayarlayabilirsiniz. + +Örneğin: + +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} + +## Tag'ler için Metadata { #metadata-for-tags } + +`openapi_tags` parametresiyle, path operation'larınızı gruplamak için kullandığınız farklı tag'ler adına ek metadata da ekleyebilirsiniz. + +Bu parametre, her tag için bir sözlük (dictionary) içeren bir liste alır. + +Her sözlük şunları içerebilir: + +* `name` (**zorunlu**): *path operation*'larda ve `APIRouter`'larda `tags` parametresinde kullandığınız tag adıyla aynı olan bir `str`. +* `description`: tag için kısa bir açıklama içeren `str`. Markdown içerebilir ve doküman arayüzünde gösterilir. +* `externalDocs`: harici dokümanları tanımlayan bir `dict`: + * `description`: harici dokümanlar için kısa açıklama içeren `str`. + * `url` (**zorunlu**): harici dokümantasyonun URL'sini içeren `str`. + +### Tag'ler için metadata oluşturun { #create-metadata-for-tags } + +`users` ve `items` tag'lerini içeren bir örnekle deneyelim. + +Tag'leriniz için metadata oluşturup `openapi_tags` parametresine geçin: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} + +Açıklamaların içinde Markdown kullanabileceğinizi unutmayın; örneğin "login" kalın (**login**) ve "fancy" italik (_fancy_) olarak gösterilecektir. + +/// tip | İpucu + +Kullandığınız tüm tag'ler için metadata eklemek zorunda değilsiniz. + +/// + +### Tag'lerinizi kullanın { #use-your-tags } + +*path operation*'larınızı (ve `APIRouter`'ları) farklı tag'lere atamak için `tags` parametresini kullanın: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} + +/// info | Bilgi + +Tag'ler hakkında daha fazlası için: [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank}. + +/// + +### Dokümanları kontrol edin { #check-the-docs } + +Artık dokümanlara baktığınızda, eklediğiniz tüm metadata gösterilir: + + + +### Tag sırası { #order-of-tags } + +Her tag metadata sözlüğünün listedeki sırası, doküman arayüzünde gösterilecek sırayı da belirler. + +Örneğin alfabetik sıralamada `users`, `items`'tan sonra gelirdi; ancak listedeki ilk sözlük olarak `users` metadata'sını eklediğimiz için, dokümanlarda önce o görünür. + +## OpenAPI URL'si { #openapi-url } + +Varsayılan olarak OpenAPI şeması `/openapi.json` adresinden sunulur. + +Ancak bunu `openapi_url` parametresiyle yapılandırabilirsiniz. + +Örneğin `/api/v1/openapi.json` adresinden sunulacak şekilde ayarlamak için: + +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} + +OpenAPI şemasını tamamen kapatmak isterseniz `openapi_url=None` ayarlayabilirsiniz; bu, onu kullanan dokümantasyon arayüzlerini de devre dışı bırakır. + +## Doküman URL'leri { #docs-urls } + +Dahil gelen iki dokümantasyon arayüzünü yapılandırabilirsiniz: + +* **Swagger UI**: `/docs` adresinden sunulur. + * URL'sini `docs_url` parametresiyle ayarlayabilirsiniz. + * `docs_url=None` ayarlayarak devre dışı bırakabilirsiniz. +* **ReDoc**: `/redoc` adresinden sunulur. + * URL'sini `redoc_url` parametresiyle ayarlayabilirsiniz. + * `redoc_url=None` ayarlayarak devre dışı bırakabilirsiniz. + +Örneğin Swagger UI'yi `/documentation` adresinden sunup ReDoc'u kapatmak için: + +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/tr/docs/tutorial/middleware.md b/docs/tr/docs/tutorial/middleware.md new file mode 100644 index 000000000..68222265e --- /dev/null +++ b/docs/tr/docs/tutorial/middleware.md @@ -0,0 +1,95 @@ +# Middleware { #middleware } + +**FastAPI** uygulamalarına middleware ekleyebilirsiniz. + +"Middleware", herhangi bir özel *path operation* tarafından işlenmeden önce her **request** ile çalışan bir fonksiyondur. Ayrıca geri döndürmeden önce her **response** ile de çalışır. + +* Uygulamanıza gelen her **request**'i alır. +* Ardından o **request** üzerinde bir işlem yapabilir veya gerekli herhangi bir kodu çalıştırabilir. +* Sonra **request**'i uygulamanın geri kalanı tarafından işlenmesi için iletir (bir *path operation* tarafından). +* Ardından uygulama tarafından üretilen **response**'u alır (bir *path operation* tarafından). +* Sonra o **response** üzerinde bir işlem yapabilir veya gerekli herhangi bir kodu çalıştırabilir. +* Son olarak **response**'u döndürür. + +/// note | Teknik Detaylar + +`yield` ile dependency'leriniz varsa, çıkış (exit) kodu middleware'den *sonra* çalışır. + +Herhangi bir background task varsa ([Background Tasks](background-tasks.md){.internal-link target=_blank} bölümünde ele alınıyor, ileride göreceksiniz), bunlar tüm middleware'ler *tamamlandıktan sonra* çalışır. + +/// + +## Middleware Oluşturma { #create-a-middleware } + +Bir middleware oluşturmak için bir fonksiyonun üzerine `@app.middleware("http")` decorator'ünü kullanırsınız. + +Middleware fonksiyonu şunları alır: + +* `request`. +* Parametre olarak `request` alacak bir `call_next` fonksiyonu. + * Bu fonksiyon `request`'i ilgili *path operation*'a iletir. + * Ardından ilgili *path operation* tarafından üretilen `response`'u döndürür. +* Sonrasında `response`'u döndürmeden önce ayrıca değiştirebilirsiniz. + +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} + +/// tip | İpucu + +Özel (proprietary) header'lar `X-` prefix'i kullanılarak eklenebilir, bunu aklınızda tutun. + +Ancak tarayıcıdaki bir client'ın görebilmesini istediğiniz özel header'larınız varsa, bunları CORS konfigürasyonlarınıza ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) eklemeniz gerekir. Bunun için, Starlette'ın CORS dokümanlarında belgelenen `expose_headers` parametresini kullanın. + +/// + +/// note | Teknik Detaylar + +`from starlette.requests import Request` da kullanabilirdiniz. + +**FastAPI** bunu geliştirici olarak size kolaylık olsun diye sunar. Ancak doğrudan Starlette'tan gelir. + +/// + +### `response`'tan Önce ve Sonra { #before-and-after-the-response } + +Herhangi bir *path operation* `request`'i almadan önce, `request` ile birlikte çalışacak kod ekleyebilirsiniz. + +Ayrıca `response` üretildikten sonra, geri döndürmeden önce de kod çalıştırabilirsiniz. + +Örneğin, request'i işleyip response üretmenin kaç saniye sürdüğünü içeren `X-Process-Time` adlı özel bir header ekleyebilirsiniz: + +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} + +/// tip | İpucu + +Burada `time.time()` yerine `time.perf_counter()` kullanıyoruz, çünkü bu kullanım senaryolarında daha hassas olabilir. 🤓 + +/// + +## Birden Fazla Middleware Çalıştırma Sırası { #multiple-middleware-execution-order } + +`@app.middleware()` decorator'ü veya `app.add_middleware()` metodu ile birden fazla middleware eklediğinizde, eklenen her yeni middleware uygulamayı sarar ve bir stack oluşturur. En son eklenen middleware en *dıştaki* (outermost), ilk eklenen ise en *içteki* (innermost) olur. + +Request tarafında önce en *dıştaki* middleware çalışır. + +Response tarafında ise en son o çalışır. + +Örneğin: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Bu, aşağıdaki çalıştırma sırasını oluşturur: + +* **Request**: MiddlewareB → MiddlewareA → route + +* **Response**: route → MiddlewareA → MiddlewareB + +Bu stack davranışı, middleware'lerin öngörülebilir ve kontrol edilebilir bir sırayla çalıştırılmasını sağlar. + +## Diğer Middleware'ler { #other-middlewares } + +Diğer middleware'ler hakkında daha fazlasını daha sonra [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank} bölümünde okuyabilirsiniz. + +Bir sonraki bölümde, middleware ile CORS'un nasıl ele alınacağını göreceksiniz. diff --git a/docs/tr/docs/tutorial/path-operation-configuration.md b/docs/tr/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..3fe24dc0a --- /dev/null +++ b/docs/tr/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,107 @@ +# Path Operation Yapılandırması { #path-operation-configuration } + +Onu yapılandırmak için *path operation decorator*’ınıza geçebileceğiniz çeşitli parametreler vardır. + +/// warning | Uyarı + +Bu parametrelerin *path operation function*’ınıza değil, doğrudan *path operation decorator*’ına verildiğine dikkat edin. + +/// + +## Response Status Code { #response-status-code } + +*Path operation*’ınızın response’unda kullanılacak (HTTP) `status_code`’u tanımlayabilirsiniz. + +`404` gibi `int` kodu doğrudan verebilirsiniz. + +Ancak her sayısal kodun ne işe yaradığını hatırlamıyorsanız, `status` içindeki kısayol sabitlerini kullanabilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} + +Bu status code response’da kullanılacak ve OpenAPI şemasına eklenecektir. + +/// note | Teknik Detaylar + +`from starlette import status` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak işinizi kolaylaştırmak için `starlette.status`’u `fastapi.status` olarak da sunar. Ancak kaynağı doğrudan Starlette’tir. + +/// + +## Tags { #tags } + +*Path operation*’ınıza tag ekleyebilirsiniz; `tags` parametresine `str` öğelerinden oluşan bir `list` verin (genellikle tek bir `str`): + +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} + +Bunlar OpenAPI şemasına eklenecek ve otomatik dokümantasyon arayüzleri tarafından kullanılacaktır: + + + +### Enum ile Tags { #tags-with-enums } + +Büyük bir uygulamanız varsa, zamanla **birden fazla tag** birikebilir ve ilişkili *path operation*’lar için her zaman **aynı tag**’i kullandığınızdan emin olmak isteyebilirsiniz. + +Bu durumlarda tag’leri bir `Enum` içinde tutmak mantıklı olabilir. + +**FastAPI** bunu düz string’lerde olduğu gibi aynı şekilde destekler: + +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} + +## Özet ve açıklama { #summary-and-description } + +Bir `summary` ve `description` ekleyebilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} + +## Docstring’den Description { #description-from-docstring } + +Açıklamalar genelde uzun olur ve birden fazla satıra yayılır; bu yüzden *path operation* açıklamasını, fonksiyonun içinde docstring olarak tanımlayabilirsiniz; **FastAPI** de onu buradan okur. + +Docstring içinde Markdown yazabilirsiniz; doğru şekilde yorumlanır ve gösterilir (docstring girintisi dikkate alınarak). + +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} + +Interactive docs’ta şöyle kullanılacaktır: + + + +## Response description { #response-description } + +`response_description` parametresi ile response açıklamasını belirtebilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} + +/// info | Bilgi + +`response_description` özellikle response’u ifade eder; `description` ise genel olarak *path operation*’ı ifade eder. + +/// + +/// check | Ek bilgi + +OpenAPI, her *path operation* için bir response description zorunlu kılar. + +Bu yüzden siz sağlamazsanız, **FastAPI** otomatik olarak "Successful response" üretir. + +/// + + + +## Bir *path operation*’ı Deprecate Etmek { #deprecate-a-path-operation } + +Bir *path operation*’ı kaldırmadan, deprecated olarak işaretlemeniz gerekiyorsa `deprecated` parametresini verin: + +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} + +Interactive docs’ta deprecated olduğu net şekilde işaretlenecektir: + + + +Deprecated olan ve olmayan *path operation*’ların nasıl göründüğüne bakın: + + + +## Özet { #recap } + +*Path operation*’larınızı, *path operation decorator*’larına parametre geçirerek kolayca yapılandırabilir ve metadata ekleyebilirsiniz. diff --git a/docs/tr/docs/tutorial/path-params-numeric-validations.md b/docs/tr/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..e0118d71d --- /dev/null +++ b/docs/tr/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,154 @@ +# Path Parametreleri ve Sayısal Doğrulamalar { #path-parameters-and-numeric-validations } + +`Query` ile query parametreleri için daha fazla doğrulama ve metadata tanımlayabildiğiniz gibi, `Path` ile de path parametreleri için aynı tür doğrulama ve metadata tanımlayabilirsiniz. + +## `Path`'i İçe Aktarın { #import-path } + +Önce `fastapi` içinden `Path`'i ve `Annotated`'ı içe aktarın: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} + +/// info | Bilgi + +FastAPI, 0.95.0 sürümünde `Annotated` desteğini ekledi (ve bunu önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız, `Annotated` kullanmaya çalıştığınızda hata alırsınız. + +`Annotated` kullanmadan önce mutlaka FastAPI sürümünü en az 0.95.1 olacak şekilde [FastAPI sürümünü yükseltin](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +## Metadata Tanımlayın { #declare-metadata } + +`Query` için geçerli olan parametrelerin aynısını tanımlayabilirsiniz. + +Örneğin, `item_id` path parametresi için bir `title` metadata değeri tanımlamak isterseniz şunu yazabilirsiniz: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} + +/// note | Not + +Bir path parametresi her zaman zorunludur, çünkü path'in bir parçası olmak zorundadır. `None` ile tanımlasanız veya bir varsayılan değer verseniz bile bu hiçbir şeyi değiştirmez; yine her zaman zorunlu olur. + +/// + +## Parametreleri İhtiyacınıza Göre Sıralayın { #order-the-parameters-as-you-need } + +/// tip | İpucu + +`Annotated` kullanıyorsanız, bu muhtemelen o kadar önemli ya da gerekli değildir. + +/// + +Diyelim ki query parametresi `q`'yu zorunlu bir `str` olarak tanımlamak istiyorsunuz. + +Ayrıca bu parametre için başka bir şey tanımlamanız gerekmiyor; dolayısıyla `Query` kullanmanıza da aslında gerek yok. + +Ancak `item_id` path parametresi için yine de `Path` kullanmanız gerekiyor. Ve bir sebepten `Annotated` kullanmak istemiyorsunuz. + +Python, "default" değeri olan bir parametreyi, "default" değeri olmayan bir parametreden önce yazarsanız şikayet eder. + +Ama bunların sırasını değiştirebilir ve default değeri olmayan parametreyi (query parametresi `q`) en başa koyabilirsiniz. + +Bu **FastAPI** için önemli değildir. FastAPI parametreleri isimlerine, tiplerine ve default tanımlarına (`Query`, `Path`, vb.) göre tespit eder; sırayla ilgilenmez. + +Dolayısıyla fonksiyonunuzu şöyle tanımlayabilirsiniz: + +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} + +Ancak şunu unutmayın: `Annotated` kullanırsanız bu problem olmaz; çünkü `Query()` veya `Path()` için fonksiyon parametresi default değerlerini kullanmıyorsunuz. + +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## Parametreleri İhtiyacınıza Göre Sıralayın: Küçük Hileler { #order-the-parameters-as-you-need-tricks } + +/// tip | İpucu + +`Annotated` kullanıyorsanız, bu muhtemelen o kadar önemli ya da gerekli değildir. + +/// + +İşte bazen işe yarayan **küçük bir hile**; ama çok sık ihtiyacınız olmayacak. + +Şunları yapmak istiyorsanız: + +* `q` query parametresini `Query` kullanmadan ve herhangi bir default değer vermeden tanımlamak +* `item_id` path parametresini `Path` kullanarak tanımlamak +* bunları farklı bir sırada yazmak +* `Annotated` kullanmamak + +...Python bunun için küçük, özel bir sözdizimi sunar. + +Fonksiyonun ilk parametresi olarak `*` geçin. + +Python bu `*` ile bir şey yapmaz; ama bundan sonraki tüm parametrelerin keyword argument (anahtar-değer çiftleri) olarak çağrılması gerektiğini bilir; buna kwargs da denir. Default değerleri olmasa bile. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} + +### `Annotated` ile Daha İyi { #better-with-annotated } + +Şunu da unutmayın: `Annotated` kullanırsanız, fonksiyon parametresi default değerlerini kullanmadığınız için bu sorun ortaya çıkmaz ve muhtemelen `*` kullanmanız da gerekmez. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: Büyük Eşit { #number-validations-greater-than-or-equal } + +`Query` ve `Path` (ve ileride göreceğiniz diğerleri) ile sayı kısıtları tanımlayabilirsiniz. + +Burada `ge=1` ile, `item_id` değerinin `1`'den "`g`reater than or `e`qual" olacak şekilde bir tam sayı olması gerekir. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: Büyük ve Küçük Eşit { #number-validations-greater-than-and-less-than-or-equal } + +Aynısı şunlar için de geçerlidir: + +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual + +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: `float` Değerler, Büyük ve Küçük { #number-validations-floats-greater-than-and-less-than } + +Sayı doğrulamaları `float` değerler için de çalışır. + +Burada gt tanımlayabilmek (sadece ge değil) önemli hale gelir. Çünkü örneğin bir değerin `0`'dan büyük olmasını isteyebilirsiniz; `1`'den küçük olsa bile. + +Bu durumda `0.5` geçerli bir değer olur. Ancak `0.0` veya `0` geçerli olmaz. + +Aynısı lt için de geçerlidir. + +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} + +## Özet { #recap } + +`Query`, `Path` (ve henüz görmedikleriniz) ile metadata ve string doğrulamalarını, [Query Parametreleri ve String Doğrulamalar](query-params-str-validations.md){.internal-link target=_blank} bölümündekiyle aynı şekilde tanımlayabilirsiniz. + +Ayrıca sayısal doğrulamalar da tanımlayabilirsiniz: + +* `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 | Bilgi + +`Query`, `Path` ve ileride göreceğiniz diğer class'lar ortak bir `Param` class'ının alt class'larıdır. + +Hepsi, gördüğünüz ek doğrulama ve metadata parametrelerini paylaşır. + +/// + +/// note | Teknik Detaylar + +`Query`, `Path` ve diğerlerini `fastapi` içinden import ettiğinizde, bunlar aslında birer fonksiyondur. + +Çağrıldıklarında, aynı isme sahip class'ların instance'larını döndürürler. + +Yani `Query`'yi import edersiniz; bu bir fonksiyondur. Onu çağırdığınızda, yine `Query` adlı bir class'ın instance'ını döndürür. + +Bu fonksiyonlar (class'ları doğrudan kullanmak yerine) editörünüzün type'larıyla ilgili hata işaretlememesi için vardır. + +Bu sayede, bu hataları yok saymak üzere özel ayarlar eklemeden normal editörünüzü ve coding araçlarınızı kullanabilirsiniz. + +/// diff --git a/docs/tr/docs/tutorial/query-param-models.md b/docs/tr/docs/tutorial/query-param-models.md new file mode 100644 index 000000000..75139a677 --- /dev/null +++ b/docs/tr/docs/tutorial/query-param-models.md @@ -0,0 +1,68 @@ +# Query Parameter Modelleri { #query-parameter-models } + +Birbirleriyle ilişkili bir **query parameter** grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Böylece **modeli yeniden kullanabilir**, **birden fazla yerde** tekrar tekrar kullanabilir ve tüm parametreler için validation (doğrulama) ile metadata’yı tek seferde tanımlayabilirsiniz. 😎 + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. 🤓 + +/// + +## Pydantic Model ile Query Parameters { #query-parameters-with-a-pydantic-model } + +İhtiyacınız olan **query parameter**’ları bir **Pydantic model** içinde tanımlayın, ardından parametreyi `Query` olarak belirtin: + +{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} + +**FastAPI**, request’teki **query parameter**’lardan **her field** için veriyi **extract** eder ve tanımladığınız Pydantic model’i size verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Query parameter’ları `/docs` altındaki dokümantasyon arayüzünde görebilirsiniz: + +
+ +
+ +## Ek Query Parameter'ları Yasaklayın { #forbid-extra-query-parameters } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değil), almak istediğiniz query parameter’ları **kısıtlamak** isteyebilirsiniz. + +Pydantic’in model konfigürasyonunu kullanarak `extra` field’ları `forbid` edebilirsiniz: + +{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client, **query parameter**’larda **ek (extra)** veri göndermeye çalışırsa, **error** response alır. + +Örneğin client, değeri `plumbus` olan bir `tool` query parameter’ı göndermeye çalışırsa: + +```http +https://example.com/items/?limit=10&tool=plumbus +``` + +`tool` query parameter’ına izin verilmediğini söyleyen bir **error** response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus" + } + ] +} +``` + +## Özet { #summary } + +**FastAPI** içinde **query parameter**’ları tanımlamak için **Pydantic model**’leri kullanabilirsiniz. 😎 + +/// tip | İpucu + +Spoiler: cookie ve header’ları tanımlamak için de Pydantic model’leri kullanabilirsiniz; ancak bunu tutorial’ın ilerleyen bölümlerinde göreceksiniz. 🤫 + +/// diff --git a/docs/tr/docs/tutorial/query-params-str-validations.md b/docs/tr/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..18f0249e5 --- /dev/null +++ b/docs/tr/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,473 @@ +# Query Parametreleri ve String Doğrulamaları { #query-parameters-and-string-validations } + +**FastAPI**, parametreleriniz için ek bilgi ve doğrulamalar (validation) tanımlamanıza izin verir. + +Örnek olarak şu uygulamayı ele alalım: + +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} + +Query parametresi `q`, `str | None` tipindedir. Yani tipi `str`’dir ama `None` da olabilir. Nitekim varsayılan değer `None` olduğu için FastAPI bunun zorunlu olmadığını anlar. + +/// note | Not + +FastAPI, `q`’nun zorunlu olmadığını `= None` varsayılan değerinden anlar. + +`str | None` kullanmak, editörünüzün daha iyi destek vermesini ve hataları yakalamasını sağlar. + +/// + +## Ek doğrulama { #additional-validation } + +`q` opsiyonel olsa bile, verildiği durumda **uzunluğunun 50 karakteri geçmemesini** zorlayacağız. + +### `Query` ve `Annotated` import edin { #import-query-and-annotated } + +Bunu yapmak için önce şunları import edin: + +* `fastapi` içinden `Query` +* `typing` içinden `Annotated` + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} + +/// info | Bilgi + +FastAPI, 0.95.0 sürümünde `Annotated` desteğini ekledi (ve önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız `Annotated` kullanmaya çalışırken hata alırsınız. + +`Annotated` kullanmadan önce FastAPI sürümünü en az 0.95.1’e yükseltmek için [FastAPI sürümünü yükseltin](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +## `q` parametresinin tipinde `Annotated` kullanın { #use-annotated-in-the-type-for-the-q-parameter } + +[Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} içinde `Annotated` ile parametrelerinize metadata ekleyebileceğinizi söylemiştim, hatırlıyor musunuz? + +Şimdi bunu FastAPI ile kullanmanın zamanı. 🚀 + +Şu tip anotasyonuna sahiptik: + +//// tab | Python 3.10+ + +```Python +q: str | None = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Union[str, None] = None +``` + +//// + +Şimdi bunu `Annotated` ile saracağız; şöyle olacak: + +//// tab | Python 3.10+ + +```Python +q: Annotated[str | None] = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Annotated[Union[str, None]] = None +``` + +//// + +Bu iki sürüm de aynı anlama gelir: `q`, `str` veya `None` olabilen bir parametredir ve varsayılan olarak `None`’dır. + +Şimdi işin eğlenceli kısmına geçelim. 🎉 + +## `q` parametresindeki `Annotated` içine `Query` ekleyin { #add-query-to-annotated-in-the-q-parameter } + +Artık ek bilgi (bu durumda ek doğrulama) koyabildiğimiz bir `Annotated`’ımız olduğuna göre, `Annotated` içine `Query` ekleyin ve `max_length` parametresini `50` olarak ayarlayın: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Varsayılan değerin hâlâ `None` olduğuna dikkat edin; yani parametre hâlâ opsiyonel. + +Ama şimdi `Annotated` içinde `Query(max_length=50)` kullanarak FastAPI’ye bu değer için **ek doğrulama** istediğimizi söylüyoruz: en fazla 50 karakter. 😎 + +/// tip | İpucu + +Burada `Query()` kullanıyoruz çünkü bu bir **query parametresi**. İleride `Path()`, `Body()`, `Header()` ve `Cookie()` gibi, `Query()` ile aynı argümanları kabul eden diğerlerini de göreceğiz. + +/// + +FastAPI artık şunları yapacak: + +* Verinin uzunluğunun en fazla 50 karakter olduğundan emin olacak şekilde **doğrulayacak** +* Veri geçerli değilse client için **net bir hata** gösterecek +* Parametreyi OpenAPI şemasındaki *path operation* içinde **dokümante edecek** (dolayısıyla **otomatik dokümantasyon arayüzünde** görünecek) + +## Alternatif (eski): Varsayılan değer olarak `Query` { #alternative-old-query-as-the-default-value } + +FastAPI’nin önceki sürümlerinde (0.95.0 öncesi) `Query`’yi `Annotated` içine koymak yerine, parametrenizin varsayılan değeri olarak kullanmanız gerekiyordu. Etrafta bu şekilde yazılmış kod görme ihtimaliniz yüksek; bu yüzden açıklayalım. + +/// tip | İpucu + +Yeni kodlarda ve mümkün olduğunda, yukarıda anlatıldığı gibi `Annotated` kullanın. Birden fazla avantajı vardır (aşağıda anlatılıyor) ve dezavantajı yoktur. 🍰 + +/// + +Fonksiyon parametresinin varsayılan değeri olarak `Query()` kullanıp `max_length` parametresini 50 yapmak şöyle olurdu: + +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Bu senaryoda (`Annotated` kullanmadığımız için) fonksiyondaki `None` varsayılan değerini `Query()` ile değiştirmemiz gerekiyor. Bu durumda varsayılan değeri `Query(default=None)` ile vermeliyiz; bu, (en azından FastAPI açısından) aynı varsayılan değeri tanımlama amacına hizmet eder. + +Yani: + +```Python +q: str | None = Query(default=None) +``` + +...parametreyi `None` varsayılan değeriyle opsiyonel yapar; şununla aynı: + +```Python +q: str | None = None +``` + +Ancak `Query` sürümü bunun bir query parametresi olduğunu açıkça belirtir. + +Sonrasında `Query`’ye daha fazla parametre geçebiliriz. Bu örnekte string’ler için geçerli olan `max_length`: + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Bu, veriyi doğrular, veri geçerli değilse net bir hata gösterir ve parametreyi OpenAPI şemasındaki *path operation* içinde dokümante eder. + +### Varsayılan değer olarak `Query` veya `Annotated` içinde `Query` { #query-as-the-default-value-or-in-annotated } + +`Annotated` içinde `Query` kullanırken `Query` için `default` parametresini kullanamayacağınızı unutmayın. + +Bunun yerine fonksiyon parametresinin gerçek varsayılan değerini kullanın. Aksi halde tutarsız olur. + +Örneğin şu kullanım izinli değildir: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...çünkü varsayılan değerin `"rick"` mi `"morty"` mi olması gerektiği belli değildir. + +Bu nedenle (tercihen) şöyle kullanırsınız: + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...veya eski kod tabanlarında şuna rastlarsınız: + +```Python +q: str = Query(default="rick") +``` + +### `Annotated`’ın avantajları { #advantages-of-annotated } + +Fonksiyon parametrelerindeki varsayılan değer stiline göre **`Annotated` kullanmanız önerilir**; birden fazla nedenle **daha iyidir**. 🤓 + +**Fonksiyon parametresinin** **varsayılan** değeri, **gerçek varsayılan** değerdir; bu genel olarak Python açısından daha sezgiseldir. 😌 + +Aynı fonksiyonu FastAPI olmadan **başka yerlerde** de **çağırabilirsiniz** ve **beklendiği gibi çalışır**. Eğer **zorunlu** bir parametre varsa (varsayılan değer yoksa) editörünüz hata ile bunu belirtir; ayrıca gerekli parametreyi vermeden çalıştırırsanız **Python** da şikayet eder. + +`Annotated` kullanmayıp bunun yerine **(eski) varsayılan değer stilini** kullanırsanız, o fonksiyonu FastAPI olmadan **başka yerlerde** çağırdığınızda doğru çalışması için argümanları geçmeniz gerektiğini **hatırlamak** zorunda kalırsınız; yoksa değerler beklediğinizden farklı olur (ör. `str` yerine `QueryInfo` veya benzeri). Üstelik editörünüz de şikayet etmez ve Python da fonksiyonu çalıştırırken şikayet etmez; ancak içerideki operasyonlar hata verince ortaya çıkar. + +`Annotated` birden fazla metadata anotasyonu alabildiği için, artık aynı fonksiyonu Typer gibi başka araçlarla da kullanabilirsiniz. 🚀 + +## Daha fazla doğrulama ekleyin { #add-more-validations } + +`min_length` parametresini de ekleyebilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Regular expression ekleyin { #add-regular-expressions } + +Parametrenin eşleşmesi gereken bir `pattern` regular expression tanımlayabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Bu özel regular expression pattern’i, gelen parametre değerinin şunları sağladığını kontrol eder: + +* `^`: Aşağıdaki karakterlerle başlar; öncesinde karakter yoktur. +* `fixedquery`: Tam olarak `fixedquery` değerine sahiptir. +* `$`: Orada biter; `fixedquery` sonrasında başka karakter yoktur. + +Bu **"regular expression"** konuları gözünüzü korkutuyorsa sorun değil. Birçok kişi için zor bir konudur. Regular expression’lara ihtiyaç duymadan da pek çok şey yapabilirsiniz. + +Artık ihtiyaç duyduğunuzda **FastAPI** içinde kullanabileceğinizi biliyorsunuz. + +## Varsayılan değerler { #default-values } + +Elbette `None` dışında varsayılan değerler de kullanabilirsiniz. + +Örneğin `q` query parametresi için `min_length` değerini `3` yapmak ve varsayılan değer olarak `"fixedquery"` vermek istediğinizi düşünelim: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Not + +`None` dahil herhangi bir tipte varsayılan değere sahip olmak, parametreyi opsiyonel (zorunlu değil) yapar. + +/// + +## Zorunlu parametreler { #required-parameters } + +Daha fazla doğrulama veya metadata tanımlamamız gerekmiyorsa, `q` query parametresini yalnızca varsayılan değer tanımlamayarak zorunlu yapabiliriz: + +```Python +q: str +``` + +şunun yerine: + +```Python +q: str | None = None +``` + +Ancak biz artık `Query` ile tanımlıyoruz; örneğin şöyle: + +```Python +q: Annotated[str | None, Query(min_length=3)] = None +``` + +Dolayısıyla `Query` kullanırken bir değeri zorunlu yapmak istediğinizde, varsayılan değer tanımlamamanız yeterlidir: + +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} + +### Zorunlu ama `None` olabilir { #required-can-be-none } + +Bir parametrenin `None` kabul edebileceğini söyleyip yine de zorunlu olmasını sağlayabilirsiniz. Bu, client’ların değer göndermesini zorunlu kılar; değer `None` olsa bile. + +Bunu yapmak için `None`’ı geçerli bir tip olarak tanımlayın ama varsayılan değer vermeyin: + +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} + +## Query parametresi listesi / birden fazla değer { #query-parameter-list-multiple-values } + +Bir query parametresini `Query` ile açıkça tanımladığınızda, bir değer listesi alacak şekilde (başka bir deyişle, birden fazla değer alacak şekilde) de tanımlayabilirsiniz. + +Örneğin URL’de `q` query parametresinin birden fazla kez görünebilmesini istiyorsanız şöyle yazabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} + +Sonra şu URL ile: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +*path operation function* içinde, *function parameter* olan `q` parametresinde, birden fazla `q` *query parameters* değerini (`foo` ve `bar`) bir Python `list`’i olarak alırsınız. + +Dolayısıyla bu URL’ye response şöyle olur: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +/// tip | İpucu + +Yukarıdaki örnekte olduğu gibi tipi `list` olan bir query parametresi tanımlamak için `Query`’yi açıkça kullanmanız gerekir; aksi halde request body olarak yorumlanır. + +/// + +Etkileşimli API dokümanları da buna göre güncellenir ve birden fazla değere izin verir: + + + +### Varsayılanlarla query parametresi listesi / birden fazla değer { #query-parameter-list-multiple-values-with-defaults } + +Hiç değer verilmezse varsayılan bir `list` de tanımlayabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} + +Şu adrese giderseniz: + +``` +http://localhost:8000/items/ +``` + +`q`’nun varsayılanı `["foo", "bar"]` olur ve response şöyle olur: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Sadece `list` kullanmak { #using-just-list } + +`list[str]` yerine doğrudan `list` de kullanabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} + +/// note | Not + +Bu durumda FastAPI, listenin içeriğini kontrol etmez. + +Örneğin `list[int]`, listenin içeriğinin integer olduğunu kontrol eder (ve dokümante eder). Ancak tek başına `list` bunu yapmaz. + +/// + +## Daha fazla metadata tanımlayın { #declare-more-metadata } + +Parametre hakkında daha fazla bilgi ekleyebilirsiniz. + +Bu bilgiler oluşturulan OpenAPI’a dahil edilir ve dokümantasyon arayüzleri ile harici araçlar tarafından kullanılır. + +/// note | Not + +Farklı araçların OpenAPI desteği farklı seviyelerde olabilir. + +Bazıları tanımladığınız ek bilgilerin hepsini göstermeyebilir; ancak çoğu durumda eksik özellik geliştirme planındadır. + +/// + +Bir `title` ekleyebilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} + +Ve bir `description`: + +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} + +## Alias parametreleri { #alias-parameters } + +Parametrenin adının `item-query` olmasını istediğinizi düşünün. + +Örneğin: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Ancak `item-query` geçerli bir Python değişken adı değildir. + +En yakın seçenek `item_query` olur. + +Ama sizin hâlâ tam olarak `item-query` olmasına ihtiyacınız var... + +O zaman bir `alias` tanımlayabilirsiniz; bu alias, parametre değerini bulmak için kullanılacaktır: + +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} + +## Parametreleri deprecated yapmak { #deprecating-parameters } + +Diyelim ki artık bu parametreyi istemiyorsunuz. + +Bazı client’lar hâlâ kullandığı için bir süre tutmanız gerekiyor, ama dokümanların bunu açıkça deprecated olarak göstermesini istiyorsunuz. + +O zaman `Query`’ye `deprecated=True` parametresini geçin: + +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} + +Dokümanlarda şöyle görünür: + + + +## Parametreleri OpenAPI’dan hariç tutun { #exclude-parameters-from-openapi } + +Oluşturulan OpenAPI şemasından (dolayısıyla otomatik dokümantasyon sistemlerinden) bir query parametresini hariç tutmak için `Query`’nin `include_in_schema` parametresini `False` yapın: + +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## Özel Doğrulama { #custom-validation } + +Yukarıdaki parametrelerle yapılamayan bazı **özel doğrulama** ihtiyaçlarınız olabilir. + +Bu durumlarda, normal doğrulamadan sonra (ör. değerin `str` olduğunun doğrulanmasından sonra) uygulanacak bir **custom validator function** kullanabilirsiniz. + +Bunu, `Annotated` içinde Pydantic’in `AfterValidator`’ını kullanarak yapabilirsiniz. + +/// tip | İpucu + +Pydantic’te `BeforeValidator` ve başka validator’lar da vardır. 🤓 + +/// + +Örneğin bu custom validator, bir item ID’sinin ISBN kitap numarası için `isbn-` ile veya IMDB film URL ID’si için `imdb-` ile başladığını kontrol eder: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | Bilgi + +Bu özellik Pydantic 2 ve üzeri sürümlerde mevcuttur. 😎 + +/// + +/// tip | İpucu + +Veritabanı veya başka bir API gibi herhangi bir **harici bileşen** ile iletişim gerektiren bir doğrulama yapmanız gerekiyorsa, bunun yerine **FastAPI Dependencies** kullanmalısınız; onları ileride öğreneceksiniz. + +Bu custom validator’lar, request’te sağlanan **yalnızca** **aynı veri** ile kontrol edilebilen şeyler içindir. + +/// + +### O Kodu Anlamak { #understand-that-code } + +Önemli nokta, **`Annotated` içinde bir fonksiyonla birlikte `AfterValidator` kullanmak**. İsterseniz bu kısmı atlayabilirsiniz. 🤸 + +--- + +Ama bu örnek kodun detaylarını merak ediyorsanız, birkaç ek bilgi: + +#### `value.startswith()` ile String { #string-with-value-startswith } + +Fark ettiniz mi? `value.startswith()` ile bir string, tuple alabilir ve tuple içindeki her değeri kontrol eder: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Rastgele Bir Item { #a-random-item } + +`data.items()` ile, her dictionary öğesi için key ve value içeren tuple’lardan oluşan bir iterable object elde ederiz. + +Bu iterable object’i `list(data.items())` ile düzgün bir `list`’e çeviririz. + +Ardından `random.choice()` ile list’ten **rastgele bir değer** alırız; yani `(id, name)` içeren bir tuple elde ederiz. Şuna benzer: `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Sonra tuple içindeki bu **iki değeri** `id` ve `name` değişkenlerine **atarız**. + +Böylece kullanıcı bir item ID’si vermemiş olsa bile yine de rastgele bir öneri alır. + +...bütün bunları **tek bir basit satırda** yapıyoruz. 🤯 Python’u sevmemek elde mi? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Özet { #recap } + +Parametreleriniz için ek doğrulamalar ve metadata tanımlayabilirsiniz. + +Genel doğrulamalar ve metadata: + +* `alias` +* `title` +* `description` +* `deprecated` + +String’lere özel doğrulamalar: + +* `min_length` +* `max_length` +* `pattern` + +`AfterValidator` ile custom doğrulamalar. + +Bu örneklerde `str` değerleri için doğrulamanın nasıl tanımlanacağını gördünüz. + +Sayılar gibi diğer tipler için doğrulamaları nasıl tanımlayacağınızı öğrenmek için sonraki bölümlere geçin. diff --git a/docs/tr/docs/tutorial/request-files.md b/docs/tr/docs/tutorial/request-files.md new file mode 100644 index 000000000..0bbc557e0 --- /dev/null +++ b/docs/tr/docs/tutorial/request-files.md @@ -0,0 +1,176 @@ +# Request Dosyaları { #request-files } + +İstemcinin upload edeceği dosyaları `File` kullanarak tanımlayabilirsiniz. + +/// info | Bilgi + +Upload edilen dosyaları alabilmek için önce `python-multipart` yükleyin. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, aktive ettiğinizden ve ardından paketi yüklediğinizden emin olun. Örneğin: + +```console +$ pip install python-multipart +``` + +Bunun nedeni, upload edilen dosyaların "form data" olarak gönderilmesidir. + +/// + +## `File` Import Edin { #import-file } + +`fastapi` içinden `File` ve `UploadFile` import edin: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} + +## `File` Parametrelerini Tanımlayın { #define-file-parameters } + +`Body` veya `Form` için yaptığınız gibi dosya parametreleri oluşturun: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} + +/// info | Bilgi + +`File`, doğrudan `Form`’dan türeyen bir sınıftır. + +Ancak unutmayın: `fastapi` içinden `Query`, `Path`, `File` ve diğerlerini import ettiğinizde, bunlar aslında özel sınıflar döndüren fonksiyonlardır. + +/// + +/// tip | İpucu + +File body’leri tanımlamak için `File` kullanmanız gerekir; aksi halde parametreler query parametreleri veya body (JSON) parametreleri olarak yorumlanır. + +/// + +Dosyalar "form data" olarak upload edilir. + +*path operation function* parametrenizin tipini `bytes` olarak tanımlarsanız, **FastAPI** dosyayı sizin için okur ve içeriği `bytes` olarak alırsınız. + +Bunun, dosyanın tüm içeriğinin bellekte tutulacağı anlamına geldiğini unutmayın. Küçük dosyalar için iyi çalışır. + +Ancak bazı durumlarda `UploadFile` kullanmak size fayda sağlayabilir. + +## `UploadFile` ile Dosya Parametreleri { #file-parameters-with-uploadfile } + +Tipi `UploadFile` olan bir dosya parametresi tanımlayın: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} + +`UploadFile` kullanmanın `bytes`’a göre birkaç avantajı vardır: + +* Parametrenin varsayılan değerinde `File()` kullanmak zorunda değilsiniz. +* "Spooled" bir dosya kullanır: + * Belirli bir maksimum boyuta kadar bellekte tutulan, bu limiti aşınca diske yazılan bir dosya. +* Bu sayede görüntüler, videolar, büyük binary’ler vb. gibi büyük dosyalarda tüm belleği tüketmeden iyi çalışır. +* Upload edilen dosyadan metadata alabilirsiniz. +* file-like bir `async` arayüze sahiptir. +* `SpooledTemporaryFile` nesnesini dışa açar; bunu, file-like nesne bekleyen diğer library’lere doğrudan geçebilirsiniz. + +### `UploadFile` { #uploadfile } + +`UploadFile` şu attribute’lara sahiptir: + +* `filename`: Upload edilen orijinal dosya adını içeren bir `str` (örn. `myimage.jpg`). +* `content_type`: Content type’ı (MIME type / media type) içeren bir `str` (örn. `image/jpeg`). +* `file`: Bir `SpooledTemporaryFile` (bir file-like nesne). Bu, "file-like" nesne bekleyen diğer fonksiyonlara veya library’lere doğrudan verebileceğiniz gerçek Python file nesnesidir. + +`UploadFile` şu `async` method’lara sahiptir. Bunların hepsi altta ilgili dosya method’larını çağırır (dahili `SpooledTemporaryFile` kullanarak). + +* `write(data)`: Dosyaya `data` (`str` veya `bytes`) yazar. +* `read(size)`: Dosyadan `size` (`int`) kadar byte/karakter okur. +* `seek(offset)`: Dosyada `offset` (`int`) byte pozisyonuna gider. + * Örn. `await myfile.seek(0)` dosyanın başına gider. + * Bu, özellikle bir kez `await myfile.read()` çalıştırdıysanız ve sonra içeriği yeniden okumaya ihtiyaç duyuyorsanız faydalıdır. +* `close()`: Dosyayı kapatır. + +Bu method’ların hepsi `async` olduğundan, bunları "await" etmeniz gerekir. + +Örneğin, bir `async` *path operation function* içinde içeriği şöyle alabilirsiniz: + +```Python +contents = await myfile.read() +``` + +Normal bir `def` *path operation function* içindeyseniz `UploadFile.file`’a doğrudan erişebilirsiniz, örneğin: + +```Python +contents = myfile.file.read() +``` + +/// note | `async` Teknik Detaylar + +`async` method’ları kullandığınızda, **FastAPI** dosya method’larını bir threadpool içinde çalıştırır ve bunları await eder. + +/// + +/// note | Starlette Teknik Detaylar + +**FastAPI**’nin `UploadFile`’ı doğrudan **Starlette**’in `UploadFile`’ından türetilmiştir; ancak **Pydantic** ve FastAPI’nin diğer parçalarıyla uyumlu olması için bazı gerekli eklemeler yapar. + +/// + +## "Form Data" Nedir { #what-is-form-data } + +HTML formları (`
`) veriyi server’a gönderirken normalde JSON’dan farklı, veri için "özel" bir encoding kullanır. + +**FastAPI**, JSON yerine bu veriyi doğru yerden okuyacağından emin olur. + +/// note | Teknik Detaylar + +Formlardan gelen veri, dosya içermiyorsa normalde "media type" olarak `application/x-www-form-urlencoded` ile encode edilir. + +Ancak form dosya içeriyorsa `multipart/form-data` olarak encode edilir. `File` kullanırsanız, **FastAPI** dosyaları body’nin doğru kısmından alması gerektiğini bilir. + +Bu encoding’ler ve form alanları hakkında daha fazla okumak isterseniz MDN web dokümanlarındaki POST sayfasına bakın. + +/// + +/// warning | Uyarı + +Bir *path operation* içinde birden fazla `File` ve `Form` parametresi tanımlayabilirsiniz, ancak JSON olarak almayı beklediğiniz `Body` alanlarını ayrıca tanımlayamazsınız; çünkü request body `application/json` yerine `multipart/form-data` ile encode edilmiş olur. + +Bu, **FastAPI**’nin bir kısıtı değildir; HTTP protocol’ünün bir parçasıdır. + +/// + +## Opsiyonel Dosya Upload { #optional-file-upload } + +Standart type annotation’ları kullanıp varsayılan değeri `None` yaparak bir dosyayı opsiyonel hale getirebilirsiniz: + +{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} + +## Ek Metadata ile `UploadFile` { #uploadfile-with-additional-metadata } + +Ek metadata ayarlamak için `UploadFile` ile birlikte `File()` da kullanabilirsiniz. Örneğin: + +{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} + +## Birden Fazla Dosya Upload { #multiple-file-uploads } + +Aynı anda birden fazla dosya upload etmek mümkündür. + +Bu dosyalar, "form data" ile gönderilen aynı "form field" ile ilişkilendirilir. + +Bunu kullanmak için `bytes` veya `UploadFile` listesini tanımlayın: + +{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} + +Tanımladığınız gibi, `bytes` veya `UploadFile`’lardan oluşan bir `list` alırsınız. + +/// note | Teknik Detaylar + +`from starlette.responses import HTMLResponse` da kullanabilirsiniz. + +**FastAPI**, geliştiriciye kolaylık olsun diye `starlette.responses` modülünü `fastapi.responses` olarak da sağlar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. + +/// + +### Ek Metadata ile Birden Fazla Dosya Upload { #multiple-file-uploads-with-additional-metadata } + +Daha önce olduğu gibi, `UploadFile` için bile ek parametreler ayarlamak amacıyla `File()` kullanabilirsiniz: + +{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} + +## Özet { #recap } + +Request’te (form data olarak gönderilen) upload edilecek dosyaları tanımlamak için `File`, `bytes` ve `UploadFile` kullanın. diff --git a/docs/tr/docs/tutorial/request-form-models.md b/docs/tr/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..c35f956fc --- /dev/null +++ b/docs/tr/docs/tutorial/request-form-models.md @@ -0,0 +1,78 @@ +# Form Model'leri { #form-models } + +FastAPI'de **form field**'larını tanımlamak için **Pydantic model**'lerini kullanabilirsiniz. + +/// info | Bilgi + +Form'ları kullanmak için önce `python-multipart`'ı yükleyin. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu etkinleştirdiğinizden ve ardından paketi kurduğunuzdan emin olun. Örneğin: + +```console +$ pip install python-multipart +``` + +/// + +/// note | Not + +Bu özellik FastAPI `0.113.0` sürümünden itibaren desteklenmektedir. 🤓 + +/// + +## Form'lar için Pydantic Model'leri { #pydantic-models-for-forms } + +Sadece, **form field** olarak almak istediğiniz alanlarla bir **Pydantic model** tanımlayın ve ardından parametreyi `Form` olarak bildirin: + +{* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} + +**FastAPI**, request içindeki **form data**'dan **her bir field** için veriyi **çıkarır** ve size tanımladığınız Pydantic model'ini verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Bunu `/docs` altındaki doküman arayüzünde doğrulayabilirsiniz: + +
+ +
+ +## Fazladan Form Field'larını Yasaklayın { #forbid-extra-form-fields } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değildir), form field'larını yalnızca Pydantic model'inde tanımlananlarla **sınırlamak** isteyebilirsiniz. Ve **fazladan** gelen field'ları **yasaklayabilirsiniz**. + +/// note | Not + +Bu özellik FastAPI `0.114.0` sürümünden itibaren desteklenmektedir. 🤓 + +/// + +Herhangi bir `extra` field'ı `forbid` etmek için Pydantic'in model konfigürasyonunu kullanabilirsiniz: + +{* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} + +Bir client fazladan veri göndermeye çalışırsa, bir **error** response alır. + +Örneğin, client şu form field'larını göndermeye çalışırsa: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +`extra` field'ının izinli olmadığını söyleyen bir error response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Özet { #summary } + +FastAPI'de form field'larını tanımlamak için Pydantic model'lerini kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/request-forms-and-files.md b/docs/tr/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..86d26b498 --- /dev/null +++ b/docs/tr/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,41 @@ +# Request Forms ve Files { #request-forms-and-files } + +`File` ve `Form` kullanarak aynı anda hem dosyaları hem de form alanlarını tanımlayabilirsiniz. + +/// info | Bilgi + +Yüklenen dosyaları ve/veya form verisini almak için önce `python-multipart` paketini kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden ve ardından paketi kurduğunuzdan emin olun, örneğin: + +```console +$ pip install python-multipart +``` + +/// + +## `File` ve `Form` Import Edin { #import-file-and-form } + +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} + +## `File` ve `Form` Parametrelerini Tanımlayın { #define-file-and-form-parameters } + +Dosya ve form parametrelerini, `Body` veya `Query` için yaptığınız şekilde oluşturun: + +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} + +Dosyalar ve form alanları form data olarak upload edilir ve siz de dosyaları ve form alanlarını alırsınız. + +Ayrıca bazı dosyaları `bytes` olarak, bazılarını da `UploadFile` olarak tanımlayabilirsiniz. + +/// warning | Uyarı + +Bir *path operation* içinde birden fazla `File` ve `Form` parametresi tanımlayabilirsiniz; ancak request'in body'si `application/json` yerine `multipart/form-data` ile encode edileceği için, JSON olarak almayı beklediğiniz `Body` alanlarını aynı anda tanımlayamazsınız. + +Bu **FastAPI** kısıtı değildir; HTTP protokolünün bir parçasıdır. + +/// + +## Özet { #recap } + +Aynı request içinde hem veri hem de dosya almanız gerektiğinde `File` ve `Form`'u birlikte kullanın. diff --git a/docs/tr/docs/tutorial/response-model.md b/docs/tr/docs/tutorial/response-model.md new file mode 100644 index 000000000..f1d1f7d15 --- /dev/null +++ b/docs/tr/docs/tutorial/response-model.md @@ -0,0 +1,343 @@ +# Response Model - Dönüş Tipi { #response-model-return-type } + +*Path operation function* **dönüş tipini** (return type) type annotation ile belirtip response için kullanılacak tipi tanımlayabilirsiniz. + +Fonksiyon **parametreleri** için input data’da kullandığınız **type annotations** yaklaşımının aynısını burada da kullanabilirsiniz; Pydantic model’leri, list’ler, dict’ler, integer, boolean gibi skaler değerler vb. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPI bu dönüş tipini şunlar için kullanır: + +* Dönen veriyi **doğrulamak** (validate). + * Veri geçersizse (ör. bir field eksikse), bu *sizin* uygulama kodunuzun bozuk olduğu, olması gerekeni döndürmediği anlamına gelir; bu yüzden yanlış veri döndürmek yerine server error döner. Böylece siz ve client’larınız, beklenen veri ve veri şeklinin geleceğinden emin olabilirsiniz. +* OpenAPI’deki *path operation* içine response için bir **JSON Schema** eklemek. + * Bu, **otomatik dokümantasyon** tarafından kullanılır. + * Ayrıca otomatik client code generation araçları tarafından da kullanılır. + +Ama en önemlisi: + +* Çıktı verisini, dönüş tipinde tanımlı olana göre **sınırlar ve filtreler**. + * Bu, özellikle **güvenlik** açısından önemlidir; aşağıda daha fazlasını göreceğiz. + +## `response_model` Parametresi { #response-model-parameter } + +Bazı durumlarda, tam olarak dönüş tipinin söylediği gibi olmayan bir veriyi döndürmeniz gerekebilir ya da isteyebilirsiniz. + +Örneğin, **bir dict** veya bir veritabanı objesi döndürmek isteyip, ama **onu bir Pydantic model olarak declare etmek** isteyebilirsiniz. Böylece Pydantic model, döndürdüğünüz obje (ör. dict veya veritabanı objesi) için dokümantasyon, doğrulama vb. işlerin tamamını yapar. + +Eğer dönüş tipi annotation’ını eklerseniz, araçlar ve editörler (doğru şekilde) fonksiyonunuzun, declare ettiğiniz tipten (ör. Pydantic model) farklı bir tip (ör. dict) döndürdüğünü söyleyip hata verir. + +Bu gibi durumlarda, dönüş tipi yerine *path operation decorator* parametresi olan `response_model`’i kullanabilirsiniz. + +`response_model` parametresini herhangi bir *path operation* içinde kullanabilirsiniz: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* vb. + +{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} + +/// note | Not + +`response_model`’in "decorator" metodunun (`get`, `post` vb.) bir parametresi olduğuna dikkat edin. Body ve diğer parametreler gibi, sizin *path operation function*’ınızın parametresi değildir. + +/// + +`response_model`, Pydantic model field’ı için declare edeceğiniz aynı tipi alır; yani bir Pydantic model olabilir ama örneğin `List[Item]` gibi Pydantic model’lerden oluşan bir `list` de olabilir. + +FastAPI bu `response_model`’i; dokümantasyon, doğrulama vb. her şey için ve ayrıca çıktı verisini **tip tanımına göre dönüştürmek ve filtrelemek** için kullanır. + +/// tip | İpucu + +Editörünüzde, mypy vb. ile sıkı type kontrolü yapıyorsanız, fonksiyon dönüş tipini `Any` olarak declare edebilirsiniz. + +Böylece editöre bilerek her şeyi döndürebileceğinizi söylemiş olursunuz. Ancak FastAPI, `response_model` ile dokümantasyon, doğrulama, filtreleme vb. işlemleri yine de yapar. + +/// + +### `response_model` Önceliği { #response-model-priority } + +Hem dönüş tipi hem de `response_model` declare ederseniz, FastAPI’de `response_model` önceliklidir ve o kullanılır. + +Böylece, response model’den farklı bir tip döndürdüğünüz durumlarda bile editör ve mypy gibi araçlar için fonksiyonlarınıza doğru type annotation’lar ekleyebilir, aynı zamanda FastAPI’nin `response_model` üzerinden veri doğrulama, dokümantasyon vb. yapmasını sağlayabilirsiniz. + +Ayrıca `response_model=None` kullanarak, ilgili *path operation* için response model oluşturulmasını devre dışı bırakabilirsiniz. Bu, Pydantic field’ı olarak geçerli olmayan şeyler için type annotation eklediğinizde gerekebilir; aşağıdaki bölümlerden birinde bunun örneğini göreceksiniz. + +## Aynı input verisini geri döndürmek { #return-the-same-input-data } + +Burada `UserIn` adında bir model declare ediyoruz; bu model plaintext bir password içerecek: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} + +/// info | Bilgi + +`EmailStr` kullanmak için önce `email-validator` paketini kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden emin olun ve ardından örneğin şöyle kurun: + +```console +$ pip install email-validator +``` + +veya şöyle: + +```console +$ pip install "pydantic[email]" +``` + +/// + +Bu model ile hem input’u declare ediyoruz hem de output’u aynı model ile declare ediyoruz: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} + +Artık bir browser password ile user oluşturduğunda, API response içinde aynı password’ü geri döndürecek. + +Bu örnekte sorun olmayabilir; çünkü password’ü gönderen kullanıcı zaten aynı kişi. + +Ancak aynı modeli başka bir *path operation* için kullanırsak, kullanıcının password’lerini her client’a gönderiyor olabiliriz. + +/// danger + +Tüm riskleri bildiğinizden ve ne yaptığınızdan emin olmadığınız sürece, bir kullanıcının plain password’ünü asla saklamayın ve bu şekilde response içinde göndermeyin. + +/// + +## Bir output modeli ekleyin { #add-an-output-model } + +Bunun yerine, plaintext password içeren bir input modeli ve password’ü içermeyen bir output modeli oluşturabiliriz: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} + +Burada *path operation function* password içeren aynı input user’ı döndürüyor olsa bile: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} + +...`response_model` olarak, password’ü içermeyen `UserOut` modelimizi declare ettik: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} + +Dolayısıyla **FastAPI**, output model’de declare edilmemiş tüm verileri (Pydantic kullanarak) filtrelemekle ilgilenir. + +### `response_model` mi Return Type mı? { #response-model-or-return-type } + +Bu durumda iki model farklı olduğu için fonksiyon dönüş tipini `UserOut` olarak annotate etseydik, editör ve araçlar farklı class’lar olduğu için geçersiz bir tip döndürdüğümüzü söyleyip hata verecekti. + +Bu yüzden bu örnekte `response_model` parametresinde declare etmek zorundayız. + +...ama bunu nasıl aşabileceğinizi görmek için aşağıyı okumaya devam edin. + +## Return Type ve Veri Filtreleme { #return-type-and-data-filtering } + +Önceki örnekten devam edelim. Fonksiyonu **tek bir tip ile annotate etmek** istiyoruz; ama fonksiyondan gerçekte **daha fazla veri** içeren bir şey döndürebilmek istiyoruz. + +FastAPI’nin response model’i kullanarak veriyi **filtrelemeye** devam etmesini istiyoruz. Yani fonksiyon daha fazla veri döndürse bile response, sadece response model’de declare edilmiş field’ları içersin. + +Önceki örnekte class’lar farklı olduğu için `response_model` parametresini kullanmak zorundaydık. Ancak bu, editör ve araçların fonksiyon dönüş tipi kontrolünden gelen desteğini alamadığımız anlamına da geliyor. + +Ama bu tarz durumların çoğunda modelin amacı, bu örnekteki gibi bazı verileri **filtrelemek/kaldırmak** olur. + +Bu gibi durumlarda class’lar ve inheritance kullanarak, fonksiyon **type annotations** sayesinde editör ve araçlarda daha iyi destek alabilir, aynı zamanda FastAPI’nin **veri filtrelemesini** de koruyabiliriz. + +{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} + +Bununla birlikte, code type’lar açısından doğru olduğu için editörler ve mypy araç desteği verir; ayrıca FastAPI’den veri filtrelemeyi de alırız. + +Bu nasıl çalışıyor? Bir bakalım. 🤓 + +### Type Annotations ve Araç Desteği { #type-annotations-and-tooling } + +Önce editörler, mypy ve diğer araçlar bunu nasıl görür, ona bakalım. + +`BaseUser` temel field’lara sahiptir. Ardından `UserIn`, `BaseUser`’dan miras alır ve `password` field’ını ekler; yani iki modelin field’larının tamamını içerir. + +Fonksiyonun dönüş tipini `BaseUser` olarak annotate ediyoruz ama gerçekte bir `UserIn` instance’ı döndürüyoruz. + +Editör, mypy ve diğer araçlar buna itiraz etmez; çünkü typing açısından `UserIn`, `BaseUser`’ın subclass’ıdır. Bu da, bir `BaseUser` bekleniyorken `UserIn`’in *geçerli* bir tip olduğu anlamına gelir. + +### FastAPI Veri Filtreleme { #fastapi-data-filtering } + +FastAPI açısından ise dönüş tipini görür ve döndürdüğünüz şeyin **yalnızca** tipte declare edilen field’ları içerdiğinden emin olur. + +FastAPI, Pydantic ile içeride birkaç işlem yapar; böylece class inheritance kurallarının dönen veri filtrelemede aynen kullanılmasına izin vermez. Aksi halde beklediğinizden çok daha fazla veriyi response’ta döndürebilirdiniz. + +Bu sayede iki dünyanın da en iyisini alırsınız: **araç desteği** veren type annotations ve **veri filtreleme**. + +## Dokümanlarda görün { #see-it-in-the-docs } + +Otomatik dokümanları gördüğünüzde, input model ve output model’in her birinin kendi JSON Schema’sına sahip olduğunu kontrol edebilirsiniz: + + + +Ve her iki model de etkileşimli API dokümantasyonunda kullanılır: + + + +## Diğer Return Type Annotation’ları { #other-return-type-annotations } + +Bazı durumlarda Pydantic field olarak geçerli olmayan bir şey döndürebilir ve bunu fonksiyonda annotate edebilirsiniz; amaç sadece araçların (editör, mypy vb.) sağladığı desteği almaktır. + +### Doğrudan Response Döndürmek { #return-a-response-directly } + +En yaygın durum, [ileri seviye dokümanlarda daha sonra anlatıldığı gibi doğrudan bir Response döndürmektir](../advanced/response-directly.md){.internal-link target=_blank}. + +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} + +Bu basit durum FastAPI tarafından otomatik olarak ele alınır; çünkü dönüş tipi annotation’ı `Response` class’ıdır (veya onun bir subclass’ı). + +Araçlar da memnun olur; çünkü hem `RedirectResponse` hem `JSONResponse`, `Response`’un subclass’ıdır. Yani type annotation doğrudur. + +### Bir Response Subclass’ını Annotate Etmek { #annotate-a-response-subclass } + +Type annotation içinde `Response`’un bir subclass’ını da kullanabilirsiniz: + +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} + +Bu da çalışır; çünkü `RedirectResponse`, `Response`’un subclass’ıdır ve FastAPI bu basit durumu otomatik olarak yönetir. + +### Geçersiz Return Type Annotation’ları { #invalid-return-type-annotations } + +Ancak geçerli bir Pydantic tipi olmayan başka rastgele bir obje (ör. bir veritabanı objesi) döndürür ve fonksiyonu da öyle annotate ederseniz, FastAPI bu type annotation’dan bir Pydantic response model oluşturmaya çalışır ve başarısız olur. + +Aynı şey, farklı tipler arasında bir union kullandığınızda ve bu tiplerden biri veya birkaçı geçerli bir Pydantic tipi değilse de olur; örneğin şu kullanım patlar 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +...bu, type annotation Pydantic tipi olmadığı ve tek bir `Response` class’ı (veya subclass’ı) olmadığı için başarısız olur; bu, bir `Response` ile bir `dict` arasında union’dır (ikiden herhangi biri). + +### Response Model’i Devre Dışı Bırakmak { #disable-response-model } + +Yukarıdaki örnekten devam edersek; FastAPI’nin varsayılan olarak yaptığı veri doğrulama, dokümantasyon, filtreleme vb. işlemleri istemiyor olabilirsiniz. + +Ancak yine de editörler ve type checker’lar (ör. mypy) gibi araçların desteğini almak için fonksiyonda dönüş tipi annotation’ını korumak isteyebilirsiniz. + +Bu durumda `response_model=None` ayarlayarak response model üretimini devre dışı bırakabilirsiniz: + +{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} + +Bu, FastAPI’nin response model üretimini atlamasını sağlar; böylece FastAPI uygulamanızı etkilemeden ihtiyacınız olan herhangi bir return type annotation’ını kullanabilirsiniz. 🤓 + +## Response Model encoding parametreleri { #response-model-encoding-parameters } + +Response model’inizde şu şekilde default değerler olabilir: + +{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} + +* `description: Union[str, None] = None` (veya Python 3.10’da `str | None = None`) için default `None`’dır. +* `tax: float = 10.5` için default `10.5`’tir. +* `tags: List[str] = []` için default, boş bir list’tir: `[]`. + +Ancak gerçekte kaydedilmedilerse, bunları sonuçtan çıkarmak isteyebilirsiniz. + +Örneğin NoSQL veritabanında çok sayıda optional attribute içeren modelleriniz varsa, default değerlerle dolu çok uzun JSON response’ları göndermek istemeyebilirsiniz. + +### `response_model_exclude_unset` parametresini kullanın { #use-the-response-model-exclude-unset-parameter } + +*Path operation decorator* parametresi olarak `response_model_exclude_unset=True` ayarlayabilirsiniz: + +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} + +böylece response’a default değerler dahil edilmez; yalnızca gerçekten set edilmiş değerler gelir. + +Dolayısıyla ID’si `foo` olan item için bu *path operation*’a request atarsanız, response (default değerler olmadan) şöyle olur: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +/// info | Bilgi + +Ayrıca şunları da kullanabilirsiniz: + +* `response_model_exclude_defaults=True` +* `response_model_exclude_none=True` + +Bunlar, `exclude_defaults` ve `exclude_none` için Pydantic dokümanlarında anlatıldığı gibidir. + +/// + +#### Default’u olan field’lar için değer içeren data { #data-with-values-for-fields-with-defaults } + +Ama data’nız modelde default değeri olan field’lar için değer içeriyorsa, örneğin ID’si `bar` olan item gibi: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +bunlar response’a dahil edilir. + +#### Default değerlerle aynı değerlere sahip data { #data-with-the-same-values-as-the-defaults } + +Eğer data, default değerlerle aynı değerlere sahipse, örneğin ID’si `baz` olan item gibi: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI yeterince akıllıdır (aslında Pydantic yeterince akıllıdır) ve `description`, `tax`, `tags` default ile aynı olsa bile bunların explicit olarak set edildiğini (default’tan alınmadığını) anlar. + +Bu yüzden JSON response içinde yer alırlar. + +/// tip | İpucu + +Default değerlerin yalnızca `None` olmak zorunda olmadığını unutmayın. + +Bir list (`[]`), `10.5` gibi bir `float` vb. olabilirler. + +/// + +### `response_model_include` ve `response_model_exclude` { #response-model-include-and-response-model-exclude } + +Ayrıca *path operation decorator* parametreleri `response_model_include` ve `response_model_exclude`’u da kullanabilirsiniz. + +Bunlar; dahil edilecek attribute isimlerini (geri kalanını atlayarak) ya da hariç tutulacak attribute isimlerini (geri kalanını dahil ederek) belirten `str` değerlerinden oluşan bir `set` alır. + +Tek bir Pydantic model’iniz varsa ve output’tan bazı verileri hızlıca çıkarmak istiyorsanız, bu yöntem pratik bir kısayol olabilir. + +/// tip | İpucu + +Ancak yine de, bu parametreler yerine yukarıdaki yaklaşımı (birden fazla class kullanmayı) tercih etmeniz önerilir. + +Çünkü `response_model_include` veya `response_model_exclude` ile bazı attribute’ları atlıyor olsanız bile, uygulamanızın OpenAPI’sinde (ve dokümanlarda) üretilen JSON Schema hâlâ tam modelin JSON Schema’sı olacaktır. + +Bu durum, benzer şekilde çalışan `response_model_by_alias` için de geçerlidir. + +/// + +{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *} + +/// tip | İpucu + +`{"name", "description"}` sözdizimi, bu iki değere sahip bir `set` oluşturur. + +Bu, `set(["name", "description"])` ile eşdeğerdir. + +/// + +#### `set` yerine `list` kullanmak { #using-lists-instead-of-sets } + +Yanlışlıkla `set` yerine `list` veya `tuple` kullanırsanız, FastAPI bunu yine `set`’e çevirir ve doğru şekilde çalışır: + +{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} + +## Özet { #recap } + +Response model’leri tanımlamak ve özellikle private data’nın filtrelendiğinden emin olmak için *path operation decorator* parametresi `response_model`’i kullanın. + +Yalnızca explicit olarak set edilmiş değerleri döndürmek için `response_model_exclude_unset` kullanın. diff --git a/docs/tr/docs/tutorial/response-status-code.md b/docs/tr/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..57ae7bde3 --- /dev/null +++ b/docs/tr/docs/tutorial/response-status-code.md @@ -0,0 +1,101 @@ +# Response Status Code { #response-status-code } + +Bir response model tanımlayabildiğiniz gibi, herhangi bir *path operation* içinde `status_code` parametresiyle response için kullanılacak HTTP status code'u da belirtebilirsiniz: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* vb. + +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} + +/// note | Not + +`status_code`'un, "decorator" metodunun (`get`, `post`, vb.) bir parametresi olduğuna dikkat edin. Tüm parametreler ve body gibi, sizin *path operation function*'ınızın bir parametresi değildir. + +/// + +`status_code` parametresi, HTTP status code'u içeren bir sayı alır. + +/// info | Bilgi + +Alternatif olarak `status_code`, Python'un `http.HTTPStatus`'ı gibi bir `IntEnum` da alabilir. + +/// + +Bu sayede: + +* Response'da o status code döner. +* OpenAPI şemasında (dolayısıyla kullanıcı arayüzlerinde de) bu şekilde dokümante edilir: + + + +/// note | Not + +Bazı response code'lar (bir sonraki bölümde göreceğiz) response'un bir body'ye sahip olmadığını belirtir. + +FastAPI bunu bilir ve response body olmadığını söyleyen OpenAPI dokümantasyonunu üretir. + +/// + +## HTTP status code'lar hakkında { #about-http-status-codes } + +/// note | Not + +HTTP status code'ların ne olduğunu zaten biliyorsanız, bir sonraki bölüme geçin. + +/// + +HTTP'de, response'un bir parçası olarak 3 basamaklı sayısal bir status code gönderirsiniz. + +Bu status code'ların tanınmalarını sağlayan bir isimleri de vardır; ancak önemli olan kısım sayıdır. + +Kısaca: + +* `100 - 199` "Information" içindir. Doğrudan nadiren kullanırsınız. Bu status code'lara sahip response'lar body içeremez. +* **`200 - 299`** "Successful" response'lar içindir. En sık kullanacağınız aralık budur. + * `200`, varsayılan status code'dur ve her şeyin "OK" olduğunu ifade eder. + * Başka bir örnek `201` ("Created") olabilir. Genellikle veritabanında yeni bir kayıt oluşturduktan sonra kullanılır. + * Özel bir durum ise `204` ("No Content")'tür. Client'a döndürülecek içerik olmadığında kullanılır; bu nedenle response body olmamalıdır. +* **`300 - 399`** "Redirection" içindir. Bu status code'lara sahip response'lar, `304` ("Not Modified") hariç, body içerebilir de içermeyebilir de; `304` kesinlikle body içermemelidir. +* **`400 - 499`** "Client error" response'ları içindir. Muhtemelen en sık kullanacağınız ikinci aralık budur. + * Örneğin `404`, "Not Found" response'u içindir. + * Client kaynaklı genel hatalar için doğrudan `400` kullanabilirsiniz. +* `500 - 599` server hataları içindir. Neredeyse hiç doğrudan kullanmazsınız. Uygulama kodunuzun bir bölümünde ya da server'da bir şeyler ters giderse, otomatik olarak bu status code'lardan biri döner. + +/// tip | İpucu + +Her bir status code hakkında daha fazla bilgi almak ve hangi kodun ne için kullanıldığını görmek için HTTP status code'lar hakkında MDN dokümantasyonuna göz atın. + +/// + +## İsimleri hatırlamak için kısayol { #shortcut-to-remember-the-names } + +Önceki örneğe tekrar bakalım: + +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} + +`201`, "Created" için kullanılan status code'dur. + +Ancak bu kodların her birinin ne anlama geldiğini ezberlemek zorunda değilsiniz. + +`fastapi.status` içindeki kolaylık değişkenlerini (convenience variables) kullanabilirsiniz. + +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} + +Bunlar sadece kolaylık sağlar; aynı sayıyı taşırlar. Ancak bu şekilde editörün autocomplete özelliğiyle kolayca bulabilirsiniz: + + + +/// note | Teknik Detaylar + +`from starlette import status` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.status`'u `fastapi.status` olarak da sunar. Ancak bu aslında doğrudan Starlette'den gelir. + +/// + +## Varsayılanı değiştirmek { #changing-the-default } + +Daha sonra, [İleri Düzey Kullanıcı Kılavuzu](../advanced/response-change-status-code.md){.internal-link target=_blank} içinde, burada tanımladığınız varsayılanın dışında farklı bir status code nasıl döndüreceğinizi göreceksiniz. diff --git a/docs/tr/docs/tutorial/schema-extra-example.md b/docs/tr/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..a91dda892 --- /dev/null +++ b/docs/tr/docs/tutorial/schema-extra-example.md @@ -0,0 +1,202 @@ +# Request Örnek Verilerini Tanımlama { #declare-request-example-data } + +Uygulamanızın alabileceği veriler için örnekler (examples) tanımlayabilirsiniz. + +Bunu yapmanın birkaç yolu var. + +## Pydantic modellerinde ek JSON Schema verisi { #extra-json-schema-data-in-pydantic-models } + +Oluşturulan JSON Schema’ya eklenecek şekilde bir Pydantic model için `examples` tanımlayabilirsiniz. + +{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} + +Bu ek bilgi, o modelin çıktı **JSON Schema**’sına olduğu gibi eklenir ve API dokümanlarında kullanılır. + +Pydantic dokümanları: Configuration bölümünde anlatıldığı gibi, bir `dict` alan `model_config` niteliğini kullanabilirsiniz. + +Üretilen JSON Schema’da görünmesini istediğiniz (ör. `examples` dahil) her türlü ek veriyi içeren bir `dict` ile `"json_schema_extra"` ayarlayabilirsiniz. + +/// tip | İpucu + +Aynı tekniği JSON Schema’yı genişletmek ve kendi özel ek bilgilerinizi eklemek için de kullanabilirsiniz. + +Örneğin, bir frontend kullanıcı arayüzü için metadata eklemek vb. amaçlarla kullanılabilir. + +/// + +/// info | Bilgi + +OpenAPI 3.1.0 (FastAPI 0.99.0’dan beri kullanılıyor), **JSON Schema** standardının bir parçası olan `examples` için destek ekledi. + +Bundan önce yalnızca tek bir örnek için `example` anahtar kelimesini destekliyordu. Bu hâlâ OpenAPI 3.1.0 tarafından desteklenir; ancak artık deprecated durumdadır ve JSON Schema standardının parçası değildir. Bu nedenle `example` kullanımını `examples`’a taşımanız önerilir. 🤓 + +Daha fazlasını sayfanın sonunda okuyabilirsiniz. + +/// + +## `Field` ek argümanları { #field-additional-arguments } + +Pydantic modelleriyle `Field()` kullanırken ek `examples` de tanımlayabilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} + +## JSON Schema - OpenAPI içinde `examples` { #examples-in-json-schema-openapi } + +Aşağıdakilerden herhangi birini kullanırken: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +OpenAPI içindeki **JSON Schema**’larına eklenecek ek bilgilerle birlikte bir `examples` grubu da tanımlayabilirsiniz. + +### `examples` ile `Body` { #body-with-examples } + +Burada `Body()` içinde beklenen veri için tek bir örnek içeren `examples` geçiriyoruz: + +{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} + +### Doküman arayüzünde örnek { #example-in-the-docs-ui } + +Yukarıdaki yöntemlerden herhangi biriyle `/docs` içinde şöyle görünür: + + + +### Birden fazla `examples` ile `Body` { #body-with-multiple-examples } + +Elbette birden fazla `examples` da geçebilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} + +Bunu yaptığınızda, örnekler bu body verisi için dahili **JSON Schema**’nın bir parçası olur. + +Buna rağmen, time of writing this, doküman arayüzünü gösteren araç olan Swagger UI, **JSON Schema** içindeki veriler için birden fazla örneği göstermeyi desteklemiyor. Ancak aşağıda bir çözüm yolu var. + +### OpenAPI’ye özel `examples` { #openapi-specific-examples } + +**JSON Schema** `examples`’ı desteklemeden önce OpenAPI, yine `examples` adlı farklı bir alanı destekliyordu. + +Bu **OpenAPI’ye özel** `examples`, OpenAPI spesifikasyonunda başka bir bölümde yer alır. Her bir JSON Schema’nın içinde değil, **her bir *path operation* detayları** içinde bulunur. + +Swagger UI da bu özel `examples` alanını bir süredir destekliyor. Dolayısıyla bunu, **doküman arayüzünde** farklı **örnekleri göstermek** için kullanabilirsiniz. + +OpenAPI’ye özel bu `examples` alanının şekli, (bir `list` yerine) **birden fazla örnek** içeren bir `dict`’tir; her örnek ayrıca **OpenAPI**’ye eklenecek ekstra bilgiler içerir. + +Bu, OpenAPI’nin içerdiği JSON Schema’ların içine girmez; bunun yerine doğrudan *path operation* üzerinde, dışarıda yer alır. + +### `openapi_examples` Parametresini Kullanma { #using-the-openapi-examples-parameter } + +FastAPI’de OpenAPI’ye özel `examples`’ı, şu araçlar için `openapi_examples` parametresiyle tanımlayabilirsiniz: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +`dict`’in anahtarları her bir örneği tanımlar; her bir değer ise başka bir `dict`’tir. + +`examples` içindeki her bir örnek `dict`’i şunları içerebilir: + +* `summary`: Örnek için kısa açıklama. +* `description`: Markdown metni içerebilen uzun açıklama. +* `value`: Gösterilecek gerçek örnek (ör. bir `dict`). +* `externalValue`: `value`’a alternatif; örneğe işaret eden bir URL. Ancak bu, `value` kadar çok araç tarafından desteklenmiyor olabilir. + +Şöyle kullanabilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} + +### Doküman Arayüzünde OpenAPI Örnekleri { #openapi-examples-in-the-docs-ui } + +`Body()`’ye `openapi_examples` eklendiğinde `/docs` şöyle görünür: + + + +## Teknik Detaylar { #technical-details } + +/// tip | İpucu + +Zaten **FastAPI** sürümü **0.99.0 veya üzerini** kullanıyorsanız, büyük olasılıkla bu detayları **atlanabilirsiniz**. + +Bunlar daha çok OpenAPI 3.1.0’ın henüz mevcut olmadığı eski sürümler için geçerlidir. + +Bunu kısa bir OpenAPI ve JSON Schema **tarih dersi** gibi düşünebilirsiniz. 🤓 + +/// + +/// warning | Uyarı + +Bunlar **JSON Schema** ve **OpenAPI** standartları hakkında oldukça teknik detaylardır. + +Yukarıdaki fikirler sizin için zaten çalışıyorsa bu kadarı yeterli olabilir; muhtemelen bu detaylara ihtiyacınız yoktur, gönül rahatlığıyla atlayabilirsiniz. + +/// + +OpenAPI 3.1.0’dan önce OpenAPI, **JSON Schema**’nın daha eski ve değiştirilmiş bir sürümünü kullanıyordu. + +JSON Schema’da `examples` yoktu; bu yüzden OpenAPI, değiştirilmiş sürümüne kendi `example` alanını ekledi. + +OpenAPI ayrıca spesifikasyonun diğer bölümlerine de `example` ve `examples` alanlarını ekledi: + +* `Parameter Object` (spesifikasyonda) — FastAPI’de şunlar tarafından kullanılıyordu: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`; `content` alanında, `Media Type Object` üzerinde (spesifikasyonda) — FastAPI’de şunlar tarafından kullanılıyordu: + * `Body()` + * `File()` + * `Form()` + +/// info | Bilgi + +Bu eski OpenAPI’ye özel `examples` parametresi, FastAPI `0.103.0` sürümünden beri `openapi_examples` olarak kullanılıyor. + +/// + +### JSON Schema’nın `examples` alanı { #json-schemas-examples-field } + +Sonrasında JSON Schema, spesifikasyonun yeni bir sürümüne `examples` alanını ekledi. + +Ardından yeni OpenAPI 3.1.0, bu yeni `examples` alanını içeren en güncel sürümü (JSON Schema 2020-12) temel aldı. + +Ve artık, deprecated olan eski tekil (ve özel) `example` alanına kıyasla bu yeni `examples` alanı önceliklidir. + +JSON Schema’daki bu yeni `examples` alanı, OpenAPI’de başka yerlerde kullanılan (yukarıda anlatılan) metadata’lı `dict` yapısından farklı olarak **sadece örneklerden oluşan bir `list`**’tir. + +/// info | Bilgi + +OpenAPI 3.1.0, JSON Schema ile bu yeni ve daha basit entegrasyonla yayımlandıktan sonra bile bir süre, otomatik dokümantasyonu sağlayan araç Swagger UI OpenAPI 3.1.0’ı desteklemiyordu (5.0.0 sürümünden beri destekliyor 🎉). + +Bu nedenle, FastAPI’nin 0.99.0 öncesi sürümleri OpenAPI 3.1.0’dan daha düşük sürümleri kullanmaya devam etti. + +/// + +### Pydantic ve FastAPI `examples` { #pydantic-and-fastapi-examples } + +Bir Pydantic modelinin içine `schema_extra` ya da `Field(examples=["something"])` kullanarak `examples` eklediğinizde, bu örnek o Pydantic modelinin **JSON Schema**’sına eklenir. + +Ve Pydantic modelinin bu **JSON Schema**’sı, API’nizin **OpenAPI**’sine dahil edilir; ardından doküman arayüzünde kullanılır. + +FastAPI 0.99.0’dan önceki sürümlerde (0.99.0 ve üzeri daha yeni OpenAPI 3.1.0’ı kullanır) `Query()`, `Body()` vb. diğer araçlarla `example` veya `examples` kullandığınızda, bu örnekler o veriyi tanımlayan JSON Schema’ya (OpenAPI’nin kendi JSON Schema sürümüne bile) eklenmiyordu; bunun yerine doğrudan OpenAPI’deki *path operation* tanımına ekleniyordu (JSON Schema kullanan OpenAPI bölümlerinin dışında). + +Ancak artık FastAPI 0.99.0 ve üzeri OpenAPI 3.1.0 kullandığı (JSON Schema 2020-12) ve Swagger UI 5.0.0 ve üzeriyle birlikte, her şey daha tutarlı ve örnekler JSON Schema’ya dahil ediliyor. + +### Swagger UI ve OpenAPI’ye özel `examples` { #swagger-ui-and-openapi-specific-examples } + +Swagger UI (2023-08-26 itibarıyla) birden fazla JSON Schema örneğini desteklemediği için, kullanıcıların dokümanlarda birden fazla örnek göstermesi mümkün değildi. + +Bunu çözmek için FastAPI `0.103.0`, yeni `openapi_examples` parametresiyle aynı eski **OpenAPI’ye özel** `examples` alanını tanımlamayı **desteklemeye başladı**. 🤓 + +### Özet { #summary } + +Eskiden tarihten pek hoşlanmadığımı söylerdim... şimdi bakın, "teknoloji tarihi" dersi anlatıyorum. 😅 + +Kısacası, **FastAPI 0.99.0 veya üzerine yükseltin**; her şey çok daha **basit, tutarlı ve sezgisel** olur ve bu tarihsel detayların hiçbirini bilmeniz gerekmez. 😎 diff --git a/docs/tr/docs/tutorial/security/first-steps.md b/docs/tr/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..7e0a70a02 --- /dev/null +++ b/docs/tr/docs/tutorial/security/first-steps.md @@ -0,0 +1,203 @@ +# Güvenlik - İlk Adımlar { #security-first-steps } + +**backend** API’nizin bir domain’de olduğunu düşünelim. + +Ve başka bir domain’de ya da aynı domain’in farklı bir path’inde (veya bir mobil uygulamada) bir **frontend**’iniz var. + +Ve frontend’in, **username** ve **password** kullanarak backend ile kimlik doğrulaması yapabilmesini istiyorsunuz. + +Bunu **FastAPI** ile **OAuth2** kullanarak oluşturabiliriz. + +Ama ihtiyacınız olan küçük bilgi parçalarını bulmak için uzun spesifikasyonun tamamını okuma zahmetine girmeyelim. + +Güvenliği yönetmek için **FastAPI**’nin sunduğu araçları kullanalım. + +## Nasıl Görünüyor { #how-it-looks } + +Önce kodu kullanıp nasıl çalıştığına bakalım, sonra neler olup bittiğini anlamak için geri döneriz. + +## `main.py` Oluşturun { #create-main-py } + +Örneği `main.py` adlı bir dosyaya kopyalayın: + +{* ../../docs_src/security/tutorial001_an_py39.py *} + +## Çalıştırın { #run-it } + +/// info | Bilgi + +`python-multipart` paketi, `pip install "fastapi[standard]"` komutunu çalıştırdığınızda **FastAPI** ile birlikte otomatik olarak kurulur. + +Ancak `pip install fastapi` komutunu kullanırsanız, `python-multipart` paketi varsayılan olarak dahil edilmez. + +Elle kurmak için bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden emin olun ve ardından şununla kurun: + +```console +$ pip install python-multipart +``` + +Bunun nedeni, **OAuth2**’nin `username` ve `password` göndermek için "form data" kullanmasıdır. + +/// + +Örneği şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## Kontrol Edin { #check-it } + +Etkileşimli dokümantasyona gidin: http://127.0.0.1:8000/docs. + +Şuna benzer bir şey göreceksiniz: + + + +/// check | Authorize butonu! + +Artık parıl parıl yeni bir "Authorize" butonunuz var. + +Ayrıca *path operation*’ınızın sağ üst köşesinde tıklayabileceğiniz küçük bir kilit simgesi de bulunuyor. + +/// + +Ve ona tıklarsanız, `username` ve `password` (ve diğer opsiyonel alanları) girebileceğiniz küçük bir yetkilendirme formu görürsünüz: + + + +/// note | Not + +Formda ne yazdığınızın önemi yok; şimdilik çalışmayacak. Ama birazdan oraya da geleceğiz. + +/// + +Bu, elbette son kullanıcılar için bir frontend değil; ancak tüm API’nizi etkileşimli şekilde belgelemek için harika bir otomatik araçtır. + +Frontend ekibi tarafından kullanılabilir (bu ekip siz de olabilirsiniz). + +Üçüncü taraf uygulamalar ve sistemler tarafından kullanılabilir. + +Ve aynı uygulamayı debug etmek, kontrol etmek ve test etmek için sizin tarafınızdan da kullanılabilir. + +## `password` Flow { #the-password-flow } + +Şimdi biraz geri dönüp bunların ne olduğuna bakalım. + +`password` "flow"u, OAuth2’de güvenlik ve authentication’ı yönetmek için tanımlanmış yöntemlerden ("flow"lardan) biridir. + +OAuth2, backend’in veya API’nin, kullanıcıyı authenticate eden server’dan bağımsız olabilmesi için tasarlanmıştır. + +Ancak bu örnekte, aynı **FastAPI** uygulaması hem API’yi hem de authentication’ı yönetecek. + +O yüzden basitleştirilmiş bu bakış açısından üzerinden geçelim: + +* Kullanıcı frontend’de `username` ve `password` yazar ve `Enter`’a basar. +* Frontend (kullanıcının browser’ında çalışır), bu `username` ve `password` değerlerini API’mizdeki belirli bir URL’ye gönderir (`tokenUrl="token"` ile tanımlanan). +* API, `username` ve `password` değerlerini kontrol eder ve bir "token" ile response döner (henüz bunların hiçbirini implement etmedik). + * "Token", daha sonra bu kullanıcıyı doğrulamak için kullanabileceğimiz içerik taşıyan bir string’dir. + * Normalde token’ın bir süre sonra süresi dolacak şekilde ayarlanması beklenir. + * Böylece kullanıcının bir noktada tekrar giriş yapması gerekir. + * Ayrıca token çalınırsa risk daha düşük olur. Çoğu durumda, sonsuza kadar çalışacak kalıcı bir anahtar gibi değildir. +* Frontend bu token’ı geçici olarak bir yerde saklar. +* Kullanıcı frontend’de tıklayarak web uygulamasının başka bir bölümüne gider. +* Frontend’in API’den daha fazla veri alması gerekir. + * Ancak o endpoint için authentication gereklidir. + * Bu yüzden API’mizle authenticate olmak için `Authorization` header’ını, `Bearer ` + token değeriyle gönderir. + * Token `foobar` içeriyorsa `Authorization` header’ının içeriği `Bearer foobar` olur. + +## **FastAPI**’nin `OAuth2PasswordBearer`’ı { #fastapis-oauth2passwordbearer } + +**FastAPI**, bu güvenlik özelliklerini implement etmek için farklı soyutlama seviyelerinde çeşitli araçlar sağlar. + +Bu örnekte **OAuth2**’yi, **Password** flow ile, **Bearer** token kullanarak uygulayacağız. Bunu `OAuth2PasswordBearer` sınıfı ile yaparız. + +/// info | Bilgi + +"Bearer" token tek seçenek değildir. + +Ama bizim kullanım senaryomuz için en iyi seçenek odur. + +Ayrıca bir OAuth2 uzmanı değilseniz ve ihtiyaçlarınıza daha uygun başka bir seçeneğin neden gerekli olduğunu net olarak bilmiyorsanız, çoğu kullanım senaryosu için de en uygun seçenek olacaktır. + +Bu durumda bile **FastAPI**, onu oluşturabilmeniz için gereken araçları sunar. + +/// + +`OAuth2PasswordBearer` sınıfının bir instance’ını oluştururken `tokenUrl` parametresini veririz. Bu parametre, client’ın (kullanıcının browser’ında çalışan frontend’in) token almak için `username` ve `password` göndereceği URL’yi içerir. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} + +/// tip | İpucu + +Burada `tokenUrl="token"`, henüz oluşturmadığımız göreli bir URL olan `token`’ı ifade eder. Göreli URL olduğu için `./token` ile eşdeğerdir. + +Göreli URL kullandığımız için, API’niz `https://example.com/` adresinde olsaydı `https://example.com/token` anlamına gelirdi. Ama API’niz `https://example.com/api/v1/` adresinde olsaydı, bu kez `https://example.com/api/v1/token` anlamına gelirdi. + +Göreli URL kullanmak, [Behind a Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank} gibi daha ileri kullanım senaryolarında bile uygulamanızın çalışmaya devam etmesini garanti etmek açısından önemlidir. + +/// + +Bu parametre o endpoint’i / *path operation*’ı oluşturmaz; fakat `/token` URL’sinin client’ın token almak için kullanması gereken URL olduğunu bildirir. Bu bilgi OpenAPI’de, dolayısıyla etkileşimli API dokümantasyon sistemlerinde kullanılır. + +Birazdan gerçek path operation’ı da oluşturacağız. + +/// info | Teknik Detaylar + +Eğer çok katı bir "Pythonista" iseniz, `token_url` yerine `tokenUrl` şeklindeki parametre adlandırma stilini sevmeyebilirsiniz. + +Bunun nedeni, OpenAPI spesifikasyonundaki isimle aynı adın kullanılmasıdır. Böylece bu güvenlik şemalarından herhangi biri hakkında daha fazla araştırma yapmanız gerekirse, adı kopyalayıp yapıştırarak kolayca daha fazla bilgi bulabilirsiniz. + +/// + +`oauth2_scheme` değişkeni, `OAuth2PasswordBearer`’ın bir instance’ıdır; ama aynı zamanda "callable"dır. + +Şu şekilde çağrılabilir: + +```Python +oauth2_scheme(some, parameters) +``` + +Dolayısıyla `Depends` ile kullanılabilir. + +### Kullanın { #use-it } + +Artık `Depends` ile bir dependency olarak `oauth2_scheme`’i geçebilirsiniz. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Bu dependency, *path operation function* içindeki `token` parametresine atanacak bir `str` sağlar. + +**FastAPI**, bu dependency’yi OpenAPI şemasında (ve otomatik API dokümanlarında) bir "security scheme" tanımlamak için kullanabileceğini bilir. + +/// info | Teknik Detaylar + +**FastAPI**, bir dependency içinde tanımlanan `OAuth2PasswordBearer` sınıfını OpenAPI’de security scheme tanımlamak için kullanabileceğini bilir; çünkü bu sınıf `fastapi.security.oauth2.OAuth2`’den kalıtım alır, o da `fastapi.security.base.SecurityBase`’den kalıtım alır. + +OpenAPI (ve otomatik API dokümanları) ile entegre olan tüm security araçları `SecurityBase`’den kalıtım alır; **FastAPI** bu sayede onları OpenAPI’ye nasıl entegre edeceğini anlayabilir. + +/// + +## Ne Yapar { #what-it-does } + +Request içinde `Authorization` header’ını arar, değerin `Bearer ` + bir token olup olmadığını kontrol eder ve token’ı `str` olarak döndürür. + +Eğer `Authorization` header’ını görmezse ya da değer `Bearer ` token’ı içermiyorsa, doğrudan 401 status code hatasıyla (`UNAUTHORIZED`) response döner. + +Token’ın var olup olmadığını kontrol edip ayrıca hata döndürmenize bile gerek yoktur. Fonksiyonunuz çalışıyorsa, token içinde bir `str` olacağından emin olabilirsiniz. + +Bunu şimdiden etkileşimli dokümanlarda deneyebilirsiniz: + + + +Henüz token’ın geçerliliğini doğrulamıyoruz, ama başlangıç için bu bile yeterli. + +## Özet { #recap } + +Yani sadece 3 veya 4 ekstra satırla, şimdiden ilkel de olsa bir güvenlik katmanı elde etmiş oldunuz. diff --git a/docs/tr/docs/tutorial/security/get-current-user.md b/docs/tr/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..9f56c7628 --- /dev/null +++ b/docs/tr/docs/tutorial/security/get-current-user.md @@ -0,0 +1,105 @@ +# Mevcut Kullanıcıyı Alma { #get-current-user } + +Önceki bölümde güvenlik sistemi (dependency injection sistemine dayanır) *path operation function*'a `str` olarak bir `token` veriyordu: + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Ancak bu hâlâ pek kullanışlı değil. + +Bize mevcut kullanıcıyı verecek şekilde düzenleyelim. + +## Bir kullanıcı modeli oluşturun { #create-a-user-model } + +Önce bir Pydantic kullanıcı modeli oluşturalım. + +Body'leri bildirmek için Pydantic'i nasıl kullanıyorsak, aynı şekilde onu başka her yerde de kullanabiliriz: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} + +## `get_current_user` dependency'si oluşturun { #create-a-get-current-user-dependency } + +Bir `get_current_user` dependency'si oluşturalım. + +Dependency'lerin alt dependency'leri olabileceğini hatırlıyor musunuz? + +`get_current_user`, daha önce oluşturduğumuz `oauth2_scheme` ile aynı dependency'yi kullanacak. + +Daha önce *path operation* içinde doğrudan yaptığımız gibi, yeni dependency'miz `get_current_user`, alt dependency olan `oauth2_scheme` üzerinden `str` olarak bir `token` alacak: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} + +## Kullanıcıyı alın { #get-the-user } + +`get_current_user`, oluşturduğumuz (sahte) bir yardımcı (utility) fonksiyonu kullanacak; bu fonksiyon `str` olarak bir token alır ve Pydantic `User` modelimizi döndürür: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} + +## Mevcut kullanıcıyı enjekte edin { #inject-the-current-user } + +Artık *path operation* içinde `get_current_user` ile aynı `Depends` yaklaşımını kullanabiliriz: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} + +`current_user` tipini Pydantic `User` modeli olarak belirttiğimize dikkat edin. + +Bu sayede fonksiyonun içinde otomatik tamamlama ve tip kontrollerinin tamamından faydalanırız. + +/// tip | İpucu + +Request body'lerinin de Pydantic modelleri ile bildirildiğini hatırlıyor olabilirsiniz. + +Burada `Depends` kullandığınız için **FastAPI** karışıklık yaşamaz. + +/// + +/// check | Ek bilgi + +Bu dependency sisteminin tasarımı, hepsi `User` modeli döndüren farklı dependency'lere (farklı "dependable"lara) sahip olmamıza izin verir. + +Bu tipte veri döndürebilen yalnızca tek bir dependency ile sınırlı değiliz. + +/// + +## Diğer modeller { #other-models } + +Artık *path operation function* içinde mevcut kullanıcıyı doğrudan alabilir ve güvenlik mekanizmalarını `Depends` kullanarak **Dependency Injection** seviyesinde yönetebilirsiniz. + +Ayrıca güvenlik gereksinimleri için herhangi bir model veya veri kullanabilirsiniz (bu örnekte bir Pydantic `User` modeli). + +Ancak belirli bir data model, class ya da type kullanmak zorunda değilsiniz. + +Modelinizde bir `id` ve `email` olsun, ama `username` olmasın mı istiyorsunuz? Elbette. Aynı araçları kullanabilirsiniz. + +Sadece bir `str` mı istiyorsunuz? Ya da sadece bir `dict`? Veya doğrudan bir veritabanı class model instance'ı? Hepsi aynı şekilde çalışır. + +Uygulamanıza giriş yapan kullanıcılar yok da robotlar, botlar veya yalnızca bir access token'a sahip başka sistemler mi var? Yine, her şey aynı şekilde çalışır. + +Uygulamanız için neye ihtiyacınız varsa o türden bir model, class ve veritabanı kullanın. **FastAPI**, dependency injection sistemiyle bunları destekler. + +## Kod boyutu { #code-size } + +Bu örnek biraz uzun görünebilir. Güvenlik, data model'ler, utility fonksiyonlar ve *path operation*'ları aynı dosyada bir araya getirdiğimizi unutmayın. + +Ama kritik nokta şu: + +Güvenlik ve dependency injection tarafını bir kez yazarsınız. + +İstediğiniz kadar karmaşık hâle getirebilirsiniz. Yine de hepsi tek bir yerde ve sadece bir kez yazılmış olur. Üstelik tüm esneklikle. + +Sonrasında aynı güvenlik sistemini kullanan binlerce endpoint (*path operation*) olabilir. + +Ve bunların hepsi (ya da istediğiniz bir kısmı) bu dependency'leri veya oluşturacağınız başka dependency'leri yeniden kullanmaktan faydalanabilir. + +Hatta bu binlerce *path operation* 3 satır kadar kısa olabilir: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} + +## Özet { #recap } + +Artık *path operation function* içinde mevcut kullanıcıyı doğrudan alabilirsiniz. + +Yolun yarısına geldik. + +Kullanıcının/istemcinin gerçekten `username` ve `password` göndermesini sağlayacak bir *path operation* eklememiz gerekiyor. + +Sırada bu var. diff --git a/docs/tr/docs/tutorial/security/index.md b/docs/tr/docs/tutorial/security/index.md new file mode 100644 index 000000000..a592db6df --- /dev/null +++ b/docs/tr/docs/tutorial/security/index.md @@ -0,0 +1,106 @@ +# Güvenlik { #security } + +Güvenlik, authentication ve authorization’ı ele almanın birçok yolu vardır. + +Ve bu konu genellikle karmaşık ve "zor"dur. + +Birçok framework ve sistemde yalnızca security ve authentication’ı yönetmek bile ciddi miktarda emek ve kod gerektirir (çoğu durumda yazılan toplam kodun %50’si veya daha fazlası olabilir). + +**FastAPI**, tüm security spesifikasyonlarını baştan sona inceleyip öğrenmek zorunda kalmadan **Security** konusunu kolay, hızlı ve standart bir şekilde ele almanıza yardımcı olacak çeşitli araçlar sunar. + +Ama önce, küçük birkaç kavrama bakalım. + +## Acelem var? { #in-a-hurry } + +Bu terimlerin hiçbirini umursamıyorsanız ve sadece kullanıcı adı ve parola tabanlı authentication ile security’yi *hemen şimdi* eklemeniz gerekiyorsa, bir sonraki bölümlere geçin. + +## OAuth2 { #oauth2 } + +OAuth2, authentication ve authorization’ı yönetmek için çeşitli yöntemleri tanımlayan bir spesifikasyondur. + +Oldukça kapsamlı bir spesifikasyondur ve birkaç karmaşık use case’i kapsar. + +"Üçüncü taraf" kullanarak authentication yapmanın yollarını da içerir. + +"Facebook, Google, X (Twitter), GitHub ile giriş yap" bulunan sistemlerin arka planda kullandığı şey de budur. + +### OAuth 1 { #oauth-1 } + +OAuth 1 de vardı; OAuth2’den çok farklıdır ve daha karmaşıktır, çünkü iletişimi nasıl şifreleyeceğinize dair doğrudan spesifikasyonlar içeriyordu. + +Günümüzde pek popüler değildir veya pek kullanılmaz. + +OAuth2 ise iletişimin nasıl şifreleneceğini belirtmez; uygulamanızın HTTPS ile sunulmasını bekler. + +/// tip | İpucu + +**deployment** bölümünde Traefik ve Let's Encrypt kullanarak ücretsiz şekilde HTTPS’i nasıl kuracağınızı göreceksiniz. + +/// + +## OpenID Connect { #openid-connect } + +OpenID Connect, **OAuth2** tabanlı başka bir spesifikasyondur. + +OAuth2’de nispeten belirsiz kalan bazı noktaları tanımlayarak onu daha birlikte çalışabilir (interoperable) hâle getirmeye çalışır. + +Örneğin, Google ile giriş OpenID Connect kullanır (arka planda OAuth2 kullanır). + +Ancak Facebook ile giriş OpenID Connect’i desteklemez. Kendine özgü bir OAuth2 çeşidi vardır. + +### OpenID ("OpenID Connect" değil) { #openid-not-openid-connect } + +Bir de "OpenID" spesifikasyonu vardı. Bu da **OpenID Connect** ile aynı problemi çözmeye çalışıyordu ama OAuth2 tabanlı değildi. + +Dolayısıyla tamamen ayrı, ek bir sistemdi. + +Günümüzde pek popüler değildir veya pek kullanılmaz. + +## OpenAPI { #openapi } + +OpenAPI (önceden Swagger olarak biliniyordu), API’ler inşa etmek için açık spesifikasyondur (artık Linux Foundation’ın bir parçası). + +**FastAPI**, **OpenAPI** tabanlıdır. + +Bu sayede birden fazla otomatik etkileşimli dokümantasyon arayüzü, code generation vb. mümkün olur. + +OpenAPI, birden fazla security "scheme" tanımlamanın bir yolunu sunar. + +Bunları kullanarak, etkileşimli dokümantasyon sistemleri de dahil olmak üzere tüm bu standart tabanlı araçlardan faydalanabilirsiniz. + +OpenAPI şu security scheme’lerini tanımlar: + +* `apiKey`: uygulamaya özel bir anahtar; şuradan gelebilir: + * Bir query parameter. + * Bir header. + * Bir cookie. +* `http`: standart HTTP authentication sistemleri, örneğin: + * `bearer`: `Authorization` header’ı; değeri `Bearer ` + bir token olacak şekilde. Bu, OAuth2’den gelir. + * HTTP Basic authentication. + * HTTP Digest, vb. +* `oauth2`: OAuth2 ile security’yi yönetmenin tüm yolları ("flow" olarak adlandırılır). + * Bu flow’ların birçoğu, bir OAuth 2.0 authentication provider (Google, Facebook, X (Twitter), GitHub vb.) oluşturmak için uygundur: + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Ancak, aynı uygulamanın içinde doğrudan authentication yönetmek için mükemmel şekilde kullanılabilecek özel bir "flow" vardır: + * `password`: sonraki bazı bölümlerde bunun örnekleri ele alınacak. +* `openIdConnect`: OAuth2 authentication verisinin otomatik olarak nasıl keşfedileceğini tanımlamanın bir yolunu sunar. + * Bu otomatik keşif, OpenID Connect spesifikasyonunda tanımlanan şeydir. + + +/// tip | İpucu + +Google, Facebook, X (Twitter), GitHub vb. gibi diğer authentication/authorization provider’larını entegre etmek de mümkündür ve nispeten kolaydır. + +En karmaşık kısım, bu tür bir authentication/authorization provider’ı inşa etmektir; ancak **FastAPI** ağır işleri sizin yerinize yaparken bunu kolayca yapabilmeniz için araçlar sunar. + +/// + +## **FastAPI** yardımcı araçları { #fastapi-utilities } + +FastAPI, `fastapi.security` modülünde bu security scheme’lerinin her biri için, bu mekanizmaları kullanmayı kolaylaştıran çeşitli araçlar sağlar. + +Sonraki bölümlerde, **FastAPI**’nin sunduğu bu araçları kullanarak API’nize nasıl security ekleyeceğinizi göreceksiniz. + +Ayrıca bunun etkileşimli dokümantasyon sistemine nasıl otomatik olarak entegre edildiğini de göreceksiniz. diff --git a/docs/tr/docs/tutorial/security/oauth2-jwt.md b/docs/tr/docs/tutorial/security/oauth2-jwt.md new file mode 100644 index 000000000..716761157 --- /dev/null +++ b/docs/tr/docs/tutorial/security/oauth2-jwt.md @@ -0,0 +1,273 @@ +# Password ile OAuth2 (ve hashing), JWT token'ları ile Bearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } + +Artık tüm security flow elimizde olduğuna göre, uygulamayı gerçekten güvenli hâle getirelim: JWT token'ları ve güvenli password hashing kullanacağız. + +Bu kodu uygulamanızda gerçekten kullanabilirsiniz; password hash'lerini veritabanınıza kaydedebilirsiniz, vb. + +Bir önceki bölümde bıraktığımız yerden başlayıp üzerine ekleyerek ilerleyeceğiz. + +## JWT Hakkında { #about-jwt } + +JWT, "JSON Web Tokens" anlamına gelir. + +Bir JSON nesnesini, boşluk içermeyen uzun ve yoğun bir string'e kodlamak için kullanılan bir standarttır. Şuna benzer: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +Şifrelenmiş değildir; yani herkes içeriğindeki bilgiyi geri çıkarabilir. + +Ancak imzalanmıştır. Bu yüzden, sizin ürettiğiniz bir token'ı aldığınızda, gerçekten onu sizin ürettiğinizi doğrulayabilirsiniz. + +Bu şekilde, örneğin 1 haftalık süre sonu (expiration) olan bir token oluşturabilirsiniz. Sonra kullanıcı ertesi gün token ile geri geldiğinde, kullanıcının hâlâ sisteminizde oturum açmış olduğunu bilirsiniz. + +Bir hafta sonra token'ın süresi dolar; kullanıcı yetkilendirilmez ve yeni bir token almak için tekrar giriş yapmak zorunda kalır. Ayrıca kullanıcı (veya üçüncü bir taraf) token'ı değiştirip süre sonunu farklı göstermek isterse bunu tespit edebilirsiniz; çünkü imzalar eşleşmez. + +JWT token'larıyla oynayıp nasıl çalıştıklarını görmek isterseniz https://jwt.io adresine bakın. + +## `PyJWT` Kurulumu { #install-pyjwt } + +Python'da JWT token'larını üretmek ve doğrulamak için `PyJWT` kurmamız gerekiyor. + +Bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktif edin ve ardından `pyjwt` kurun: + +
+ +```console +$ pip install pyjwt + +---> 100% +``` + +
+ +/// info | Bilgi + +RSA veya ECDSA gibi dijital imza algoritmaları kullanmayı planlıyorsanız, `pyjwt[crypto]` bağımlılığı olan `cryptography` kütüphanesini kurmalısınız. + +Daha fazla bilgi için PyJWT Installation docs sayfasını okuyabilirsiniz. + +/// + +## Password hashing { #password-hashing } + +"Hashing", bazı içerikleri (bu örnekte bir password) anlamsız görünen bir bayt dizisine (pratikte bir string) dönüştürmek demektir. + +Aynı içeriği (aynı password'ü) her seferinde verirseniz, her seferinde aynı anlamsız çıktıyı elde edersiniz. + +Ancak bu anlamsız çıktıdan geri password'e dönüştürme yapılamaz. + +### Neden password hashing kullanılır { #why-use-password-hashing } + +Veritabanınız çalınırsa, hırsız kullanıcılarınızın düz metin (plaintext) password'lerini değil, sadece hash'leri elde eder. + +Dolayısıyla, o password'ü başka bir sistemde denemek kolay olmaz (pek çok kullanıcı her yerde aynı password'ü kullandığı için bu tehlikeli olurdu). + +## `pwdlib` Kurulumu { #install-pwdlib } + +pwdlib, password hash'leriyle çalışmak için çok iyi bir Python paketidir. + +Birçok güvenli hashing algoritmasını ve bunlarla çalışmak için yardımcı araçları destekler. + +Önerilen algoritma "Argon2"dir. + +Bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktif edin ve sonra Argon2 ile birlikte pwdlib'i kurun: + +
+ +```console +$ pip install "pwdlib[argon2]" + +---> 100% +``` + +
+ +/// tip | İpucu + +`pwdlib` ile, **Django** tarafından oluşturulmuş password'leri, bir **Flask** security eklentisinin ürettiklerini veya başka birçok kaynaktan gelen password'leri okuyabilecek şekilde bile yapılandırabilirsiniz. + +Böylece örneğin bir Django uygulamasındaki verileri aynı veritabanında bir FastAPI uygulamasıyla paylaşabilirsiniz. Ya da aynı veritabanını kullanarak bir Django uygulamasını kademeli şekilde taşıyabilirsiniz. + +Ayrıca kullanıcılarınız, aynı anda hem Django uygulamanızdan hem de **FastAPI** uygulamanızdan login olabilir. + +/// + +## Password'leri hash'leme ve doğrulama { #hash-and-verify-the-passwords } + +Gerekli araçları `pwdlib` içinden import edelim. + +Önerilen ayarlarla bir PasswordHash instance'ı oluşturalım; bunu password'leri hash'lemek ve doğrulamak için kullanacağız. + +/// tip | İpucu + +pwdlib, bcrypt hashing algoritmasını da destekler; ancak legacy algoritmaları içermez. Eski hash'lerle çalışmak için passlib kütüphanesini kullanmanız önerilir. + +Örneğin, başka bir sistemin (Django gibi) ürettiği password'leri okuyup doğrulayabilir, ancak yeni password'leri Argon2 veya Bcrypt gibi farklı bir algoritmayla hash'leyebilirsiniz. + +Ve aynı anda hepsiyle uyumlu kalabilirsiniz. + +/// + +Kullanıcıdan gelen password'ü hash'lemek için bir yardımcı (utility) fonksiyon oluşturalım. + +Sonra, alınan password'ün kayıttaki hash ile eşleşip eşleşmediğini doğrulayan başka bir yardımcı fonksiyon yazalım. + +Bir tane de kullanıcıyı authenticate edip geri döndüren bir yardımcı fonksiyon ekleyelim. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} + +/// note | Not + +Yeni (sahte) veritabanı `fake_users_db`'ye bakarsanız, hash'lenmiş password'ün artık nasıl göründüğünü görebilirsiniz: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. + +/// + +## JWT token'larını yönetme { #handle-jwt-tokens } + +Kurulu modülleri import edelim. + +JWT token'larını imzalamak için kullanılacak rastgele bir secret key oluşturalım. + +Güvenli, rastgele bir secret key üretmek için şu komutu kullanın: + +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +``` + +
+ +Çıktıyı `SECRET_KEY` değişkenine kopyalayın (örnektekini kullanmayın). + +JWT token'ını imzalamak için kullanılan algoritmayı tutacak `ALGORITHM` adlı bir değişken oluşturup değerini `"HS256"` yapın. + +Token'ın süre sonu (expiration) için bir değişken oluşturun. + +Response için token endpoint'inde kullanılacak bir Pydantic Model tanımlayın. + +Yeni bir access token üretmek için bir yardımcı fonksiyon oluşturun. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} + +## Dependency'leri güncelleme { #update-the-dependencies } + +`get_current_user` fonksiyonunu, öncekiyle aynı token'ı alacak şekilde güncelleyelim; ancak bu sefer JWT token'larını kullanacağız. + +Gelen token'ı decode edin, doğrulayın ve mevcut kullanıcıyı döndürün. + +Token geçersizse, hemen bir HTTP hatası döndürün. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} + +## `/token` *path operation*'ını güncelleme { #update-the-token-path-operation } + +Token'ın süre sonu için bir `timedelta` oluşturun. + +Gerçek bir JWT access token üretip döndürün. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} + +### JWT "subject" `sub` Hakkında Teknik Detaylar { #technical-details-about-the-jwt-subject-sub } + +JWT spesifikasyonu, token'ın konusu (subject) için `sub` adlı bir anahtar olduğunu söyler. + +Bunu kullanmak zorunlu değildir; ancak kullanıcı kimliğini koymak için uygun yer burasıdır, bu yüzden burada onu kullanıyoruz. + +JWT, sadece bir kullanıcıyı tanımlamak ve API'nizde doğrudan işlem yapmasına izin vermek dışında başka amaçlarla da kullanılabilir. + +Örneğin bir "araba"yı veya bir "blog post"u tanımlayabilirsiniz. + +Sonra o varlık için izinler ekleyebilirsiniz; örneğin (araba için) "drive" ya da (blog için) "edit". + +Ardından bu JWT token'ını bir kullanıcıya (veya bot'a) verebilirsiniz; onlar da, hesapları olmasına bile gerek kalmadan, sadece API'nizin bunun için ürettiği JWT token'ıyla bu aksiyonları gerçekleştirebilir (arabayı sürmek veya blog post'u düzenlemek gibi). + +Bu fikirlerle JWT, çok daha gelişmiş senaryolarda kullanılabilir. + +Bu durumlarda, birden fazla varlığın aynı ID'ye sahip olması mümkündür; örneğin `foo` (kullanıcı `foo`, araba `foo`, blog post `foo`). + +Dolayısıyla ID çakışmalarını önlemek için, kullanıcı için JWT token oluştururken `sub` anahtarının değerine bir önek ekleyebilirsiniz; örneğin `username:`. Bu örnekte `sub` değeri şöyle olabilirdi: `username:johndoe`. + +Unutmamanız gereken önemli nokta şudur: `sub` anahtarı, tüm uygulama genelinde benzersiz bir tanımlayıcı olmalı ve string olmalıdır. + +## Kontrol Edelim { #check-it } + +Server'ı çalıştırın ve docs'a gidin: http://127.0.0.1:8000/docs. + +Şuna benzer bir arayüz göreceksiniz: + + + +Uygulamayı, öncekiyle aynı şekilde authorize edin. + +Şu kimlik bilgilerini kullanarak: + +Username: `johndoe` +Password: `secret` + +/// check | Ek bilgi + +Kodun hiçbir yerinde düz metin password "`secret`" yok; sadece hash'lenmiş hâli var. + +/// + + + +`/users/me/` endpoint'ini çağırın; response şöyle olacaktır: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false +} +``` + + + +Developer tools'u açarsanız, gönderilen verinin sadece token'ı içerdiğini görebilirsiniz. Password sadece kullanıcıyı authenticate edip access token almak için yapılan ilk request'te gönderilir, sonrasında gönderilmez: + + + +/// note | Not + +`Authorization` header'ına dikkat edin; değeri `Bearer ` ile başlıyor. + +/// + +## `scopes` ile İleri Seviye Kullanım { #advanced-usage-with-scopes } + +OAuth2'nin "scopes" kavramı vardır. + +Bunları kullanarak bir JWT token'a belirli bir izin seti ekleyebilirsiniz. + +Sonra bu token'ı bir kullanıcıya doğrudan veya bir üçüncü tarafa verip, API'nizle belirli kısıtlarla etkileşime girmesini sağlayabilirsiniz. + +Nasıl kullanıldıklarını ve **FastAPI** ile nasıl entegre olduklarını, ileride **Advanced User Guide** içinde öğreneceksiniz. + +## Özet { #recap } + +Şimdiye kadar gördüklerinizle, OAuth2 ve JWT gibi standartları kullanarak güvenli bir **FastAPI** uygulaması kurabilirsiniz. + +Neredeyse her framework'te security'yi ele almak oldukça hızlı bir şekilde karmaşık bir konu hâline gelir. + +Bunu çok basitleştiren birçok paket, veri modeli, veritabanı ve mevcut özelliklerle ilgili pek çok ödün vermek zorunda kalır. Hatta bazıları işi aşırı basitleştirirken arka planda güvenlik açıkları da barındırır. + +--- + +**FastAPI**, hiçbir veritabanı, veri modeli veya araç konusunda ödün vermez. + +Projenize en uygun olanları seçebilmeniz için size tam esneklik sağlar. + +Ayrıca `pwdlib` ve `PyJWT` gibi iyi bakımı yapılan ve yaygın kullanılan paketleri doğrudan kullanabilirsiniz; çünkü **FastAPI**, haricî paketleri entegre etmek için karmaşık mekanizmalara ihtiyaç duymaz. + +Buna rağmen, esneklikten, sağlamlıktan veya güvenlikten ödün vermeden süreci mümkün olduğunca basitleştiren araçları sağlar. + +Ve OAuth2 gibi güvenli, standart protokolleri nispeten basit bir şekilde kullanabilir ve uygulayabilirsiniz. + +Aynı standartları izleyerek, daha ince taneli (fine-grained) bir izin sistemi için OAuth2 "scopes" kullanımını **Advanced User Guide** içinde daha detaylı öğrenebilirsiniz. Scopes'lu OAuth2; Facebook, Google, GitHub, Microsoft, X (Twitter) vb. pek çok büyük kimlik doğrulama sağlayıcısının, üçüncü taraf uygulamaların kullanıcıları adına API'leriyle etkileşebilmesine izin vermek için kullandığı mekanizmadır. diff --git a/docs/tr/docs/tutorial/security/simple-oauth2.md b/docs/tr/docs/tutorial/security/simple-oauth2.md new file mode 100644 index 000000000..88efd98e5 --- /dev/null +++ b/docs/tr/docs/tutorial/security/simple-oauth2.md @@ -0,0 +1,289 @@ +# Password ve Bearer ile Basit OAuth2 { #simple-oauth2-with-password-and-bearer } + +Şimdi önceki bölümün üzerine inşa edip, eksik parçaları ekleyerek tam bir güvenlik akışı oluşturalım. + +## `username` ve `password`’ü Alma { #get-the-username-and-password } + +`username` ve `password`’ü almak için **FastAPI** security yardımcı araçlarını kullanacağız. + +OAuth2, (bizim kullandığımız) "password flow" kullanılırken client/kullanıcının form verisi olarak `username` ve `password` alanlarını göndermesi gerektiğini belirtir. + +Ayrıca spesifikasyon, bu alanların adlarının tam olarak böyle olması gerektiğini söyler. Yani `user-name` veya `email` işe yaramaz. + +Ancak merak etmeyin, frontend’de son kullanıcılarınıza dilediğiniz gibi gösterebilirsiniz. + +Veritabanı model(ler)inizde de istediğiniz başka isimleri kullanabilirsiniz. + +Fakat login *path operation*’ı için, spesifikasyonla uyumlu olmak (ve örneğin entegre API dokümantasyon sistemini kullanabilmek) adına bu isimleri kullanmamız gerekiyor. + +Spesifikasyon ayrıca `username` ve `password`’ün form verisi olarak gönderilmesi gerektiğini de söyler (yani burada JSON yok). + +### `scope` { #scope } + +Spesifikasyon, client’ın "`scope`" adlı başka bir form alanı da gönderebileceğini söyler. + +Form alanının adı `scope`’tur (tekil), ama aslında boşluklarla ayrılmış "scope"’lardan oluşan uzun bir string’dir. + +Her bir "scope" sadece bir string’dir (boşluk içermez). + +Genelde belirli güvenlik izinlerini (permission) belirtmek için kullanılırlar, örneğin: + +* `users:read` veya `users:write` yaygın örneklerdir. +* `instagram_basic` Facebook / Instagram tarafından kullanılır. +* `https://www.googleapis.com/auth/drive` Google tarafından kullanılır. + +/// info | Bilgi + +OAuth2’de bir "scope", gerekli olan belirli bir izni ifade eden basit bir string’dir. + +`:` gibi başka karakterler içermesi veya URL olması önemli değildir. + +Bu detaylar implementasyon’a özeldir. + +OAuth2 açısından bunlar sadece string’lerdir. + +/// + +## `username` ve `password`’ü Almak İçin Kod { #code-to-get-the-username-and-password } + +Şimdi bunu yönetmek için **FastAPI**’nin sağladığı araçları kullanalım. + +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } + +Önce `OAuth2PasswordRequestForm`’u import edin ve `/token` için *path operation* içinde `Depends` ile dependency olarak kullanın: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} + +`OAuth2PasswordRequestForm`, şu alanları içeren bir form body tanımlayan bir class dependency’sidir: + +* `username`. +* `password`. +* Boşlukla ayrılmış string’lerden oluşan büyük bir string olarak opsiyonel `scope` alanı. +* Opsiyonel `grant_type`. + +/// tip | İpucu + +OAuth2 spesifikasyonu aslında `grant_type` alanını sabit bir `password` değeriyle *zorunlu kılar*, ancak `OAuth2PasswordRequestForm` bunu zorlamaz. + +Bunu zorlamak istiyorsanız, `OAuth2PasswordRequestForm` yerine `OAuth2PasswordRequestFormStrict` kullanın. + +/// + +* Opsiyonel `client_id` (bu örnekte ihtiyacımız yok). +* Opsiyonel `client_secret` (bu örnekte ihtiyacımız yok). + +/// info | Bilgi + +`OAuth2PasswordRequestForm`, `OAuth2PasswordBearer` gibi **FastAPI**’ye özel “özel bir sınıf” değildir. + +`OAuth2PasswordBearer`, bunun bir security scheme olduğunu **FastAPI**’ye bildirir. Bu yüzden OpenAPI’ye o şekilde eklenir. + +Ama `OAuth2PasswordRequestForm` sadece bir class dependency’dir; bunu kendiniz de yazabilirdiniz ya da doğrudan `Form` parametreleri tanımlayabilirdiniz. + +Fakat çok yaygın bir kullanım olduğu için **FastAPI** bunu işleri kolaylaştırmak adına doğrudan sağlar. + +/// + +### Form Verisini Kullanma { #use-the-form-data } + +/// tip | İpucu + +`OAuth2PasswordRequestForm` dependency class’ının instance’ında boşluklarla ayrılmış uzun string olarak bir `scope` attribute’u olmaz; bunun yerine gönderilen her scope için gerçek string listesini içeren `scopes` attribute’u olur. + +Bu örnekte `scopes` kullanmıyoruz, ama ihtiyacınız olursa bu özellik hazır. + +/// + +Şimdi form alanındaki `username`’i kullanarak (sahte) veritabanından kullanıcı verisini alın. + +Böyle bir kullanıcı yoksa, "Incorrect username or password" diyerek bir hata döndürelim. + +Hata için `HTTPException` exception’ını kullanıyoruz: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} + +### Password’ü Kontrol Etme { #check-the-password } + +Bu noktada veritabanından kullanıcı verisine sahibiz, ancak password’ü henüz kontrol etmedik. + +Önce bu veriyi Pydantic `UserInDB` modeline koyalım. + +Asla düz metin (plaintext) password kaydetmemelisiniz; bu yüzden (sahte) password hashing sistemini kullanacağız. + +Password’ler eşleşmezse, aynı hatayı döndürürüz. + +#### Password hashing { #password-hashing } + +"Hashing" şudur: bir içeriği (bu örnekte password) anlaşılmaz görünen bayt dizisine (yani bir string’e) dönüştürmek. + +Aynı içeriği (aynı password’ü) her verdiğinizde, birebir aynı anlamsız görünen çıktıyı elde edersiniz. + +Ama bu anlamsız çıktıyı tekrar password’e geri çeviremezsiniz. + +##### Neden password hashing kullanılır { #why-use-password-hashing } + +Veritabanınız çalınırsa, hırsız kullanıcılarınızın düz metin password’lerine değil, sadece hash’lere sahip olur. + +Dolayısıyla hırsız, aynı password’leri başka bir sistemde denemeye çalışamaz (birçok kullanıcı her yerde aynı password’ü kullandığı için bu tehlikeli olurdu). + +{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} + +#### `**user_dict` Hakkında { #about-user-dict } + +`UserInDB(**user_dict)` şu anlama gelir: + +*`user_dict` içindeki key ve value’ları doğrudan key-value argümanları olarak geçir; şu ifadeyle eşdeğerdir:* + +```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 | Bilgi + +`**user_dict` için daha kapsamlı bir açıklama için [**Extra Models** dokümantasyonundaki ilgili bölüme](../extra-models.md#about-user-in-dict){.internal-link target=_blank} geri dönüp bakın. + +/// + +## Token’ı Döndürme { #return-the-token } + +`token` endpoint’inin response’u bir JSON object olmalıdır. + +Bir `token_type` içermelidir. Biz "Bearer" token’ları kullandığımız için token type "`bearer`" olmalıdır. + +Ayrıca `access_token` içermelidir; bunun değeri access token’ımızı içeren bir string olmalıdır. + +Bu basit örnekte tamamen güvensiz davranıp token olarak aynı `username`’i döndüreceğiz. + +/// tip | İpucu + +Bir sonraki bölümde, password hashing ve JWT token’ları ile gerçekten güvenli bir implementasyon göreceksiniz. + +Ama şimdilik ihtiyacımız olan spesifik detaylara odaklanalım. + +/// + +{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} + +/// tip | İpucu + +Spesifikasyona göre, bu örnekteki gibi `access_token` ve `token_type` içeren bir JSON döndürmelisiniz. + +Bunu kendi kodunuzda kendiniz yapmalı ve bu JSON key’lerini kullandığınızdan emin olmalısınız. + +Spesifikasyonlara uyum için, doğru yapmanız gereken neredeyse tek şey budur. + +Geri kalanını **FastAPI** sizin yerinize yönetir. + +/// + +## Dependency’leri Güncelleme { #update-the-dependencies } + +Şimdi dependency’lerimizi güncelleyeceğiz. + +`current_user`’ı *sadece* kullanıcı aktifse almak istiyoruz. + +Bu yüzden, `get_current_user`’ı dependency olarak kullanan ek bir dependency olan `get_current_active_user`’ı oluşturuyoruz. + +Bu iki dependency de kullanıcı yoksa veya pasifse sadece HTTP hatası döndürecek. + +Dolayısıyla endpoint’imizde kullanıcıyı ancak kullanıcı varsa, doğru şekilde authenticate edildiyse ve aktifse alacağız: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} + +/// info | Bilgi + +Burada `Bearer` değerine sahip ek `WWW-Authenticate` header’ını döndürmemiz de spesifikasyonun bir parçasıdır. + +Herhangi bir HTTP (hata) durum kodu 401 "UNAUTHORIZED", ayrıca `WWW-Authenticate` header’ı da döndürmelidir. + +Bearer token’lar (bizim durumumuz) için bu header’ın değeri `Bearer` olmalıdır. + +Aslında bu ekstra header’ı atlayabilirsiniz, yine de çalışır. + +Ama spesifikasyonlara uyumlu olması için burada eklenmiştir. + +Ayrıca, bunu bekleyen ve kullanan araçlar olabilir (şimdi veya ileride) ve bu da sizin ya da kullanıcılarınız için faydalı olabilir. + +Standartların faydası da bu... + +/// + +## Çalışır Halini Görün { #see-it-in-action } + +Etkileşimli dokümanları açın: http://127.0.0.1:8000/docs. + +### Authenticate Olma { #authenticate } + +"Authorize" butonuna tıklayın. + +Şu bilgileri kullanın: + +User: `johndoe` + +Password: `secret` + + + +Sistemde authenticate olduktan sonra şöyle görürsünüz: + + + +### Kendi Kullanıcı Verinizi Alma { #get-your-own-user-data } + +Şimdi `/users/me` path’inde `GET` operasyonunu kullanın. + +Kullanıcınızın verisini şöyle alırsınız: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false, + "hashed_password": "fakehashedsecret" +} +``` + + + +Kilit ikonuna tıklayıp logout olursanız ve sonra aynı operasyonu tekrar denerseniz, şu şekilde bir HTTP 401 hatası alırsınız: + +```JSON +{ + "detail": "Not authenticated" +} +``` + +### Pasif Kullanıcı { #inactive-user } + +Şimdi pasif bir kullanıcıyla deneyin; şu bilgilerle authenticate olun: + +User: `alice` + +Password: `secret2` + +Ve `/users/me` path’inde `GET` operasyonunu kullanmayı deneyin. + +Şöyle bir "Inactive user" hatası alırsınız: + +```JSON +{ + "detail": "Inactive user" +} +``` + +## Özet { #recap } + +Artık API’niz için `username` ve `password` tabanlı, eksiksiz bir güvenlik sistemi implement etmek için gerekli araçlara sahipsiniz. + +Bu araçlarla güvenlik sistemini herhangi bir veritabanıyla ve herhangi bir user veya veri modeliyle uyumlu hale getirebilirsiniz. + +Eksik kalan tek detay, bunun henüz gerçekten "güvenli" olmamasıdır. + +Bir sonraki bölümde güvenli bir password hashing kütüphanesini ve JWT token’larını nasıl kullanacağınızı göreceksiniz. diff --git a/docs/tr/docs/tutorial/sql-databases.md b/docs/tr/docs/tutorial/sql-databases.md new file mode 100644 index 000000000..e1638cb04 --- /dev/null +++ b/docs/tr/docs/tutorial/sql-databases.md @@ -0,0 +1,357 @@ +# SQL (İlişkisel) Veritabanları { #sql-relational-databases } + +**FastAPI**, SQL (ilişkisel) bir veritabanı kullanmanızı zorunlu kılmaz. Ancak isterseniz **istediğiniz herhangi bir veritabanını** kullanabilirsiniz. + +Burada SQLModel kullanarak bir örnek göreceğiz. + +**SQLModel**, SQLAlchemy ve Pydantic’in üzerine inşa edilmiştir. **FastAPI**’nin yazarı tarafından, **SQL veritabanları** kullanması gereken FastAPI uygulamalarıyla mükemmel uyum sağlaması için geliştirilmiştir. + +/// tip | İpucu + +İstediğiniz başka bir SQL veya NoSQL veritabanı kütüphanesini kullanabilirsiniz (bazı durumlarda "ORMs" olarak adlandırılır). FastAPI sizi hiçbir şeye zorlamaz. + +/// + +SQLModel, SQLAlchemy tabanlı olduğu için SQLAlchemy’nin **desteklediği herhangi bir veritabanını** kolayca kullanabilirsiniz (bu da SQLModel tarafından da desteklendikleri anlamına gelir), örneğin: + +* PostgreSQL +* MySQL +* SQLite +* Oracle +* Microsoft SQL Server, vb. + +Bu örnekte **SQLite** kullanacağız; çünkü tek bir dosya kullanır ve Python’da yerleşik desteği vardır. Yani bu örneği kopyalayıp olduğu gibi çalıştırabilirsiniz. + +Daha sonra, production uygulamanız için **PostgreSQL** gibi bir veritabanı sunucusu kullanmak isteyebilirsiniz. + +/// tip | İpucu + +Frontend ve daha fazla araçla birlikte **FastAPI** + **PostgreSQL** içeren resmi bir proje oluşturucu (project generator) var: https://github.com/fastapi/full-stack-fastapi-template + +/// + +Bu çok basit ve kısa bir eğitimdir. Veritabanları genelinde, SQL hakkında veya daha ileri özellikler hakkında öğrenmek isterseniz SQLModel dokümantasyonuna gidin. + +## `SQLModel` Kurulumu { #install-sqlmodel } + +Önce [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktive edin ve ardından `sqlmodel`’i yükleyin: + +
+ +```console +$ pip install sqlmodel +---> 100% +``` + +
+ +## Tek Model ile Uygulamayı Oluşturma { #create-the-app-with-a-single-model } + +Önce, tek bir **SQLModel** modeliyle uygulamanın en basit ilk sürümünü oluşturacağız. + +Aşağıda, **birden fazla model** kullanarak güvenliği ve esnekliği artırıp geliştireceğiz. + +### Modelleri Oluşturma { #create-models } + +`SQLModel`’i import edin ve bir veritabanı modeli oluşturun: + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} + +`Hero` sınıfı, bir Pydantic modeline çok benzer (hatta altta aslında *bir Pydantic modelidir*). + +Birkaç fark var: + +* `table=True`, SQLModel’e bunun bir *table model* olduğunu söyler; SQL veritabanında bir **table**’ı temsil etmelidir, sadece bir *data model* değildir (diğer normal Pydantic sınıflarında olduğu gibi). + +* `Field(primary_key=True)`, SQLModel’e `id`’nin SQL veritabanındaki **primary key** olduğunu söyler (SQL primary key’leri hakkında daha fazlasını SQLModel dokümantasyonunda öğrenebilirsiniz). + + **Not:** primary key alanı için `int | None` kullanıyoruz; böylece Python kodunda *`id` olmadan bir nesne oluşturabiliriz* (`id=None`) ve veritabanının *kaydederken bunu üreteceğini* varsayarız. SQLModel, veritabanının `id` sağlayacağını anlar ve *veritabanı şemasında sütunu null olamayan bir `INTEGER`* olarak tanımlar. Detaylar için primary key’ler hakkında SQLModel dokümantasyonuna bakın. + +* `Field(index=True)`, SQLModel’e bu sütun için bir **SQL index** oluşturmasını söyler; bu da bu sütuna göre filtrelenmiş verileri okurken veritabanında daha hızlı arama yapılmasını sağlar. + + SQLModel, `str` olarak tanımlanan bir şeyin SQL tarafında `TEXT` (veya veritabanına bağlı olarak `VARCHAR`) tipinde bir sütun olacağını bilir. + +### Engine Oluşturma { #create-an-engine } + +Bir SQLModel `engine`’i (altta aslında bir SQLAlchemy `engine`’idir) veritabanına olan **bağlantıları tutan** yapıdır. + +Tüm kodunuzun aynı veritabanına bağlanması için **tek bir `engine` nesnesi** kullanırsınız. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} + +`check_same_thread=False` kullanmak, FastAPI’nin aynı SQLite veritabanını farklı thread’lerde kullanmasına izin verir. Bu gereklidir; çünkü **tek bir request** **birden fazla thread** kullanabilir (örneğin dependency’lerde). + +Merak etmeyin; kodun yapısı gereği, ileride **her request için tek bir SQLModel *session*** kullandığımızdan emin olacağız. Zaten `check_same_thread` de temelde bunu mümkün kılmaya çalışır. + +### Table’ları Oluşturma { #create-the-tables } + +Sonra `SQLModel.metadata.create_all(engine)` kullanan bir fonksiyon ekleyerek tüm *table model*’ler için **table’ları oluştururuz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} + +### Session Dependency’si Oluşturma { #create-a-session-dependency } + +Bir **`Session`**, **nesneleri memory’de** tutar ve verideki gerekli değişiklikleri takip eder; ardından veritabanıyla iletişim kurmak için **`engine` kullanır**. + +`yield` ile, her request için yeni bir `Session` sağlayacak bir FastAPI **dependency** oluşturacağız. Bu da her request’te tek session kullanmamızı garanti eder. + +Ardından bu dependency’yi kullanacak kodun geri kalanını sadeleştirmek için `Annotated` ile `SessionDep` dependency’sini oluştururuz. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} + +### Startup’ta Veritabanı Table’larını Oluşturma { #create-database-tables-on-startup } + +Uygulama başlarken veritabanı table’larını oluşturacağız. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} + +Burada bir uygulama startup event’inde table’ları oluşturuyoruz. + +Production’da büyük ihtimalle uygulamayı başlatmadan önce çalışan bir migration script’i kullanırsınız. + +/// tip | İpucu + +SQLModel, Alembic’i saran migration araçlarına sahip olacak; ancak şimdilik Alembic’i doğrudan kullanabilirsiniz. + +/// + +### Hero Oluşturma { #create-a-hero } + +Her SQLModel modeli aynı zamanda bir Pydantic modeli olduğu için, Pydantic modelleriyle kullanabildiğiniz **type annotation**’larda aynı şekilde kullanabilirsiniz. + +Örneğin `Hero` tipinde bir parametre tanımlarsanız, bu parametre **JSON body**’den okunur. + +Aynı şekilde, bunu fonksiyonun **return type**’ı olarak da tanımlayabilirsiniz; böylece verinin şekli otomatik API docs arayüzünde görünür. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} + +Burada `SessionDep` dependency’sini (bir `Session`) kullanarak yeni `Hero`’yu `Session` instance’ına ekliyoruz, değişiklikleri veritabanına commit ediyoruz, `hero` içindeki veriyi refresh ediyoruz ve sonra geri döndürüyoruz. + +### Hero’ları Okuma { #read-heroes } + +`select()` kullanarak veritabanından `Hero`’ları **okuyabiliriz**. Sonuçları sayfalama (pagination) yapmak için `limit` ve `offset` ekleyebiliriz. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} + +### Tek Bir Hero Okuma { #read-one-hero } + +Tek bir `Hero` **okuyabiliriz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} + +### Hero Silme { #delete-a-hero } + +Bir `Hero`’yu **silebiliriz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} + +### Uygulamayı Çalıştırma { #run-the-app } + +Uygulamayı çalıştırabilirsiniz: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Sonra `/docs` arayüzüne gidin; **FastAPI**’nin API’yi **dokümante etmek** için bu **modelleri** kullandığını göreceksiniz. Ayrıca veriyi **serialize** ve **validate** etmek için de onları kullanacaktır. + +
+ +
+ +## Birden Fazla Model ile Uygulamayı Güncelleme { #update-the-app-with-multiple-models } + +Şimdi bu uygulamayı biraz **refactor** edelim ve **güvenliği** ile **esnekliği** artıralım. + +Önceki uygulamaya bakarsanız, UI’da şu ana kadar client’ın oluşturulacak `Hero`’nun `id` değerini belirlemesine izin verdiğini görebilirsiniz. + +Buna izin vermemeliyiz; DB’de zaten atanmış bir `id`’yi ezebilirler. `id` belirlemek **client** tarafından değil, **backend** veya **veritabanı** tarafından yapılmalıdır. + +Ayrıca hero için bir `secret_name` oluşturuyoruz ama şimdiye kadar her yerde geri döndürüyoruz; bu pek de **secret** sayılmaz... + +Bunları birkaç **ek model** ekleyerek düzelteceğiz. SQLModel’in parlayacağı yer de burası. + +### Birden Fazla Model Oluşturma { #create-multiple-models } + +**SQLModel**’de, `table=True` olan herhangi bir model sınıfı bir **table model**’dir. + +`table=True` olmayan her model sınıfı ise bir **data model**’dir; bunlar aslında sadece Pydantic modelleridir (bazı küçük ek özelliklerle). + +SQLModel ile **inheritance** kullanarak her durumda tüm alanları tekrar tekrar yazmaktan **kaçınabiliriz**. + +#### `HeroBase` - temel sınıf { #herobase-the-base-class } + +Önce tüm modeller tarafından **paylaşılan alanları** içeren bir `HeroBase` modeliyle başlayalım: + +* `name` +* `age` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} + +#### `Hero` - *table model* { #hero-the-table-model } + +Sonra gerçek *table model* olan `Hero`’yu, diğer modellerde her zaman bulunmayan **ek alanlarla** oluşturalım: + +* `id` +* `secret_name` + +`Hero`, `HeroBase`’ten miras aldığı için `HeroBase`’te tanımlanan alanlara da sahiptir. Dolayısıyla `Hero` için tüm alanlar: + +* `id` +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} + +#### `HeroPublic` - public *data model* { #heropublic-the-public-data-model } + +Sonraki adımda `HeroPublic` modelini oluştururuz; bu model API client’larına **geri döndürülecek** modeldir. + +`HeroBase` ile aynı alanlara sahiptir; dolayısıyla `secret_name` içermez. + +Sonunda kahramanlarımızın kimliği korunmuş oldu! + +Ayrıca `id: int` alanını yeniden tanımlar. Bunu yaparak API client’larıyla bir **contract** (sözleşme) oluşturmuş oluruz; böylece `id` alanının her zaman var olacağını ve `int` olacağını (asla `None` olmayacağını) bilirler. + +/// tip | İpucu + +Return model’in bir değerin her zaman mevcut olduğunu ve her zaman `int` olduğunu (`None` değil) garanti etmesi API client’ları için çok faydalıdır; bu kesinlik sayesinde daha basit kod yazabilirler. + +Ayrıca **otomatik üretilen client**’ların arayüzleri de daha basit olur; böylece API’nizle çalışan geliştiriciler için süreç çok daha rahat olur. + +/// + +`HeroPublic` içindeki tüm alanlar `HeroBase` ile aynıdır; tek fark `id`’nin `int` olarak tanımlanmasıdır (`None` değil): + +* `id` +* `name` +* `age` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} + +#### `HeroCreate` - hero oluşturmak için *data model* { #herocreate-the-data-model-to-create-a-hero } + +Şimdi `HeroCreate` modelini oluştururuz; bu model client’tan gelen veriyi **validate** etmek için kullanılır. + +`HeroBase` ile aynı alanlara sahiptir ve ek olarak `secret_name` içerir. + +Artık client’lar **yeni bir hero oluştururken** `secret_name` gönderecek; bu değer veritabanında saklanacak, ancak API response’larında client’a geri döndürülmeyecek. + +/// tip | İpucu + +**Password**’ları bu şekilde ele alırsınız: alırsınız ama API’de geri döndürmezsiniz. + +Ayrıca password değerlerini saklamadan önce **hash** etmelisiniz; **asla plain text olarak saklamayın**. + +/// + +`HeroCreate` alanları: + +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} + +#### `HeroUpdate` - hero güncellemek için *data model* { #heroupdate-the-data-model-to-update-a-hero } + +Uygulamanın önceki sürümünde bir hero’yu **güncellemenin** bir yolu yoktu; ancak artık **birden fazla model** ile bunu yapabiliriz. + +`HeroUpdate` *data model* biraz özeldir: yeni bir hero oluşturmak için gereken alanların **tamamına** sahiptir, ancak tüm alanlar **opsiyoneldir** (hepsinin bir default değeri vardır). Bu sayede hero güncellerken sadece güncellemek istediğiniz alanları gönderebilirsiniz. + +Tüm **alanlar aslında değiştiği** için (tip artık `None` içeriyor ve default değerleri `None` oluyor), onları **yeniden tanımlamamız** gerekir. + +Aslında `HeroBase`’ten miras almamız gerekmiyor; çünkü tüm alanları yeniden tanımlıyoruz. Tutarlılık için miras almayı bırakıyorum ama bu gerekli değil. Daha çok kişisel tercih meselesi. + +`HeroUpdate` alanları: + +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} + +### `HeroCreate` ile Oluşturma ve `HeroPublic` Döndürme { #create-with-herocreate-and-return-a-heropublic } + +Artık **birden fazla model** olduğuna göre, onları kullanan uygulama kısımlarını güncelleyebiliriz. + +Request’te bir `HeroCreate` *data model* alırız ve bundan bir `Hero` *table model* oluştururuz. + +Bu yeni *table model* `Hero`, client’ın gönderdiği alanlara sahip olur ve ayrıca veritabanının ürettiği bir `id` alır. + +Sonra fonksiyondan bu *table model* `Hero`’yu olduğu gibi döndürürüz. Ancak `response_model`’i `HeroPublic` *data model* olarak belirlediğimiz için **FastAPI**, veriyi validate ve serialize etmek için `HeroPublic` kullanır. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} + +/// tip | İpucu + +Burada **return type annotation** `-> HeroPublic` yerine `response_model=HeroPublic` kullanıyoruz; çünkü gerçekte döndürdüğümüz değer *bir* `HeroPublic` değil. + +Eğer `-> HeroPublic` yazsaydık, editörünüz ve linter’ınız (haklı olarak) `HeroPublic` yerine `Hero` döndürdüğünüz için şikayet edecekti. + +Bunu `response_model` içinde belirterek **FastAPI**’ye işini yapmasını söylüyoruz; type annotation’lara ve editörünüzün/diğer araçların sağladığı desteğe karışmamış oluyoruz. + +/// + +### `HeroPublic` ile Hero’ları Okuma { #read-heroes-with-heropublic } + +Daha öncekiyle aynı şekilde `Hero`’ları **okuyabiliriz**; yine `response_model=list[HeroPublic]` kullanarak verinin doğru biçimde validate ve serialize edilmesini garanti ederiz. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} + +### `HeroPublic` ile Tek Bir Hero Okuma { #read-one-hero-with-heropublic } + +Tek bir hero’yu **okuyabiliriz**: + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} + +### `HeroUpdate` ile Hero Güncelleme { #update-a-hero-with-heroupdate } + +Bir hero’yu **güncelleyebiliriz**. Bunun için HTTP `PATCH` operasyonu kullanırız. + +Kodda, client’ın gönderdiği tüm verilerle bir `dict` alırız; **yalnızca client’ın gönderdiği veriler**, yani sadece default değer oldukları için orada bulunan değerler hariç. Bunu yapmak için `exclude_unset=True` kullanırız. Asıl numara bu. + +Sonra `hero_db.sqlmodel_update(hero_data)` ile `hero_db`’yi `hero_data` içindeki verilerle güncelleriz. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} + +### Hero’yu Tekrar Silme { #delete-a-hero-again } + +Bir hero’yu **silmek** büyük ölçüde aynı kalıyor. + +Bu örnekte her şeyi refactor etme isteğimizi bastıracağız. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} + +### Uygulamayı Tekrar Çalıştırma { #run-the-app-again } + +Uygulamayı tekrar çalıştırabilirsiniz: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +`/docs` API UI’a giderseniz artık güncellendiğini göreceksiniz; hero oluştururken client’tan `id` beklemeyecek, vb. + +
+ +
+ +## Özet { #recap } + +Bir SQL veritabanıyla etkileşim kurmak için **SQLModel** kullanabilir ve *data model* ile *table model* yaklaşımıyla kodu sadeleştirebilirsiniz. + +**SQLModel** dokümantasyonunda çok daha fazlasını öğrenebilirsiniz; **FastAPI** ile SQLModel kullanımı için daha uzun bir mini tutorial da bulunuyor. diff --git a/docs/tr/docs/tutorial/testing.md b/docs/tr/docs/tutorial/testing.md new file mode 100644 index 000000000..887156606 --- /dev/null +++ b/docs/tr/docs/tutorial/testing.md @@ -0,0 +1,190 @@ +# Test Etme { #testing } + +Starlette sayesinde **FastAPI** uygulamalarını test etmek kolay ve keyiflidir. + +Temelde HTTPX üzerine kuruludur; HTTPX de Requests’i temel alarak tasarlandığı için oldukça tanıdık ve sezgiseldir. + +Bununla birlikte **FastAPI** ile pytest'i doğrudan kullanabilirsiniz. + +## `TestClient` Kullanımı { #using-testclient } + +/// info | Bilgi + +`TestClient` kullanmak için önce `httpx`'i kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin: + +```console +$ pip install httpx +``` + +/// + +`TestClient`'ı import edin. + +**FastAPI** uygulamanızı ona vererek bir `TestClient` oluşturun. + +Adı `test_` ile başlayan fonksiyonlar oluşturun (bu, `pytest`'in standart konvansiyonudur). + +`TestClient` nesnesini `httpx` ile kullandığınız şekilde kullanın. + +Kontrol etmeniz gereken şeyler için standart Python ifadeleriyle basit `assert` satırları yazın (bu da `pytest` standardıdır). + +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} + +/// tip | İpucu + +Test fonksiyonlarının `async def` değil, normal `def` olduğuna dikkat edin. + +Client'a yapılan çağrılar da `await` kullanılmadan, normal çağrılardır. + +Bu sayede `pytest`'i ek bir karmaşıklık olmadan doğrudan kullanabilirsiniz. + +/// + +/// note | Teknik Detaylar + +İsterseniz `from starlette.testclient import TestClient` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.testclient`'ı `fastapi.testclient` üzerinden de sunar. Ancak asıl kaynak doğrudan Starlette'tır. + +/// + +/// tip | İpucu + +FastAPI uygulamanıza request göndermenin dışında testlerinizde `async` fonksiyonlar çağırmak istiyorsanız (örn. asenkron veritabanı fonksiyonları), ileri seviye bölümdeki [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} dokümanına göz atın. + +/// + +## Testleri Ayırma { #separating-tests } + +Gerçek bir uygulamada testlerinizi büyük ihtimalle farklı bir dosyada tutarsınız. + +Ayrıca **FastAPI** uygulamanız birden fazla dosya/modül vb. ile de oluşturulmuş olabilir. + +### **FastAPI** Uygulama Dosyası { #fastapi-app-file } + +[Bigger Applications](bigger-applications.md){.internal-link target=_blank}'te anlatılan şekilde bir dosya yapınız olduğunu varsayalım: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +`main.py` dosyasında **FastAPI** uygulamanız bulunuyor olsun: + +{* ../../docs_src/app_testing/app_a_py39/main.py *} + +### Test Dosyası { #testing-file } + +Sonra testlerinizin olduğu bir `test_main.py` dosyanız olabilir. Bu dosya aynı Python package içinde (yani `__init__.py` dosyası olan aynı dizinde) durabilir: + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Bu dosya aynı package içinde olduğu için, `main` modülünden (`main.py`) `app` nesnesini import etmek üzere relative import kullanabilirsiniz: + +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} + +...ve test kodunu da öncekiyle aynı şekilde yazabilirsiniz. + +## Test Etme: Genişletilmiş Örnek { #testing-extended-example } + +Şimdi bu örneği genişletelim ve farklı parçaların nasıl test edildiğini görmek için daha fazla detay ekleyelim. + +### Genişletilmiş **FastAPI** Uygulama Dosyası { #extended-fastapi-app-file } + +Aynı dosya yapısıyla devam edelim: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Diyelim ki **FastAPI** uygulamanızın bulunduğu `main.py` dosyasında artık başka **path operations** da var. + +Hata döndürebilecek bir `GET` operation'ı var. + +Birden fazla farklı hata döndürebilecek bir `POST` operation'ı var. + +Her iki *path operation* da `X-Token` header'ını gerektiriyor. + +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} + +### Genişletilmiş Test Dosyası { #extended-testing-file } + +Sonrasında `test_main.py` dosyanızı genişletilmiş testlerle güncelleyebilirsiniz: + +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} + +Client'ın request içinde bir bilgi göndermesi gerektiğinde ve bunu nasıl yapacağınızı bilemediğinizde, `httpx` ile nasıl yapılacağını aratabilirsiniz (Google) ya da HTTPX’in tasarımı Requests’e dayandığı için `requests` ile nasıl yapıldığını da arayabilirsiniz. + +Sonra testlerinizde aynısını uygularsınız. + +Örn.: + +* Bir *path* veya *query* parametresi geçirmek için, URL’nin kendisine ekleyin. +* JSON body göndermek için, `json` parametresine bir Python nesnesi (örn. bir `dict`) verin. +* JSON yerine *Form Data* göndermeniz gerekiyorsa, bunun yerine `data` parametresini kullanın. +* *headers* göndermek için, `headers` parametresine bir `dict` verin. +* *cookies* için, `cookies` parametresine bir `dict` verin. + +Backend'e veri geçme hakkında daha fazla bilgi için (`httpx` veya `TestClient` kullanarak) HTTPX dokümantasyonu'na bakın. + +/// info | Bilgi + +`TestClient`'ın Pydantic model'lerini değil, JSON'a dönüştürülebilen verileri aldığını unutmayın. + +Testinizde bir Pydantic model'iniz varsa ve test sırasında verisini uygulamaya göndermek istiyorsanız, [JSON Compatible Encoder](encoder.md){.internal-link target=_blank} içinde açıklanan `jsonable_encoder`'ı kullanabilirsiniz. + +/// + +## Çalıştırma { #run-it } + +Bundan sonra yapmanız gereken tek şey `pytest`'i kurmaktır. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Dosyaları ve testleri otomatik olarak bulur, çalıştırır ve sonuçları size raporlar. + +Testleri şu şekilde çalıştırın: + +
+ +```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/tr/docs/virtual-environments.md b/docs/tr/docs/virtual-environments.md new file mode 100644 index 000000000..cf7fab778 --- /dev/null +++ b/docs/tr/docs/virtual-environments.md @@ -0,0 +1,862 @@ +# Virtual Environments { #virtual-environments } + +Python projeleriyle çalışırken, her proje için kurduğunuz package'leri birbirinden izole etmek adına büyük ihtimalle bir **virtual environment** (veya benzer bir mekanizma) kullanmalısınız. + +/// info | Bilgi + +Virtual environment'leri, nasıl oluşturulduklarını ve nasıl kullanıldıklarını zaten biliyorsanız bu bölümü atlamak isteyebilirsiniz. 🤓 + +/// + +/// tip | İpucu + +**Virtual environment**, **environment variable** ile aynı şey değildir. + +**Environment variable**, sistemde bulunan ve programların kullanabildiği bir değişkendir. + +**Virtual environment** ise içinde bazı dosyalar bulunan bir klasördür. + +/// + +/// info | Bilgi + +Bu sayfada **virtual environment**'leri nasıl kullanacağınızı ve nasıl çalıştıklarını öğreneceksiniz. + +Eğer Python'ı kurmak dahil her şeyi sizin yerinize yöneten bir **tool** kullanmaya hazırsanız, uv'yi deneyin. + +/// + +## Proje Oluşturun { #create-a-project } + +Önce projeniz için bir klasör oluşturun. + +Ben genelde home/user klasörümün içinde `code` adlı bir klasör oluştururum. + +Sonra bunun içinde her proje için ayrı bir klasör oluştururum. + +
+ +```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 +``` + +
+ +## Virtual Environment Oluşturun { #create-a-virtual-environment } + +Bir Python projesi üzerinde **ilk kez** çalışmaya başladığınızda, projenizin içinde **virtual environment** oluşturun. + +/// tip | İpucu + +Bunu her çalıştığınızda değil, **proje başına sadece bir kez** yapmanız yeterlidir. + +/// + +//// tab | `venv` + +Bir virtual environment oluşturmak için, Python ile birlikte gelen `venv` modülünü kullanabilirsiniz. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | Bu komut ne anlama geliyor + +* `python`: `python` adlı programı kullan +* `-m`: bir modülü script gibi çalıştır; bir sonraki kısımda hangi modül olduğunu söyleyeceğiz +* `venv`: normalde Python ile birlikte kurulu gelen `venv` modülünü kullan +* `.venv`: virtual environment'i yeni `.venv` klasörünün içine oluştur + +/// + +//// + +//// tab | `uv` + +Eğer `uv` kuruluysa, onunla da virtual environment oluşturabilirsiniz. + +
+ +```console +$ uv venv +``` + +
+ +/// tip | İpucu + +Varsayılan olarak `uv`, `.venv` adlı bir klasörde virtual environment oluşturur. + +Ancak ek bir argümanla klasör adını vererek bunu özelleştirebilirsiniz. + +/// + +//// + +Bu komut `.venv` adlı bir klasörün içinde yeni bir virtual environment oluşturur. + +/// details | `.venv` veya başka bir ad + +Virtual environment'i başka bir klasörde de oluşturabilirsiniz; ancak buna `.venv` demek yaygın bir konvansiyondur. + +/// + +## Virtual Environment'i Aktif Edin { #activate-the-virtual-environment } + +Oluşturduğunuz virtual environment'i aktif edin; böylece çalıştırdığınız her Python komutu veya kurduğunuz her package onu kullanır. + +/// tip | İpucu + +Projede çalışmak için **yeni bir terminal oturumu** başlattığınız **her seferinde** bunu yapın. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ya da Windows'ta Bash kullanıyorsanız (örn. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip | İpucu + +Bu environment'e **yeni bir package** kurduğunuz her seferinde environment'i yeniden **aktif edin**. + +Böylece, o package'in kurduğu bir **terminal (CLI) programı** kullanıyorsanız, global olarak kurulu (ve muhtemelen ihtiyacınız olandan farklı bir versiyona sahip) başka bir program yerine, virtual environment'inizdeki programı kullanmış olursunuz. + +/// + +## Virtual Environment'in Aktif Olduğunu Kontrol Edin { #check-the-virtual-environment-is-active } + +Virtual environment'in aktif olduğunu (bir önceki komutun çalıştığını) kontrol edin. + +/// tip | İpucu + +Bu **opsiyoneldir**; ancak her şeyin beklendiği gibi çalıştığını ve hedeflediğiniz virtual environment'i kullandığınızı **kontrol etmek** için iyi bir yöntemdir. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +Eğer `python` binary'sini projenizin içinde (bu örnekte `awesome-project`) `.venv/bin/python` yolunda gösteriyorsa, tamamdır. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +Eğer `python` binary'sini projenizin içinde (bu örnekte `awesome-project`) `.venv\Scripts\python` yolunda gösteriyorsa, tamamdır. 🎉 + +//// + +## `pip`'i Yükseltin { #upgrade-pip } + +/// tip | İpucu + +`uv` kullanıyorsanız, `pip` yerine onunla kurulum yaparsınız; dolayısıyla `pip`'i yükseltmeniz gerekmez. 😎 + +/// + +Package'leri kurmak için `pip` kullanıyorsanız (Python ile varsayılan olarak gelir), en güncel sürüme **yükseltmeniz** gerekir. + +Bir package kurarken görülen birçok garip hata, önce `pip`'i yükseltince çözülür. + +/// tip | İpucu + +Bunu genelde virtual environment'i oluşturduktan hemen sonra **bir kez** yaparsınız. + +/// + +Virtual environment'in aktif olduğundan emin olun (yukarıdaki komutla) ve sonra şunu çalıştırın: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +/// tip | İpucu + +Bazen pip'i yükseltmeye çalışırken **`No module named pip`** hatası alabilirsiniz. + +Böyle olursa, aşağıdaki komutla pip'i kurup yükseltin: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Bu komut pip kurulu değilse kurar ve ayrıca kurulu pip sürümünün `ensurepip` içinde bulunan sürüm kadar güncel olmasını garanti eder. + +/// + +## `.gitignore` Ekleyin { #add-gitignore } + +**Git** kullanıyorsanız (kullanmalısınız), `.venv` içindeki her şeyi Git'ten hariç tutmak için bir `.gitignore` dosyası ekleyin. + +/// tip | İpucu + +Virtual environment'i `uv` ile oluşturduysanız, bunu zaten sizin için yaptı; bu adımı atlayabilirsiniz. 😎 + +/// + +/// tip | İpucu + +Bunu virtual environment'i oluşturduktan hemen sonra **bir kez** yapın. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | Bu komut ne anlama geliyor + +* `echo "*"`: terminale `*` metnini "yazar" (sonraki kısım bunu biraz değiştiriyor) +* `>`: `>` işaretinin solundaki komutun terminale yazdıracağı çıktı, ekrana basılmak yerine sağ taraftaki dosyaya yazılsın +* `.gitignore`: metnin yazılacağı dosyanın adı + +Git'te `*` "her şey" demektir. Yani `.venv` klasörü içindeki her şeyi ignore eder. + +Bu komut, içeriği şu olan bir `.gitignore` dosyası oluşturur: + +```gitignore +* +``` + +/// + +## Package'leri Kurun { #install-packages } + +Environment'i aktif ettikten sonra, içine package kurabilirsiniz. + +/// tip | İpucu + +Projede ihtiyaç duyduğunuz package'leri ilk kez kurarken veya yükseltirken bunu **bir kez** yapın. + +Bir sürümü yükseltmeniz veya yeni bir package eklemeniz gerekirse **tekrar** yaparsınız. + +/// + +### Package'leri Doğrudan Kurun { #install-packages-directly } + +Acele ediyorsanız ve projenizin package gereksinimlerini bir dosyada belirtmek istemiyorsanız, doğrudan kurabilirsiniz. + +/// tip | İpucu + +Programınızın ihtiyaç duyduğu package'leri ve versiyonlarını bir dosyada tutmak (ör. `requirements.txt` veya `pyproject.toml`) (çok) iyi bir fikirdir. + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Eğer `uv` varsa: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### `requirements.txt`'ten Kurun { #install-from-requirements-txt } + +Bir `requirements.txt` dosyanız varsa, içindeki package'leri kurmak için artık onu kullanabilirsiniz. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Eğer `uv` varsa: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +Bazı package'ler içeren bir `requirements.txt` şöyle görünebilir: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Programınızı Çalıştırın { #run-your-program } + +Virtual environment'i aktif ettikten sonra programınızı çalıştırabilirsiniz; program, virtual environment'in içindeki Python'ı ve oraya kurduğunuz package'leri kullanır. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Editörünüzü Yapılandırın { #configure-your-editor } + +Muhtemelen bir editör kullanırsınız; otomatik tamamlamayı ve satır içi hataları alabilmek için, editörünüzü oluşturduğunuz aynı virtual environment'i kullanacak şekilde yapılandırdığınızdan emin olun (muhtemelen otomatik algılar). + +Örneğin: + +* VS Code +* PyCharm + +/// tip | İpucu + +Bunu genelde yalnızca **bir kez**, virtual environment'i oluşturduğunuzda yapmanız gerekir. + +/// + +## Virtual Environment'i Devre Dışı Bırakın { #deactivate-the-virtual-environment } + +Projeniz üzerinde işiniz bittiğinde virtual environment'i **deactivate** edebilirsiniz. + +
+ +```console +$ deactivate +``` + +
+ +Böylece `python` çalıştırdığınızda, o virtual environment içinden (ve oraya kurulu package'lerle) çalıştırmaya çalışmaz. + +## Çalışmaya Hazırsınız { #ready-to-work } + +Artık projeniz üzerinde çalışmaya başlayabilirsiniz. + +/// tip | İpucu + +Yukarıdaki her şeyin aslında ne olduğunu anlamak ister misiniz? + +Okumaya devam edin. 👇🤓 + +/// + +## Neden Virtual Environment { #why-virtual-environments } + +FastAPI ile çalışmak için Python kurmanız gerekir. + +Sonrasında FastAPI'yi ve kullanmak istediğiniz diğer tüm **package**'leri **kurmanız** gerekir. + +Package kurmak için genelde Python ile gelen `pip` komutunu (veya benzeri alternatifleri) kullanırsınız. + +Ancak `pip`'i doğrudan kullanırsanız, package'ler **global Python environment**'ınıza (Python'ın global kurulumuna) yüklenir. + +### Problem { #the-problem } + +Peki package'leri global Python environment'a kurmanın sorunu ne? + +Bir noktada, muhtemelen **farklı package**'lere bağımlı birçok farklı program yazacaksınız. Ayrıca üzerinde çalıştığınız bazı projeler, aynı package'in **farklı versiyonlarına** ihtiyaç duyacak. 😱 + +Örneğin `philosophers-stone` adında bir proje oluşturduğunuzu düşünün; bu program, `harry` adlı başka bir package'e **`1` versiyonu ile** bağlı. Yani `harry`'yi kurmanız gerekir. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Sonra daha ileri bir zamanda `prisoner-of-azkaban` adlı başka bir proje oluşturuyorsunuz; bu proje de `harry`'ye bağlı, fakat bu proje **`harry` versiyon `3`** istiyor. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +Şimdi sorun şu: package'leri local bir **virtual environment** yerine global (global environment) olarak kurarsanız, `harry`'nin hangi versiyonunu kuracağınıza karar vermek zorunda kalırsınız. + +`philosophers-stone`'u çalıştırmak istiyorsanız önce `harry` versiyon `1`'i kurmanız gerekir; örneğin: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +Sonuç olarak global Python environment'ınızda `harry` versiyon `1` kurulu olur. + +```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 +``` + +Fakat `prisoner-of-azkaban`'ı çalıştırmak istiyorsanız, `harry` versiyon `1`'i kaldırıp `harry` versiyon `3`'ü kurmanız gerekir (ya da sadece `3`'ü kurmak, otomatik olarak `1`'i kaldırabilir). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +Sonuç olarak global Python environment'ınızda `harry` versiyon `3` kurulu olur. + +Ve `philosophers-stone`'u tekrar çalıştırmaya kalkarsanız, `harry` versiyon `1`'e ihtiyaç duyduğu için **çalışmama** ihtimali vardır. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip | İpucu + +Python package'lerinde **yeni versiyonlarda** **breaking change**'lerden kaçınmak oldukça yaygındır; ancak yine de daha güvenlisi, yeni versiyonları bilinçli şekilde kurmak ve mümkünse test'leri çalıştırıp her şeyin doğru çalıştığını doğrulamaktır. + +/// + +Şimdi bunu, **projelerinizin bağımlı olduğu** daha **birçok** başka **package** ile birlikte düşünün. Yönetmesi epey zorlaşır. Sonunda bazı projeleri package'lerin **uyumsuz versiyonlarıyla** çalıştırıp, bir şeylerin neden çalışmadığını anlamamak gibi durumlara düşebilirsiniz. + +Ayrıca işletim sisteminize (örn. Linux, Windows, macOS) bağlı olarak Python zaten kurulu gelmiş olabilir. Bu durumda, sisteminizin **ihtiyaç duyduğu** bazı package'ler belirli versiyonlarla önceden kurulu olabilir. Global Python environment'a package kurarsanız, işletim sistemiyle gelen bazı programları **bozma** ihtimaliniz olabilir. + +## Package'ler Nereye Kuruluyor { #where-are-packages-installed } + +Python'ı kurduğunuzda, bilgisayarınızda bazı dosyalar içeren klasörler oluşturulur. + +Bu klasörlerin bir kısmı, kurduğunuz tüm package'leri barındırmaktan sorumludur. + +Şunu çalıştırdığınızda: + +
+ +```console +// Don't run this now, it's just an example 🤓 +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +Bu, FastAPI kodunu içeren sıkıştırılmış bir dosyayı genellikle PyPI'dan indirir. + +Ayrıca FastAPI'nin bağımlı olduğu diğer package'ler için de dosyaları **indirir**. + +Sonra tüm bu dosyaları **açar (extract)** ve bilgisayarınızdaki bir klasöre koyar. + +Varsayılan olarak bu indirilip çıkarılan dosyaları, Python kurulumunuzla birlikte gelen klasöre yerleştirir; yani **global environment**'a. + +## Virtual Environment Nedir { #what-are-virtual-environments } + +Global environment'da tüm package'leri bir arada tutmanın sorunlarına çözüm, çalıştığınız her proje için ayrı bir **virtual environment** kullanmaktır. + +Virtual environment, global olana çok benzeyen bir **klasördür**; bir projenin ihtiyaç duyduğu package'leri buraya kurarsınız. + +Böylece her projenin kendi virtual environment'i (`.venv` klasörü) ve kendi package'leri olur. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## Virtual Environment'i Aktif Etmek Ne Demek { #what-does-activating-a-virtual-environment-mean } + +Bir virtual environment'i örneğin şununla aktif ettiğinizde: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ya da Windows'ta Bash kullanıyorsanız (örn. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Bu komut, sonraki komutlarda kullanılabilecek bazı [environment variable](environment-variables.md){.internal-link target=_blank}'ları oluşturur veya değiştirir. + +Bunlardan biri `PATH` değişkenidir. + +/// tip | İpucu + +`PATH` environment variable hakkında daha fazla bilgiyi [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} bölümünde bulabilirsiniz. + +/// + +Bir virtual environment'i aktive etmek, onun `.venv/bin` (Linux ve macOS'ta) veya `.venv\Scripts` (Windows'ta) yolunu `PATH` environment variable'ına ekler. + +Diyelim ki environment'i aktive etmeden önce `PATH` değişkeni şöyleydi: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin programları şu klasörlerde arayacağı anlamına gelir: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +Bu, sistemin programları şurada arayacağı anlamına gelir: + +* `C:\Windows\System32` + +//// + +Virtual environment'i aktive ettikten sonra `PATH` değişkeni şuna benzer hale gelir: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin artık programları önce şurada aramaya başlayacağı anlamına gelir: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +diğer klasörlere bakmadan önce. + +Dolayısıyla terminale `python` yazdığınızda, sistem Python programını şurada bulur: + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +ve onu kullanır. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +Bu, sistemin artık programları önce şurada aramaya başlayacağı anlamına gelir: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +diğer klasörlere bakmadan önce. + +Dolayısıyla terminale `python` yazdığınızda, sistem Python programını şurada bulur: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +ve onu kullanır. + +//// + +Önemli bir detay: virtual environment yolu `PATH` değişkeninin **en başına** eklenir. Sistem, mevcut başka herhangi bir Python'ı bulmadan **önce** bunu bulur. Böylece `python` çalıştırdığınızda, başka bir `python` (örneğin global environment'tan gelen `python`) yerine **virtual environment'taki** Python kullanılır. + +Virtual environment'i aktive etmek birkaç şeyi daha değiştirir; ancak yaptığı en önemli işlerden biri budur. + +## Virtual Environment'i Kontrol Etmek { #checking-a-virtual-environment } + +Bir virtual environment'in aktif olup olmadığını örneğin şununla kontrol ettiğinizde: + +//// 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 +``` + +
+ +//// + +Bu, kullanılacak `python` programının **virtual environment'in içindeki** Python olduğu anlamına gelir. + +Linux ve macOS'ta `which`, Windows PowerShell'de ise `Get-Command` kullanırsınız. + +Bu komutun çalışma mantığı şudur: `PATH` environment variable içindeki **her yolu sırayla** dolaşır, `python` adlı programı arar. Bulduğunda, size o programın **dosya yolunu** gösterir. + +En önemli kısım şu: `python` dediğinizde çalışacak olan "`python`" tam olarak budur. + +Yani doğru virtual environment'da olup olmadığınızı doğrulayabilirsiniz. + +/// tip | İpucu + +Bir virtual environment'i aktive etmek kolaydır; sonra o Python ile kalıp **başka bir projeye geçmek** de kolaydır. + +Bu durumda ikinci proje, başka bir projenin virtual environment'ından gelen **yanlış Python**'ı kullandığınız için **çalışmayabilir**. + +Hangi `python`'ın kullanıldığını kontrol edebilmek bu yüzden faydalıdır. 🤓 + +/// + +## Neden Virtual Environment'i Deactivate Edelim { #why-deactivate-a-virtual-environment } + +Örneğin `philosophers-stone` projesi üzerinde çalışıyor olabilirsiniz; **o virtual environment'i aktive eder**, package kurar ve o environment ile çalışırsınız. + +Sonra **başka bir proje** olan `prisoner-of-azkaban` üzerinde çalışmak istersiniz. + +O projeye gidersiniz: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +Eğer `philosophers-stone` için olan virtual environment'i deactivate etmezseniz, terminalde `python` çalıştırdığınızda `philosophers-stone`'dan gelen Python'ı kullanmaya çalışır. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Error importing sirius, it's not installed 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +Ama virtual environment'i deactivate edip `prisoner-of-askaban` için yeni olanı aktive ederseniz, `python` çalıştırdığınızda `prisoner-of-azkaban` içindeki virtual environment'dan gelen Python kullanılır. + +
+ +```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 + +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ +$ python main.py + +I solemnly swear 🐺 +``` + +
+ +## Alternatifler { #alternatives } + +Bu, başlamanız için basit bir rehber ve alttaki mekanizmaların nasıl çalıştığını öğretmeyi amaçlıyor. + +Virtual environment'leri, package bağımlılıklarını (requirements) ve projeleri yönetmek için birçok **alternatif** vardır. + +Hazır olduğunuzda ve package bağımlılıkları, virtual environment'ler vb. dahil **tüm projeyi yönetmek** için bir tool kullanmak istediğinizde, uv'yi denemenizi öneririm. + +`uv` birçok şey yapabilir, örneğin: + +* Sizin için **Python kurabilir**, farklı sürümler dahil +* Projelerinizin **virtual environment**'ini yönetebilir +* **Package** kurabilir +* Projeniz için package **bağımlılıklarını ve versiyonlarını** yönetebilir +* Bağımlılıkları dahil, kurulacak package ve versiyonların **tam (exact)** bir setini garanti edebilir; böylece geliştirirken bilgisayarınızda çalıştırdığınız projeyi production'da da birebir aynı şekilde çalıştırabileceğinizden emin olursunuz; buna **locking** denir +* Ve daha birçok şey + +## Sonuç { #conclusion } + +Buradaki her şeyi okuduysanız ve anladıysanız, artık birçok geliştiriciden **çok daha fazla** virtual environment bilgisine sahipsiniz. 🤓 + +Bu detayları bilmek, ileride karmaşık görünen bir sorunu debug ederken büyük olasılıkla işinize yarayacak; çünkü **altta nasıl çalıştığını** biliyor olacaksınız. 😎 diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 526409c5c..4f089c4e1 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -8,7 +8,7 @@ FastAPI

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

@@ -33,14 +33,14 @@ --- -FastAPI — це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. +FastAPI - це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. Ключові особливості: * **Швидкий**: дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших Python-фреймворків](#performance). * **Швидке написання коду**: пришвидшує розробку функціоналу приблизно на 200%–300%. * * **Менше помилок**: зменшує приблизно на 40% кількість помилок, спричинених людиною (розробником). * -* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження. +* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження. * **Простий**: спроєктований так, щоб бути простим у використанні та вивченні. Менше часу на читання документації. * **Короткий**: мінімізує дублювання коду. Кілька можливостей з кожного оголошення параметра. Менше помилок. * **Надійний**: ви отримуєте код, готовий до продакшину. З автоматичною інтерактивною документацією. @@ -127,9 +127,9 @@ FastAPI — це сучасний, швидкий (високопродукти -Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**. +Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**. -**Typer** — молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 +**Typer** - молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 ## Вимоги { #requirements } @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Створіть файл `main.py` з: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Якщо ваш код використовує `async` / `await`, скористайтеся `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Оголосіть тіло, використовуючи стандартні типи Python, завдяки Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -374,7 +368,7 @@ item: Item * Валідацію даних: * Автоматичні та зрозумілі помилки, коли дані некоректні. * Валідацію навіть для глибоко вкладених JSON-обʼєктів. -* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: +* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: * JSON. * Параметрів шляху. * Параметрів запиту. @@ -382,7 +376,7 @@ item: Item * Headers. * Forms. * Files. -* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): +* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): * Перетворення типів Python (`str`, `int`, `float`, `bool`, `list`, тощо). * Обʼєктів `datetime`. * Обʼєктів `UUID`. @@ -439,9 +433,9 @@ item: Item ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Для більш повного прикладу, що включає більше можливостей, перегляньте Туторіал — Посібник користувача. +Для більш повного прикладу, що включає більше можливостей, перегляньте Навчальний посібник - Посібник користувача. -**Spoiler alert**: туторіал — посібник користувача містить: +**Попередження про спойлер**: навчальний посібник - посібник користувача містить: * Оголошення **параметрів** з інших різних місць, як-от: **headers**, **cookies**, **form fields** та **files**. * Як встановлювати **обмеження валідації** як `maximum_length` або `regex`. @@ -500,11 +494,11 @@ Deploying to FastAPI Cloud... Він забезпечує той самий **developer experience** створення застосунків на FastAPI під час їх **розгортання** у хмарі. 🎉 -FastAPI Cloud — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ +FastAPI Cloud - основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ #### Розгортання в інших хмарних провайдерів { #deploy-to-other-cloud-providers } -FastAPI — open source і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. +FastAPI - open source проект і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути застосунки FastAPI у нього. 🤓 @@ -530,7 +524,7 @@ FastAPI залежить від Pydantic і Starlette. * httpx - потрібно, якщо ви хочете використовувати `TestClient`. * jinja2 - потрібно, якщо ви хочете використовувати конфігурацію шаблонів за замовчуванням. -* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`. +* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`. Використовується FastAPI: diff --git a/docs/uk/docs/tutorial/body-multiple-params.md b/docs/uk/docs/tutorial/body-multiple-params.md index dc9a768c3..f541beea7 100644 --- a/docs/uk/docs/tutorial/body-multiple-params.md +++ b/docs/uk/docs/tutorial/body-multiple-params.md @@ -102,15 +102,16 @@ Оскільки за замовчуванням одиничні значення інтерпретуються як параметри запиту, вам не потрібно явно додавати `Query`, ви можете просто зробити: +```Python +q: str | None = None +``` + +Або в Python 3.9: + ```Python q: Union[str, None] = None ``` -Або в Python 3.10 і вище: - -```Python -q: str | None = None -``` Наприклад: @@ -129,7 +130,7 @@ q: str | None = None За замовчуванням **FastAPI** очікуватиме його тіло безпосередньо. -Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього — вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` — `embed`: +Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього - вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` - `embed`: ```Python item: Item = Body(embed=True) diff --git a/docs/uk/llm-prompt.md b/docs/uk/llm-prompt.md index f1c5377a4..e8cd3dabc 100644 --- a/docs/uk/llm-prompt.md +++ b/docs/uk/llm-prompt.md @@ -8,6 +8,7 @@ Language code: uk. - Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). - Keep the tone concise and technical. +- Use one style of dashes. For example, if text contains "-" then use only this symbol to represent a dash. ### Headings @@ -32,6 +33,71 @@ Use the following preferred translations when they apply in documentation prose: - response (HTTP): відповідь - path operation: операція шляху - path operation function: функція операції шляху +- prompt: підсказка +- check: перевірка +- Parallel Server Gateway Interface: Інтерфейс Шлюзу Паралельного Сервера +- Mozilla Developer Network: Мережа Розробників Mozilla +- tutorial: навчальний посібник +- advanced user guide: просунутий посібник користувача +- deep learning: глибоке навчання +- machine learning: машинне навчання +- dependency injection: впровадження залежностей +- digest (HTTP): дайджест +- basic authentication (HTTP): базова автентифікація +- JSON schema: Схема JSON +- password flow: потік паролю +- mobile: мобільний +- body: тіло +- form: форма +- path: шлях +- query: запит +- cookie: кукі +- header: заголовок +- startup: запуск +- shutdown: вимкнення +- lifespan: тривалість життя +- authorization: авторизація +- forwarded header: направлений заголовок +- dependable: залежний +- dependent: залежний +- bound: межа +- concurrency: рівночасність +- parallelism: паралелізм +- multiprocessing: багатопроцесорність +- env var: змінна оточення +- dict: словник +- enum: перелік +- issue: проблема +- server worker: серверний працівник +- worker: працівник +- software development kit: набір для розробки програмного забезпечення +- bearer token: токен носія +- breaking change: несумісна зміна +- bug: помилка +- button: кнопка +- callable: викликаємий +- code: код +- commit: фіксація +- context manager: менеджер контексту +- coroutine: співпрограма +- engine: рушій +- fake X: фальшивий X +- item: предмет +- lock: блокування +- middleware: проміжне програмне забезпечення +- mounting: монтування +- origin: джерело +- override: переписування +- payload: корисне навантаження +- processor: процесор +- property: властивість +- proxy: представник +- pull request: запит на витяг +- random-access memory: пам'ять з довільним доступом +- status code: код статусу +- string: строка +- tag: мітка +- wildcard: дика карта ### `///` admonitions @@ -44,3 +110,4 @@ Use the following preferred translations when they apply in documentation prose: - `/// warning | Попередження` - `/// info | Інформація` - `/// danger | Обережно` +- `/// check | Перевірте` diff --git a/docs/zh-hant/docs/about/index.md b/docs/zh-hant/docs/about/index.md index 5dcee68f2..cf5b5742c 100644 --- a/docs/zh-hant/docs/about/index.md +++ b/docs/zh-hant/docs/about/index.md @@ -1,3 +1,3 @@ -# 關於 FastAPI +# 關於 { #about } 關於 FastAPI、其設計、靈感來源等更多資訊。 🤓 diff --git a/docs/zh-hant/docs/benchmarks.md b/docs/zh-hant/docs/benchmarks.md index c59e8e71c..df49621c5 100644 --- a/docs/zh-hant/docs/benchmarks.md +++ b/docs/zh-hant/docs/benchmarks.md @@ -1,10 +1,10 @@ -# 基準測試 +# 基準測試 { #benchmarks } 由第三方機構 TechEmpower 的基準測試表明在 Uvicorn 下運行的 **FastAPI** 應用程式是 最快的 Python 可用框架之一,僅次於 Starlette 和 Uvicorn 本身(於 FastAPI 內部使用)。 但是在查看基準得分和對比時,請注意以下幾點。 -## 基準測試和速度 +## 基準測試和速度 { #benchmarks-and-speed } 當你查看基準測試時,時常會見到幾個不同類型的工具被同時進行測試。 @@ -31,4 +31,4 @@ * FastAPI 在 Starlette 基礎之上提供了更多功能。包含建構 API 時所需要的功能,例如資料驗證和序列化。FastAPI 可以幫助你自動產生 API 文件,(應用程式啟動時將會自動生成文件,所以不會增加應用程式運行時的開銷)。 * 如果你沒有使用 FastAPI 而是直接使用 Starlette(或其他工具,如 Sanic、Flask、Responder 等),你將必須自行實現所有資料驗證和序列化。因此,你的最終應用程式仍然具有與使用 FastAPI 建置相同的開銷。在許多情況下,這種資料驗證和序列化是應用程式中編寫最大量的程式碼。 * 因此透過使用 FastAPI,你可以節省開發時間、錯誤與程式碼數量,並且相比不使用 FastAPI 你很大可能會獲得相同或更好的效能(因為那樣你必須在程式碼中實現所有相同的功能)。 - * 如果你要與 FastAPI 比較,請將其與能夠提供資料驗證、序列化和文件的網頁應用程式框架(或工具集)進行比較,例如 Flask-apispec、NestJS、Molten 等框架。 + * 如果你要與 FastAPI 比較,請將其與能夠提供資料驗證、序列化和文件的網頁應用程式框架(或工具集)進行比較,例如 Flask-apispec、NestJS、Molten 等框架。具備整合式自動資料驗證、序列化與文件的框架。 diff --git a/docs/zh-hant/docs/deployment/cloud.md b/docs/zh-hant/docs/deployment/cloud.md index 426937d3e..fffb2fcfe 100644 --- a/docs/zh-hant/docs/deployment/cloud.md +++ b/docs/zh-hant/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 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/zh-hant/docs/deployment/index.md b/docs/zh-hant/docs/deployment/index.md index 1726562b4..9edd3368b 100644 --- a/docs/zh-hant/docs/deployment/index.md +++ b/docs/zh-hant/docs/deployment/index.md @@ -1,8 +1,8 @@ -# 部署 +# 部署 { #deployment } 部署 **FastAPI** 應用程式相對容易。 -## 部署是什麼意思 +## 部署是什麼意思 { #what-does-deployment-mean } **部署**應用程式指的是執行一系列必要的步驟,使其能夠**讓使用者存取和使用**。 @@ -10,12 +10,14 @@ 這與**開發**階段形成鮮明對比,在**開發**階段,你會不斷更改程式碼、破壞程式碼、修復程式碼,然後停止和重新啟動伺服器等。 -## 部署策略 +## 部署策略 { #deployment-strategies } 根據你的使用場景和使用工具,有多種方法可以實現此目的。 你可以使用一些工具自行**部署伺服器**,你也可以使用能為你完成部分工作的**雲端服務**,或其他可能的選項。 +例如,我們(FastAPI 的團隊)打造了 **FastAPI Cloud**,讓將 FastAPI 應用程式部署到雲端變得盡可能流暢,並保持與使用 FastAPI 開發時相同的開發者體驗。 + 我將向你展示在部署 **FastAPI** 應用程式時你可能應該記住的一些主要概念(儘管其中大部分適用於任何其他類型的 Web 應用程式)。 在接下來的部分中,你將看到更多需要記住的細節以及一些技巧。 ✨ diff --git a/docs/zh-hant/docs/environment-variables.md b/docs/zh-hant/docs/environment-variables.md index a1598fc01..5b684b9e6 100644 --- a/docs/zh-hant/docs/environment-variables.md +++ b/docs/zh-hant/docs/environment-variables.md @@ -1,4 +1,4 @@ -# 環境變數 +# 環境變數 { #environment-variables } /// tip @@ -10,7 +10,7 @@ 環境變數對於處理應用程式**設定**(作為 Python **安裝**的一部分等方面)非常有用。 -## 建立和使用環境變數 +## 建立和使用環境變數 { #create-and-use-env-vars } 你在 **shell(終端機)**中就可以**建立**和使用環境變數,並不需要用到 Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## 在 Python 中讀取環境變數 +## 在 Python 中讀取環境變數 { #read-env-vars-in-python } 你也可以在 Python **之外**的終端機中建立環境變數(或使用其他方法),然後在 Python 中**讀取**它們。 @@ -65,7 +65,7 @@ print(f"Hello {name} from Python") /// tip -第二個參數是 `os.getenv()` 的預設回傳值。 +第二個參數是 `os.getenv()` 的預設回傳值。 如果沒有提供,預設值為 `None`,這裡我們提供 `"World"` 作為預設值。 @@ -153,19 +153,19 @@ Hello World from Python /// tip -你可以在 The Twelve-Factor App: 配置中了解更多資訊。 +你可以在 The Twelve-Factor App: 配置中了解更多資訊。 /// -## 型別和驗證 +## 型別和驗證 { #types-and-validation } 這些環境變數只能處理**文字字串**,因為它們是位於 Python 範疇之外的,必須與其他程式和作業系統的其餘部分相容(甚至與不同的作業系統相容,如 Linux、Windows、macOS)。 這意味著從環境變數中讀取的**任何值**在 Python 中都將是一個 `str`,任何型別轉換或驗證都必須在程式碼中完成。 -你將在[進階使用者指南 - 設定和環境變數](./advanced/settings.md)中了解更多關於使用環境變數處理**應用程式設定**的資訊。 +你將在[進階使用者指南 - 設定和環境變數](./advanced/settings.md){.internal-link target=_blank}中了解更多關於使用環境變數處理**應用程式設定**的資訊。 -## `PATH` 環境變數 +## `PATH` 環境變數 { #path-environment-variable } 有一個**特殊的**環境變數稱為 **`PATH`**,作業系統(Linux、macOS、Windows)用它來查找要執行的程式。 @@ -209,7 +209,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 如果找到了,那麼作業系統將**使用它**;否則,作業系統會繼續在**其他目錄**中查找。 -### 安裝 Python 並更新 `PATH` +### 安裝 Python 並更新 `PATH` { #installing-python-and-updating-the-path } 安裝 Python 時,可能會詢問你是否要更新 `PATH` 環境變數。 @@ -233,7 +233,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 假設你安裝了 Python,並將其安裝在目錄 `C:\opt\custompython\bin` 中。 -如果你選擇更新 `PATH` 環境變數(在 Python 安裝程式中,這個選項是名為 `Add Python x.xx to PATH` 的勾選框——譯者註),那麼安裝程式會將 `C:\opt\custompython\bin` 加入到 `PATH` 環境變數中。 +如果你選擇更新 `PATH` 環境變數,那麼安裝程式會將 `C:\opt\custompython\bin` 加入到 `PATH` 環境變數中。 ```plaintext C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin @@ -285,13 +285,13 @@ $ C:\opt\custompython\bin\python //// -當學習[虛擬環境](virtual-environments.md)時,這些資訊將會很有用。 +當學習[虛擬環境](virtual-environments.md){.internal-link target=_blank}時,這些資訊將會很有用。 -## 結論 +## 結論 { #conclusion } 透過這個教學,你應該對**環境變數**是什麼以及如何在 Python 中使用它們有了基本的了解。 -你也可以在環境變數 - 維基百科 (Wikipedia for Environment Variable) 中了解更多關於它們的資訊。 +你也可以在 環境變數的維基百科條目 中閱讀更多。 在許多情況下,環境變數的用途和適用性可能不會立刻顯現。但是在開發過程中,它們會在許多不同的場景中出現,因此瞭解它們是非常必要的。 diff --git a/docs/zh-hant/docs/how-to/index.md b/docs/zh-hant/docs/how-to/index.md index db740140d..6c9a8202c 100644 --- a/docs/zh-hant/docs/how-to/index.md +++ b/docs/zh-hant/docs/how-to/index.md @@ -1,4 +1,4 @@ -# 使用指南 - 範例集 +# 使用指南 - 範例集 { #how-to-recipes } 在這裡,你將會看到**不同主題**的範例或「如何使用」的指南。 diff --git a/docs/zh-hant/docs/index.md b/docs/zh-hant/docs/index.md index 4390d9609..a31647f3c 100644 --- a/docs/zh-hant/docs/index.md +++ b/docs/zh-hant/docs/index.md @@ -1,5 +1,11 @@ +# FastAPI { #fastapi } + + +

- FastAPI + FastAPI

FastAPI 框架,高效能,易於學習,快速開發,適用於生產環境 @@ -21,138 +27,140 @@ --- -**文件**: https://fastapi.tiangolo.com +**文件**: https://fastapi.tiangolo.com/zh-hant **程式碼**: https://github.com/fastapi/fastapi --- -FastAPI 是一個現代、快速(高效能)的 web 框架,用於 Python 並採用標準 Python 型別提示。 +FastAPI 是一個現代、快速(高效能)的 Web 框架,用於以 Python 並基於標準的 Python 型別提示來構建 API。 主要特點包含: -- **快速**: 非常高的效能,可與 **NodeJS** 和 **Go** 效能相當 (歸功於 Starlette and Pydantic)。 [FastAPI 是最快的 Python web 框架之一](#performance)。 -- **極速開發**: 提高開發功能的速度約 200% 至 300%。 \* -- **更少的 Bug**: 減少約 40% 的人為(開發者)導致的錯誤。 \* -- **直覺**: 具有出色的編輯器支援,處處都有自動補全以減少偵錯時間。 -- **簡單**: 設計上易於使用和學習,大幅減少閱讀文件的時間。 -- **簡潔**: 最小化程式碼重複性。可以通過不同的參數聲明來實現更豐富的功能,和更少的錯誤。 -- **穩健**: 立即獲得生產級可用的程式碼,還有自動生成互動式文件。 -- **標準化**: 基於 (且完全相容於) OpenAPIs 的相關標準:OpenAPI(之前被稱為 Swagger)和JSON Schema。 +* **快速**:非常高的效能,可與 **NodeJS** 和 **Go** 相當(歸功於 Starlette 和 Pydantic)。[最快的 Python 框架之一](#performance)。 +* **極速開發**:開發功能的速度可提升約 200% 至 300%。* +* **更少的 Bug**:減少約 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 %} -其他贊助商 +其他贊助商 -## 評價 +## 評價 { #opinions } -"_[...] 近期大量的使用 **FastAPI**。 [...] 目前正在計畫在**微軟**團隊的**機器學習**服務中導入。其中一些正在整合到核心的 **Windows** 產品和一些 **Office** 產品。_" +"_[...] 近期大量使用 **FastAPI**。[...] 我實際上打算在我在**微軟**團隊的所有**機器學習**服務上使用它。其中一些正在整合到核心的 **Windows** 產品,以及一些 **Office** 產品。_"

Kabir Khan - Microsoft (ref)
--- -"_我們使用 **FastAPI** 來建立產生**預測**結果的 **REST** 伺服器。 [for Ludwig]_" +"_我們採用了 **FastAPI** 函式庫來啟動一個 **REST** 伺服器,供查詢以取得**預測**。[for Ludwig]_"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** 很榮幸地宣布開源**危機管理**協調框架: **Dispatch**! [是使用 **FastAPI** 建構]_" +"_**Netflix** 很高興宣布我們的**危機管理**協調框架 **Dispatch** 開源![使用 **FastAPI** 建構]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_我對 **FastAPI** 興奮得不得了。它太有趣了!_" +"_我對 **FastAPI** 興奮得不得了。超好玩!_" -
Brian Okken - Python Bytes podcast host (ref)
+
Brian Okken - Python Bytes podcast 主持人 (ref)
--- -"_老實說,你建造的東西看起來非常堅固和精緻。在很多方面,這就是我想要的,看到有人建造它真的很鼓舞人心。_" +"_老實說,你們做的看起來非常穩健又精緻。很多方面都正是我希望 **Hug** 成為的樣子——看到有人把它建出來真的很鼓舞人心。_" -
Timothy Crosley - Hug creator (ref)
+
Timothy Crosley - Hug 創作者 (ref)
--- -"_如果您想學習一種用於構建 REST API 的**現代框架**,不能錯過 **FastAPI** [...] 它非常快速、且易於使用和學習 [...]_" +"_如果你想學一個用於構建 REST API 的**現代框架**,看看 **FastAPI** [...] 它很快、易用、也容易學習 [...]_" -"_我們的 **APIs** 已經改用 **FastAPI** [...] 我想你會喜歡它 [...]_" +"_我們的 **API** 已經改用 **FastAPI** [...] 我想你會喜歡它 [...]_" -
Ines Montani - Matthew Honnibal - Explosion AI 創辦人 - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - Explosion AI 創辦人 - spaCy 創作者 (ref) - (ref)
--- -"_如果有人想要建立一個生產環境的 Python API,我強烈推薦 **FastAPI**,它**設計精美**,**使用簡單**且**高度可擴充**,它已成為我們 API 優先開發策略中的**關鍵組件**,並且驅動了許多自動化服務,例如我們的 Virtual TAC Engineer。_" +"_如果有人想要建立一個可投入生產的 Python API,我強烈推薦 **FastAPI**。它**設計精美**、**使用簡單**且**高度可擴充**,已成為我們 API 優先開發策略中的**關鍵組件**,推動了許多自動化與服務,例如我們的 Virtual TAC Engineer。_"
Deon Pillsbury - Cisco (ref)
--- -## **Typer**,命令列中的 FastAPI +## FastAPI 迷你紀錄片 { #fastapi-mini-documentary } + +在 2025 年底發布了一支 FastAPI 迷你紀錄片,你可以在線上觀看: + +FastAPI Mini Documentary + +## **Typer**,命令列的 FastAPI { #typer-the-fastapi-of-clis } -如果你不是在開發網頁 API,而是正在開發一個在終端機中運行的命令列應用程式,不妨嘗試 **Typer**。 +如果你不是在做 Web API,而是要建立一個在終端機中使用的 CLI 應用程式,可以看看 **Typer**。 -**Typer** 是 FastAPI 的小兄弟。他立志成為命令列的 **FastAPI**。 ⌨️ 🚀 +**Typer** 是 FastAPI 的小老弟。他立志成為命令列世界的 **FastAPI**。⌨️ 🚀 -## 安裝需求 +## 需求 { #requirements } FastAPI 是站在以下巨人的肩膀上: -- Starlette 負責網頁的部分 -- Pydantic 負責資料的部分 +* Starlette 負責 Web 部分。 +* Pydantic 負責資料部分。 -## 安裝 +## 安裝 { #installation } + +建立並啟用一個虛擬環境,然後安裝 FastAPI:
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-你同時也會需要 ASGI 伺服器用於生產環境,像是 UvicornHypercorn。 +**注意**:請務必將 `"fastapi[standard]"` 用引號包起來,以確保在所有終端機中都能正常運作。 -
+## 範例 { #example } -```console -$ pip install "uvicorn[standard]" +### 建立 { #create-it } ----> 100% -``` - -
- -## 範例 - -### 建立 - -- 建立一個 python 檔案 `main.py`,並寫入以下程式碼: +建立檔案 `main.py`,內容如下: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -164,18 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-或可以使用 async def... +或使用 async def... -如果你的程式使用 `async` / `await`,請使用 `async def`: - -```Python hl_lines="9 14" -from typing import Union +如果你的程式碼使用 `async` / `await`,請使用 `async def`: +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -187,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` **注意**: -如果你不知道是否會用到,可以查看 _"In a hurry?"_ 章節中,關於 `async` 和 `await` 的部分。 +如果你不確定,請查看文件中 _"In a hurry?"_ 章節的 `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. ``` @@ -216,21 +235,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。 -你將會看到以下的 JSON 回應: +你將會看到以下 JSON 回應: ```JSON {"item_id": 5, "q": "somequery"} @@ -238,36 +257,34 @@ INFO: Application startup complete. 你已經建立了一個具有以下功能的 API: -- 透過路徑 `/` 和 `/items/{item_id}` 接受 HTTP 請求。 -- 以上路經都接受 `GET` 請求(也被稱為 HTTP _方法_)。 -- 路徑 `/items/{item_id}` 有一個 `int` 型別的 `item_id` 參數。 -- 路徑 `/items/{item_id}` 有一個 `str` 型別的查詢參數 `q`。 +* 透過路徑 `/` 和 `/items/{item_id}` 接受 HTTP 請求。 +* 以上兩個路徑都接受 `GET` 操作(也被稱為 HTTP _方法_)。 +* 路徑 `/items/{item_id}` 有一個 `int` 型別的路徑參數 `item_id`。 +* 路徑 `/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) -### ReDoc API 文件 +### 替代 API 文件 { #alternative-api-docs } -使用瀏覽器開啟 http://127.0.0.1:8000/redoc。 +現在前往 http://127.0.0.1:8000/redoc。 -你將看到 ReDoc 文件 (由 ReDoc 生成): +你會看到另一種自動文件(由 ReDoc 提供): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 範例升級 +## 範例升級 { #example-upgrade } -現在繼續修改 `main.py` 檔案,來接收一個帶有 body 的 `PUT` 請求。 +現在修改 `main.py` 檔案,使其能從 `PUT` 請求接收 body。 -我們使用 Pydantic 來使用標準的 Python 型別聲明請求。 - -```Python hl_lines="4 9-12 25-27" -from typing import Union +多虧了 Pydantic,你可以用標準的 Python 型別來宣告 body。 +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -277,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -286,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -295,43 +312,43 @@ 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。 +前往 http://127.0.0.1:8000/docs。 -- 互動式 API 文件會自動更新,並加入新的 body 請求: +* 互動式 API 文件會自動更新,包含新的 body: ![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) -### ReDoc API 文件升級 +### 替代 API 文件升級 { #alternative-api-docs-upgrade } -使用瀏覽器開啟 http://127.0.0.1:8000/redoc。 +現在前往 http://127.0.0.1:8000/redoc。 -- ReDoc API 文件會自動更新,並加入新的參數和 body 請求: +* 替代文件也會反映新的查詢參數與 body: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 總結 +### 總結 { #recap } -總結來說, 你就像宣告函式的參數型別一樣,只宣告了一次請求參數和請求主體參數等型別。 +總結來說,你只需在函式參數中**一次**宣告參數、body 等的型別。 -你使用 Python 標準型別來完成聲明。 +你使用的是現代標準的 Python 型別。 -你不需要學習新的語法、類別、方法或函式庫等等。 +你不需要學新的語法、特定函式庫的方法或類別,等等。 -只需要使用 **Python 以上的版本**。 +就用標準的 **Python**。 -舉個範例,比如宣告 int 的型別: +例如,對於一個 `int`: ```Python item_id: int @@ -343,126 +360,200 @@ item_id: int item: Item ``` -在進行一次宣告後,你將獲得: +…透過一次宣告,你將獲得: -- 編輯器支援: - - 自動補全 - - 型別檢查 -- 資料驗證: - - 驗證失敗時自動生成清楚的錯誤訊息 - - 可驗證多層巢狀的 JSON 物件 -- 轉換輸入的資料: 轉換來自網路請求到 Python 資料型別。包含以下數據: - - JSON - - 路徑參數 - - 查詢參數 - - Cookies - - 請求標頭 - - 表單 - - 文件 -- 轉換輸出的資料: 轉換 Python 資料型別到網路傳輸的 JSON: - - 轉換 Python 型別 (`str`、 `int`、 `float`、 `bool`、 `list` 等) - - `datetime` 物件 - - `UUID` 物件 - - 數據模型 - - ...還有其他更多 -- 自動生成的 API 文件,包含 2 種不同的使用介面: - - Swagger UI - - ReDoc +* 編輯器支援,包括: + * 自動補全。 + * 型別檢查。 +* 資料驗證: + * 當資料無效時,自動且清楚的錯誤。 + * 即使是深度巢狀的 JSON 物件也能驗證。 +* 輸入資料的 轉換:從網路讀入到 Python 資料與型別。包含: + * JSON。 + * 路徑參數。 + * 查詢參數。 + * Cookies。 + * 標頭。 + * 表單。 + * 檔案。 +* 輸出資料的 轉換:從 Python 資料與型別轉換為網路資料(JSON): + * 轉換 Python 型別(`str`、`int`、`float`、`bool`、`list` 等)。 + * `datetime` 物件。 + * `UUID` 物件。 + * 資料庫模型。 + * ...還有更多。 +* 自動生成的互動式 API 文件,包含 2 種替代的使用者介面: + * Swagger UI。 + * ReDoc。 --- -回到前面的的程式碼範例,**FastAPI** 還會: +回到前面的程式碼範例,**FastAPI** 會: -- 驗證 `GET` 和 `PUT` 請求路徑中是否包含 `item_id`。 -- 驗證 `GET` 和 `PUT` 請求中的 `item_id` 是否是 `int` 型別。 - - 如果驗證失敗,將會返回清楚有用的錯誤訊息。 -- 查看 `GET` 請求中是否有命名為 `q` 的查詢參數 (例如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 - - 因為 `q` 參數被宣告為 `= None`,所以是選填的。 - - 如果沒有宣告 `None`,則此參數將會是必填 (例如 `PUT` 範例的請求 body)。 -- 對於 `PUT` 的請求 `/items/{item_id}`,將會讀取 body 為 JSON: - - 驗證是否有必填屬性 `name` 且型別是 `str`。 - - 驗證是否有必填屬性 `price` 且型別是 `float`。 - - 驗證是否有選填屬性 `is_offer` 且型別是 `bool`。 - - 以上驗證都適用於多層次巢狀 JSON 物件。 -- 自動轉換 JSON 格式。 -- 透過 OpenAPI 文件來記錄所有內容,可以被用於: - - 互動式文件系統。 - - 自動為多種程式語言生成用戶端的程式碼。 -- 提供兩種交互式文件介面。 +* 驗證 `GET` 與 `PUT` 請求的路徑中是否包含 `item_id`。 +* 驗證 `GET` 與 `PUT` 請求中的 `item_id` 是否為 `int` 型別。 + * 如果不是,客戶端會看到清楚有用的錯誤。 +* 在 `GET` 請求中檢查是否有名為 `q` 的可選查詢參數(如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 + * 因為 `q` 參數被宣告為 `= None`,所以它是可選的。 + * 若沒有 `None`,則它會是必填(就像 `PUT` 時的 body)。 +* 對於 `/items/{item_id}` 的 `PUT` 請求,以 JSON 讀取 body: + * 檢查是否有必填屬性 `name`,且為 `str`。 + * 檢查是否有必填屬性 `price`,且為 `float`。 + * 檢查是否有可選屬性 `is_offer`,若存在則應為 `bool`。 + * 以上也適用於深度巢狀的 JSON 物件。 +* 自動在 JSON 與 Python 之間轉換。 +* 以 OpenAPI 記錄所有內容,可用於: + * 互動式文件系統。 + * 為多種語言自動產生用戶端程式碼的系統。 +* 直接提供兩種互動式文件網頁介面。 --- -雖然我們只敘述了表面的功能,但其實你已經理解了它是如何執行。 +我們只觸及了表面,但你已經了解它的運作方式了。 -試著修改以下程式碼: +試著把這一行: ```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) -有關更多功能的完整範例,可以參考 教學 - 使用者指南。 +若想看包含更多功能的完整範例,請參考 教學 - 使用者指南。 -**劇透警告**: 教學 - 使用者指南內容有: +**劇透警告**:教學 - 使用者指南包含: -- 對來自不同地方的**參數**進行宣告:像是 **headers**, **cookies**, **form 表單**以及**上傳檔案**。 -- 如何設定 **驗證限制** 像是 `maximum_length` or `regex`。 -- 簡單且非常容易使用的 **依賴注入** 系統。 -- 安全性和身份驗證,包含提供支援 **OAuth2**、**JWT tokens** 和 **HTTP Basic** 驗證。 -- 更進階 (但同樣簡單) 的宣告 **多層次的巢狀 JSON 格式** (感謝 Pydantic)。 -- **GraphQL** 與 Strawberry 以及其他的相關函式庫進行整合。 -- 更多其他的功能 (感謝 Starlette) 像是: - - **WebSockets** - - 於 HTTPX 和 `pytest` 的非常簡單測試 - - **CORS** - - **Cookie Sessions** - - ...以及更多 +* 來自不同來源的**參數**宣告:例如 **headers**、**cookies**、**form fields** 和 **files**。 +* 如何設定**驗證限制**,如 `maximum_length` 或 `regex`。 +* 一個非常強大且易用的 **依賴注入** 系統。 +* 安全與驗證,包含支援 **OAuth2** 搭配 **JWT tokens** 與 **HTTP Basic** 驗證。 +* 宣告**深度巢狀 JSON 模型**的進階(但同樣簡單)技巧(感謝 Pydantic)。 +* 與 Strawberry 及其他函式庫的 **GraphQL** 整合。 +* 許多額外功能(感謝 Starlette),例如: + * **WebSockets** + * 基於 HTTPX 與 `pytest` 的極其簡單的測試 + * **CORS** + * **Cookie Sessions** + * ...以及更多。 -## 效能 +### 部署你的應用(可選) { #deploy-your-app-optional } -來自獨立機構 TechEmpower 的測試結果,顯示在 Uvicorn 執行下的 **FastAPI** 是 最快的 Python 框架之一, 僅次於 Starlette 和 Uvicorn 本身 (兩者是 FastAPI 的底層)。 (\*) +你也可以選擇將 FastAPI 應用部署到 FastAPI Cloud,如果你還沒加入,去登記等候名單吧。🚀 -想了解更多訊息,可以參考 測試結果。 +如果你已經有 **FastAPI Cloud** 帳號(我們已從等候名單邀請你 😉),你可以用一個指令部署你的應用。 -## 可選的依賴套件 +部署前,先確認你已登入: -用於 Pydantic: +
-- email-validator - 用於電子郵件驗證。 -- pydantic-settings - 用於設定管理。 -- pydantic-extra-types - 用於與 Pydantic 一起使用的額外型別。 +```console +$ fastapi login -用於 Starlette: +You are logged in to FastAPI Cloud 🚀 +``` -- httpx - 使用 `TestClient`時必須安裝。 -- jinja2 - 使用預設的模板配置時必須安裝。 -- python-multipart - 需要使用 `request.form()` 對表單進行 "解析" 時安裝。 -- itsdangerous - 需要使用 `SessionMiddleware` 支援時安裝。 -- pyyaml - 用於支援 Starlette 的 `SchemaGenerator` (如果你使用 FastAPI,可能不需要它)。 +
-用於 FastAPI / Starlette: +接著部署你的應用: -- uvicorn - 用於加載和運行應用程式的服務器。 -- orjson - 使用 `ORJSONResponse`時必須安裝。 -- ujson - 使用 `UJSONResponse` 時必須安裝。 +
-你可以使用 `pip install "fastapi[all]"` 來安裝這些所有依賴套件。 +```console +$ fastapi deploy -## 授權 +Deploying to FastAPI Cloud... -該項目遵循 MIT 許可協議。 +✅ 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 與好朋友們」這些開源專案的主要贊助與資金來源。✨ + +#### 部署到其他雲端供應商 { #deploy-to-other-cloud-providers } + +FastAPI 是開源且基於標準。你可以把 FastAPI 應用部署到任何你選擇的雲端供應商。 + +依照你雲端供應商的指南來部署 FastAPI 應用吧。🤓 + +## 效能 { #performance } + +獨立的 TechEmpower 基準測試顯示,在 Uvicorn 下運行的 **FastAPI** 應用是最快的 Python 框架之一,僅次於 Starlette 與 Uvicorn 本身(FastAPI 內部使用它們)。(*) + +想了解更多,請參閱測試結果。 + +## 依賴套件 { #dependencies } + +FastAPI 依賴 Pydantic 與 Starlette。 + +### `standard` 依賴套件 { #standard-dependencies } + +當你以 `pip install "fastapi[standard]"` 安裝 FastAPI 時,會包含 `standard` 這組可選依賴套件: + +Pydantic 會使用: + +* email-validator - 用於電子郵件驗證。 + +Starlette 會使用: + +* httpx - 若要使用 `TestClient` 必須安裝。 +* jinja2 - 若要使用預設的模板設定必須安裝。 +* python-multipart - 若要支援表單 "解析",搭配 `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 } + +如果你想安裝帶有 standard 依賴、但不包含 `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` 必須安裝。 + +## 授權 { #license } + +本專案以 MIT 授權條款釋出。 diff --git a/docs/zh-hant/docs/learn/index.md b/docs/zh-hant/docs/learn/index.md index eb7d7096a..43e4519a5 100644 --- a/docs/zh-hant/docs/learn/index.md +++ b/docs/zh-hant/docs/learn/index.md @@ -1,5 +1,5 @@ -# 學習 +# 學習 { #learn } -以下是學習 FastAPI 的入門介紹和教學。 +以下是學習 **FastAPI** 的入門介紹和教學。 你可以將其視為一本**書籍**或一門**課程**,這是**官方**認可並推薦的 FastAPI 學習方式。 😎 diff --git a/docs/zh-hant/docs/resources/index.md b/docs/zh-hant/docs/resources/index.md index f4c70a3a0..ea77cb728 100644 --- a/docs/zh-hant/docs/resources/index.md +++ b/docs/zh-hant/docs/resources/index.md @@ -1,3 +1,3 @@ -# 資源 +# 資源 { #resources } -額外的資源、外部連結、文章等。 ✈️ +額外的資源、外部連結等。 ✈️ diff --git a/docs/zh-hant/docs/tutorial/first-steps.md b/docs/zh-hant/docs/tutorial/first-steps.md index d6684bc51..6bcb453bf 100644 --- a/docs/zh-hant/docs/tutorial/first-steps.md +++ b/docs/zh-hant/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 第一步 +# 第一步 { #first-steps } 最簡單的 FastAPI 檔案可能看起來像這樣: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} 將其複製到一個名為 `main.py` 的文件中。 @@ -11,47 +11,39 @@
```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) 那列顯示了你的應用程式正在本地端機器上運行的 URL。 -### 查看它 +### 查看它 { #check-it } 在瀏覽器中打開 http://127.0.0.1:8000. @@ -74,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 互動式 API 文件 +### 互動式 API 文件 { #interactive-api-docs } 現在,前往 http://127.0.0.1:8000/docs. @@ -82,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![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. @@ -90,33 +82,33 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** 使用定義 API 的 **OpenAPI** 標準來生成一個 「schema」 與你的所有 API。 +**FastAPI** 使用定義 API 的 **OpenAPI** 標準來生成一個「schema」,涵蓋你的全部 API。 -#### 「Schema」 +#### 「Schema」 { #schema } 「schema」是對某個事物的定義或描述。它並不是實作它的程式碼,而僅僅是一個抽象的描述。 -#### API 「schema」 +#### API 「schema」 { #api-schema } 在這種情況下,OpenAPI 是一個規範,它規定了如何定義 API 的 schema。 這個 schema 定義包含了你的 API 路徑、可能接收的參數等內容。 -#### 資料 「schema」 +#### 資料「schema」 { #data-schema } 「schema」這個術語也可能指某些資料的結構,比如 JSON 內容的結構。 在這種情況下,它指的是 JSON 的屬性、資料型別等。 -#### OpenAPI 和 JSON Schema +#### OpenAPI 和 JSON Schema { #openapi-and-json-schema } -OpenAPI 定義了 API 的 schema。這個 schema 包含了使用 **JSON Schema** 定義的資料,這是 JSON 資料 schema 的標準。 +OpenAPI 為你的 API 定義了 API 的 schema。而該 schema 會包含你的 API 所傳送與接收資料的定義(或稱「schemas」),使用 **JSON Schema**,這是 JSON 資料 schema 的標準。 -#### 檢查 `openapi.json` +#### 檢查 `openapi.json` { #check-the-openapi-json } -如果你好奇原始的 OpenAPI schema 長什麼樣子,FastAPI 會自動生成一個包含所有 API 描述的 JSON (schema)。 +如果你好奇原始的 OpenAPI schema 長什麼樣子,FastAPI 會自動生成一個包含所有 API 描述的 JSON(schema)。 你可以直接在 http://127.0.0.1:8000/openapi.json 查看它。 @@ -143,23 +135,59 @@ OpenAPI 定義了 API 的 schema。這個 schema 包含了使用 **JSON Schema** ... ``` -#### OpenAPI 的用途 +#### OpenAPI 的用途 { #what-is-openapi-for } OpenAPI schema 驅動了兩個互動式文件系統。 而且有許多替代方案,所有這些都是基於 OpenAPI。你可以輕鬆地將任何這些替代方案添加到使用 **FastAPI** 建置的應用程式中。 -你也可以用它自動生成程式碼,讓前端、手機應用程式或物聯網設備等與你的 API 進行通訊。 +你也可以用它自動生成程式碼,讓用戶端與你的 API 通訊。例如前端、手機或物聯網(IoT)應用程式。 -## 逐步回顧 +### 部署你的應用程式(可選) { #deploy-your-app-optional } -### 第一步:引入 `FastAPI` +你可以選擇將你的 FastAPI 應用程式部署到 FastAPI Cloud,如果還沒有,去加入候補名單吧。🚀 -{* ../../docs_src/first_steps/tutorial001.py h1[1] *} +如果你已經有 **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 } + +### 第一步:引入 `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` 是一個 Python 類別,提供所有 API 的全部功能。 -/// note | Technical Details +/// note | 技術細節 `FastAPI` 是一個直接繼承自 `Starlette` 的類別。 @@ -167,17 +195,17 @@ OpenAPI schema 驅動了兩個互動式文件系統。 /// -### 第二步:建立一個 `FastAPI` 「實例」 +### 第二步:建立一個 `FastAPI`「實例」 { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py h1[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} 這裡的 `app` 變數將會是 `FastAPI` 類別的「實例」。 這將是你建立所有 API 的主要互動點。 -### 第三步:建立一個 *路徑操作* +### 第三步:建立一個「路徑操作」 { #step-3-create-a-path-operation } -#### 路徑 +#### 路徑 { #path } 這裡的「路徑」指的是 URL 中自第一個 `/` 以後的部分。 @@ -201,7 +229,7 @@ https://example.com/items/foo 在建置 API 時,「路徑」是分離「關注點」和「資源」的主要方式。 -#### 操作 +#### 操作 { #operation } 這裡的「操作」指的是 HTTP 的「方法」之一。 @@ -236,16 +264,16 @@ https://example.com/items/foo 我們將會稱它們為「**操作**」。 -#### 定義一個 *路徑操作裝飾器* +#### 定義一個「路徑操作裝飾器」 { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py h1[6] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} `@app.get("/")` 告訴 **FastAPI** 那個函式負責處理請求: * 路徑 `/` * 使用 get操作 -/// info | `@decorator` Info +/// info | `@decorator` 說明 Python 中的 `@something` 語法被稱為「裝飾器」。 @@ -253,7 +281,7 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 一個「裝飾器」會對下面的函式做一些事情。 -在這種情況下,這個裝飾器告訴 **FastAPI** 那個函式對應於 **路徑** `/` 和 **操作** `get`. +在這種情況下,這個裝飾器告訴 **FastAPI** 那個函式對應於 **路徑** `/` 和 **操作** `get`。 這就是「**路徑操作裝飾器**」。 @@ -284,27 +312,27 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 /// -### 第四步:定義 **路徑操作函式** +### 第四步:定義「路徑操作函式」 { #step-4-define-the-path-operation-function } 這是我們的「**路徑操作函式**」: -* **path**: 是 `/`. -* **operation**: 是 `get`. -* **function**: 是裝飾器下面的函式(在 `@app.get("/")` 下面)。 +* **path**:是 `/`。 +* **operation**:是 `get`。 +* **function**:是裝飾器下面的函式(在 `@app.get("/")` 下面)。 -{* ../../docs_src/first_steps/tutorial001.py h1[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} 這就是一個 Python 函式。 -它將會在 **FastAPI** 收到一個請求時被呼叫,使用 `GET` 操作。 +它將會在 **FastAPI** 收到一個使用 `GET` 操作、網址為「`/`」的請求時被呼叫。 在這種情況下,它是一個 `async` 函式。 --- -你可以將它定義為一個正常的函式,而不是 `async def`: +你也可以將它定義為一般函式,而不是 `async def`: -{* ../../docs_src/first_steps/tutorial003.py h1[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note @@ -312,9 +340,9 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 /// -### 第五步:回傳內容 +### 第五步:回傳內容 { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py h1[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} 你可以返回一個 `dict`、`list`、單個值作為 `str`、`int` 等。 @@ -322,10 +350,31 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 有很多其他物件和模型會自動轉換為 JSON(包括 ORMs,等等)。試用你最喜歡的,很有可能它們已經有支援。 -## 回顧 +### 第六步:部署 { #step-6-deploy-it } -* 引入 `FastAPI`. +用一行指令將你的應用程式部署到 **FastAPI Cloud**:`fastapi deploy`。🎉 + +#### 關於 FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** 由 **FastAPI** 的作者與團隊打造。 + +它讓你以最小的成本完成 API 的**建置**、**部署**與**存取**流程。 + +它把用 FastAPI 開發應用的同樣**開發者體驗**帶到將應用**部署**到雲端的流程中。🎉 + +FastAPI Cloud 也是「FastAPI 與其好友」這些開源專案的主要贊助與資金提供者。✨ + +#### 部署到其他雲端供應商 { #deploy-to-other-cloud-providers } + +FastAPI 是開源並基於標準的。你可以把 FastAPI 應用部署到你選擇的任何雲端供應商。 + +依照你的雲端供應商的指南部署 FastAPI 應用吧。🤓 + +## 回顧 { #recap } + +* 引入 `FastAPI`。 * 建立一個 `app` 實例。 -* 寫一個 **路徑操作裝飾器** 使用裝飾器像 `@app.get("/")`。 -* 定義一個 **路徑操作函式**;例如,`def root(): ...`。 +* 寫一個「路徑操作裝飾器」,像是 `@app.get("/")`。 +* 定義一個「路徑操作函式」;例如,`def root(): ...`。 * 使用命令 `fastapi dev` 執行開發伺服器。 +* 可選:使用 `fastapi deploy` 部署你的應用程式。 diff --git a/docs/zh-hant/docs/tutorial/index.md b/docs/zh-hant/docs/tutorial/index.md index ae0056f52..5e1961fd7 100644 --- a/docs/zh-hant/docs/tutorial/index.md +++ b/docs/zh-hant/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 教學 - 使用者指南 +# 教學 - 使用者指南 { #tutorial-user-guide } 本教學將一步一步展示如何使用 **FastAPI** 及其大多數功能。 @@ -6,7 +6,7 @@ 它也被設計成可作為未來的參考,讓你隨時回來查看所需的內容。 -## 運行程式碼 +## 運行程式碼 { #run-the-code } 所有程式碼區塊都可以直接複製和使用(它們實際上是經過測試的 Python 檔案)。 @@ -15,48 +15,39 @@
```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 @@ $ fastapi dev -/// note +/// note | 注意 -當你使用 `pip install "fastapi[standard]"` 安裝時,會包含一些預設的可選標準依賴項。 +當你使用 `pip install "fastapi[standard]"` 安裝時,會包含一些預設的可選標準依賴項,其中包括 `fastapi-cloud-cli`,它可以讓你部署到 FastAPI Cloud。 -如果你不想包含那些可選的依賴項,你可以使用 `pip install fastapi` 來安裝。 +如果你不想包含那些可選的依賴項,你可以改為安裝 `pip install fastapi`。 + +如果你想安裝標準依賴項,但不包含 `fastapi-cloud-cli`,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"` 安裝。 /// -## 進階使用者指南 +## 進階使用者指南 { #advanced-user-guide } -還有一個**進階使用者指南**你可以稍後閱讀。 +還有一個**進階使用者指南**你可以在讀完這個**教學 - 使用者指南**後再閱讀。 **進階使用者指南**建立在這個教學之上,使用相同的概念,並教你一些額外的功能。 diff --git a/docs/zh-hant/docs/tutorial/query-param-models.md b/docs/zh-hant/docs/tutorial/query-param-models.md index 76ee74016..e36028199 100644 --- a/docs/zh-hant/docs/tutorial/query-param-models.md +++ b/docs/zh-hant/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# 查詢參數模型 +# 查詢參數模型 { #query-parameter-models } 如果你有一組具有相關性的**查詢參數**,你可以建立一個 **Pydantic 模型**來聲明它們。 @@ -10,7 +10,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 /// -## 使用 Pydantic 模型的查詢參數 +## 使用 Pydantic 模型的查詢參數 { #query-parameters-with-a-pydantic-model } 在一個 **Pydantic 模型**中聲明你需要的**查詢參數**,然後將參數聲明為 `Query`: @@ -18,7 +18,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 **FastAPI** 將會從請求的**查詢參數**中**提取**出**每個欄位**的資料,並將其提供給你定義的 Pydantic 模型。 -## 查看文件 +## 查看文件 { #check-the-docs } 你可以在 `/docs` 頁面的 UI 中查看查詢參數: @@ -26,7 +26,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 -## 禁止額外的查詢參數 +## 禁止額外的查詢參數 { #forbid-extra-query-parameters } 在一些特殊的使用場景中(可能不是很常見),你可能希望**限制**你要收到的查詢參數。 @@ -57,7 +57,7 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## 總結 +## 總結 { #summary } 你可以使用 **Pydantic 模型**在 **FastAPI** 中聲明**查詢參數**。😎 diff --git a/docs/zh-hant/docs/virtual-environments.md b/docs/zh-hant/docs/virtual-environments.md index d8e31e08e..acd1d914e 100644 --- a/docs/zh-hant/docs/virtual-environments.md +++ b/docs/zh-hant/docs/virtual-environments.md @@ -1,4 +1,4 @@ -# 虛擬環境 +# 虛擬環境 { #virtual-environments } 當你在 Python 專案中工作時,你可能會需要使用一個**虛擬環境**(或類似的機制)來隔離你為每個專案安裝的套件。 @@ -26,7 +26,7 @@ /// -## 建立一個專案 +## 建立一個專案 { #create-a-project } 首先,為你的專案建立一個目錄。 @@ -51,7 +51,7 @@ $ cd awesome-project -## 建立一個虛擬環境 +## 建立一個虛擬環境 { #create-a-virtual-environment } 在開始一個 Python 專案的**第一時間**,**在你的專案內部**建立一個虛擬環境。 @@ -114,7 +114,7 @@ $ uv venv /// -## 啟動虛擬環境 +## 啟動虛擬環境 { #activate-the-virtual-environment } 啟動新的虛擬環境來確保你運行的任何 Python 指令或安裝的套件都能使用到它。 @@ -166,11 +166,11 @@ $ source .venv/Scripts/activate 每次你在這個環境中安裝一個**新的套件**時,都需要**重新啟動**這個環境。 -這麼做確保了當你使用一個由這個套件安裝的**終端(CLI)程式**時,你使用的是你的虛擬環境中的程式,而不是全域安裝、可能版本不同的程式。 +這麼做確保了當你使用一個由這個套件安裝的**終端(CLI)程式**時,你使用的是你的虛擬環境中的程式,而不是全域安裝、可能版本不同的程式。 /// -## 檢查虛擬環境是否啟動 +## 檢查虛擬環境是否啟動 { #check-the-virtual-environment-is-active } 檢查虛擬環境是否啟動(前面的指令是否生效)。 @@ -212,7 +212,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -## 升級 `pip` +## 升級 `pip` { #upgrade-pip } /// tip @@ -242,7 +242,27 @@ $ python -m pip install --upgrade pip -## 加入 `.gitignore` +/// tip | 注意 + +有時你在嘗試升級 pip 時,可能會遇到 **`No module named pip`** 的錯誤。 + +如果發生這種情況,請用下面的指令安裝並升級 pip: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +此指令會在未安裝 pip 時為你安裝它,並確保安裝的 pip 版本至少與 `ensurepip` 所提供的版本一樣新。 + +/// + +## 加入 `.gitignore` { #add-gitignore } 如果你使用 **Git**(這是你應該使用的),加入一個 `.gitignore` 檔案來排除你的 `.venv` 中的所有內容。 @@ -282,7 +302,7 @@ $ echo "*" > .venv/.gitignore /// -## 安裝套件 +## 安裝套件 { #install-packages } 在啟用虛擬環境後,你可以在其中安裝套件。 @@ -294,7 +314,7 @@ $ echo "*" > .venv/.gitignore /// -### 直接安裝套件 +### 直接安裝套件 { #install-packages-directly } 如果你急於安裝,不想使用檔案來聲明專案的套件依賴,你可以直接安裝它們。 @@ -333,7 +353,7 @@ $ uv pip install "fastapi[standard]" //// -### 從 `requirements.txt` 安裝 +### 從 `requirements.txt` 安裝 { #install-from-requirements-txt } 如果你有一個 `requirements.txt` 檔案,你可以使用它來安裝其中的套件。 @@ -376,7 +396,7 @@ pydantic==2.8.0 /// -## 執行程式 +## 執行程式 { #run-your-program } 在啟用虛擬環境後,你可以執行你的程式,它將使用虛擬環境中的 Python 和你在其中安裝的套件。 @@ -390,7 +410,7 @@ Hello World -## 設定編輯器 +## 設定編輯器 { #configure-your-editor } 你可能會用到編輯器,請確保設定它使用你建立的相同虛擬環境(它可能會自動偵測到),以便你可以獲得自動完成和内嵌錯誤提示。 @@ -405,7 +425,7 @@ Hello World /// -## 退出虛擬環境 +## 退出虛擬環境 { #deactivate-the-virtual-environment } 當你完成工作後,你可以**退出**虛擬環境。 @@ -419,7 +439,7 @@ $ deactivate 這樣,當你執行 `python` 時它不會嘗試從已安裝套件的虛擬環境中執行。 -## 開始工作 +## 開始工作 { #ready-to-work } 現在你已經準備好開始你的工作了。 @@ -433,7 +453,7 @@ $ deactivate /// -## 為什麼要使用虛擬環境 +## 為什麼要使用虛擬環境 { #why-virtual-environments } 你需要安裝 Python 才能使用 FastAPI。 @@ -443,7 +463,7 @@ $ deactivate 然而,如果你直接使用 `pip`,套件將會安裝在你的**全域 Python 環境**中(即 Python 的全域安裝)。 -### 存在的問題 +### 存在的問題 { #the-problem } 那麼,在全域 Python 環境中安裝套件有什麼問題呢? @@ -526,7 +546,7 @@ Python 套件在推出**新版本**時通常會儘量**避免破壞性更改** 此外,取決於你的操作系統(例如 Linux、Windows、macOS),它可能已經預先安裝了 Python。在這種情況下,它可能已經有一些系統所需的套件和特定版本。如果你在全域 Python 環境中安裝套件,可能會**破壞**某些隨作業系統一起安裝的程式。 -## 套件安裝在哪裡 +## 套件安裝在哪裡 { #where-are-packages-installed } 當你安裝 Python 時,它會在你的電腦中建立一些目錄並放置一些檔案。 @@ -552,7 +572,7 @@ $ pip install "fastapi[standard]" 預設情況下,這些下載和解壓的檔案會放置於隨 Python 安裝的目錄中,即**全域環境**。 -## 什麼是虛擬環境 +## 什麼是虛擬環境 { #what-are-virtual-environments } 解決套件都安裝在全域環境中的問題方法是為你所做的每個專案使用一個**虛擬環境**。 @@ -577,7 +597,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 啟用虛擬環境意味著什麼 +## 啟用虛擬環境意味著什麼 { #what-does-activating-a-virtual-environment-mean } 當你啟用了虛擬環境,例如: @@ -714,7 +734,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 啟用虛擬環境還會改變其他一些內容,但這是它所做的最重要的事情之一。 -## 檢查虛擬環境 +## 檢查虛擬環境 { #checking-a-virtual-environment } 當你檢查虛擬環境是否啟動時,例如: @@ -766,7 +786,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python /// -## 為什麼要停用虛擬環境 +## 為什麼要停用虛擬環境 { #why-deactivate-a-virtual-environment } 例如,你可能正在一個專案 `philosophers-stone` 上工作,**啟動了該虛擬環境**,安裝了套件並使用了該環境, @@ -820,7 +840,7 @@ I solemnly swear 🐺 -## 替代方案 +## 替代方案 { #alternatives } 這是一個簡單的指南,幫助你入門並教會你如何理解一切**底層**的原理。 @@ -837,7 +857,7 @@ I solemnly swear 🐺 * 確保你有一個**精確**的套件和版本集合來安裝,包括它們的依賴項,這樣你可以確保專案在生產環境中運行的狀態與開發時在你的電腦上運行的狀態完全相同,這被稱為**鎖定** * 還有很多其他功能 -## 結論 +## 結論 { #conclusion } 如果你讀過並理解了所有這些,現在**你對虛擬環境的了解已超過許多開發者**。🤓 diff --git a/docs/zh/docs/advanced/additional-responses.md b/docs/zh/docs/advanced/additional-responses.md index 362ef9460..bc197280a 100644 --- a/docs/zh/docs/advanced/additional-responses.md +++ b/docs/zh/docs/advanced/additional-responses.md @@ -1,44 +1,57 @@ -# OPENAPI 中的其他响应 +# OpenAPI 中的附加响应 { #additional-responses-in-openapi } -您可以声明附加响应,包括附加状态代码、媒体类型、描述等。 +/// warning | 警告 -这些额外的响应将包含在OpenAPI模式中,因此它们也将出现在API文档中。 +这是一个相对高级的话题。 -但是对于那些额外的响应,你必须确保你直接返回一个像 `JSONResponse` 一样的 `Response` ,并包含你的状态代码和内容。 - -## `model`附加响应 -您可以向路径操作装饰器传递参数 `responses` 。 - -它接收一个 `dict`,键是每个响应的状态代码(如`200`),值是包含每个响应信息的其他 `dict`。 - -每个响应字典都可以有一个关键模型,其中包含一个 `Pydantic` 模型,就像 `response_model` 一样。 - -**FastAPI**将采用该模型,生成其`JSON Schema`并将其包含在`OpenAPI`中的正确位置。 - -例如,要声明另一个具有状态码 `404` 和`Pydantic`模型 `Message` 的响应,可以写: -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} - -/// note - -请记住,您必须直接返回 `JSONResponse` 。 +如果你刚开始使用 **FastAPI**,可能暂时用不到。 /// -/// info +你可以声明附加响应,包括额外的状态码、媒体类型、描述等。 -`model` 密钥不是OpenAPI的一部分。 -**FastAPI**将从那里获取`Pydantic`模型,生成` JSON Schema` ,并将其放在正确的位置。 -- 正确的位置是: - - 在键 `content` 中,其具有另一个`JSON`对象( `dict` )作为值,该`JSON`对象包含: - - 媒体类型的密钥,例如 `application/json` ,它包含另一个`JSON`对象作为值,该对象包含: - - 一个键` schema` ,它的值是来自模型的`JSON Schema`,正确的位置在这里。 - - **FastAPI**在这里添加了对OpenAPI中另一个地方的全局JSON模式的引用,而不是直接包含它。这样,其他应用程序和客户端可以直接使用这些JSON模式,提供更好的代码生成工具等。 +这些附加响应会被包含在 OpenAPI 模式中,因此它们也会出现在 API 文档中。 + +但是对于这些附加响应,你必须确保直接返回一个 `Response`(例如 `JSONResponse`),并携带你的状态码和内容。 + +## 带有 `model` 的附加响应 { #additional-response-with-model } + +你可以向你的*路径操作装饰器*传入参数 `responses`。 + +它接收一个 `dict`:键是每个响应的状态码(例如 `200`),值是包含该响应信息的另一个 `dict`。 + +这些响应的每个 `dict` 都可以有一个键 `model`,包含一个 Pydantic 模型,就像 `response_model` 一样。 + +**FastAPI** 会获取该模型,生成它的 JSON Schema,并将其放在 OpenAPI 中的正确位置。 + +例如,要声明另一个状态码为 `404` 且具有 Pydantic 模型 `Message` 的响应,你可以这样写: + +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} + +/// note | 注意 + +记住你需要直接返回 `JSONResponse`。 /// -**在OpenAPI中为该路径操作生成的响应将是:** +/// info | 信息 -```json hl_lines="3-12" +`model` 键不是 OpenAPI 的一部分。 + +**FastAPI** 会从这里获取 Pydantic 模型,生成 JSON Schema,并把它放到正确的位置。 + +正确的位置是: + +* 在键 `content` 中,它的值是另一个 JSON 对象(`dict`),该对象包含: + * 一个媒体类型作为键,例如 `application/json`,它的值是另一个 JSON 对象,该对象包含: + * 一个键 `schema`,它的值是来自该模型的 JSON Schema,这里就是正确的位置。 + * **FastAPI** 会在这里添加一个引用,指向你 OpenAPI 中另一个位置的全局 JSON Schemas,而不是直接内联。这样,其他应用和客户端可以直接使用这些 JSON Schemas,提供更好的代码生成工具等。 + +/// + +为该*路径操作*在 OpenAPI 中生成的响应将是: + +```JSON hl_lines="3-12" { "responses": { "404": { @@ -73,10 +86,11 @@ } } } - ``` -**模式被引用到OpenAPI模式中的另一个位置:** -```json hl_lines="4-16" + +这些模式在 OpenAPI 模式中被引用到另一个位置: + +```JSON hl_lines="4-16" { "components": { "schemas": { @@ -153,48 +167,54 @@ } } } - ``` -## 主响应的其他媒体类型 -您可以使用相同的 `responses` 参数为相同的主响应添加不同的媒体类型。 +## 主响应的其他媒体类型 { #additional-media-types-for-the-main-response } -例如,您可以添加一个额外的媒体类型` image/png` ,声明您的路径操作可以返回JSON对象(媒体类型 `application/json` )或PNG图像: +你可以使用同一个 `responses` 参数为同一个主响应添加不同的媒体类型。 -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +例如,你可以添加一个额外的媒体类型 `image/png`,声明你的*路径操作*可以返回 JSON 对象(媒体类型为 `application/json`)或 PNG 图片: -/// note +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} -- 请注意,您必须直接使用 `FileResponse` 返回图像。 +/// note | 注意 + +请注意,你必须直接使用 `FileResponse` 返回图片。 /// -/// info +/// info | 信息 -- 除非在 `responses` 参数中明确指定不同的媒体类型,否则**FastAPI**将假定响应与主响应类具有相同的媒体类型(默认为` application/json` )。 -- 但是如果您指定了一个自定义响应类,并将 `None `作为其媒体类型,**FastAPI**将使用 `application/json` 作为具有关联模型的任何其他响应。 +除非你在 `responses` 参数中明确指定不同的媒体类型,否则 FastAPI 会假设响应与主响应类具有相同的媒体类型(默认是 `application/json`)。 + +但是如果你指定了一个媒体类型为 `None` 的自定义响应类,FastAPI 会对任何具有关联模型的附加响应使用 `application/json`。 /// -## 组合信息 -您还可以联合接收来自多个位置的响应信息,包括 `response_model `、 `status_code` 和 `responses `参数。 +## 组合信息 { #combining-information } -您可以使用默认的状态码 `200` (或者您需要的自定义状态码)声明一个 `response_model `,然后直接在OpenAPI模式中在 `responses` 中声明相同响应的其他信息。 +你也可以把来自多个位置的响应信息组合在一起,包括 `response_model`、`status_code` 和 `responses` 参数。 -**FastAPI**将保留来自 `responses` 的附加信息,并将其与模型中的JSON Schema结合起来。 +你可以声明一个 `response_model`,使用默认状态码 `200`(或根据需要使用自定义状态码),然后在 `responses` 中直接在 OpenAPI 模式里为同一个响应声明附加信息。 -例如,您可以使用状态码 `404` 声明响应,该响应使用`Pydantic`模型并具有自定义的` description` 。 +**FastAPI** 会保留来自 `responses` 的附加信息,并把它与你的模型生成的 JSON Schema 合并。 -以及一个状态码为 `200` 的响应,它使用您的 `response_model` ,但包含自定义的 `example` : +例如,你可以声明一个状态码为 `404` 的响应,它使用一个 Pydantic 模型并带有自定义的 `description`。 -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +以及一个状态码为 `200` 的响应,它使用你的 `response_model`,但包含自定义的 `example`: -所有这些都将被合并并包含在您的OpenAPI中,并在API文档中显示: +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} -## 联合预定义响应和自定义响应 +所有这些都会被合并并包含到你的 OpenAPI 中,并显示在 API 文档里: + + + +## 组合预定义响应和自定义响应 { #combine-predefined-responses-and-custom-ones } + +你可能希望有一些适用于许多*路径操作*的预定义响应,但同时又想把它们与每个*路径操作*所需的自定义响应组合在一起。 + +在这些情况下,你可以使用 Python 的“解包”`dict` 的技巧 `**dict_to_unpack`: -您可能希望有一些应用于许多路径操作的预定义响应,但是你想将不同的路径和自定义的相应组合在一块。 -对于这些情况,你可以使用Python的技术,将 `dict` 与 `**dict_to_unpack` 解包: ```Python old_dict = { "old key": "old value", @@ -203,19 +223,25 @@ old_dict = { new_dict = {**old_dict, "new key": "new value"} ``` -这里, new_dict 将包含来自 old_dict 的所有键值对加上新的键值对: -```python +这里,`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] *} -## 有关OpenAPI响应的更多信息 -要了解您可以在响应中包含哪些内容,您可以查看OpenAPI规范中的以下部分: - + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responsesObject),它包括 Response Object 。 - + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responseObject),您可以直接在 `responses` 参数中的每个响应中包含任何内容。包括 `description` 、 `headers` 、 `content` (其中是声明不同的媒体类型和JSON Schemas)和 `links` 。 +你可以使用该技巧在*路径操作*中重用一些预定义响应,并把它们与额外的自定义响应组合在一起。 + +例如: + +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} + +## 关于 OpenAPI 响应的更多信息 { #more-information-about-openapi-responses } + +要查看响应中究竟可以包含什么,你可以查看 OpenAPI 规范中的以下部分: + +* OpenAPI Responses 对象,它包含 `Response Object`。 +* OpenAPI Response 对象,你可以把这里的任何内容直接包含到 `responses` 参数中的每个响应里。包括 `description`、`headers`、`content`(在这里声明不同的媒体类型和 JSON Schemas),以及 `links`。 diff --git a/docs/zh/docs/advanced/additional-status-codes.md b/docs/zh/docs/advanced/additional-status-codes.md index b048a2a17..23ceab4e8 100644 --- a/docs/zh/docs/advanced/additional-status-codes.md +++ b/docs/zh/docs/advanced/additional-status-codes.md @@ -1,10 +1,10 @@ -# 额外的状态码 +# 额外的状态码 { #additional-status-codes } **FastAPI** 默认使用 `JSONResponse` 返回一个响应,将你的 *路径操作* 中的返回内容放到该 `JSONResponse` 中。 **FastAPI** 会自动使用默认的状态码或者使用你在 *路径操作* 中设置的状态码。 -## 额外的状态码 +## 额外的状态码 { #additional-status-codes_1 } 如果你想要返回主要状态码之外的状态码,你可以通过直接返回一个 `Response` 来实现,比如 `JSONResponse`,然后直接设置额外的状态码。 @@ -12,15 +12,15 @@ 但是你也希望它能够接受新的条目。并且当这些条目不存在时,会自动创建并返回 201 「创建」的 HTTP 状态码。 -要实现它,导入 `JSONResponse`,然后在其中直接返回你的内容,并将 `status_code` 设置为为你要的值。 +要实现它,导入 `JSONResponse`,然后在其中直接返回你的内容,并将 `status_code` 设置为你要的值。 -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} -/// warning | 警告 +/// warning 当你直接返回一个像上面例子中的 `Response` 对象时,它会直接返回。 -FastAPI 不会用模型等对该响应进行序列化。 +它不会用模型等进行序列化。 确保其中有你想要的数据,且返回的值为合法的 JSON(如果你使用 `JSONResponse` 的话)。 @@ -34,7 +34,7 @@ FastAPI 不会用模型等对该响应进行序列化。 /// -## OpenAPI 和 API 文档 +## OpenAPI 和 API 文档 { #openapi-and-api-docs } 如果你直接返回额外的状态码和响应,它们不会包含在 OpenAPI 方案(API 文档)中,因为 FastAPI 没办法预先知道你要返回什么。 diff --git a/docs/zh/docs/advanced/advanced-dependencies.md b/docs/zh/docs/advanced/advanced-dependencies.md index 8375bd48e..3efca8944 100644 --- a/docs/zh/docs/advanced/advanced-dependencies.md +++ b/docs/zh/docs/advanced/advanced-dependencies.md @@ -1,65 +1,163 @@ -# 高级依赖项 +# 高级依赖项 { #advanced-dependencies } -## 参数化的依赖项 +## 参数化的依赖项 { #parameterized-dependencies } -我们之前看到的所有依赖项都是写死的函数或类。 +目前我们看到的依赖项都是固定的函数或类。 -但也可以为依赖项设置参数,避免声明多个不同的函数或类。 +但有时你可能希望为依赖项设置参数,而不必声明许多不同的函数或类。 -假设要创建校验查询参数 `q` 是否包含固定内容的依赖项。 +假设我们要有一个依赖项,用来检查查询参数 `q` 是否包含某个固定内容。 -但此处要把待检验的固定内容定义为参数。 +但我们希望能够把这个固定内容参数化。 -## **可调用**实例 +## “可调用”的实例 { #a-callable-instance } -Python 可以把类实例变为**可调用项**。 +在 Python 中,可以让某个类的实例变成“可调用对象”(callable)。 -这里说的不是类本身(类本就是可调用项),而是类实例。 +这里指的是类的实例(类本身已经是可调用的),而不是类本身。 -为此,需要声明 `__call__` 方法: +为此,声明一个 `__call__` 方法: -{* ../../docs_src/dependencies/tutorial011.py hl[10] *} +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *} -本例中,**FastAPI** 使用 `__call__` 检查附加参数及子依赖项,稍后,还要调用它向*路径操作函数*传递值。 +在这种情况下,**FastAPI** 会使用这个 `__call__` 来检查附加参数和子依赖,并且稍后会调用它,把返回值传递给你的*路径操作函数*中的参数。 -## 参数化实例 +## 参数化实例 { #parameterize-the-instance } -接下来,使用 `__init__` 声明用于**参数化**依赖项的实例参数: +现在,我们可以用 `__init__` 声明实例的参数,用来“参数化”这个依赖项: -{* ../../docs_src/dependencies/tutorial011.py hl[7] *} +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[9] *} -本例中,**FastAPI** 不使用 `__init__`,我们要直接在代码中使用。 +在本例中,**FastAPI** 不会接触或关心 `__init__`,我们会在自己的代码中直接使用它。 -## 创建实例 +## 创建实例 { #create-an-instance } -使用以下代码创建类实例: +我们可以这样创建该类的实例: -{* ../../docs_src/dependencies/tutorial011.py hl[16] *} +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[18] *} -这样就可以**参数化**依赖项,它包含 `checker.fixed_content` 的属性 - `"bar"`。 +这样就把依赖项“参数化”了,现在它内部带有属性 `checker.fixed_content` 的值 `"bar"`。 -## 把实例作为依赖项 +## 把实例作为依赖项 { #use-the-instance-as-a-dependency } -然后,不要再在 `Depends(checker)` 中使用 `Depends(FixedContentQueryChecker)`, 而是要使用 `checker`,因为依赖项是类实例 - `checker`,不是类。 +然后,我们可以在 `Depends(checker)` 中使用这个 `checker`,而不是 `Depends(FixedContentQueryChecker)`,因为依赖项是实例 `checker`,不是类本身。 -处理依赖项时,**FastAPI** 以如下方式调用 `checker`: +解析依赖项时,**FastAPI** 会像这样调用 `checker`: ```Python checker(q="somequery") ``` -……并用*路径操作函数*的参数 `fixed_content_included` 返回依赖项的值: +...并将其返回值作为依赖项的值,传给我们的*路径操作函数*中的参数 `fixed_content_included`: -{* ../../docs_src/dependencies/tutorial011.py hl[20] *} +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} /// tip | 提示 -本章示例有些刻意,也看不出有什么用处。 +这些看起来可能有些牵强,目前它的用处也许还不太明显。 -这个简例只是为了说明高级依赖项的运作机制。 +这些示例刻意保持简单,但展示了整体的工作方式。 -在有关安全的章节中,工具函数将以这种方式实现。 +在安全相关的章节里,有一些工具函数就是以相同的方式实现的。 -只要能理解本章内容,就能理解安全工具背后的运行机制。 +如果你理解了这里的内容,你就已经知道那些安全工具在底层是如何工作的。 /// + +## 带 `yield` 的依赖项、`HTTPException`、`except` 与后台任务 { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | 警告 + +你很可能不需要了解这些技术细节。 + +这些细节主要在你的 FastAPI 应用版本低于 0.121.0 且你正遇到带 `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` 之后的退出代码会在响应发送之后执行。 + +你可以在文档 [带 `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` 之后的退出代码。 + +这样做的目的是避免在等待响应通过网络传输期间不必要地占用资源。 + +这也意味着,如果你返回的是 `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` 依赖项中提前退出的用例,请创建一个 GitHub 讨论问题,说明你的具体用例以及为何提前关闭会对你有帮助。 + +如果确有有力的用例需要提前关闭,我会考虑新增一种选择性启用提前关闭的方式。 + +### 带 `yield` 的依赖项与 `except`(技术细节) { #dependencies-with-yield-and-except-technical-details } + +在 FastAPI 0.110.0 之前,如果你在带 `yield` 的依赖项中用 `except` 捕获了一个异常,并且没有再次抛出它,那么该异常会被自动抛出/转发给任意异常处理器或内部服务器错误处理器。 + +在 0.110.0 中对此作出了变更,以修复将异常转发为未处理(内部服务器错误)时造成的内存消耗问题,并使其与常规 Python 代码的行为保持一致。 + +### 后台任务与带 `yield` 的依赖项(技术细节) { #background-tasks-and-dependencies-with-yield-technical-details } + +在 FastAPI 0.106.0 之前,`yield` 之后抛出异常是不可行的,因为带 `yield` 的依赖项中的退出代码会在响应发送之后才执行,此时[异常处理器](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}已经运行完毕。 + +之所以这样设计,主要是为了允许在后台任务中继续使用依赖项通过 `yield`“产出”的对象,因为退出代码会在后台任务完成之后才执行。 + +在 FastAPI 0.106.0 中,这一行为被修改,目的是避免在等待响应通过网络传输时一直占用资源。 + +/// tip | 提示 + +另外,后台任务通常是一段独立的逻辑,应该单独处理,并使用它自己的资源(例如它自己的数据库连接)。 + +因此,这样做你的代码通常会更清晰。 + +/// + +如果你过去依赖于旧行为,现在应在后台任务内部自行创建所需资源,并且只在内部使用不依赖于带 `yield` 依赖项资源的数据。 + +例如,不要复用相同的数据库会话,而是在后台任务内部创建一个新的会话,并用这个新会话从数据库获取对象。然后,不是把数据库对象本身作为参数传给后台任务函数,而是传递该对象的 ID,并在后台任务函数内部再次获取该对象。 diff --git a/docs/zh/docs/advanced/async-tests.md b/docs/zh/docs/advanced/async-tests.md index b5ac15b5b..6803358d2 100644 --- a/docs/zh/docs/advanced/async-tests.md +++ b/docs/zh/docs/advanced/async-tests.md @@ -1,4 +1,4 @@ -# 异步测试 +# 异步测试 { #async-tests } 您已经了解了如何使用 `TestClient` 测试 **FastAPI** 应用程序。但是到目前为止,您只了解了如何编写同步测试,而没有使用 `async` 异步函数。 @@ -6,11 +6,11 @@ 让我们看看如何才能实现这一点。 -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } 如果我们想在测试中调用异步函数,那么我们的测试函数必须是异步的。 AnyIO 为此提供了一个简洁的插件,它允许我们指定一些测试函数要异步调用。 -## HTTPX +## HTTPX { #httpx } 即使您的 **FastAPI** 应用程序使用普通的 `def` 函数而不是 `async def` ,它本质上仍是一个 `async` 异步应用程序。 @@ -18,7 +18,7 @@ `TestClient` 是基于 HTTPX 的。幸运的是,我们可以直接使用它来测试API。 -## 示例 +## 示例 { #example } 举个简单的例子,让我们来看一个[更大的应用](../tutorial/bigger-applications.md){.internal-link target=_blank}和[测试](../tutorial/testing.md){.internal-link target=_blank}中描述的类似文件结构: @@ -32,13 +32,13 @@ 文件 `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 } 您可以通过以下方式照常运行测试: @@ -52,13 +52,13 @@ $ pytest -## 详细说明 +## 详细说明 { #in-detail } 这个标记 `@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 +/// tip | 提示 请注意,测试函数现在用的是 `async def`,而不是像以前使用 `TestClient` 时那样只是 `def` 。 @@ -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] *} 这相当于: @@ -76,24 +76,24 @@ response = client.get('/') 我们曾经通过它向 `TestClient` 发出请求。 -/// tip +/// tip | 提示 请注意,我们正在将 async/await 与新的 `AsyncClient` 一起使用——请求是异步的。 /// -/// warning +/// warning | 警告 如果您的应用程序依赖于生命周期事件, `AsyncClient` 将不会触发这些事件。为了确保它们被触发,请使用 florimondmanca/asgi-lifespan 中的 `LifespanManager` 。 /// -## 其他异步函数调用 +## 其他异步函数调用 { #other-asynchronous-function-calls } 由于测试函数现在是异步的,因此除了在测试中向 FastAPI 应用程序发送请求之外,您现在还可以调用(和使用 `await` 等待)其他 `async` 异步函数,就和您在代码中的其他任何地方调用它们的方法一样。 -/// tip +/// tip | 提示 -如果您在测试程序中集成异步函数调用的时候遇到一个 `RuntimeError: Task attached to a different loop` 的报错(例如,使用 MongoDB 的 MotorClient 时),请记住,只能在异步函数中实例化需要事件循环的对象,例如通过 `'@app.on_event("startup")` 回调函数进行初始化。 +如果您在测试程序中集成异步函数调用的时候遇到一个 `RuntimeError: Task attached to a different loop` 的报错(例如,使用 MongoDB 的 MotorClient 时),请记住,只能在异步函数中实例化需要事件循环的对象,例如在 `@app.on_event("startup")` 回调中初始化。 /// diff --git a/docs/zh/docs/advanced/behind-a-proxy.md b/docs/zh/docs/advanced/behind-a-proxy.md index f8f61c8a3..b44b0b5ac 100644 --- a/docs/zh/docs/advanced/behind-a-proxy.md +++ b/docs/zh/docs/advanced/behind-a-proxy.md @@ -1,30 +1,131 @@ -# 使用代理 +# 使用代理 { #behind-a-proxy } -有些情况下,您可能要使用 Traefik 或 Nginx 等**代理**服务器,并添加应用不能识别的附加路径前缀配置。 +在很多情况下,你会在 FastAPI 应用前面使用像 Traefik 或 Nginx 这样的**代理**。 -此时,要使用 `root_path` 配置应用。 +这些代理可以处理 HTTPS 证书等事项。 -`root_path` 是 ASGI 规范提供的机制,FastAPI 就是基于此规范开发的(通过 Starlette)。 +## 代理转发的请求头 { #proxy-forwarded-headers } + +在你的应用前面的**代理**通常会在把请求转发给你的**服务器**之前,临时设置一些请求头,让服务器知道该请求是由代理**转发**的,并告知原始(公网)URL,包括域名、是否使用 HTTPS 等。 + +**服务器**程序(例如通过 **FastAPI CLI** 运行的 **Uvicorn**)能够解析这些请求头,然后把这些信息传递给你的应用。 + +但出于安全考虑,由于服务器并不知道自己处在受信任的代理之后,它默认不会解析这些请求头。 + +/// note | 技术细节 + +这些代理相关的请求头包括: + +- X-Forwarded-For +- X-Forwarded-Proto +- X-Forwarded-Host + +/// + +### 启用代理转发的请求头 { #enable-proxy-forwarded-headers } + +你可以用 *CLI 选项* `--forwarded-allow-ips` 启动 FastAPI CLI,并传入应该被信任、允许读取这些转发请求头的 IP 地址列表。 + +如果设置为 `--forwarded-allow-ips="*"`,就会信任所有来源 IP。 + +如果你的**服务器**位于受信任的**代理**之后,并且只有代理会与它通信,这将使其接受该**代理**的任何 IP。 + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### 使用 HTTPS 的重定向 { #redirects-with-https } + +例如,假设你定义了一个*路径操作* `/items/`: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +如果客户端尝试访问 `/items`,默认会被重定向到 `/items/`。 + +但在设置 *CLI 选项* `--forwarded-allow-ips` 之前,它可能会重定向到 `http://localhost:8000/items/`。 + +而你的应用可能托管在 `https://mysuperapp.com`,重定向应当是 `https://mysuperapp.com/items/`。 + +通过设置 `--proxy-headers`,FastAPI 现在就可以重定向到正确的位置。😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | 提示 + +如果你想了解更多关于 HTTPS 的内容,查看指南:[关于 HTTPS](../deployment/https.md){.internal-link target=_blank}。 + +/// + +### 代理转发请求头如何工作 { #how-proxy-forwarded-headers-work } + +下面是一个可视化图示,展示了**代理**如何在客户端与**应用服务器**之间添加转发请求头: + +```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 +``` + +**代理**会拦截原始客户端请求,并在将请求传递给**应用服务器**之前,添加特殊的*转发*请求头(`X-Forwarded-*`)。 + +这些请求头保留了原始请求中否则会丢失的信息: + +- X-Forwarded-For:原始客户端的 IP 地址 +- X-Forwarded-Proto:原始协议(`https`) +- X-Forwarded-Host:原始主机(`mysuperapp.com`) + +当 **FastAPI CLI** 配置了 `--forwarded-allow-ips` 后,它会信任并使用这些请求头,例如用于在重定向中生成正确的 URL。 + +## 移除路径前缀的代理 { #proxy-with-a-stripped-path-prefix } + +你可能会有一个代理,为你的应用添加一个路径前缀。 + +在这些情况下,你可以使用 `root_path` 来配置你的应用。 + +`root_path` 是 ASGI 规范(FastAPI 基于该规范,通过 Starlette 构建)提供的机制。 `root_path` 用于处理这些特定情况。 -在挂载子应用时,也可以在内部使用。 +在挂载子应用时,它也会在内部使用。 -## 移除路径前缀的代理 +“移除路径前缀的代理”在这里的意思是:你可以在代码中声明一个路径 `/app`,然后在顶层添加一层(代理),把你的 **FastAPI** 应用放在类似 `/api/v1` 的路径下。 -本例中,移除路径前缀的代理是指在代码中声明路径 `/app`,然后在应用顶层添加代理,把 **FastAPI** 应用放在 `/api/v1` 路径下。 +在这种情况下,原始路径 `/app` 实际上会在 `/api/v1/app` 提供服务。 -本例的原始路径 `/app` 实际上是在 `/api/v1/app` 提供服务。 +即使你的所有代码都假设只有 `/app`。 -哪怕所有代码都假设只有 `/app`。 +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} -代理只在把请求传送给 Uvicorn 之前才会**移除路径前缀**,让应用以为它是在 `/app` 提供服务,因此不必在代码中加入前缀 `/api/v1`。 +代理会在将请求传递给应用服务器(可能是通过 FastAPI CLI 运行的 Uvicorn)之前,实时**“移除”**这个**路径前缀**,让你的应用认为它是在 `/app` 被服务,这样你就不需要更新所有代码去包含 `/api/v1` 前缀。 -但之后,在(前端)打开 API 文档时,代理会要求在 `/openapi.json`,而不是 `/api/v1/openapi.json` 中提取 OpenAPI 概图。 +到这里,一切都会像往常一样工作。 -因此, (运行在浏览器中的)前端会尝试访问 `/openapi.json`,但没有办法获取 OpenAPI 概图。 +但是,当你打开集成的文档界面(前端)时,它会期望在 `/openapi.json` 获取 OpenAPI 模式,而不是在 `/api/v1/openapi.json`。 -这是因为应用使用了以 `/api/v1` 为路径前缀的代理,前端要从 `/api/v1/openapi.json` 中提取 OpenAPI 概图。 +因此,(在浏览器中运行的)前端会尝试访问 `/openapi.json`,但无法获取 OpenAPI 模式。 + +因为我们的应用使用了路径前缀为 `/api/v1` 的代理,前端需要从 `/api/v1/openapi.json` 获取 OpenAPI 模式。 ```mermaid graph LR @@ -39,15 +140,15 @@ proxy --> server /// tip | 提示 -IP `0.0.0.0` 常用于指程序监听本机或服务器上的所有有效 IP。 +IP `0.0.0.0` 通常表示程序监听该机器/服务器上的所有可用 IP。 /// -API 文档还需要 OpenAPI 概图声明 API `server` 位于 `/api/v1`(使用代理时的 URL)。例如: +文档界面还需要 OpenAPI 模式声明该 API 的 `server` 位于 `/api/v1`(代理后面)。例如: ```JSON hl_lines="4-8" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { @@ -60,53 +161,53 @@ API 文档还需要 OpenAPI 概图声明 API `server` 位于 `/api/v1`(使用 } ``` -本例中的 `Proxy` 是 **Traefik**,`server` 是运行 FastAPI 应用的 **Uvicorn**。 +在此示例中,“Proxy” 可以是 **Traefik** 之类的。服务器可以是用 **Uvicorn** 的 **FastAPI CLI** 运行你的 FastAPI 应用。 -### 提供 `root_path` +### 提供 `root_path` { #providing-the-root-path } -为此,要以如下方式使用命令行选项 `--root-path`: +为此,你可以像下面这样使用命令行选项 `--root-path`:
```console -$ uvicorn main:app --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) ```
-Hypercorn 也支持 `--root-path `选项。 +如果你使用 Hypercorn,它也有 `--root-path` 选项。 /// note | 技术细节 -ASGI 规范定义的 `root_path` 就是为了这种用例。 +ASGI 规范为这种用例定义了 `root_path`。 -并且 `--root-path` 命令行选项支持 `root_path`。 +命令行选项 `--root-path` 会提供该 `root_path`。 /// -### 查看当前的 `root_path` +### 查看当前的 `root_path` { #checking-the-current-root-path } -获取应用为每个请求使用的当前 `root_path`,这是 `scope` 字典的内容(也是 ASGI 规范的内容)。 +你可以获取应用在每个请求中使用的当前 `root_path`,它是 `scope` 字典的一部分(ASGI 规范的一部分)。 -我们在这里的信息里包含 `roo_path` 只是为了演示。 +这里我们把它包含在响应消息中仅用于演示。 -{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *} +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} -然后,用以下命令启动 Uvicorn: +然后,如果你这样启动 Uvicorn:
```console -$ uvicorn main:app --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) ```
-返回的响应如下: +响应类似于: ```JSON { @@ -115,19 +216,19 @@ $ uvicorn main:app --root-path /api/v1 } ``` -### 在 FastAPI 应用里设置 `root_path` +### 在 FastAPI 应用中设置 `root_path` { #setting-the-root-path-in-the-fastapi-app } -还有一种方案,如果不能提供 `--root-path` 或等效的命令行选项,则在创建 FastAPI 应用时要设置 `root_path` 参数。 +或者,如果你无法提供类似 `--root-path` 的命令行选项,你可以在创建 FastAPI 应用时设置参数 `root_path`: -{* ../../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 一样。 +把 `root_path` 传给 `FastAPI` 等同于把命令行选项 `--root-path` 传给 Uvicorn 或 Hypercorn。 -### 关于 `root_path` +### 关于 `root_path` { #about-root-path } -注意,服务器(Uvicorn)只是把 `root_path` 传递给应用。 +请注意,服务器(Uvicorn)不会用这个 `root_path` 做别的事情,只会把它传给应用。 -在浏览器中输入 http://127.0.0.1:8000/app 时能看到标准响应: +但是,如果你用浏览器打开 http://127.0.0.1:8000/app,你会看到正常的响应: ```JSON { @@ -136,25 +237,25 @@ $ uvicorn main:app --root-path /api/v1 } ``` -它不要求访问 `http://127.0.0.1:800/api/v1/app`。 +因此,它不会期望被访问于 `http://127.0.0.1:8000/api/v1/app`。 -Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶部添加 `/api/v1` 前缀是代理要做的事情。 +Uvicorn 会期望代理以 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶部额外添加 `/api/v1` 前缀是代理的职责。 -## 关于移除路径前缀的代理 +## 关于移除路径前缀的代理 { #about-proxies-with-a-stripped-path-prefix } -注意,移除路径前缀的代理只是配置代理的方式之一。 +请记住,移除路径前缀只是配置代理的一种方式。 -大部分情况下,代理默认都不会移除路径前缀。 +在很多情况下,默认是代理不会移除路径前缀。 -(未移除路径前缀时)代理监听 `https://myawesomeapp.com` 等对象,如果浏览器跳转到 `https://myawesomeapp.com/api/v1/app`,且服务器(例如 Uvicorn)监听 `http://127.0.0.1:8000` 代理(未移除路径前缀) 会在同样的路径:`http://127.0.0.1:8000/api/v1/app` 访问 Uvicorn。 +在这种情况下(没有移除路径前缀),代理会监听类似 `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 进行本地测试 { #testing-locally-with-traefik } -您可以轻易地在本地使用 Traefik 运行移除路径前缀的试验。 +你可以很容易地使用 Traefik 在本地运行一个移除路径前缀的实验。 -下载 Traefik,这是一个二进制文件,需要解压文件,并在 Terminal 中直接运行。 +下载 Traefik,它是一个单独的二进制文件,你可以解压压缩包并直接在终端中运行。 -然后创建包含如下内容的 `traefik.toml` 文件: +然后创建一个 `traefik.toml` 文件,内容如下: ```TOML hl_lines="3" [entryPoints] @@ -166,15 +267,15 @@ Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶 filename = "routes.toml" ``` -这个文件把 Traefik 监听端口设置为 `9999`,并设置要使用另一个文件 `routes.toml`。 +这告诉 Traefik 监听端口 9999,并使用另一个文件 `routes.toml`。 /// tip | 提示 -使用端口 9999 代替标准的 HTTP 端口 80,这样就不必使用管理员权限运行(`sudo`)。 +我们使用 9999 端口而不是标准 HTTP 端口 80,这样你就不需要用管理员(`sudo`)权限运行。 /// -接下来,创建 `routes.toml`: +现在创建另一个文件 `routes.toml`: ```TOML hl_lines="5 12 20" [http] @@ -201,9 +302,9 @@ Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶 这个文件配置 Traefik 使用路径前缀 `/api/v1`。 -然后,它把请求重定位到运行在 `http://127.0.0.1:8000` 上的 Uvicorn。 +随后 Traefik 会把请求转发到运行在 `http://127.0.0.1:8000` 的 Uvicorn。 -现在,启动 Traefik: +现在启动 Traefik:
@@ -215,21 +316,21 @@ INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
-接下来,使用 Uvicorn 启动应用,并使用 `--root-path` 选项: +然后使用 `--root-path` 选项启动你的应用:
```console -$ uvicorn main:app --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) ```
-### 查看响应 +### 查看响应 { #check-the-responses } -访问含 Uvicorn 端口的 URL:http://127.0.0.1:8000/app,就能看到标准响应: +现在,如果你访问 Uvicorn 端口对应的 URL:http://127.0.0.1:8000/app,你会看到正常响应: ```JSON { @@ -240,13 +341,13 @@ $ uvicorn main:app --root-path /api/v1 /// tip | 提示 -注意,就算访问 `http://127.0.0.1:8000/app`,也显示从选项 `--root-path` 中提取的 `/api/v1`,这是 `root_path` 的值。 +注意,尽管你是通过 `http://127.0.0.1:8000/app` 访问,它仍显示 `root_path` 为 `/api/v1`,该值来自 `--root-path` 选项。 /// -打开含 Traefik 端口的 URL,包含路径前缀:http://127.0.0.1:9999/api/v1/app。 +现在打开包含路径前缀、使用 Traefik 端口的 URL:http://127.0.0.1:9999/api/v1/app。 -得到同样的响应: +我们得到相同的响应: ```JSON { @@ -255,57 +356,57 @@ $ uvicorn main:app --root-path /api/v1 } ``` -但这一次 URL 包含了代理提供的路径前缀:`/api/v1`。 +但这次 URL 中带有代理提供的前缀路径:`/api/v1`。 -当然,这是通过代理访问应用的方式,因此,路径前缀 `/app/v1` 版本才是**正确**的。 +当然,这里的想法是每个人都通过代理访问应用,因此带有路径前缀 `/api/v1` 的版本才是“正确”的。 -而不带路径前缀的版本(`http://127.0.0.1:8000/app`),则由 Uvicorn 直接提供,专供*代理*(Traefik)访问。 +而不带路径前缀的版本(`http://127.0.0.1:8000/app`)由 Uvicorn 直接提供,仅供_代理_(Traefik)访问。 -这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用选项 `--root-path` 中的 `root_path`。 +这说明了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用 `--root-path` 选项提供的 `root_path`。 -### 查看文档 +### 查看文档界面 { #check-the-docs-ui } -但这才是有趣的地方 ✨ +有趣的部分来了。✨ -访问应用的**官方**方式是通过含路径前缀的代理。因此,不出所料,如果没有在 URL 中添加路径前缀,直接访问通过 Uvicorn 运行的 API 文档,不能正常访问,因为需要通过代理才能访问。 +访问应用的“官方”方式应该是通过我们定义的带有路径前缀的代理。因此,正如预期的那样,如果你尝试不带路径前缀、直接由 Uvicorn 提供的文档界面,它将无法工作,因为它期望通过代理访问。 -输入 http://127.0.0.1:8000/docs 查看 API 文档: +你可以在 http://127.0.0.1:8000/docs 查看: -但输入**官方**链接 `/api/v1/docs`,并使用端口 `9999` 访问 API 文档,就能正常运行了!🎉 +但如果我们在“官方”URL(代理端口为 `9999`)的 `/api/v1/docs` 访问文档界面,它就能正常工作!🎉 -输入 http://127.0.0.1:9999/api/v1/docs 查看文档: +你可以在 http://127.0.0.1:9999/api/v1/docs 查看: -一切正常。 ✔️ +完全符合我们的预期。✔️ -这是因为 FastAPI 在 OpenAPI 里使用 `root_path` 提供的 URL 创建默认 `server`。 +这是因为 FastAPI 使用该 `root_path` 在 OpenAPI 中创建默认的 `server`,其 URL 来自 `root_path`。 -## 附加的服务器 +## 附加的服务器 { #additional-servers } /// warning | 警告 -此用例较难,可以跳过。 +这是一个更高级的用例,可以跳过。 /// -默认情况下,**FastAPI** 使用 `root_path` 的链接在 OpenAPI 概图中创建 `server`。 +默认情况下,**FastAPI** 会在 OpenAPI 模式中使用 `root_path` 的 URL 创建一个 `server`。 -但也可以使用其它备选 `servers`,例如,需要同一个 API 文档与 staging 和生产环境交互。 +但你也可以提供其他备选的 `servers`,例如你希望让“同一个”文档界面同时与预发布环境和生产环境交互。 -如果传递自定义 `servers` 列表,并有 `root_path`( 因为 API 使用了代理),**FastAPI** 会在列表开头使用这个 `root_path` 插入**服务器**。 +如果你传入了自定义的 `servers` 列表,并且存在 `root_path`(因为你的 API 位于代理后面),**FastAPI** 会在列表开头插入一个使用该 `root_path` 的“server”。 例如: -{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *} +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} -这段代码生产如下 OpenAPI 概图: +会生成如下的 OpenAPI 模式: ```JSON hl_lines="5-7" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { @@ -328,30 +429,38 @@ $ uvicorn main:app --root-path /api/v1 /// tip | 提示 -注意,自动生成服务器时,`url` 的值 `/api/v1` 提取自 `roog_path`。 +注意这个自动生成的服务器,`url` 的值为 `/api/v1`,取自 `root_path`。 /// -http://127.0.0.1:9999/api/v1/docs 的 API 文档所示如下: +在 http://127.0.0.1:9999/api/v1/docs 的文档界面中,它看起来是这样的: /// tip | 提示 -API 文档与所选的服务器进行交互。 +文档界面会与你所选择的服务器交互。 /// -### 从 `root_path` 禁用自动服务器 +/// note | 技术细节 -如果不想让 **FastAPI** 包含使用 `root_path` 的自动服务器,则要使用参数 `root_path_in_servers=False`: +OpenAPI 规范中的 `servers` 属性是可选的。 -{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *} +如果你没有指定 `servers` 参数,并且 `root_path` 等于 `/`,则默认情况下,生成的 OpenAPI 模式中会完全省略 `servers` 属性,这等价于只有一个 `url` 值为 `/` 的服务器。 -这样,就不会在 OpenAPI 概图中包含服务器了。 +/// -## 挂载子应用 +### 从 `root_path` 禁用自动服务器 { #disable-automatic-server-from-root-path } -如需挂载子应用(详见 [子应用 - 挂载](sub-applications.md){.internal-link target=_blank}),也要通过 `root_path` 使用代理,这与正常应用一样,别无二致。 +如果你不希望 **FastAPI** 包含一个使用 `root_path` 的自动服务器,可以使用参数 `root_path_in_servers=False`: -FastAPI 在内部使用 `root_path`,因此子应用也可以正常运行。✨ +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} + +这样它就不会被包含到 OpenAPI 模式中。 + +## 挂载子应用 { #mounting-a-sub-application } + +如果你需要在使用带有 `root_path` 的代理时挂载一个子应用(参见 [子应用 - 挂载](sub-applications.md){.internal-link target=_blank}),你可以像预期的那样正常操作。 + +FastAPI 会在内部智能地使用 `root_path`,因此它可以直接正常工作。✨ diff --git a/docs/zh/docs/advanced/custom-response.md b/docs/zh/docs/advanced/custom-response.md index 22a9b4b51..f5bec5fdc 100644 --- a/docs/zh/docs/advanced/custom-response.md +++ b/docs/zh/docs/advanced/custom-response.md @@ -1,32 +1,38 @@ -# 自定义响应 - HTML,流,文件和其他 +# 自定义响应 - HTML、流、文件等 { #custom-response-html-stream-file-others } **FastAPI** 默认会使用 `JSONResponse` 返回响应。 你可以通过直接返回 `Response` 来重载它,参见 [直接返回响应](response-directly.md){.internal-link target=_blank}。 -但如果你直接返回 `Response`,返回数据不会自动转换,也不会自动生成文档(例如,在 HTTP 头 `Content-Type` 中包含特定的「媒体类型」作为生成的 OpenAPI 的一部分)。 +但如果你直接返回一个 `Response`(或其任意子类,比如 `JSONResponse`),返回数据不会自动转换(即使你声明了 `response_model`),也不会自动生成文档(例如,在生成的 OpenAPI 中,HTTP 头 `Content-Type` 里的特定「媒体类型」不会被包含)。 -你还可以在 *路径操作装饰器* 中声明你想用的 `Response`。 +你还可以在 *路径操作装饰器* 中通过 `response_class` 参数声明要使用的 `Response`(例如任意 `Response` 子类)。 你从 *路径操作函数* 中返回的内容将被放在该 `Response` 中。 -并且如果该 `Response` 有一个 JSON 媒体类型(`application/json`),比如使用 `JSONResponse` 或者 `UJSONResponse` 的时候,返回的数据将使用你在路径操作装饰器中声明的任何 Pydantic 的 `response_model` 自动转换(和过滤)。 +并且如果该 `Response` 有一个 JSON 媒体类型(`application/json`),比如使用 `JSONResponse` 或 `UJSONResponse` 的时候,返回的数据将使用你在路径操作装饰器中声明的任何 Pydantic 的 `response_model` 自动转换(和过滤)。 -/// note | 说明 +/// note | 注意 -如果你使用不带有任何媒体类型的响应类,FastAPI 认为你的响应没有任何内容,所以不会在生成的OpenAPI文档中记录响应格式。 +如果你使用不带有任何媒体类型的响应类,FastAPI 会认为你的响应没有任何内容,所以不会在生成的 OpenAPI 文档中记录响应格式。 /// -## 使用 `ORJSONResponse` +## 使用 `ORJSONResponse` { #use-orjsonresponse } 例如,如果你需要压榨性能,你可以安装并使用 `orjson` 并将响应设置为 `ORJSONResponse`。 导入你想要使用的 `Response` 类(子类)然后在 *路径操作装饰器* 中声明它。 -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +对于较大的响应,直接返回一个 `Response` 会比返回一个字典快得多。 -/// info | 提示 +这是因为默认情况下,FastAPI 会检查其中的每一项并确保它可以被序列化为 JSON,使用教程中解释的相同 [JSON 兼容编码器](../tutorial/encoder.md){.internal-link target=_blank}。这正是它允许你返回「任意对象」的原因,例如数据库模型。 + +但如果你确定你返回的内容是「可以用 JSON 序列化」的,你可以将它直接传给响应类,从而避免在传给响应类之前先通过 `jsonable_encoder` 带来的额外开销。 + +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} + +/// info | 信息 参数 `response_class` 也会用来定义响应的「媒体类型」。 @@ -36,22 +42,22 @@ /// -/// tip | 小贴士 +/// tip | 提示 `ORJSONResponse` 目前只在 FastAPI 中可用,而在 Starlette 中不可用。 /// -## HTML 响应 +## HTML 响应 { #html-response } 使用 `HTMLResponse` 来从 **FastAPI** 中直接返回一个 HTML 响应。 * 导入 `HTMLResponse`。 * 将 `HTMLResponse` 作为你的 *路径操作* 的 `response_class` 参数传入。 -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} -/// info | 提示 +/// info | 信息 参数 `response_class` 也会用来定义响应的「媒体类型」。 @@ -61,13 +67,13 @@ /// -### 返回一个 `Response` +### 返回一个 `Response` { #return-a-response } 正如你在 [直接返回响应](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 | 警告 @@ -75,33 +81,33 @@ /// -/// info | 提示 +/// info | 信息 -当然,实际的 `Content-Type` 头,状态码等等,将来自于你返回的 `Response` 对象。 +当然,实际的 `Content-Type` 头、状态码等等,将来自于你返回的 `Response` 对象。 /// -### OpenAPI 中的文档和重载 `Response` +### 在 OpenAPI 中文档化并重载 `Response` { #document-in-openapi-and-override-response } 如果你想要在函数内重载响应,但是同时在 OpenAPI 中文档化「媒体类型」,你可以使用 `response_class` 参数并返回一个 `Response` 对象。 接着 `response_class` 参数只会被用来文档化 OpenAPI 的 *路径操作*,你的 `Response` 用来返回响应。 -### 直接返回 `HTMLResponse` +#### 直接返回 `HTMLResponse` { #return-an-htmlresponse-directly } 比如像这样: -{* ../../docs_src/custom_response/tutorial004.py hl[7,23,21] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} 在这个例子中,函数 `generate_html_response()` 已经生成并返回 `Response` 对象而不是在 `str` 中返回 HTML。 -通过返回函数 `generate_html_response()` 的调用结果,你已经返回一个重载 **FastAPI** 默认行为的 `Response` 对象, +通过返回函数 `generate_html_response()` 的调用结果,你已经返回一个重载 **FastAPI** 默认行为的 `Response` 对象。 -但如果你在 `response_class` 中也传入了 `HTMLResponse`,**FastAPI** 会知道如何在 OpenAPI 和交互式文档中使用 `text/html` 将其文档化为 HTML。 +但如果你在 `response_class` 中也传入了 `HTMLResponse`,**FastAPI** 会知道如何在 OpenAPI 和交互式文档中使用 `text/html` 将其文档化为 HTML: -## 可用响应 +## 可用响应 { #available-responses } 这里有一些可用的响应。 @@ -115,7 +121,7 @@ /// -### `Response` +### `Response` { #response } 其他全部的响应都继承自主类 `Response`。 @@ -128,77 +134,115 @@ * `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_py39.py hl[1,18] *} -{* ../../docs_src/response_directly/tutorial002.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 } 如上文所述,`ORJSONResponse` 是一个使用 `orjson` 的快速的可选 JSON 响应。 +/// info | 信息 -### `UJSONResponse` +这需要先安装 `orjson`,例如使用 `pip install orjson`。 + +/// + +### `UJSONResponse` { #ujsonresponse } `UJSONResponse` 是一个使用 `ujson` 的可选 JSON 响应。 +/// info | 信息 + +这需要先安装 `ujson`,例如使用 `pip install ujson`。 + +/// + /// warning | 警告 在处理某些边缘情况时,`ujson` 不如 Python 的内置实现那么谨慎。 /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} -/// tip | 小贴士 +/// tip | 提示 `ORJSONResponse` 可能是一个更快的选择。 /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } -返回 HTTP 重定向。默认情况下使用 307 状态代码(临时重定向)。 +返回 HTTP 重定向。默认情况下使用 307 状态码(临时重定向)。 -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +你可以直接返回一个 `RedirectResponse`: -### `StreamingResponse` +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} + +--- + +或者你可以把它用于 `response_class` 参数: + +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} + +如果你这么做,那么你可以在 *路径操作* 函数中直接返回 URL。 + +在这种情况下,将使用 `RedirectResponse` 的默认 `status_code`,即 `307`。 + +--- + +你也可以将 `status_code` 参数和 `response_class` 参数结合使用: + +{* ../../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` +#### 对类似文件的对象使用 `StreamingResponse` { #using-streamingresponse-with-file-like-objects } -如果您有类似文件的对象(例如,由 `open()` 返回的对象),则可以在 `StreamingResponse` 中将其返回。 +如果您有一个类文件对象(例如由 `open()` 返回的对象),你可以创建一个生成器函数来迭代该类文件对象。 -包括许多与云存储,视频处理等交互的库。 +这样,你就不必先把它全部读入内存,可以将该生成器函数传给 `StreamingResponse` 并返回它。 -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +这也包括许多与云存储、视频处理等交互的库。 -/// tip | 小贴士 +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} + +1. 这是生成器函数。之所以是「生成器函数」,是因为它内部包含 `yield` 语句。 +2. 通过使用 `with` 代码块,我们可以确保在生成器函数完成后关闭类文件对象。因此,在它完成发送响应之后会被关闭。 +3. 这个 `yield from` 告诉函数去迭代名为 `file_like` 的那个对象。然后,对于每个被迭代出来的部分,都把该部分作为来自这个生成器函数(`iterfile`)的值再 `yield` 出去。 + + 因此,它是一个把「生成」工作内部转交给其他东西的生成器函数。 + + 通过这种方式,我们可以把它放在 `with` 代码块中,从而确保类文件对象在结束后被关闭。 + +/// tip | 提示 注意在这里,因为我们使用的是不支持 `async` 和 `await` 的标准 `open()`,我们使用普通的 `def` 声明了路径操作。 /// -### `FileResponse` +### `FileResponse` { #fileresponse } 异步传输文件作为响应。 @@ -209,10 +253,60 @@ FastAPI(实际上是 Starlette)将自动包含 Content-Length 的头。它 * `media_type` - 给出媒体类型的字符串。如果未设置,则文件名或路径将用于推断媒体类型。 * `filename` - 如果给出,它将包含在响应的 `Content-Disposition` 中。 -文件响应将包含适当的 `Content-Length`,`Last-Modified` 和 `ETag` 的响应头。 +文件响应将包含适当的 `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` 参数: -您还可以使用 `response` 在 OpenAPI 中声明媒体类型和许多其他详细信息:[OpenAPI 中的额外文档](additional-responses.md){.internal-link target=_blank}。 +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} + +在这种情况下,你可以在 *路径操作* 函数中直接返回文件路径。 + +## 自定义响应类 { #custom-response-class } + +你可以创建你自己的自定义响应类,继承自 `Response` 并使用它。 + +例如,假设你想使用 `orjson`,但要使用内置 `ORJSONResponse` 类没有启用的一些自定义设置。 + +假设你想让它返回带缩进、格式化的 JSON,因此你想使用 orjson 选项 `orjson.OPT_INDENT_2`。 + +你可以创建一个 `CustomORJSONResponse`。你需要做的主要事情是实现一个返回 `bytes` 的 `Response.render(content)` 方法: + +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} + +现在,不再是返回: + +```json +{"message": "Hello World"} +``` + +…这个响应将返回: + +```json +{ + "message": "Hello World" +} +``` + +当然,你很可能会找到比格式化 JSON 更好的方式来利用这一点。😉 + +## 默认响应类 { #default-response-class } + +在创建 **FastAPI** 类实例或 `APIRouter` 时,你可以指定默认要使用的响应类。 + +用于定义它的参数是 `default_response_class`。 + +在下面的示例中,**FastAPI** 会在所有 *路径操作* 中默认使用 `ORJSONResponse`,而不是 `JSONResponse`。 + +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} + +/// tip | 提示 + +你仍然可以像之前一样在 *路径操作* 中重载 `response_class`。 + +/// + +## 额外文档 { #additional-documentation } + +你还可以使用 `responses` 在 OpenAPI 中声明媒体类型和许多其他详细信息:[OpenAPI 中的额外文档](additional-responses.md){.internal-link target=_blank}。 diff --git a/docs/zh/docs/advanced/dataclasses.md b/docs/zh/docs/advanced/dataclasses.md index 4e8e77d2a..d62aef8f3 100644 --- a/docs/zh/docs/advanced/dataclasses.md +++ b/docs/zh/docs/advanced/dataclasses.md @@ -1,97 +1,87 @@ -# 使用数据类 +# 使用数据类 { #using-dataclasses } -FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 模型声明请求与响应。 +FastAPI 基于 **Pydantic** 构建,我已经向你展示过如何使用 Pydantic 模型声明请求与响应。 -但 FastAPI 还可以使用数据类(`dataclasses`): +但 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`。 +这仍然得益于 **Pydantic**,因为它对 `dataclasses` 的内置支持。 -因此,即便上述代码没有显式使用 Pydantic,FastAPI 仍会使用 Pydantic 把标准数据类转换为 Pydantic 数据类(`dataclasses`)。 +因此,即便上面的代码没有显式使用 Pydantic,FastAPI 也会使用 Pydantic 将那些标准数据类转换为 Pydantic 风格的 dataclasses。 并且,它仍然支持以下功能: * 数据验证 * 数据序列化 -* 数据存档等 +* 数据文档等 -数据类的和运作方式与 Pydantic 模型相同。实际上,它的底层使用的也是 Pydantic。 +这与使用 Pydantic 模型时的工作方式相同。而且底层实际上也是借助 Pydantic 实现的。 -/// info | 说明 +/// info | 信息 -注意,数据类不支持 Pydantic 模型的所有功能。 +请注意,数据类不能完成 Pydantic 模型能做的所有事情。 -因此,开发时仍需要使用 Pydantic 模型。 +因此,你可能仍然需要使用 Pydantic 模型。 -但如果数据类很多,这一技巧能给 FastAPI 开发 Web API 增添不少助力。🤓 +但如果你已有一堆数据类,这个技巧可以让它们很好地为使用 FastAPI 的 Web API 所用。🤓 /// -## `response_model` 使用数据类 +## 在 `response_model` 中使用数据类 { #dataclasses-in-response-model } -在 `response_model` 参数中使用 `dataclasses`: +你也可以在 `response_model` 参数中使用 `dataclasses`: -{* ../../docs_src/dataclasses_/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} -本例把数据类自动转换为 Pydantic 数据类。 +该数据类会被自动转换为 Pydantic 的数据类。 -API 文档中也会显示相关概图: +这样,它的模式会显示在 API 文档界面中: -## 在嵌套数据结构中使用数据类 +## 在嵌套数据结构中使用数据类 { #dataclasses-in-nested-data-structures } -您还可以把 `dataclasses` 与其它类型注解组合在一起,创建嵌套数据结构。 +你也可以把 `dataclasses` 与其它类型注解组合在一起,创建嵌套数据结构。 -还有一些情况也可以使用 Pydantic 的 `dataclasses`。例如,在 API 文档中显示错误。 +在某些情况下,你可能仍然需要使用 Pydantic 的 `dataclasses` 版本。例如,如果自动生成的 API 文档出现错误。 -本例把标准的 `dataclasses` 直接替换为 `pydantic.dataclasses`: +在这种情况下,你可以直接把标准的 `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_py310.py hl[1,4,7:10,13:16,22:24,27] *} -1. 本例依然要从标准的 `dataclasses` 中导入 `field`; +1. 我们仍然从标准库的 `dataclasses` 导入 `field`。 +2. `pydantic.dataclasses` 是 `dataclasses` 的可直接替换版本。 +3. `Author` 数据类包含一个由 `Item` 数据类组成的列表。 +4. `Author` 数据类被用作 `response_model` 参数。 +5. 你可以将其它标准类型注解与数据类一起用作请求体。 -2. 使用 `pydantic.dataclasses` 直接替换 `dataclasses`; + 在本例中,它是一个 `Item` 数据类列表。 +6. 这里我们返回一个字典,里面的 `items` 是一个数据类列表。 -3. `Author` 数据类包含 `Item` 数据类列表; + FastAPI 仍然能够将数据序列化为 JSON。 +7. 这里的 `response_model` 使用了 “`Author` 数据类列表” 的类型注解。 -4. `Author` 数据类用于 `response_model` 参数; + 同样,你可以将 `dataclasses` 与标准类型注解组合使用。 +8. 注意,这个 *路径操作函数* 使用的是常规的 `def` 而不是 `async def`。 -5. 其它带有数据类的标准类型注解也可以作为请求体; + 一如既往,在 FastAPI 中你可以按需组合 `def` 和 `async def`。 - 本例使用的是 `Item` 数据类列表; + 如果需要回顾何时用哪一个,请查看关于 [`async` 和 `await`](../async.md#in-a-hurry){.internal-link target=_blank} 的文档中的 _“急不可待?”_ 一节。 +9. 这个 *路径操作函数* 返回的不是数据类(当然也可以返回数据类),而是包含内部数据的字典列表。 -6. 这行代码返回的是包含 `items` 的字典,`items` 是数据类列表; + FastAPI 会使用(包含数据类的)`response_model` 参数来转换响应。 - FastAPI 仍能把数据序列化为 JSON; +你可以将 `dataclasses` 与其它类型注解以多种不同方式组合,来构建复杂的数据结构。 -7. 这行代码中,`response_model` 的类型注解是 `Author` 数据类列表; +更多细节请参考上面代码中的内联注释提示。 - 再一次,可以把 `dataclasses` 与标准类型注解一起使用; +## 深入学习 { #learn-more } -8. 注意,*路径操作函数*使用的是普通函数,不是异步函数; +你还可以把 `dataclasses` 与其它 Pydantic 模型组合、从它们继承、把它们包含到你自己的模型中等。 - 与往常一样,在 FastAPI 中,可以按需组合普通函数与异步函数; +想了解更多,请查看 Pydantic 关于 dataclasses 的文档。 - 如果不清楚何时使用异步函数或普通函数,请参阅**急不可待?**一节中对 `async` 与 `await` 的说明; +## 版本 { #version } -9. *路径操作函数*返回的不是数据类(虽然它可以返回数据类),而是返回内含数据的字典列表; - - FastAPI 使用(包含数据类的) `response_model` 参数转换响应。 - -把 `dataclasses` 与其它类型注解组合在一起,可以组成不同形式的复杂数据结构。 - -更多内容详见上述代码内的注释。 - -## 深入学习 - -您还可以把 `dataclasses` 与其它 Pydantic 模型组合在一起,继承合并的模型,把它们包含在您自己的模型里。 - -详见 Pydantic 官档 - 数据类。 - -## 版本 - -本章内容自 FastAPI `0.67.0` 版起生效。🔖 +自 FastAPI 版本 `0.67.0` 起可用。🔖 diff --git a/docs/zh/docs/advanced/events.md b/docs/zh/docs/advanced/events.md index 1ef6cdd3c..7b49931a4 100644 --- a/docs/zh/docs/advanced/events.md +++ b/docs/zh/docs/advanced/events.md @@ -1,18 +1,18 @@ -# 生命周期事件 +# 生命周期事件 { #lifespan-events } 你可以定义在应用**启动**前执行的逻辑(代码)。这意味着在应用**开始接收请求**之前,这些代码只会被执行**一次**。 同样地,你可以定义在应用**关闭**时应执行的逻辑。在这种情况下,这段代码将在**处理可能的多次请求后**执行**一次**。 -因为这段代码在应用开始接收请求**之前**执行,也会在处理可能的若干请求**之后**执行,它覆盖了整个应用程序的**生命周期**("生命周期"这个词很重要😉)。 +因为这段代码在应用开始接收请求**之前**执行,也会在处理可能的若干请求**之后**执行,它覆盖了整个应用程序的**生命周期**(“生命周期”这个词很重要😉)。 这对于设置你需要在整个应用中使用的**资源**非常有用,这些资源在请求之间**共享**,你可能需要在之后进行**释放**。例如,数据库连接池,或加载一个共享的机器学习模型。 -## 用例 +## 用例 { #use-case } -让我们从一个示例用例开始,看看如何解决它。 +让我们从一个示例**用例**开始,看看如何用它来解决问题。 -假设你有几个**机器学习的模型**,你想要用它们来处理请求。 +假设你有几个**机器学习的模型**,你想要用它们来处理请求。🤖 相同的模型在请求之间是共享的,因此并非每个请求或每个用户各自拥有一个模型。 @@ -20,19 +20,17 @@ 你可以在模块/文件的顶部加载它,但这也意味着即使你只是在运行一个简单的自动化测试,它也会**加载模型**,这样测试将**变慢**,因为它必须在能够独立运行代码的其他部分之前等待模型加载完成。 -这就是我们要解决的问题——在处理请求前加载模型,但只是在应用开始接收请求前,而不是代码执行时。 +这就是我们要解决的问题——在处理请求前加载模型,但只是在应用开始接收请求前,而不是在代码被加载时。 -## 生命周期 lifespan +## Lifespan { #lifespan } -你可以使用`FastAPI()`应用的`lifespan`参数和一个上下文管理器(稍后我将为你展示)来定义**启动**和**关闭**的逻辑。 +你可以使用 `FastAPI` 应用的 `lifespan` 参数和一个“上下文管理器”(稍后我将为你展示)来定义**启动**和**关闭**的逻辑。 让我们从一个例子开始,然后详细介绍。 -我们使用`yield`创建了一个异步函数`lifespan()`像这样: +我们使用 `yield` 创建了一个异步函数 `lifespan()` 像这样: -```Python hl_lines="16 19" -{!../../docs_src/events/tutorial003.py!} -``` +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} 在这里,我们在 `yield` 之前将(虚拟的)模型函数放入机器学习模型的字典中,以此模拟加载模型的耗时**启动**操作。这段代码将在应用程序**开始处理请求之前**执行,即**启动**期间。 @@ -40,35 +38,31 @@ /// tip | 提示 -**关闭**事件只会在你停止应用时触发。 +**关闭**事件会在你**停止**应用时发生。 -可能你需要启动一个新版本,或者你只是你厌倦了运行它。 🤷 +可能你需要启动一个新版本,或者你只是厌倦了运行它。 🤷 /// -## 生命周期函数 +### 生命周期函数 { #lifespan-function } 首先要注意的是,我们定义了一个带有 `yield` 的异步函数。这与带有 `yield` 的依赖项非常相似。 -```Python hl_lines="14-19" -{!../../docs_src/events/tutorial003.py!} -``` +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} -这个函数在 `yield`之前的部分,会在应用启动前执行。 +这个函数在 `yield` 之前的部分,会在应用启动前执行。 剩下的部分在 `yield` 之后,会在应用完成后执行。 -## 异步上下文管理器 +### 异步上下文管理器 { #async-context-manager } -如你所见,这个函数有一个装饰器 `@asynccontextmanager` 。 +如你所见,这个函数有一个装饰器 `@asynccontextmanager`。 它将函数转化为所谓的“**异步上下文管理器**”。 -```Python hl_lines="1 13" -{!../../docs_src/events/tutorial003.py!} -``` +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} -在 Python 中, **上下文管理器**是一个你可以在 `with` 语句中使用的东西,例如,`open()` 可以作为上下文管理器使用。 +在 Python 中,**上下文管理器**是一个你可以在 `with` 语句中使用的东西,例如,`open()` 可以作为上下文管理器使用。 ```Python with open("file.txt") as file: @@ -82,21 +76,19 @@ async with lifespan(app): await do_stuff() ``` -你可以像上面一样创建了一个上下文管理器或者异步上下文管理器,它的作用是在进入 `with` 块时,执行 `yield` 之前的代码,并且在离开 `with` 块时,执行 `yield` 后面的代码。 +你可以像上面一样创建一个上下文管理器或者异步上下文管理器,它的作用是在进入 `with` 块时,执行 `yield` 之前的代码,并且在离开 `with` 块时,执行 `yield` 后面的代码。 但在我们上面的例子里,我们并不是直接使用,而是传递给 FastAPI 来供其使用。 -`FastAPI()` 的 `lifespan` 参数接受一个**异步上下文管理器**,所以我们可以把我们新定义的上下文管理器 `lifespan` 传给它。 +`FastAPI` 的 `lifespan` 参数接受一个**异步上下文管理器**,所以我们可以把我们新定义的异步上下文管理器 `lifespan` 传给它。 -```Python hl_lines="22" -{!../../docs_src/events/tutorial003.py!} -``` +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} -## 替代事件(弃用) +## 替代事件(弃用) { #alternative-events-deprecated } /// warning | 警告 -配置**启动**和**关闭**事件的推荐方法是使用 `FastAPI()` 应用的 `lifespan` 参数,如前所示。如果你提供了一个 `lifespan` 参数,启动(`startup`)和关闭(`shutdown`)事件处理器将不再生效。要么使用 `lifespan`,要么配置所有事件,两者不能共用。 +配置**启动**和**关闭**的推荐方法是使用 `FastAPI` 应用的 `lifespan` 参数,如前所示。如果你提供了一个 `lifespan` 参数,启动(`startup`)和关闭(`shutdown`)事件处理器将不再生效。要么使用 `lifespan`,要么配置所有事件,两者不能共用。 你可以跳过这一部分。 @@ -104,70 +96,70 @@ async with lifespan(app): 有一种替代方法可以定义在**启动**和**关闭**期间执行的逻辑。 -**FastAPI** 支持定义在应用启动前,或应用关闭时执行的事件处理器(函数)。 +你可以定义在应用启动前或应用关闭时需要执行的事件处理器(函数)。 事件函数既可以声明为异步函数(`async def`),也可以声明为普通函数(`def`)。 -### `startup` 事件 +### `startup` 事件 { #startup-event } -使用 `startup` 事件声明 `app` 启动前运行的函数: +使用事件 `"startup"` 声明一个在应用启动前运行的函数: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} -本例中,`startup` 事件处理器函数为项目数据库(只是**字典**)提供了一些初始值。 +本例中,`startup` 事件处理器函数为项目“数据库”(只是一个 `dict`)提供了一些初始值。 **FastAPI** 支持多个事件处理器函数。 只有所有 `startup` 事件处理器运行完毕,**FastAPI** 应用才开始接收请求。 -### `shutdown` 事件 +### `shutdown` 事件 { #shutdown-event } -使用 `shutdown` 事件声明 `app` 关闭时运行的函数: +使用事件 `"shutdown"` 声明一个在应用关闭时运行的函数: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} -此处,`shutdown` 事件处理器函数在 `log.txt` 中写入一行文本 `Application shutdown`。 +此处,`shutdown` 事件处理器函数会向文件 `log.txt` 写入一行文本 `"Application shutdown"`。 -/// info | 说明 +/// info | 信息 -`open()` 函数中,`mode="a"` 指的是**追加**。因此这行文本会添加在文件已有内容之后,不会覆盖之前的内容。 +在 `open()` 函数中,`mode="a"` 指的是“追加”。因此这行文本会添加在文件已有内容之后,不会覆盖之前的内容。 /// /// tip | 提示 -注意,本例使用 Python `open()` 标准函数与文件交互。 +注意,本例使用 Python 标准的 `open()` 函数与文件交互。 -这个函数执行 I/O(输入/输出)操作,需要等待内容写进磁盘。 +这个函数执行 I/O(输入/输出)操作,需要“等待”内容写进磁盘。 -但 `open()` 函数不支持使用 `async` 与 `await`。 +但 `open()` 不使用 `async` 和 `await`。 -因此,声明事件处理函数要使用 `def`,不能使用 `asnyc def`。 +因此,声明事件处理函数要使用 `def`,而不是 `async def`。 /// -### `startup` 和 `shutdown` 一起使用 +### `startup` 和 `shutdown` 一起使用 { #startup-and-shutdown-together } 启动和关闭的逻辑很可能是连接在一起的,你可能希望启动某个东西然后结束它,获取一个资源然后释放它等等。 在不共享逻辑或变量的不同函数中处理这些逻辑比较困难,因为你需要在全局变量中存储值或使用类似的方式。 -因此,推荐使用 `lifespan` 。 +因此,推荐使用上面所述的 `lifespan`。 -## 技术细节 +## 技术细节 { #technical-details } 只是为好奇者提供的技术细节。🤓 -在底层,这部分是生命周期协议的一部分,参见 ASGI 技术规范,定义了称为启动(`startup`)和关闭(`shutdown`)的事件。 +在底层,这部分是 ASGI 技术规范中的 Lifespan 协议的一部分,定义了称为 `startup` 和 `shutdown` 的事件。 -/// info | 说明 +/// info | 信息 -有关事件处理器的详情,请参阅 Starlette 官档 - 事件。 +你可以在 Starlette 的 Lifespan 文档 中阅读更多关于 `lifespan` 处理器的内容。 -包括如何处理生命周期状态,这可以用于程序的其他部分。 +包括如何处理生命周期状态,以便在代码的其他部分使用。 /// -## 子应用 +## 子应用 { #sub-applications } -🚨 **FastAPI** 只会触发主应用中的生命周期事件,不包括[子应用 - 挂载](sub-applications.md){.internal-link target=_blank}中的。 +🚨 请注意,这些生命周期事件(startup 和 shutdown)只会在主应用上执行,不会在[子应用 - 挂载](sub-applications.md){.internal-link target=_blank}上执行。 diff --git a/docs/zh/docs/advanced/generate-clients.md b/docs/zh/docs/advanced/generate-clients.md index bcb9ba2bf..48a4ba07a 100644 --- a/docs/zh/docs/advanced/generate-clients.md +++ b/docs/zh/docs/advanced/generate-clients.md @@ -1,237 +1,208 @@ -# 生成客户端 +# 生成 SDK { #generating-sdks } -因为 **FastAPI** 是基于OpenAPI规范的,自然您可以使用许多相匹配的工具,包括自动生成API文档 (由 Swagger UI 提供)。 +因为 **FastAPI** 基于 **OpenAPI** 规范,它的 API 可以用许多工具都能理解的标准格式来描述。 -一个不太明显而又特别的优势是,你可以为你的API针对不同的**编程语言**来**生成客户端**(有时候被叫做 **SDKs** )。 +这让你可以轻松生成最新的**文档**、多语言的客户端库(**SDKs**),以及与代码保持同步的**测试**或**自动化工作流**。 -## OpenAPI 客户端生成 +本指南将带你为 FastAPI 后端生成一个 **TypeScript SDK**。 -有许多工具可以从**OpenAPI**生成客户端。 +## 开源 SDK 生成器 { #open-source-sdk-generators } -一个常见的工具是 OpenAPI Generator。 +一个功能多样的选择是 OpenAPI Generator,它支持**多种编程语言**,可以根据你的 OpenAPI 规范生成 SDK。 -如果您正在开发**前端**,一个非常有趣的替代方案是 openapi-ts。 +对于 **TypeScript 客户端**,Hey API 是为 TypeScript 生态打造的专用方案,提供优化的使用体验。 -## 生成一个 TypeScript 前端客户端 +你还可以在 OpenAPI.Tools 上发现更多 SDK 生成器。 -让我们从一个简单的 FastAPI 应用开始: +/// tip | 提示 -{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} - -请注意,*路径操作* 定义了他们所用于请求数据和回应数据的模型,所使用的模型是`Item` 和 `ResponseMessage`。 - -### API 文档 - -如果您访问API文档,您将看到它具有在请求中发送和在响应中接收数据的**模式(schemas)**: - - - -您可以看到这些模式,因为它们是用程序中的模型声明的。 - -那些信息可以在应用的 **OpenAPI模式** 被找到,然后显示在API文档中(通过Swagger UI)。 - -OpenAPI中所包含的模型里有相同的信息可以用于 **生成客户端代码**。 - -### 生成一个TypeScript 客户端 - -现在我们有了带有模型的应用,我们可以为前端生成客户端代码。 - -#### 安装 `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" - } -} -``` - -在这里添加 NPM `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`(前端HTTP库)。 - -### 尝试客户端代码 - -现在您可以导入并使用客户端代码,它可能看起来像这样,请注意,您可以为这些方法使用自动补全: - - - -您还将自动补全要发送的数据: - - - -/// tip - -请注意, `name` 和 `price` 的自动补全,是通过其在`Item`模型(FastAPI)中的定义实现的。 +FastAPI 会自动生成 **OpenAPI 3.1** 规范,因此你使用的任何工具都必须支持该版本。 /// -如果发送的数据字段不符,你也会看到编辑器的错误提示: +## 来自 FastAPI 赞助商的 SDK 生成器 { #sdk-generators-from-fastapi-sponsors } + +本节介绍的是由赞助 FastAPI 的公司提供的、具备**风险投资背景**或**公司支持**的方案。这些产品在高质量生成的 SDK 之上,提供了**更多特性**和**集成**。 + +通过 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,这些公司帮助确保框架及其**生态**保持健康并且**可持续**。 + +他们的赞助也体现了对 FastAPI **社区**(也就是你)的高度承诺,不仅关注提供**优秀的服务**,也支持一个**健壮且繁荣的框架**——FastAPI。🙇 + +例如,你可以尝试: + +* Speakeasy +* Stainless +* liblab + +其中一些方案也可能是开源的或提供免费层级,你可以不花钱就先试用。其他商业 SDK 生成器也可在网上找到。🤓 + +## 创建一个 TypeScript SDK { #create-a-typescript-sdk } + +先从一个简单的 FastAPI 应用开始: + +{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} + +请注意,这些*路径操作*使用 `Item` 和 `ResponseMessage` 模型来定义它们的请求载荷和响应载荷。 + +### API 文档 { #api-docs } + +访问 `/docs` 时,你会看到有用于请求发送和响应接收数据的**模式**: + + + +之所以能看到这些模式,是因为它们在应用中用模型声明了。 + +这些信息会包含在应用的 **OpenAPI 模式** 中,并显示在 API 文档里。 + +OpenAPI 中包含的这些模型信息就是用于**生成客户端代码**的基础。 + +### Hey API { #hey-api } + +当我们有了带模型的 FastAPI 应用后,可以使用 Hey API 来生成 TypeScript 客户端。最快的方式是通过 npx: + +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client +``` + +这会在 `./src/client` 生成一个 TypeScript SDK。 + +你可以在其官网了解如何安装 `@hey-api/openapi-ts`,以及阅读生成结果的说明。 + +### 使用 SDK { #using-the-sdk } + +现在你可以导入并使用客户端代码了。它可能是这样,并且你会发现方法有自动补全: + + + +要发送的载荷也会有自动补全: + + + +/// tip | 提示 + +请注意 `name` 和 `price` 的自动补全,它们是在 FastAPI 应用中的 `Item` 模型里定义的。 + +/// + +你发送的数据如果不符合要求,会在编辑器中显示内联错误: -响应(response)对象也拥有自动补全: +响应对象同样有自动补全: -## 带有标签的 FastAPI 应用 +## 带有标签的 FastAPI 应用 { #fastapi-app-with-tags } -在许多情况下,你的FastAPI应用程序会更复杂,你可能会使用标签来分隔不同组的*路径操作(path operations)*。 +很多情况下,你的 FastAPI 应用会更大,你可能会用标签来划分不同组的*路径操作*。 -例如,您可以有一个用 `items` 的部分和另一个用于 `users` 的部分,它们可以用标签来分隔: +例如,你可以有一个 **items** 相关的部分和另一个 **users** 相关的部分,它们可以用标签来分隔: {* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} -### 生成带有标签的 TypeScript 客户端 +### 生成带标签的 TypeScript 客户端 { #generate-a-typescript-client-with-tags } -如果您使用标签为FastAPI应用生成客户端,它通常也会根据标签分割客户端代码。 +如果你为使用了标签的 FastAPI 应用生成客户端,通常也会根据标签来拆分客户端代码。 -通过这种方式,您将能够为客户端代码进行正确地排序和分组: +这样你就可以在客户端代码中把内容正确地组织和分组: -在这个案例中,您有: +在这个例子中,你会有: * `ItemsService` * `UsersService` -### 客户端方法名称 +### 客户端方法名 { #client-method-names } -现在生成的方法名像 `createItemItemsPost` 看起来不太简洁: +现在,像 `createItemItemsPost` 这样的生成方法名看起来不太简洁: ```TypeScript ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) ``` -...这是因为客户端生成器为每个 *路径操作* 使用OpenAPI的内部 **操作 ID(operation ID)**。 +……这是因为客户端生成器会把每个*路径操作*的 OpenAPI 内部**操作 ID(operation ID)**用作方法名的一部分。 -OpenAPI要求每个操作 ID 在所有 *路径操作* 中都是唯一的,因此 FastAPI 使用**函数名**、**路径**和**HTTP方法/操作**来生成此操作ID,因为这样可以确保这些操作 ID 是唯一的。 +OpenAPI 要求每个操作 ID 在所有*路径操作*中都是唯一的,因此 FastAPI 会使用**函数名**、**路径**和**HTTP 方法/操作**来生成操作 ID,以确保其唯一性。 -但接下来我会告诉你如何改进。 🤓 +接下来我会告诉你如何改进。🤓 -## 自定义操作ID和更好的方法名 +## 自定义操作 ID 与更好的方法名 { #custom-operation-ids-and-better-method-names } -您可以**修改**这些操作ID的**生成**方式,以使其更简洁,并在客户端中具有**更简洁的方法名称**。 +你可以**修改**这些操作 ID 的**生成**方式,使之更简单,从而在客户端中得到**更简洁的方法名**。 -在这种情况下,您必须确保每个操作ID在其他方面是**唯一**的。 +在这种情况下,你需要用其他方式确保每个操作 ID 依然是**唯一**的。 -例如,您可以确保每个*路径操作*都有一个标签,然后根据**标签**和*路径操作***名称**(函数名)来生成操作ID。 +例如,你可以确保每个*路径操作*都有一个标签,然后基于**标签**和*路径操作***名称**(函数名)来生成操作 ID。 -### 自定义生成唯一ID函数 +### 自定义唯一 ID 生成函数 { #custom-generate-unique-id-function } -FastAPI为每个*路径操作*使用一个**唯一ID**,它用于**操作ID**,也用于任何所需自定义模型的名称,用于请求或响应。 +FastAPI 为每个*路径操作*使用一个**唯一 ID**,它既用于**操作 ID**,也用于请求或响应里任何需要的自定义模型名称。 -你可以自定义该函数。它接受一个 `APIRoute` 对象作为输入,并输出一个字符串。 +你可以自定义这个函数。它接收一个 `APIRoute` 并返回一个字符串。 -例如,以下是一个示例,它使用第一个标签(你可能只有一个标签)和*路径操作*名称(函数名)。 +例如,这里使用第一个标签(你很可能只有一个标签)和*路径操作*名称(函数名)。 -然后,你可以将这个自定义函数作为 `generate_unique_id_function` 参数传递给 **FastAPI**: +然后你可以把这个自定义函数通过 `generate_unique_id_function` 参数传给 **FastAPI**: {* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} -### 使用自定义操作ID生成TypeScript客户端 +### 使用自定义操作 ID 生成 TypeScript 客户端 { #generate-a-typescript-client-with-custom-operation-ids } -现在,如果你再次生成客户端,你会发现它具有改善的方法名称: +现在再次生成客户端,你会看到方法名已经改进: -正如你所见,现在方法名称中只包含标签和函数名,不再包含URL路径和HTTP操作的信息。 +如你所见,方法名现在由标签和函数名组成,不再包含 URL 路径和 HTTP 操作的信息。 -### 预处理用于客户端生成器的OpenAPI规范 +### 为客户端生成器预处理 OpenAPI 规范 { #preprocess-the-openapi-specification-for-the-client-generator } -生成的代码仍然存在一些**重复的信息**。 +生成的代码中仍有一些**重复信息**。 -我们已经知道该方法与 **items** 相关,因为它在 `ItemsService` 中(从标签中获取),但方法名中仍然有标签名作为前缀。😕 +我们已经知道这个方法与 **items** 有关,因为它位于 `ItemsService`(来自标签),但方法名里仍然带有标签名前缀。😕 -一般情况下对于OpenAPI,我们可能仍然希望保留它,因为这将确保操作ID是**唯一的**。 +通常我们仍然希望在 OpenAPI 中保留它,以确保操作 ID 的**唯一性**。 -但对于生成的客户端,我们可以在生成客户端之前**修改** OpenAPI 操作ID,以使方法名称更加美观和**简洁**。 +但对于生成的客户端,我们可以在生成之前**修改** OpenAPI 的操作 ID,只是为了让方法名更美观、更**简洁**。 -我们可以将 OpenAPI JSON 下载到一个名为`openapi.json`的文件中,然后使用以下脚本**删除此前缀的标签**: +我们可以把 OpenAPI JSON 下载到 `openapi.json` 文件中,然后用如下脚本**移除这个标签前缀**: -{* ../../docs_src/generate_clients/tutorial004.py *} +{* ../../docs_src/generate_clients/tutorial004_py39.py *} -通过这样做,操作ID将从类似于 `items-get_items` 的名称重命名为 `get_items` ,这样客户端生成器就可以生成更简洁的方法名称。 +//// tab | Node.js -### 使用预处理的OpenAPI生成TypeScript客户端 - -现在,由于最终结果保存在文件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" - } -} +```Javascript +{!> ../../docs_src/generate_clients/tutorial004.js!} ``` -生成新的客户端之后,你现在将拥有**清晰的方法名称**,具备**自动补全**、**错误提示**等功能: +//// + +这样,操作 ID 会从 `items-get_items` 之类的名字重命名为 `get_items`,从而让客户端生成器生成更简洁的方法名。 + +### 使用预处理后的 OpenAPI 生成 TypeScript 客户端 { #generate-a-typescript-client-with-the-preprocessed-openapi } + +因为最终结果现在保存在 `openapi.json` 中,你需要更新输入位置: + +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client +``` + +生成新客户端后,你将拥有**简洁的方法名**,并具备**自动补全**、**内联错误**等功能: -## 优点 +## 优点 { #benefits } -当使用自动生成的客户端时,你将获得以下的自动补全功能: +使用自动生成的客户端时,你会获得以下内容的**自动补全**: -* 方法。 -* 请求体中的数据、查询参数等。 -* 响应数据。 +* 方法 +* 请求体中的数据、查询参数等 +* 响应数据 -你还将获得针对所有内容的错误提示。 +你还会为所有内容获得**内联错误**。 -每当你更新后端代码并**重新生成**前端代码时,新的*路径操作*将作为方法可用,旧的方法将被删除,并且其他任何更改将反映在生成的代码中。 🤓 +每当你更新后端代码并**重新生成**前端时,新的*路径操作*会作为方法可用,旧的方法会被移除,其他任何更改都会反映到生成的代码中。🤓 -这也意味着如果有任何更改,它将自动**反映**在客户端代码中。如果你**构建**客户端,在使用的数据上存在**不匹配**时,它将报错。 +这也意味着如果有任何变更,它会自动**反映**到客户端代码中。而当你**构建**客户端时,如果所用数据存在任何**不匹配**,它会直接报错。 -因此,你将在开发周期的早期**检测到许多错误**,而不必等待错误在生产环境中向最终用户展示,然后尝试调试问题所在。 ✨ +因此,你可以在开发周期的早期就**发现许多错误**,而不必等到错误在生产环境中暴露给最终用户后再去调试问题所在。✨ diff --git a/docs/zh/docs/advanced/index.md b/docs/zh/docs/advanced/index.md index 6525802fc..610c18713 100644 --- a/docs/zh/docs/advanced/index.md +++ b/docs/zh/docs/advanced/index.md @@ -1,21 +1,21 @@ -# 高级用户指南 +# 高级用户指南 { #advanced-user-guide } -## 额外特性 +## 附加功能 { #additional-features } -主要的教程 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 应该足以让你了解 **FastAPI** 的所有主要特性。 +主要的[教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank}足以带你了解 **FastAPI** 的所有主要特性。 -你会在接下来的章节中了解到其他的选项、配置以及额外的特性。 +在接下来的章节中,你将看到其他选项、配置和附加功能。 -/// tip +/// tip | 提示 -接下来的章节**并不一定是**「高级的」。 +接下来的章节不一定是“高级”的。 -而且对于你的使用场景来说,解决方案很可能就在其中。 +对于你的用例,解决方案很可能就在其中之一。 /// -## 先阅读教程 +## 先阅读教程 { #read-the-tutorial-first } -你可能仍会用到 **FastAPI** 主教程 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 中的大多数特性。 +仅凭主要[教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank}中的知识,你已经可以使用 **FastAPI** 的大多数功能。 -接下来的章节我们认为你已经读过 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank},并且假设你已经知晓其中主要思想。 +接下来的章节默认你已经读过它,并理解其中的核心概念。 diff --git a/docs/zh/docs/advanced/middleware.md b/docs/zh/docs/advanced/middleware.md index 65e8c183f..108bbbb5c 100644 --- a/docs/zh/docs/advanced/middleware.md +++ b/docs/zh/docs/advanced/middleware.md @@ -1,4 +1,4 @@ -# 高级中间件 +# 高级中间件 { #advanced-middleware } 用户指南介绍了如何为应用添加[自定义中间件](../tutorial/middleware.md){.internal-link target=_blank} 。 @@ -6,9 +6,9 @@ 本章学习如何使用其它中间件。 -## 添加 ASGI 中间件 +## 添加 ASGI 中间件 { #adding-asgi-middlewares } -因为 **FastAPI** 基于 Starlette,且执行 ASGI 规范,所以可以使用任意 ASGI 中间件。 +因为 **FastAPI** 基于 Starlette,且执行 ASGI 规范,所以可以使用任意 ASGI 中间件。 中间件不必是专为 FastAPI 或 Starlette 定制的,只要遵循 ASGI 规范即可。 @@ -39,11 +39,11 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") `app.add_middleware()` 的第一个参数是中间件的类,其它参数则是要传递给中间件的参数。 -## 集成中间件 +## 集成中间件 { #integrated-middlewares } **FastAPI** 为常见用例提供了一些中间件,下面介绍怎么使用这些中间件。 -/// note | 技术细节 +/// note | 注意 以下几个示例中也可以使用 `from starlette.middleware.something import SomethingMiddleware`。 @@ -51,45 +51,47 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") /// -## `HTTPSRedirectMiddleware` +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } 强制所有传入请求必须是 `https` 或 `wss`。 任何传向 `http` 或 `ws` 的请求都会被重定向至安全方案。 -{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} -## `TrustedHostMiddleware` +## `TrustedHostMiddleware` { #trustedhostmiddleware } 强制所有传入请求都必须正确设置 `Host` 请求头,以防 HTTP 主机头攻击。 -{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} 支持以下参数: -* `allowed_hosts` - 允许的域名(主机名)列表。`*.example.com` 等通配符域名可以匹配子域名,或使用 `allowed_hosts=["*"]` 允许任意主机名,或省略中间件。 +* `allowed_hosts` - 允许的域名(主机名)列表。`*.example.com` 等通配符域名可以匹配子域名。若要允许任意主机名,可使用 `allowed_hosts=["*"]` 或省略此中间件。 +* `www_redirect` - 若设置为 `True`,对允许主机的非 www 版本的请求将被重定向到其 www 版本。默认为 `True`。 如果传入的请求没有通过验证,则发送 `400` 响应。 -## `GZipMiddleware` +## `GZipMiddleware` { #gzipmiddleware } -处理 `Accept-Encoding` 请求头中包含 `gzip` 请求的 GZip 响应。 +处理 `Accept-Encoding` 请求头中包含 `"gzip"` 请求的 GZip 响应。 中间件会处理标准响应与流响应。 -{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} 支持以下参数: -* `minimum_size` - 小于最小字节的响应不使用 GZip。 默认值是 `500`。 +* `minimum_size` - 小于该最小字节数的响应不使用 GZip。默认值是 `500`。 +* `compresslevel` - GZip 压缩使用的级别,为 1 到 9 的整数。默认为 `9`。值越低压缩越快但文件更大,值越高压缩越慢但文件更小。 -## 其它中间件 +## 其它中间件 { #other-middlewares } 除了上述中间件外,FastAPI 还支持其它ASGI 中间件。 例如: -* Uvicorn 的 `ProxyHeadersMiddleware` +* Uvicorn 的 `ProxyHeadersMiddleware` * MessagePack -其它可用中间件详见 Starlette 官档 -  中间件ASGI Awesome 列表。 +其它可用中间件详见 Starlette 官档 - 中间件ASGI Awesome 列表。 diff --git a/docs/zh/docs/advanced/openapi-callbacks.md b/docs/zh/docs/advanced/openapi-callbacks.md index f021eb10a..6e8df68ae 100644 --- a/docs/zh/docs/advanced/openapi-callbacks.md +++ b/docs/zh/docs/advanced/openapi-callbacks.md @@ -1,12 +1,12 @@ -# OpenAPI 回调 +# OpenAPI 回调 { #openapi-callbacks } -您可以创建触发外部 API 请求的*路径操作* API,这个外部 API 可以是别人创建的,也可以是由您自己创建的。 +您可以创建一个包含*路径操作*的 API,它会触发对别人创建的*外部 API*的请求(很可能就是那个会“使用”您 API 的同一个开发者)。 -API 应用调用外部 API 时的流程叫做**回调**。因为外部开发者编写的软件发送请求至您的 API,然后您的 API 要进行回调,并把请求发送至外部 API。 +当您的 API 应用调用*外部 API*时,这个过程被称为“回调”。因为外部开发者编写的软件会先向您的 API 发送请求,然后您的 API 再进行*回调*,向*外部 API*发送请求(很可能也是该开发者创建的)。 -此时,我们需要存档外部 API 的*信息*,比如应该有哪些*路径操作*,返回什么样的请求体,应该返回哪种响应等。 +此时,我们需要存档外部 API 的*信息*,比如应该有哪些*路径操作*,请求体应该是什么,应该返回什么响应等。 -## 使用回调的应用 +## 使用回调的应用 { #an-app-with-callbacks } 示例如下。 @@ -14,16 +14,16 @@ API 应用调用外部 API 时的流程叫做**回调**。因为外部开发者 发票包括 `id`、`title`(可选)、`customer`、`total` 等属性。 -API 的用户 (外部开发者)要在您的 API 内使用 POST 请求创建一条发票记录。 +API 的用户(外部开发者)要在您的 API 内使用 POST 请求创建一条发票记录。 (假设)您的 API 将: * 把发票发送至外部开发者的消费者 * 归集现金 * 把通知发送至 API 的用户(外部开发者) - * 通过(从您的 API)发送 POST 请求至外部 API (即**回调**)来完成 + * 通过(从您的 API)发送 POST 请求至外部 API(即**回调**)来完成 -## 常规 **FastAPI** 应用 +## 常规 **FastAPI** 应用 { #the-normal-fastapi-app } 添加回调前,首先看下常规 API 应用是什么样子。 @@ -31,17 +31,17 @@ API 的用户 (外部开发者)要在您的 API 内使用 POST 请求创建 这部分代码很常规,您对绝大多数代码应该都比较熟悉了: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[10:14,37:54] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip | 提示 -`callback_url` 查询参数使用 Pydantic 的 URL 类型。 +`callback_url` 查询参数使用 Pydantic 的 Url 类型。 /// 此处唯一比较新的内容是*路径操作装饰器*中的 `callbacks=invoices_callback_router.routes` 参数,下文介绍。 -## 存档回调 +## 存档回调 { #documenting-the-callback } 实际的回调代码高度依赖于您自己的 API 应用。 @@ -51,14 +51,14 @@ API 的用户 (外部开发者)要在您的 API 内使用 POST 请求创建 ```Python callback_url = "https://example.com/api/v1/invoices/events/" -requests.post(callback_url, json={"description": "Invoice paid", "paid": True}) +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) ``` 但回调最重要的部分可能是,根据 API 要发送给回调请求体的数据等内容,确保您的 API 用户(外部开发者)正确地实现*外部 API*。 因此,我们下一步要做的就是添加代码,为从 API 接收回调的*外部 API*存档。 -这部分文档在 `/docs` 下的 Swagger API 文档中显示,并且会告诉外部开发者如何构建*外部 API*。 +这部分文档在 `/docs` 下的 Swagger UI 中显示,并且会告诉外部开发者如何构建*外部 API*。 本例没有实现回调本身(只是一行代码),只有文档部分。 @@ -66,17 +66,17 @@ requests.post(callback_url, json={"description": "Invoice paid", "paid": True}) 实际的回调只是 HTTP 请求。 -实现回调时,要使用 HTTPXRequests。 +实现回调时,要使用 HTTPXRequests。 /// -## 编写回调文档代码 +## 编写回调文档代码 { #write-the-callback-documentation-code } 应用不执行这部分代码,只是用它来*记录 外部 API* 。 但,您已经知道用 **FastAPI** 创建自动 API 文档有多简单了。 -我们要使用与存档*外部 API* 相同的知识……通过创建外部 API 要实现的*路径操作*(您的 API 要调用的)。 +我们要使用与存档*外部 API* 相同的知识...通过创建外部 API 要实现的*路径操作*(您的 API 要调用的)。 /// tip | 提示 @@ -86,13 +86,13 @@ requests.post(callback_url, json={"description": "Invoice paid", "paid": True}) /// -### 创建回调的 `APIRouter` +### 创建回调的 `APIRouter` { #create-a-callback-apirouter } 首先,新建包含一些用于回调的 `APIRouter`。 -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[5,26] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} -### 创建回调*路径操作* +### 创建回调*路径操作* { #create-the-callback-path-operation } 创建回调*路径操作*也使用之前创建的 `APIRouter`。 @@ -101,16 +101,16 @@ requests.post(callback_url, json={"description": "Invoice paid", "paid": True}) * 声明要接收的请求体,例如,`body: InvoiceEvent` * 还要声明要返回的响应,例如,`response_model=InvoiceEventReceived` -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[17:19,22:23,29:33] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} 回调*路径操作*与常规*路径操作*有两点主要区别: * 它不需要任何实际的代码,因为应用不会调用这段代码。它只是用于存档*外部 API*。因此,函数的内容只需要 `pass` 就可以了 -* *路径*可以包含 OpenAPI 3 表达式(详见下文),可以使用带参数的变量,以及发送至您的 API 的原始请求的部分 +* *路径*可以包含 OpenAPI 3 表达式(详见下文),可以使用带参数的变量,以及发送至您的 API 的原始请求的部分 -### 回调路径表达式 +### 回调路径表达式 { #the-callback-path-expression } -回调*路径*支持包含发送给您的 API 的原始请求的部分的 OpenAPI 3 表达式。 +回调*路径*支持包含发送给您的 API 的原始请求的部分的 OpenAPI 3 表达式。 本例中是**字符串**: @@ -159,17 +159,17 @@ JSON 请求体包含如下内容: /// tip | 提示 -注意,回调 URL包含 `callback_url` (`https://www.external.org/events`)中的查询参数,还有 JSON 请求体内部的发票 ID(`2expen51ve`)。 +注意,回调 URL 包含 `callback_url`(`https://www.external.org/events`)中的查询参数,还有 JSON 请求体内部的发票 ID(`2expen51ve`)。 /// -### 添加回调路由 +### 添加回调路由 { #add-the-callback-router } 至此,在上文创建的回调路由里就包含了*回调路径操作*(外部开发者要在外部 API 中实现)。 现在使用 API *路径操作装饰器*的参数 `callbacks`,从回调路由传递属性 `.routes`(实际上只是路由/路径操作的**列表**): -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[36] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip | 提示 @@ -177,9 +177,9 @@ JSON 请求体包含如下内容: /// -### 查看文档 +### 查看文档 { #check-the-docs } -现在,使用 Uvicorn 启动应用,打开 http://127.0.0.1:8000/docs。 +现在,使用 Uvicorn 启动应用,打开 http://127.0.0.1:8000/docs。 就能看到文档的*路径操作*已经包含了**回调**的内容以及*外部 API*: diff --git a/docs/zh/docs/advanced/openapi-webhooks.md b/docs/zh/docs/advanced/openapi-webhooks.md index 92ae8db15..9e64ed4e3 100644 --- a/docs/zh/docs/advanced/openapi-webhooks.md +++ b/docs/zh/docs/advanced/openapi-webhooks.md @@ -1,4 +1,4 @@ -# OpenAPI 网络钩子 +# OpenAPI 网络钩子 { #openapi-webhooks } 有些情况下,您可能想告诉您的 API **用户**,您的应用程序可以携带一些数据调用*他们的*应用程序(给它们发送请求),通常是为了**通知**某种**事件**。 @@ -6,7 +6,7 @@ 这通常被称为**网络钩子**(Webhook)。 -## 使用网络钩子的步骤 +## 使用网络钩子的步骤 { #webhooks-steps } 通常的过程是**您**在代码中**定义**要发送的消息,即**请求的主体**。 @@ -16,27 +16,27 @@ 所有关于注册网络钩子的 URL 的**逻辑**以及发送这些请求的实际代码都由您决定。您可以在**自己的代码**中以任何想要的方式来编写它。 -## 使用 `FastAPI` 和 OpenAPI 文档化网络钩子 +## 使用 `FastAPI` 和 OpenAPI 文档化网络钩子 { #documenting-webhooks-with-fastapi-and-openapi } 使用 **FastAPI**,您可以利用 OpenAPI 来自定义这些网络钩子的名称、您的应用可以发送的 HTTP 操作类型(例如 `POST`、`PUT` 等)以及您的应用将发送的**请求体**。 这能让您的用户更轻松地**实现他们的 API** 来接收您的**网络钩子**请求,他们甚至可能能够自动生成一些自己的 API 代码。 -/// info +/// info | 信息 网络钩子在 OpenAPI 3.1.0 及以上版本中可用,FastAPI `0.99.0` 及以上版本支持。 /// -## 带有网络钩子的应用程序 +## 带有网络钩子的应用程序 { #an-app-with-webhooks } 当您创建一个 **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` 的架构中,并出现在自动生成的**文档 UI** 中。 -/// info +/// info | 信息 `app.webhooks` 对象实际上只是一个 `APIRouter` ,与您在使用多个文件来构建应用程序时所使用的类型相同。 @@ -46,7 +46,7 @@ 这是因为我们预计**您的用户**会以其他方式(例如通过网页仪表板)来定义他们希望接收网络钩子的请求的实际 **URL 路径**。 -### 查看文档 +### 查看文档 { #check-the-docs } 现在您可以启动您的应用程序并访问 http://127.0.0.1:8000/docs. diff --git a/docs/zh/docs/advanced/path-operation-advanced-configuration.md b/docs/zh/docs/advanced/path-operation-advanced-configuration.md index 12600eddb..6c9928ffc 100644 --- a/docs/zh/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/zh/docs/advanced/path-operation-advanced-configuration.md @@ -1,26 +1,26 @@ -# 路径操作的高级配置 +# 路径操作的高级配置 { #path-operation-advanced-configuration } -## OpenAPI 的 operationId +## OpenAPI 的 operationId { #openapi-operationid } /// warning -如果你并非 OpenAPI 的「专家」,你可能不需要这部分内容。 +如果你并非 OpenAPI 的“专家”,你可能不需要这部分内容。 /// -你可以在路径操作中通过参数 `operation_id` 设置要使用的 OpenAPI `operationId`。 +你可以在 *路径操作* 中通过参数 `operation_id` 设置要使用的 OpenAPI `operationId`。 -务必确保每个操作路径的 `operation_id` 都是唯一的。 +务必确保每个操作的 `operation_id` 都是唯一的。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### 使用 *路径操作函数* 的函数名作为 operationId +### 使用 *路径操作函数* 的函数名作为 operationId { #using-the-path-operation-function-name-as-the-operationid } -如果你想用你的 API 的函数名作为 `operationId` 的名字,你可以遍历一遍 API 的函数名,然后使用他们的 `APIRoute.name` 重写每个 *路径操作* 的 `operation_id`。 +如果你想用 API 的函数名作为 `operationId`,你可以遍历所有路径操作,并使用它们的 `APIRoute.name` 重写每个 *路径操作* 的 `operation_id`。 你应该在添加了所有 *路径操作* 之后执行此操作。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12,13,14,15,16,17,18,19,20,21,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip @@ -36,19 +36,137 @@ /// -## 从 OpenAPI 中排除 +## 从 OpenAPI 中排除 { #exclude-from-openapi } -使用参数 `include_in_schema` 并将其设置为 `False` ,来从生成的 OpenAPI 方案中排除一个 *路径操作*(这样一来,就从自动化文档系统中排除掉了)。 +使用参数 `include_in_schema` 并将其设置为 `False`,来从生成的 OpenAPI 方案中排除一个 *路径操作*(这样一来,就从自动化文档系统中排除掉了): -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## docstring 的高级描述 +## 来自 docstring 的高级描述 { #advanced-description-from-docstring } 你可以限制 *路径操作函数* 的 `docstring` 中用于 OpenAPI 的行数。 -添加一个 `\f` (一个「换页」的转义字符)可以使 **FastAPI** 在那一位置截断用于 OpenAPI 的输出。 +添加一个 `\f`(一个“换页”的转义字符)可以使 **FastAPI** 在那一位置截断用于 OpenAPI 的输出。 剩余部分不会出现在文档中,但是其他工具(比如 Sphinx)可以使用剩余部分。 +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19,20,21,22,23,24,25,26,27,28,29] *} +## 附加响应 { #additional-responses } + +你可能已经见过如何为一个 *路径操作* 声明 `response_model` 和 `status_code`。 + +这定义了该 *路径操作* 主响应的元数据。 + +你也可以为它声明带有各自模型、状态码等的附加响应。 + +文档中有一个完整章节,你可以阅读这里的[OpenAPI 中的附加响应](additional-responses.md){.internal-link target=_blank}。 + +## OpenAPI Extra { #openapi-extra } + +当你在应用中声明一个 *路径操作* 时,**FastAPI** 会自动生成与该 *路径操作* 相关的元数据,以包含到 OpenAPI 方案中。 + +/// note | 技术细节 + +在 OpenAPI 规范中,这被称为 Operation 对象。 + +/// + +它包含关于该 *路径操作* 的所有信息,并用于生成自动文档。 + +它包括 `tags`、`parameters`、`requestBody`、`responses` 等。 + +这个特定于 *路径操作* 的 OpenAPI 方案通常由 **FastAPI** 自动生成,但你也可以扩展它。 + +/// tip + +这是一个较低层级的扩展点。 + +如果你只需要声明附加响应,更方便的方式是使用[OpenAPI 中的附加响应](additional-responses.md){.internal-link target=_blank}。 + +/// + +你可以使用参数 `openapi_extra` 扩展某个 *路径操作* 的 OpenAPI 方案。 + +### OpenAPI 扩展 { #openapi-extensions } + +例如,这个 `openapi_extra` 可用于声明 [OpenAPI 扩展](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +当你打开自动 API 文档时,你的扩展会显示在该 *路径操作* 的底部。 + + + +如果你查看最终生成的 OpenAPI(在你的 API 的 `/openapi.json`),你也会看到你的扩展作为该 *路径操作* 的一部分: + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### 自定义 OpenAPI 路径操作方案 { #custom-openapi-path-operation-schema } + +`openapi_extra` 中的字典会与该 *路径操作* 自动生成的 OpenAPI 方案进行深度合并。 + +因此,你可以在自动生成的方案上添加额外数据。 + +例如,你可以决定用自己的代码读取并验证请求,而不使用 FastAPI 与 Pydantic 的自动功能,但你仍然希望在 OpenAPI 方案中定义该请求。 + +你可以用 `openapi_extra` 来做到: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +在这个示例中,我们没有声明任何 Pydantic 模型。事实上,请求体甚至没有被 解析 为 JSON,而是直接以 `bytes` 读取,并由函数 `magic_data_reader()` 以某种方式负责解析。 + +尽管如此,我们仍然可以声明请求体的预期方案。 + +### 自定义 OpenAPI 内容类型 { #custom-openapi-content-type } + +使用同样的技巧,你可以用一个 Pydantic 模型来定义 JSON Schema,然后把它包含到该 *路径操作* 的自定义 OpenAPI 方案部分中。 + +即使请求中的数据类型不是 JSON,你也可以这样做。 + +例如,在这个应用中我们不使用 FastAPI 集成的从 Pydantic 模型提取 JSON Schema 的功能,也不使用对 JSON 的自动校验。实际上,我们将请求的内容类型声明为 YAML,而不是 JSON: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +尽管我们没有使用默认的集成功能,我们仍然使用 Pydantic 模型手动生成我们想以 YAML 接收的数据的 JSON Schema。 + +然后我们直接使用请求并将请求体提取为 `bytes`。这意味着 FastAPI 甚至不会尝试将请求负载解析为 JSON。 + +接着在我们的代码中,我们直接解析该 YAML 内容,然后再次使用同一个 Pydantic 模型来验证该 YAML 内容: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip + +这里我们复用了同一个 Pydantic 模型。 + +但同样地,我们也可以用其他方式对其进行验证。 + +/// diff --git a/docs/zh/docs/advanced/response-change-status-code.md b/docs/zh/docs/advanced/response-change-status-code.md index cc1f2a73e..cdcd39f50 100644 --- a/docs/zh/docs/advanced/response-change-status-code.md +++ b/docs/zh/docs/advanced/response-change-status-code.md @@ -1,10 +1,10 @@ -# 响应 - 更改状态码 +# 响应 - 更改状态码 { #response-change-status-code } 你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。 但在某些情况下,你需要返回一个不同于默认值的状态码。 -## 使用场景 +## 使用场景 { #use-case } 例如,假设你想默认返回一个HTTP状态码为“OK”`200`。 @@ -14,16 +14,16 @@ 对于这些情况,你可以使用一个`Response`参数。 -## 使用 `Response` 参数 +## 使用 `Response` 参数 { #use-a-response-parameter } 你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies和头部做的那样)。 然后你可以在这个*临时*响应对象中设置`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`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 -**FastAPI**将使用这个临时响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 +**FastAPI**将使用这个*临时*响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。 diff --git a/docs/zh/docs/advanced/response-cookies.md b/docs/zh/docs/advanced/response-cookies.md index d5f2fe6fc..cc311a270 100644 --- a/docs/zh/docs/advanced/response-cookies.md +++ b/docs/zh/docs/advanced/response-cookies.md @@ -1,10 +1,10 @@ -# 响应Cookies +# 响应Cookies { #response-cookies } -## 使用 `Response` 参数 +## 使用 `Response` 参数 { #use-a-response-parameter } -你可以在 *路径函数* 中定义一个类型为 `Response`的参数,这样你就可以在这个临时响应对象中设置cookie了。 +你可以在 *路径操作函数* 中定义一个类型为 `Response` 的参数,这样你就可以在这个临时响应对象中设置cookie了。 -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} 而且你还可以根据你的需要响应不同的对象,比如常用的 `dict`,数据库model等。 @@ -12,17 +12,17 @@ **FastAPI** 会使用这个 *临时* 响应对象去装在这些cookies信息 (同样还有headers和状态码等信息), 最终会将这些信息和通过`response_model`转化过的数据合并到最终的响应里。 -你也可以在depend中定义`Response`参数,并设置cookie和header。 +你也可以在依赖中定义`Response`参数,并设置cookie和header。 -## 直接响应 `Response` +## 直接响应 `Response` { #return-a-response-directly } 你还可以在直接响应`Response`时直接创建cookies。 -你可以参考[Return a Response Directly](response-directly.md){.internal-link target=_blank}来创建response +你可以参考[直接返回 Response](response-directly.md){.internal-link target=_blank}来创建response 然后设置Cookies,并返回: -{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} /// tip @@ -34,7 +34,7 @@ /// -### 更多信息 +### 更多信息 { #more-info } /// note | 技术细节 diff --git a/docs/zh/docs/advanced/response-directly.md b/docs/zh/docs/advanced/response-directly.md index 4d9cd53f2..8a9cf6ab8 100644 --- a/docs/zh/docs/advanced/response-directly.md +++ b/docs/zh/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# 直接返回响应 +# 直接返回响应 { #return-a-response-directly } 当你创建一个 **FastAPI** *路径操作* 时,你可以正常返回以下任意一种数据:`dict`,`list`,Pydantic 模型,数据库模型等等。 @@ -10,11 +10,11 @@ 直接返回响应可能会有用处,比如返回自定义的响应头和 cookies。 -## 返回 `Response` +## 返回 `Response` { #return-a-response } 事实上,你可以返回任意 `Response` 或者任意 `Response` 的子类。 -/// tip | 小贴士 +/// tip | 提示 `JSONResponse` 本身是一个 `Response` 的子类。 @@ -26,18 +26,17 @@ 这种特性给你极大的可扩展性。你可以返回任何数据类型,重写任何数据声明或者校验,等等。 -## 在 `Response` 中使用 `jsonable_encoder` +## 在 `Response` 中使用 `jsonable_encoder` { #using-the-jsonable-encoder-in-a-response } 由于 **FastAPI** 并未对你返回的 `Response` 做任何改变,你必须确保你已经准备好响应内容。 -例如,如果不首先将 Pydantic 模型转换为 `dict`,并将所有数据类型(如 `datetime`、`UUID` 等)转换为兼容 JSON 的类型,则不能将其放入JSONResponse中。 +例如,如果不首先将 Pydantic 模型转换为 `dict`,并将所有数据类型(如 `datetime`、`UUID` 等)转换为兼容 JSON 的类型,则不能将其放入 `JSONResponse` 中。 -对于这些情况,在将数据传递给响应之前,你可以使用 `jsonable_encoder` 来转换你的数据。 +对于这些情况,在将数据传递给响应之前,你可以使用 `jsonable_encoder` 来转换你的数据: +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} -{* ../../docs_src/response_directly/tutorial001.py hl[4,6,20,21] *} - -/// note | 技术细节 +/// note | 注意 你也可以使用 `from starlette.responses import JSONResponse`。 @@ -45,22 +44,22 @@ /// -## 返回自定义 `Response` +## 返回自定义 `Response` { #returning-a-custom-response } -上面的例子展示了需要的所有部分,但还不够实用,因为你本可以只是直接返回 `item`,而**FastAPI** 默认帮你把这个 `item` 放到 `JSONResponse` 中,又默认将其转换成了 `dict`等等。 +上面的例子展示了需要的所有部分,但还不够实用,因为你本可以只是直接返回 `item`,而 **FastAPI** 默认帮你把这个 `item` 放到 `JSONResponse` 中,又默认将其转换成了 `dict` 等等。 现在,让我们看看你如何才能返回一个自定义的响应。 假设你想要返回一个 XML 响应。 -你可以把你的 XML 内容放到一个字符串中,放到一个 `Response` 中,然后返回。 +你可以把你的 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} 给响应编写文档。 +但是你仍可以参考 [OpenAPI 中的额外响应](additional-responses.md){.internal-link target=_blank} 给响应编写文档。 在后续的章节中你可以了解到如何使用/声明这些自定义的 `Response` 的同时还保留自动化的数据转换和文档等。 diff --git a/docs/zh/docs/advanced/response-headers.md b/docs/zh/docs/advanced/response-headers.md index 5c6a62e93..fa02f53be 100644 --- a/docs/zh/docs/advanced/response-headers.md +++ b/docs/zh/docs/advanced/response-headers.md @@ -1,39 +1,41 @@ -# 响应头 +# 响应头 { #response-headers } -## 使用 `Response` 参数 +## 使用 `Response` 参数 { #use-a-response-parameter } -你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies做的那样)。 +你可以在你的*路径操作函数*中声明一个 `Response` 类型的参数(就像你可以为 cookies 做的那样)。 然后你可以在这个*临时*响应对象中设置头部。 -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} -然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} -**FastAPI**将使用这个临时响应来提取头部(也包括cookies和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 +然后你可以像平常一样返回任何你需要的对象(例如一个 `dict` 或者一个数据库模型)。 -你也可以在依赖项中声明`Response`参数,并在其中设置头部(和cookies)。 +如果你声明了一个 `response_model`,它仍然会被用来过滤和转换你返回的对象。 -## 直接返回 `Response` +**FastAPI** 将使用这个临时响应来提取头部(也包括 cookies 和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何 `response_model` 过滤。 -你也可以在直接返回`Response`时添加头部。 +你也可以在依赖项中声明 `Response` 参数,并在其中设置头部(和 cookies)。 + +## 直接返回 `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 | 技术细节 -你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。 +你也可以使用 `from starlette.responses import Response` 或 `from starlette.responses import JSONResponse`。 -**FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`,只是为了方便开发者。但是,大多数可用的响应都直接来自Starlette。 +**FastAPI** 提供了与 `fastapi.responses` 相同的 `starlette.responses`,只是为了方便你(开发者)。但是,大多数可用的响应都直接来自 Starlette。 -由于`Response`经常用于设置头部和cookies,因此**FastAPI**还在`fastapi.Response`中提供了它。 +由于 `Response` 经常用于设置头部和 cookies,**FastAPI** 还在 `fastapi.Response` 中提供了它。 /// -## 自定义头部 +## 自定义头部 { #custom-headers } -请注意,可以使用'X-'前缀添加自定义专有头部。 +请注意,可以通过使用 `X-` 前缀添加自定义专有头部。 -但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在Starlette的CORS文档中记录的`expose_headers`参数。 +但是,如果你有自定义头部,并希望浏览器中的客户端能够看到它们,你需要将它们添加到你的 CORS 配置中(在 [CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank} 中阅读更多),使用在 Starlette 的 CORS 文档中记录的 `expose_headers` 参数。 diff --git a/docs/zh/docs/advanced/security/http-basic-auth.md b/docs/zh/docs/advanced/security/http-basic-auth.md index 599429f9d..55479d8e3 100644 --- a/docs/zh/docs/advanced/security/http-basic-auth.md +++ b/docs/zh/docs/advanced/security/http-basic-auth.md @@ -1,4 +1,4 @@ -# HTTP 基础授权 +# HTTP 基础授权 { #http-basic-auth } 最简单的用例是使用 HTTP 基础授权(HTTP Basic Auth)。 @@ -6,16 +6,16 @@ 如果没有接收到 HTTP 基础授权,就返回 HTTP 401 `"Unauthorized"` 错误。 -并返回含 `Basic` 值的请求头 `WWW-Authenticate`以及可选的 `realm` 参数。 +并返回响应头 `WWW-Authenticate`,其值为 `Basic`,以及可选的 `realm` 参数。 HTTP 基础授权让浏览器显示内置的用户名与密码提示。 输入用户名与密码后,浏览器会把它们自动发送至请求头。 -## 简单的 HTTP 基础授权 +## 简单的 HTTP 基础授权 { #simple-http-basic-auth } * 导入 `HTTPBasic` 与 `HTTPBasicCredentials` -* 使用 `HTTPBasic` 创建**安全概图** +* 使用 `HTTPBasic` 创建**安全方案** * 在*路径操作*的依赖项中使用 `security` * 返回类型为 `HTTPBasicCredentials` 的对象: * 包含发送的 `username` 与 `password` @@ -26,7 +26,7 @@ HTTP 基础授权让浏览器显示内置的用户名与密码提示。 -## 检查用户名 +## 检查用户名 { #check-the-username } 以下是更完整的示例。 @@ -52,13 +52,13 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password == 但使用 `secrets.compare_digest()`,可以防御**时差攻击**,更加安全。 -### 时差攻击 +### 时差攻击 { #timing-attacks } 什么是**时差攻击**? 假设攻击者试图猜出用户名与密码。 -他们发送用户名为 `johndoe`,密码为 `love123` 的请求。 +他们发送用户名为 `johndoe`,密码为 `love123` 的请求。 然后,Python 代码执行如下操作: @@ -80,28 +80,28 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": 此时,Python 要对比 `stanleyjobsox` 与 `stanleyjobson` 中的 `stanleyjobso`,才能知道这两个字符串不一样。因此会多花费几微秒来返回**错误的用户或密码**。 -#### 反应时间对攻击者的帮助 +#### 反应时间对攻击者的帮助 { #the-time-to-answer-helps-the-attackers } 通过服务器花费了更多微秒才发送**错误的用户或密码**响应,攻击者会知道猜对了一些内容,起码开头字母是正确的。 然后,他们就可以放弃 `johndoe`,再用类似 `stanleyjobsox` 的内容进行尝试。 -#### **专业**攻击 +#### **专业**攻击 { #a-professional-attack } 当然,攻击者不用手动操作,而是编写每秒能执行成千上万次测试的攻击程序,每次都会找到更多正确字符。 但是,在您的应用的**帮助**下,攻击者利用时间差,就能在几分钟或几小时内,以这种方式猜出正确的用户名和密码。 -#### 使用 `secrets.compare_digest()` 修补 +#### 使用 `secrets.compare_digest()` 修补 { #fix-it-with-secrets-compare-digest } 在此,代码中使用了 `secrets.compare_digest()`。 简单的说,它使用相同的时间对比 `stanleyjobsox` 和 `stanleyjobson`,还有 `johndoe` 和 `stanleyjobson`。对比密码时也一样。 -在代码中使用 `secrets.compare_digest()` ,就可以安全地防御全面攻击了。 +在代码中使用 `secrets.compare_digest()` ,就可以安全地防御这整类安全攻击。 -### 返回错误 +### 返回错误 { #return-the-error } -检测到凭证不正确后,返回 `HTTPException` 及状态码 401(与无凭证时返回的内容一样),并添加请求头 `WWW-Authenticate`,让浏览器再次显示登录提示: +检测到凭证不正确后,返回 `HTTPException` 及状态码 401(与无凭证时返回的内容一样),并添加响应头 `WWW-Authenticate`,让浏览器再次显示登录提示: {* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *} diff --git a/docs/zh/docs/advanced/security/index.md b/docs/zh/docs/advanced/security/index.md index 267e7ced7..84fec7aab 100644 --- a/docs/zh/docs/advanced/security/index.md +++ b/docs/zh/docs/advanced/security/index.md @@ -1,19 +1,19 @@ -# 高级安全 +# 高级安全 { #advanced-security } -## 附加特性 +## 附加特性 { #additional-features } -除 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性. +除 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性。 -/// tip | 小贴士 +/// tip | 提示 -接下来的章节 **并不一定是 "高级的"**. +接下来的章节**并不一定是 "高级的"**。 而且对于你的使用场景来说,解决方案很可能就在其中。 /// -## 先阅读教程 +## 先阅读教程 { #read-the-tutorial-first } -接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank}. +接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank}。 -它们都基于相同的概念,但支持一些额外的功能. +它们都基于相同的概念,但支持一些额外的功能。 diff --git a/docs/zh/docs/advanced/security/oauth2-scopes.md b/docs/zh/docs/advanced/security/oauth2-scopes.md index 784c38490..ce7facf4b 100644 --- a/docs/zh/docs/advanced/security/oauth2-scopes.md +++ b/docs/zh/docs/advanced/security/oauth2-scopes.md @@ -1,274 +1,274 @@ -# OAuth2 作用域 +# OAuth2 作用域 { #oauth2-scopes } -**FastAPI** 无缝集成 OAuth2 作用域(`Scopes`),可以直接使用。 +你可以在 **FastAPI** 中直接使用 OAuth2 作用域(Scopes),它们已无缝集成。 -作用域是更精密的权限系统,遵循 OAuth2 标准,与 OpenAPI 应用(和 API 自动文档)集成。 +这样你就可以按照 OAuth2 标准,构建更精细的权限系统,并将其集成进你的 OpenAPI 应用(以及 API 文档)中。 -OAuth2 也是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制。这些身份验证应用在用户登录应用时使用 OAuth2 提供指定权限。 +带作用域的 OAuth2 是很多大型身份验证提供商使用的机制,例如 Facebook、Google、GitHub、Microsoft、X (Twitter) 等。它们用它来为用户和应用授予特定权限。 -脸书、谷歌、GitHub、微软、推特就是 OAuth2 作用域登录。 +每次你“使用” Facebook、Google、GitHub、Microsoft、X (Twitter) “登录”时,该应用就在使用带作用域的 OAuth2。 -本章介绍如何在 **FastAPI** 应用中使用 OAuth2 作用域管理验证与授权。 +本节将介绍如何在你的 **FastAPI** 应用中,使用相同的带作用域的 OAuth2 管理认证与授权。 /// warning | 警告 -本章内容较难,刚接触 FastAPI 的新手可以跳过。 +本节内容相对进阶,如果你刚开始,可以先跳过。 -OAuth2 作用域不是必需的,没有它,您也可以处理身份验证与授权。 +你并不一定需要 OAuth2 作用域,你也可以用你自己的方式处理认证与授权。 -但 OAuth2 作用域与 API(通过 OpenAPI)及 API 文档集成地更好。 +但带作用域的 OAuth2 能很好地集成进你的 API(通过 OpenAPI)和 API 文档。 -不管怎么说,**FastAPI** 支持在代码中使用作用域或其它安全/授权需求项。 +不过,无论如何,你都可以在代码中按需强制这些作用域,或任何其它安全/授权需求。 -很多情况下,OAuth2 作用域就像一把牛刀。 +很多情况下,带作用域的 OAuth2 可能有点“大材小用”。 -但如果您确定要使用作用域,或对它有兴趣,请继续阅读。 +但如果你确实需要它,或者只是好奇,请继续阅读。 /// -## OAuth2 作用域与 OpenAPI +## OAuth2 作用域与 OpenAPI { #oauth2-scopes-and-openapi } -OAuth2 规范的**作用域**是由空格分割的字符串组成的列表。 +OAuth2 规范将“作用域”定义为由空格分隔的字符串列表。 -这些字符串支持任何格式,但不能包含空格。 +这些字符串的内容可以是任意格式,但不应包含空格。 -作用域表示的是**权限**。 +这些作用域表示“权限”。 -OpenAPI 中(例如 API 文档)可以定义**安全方案**。 +在 OpenAPI(例如 API 文档)中,你可以定义“安全方案”(security schemes)。 -这些安全方案在使用 OAuth2 时,还可以声明和使用作用域。 +当这些安全方案使用 OAuth2 时,你还可以声明并使用作用域。 -**作用域**只是(不带空格的)字符串。 +每个“作用域”只是一个(不带空格的)字符串。 -常用于声明特定安全权限,例如: +它们通常用于声明特定的安全权限,例如: -* 常见用例为,`users:read` 或 `users:write` -* 脸书和 Instagram 使用 `instagram_basic` -* 谷歌使用 `https://www.googleapis.com/auth/drive` +* 常见示例:`users:read` 或 `users:write` +* Facebook / Instagram 使用 `instagram_basic` +* Google 使用 `https://www.googleapis.com/auth/drive` -/// info | 说明 +/// info | 信息 -OAuth2 中,**作用域**只是声明特定权限的字符串。 +在 OAuth2 中,“作用域”只是一个声明所需特定权限的字符串。 -是否使用冒号 `:` 等符号,或是不是 URL 并不重要。 +是否包含像 `:` 这样的字符,或者是不是一个 URL,并不重要。 -这些细节只是特定的实现方式。 +这些细节取决于具体实现。 -对 OAuth2 来说,它们都只是字符串而已。 +对 OAuth2 而言,它们都只是字符串。 /// -## 全局纵览 +## 全局纵览 { #global-view } -首先,快速浏览一下以下代码与**用户指南**中 [OAuth2 实现密码哈希与 Bearer JWT 令牌验证](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}一章中代码的区别。以下代码使用 OAuth2 作用域: +首先,让我们快速看看与**用户指南**中 [OAuth2 实现密码(含哈希)、Bearer + JWT 令牌](../../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:124,128:134,139,153] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} -下面,我们逐步说明修改的代码内容。 +下面我们逐步回顾这些更改。 -## OAuth2 安全方案 +## OAuth2 安全方案 { #oauth2-security-scheme } -第一个修改的地方是,使用两个作用域 `me` 和 `items ` 声明 OAuth2 安全方案。 +第一个变化是:我们在声明 OAuth2 安全方案时,添加了两个可用的作用域 `me` 和 `items`。 -`scopes` 参数接收**字典**,键是作用域、值是作用域的描述: +参数 `scopes` 接收一个 `dict`,以作用域为键、描述为值: -{* ../../docs_src/security/tutorial005.py hl[62:65] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *} -因为声明了作用域,所以登录或授权时会在 API 文档中显示。 +因为我们现在声明了这些作用域,所以当你登录/授权时,它们会显示在 API 文档里。 -此处,选择给予访问权限的作用域: `me` 和 `items`。 +你可以选择要授予访问权限的作用域:`me` 和 `items`。 -这也是使用脸书、谷歌、GitHub 登录时的授权机制。 +这与使用 Facebook、Google、GitHub 等登录时授予权限的机制相同: -## JWT 令牌作用域 +## 带作用域的 JWT 令牌 { #jwt-token-with-scopes } -现在,修改令牌*路径操作*,返回请求的作用域。 +现在,修改令牌的*路径操作*以返回请求的作用域。 -此处仍然使用 `OAuth2PasswordRequestForm`。它包含类型为**字符串列表**的 `scopes` 属性,且`scopes` 属性中包含要在请求里接收的每个作用域。 +我们仍然使用 `OAuth2PasswordRequestForm`。它包含 `scopes` 属性,其值是 `list[str]`,包含请求中接收到的每个作用域。 -这样,返回的 JWT 令牌中就包含了作用域。 +我们把这些作用域作为 JWT 令牌的一部分返回。 /// danger | 危险 -为了简明起见,本例把接收的作用域直接添加到了令牌里。 +为简单起见,此处我们只是把接收到的作用域直接添加到了令牌中。 -但在您的应用中,为了安全,应该只把作用域添加到确实需要作用域的用户,或预定义的用户。 +但在你的应用里,为了安全起见,你应该只添加该用户实际能够拥有的作用域,或你预先定义的作用域。 /// -{* ../../docs_src/security/tutorial005.py hl[153] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} -## 在*路径操作*与依赖项中声明作用域 +## 在*路径操作*与依赖项中声明作用域 { #declare-scopes-in-path-operations-and-dependencies } -接下来,为*路径操作* `/users/me/items/` 声明作用域 `items`。 +现在我们声明,路径操作 `/users/me/items/` 需要作用域 `items`。 -为此,要从 `fastapi` 中导入并使用 `Security` 。 +为此,从 `fastapi` 导入并使用 `Security`。 -`Security` 声明依赖项的方式和 `Depends` 一样,但 `Security` 还能接收作用域(字符串)列表类型的参数 `scopes`。 +你可以用 `Security` 来声明依赖(就像 `Depends` 一样),但 `Security` 还接收一个 `scopes` 参数,其值是作用域(字符串)列表。 -此处使用与 `Depends` 相同的方式,把依赖项函数 `get_current_active_user` 传递给 `Security`。 +在这里,我们把依赖函数 `get_current_active_user` 传给 `Security`(就像用 `Depends` 一样)。 -同时,还传递了作用域**列表**,本例中只传递了一个作用域:`items`(此处支持传递更多作用域)。 +同时还传入一个作用域 `list`,此处仅包含一个作用域:`items`(也可以包含更多)。 -依赖项函数 `get_current_active_user` 还能声明子依赖项,不仅可以使用 `Depends`,也可以使用 `Security`。声明子依赖项函数(`get_current_user`)及更多作用域。 +依赖函数 `get_current_active_user` 也可以声明子依赖,不仅可以用 `Depends`,也可以用 `Security`。它声明了自己的子依赖函数(`get_current_user`),并添加了更多的作用域需求。 -本例要求使用作用域 `me`(还可以使用更多作用域)。 +在这个例子里,它需要作用域 `me`(也可以需要多个作用域)。 -/// note | 笔记 +/// note | 注意 不必在不同位置添加不同的作用域。 -本例使用的这种方式只是为了展示 **FastAPI** 如何处理在不同层级声明的作用域。 +这里这样做,是为了演示 **FastAPI** 如何处理在不同层级声明的作用域。 /// -{* ../../docs_src/security/tutorial005.py hl[4,139,166] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} /// info | 技术细节 -`Security` 实际上是 `Depends` 的子类,而且只比 `Depends` 多一个参数。 +`Security` 实际上是 `Depends` 的子类,它只多了一个我们稍后会看到的参数。 -但使用 `Security` 代替 `Depends`,**FastAPI** 可以声明安全作用域,并在内部使用这些作用域,同时,使用 OpenAPI 存档 API。 +但当你使用 `Security` 而不是 `Depends` 时,**FastAPI** 会知道它可以声明安全作用域,在内部使用它们,并用 OpenAPI 文档化 API。 -但实际上,从 `fastapi` 导入的 `Query`、`Path`、`Depends`、`Security` 等对象,只是返回特殊类的函数。 +另外,从 `fastapi` 导入的 `Query`、`Path`、`Depends`、`Security` 等,实际上都是返回特殊类的函数。 /// -## 使用 `SecurityScopes` +## 使用 `SecurityScopes` { #use-securityscopes } -修改依赖项 `get_current_user`。 +现在更新依赖项 `get_current_user`。 -这是上面的依赖项使用的依赖项。 +上面那些依赖会用到它。 -这里使用的也是之前创建的 OAuth2 方案,并把它声明为依赖项:`oauth2_scheme`。 +这里我们使用之前创建的同一个 OAuth2 方案,并把它声明为依赖:`oauth2_scheme`。 -该依赖项函数本身不需要作用域,因此,可以使用 `Depends` 和 `oauth2_scheme`。不需要指定安全作用域时,不必使用 `Security`。 +因为这个依赖函数本身没有任何作用域需求,所以我们可以用 `Depends(oauth2_scheme)`,当不需要指定安全作用域时,不必使用 `Security`。 -此处还声明了从 `fastapi.security` 导入的 `SecurityScopes` 类型的特殊参数。 +我们还声明了一个从 `fastapi.security` 导入的特殊参数 `SecurityScopes` 类型。 -`SecuriScopes` 类与 `Request` 类似(`Request` 用于直接提取请求对象)。 +这个 `SecurityScopes` 类类似于 `Request`(`Request` 用来直接获取请求对象)。 -{* ../../docs_src/security/tutorial005.py hl[8,105] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} -## 使用 `scopes` +## 使用 `scopes` { #use-the-scopes } 参数 `security_scopes` 的类型是 `SecurityScopes`。 -它的属性 `scopes` 是作用域列表,所有依赖项都把它作为子依赖项。也就是说所有**依赖**……这听起来有些绕,后文会有解释。 +它会有一个 `scopes` 属性,包含一个列表,里面是它自身以及所有把它作为子依赖的依赖项所需要的所有作用域。也就是说,所有“依赖者”……这可能有点绕,下面会再次解释。 -(类 `SecurityScopes` 的)`security_scopes` 对象还提供了单字符串类型的属性 `scope_str`,该属性是(要在本例中使用的)用空格分割的作用域。 +`security_scopes` 对象(类型为 `SecurityScopes`)还提供了一个 `scope_str` 属性,它是一个用空格分隔这些作用域的单个字符串(我们将会用到它)。 -此处还创建了后续代码中要复用(`raise`)的 `HTTPException` 。 +我们创建一个 `HTTPException`,后面可以在多个位置复用(`raise`)它。 -该异常包含了作用域所需的(如有),以空格分割的字符串(使用 `scope_str`)。该字符串要放到包含作用域的 `WWW-Authenticate` 请求头中(这也是规范的要求)。 +在这个异常中,我们包含所需的作用域(如果有的话),以空格分隔的字符串(使用 `scope_str`)。我们把这个包含作用域的字符串放在 `WWW-Authenticate` 响应头中(这是规范要求的一部分)。 -{* ../../docs_src/security/tutorial005.py hl[105,107:115] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} -## 校验 `username` 与数据形状 +## 校验 `username` 与数据形状 { #verify-the-username-and-data-shape } -我们可以校验是否获取了 `username`,并抽取作用域。 +我们校验是否获取到了 `username`,并提取作用域。 -然后,使用 Pydantic 模型校验数据(捕获 `ValidationError` 异常),如果读取 JWT 令牌或使用 Pydantic 模型验证数据时出错,就会触发之前创建的 `HTTPException` 异常。 +然后使用 Pydantic 模型验证这些数据(捕获 `ValidationError` 异常),如果读取 JWT 令牌或用 Pydantic 验证数据时出错,就抛出我们之前创建的 `HTTPException`。 -对此,要使用新的属性 `scopes` 更新 Pydantic 模型 `TokenData`。 +为此,我们给 Pydantic 模型 `TokenData` 添加了一个新属性 `scopes`。 -使用 Pydantic 验证数据可以确保数据中含有由作用域组成的**字符串列表**,以及 `username` 字符串等内容。 +通过用 Pydantic 验证数据,我们可以确保确实得到了例如一个由作用域组成的 `list[str]`,以及一个 `str` 类型的 `username`。 -反之,如果使用**字典**或其它数据结构,就有可能在后面某些位置破坏应用,形成安全隐患。 +而不是,例如得到一个 `dict` 或其它什么,这可能会在后续某个时刻破坏应用,形成安全风险。 -还可以使用用户名验证用户,如果没有用户,也会触发之前创建的异常。 +我们还验证是否存在该用户名的用户,如果没有,就抛出前面创建的同一个异常。 -{* ../../docs_src/security/tutorial005.py hl[46,116:127] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} -## 校验 `scopes` +## 校验 `scopes` { #verify-the-scopes } -接下来,校验所有依赖项和依赖要素(包括*路径操作*)所需的作用域。这些作用域包含在令牌的 `scopes` 里,如果不在其中就会触发 `HTTPException` 异常。 +现在我们要验证,这个依赖以及所有依赖者(包括*路径操作*)所需的所有作用域,是否都包含在接收到的令牌里的作用域中,否则就抛出 `HTTPException`。 -为此,要使用包含所有作用域**字符串列表**的 `security_scopes.scopes`, 。 +为此,我们使用 `security_scopes.scopes`,它包含一个由这些作用域组成的 `list[str]`。 -{* ../../docs_src/security/tutorial005.py hl[128:134] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} -## 依赖项树与作用域 +## 依赖树与作用域 { #dependency-tree-and-scopes } -再次查看这个依赖项树与作用域。 +再次回顾这个依赖树与作用域。 -`get_current_active_user` 依赖项包含子依赖项 `get_current_user`,并在 `get_current_active_user`中声明了作用域 `"me"` 包含所需作用域列表 ,在 `security_scopes.scopes` 中传递给 `get_current_user`。 +由于 `get_current_active_user` 依赖把 `get_current_user` 作为子依赖,因此在 `get_current_active_user` 中声明的作用域 `"me"` 会被包含在传给 `get_current_user` 的 `security_scopes.scopes` 所需作用域列表中。 -*路径操作*自身也声明了作用域,`"items"`,这也是 `security_scopes.scopes` 列表传递给 `get_current_user` 的。 +*路径操作*本身也声明了一个作用域 `"items"`,它也会包含在传给 `get_current_user` 的 `security_scopes.scopes` 列表中。 -依赖项与作用域的层级架构如下: +依赖与作用域的层级结构如下: * *路径操作* `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` 是包含上述声明的所有作用域的**列表**,因此: - * `security_scopes.scopes` 包含用于*路径操作*的 `["me", "items"]` - * `security_scopes.scopes` 包含*路径操作* `read_users_me` 的 `["me"]`,因为它在依赖项里被声明 - * `security_scopes.scopes` 包含用于*路径操作* `read_system_status` 的 `[]`(空列表),并且它的依赖项 `get_current_user` 也没有声明任何 `scope` + * 带有依赖的必需作用域 `["items"]`: + * `get_current_active_user`: + * 依赖函数 `get_current_active_user` 包含: + * 带有依赖的必需作用域 `["me"]`: + * `get_current_user`: + * 依赖函数 `get_current_user` 包含: + * 自身不需要任何作用域。 + * 一个使用 `oauth2_scheme` 的依赖。 + * 一个类型为 `SecurityScopes` 的 `security_scopes` 参数: + * 该 `security_scopes` 参数有一个 `scopes` 属性,它是一个包含上面所有已声明作用域的 `list`,因此: + * 对于*路径操作* `read_own_items`,`security_scopes.scopes` 将包含 `["me", "items"]`。 + * 对于*路径操作* `read_users_me`,`security_scopes.scopes` 将包含 `["me"]`,因为它在依赖 `get_current_active_user` 中被声明。 + * 对于*路径操作* `read_system_status`,`security_scopes.scopes` 将包含 `[]`(空列表),因为它既没有声明任何带 `scopes` 的 `Security`,其依赖 `get_current_user` 也没有声明任何 `scopes`。 /// tip | 提示 -此处重要且**神奇**的事情是,`get_current_user` 检查每个*路径操作*时可以使用不同的 `scopes` 列表。 +这里重要且“神奇”的地方是,`get_current_user` 在检查每个*路径操作*时会得到不同的 `scopes` 列表。 -所有这些都依赖于在每个*路径操作*和指定*路径操作*的依赖树中的每个依赖项。 +这一切都取决于为该特定*路径操作*在其自身以及依赖树中的每个依赖里声明的 `scopes`。 /// -## `SecurityScopes` 的更多细节 +## 关于 `SecurityScopes` 的更多细节 { #more-details-about-securityscopes } -您可以任何位置或多个位置使用 `SecurityScopes`,不一定非得在**根**依赖项中使用。 +你可以在任意位置、多个位置使用 `SecurityScopes`,不一定非得在“根”依赖里。 -它总是在当前 `Security` 依赖项中和所有依赖因子对于**特定** *路径操作*和**特定**依赖树中安全作用域 +它总会包含当前 `Security` 依赖中以及所有依赖者在“该特定”*路径操作*和“该特定”依赖树里声明的安全作用域。 -因为 `SecurityScopes` 包含所有由依赖项声明的作用域,可以在核心依赖函数中用它验证所需作用域的令牌,然后再在不同的*路径操作*中声明不同作用域需求。 +因为 `SecurityScopes` 会包含依赖者声明的所有作用域,你可以在一个核心依赖函数里用它验证令牌是否具有所需作用域,然后在不同的*路径操作*里声明不同的作用域需求。 -它们会为每个*路径操作*进行单独检查。 +它们会针对每个*路径操作*分别检查。 -## 查看文档 +## 查看文档 { #check-it } -打开 API 文档,进行身份验证,并指定要授权的作用域。 +打开 API 文档,你可以进行身份验证,并指定要授权的作用域。 -没有选择任何作用域,也可以进行**身份验证**,但访问 `/uses/me` 或 `/users/me/items` 时,会显示没有足够的权限。但仍可以访问 `/status/`。 +如果你不选择任何作用域,你依然会“通过认证”,但当你访问 `/users/me/` 或 `/users/me/items/` 时,会收到一个错误,提示你没有足够的权限。你仍然可以访问 `/status/`。 -如果选择了作用域 `me`,但没有选择作用域 `items`,则可以访问 `/users/me/`,但不能访问 `/users/me/items`。 +如果你选择了作用域 `me`,但没有选择作用域 `items`,你可以访问 `/users/me/`,但不能访问 `/users/me/items/`。 -这就是通过用户提供的令牌使用第三方应用访问这些*路径操作*时会发生的情况,具体怎样取决于用户授予第三方应用的权限。 +当第三方应用使用用户提供的令牌访问这些*路径操作*时,也会发生同样的情况,取决于用户授予该应用了多少权限。 -## 关于第三方集成 +## 关于第三方集成 { #about-third-party-integrations } -本例使用 OAuth2 **密码**流。 +在这个示例中我们使用的是 OAuth2 的“password”流。 -这种方式适用于登录我们自己的应用,最好使用我们自己的前端。 +当我们登录自己的应用(很可能还有我们自己的前端)时,这是合适的。 -因为我们能控制自己的前端应用,可以信任它接收 `username` 与 `password`。 +因为我们可以信任它来接收 `username` 和 `password`,毕竟我们掌控它。 -但如果构建的是连接其它应用的 OAuth2 应用,比如具有与脸书、谷歌、GitHub 相同功能的第三方身份验证应用。那您就应该使用其它安全流。 +但如果你在构建一个 OAuth2 应用,让其它应用来连接(也就是说,你在构建等同于 Facebook、Google、GitHub 等的身份验证提供商),你应该使用其它的流。 -最常用的是隐式流。 +最常见的是隐式流(implicit flow)。 -最安全的是代码流,但实现起来更复杂,而且需要更多步骤。因为它更复杂,很多第三方身份验证应用最终建议使用隐式流。 +最安全的是代码流(authorization code flow),但实现更复杂,需要更多步骤。也因为更复杂,很多提供商最终会建议使用隐式流。 -/// note | 笔记 +/// note | 注意 -每个身份验证应用都会采用不同方式会命名流,以便融合入自己的品牌。 +每个身份验证提供商常常会用不同的方式给它们的流命名,以融入自己的品牌。 -但归根结底,它们使用的都是 OAuth2 标准。 +但归根结底,它们实现的都是同一个 OAuth2 标准。 /// -**FastAPI** 的 `fastapi.security.oauth2` 里包含了所有 OAuth2 身份验证流工具。 +**FastAPI** 在 `fastapi.security.oauth2` 中为所有这些 OAuth2 身份验证流提供了工具。 -## 装饰器 `dependencies` 中的 `Security` +## 装饰器 `dependencies` 中的 `Security` { #security-in-decorator-dependencies } -同样,您可以在装饰器的 `dependencies` 参数中定义 `Depends` 列表,(详见[路径操作装饰器依赖项](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank})),也可以把 `scopes` 与 `Security` 一起使用。 +就像你可以在装饰器的 `dependencies` 参数中定义 `Depends` 的 `list`(详见[路径操作装饰器依赖项](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}),你也可以在那儿配合 `Security` 使用 `scopes`。 diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md index e33da136f..adf264491 100644 --- a/docs/zh/docs/advanced/settings.md +++ b/docs/zh/docs/advanced/settings.md @@ -1,190 +1,94 @@ -# 设置和环境变量 +# 设置和环境变量 { #settings-and-environment-variables } -在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。 +在许多情况下,你的应用可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等。 -这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的,比如密钥。 +这些设置中的大多数是可变的(可能会改变),例如数据库 URL。并且很多可能是敏感的,比如密钥。 因此,通常会将它们提供为由应用程序读取的环境变量。 -## 环境变量 +/// tip | 提示 -/// tip - -如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。 +要理解环境变量,你可以阅读[环境变量](../environment-variables.md){.internal-link target=_blank}。 /// -环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。 +## 类型和验证 { #types-and-validation } -您可以在 shell 中创建和使用环境变量,而无需使用 Python: +这些环境变量只能处理文本字符串,因为它们在 Python 之外,并且必须与其他程序及系统的其余部分兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。 -//// tab | Linux、macOS、Windows Bash +这意味着,在 Python 中从环境变量读取的任何值都是 `str` 类型,任何到不同类型的转换或任何验证都必须在代码中完成。 + +## Pydantic 的 `Settings` { #pydantic-settings } + +幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的这些设置:Pydantic: Settings management。 + +### 安装 `pydantic-settings` { #install-pydantic-settings } + +首先,确保你创建并激活了[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后安装 `pydantic-settings` 包:
```console -// 您可以创建一个名为 MY_NAME 的环境变量 -$ export MY_NAME="Wade Wilson" - -// 然后您可以与其他程序一起使用它,例如 -$ echo "Hello $MY_NAME" - -Hello Wade Wilson +$ pip install pydantic-settings +---> 100% ```
-//// - -//// tab | Windows PowerShell +当你用以下方式安装 `all` 扩展时,它也会被一并安装:
```console -// 创建一个名为 MY_NAME 的环境变量 -$ $Env:MY_NAME = "Wade Wilson" - -// 与其他程序一起使用它,例如 -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson +$ pip install "fastapi[all]" +---> 100% ```
-//// +### 创建 `Settings` 对象 { #create-the-settings-object } -### 在 Python 中读取环境变量 +从 Pydantic 导入 `BaseSettings` 并创建一个子类,这与创建 Pydantic 模型非常相似。 -您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。 +与 Pydantic 模型一样,用类型注解声明类属性,也可以指定默认值。 -例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容: +你可以使用与 Pydantic 模型相同的验证功能和工具,例如不同的数据类型,以及使用 `Field()` 进行附加验证。 -```Python hl_lines="3" -import os +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` +/// tip | 提示 -/// tip - -`os.getenv()` 的第二个参数是要返回的默认值。 - -如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。 +如果你想要一个可以快速复制粘贴的示例,请不要使用这个示例,使用下面最后一个示例。 /// -然后,您可以调用该 Python 程序: +当你创建该 `Settings` 类的实例(此处是 `settings` 对象)时,Pydantic 会以不区分大小写的方式读取环境变量,因此,大写变量 `APP_NAME` 仍会用于属性 `app_name`。 + +接着它会转换并验证数据。因此,当你使用该 `settings` 对象时,你将获得你声明的类型的数据(例如 `items_per_user` 将是 `int`)。 + +### 使用 `settings` { #use-the-settings } + +然后你可以在应用中使用新的 `settings` 对象: + +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} + +### 运行服务器 { #run-the-server } + +接下来,运行服务器,并把配置作为环境变量传入,例如你可以设置 `ADMIN_EMAIL` 和 `APP_NAME`:
```console -// 这里我们还没有设置环境变量 -$ python main.py - -// 因为我们没有设置环境变量,所以我们得到默认值 - -Hello World from Python - -// 但是如果我们先创建一个环境变量 -$ export 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 - -您可以在 Twelve-Factor App: Config 中阅读更多相关信息。 - -/// - -### 类型和验证 - -这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。 - -这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。 - -## Pydantic 的 `Settings` - -幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即Pydantic: Settings management。 - -### 创建 `Settings` 对象 - -从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。 - -与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。 - -您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。 - -{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *} - -/// tip - -如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。 - -/// - -然后,当您创建该 `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 +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-/// tip +/// tip | 提示 -要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。 +要为单个命令设置多个环境变量,只需用空格分隔它们,并把它们都放在命令前面。 /// @@ -192,118 +96,118 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app `app_name` 将为 `"ChimichangApp"`。 -而 `items_per_user` 将保持其默认值为 `50`。 +而 `items_per_user` 会保持默认值 `50`。 -## 在另一个模块中设置 +## 在另一个模块中放置设置 { #settings-in-another-module } -您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。 +你可以把这些设置放在另一个模块文件中,就像你在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 -例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容: +例如,可以有一个 `config.py` 文件: -{* ../../docs_src/settings/app01/config.py *} +{* ../../docs_src/settings/app01_py39/config.py *} -然后在一个名为 `main.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 +/// tip | 提示 -您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 +你还需要一个 `__init__.py` 文件,就像你在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 /// -## 在依赖项中使用设置 +## 在依赖项中提供设置 { #settings-in-a-dependency } -在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。 +在某些情况下,从依赖项中提供设置可能更有用,而不是在所有地方都使用一个全局的 `settings` 对象。 -这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。 +这在测试期间尤其有用,因为可以很容易地用你自己的自定义设置覆盖依赖项。 -### 配置文件 +### 配置文件 { #the-config-file } -根据前面的示例,您的 `config.py` 文件可能如下所示: +延续上一个示例,你的 `config.py` 文件可能如下所示: -{* ../../docs_src/settings/app02/config.py hl[10] *} +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} -请注意,现在我们不创建默认实例 `settings = Settings()`。 +注意,现在我们不再创建默认实例 `settings = Settings()`。 -### 主应用程序文件 +### 主应用文件 { #the-main-app-file } 现在我们创建一个依赖项,返回一个新的 `config.Settings()`。 {* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} -/// tip +/// tip | 提示 我们稍后会讨论 `@lru_cache`。 -目前,您可以将 `get_settings()` 视为普通函数。 +目前你可以把 `get_settings()` 当作普通函数。 /// -然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。 +然后我们可以在“路径操作函数”中将其作为依赖项引入,并在需要的任何地方使用它。 {* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} -### 设置和测试 +### 设置与测试 { #settings-and-testing } -然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象: +接着,在测试期间,通过为 `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] *} 在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。 -然后,我们可以测试它是否被使用。 +然后我们可以测试它是否被使用。 -## 从 `.env` 文件中读取设置 +## 读取 `.env` 文件 { #reading-a-env-file } -如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。 +如果你有许多设置可能经常变化,或在不同环境中不同,那么把它们放进一个文件中,然后像环境变量一样从中读取,可能非常有用。 -这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中,该文件被称为“dotenv”。 +这种做法非常常见:这些环境变量通常放在名为 `.env` 的文件中,该文件被称为 “dotenv”。 -/// tip +/// tip | 提示 -以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS)中的隐藏文件。 +以点(`.`)开头的文件在类 Unix 系统(如 Linux 和 macOS)中是隐藏文件。 -但是,dotenv 文件实际上不一定要具有确切的文件名。 +但 dotenv 文件并不一定必须是这个确切的文件名。 /// -Pydantic 支持使用外部库从这些类型的文件中读取。您可以在Pydantic 设置: Dotenv (.env) 支持中阅读更多相关信息。 +Pydantic 支持使用一个外部库来从这类文件中读取。你可以在 Pydantic Settings: Dotenv (.env) support 中阅读更多信息。 -/// tip +/// tip | 提示 -要使其工作,您需要执行 `pip install python-dotenv`。 +要使其工作,你需要执行 `pip install python-dotenv`。 /// -### `.env` 文件 +### `.env` 文件 { #the-env-file } -您可以使用以下内容创建一个名为 `.env` 的文件: +你可以有一个 `.env` 文件,内容如下: ```bash ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" ``` -### 从 `.env` 文件中读取设置 +### 从 `.env` 中读取设置 { #read-settings-from-env } -然后,您可以使用以下方式更新您的 `config.py`: +然后更新 `config.py`: -{* ../../docs_src/settings/app03/config.py hl[9:10] *} +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} -在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。 +/// tip | 提示 -/// tip - -`Config` 类仅用于 Pydantic 配置。您可以在Pydantic Model Config中阅读更多相关信息。 +`model_config` 属性仅用于 Pydantic 配置。你可以在 Pydantic: Concepts: Configuration 中阅读更多信息。 /// -### 使用 `lru_cache` 仅创建一次 `Settings` +这里我们在你的 Pydantic `Settings` 类中定义配置项 `env_file`,并将其设置为我们想要使用的 dotenv 文件名。 -从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。 +### 使用 `lru_cache` 仅创建一次 `Settings` { #creating-the-settings-only-once-with-lru-cache } -但是,每次执行以下操作: +从磁盘读取文件通常是一个代价较高(缓慢)的操作,所以你可能希望只在第一次读取,然后复用同一个设置对象,而不是为每个请求都重新读取。 + +但是,每次我们执行: ```Python Settings() @@ -311,35 +215,36 @@ Settings() 都会创建一个新的 `Settings` 对象,并且在创建时会再次读取 `.env` 文件。 -如果依赖项函数只是这样的: +如果依赖项函数是这样的: ```Python def get_settings(): return Settings() ``` -我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️ +我们就会为每个请求创建该对象,并为每个请求读取 `.env` 文件。 ⚠️ -但是,由于我们在顶部使用了 `@lru_cache` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ +但由于我们在顶部使用了 `@lru_cache` 装饰器,`Settings` 对象只会在第一次调用时创建一次。 ✔️ {* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} -然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。 +接着,对于后续请求中依赖项里对 `get_settings()` 的任何调用,它不会再次执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是会一遍又一遍地返回第一次调用时返回的那个相同对象。 -#### `lru_cache` 技术细节 +#### `lru_cache` 技术细节 { #lru-cache-technical-details } -`@lru_cache` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 +`@lru_cache` 会修改它所装饰的函数,使其返回第一次返回的相同值,而不是每次都重新计算并执行函数代码。 -因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。 +因此,下面的函数会针对每个参数组合执行一次。然后,当以完全相同的参数组合调用该函数时,将重复使用该参数组合先前返回的值。 + +例如,如果你有一个函数: -例如,如果您有一个函数: ```Python @lru_cache def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` -您的程序可以像这样执行: +你的程序可能会像这样执行: ```mermaid sequenceDiagram @@ -382,16 +287,16 @@ participant execute as Execute function end ``` -对于我们的依赖项 `get_settings()`,该函数甚至不接受任何参数,因此它始终返回相同的值。 +在我们的依赖项 `get_settings()` 的情况下,该函数甚至不接受任何参数,因此它始终返回相同的值。 -这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。 +这样,它的行为几乎就像是一个全局变量。但由于它使用了依赖项函数,我们可以在测试时很容易地覆盖它。 -`@lru_cache` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在Python 文档中了解有关 `@lru_cache` 的更多信息。 +`@lru_cache` 是 `functools` 的一部分,它属于 Python 标准库。你可以在 Python 文档中关于 `@lru_cache` 的章节阅读更多信息。 -## 小结 +## 小结 { #recap } -您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。 +你可以使用 Pydantic Settings 来处理应用的设置或配置,享受 Pydantic 模型的全部能力。 -* 通过使用依赖项,您可以简化测试。 -* 您可以使用 `.env` 文件。 -* 使用 `@lru_cache` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 +- 通过使用依赖项,你可以简化测试。 +- 你可以与它一起使用 `.env` 文件。 +- 使用 `@lru_cache` 可以避免为每个请求反复读取 dotenv 文件,同时允许你在测试时进行覆盖。 diff --git a/docs/zh/docs/advanced/sub-applications.md b/docs/zh/docs/advanced/sub-applications.md index c42be2849..fe1fcd121 100644 --- a/docs/zh/docs/advanced/sub-applications.md +++ b/docs/zh/docs/advanced/sub-applications.md @@ -1,54 +1,54 @@ -# 子应用 - 挂载 +# 子应用 - 挂载 { #sub-applications-mounts } 如果需要两个独立的 FastAPI 应用,拥有各自独立的 OpenAPI 与文档,则需设置一个主应用,并**挂载**一个(或多个)子应用。 -## 挂载 **FastAPI** 应用 +## 挂载 **FastAPI** 应用 { #mounting-a-fastapi-application } **挂载**是指在特定路径中添加完全**独立**的应用,然后在该路径下使用*路径操作*声明的子应用处理所有事务。 -### 顶层应用 +### 顶层应用 { #top-level-application } 首先,创建主(顶层)**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 应用,但这个应用是被**挂载**的应用: -{* ../../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`。 本例的子应用挂载在 `/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 } -如果主文件是 `main.py`,则用以下 `uvicorn` 命令运行主应用: +现在,使用你的文件运行 `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 文档,只包括其自有的*路径操作*。 -然后查看子应用文档 http://127.0.0.1:8000/subapi/docs。 +然后查看子应用文档 http://127.0.0.1:8000/subapi/docs。 下图显示的是子应用的 API 文档,也是只包括其自有的*路径操作*,所有这些路径操作都在 `/subapi` 子路径前缀下。 @@ -56,7 +56,7 @@ $ uvicorn main:app --reload 两个用户界面都可以正常运行,因为浏览器能够与每个指定的应用或子应用会话。 -### 技术细节:`root_path` +### 技术细节:`root_path` { #technical-details-root-path } 以上述方式挂载子应用时,FastAPI 使用 ASGI 规范中的 `root_path` 机制处理挂载子应用路径之间的通信。 diff --git a/docs/zh/docs/advanced/templates.md b/docs/zh/docs/advanced/templates.md index e627eed98..f2e5c21cf 100644 --- a/docs/zh/docs/advanced/templates.md +++ b/docs/zh/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# 模板 +# 模板 { #templates } **FastAPI** 支持多种模板引擎。 @@ -6,9 +6,9 @@ Flask 等工具使用的 Jinja2 是最用的模板引擎。 在 Starlette 的支持下,**FastAPI** 应用可以直接使用工具轻易地配置 Jinja2。 -## 安装依赖项 +## 安装依赖项 { #install-dependencies } -安装 `jinja2`: +确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,并安装 `jinja2`:
@@ -20,23 +20,23 @@ $ pip install jinja2
-## 使用 `Jinja2Templates` +## 使用 `Jinja2Templates` { #using-jinja2templates } * 导入 `Jinja2Templates` * 创建可复用的 `templates` 对象 * 在返回模板的*路径操作*中声明 `Request` 参数 -* 使用 `templates` 渲染并返回 `TemplateResponse`, 传递模板的名称、request对象以及一个包含多个键值对(用于Jinja2模板)的"context"字典, +* 使用 `templates` 渲染并返回 `TemplateResponse`,传递模板的名称、request 对象以及一个包含多个键值对(用于 Jinja2 模板)的 "context" 字典。 -{* ../../docs_src/templates/tutorial001.py hl[4,11,15:16] *} +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} -/// note | 笔记 +/// note -在FastAPI 0.108.0,Starlette 0.29.0之前,`name`是第一个参数。 -并且,在此之前,`request`对象是作为context的一部分以键值对的形式传递的。 +在 FastAPI 0.108.0,Starlette 0.29.0 之前,`name` 是第一个参数。 +并且,在此之前,`request` 对象是作为 context 的一部分以键值对的形式传递的。 /// -/// tip | 提示 +/// tip 通过声明 `response_class=HTMLResponse`,API 文档就能识别响应的对象是 HTML。 @@ -46,11 +46,11 @@ $ pip install jinja2 您还可以使用 `from starlette.templating import Jinja2Templates`。 -**FastAPI** 的 `fastapi.templating` 只是为开发者提供的快捷方式。实际上,绝大多数可用响应都直接继承自 Starlette。 `Request` 与 `StaticFiles` 也一样。 +**FastAPI** 的 `fastapi.templating` 只是为开发者提供的快捷方式。实际上,绝大多数可用响应都直接继承自 Starlette。`Request` 与 `StaticFiles` 也一样。 /// -## 编写模板 +## 编写模板 { #writing-templates } 编写模板 `templates/item.html`,代码如下: @@ -58,7 +58,7 @@ $ pip install jinja2 {!../../docs_src/templates/templates/item.html!} ``` -### 模板上下文 +### 模板上下文值 { #template-context-values } 在包含如下语句的html中: @@ -70,21 +70,21 @@ Item ID: {{ id }} {% endraw %} -...这将显示你从"context"字典传递的 `id`: +...这将显示你从 "context" 字典传递的 `id`: ```Python {"id": id} ``` -例如。当ID为 `42`时, 会渲染成: +例如。当 ID 为 `42` 时, 会渲染成: ```html Item ID: 42 ``` -### 模板 `url_for` 参数 +### 模板 `url_for` 参数 { #template-url-for-arguments } -你还可以在模板内使用 `url_for()`,其参数与*路径操作函数*的参数相同. +你还可以在模板内使用 `url_for()`,其参数与*路径操作函数*的参数相同。 所以,该部分: @@ -96,30 +96,30 @@ Item ID: 42 {% endraw %} -...将生成一个与处理*路径操作函数* `read_item(id=id)`的URL相同的链接 +...将生成一个与处理*路径操作函数* `read_item(id=id)`的 URL 相同的链接 -例如。当ID为 `42`时, 会渲染成: +例如。当 ID 为 `42` 时, 会渲染成: ```html ``` -## 模板与静态文件 +## 模板与静态文件 { #templates-and-static-files } -你还可以在模板内部将 `url_for()`用于静态文件,例如你挂载的 `name="static"`的 `StaticFiles`。 +你还可以在模板内部将 `url_for()` 用于静态文件,例如你挂载的 `name="static"` 的 `StaticFiles`。 ```jinja hl_lines="4" {!../../docs_src/templates/templates/item.html!} ``` -本例中,它将链接到 `static/styles.css`中的CSS文件: +本例中,它将链接到 `static/styles.css` 中的 CSS 文件: ```CSS hl_lines="4" {!../../docs_src/templates/static/styles.css!} ``` -因为使用了 `StaticFiles`, **FastAPI** 应用会自动提供位于 URL `/static/styles.css`的 CSS 文件。 +因为使用了 `StaticFiles`,**FastAPI** 应用会自动提供位于 URL `/static/styles.css` 的 CSS 文件。 -## 更多说明 +## 更多说明 { #more-details } 包括测试模板等更多详情,请参阅 Starlette 官方文档 - 模板。 diff --git a/docs/zh/docs/advanced/testing-dependencies.md b/docs/zh/docs/advanced/testing-dependencies.md index 8d53a6d49..db0b39483 100644 --- a/docs/zh/docs/advanced/testing-dependencies.md +++ b/docs/zh/docs/advanced/testing-dependencies.md @@ -1,6 +1,6 @@ -# 测试依赖项 +# 使用覆盖测试依赖项 { #testing-dependencies-with-overrides } -## 测试时覆盖依赖项 +## 测试时覆盖依赖项 { #overriding-dependencies-during-testing } 有些场景下,您可能需要在测试时覆盖依赖项。 @@ -8,7 +8,7 @@ 反之,要在测试期间(或只是为某些特定测试)提供只用于测试的依赖项,并使用此依赖项的值替换原有依赖项的值。 -### 用例:外部服务 +### 用例:外部服务 { #use-cases-external-service } 常见实例是调用外部第三方身份验证应用。 @@ -20,7 +20,7 @@ 此时,最好覆盖调用外部验证应用的依赖项,使用返回模拟测试用户的自定义依赖项就可以了。 -### 使用 `app.dependency_overrides` 属性 +### 使用 `app.dependency_overrides` 属性 { #use-the-app-dependency-overrides-attribute } 对于这些用例,**FastAPI** 应用支持 `app.dependency_overrides` 属性,该属性就是**字典**。 @@ -46,6 +46,7 @@ FastAPI 可以覆盖这些位置的依赖项。 app.dependency_overrides = {} ``` + /// tip | 提示 如果只在某些测试时覆盖依赖项,您可以在测试开始时(在测试函数内)设置覆盖依赖项,并在结束时(在测试函数结尾)重置覆盖依赖项。 diff --git a/docs/zh/docs/advanced/testing-events.md b/docs/zh/docs/advanced/testing-events.md index 71b3739c3..221984e90 100644 --- a/docs/zh/docs/advanced/testing-events.md +++ b/docs/zh/docs/advanced/testing-events.md @@ -1,5 +1,11 @@ -# 测试事件:启动 - 关闭 +# 测试事件:lifespan 和 startup - shutdown { #testing-events-lifespan-and-startup-shutdown } -使用 `TestClient` 和 `with` 语句,在测试中运行事件处理器(`startup` 与 `shutdown`)。 +当你需要在测试中运行 `lifespan` 时,可以将 `TestClient` 与 `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] *} + +你可以在[官方 Starlette 文档站点的“在测试中运行 lifespan”](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/zh/docs/advanced/testing-websockets.md b/docs/zh/docs/advanced/testing-websockets.md index b84647a3e..64e1d3005 100644 --- a/docs/zh/docs/advanced/testing-websockets.md +++ b/docs/zh/docs/advanced/testing-websockets.md @@ -1,13 +1,13 @@ -# 测试 WebSockets +# 测试 WebSockets { #testing-websockets } -测试 WebSockets 也使用 `TestClient`。 +你可以使用同一个 `TestClient` 来测试 WebSockets。 -为此,要在 `with` 语句中使用 `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 | 笔记 +/// note | 注意 -更多细节详见 Starlette 官档 - 测试 WebSockets。 +更多细节请查看 Starlette 的文档:测试 WebSockets。 /// diff --git a/docs/zh/docs/advanced/using-request-directly.md b/docs/zh/docs/advanced/using-request-directly.md index a9658c034..64ba8da1b 100644 --- a/docs/zh/docs/advanced/using-request-directly.md +++ b/docs/zh/docs/advanced/using-request-directly.md @@ -1,4 +1,4 @@ -# 直接使用请求 +# 直接使用 Request { #using-the-request-directly } 至此,我们已经使用多种类型声明了请求的各种组件。 @@ -13,23 +13,23 @@ 但有时,我们也需要直接访问 `Request` 对象。 -## `Request` 对象的细节 +## `Request` 对象的细节 { #details-about-the-request-object } 实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 `Request` 对象。 -但直接从 `Request` 对象提取数据时(例如,读取请求体),**FastAPI** 不会验证、转换和存档数据(为 API 文档使用 OpenAPI)。 +但直接从 `Request` 对象提取数据时(例如,读取请求体),这些数据不会被 **FastAPI** 验证、转换或文档化(使用 OpenAPI,为自动的 API 用户界面)。 不过,仍可以验证、转换与注释(使用 Pydantic 模型的请求体等)其它正常声明的参数。 但在某些特定情况下,还是需要提取 `Request` 对象。 -## 直接使用 `Request` 对象 +## 直接使用 `Request` 对象 { #use-the-request-object-directly } 假设要在*路径操作函数*中获取客户端 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` 传递到参数里。 @@ -43,7 +43,7 @@ /// -## `Request` 文档 +## `Request` 文档 { #request-documentation } 更多细节详见 Starlette 官档 - `Request` 对象。 @@ -51,6 +51,6 @@ 您也可以使用 `from starlette.requests import Request`。 -**FastAPI** 的 `from fastapi import Request` 只是为开发者提供的快捷方式,但其实它直接继承自 Starlette。 +**FastAPI** 直接提供它只是为了方便开发者,但它直接来自 Starlette。 /// diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md index 005ed9242..4486a2e69 100644 --- a/docs/zh/docs/advanced/websockets.md +++ b/docs/zh/docs/advanced/websockets.md @@ -1,10 +1,12 @@ -# WebSockets +# WebSockets { #websockets } -您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。 +您可以在 **FastAPI** 中使用 WebSockets。 -## 安装 `WebSockets` +## 安装 `websockets` { #install-websockets } -首先,您需要安装 `WebSockets`: +请确保您创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank}、激活它,并安装 `websockets`(一个让使用“WebSocket”协议更容易的 Python 库): + +
```console $ pip install websockets @@ -12,11 +14,13 @@ $ pip install websockets ---> 100% ``` -## WebSockets 客户端 +
-### 在生产环境中 +## WebSockets 客户端 { #websockets-client } -在您的生产系统中,您可能使用现代框架(如React、Vue.js或Angular)创建了一个前端。 +### 在生产环境中 { #in-production } + +在您的生产系统中,您可能使用现代框架(如 React、Vue.js 或 Angular)创建了一个前端。 要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。 @@ -26,7 +30,7 @@ $ pip install websockets --- -但是,在本示例中,我们将使用一个非常简单的HTML文档,其中包含一些JavaScript,全部放在一个长字符串中。 +但是,在本示例中,我们将使用一个非常简单的 HTML 文档,其中包含一些 JavaScript,全部放在一个长字符串中。 当然,这并不是最优的做法,您不应该在生产环境中使用它。 @@ -34,13 +38,13 @@ $ pip install websockets 但这是一种专注于 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 | 技术细节 @@ -50,24 +54,28 @@ $ 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`,请使用以下命令运行应用程序: +
+ ```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。 您将看到一个简单的页面,如下所示: @@ -86,11 +94,9 @@ $ uvicorn main:app --reload -所有这些消息都将使用同一个 WebSocket 连 +所有这些消息都将使用同一个 WebSocket 连接。 -接。 - -## 使用 `Depends` 和其他依赖项 +## 使用 `Depends` 和其他依赖项 { #using-depends-and-others } 在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容: @@ -101,7 +107,7 @@ $ uvicorn main:app --reload * `Path` * `Query` -它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同: +它们的工作方式与其他 FastAPI 端点/*路径操作* 相同: {* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} @@ -113,16 +119,20 @@ $ uvicorn main:app --reload /// -### 尝试带有依赖项的 WebSockets +### 尝试带有依赖项的 WebSockets { #try-the-websockets-with-dependencies } 如果您的文件名为 `main.py`,请使用以下命令运行应用程序: +
+ ```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。 在页面中,您可以设置: @@ -140,7 +150,7 @@ $ uvicorn main:app --reload -## 处理断开连接和多个客户端 +## 处理断开连接和多个客户端 { #handling-disconnections-and-multiple-clients } 当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。 @@ -164,13 +174,13 @@ Client #1596980209979 left the chat 但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。 -如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。 +如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 encode/broadcaster。 /// -## 更多信息 +## 更多信息 { #more-info } 要了解更多选项,请查看 Starlette 的文档: -* [WebSocket 类](https://www.starlette.dev/websockets/) -* [基于类的 WebSocket 处理](https://www.starlette.dev/endpoints/#websocketendpoint)。 +* `WebSocket` 类。 +* 基于类的 WebSocket 处理。 diff --git a/docs/zh/docs/advanced/wsgi.md b/docs/zh/docs/advanced/wsgi.md index 363025a34..73fcb3219 100644 --- a/docs/zh/docs/advanced/wsgi.md +++ b/docs/zh/docs/advanced/wsgi.md @@ -1,32 +1,48 @@ -# 包含 WSGI - Flask,Django,其它 +# 包含 WSGI - Flask,Django,其它 { #including-wsgi-flask-django-others } -您可以挂载多个 WSGI 应用,正如您在 [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} 中所看到的那样。 +您可以挂载 WSGI 应用,正如您在 [子应用 - 挂载](sub-applications.md){.internal-link target=_blank}、[在代理之后](behind-a-proxy.md){.internal-link target=_blank} 中所看到的那样。 为此, 您可以使用 `WSGIMiddleware` 来包装你的 WSGI 应用,如:Flask,Django,等等。 -## 使用 `WSGIMiddleware` +## 使用 `WSGIMiddleware` { #using-wsgimiddleware } -您需要导入 `WSGIMiddleware`。 +/// info | 信息 + +需要安装 `a2wsgi`,例如使用 `pip install a2wsgi`。 + +/// + +您需要从 `a2wsgi` 导入 `WSGIMiddleware`。 然后使用该中间件包装 WSGI 应用(例如 Flask)。 之后将其挂载到某一个路径下。 -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,22] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} -## 检查 +/// note | 注意 + +之前推荐使用 `fastapi.middleware.wsgi` 中的 `WSGIMiddleware`,但它现在已被弃用。 + +建议改用 `a2wsgi` 包,使用方式保持不变。 + +只要确保已安装 `a2wsgi` 包,并且从 `a2wsgi` 正确导入 `WSGIMiddleware` 即可。 + +/// + +## 检查 { #check-it } 现在,所有定义在 `/v1/` 路径下的请求将会被 Flask 应用处理。 其余的请求则会被 **FastAPI** 处理。 -如果您使用 Uvicorn 运行应用实例并且访问 http://localhost:8000/v1/,您将会看到由 Flask 返回的响应: +如果你运行它并访问 http://localhost:8000/v1/,你将会看到由 Flask 返回的响应: ```txt Hello, World from Flask! ``` -并且如果您访问 http://localhost:8000/v2,您将会看到由 FastAPI 返回的响应: +如果你访问 http://localhost:8000/v2,你将会看到由 FastAPI 返回的响应: ```JSON { diff --git a/docs/zh/docs/async.md b/docs/zh/docs/async.md index 4028ed51a..c94c90787 100644 --- a/docs/zh/docs/async.md +++ b/docs/zh/docs/async.md @@ -1,10 +1,10 @@ -# 并发 async / await +# 并发 async / await { #concurrency-and-async-await } 有关路径操作函数的 `async def` 语法以及异步代码、并发和并行的一些背景知识。 -## 赶时间吗? +## 赶时间吗? { #in-a-hurry } -TL;DR: +TL;DR: 如果你正在使用第三方库,它们会告诉你使用 `await` 关键字来调用它们,就像这样: @@ -21,7 +21,7 @@ async def read_results(): return results ``` -/// note +/// note | 注意 你只能在被 `async def` 创建的函数内使用 `await` @@ -40,7 +40,7 @@ def results(): --- -如果你的应用程序不需要与其他任何东西通信而等待其响应,请使用 `async def`。 +如果你的应用程序不需要与其他任何东西通信而等待其响应,请使用 `async def`,即使函数内部不需要使用 `await`。 --- @@ -54,7 +54,7 @@ def results(): 但是,通过遵循上述步骤,它将能够进行一些性能优化。 -## 技术细节 +## 技术细节 { #technical-details } Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 `await` 语法的东西来写**”异步代码“**。 @@ -64,7 +64,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 * **`async` 和 `await`** * **协程** -## 异步代码 +## 异步代码 { #asynchronous-code } 异步代码仅仅意味着编程语言 💬 有办法告诉计算机/程序 🤖 在代码中的某个点,它 🤖 将不得不等待在某些地方完成一些事情。让我们假设一些事情被称为 "慢文件"📝. @@ -74,7 +74,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 接下来,它 🤖 完成第一个任务(比如是我们的"慢文件"📝) 并继续与之相关的一切。 -这个"等待其他事情"通常指的是一些相对较慢(与处理器和 RAM 存储器的速度相比)的 I/O 操作,比如说: +这个"等待其他事情"通常指的是一些相对较慢(与处理器和 RAM 存储器的速度相比)的 I/O 操作,比如说: * 通过网络发送来自客户端的数据 * 客户端接收来自网络中的数据 @@ -85,7 +85,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 * 一个数据库查询,直到返回结果 * 等等. -这个执行的时间大多是在等待 I/O 操作,因此它们被叫做 "I/O 密集型" 操作。 +这个执行的时间大多是在等待 I/O 操作,因此它们被叫做 "I/O 密集型" 操作。 它被称为"异步"的原因是因为计算机/程序不必与慢任务"同步",去等待任务完成的确切时刻,而在此期间不做任何事情直到能够获取任务结果才继续工作。 @@ -93,7 +93,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 对于"同步"(与"异步"相反),他们通常也使用"顺序"一词,因为计算机程序在切换到另一个任务之前是按顺序执行所有步骤,即使这些步骤涉及到等待。 -### 并发与汉堡 +### 并发与汉堡 { #concurrency-and-burgers } 上述异步代码的思想有时也被称为“并发”,它不同于“并行”。 @@ -103,7 +103,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 要了解差异,请想象以下关于汉堡的故事: -### 并发汉堡 +### 并发汉堡 { #concurrent-burgers } 你和你的恋人一起去快餐店,你排队在后面,收银员从你前面的人接单。😍 @@ -139,7 +139,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 -/// info +/// info | 信息 漂亮的插画来自 Ketrina Thompson. 🎨 @@ -163,7 +163,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 然后你去柜台🔀, 到现在初始任务已经完成⏯, 拿起汉堡,说声谢谢,然后把它们送到桌上。这就完成了与计数器交互的步骤/任务⏹. 这反过来又产生了一项新任务,即"吃汉堡"🔀 ⏯, 上一个"拿汉堡"的任务已经结束了⏹. -### 并行汉堡 +### 并行汉堡 { #parallel-burgers } 现在让我们假设不是"并发汉堡",而是"并行汉堡"。 @@ -205,7 +205,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 没有太多的交谈或调情,因为大部分时间 🕙 都在柜台前等待😞。 -/// info +/// info | 信息 漂亮的插画来自 Ketrina Thompson. 🎨 @@ -233,7 +233,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 你可不会想带你的恋人 😍 和你一起去银行办事🏦. -### 汉堡结论 +### 汉堡结论 { #burger-conclusion } 在"你与恋人一起吃汉堡"的这个场景中,因为有很多人在等待🕙, 使用并发系统更有意义⏸🔀⏯. @@ -253,7 +253,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 你可以同时拥有并行性和异步性,你可以获得比大多数经过测试的 NodeJS 框架更高的性能,并且与 Go 不相上下, Go 是一种更接近于 C 的编译语言(全部归功于 Starlette)。 -### 并发比并行好吗? +### 并发比并行好吗? { #is-concurrency-better-than-parallelism } 不!这不是故事的本意。 @@ -277,7 +277,7 @@ Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 在这个场景中,每个清洁工(包括你)都将是一个处理器,完成这个工作的一部分。 -由于大多数执行时间是由实际工作(而不是等待)占用的,并且计算机中的工作是由 CPU 完成的,所以他们称这些问题为"CPU 密集型"。 +由于大多数执行时间是由实际工作(而不是等待)占用的,并且计算机中的工作是由 CPU 完成的,所以他们称这些问题为"CPU 密集型"。 --- @@ -290,7 +290,7 @@ CPU 密集型操作的常见示例是需要复杂的数学处理。 * **机器学习**: 它通常需要大量的"矩阵"和"向量"乘法。想象一个包含数字的巨大电子表格,并同时将所有数字相乘; * **深度学习**: 这是机器学习的一个子领域,同样适用。只是没有一个数字的电子表格可以相乘,而是一个庞大的数字集合,在很多情况下,你需要使用一个特殊的处理器来构建和使用这些模型。 -### 并发 + 并行: Web + 机器学习 +### 并发 + 并行: Web + 机器学习 { #concurrency-parallelism-web-machine-learning } 使用 **FastAPI**,你可以利用 Web 开发中常见的并发机制的优势(NodeJS 的主要吸引力)。 @@ -300,7 +300,7 @@ CPU 密集型操作的常见示例是需要复杂的数学处理。 了解如何在生产环境中实现这种并行性,可查看此文 [Deployment](deployment/index.md){.internal-link target=_blank}。 -## `async` 和 `await` +## `async` 和 `await` { #async-and-await } 现代版本的 Python 有一种非常直观的方式来定义异步代码。这使它看起来就像正常的"顺序"代码,并在适当的时候"等待"。 @@ -316,16 +316,16 @@ burgers = await get_burgers(2) ```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 ``` @@ -334,7 +334,7 @@ def get_sequential_burgers(number: int): 当你想调用一个 `async def` 函数时,你必须"等待"它。因此,这不会起作用: ```Python -# This won't work, because get_burgers was defined with: async def +# 这样不行,因为 get_burgers 是用 async def 定义的 burgers = get_burgers(2) ``` @@ -349,7 +349,7 @@ async def read_burgers(): return burgers ``` -### 更多技术细节 +### 更多技术细节 { #more-technical-details } 你可能已经注意到,`await` 只能在 `async def` 定义的函数内部使用。 @@ -361,7 +361,7 @@ async def read_burgers(): 但如果你想在没有 FastAPI 的情况下使用 `async` / `await`,则可以这样做。 -### 编写自己的异步代码 +### 编写自己的异步代码 { #write-your-own-async-code } Starlette (和 **FastAPI**) 是基于 AnyIO 实现的,这使得它们可以兼容 Python 的标准库 asyncioTrio。 @@ -371,7 +371,7 @@ Starlette (和 **FastAPI**) 是基于 Asyncer。如果你有**结合使用异步代码和常规**(阻塞/同步)代码的需求,这个库会特别有用。 -### 其他形式的异步代码 +### 其他形式的异步代码 { #other-forms-of-asynchronous-code } 这种使用 `async` 和 `await` 的风格在语言中相对较新。 @@ -385,13 +385,13 @@ Starlette (和 **FastAPI**) 是基于 I/O 的代码。 +如果你使用过另一个不以上述方式工作的异步框架,并且你习惯于用普通的 `def` 定义普通的仅计算路径操作函数,以获得微小的性能增益(大约100纳秒),请注意,在 FastAPI 中,效果将完全相反。在这些情况下,最好使用 `async def`,除非路径操作函数内使用执行阻塞 I/O 的代码。 -在这两种情况下,与你之前的框架相比,**FastAPI** 可能[仍然很快](index.md#_11){.internal-link target=_blank}。 +在这两种情况下,与你之前的框架相比,**FastAPI** 可能[仍然很快](index.md#performance){.internal-link target=_blank}。 -### 依赖 +### 依赖 { #dependencies } 这同样适用于[依赖](tutorial/dependencies/index.md){.internal-link target=_blank}。如果一个依赖是标准的 `def` 函数而不是 `async def`,它将被运行在外部线程池中。 -### 子依赖 +### 子依赖 { #sub-dependencies } 你可以拥有多个相互依赖的依赖以及[子依赖](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作为函数的参数),它们中的一些可能是通过 `async def` 声明,也可能是通过 `def` 声明。它们仍然可以正常工作,这些通过 `def` 声明的函数将会在外部线程中调用(来自线程池),而不是"被等待"。 -### 其他函数 +### 其他函数 { #other-utility-functions } 你可直接调用通过 `def` 或 `async def` 创建的任何其他函数,FastAPI 不会影响你调用它们的方式。 @@ -441,4 +441,4 @@ Starlette (和 **FastAPI**) 是基于 赶时间吗?. +否则,你最好应该遵守的指导原则赶时间吗?. diff --git a/docs/zh/docs/benchmarks.md b/docs/zh/docs/benchmarks.md index 71e8d4838..1a4b4a3de 100644 --- a/docs/zh/docs/benchmarks.md +++ b/docs/zh/docs/benchmarks.md @@ -1,10 +1,10 @@ -# 基准测试 +# 基准测试 { #benchmarks } -第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次于 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*) +第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次于 Starlette 和 Uvicorn 本身(由 FastAPI 内部使用)。 但是在查看基准得分和对比时,请注意以下几点。 -## 基准测试和速度 +## 基准测试和速度 { #benchmarks-and-speed } 当你查看基准测试时,几个不同类型的工具被等效地做比较是很常见的情况。 @@ -20,15 +20,15 @@ * **Uvicorn**: * 具有最佳性能,因为除了服务器本身外,它没有太多额外的代码。 - * 您不会直接在 Uvicorn 中编写应用程序。这意味着您的代码至少必须包含 Starlette(或 **FastAPI**)提供的代码。如果您这样做了(即直接在 Uvicorn 中编写应用程序),最终的应用程序会和使用了框架并且最小化了应用代码和 bug 的情况具有相同的性能损耗。 - * 如果要对比与 Uvicorn 对标的服务器,请将其与 Daphne,Hypercorn,uWSGI等应用服务器进行比较。 + * 你不会直接在 Uvicorn 中编写应用程序。这意味着你的代码至少必须包含 Starlette(或 **FastAPI**)提供的代码。如果你这样做了(即直接在 Uvicorn 中编写应用程序),最终的应用程序会和使用了框架并且最小化了应用代码和 bug 的情况具有相同的性能损耗。 + * 如果你要对比 Uvicorn,请将其与 Daphne,Hypercorn,uWSGI 等应用服务器进行比较。 * **Starlette**: - * 在 Uvicorn 后使用 Starlette,性能会略有下降。实际上,Starlette 使用 Uvicorn运行。因此,由于必须执行更多的代码,它只会比 Uvicorn 更慢。 - * 但它为您提供了构建简单的网络程序的工具,并具有基于路径的路由等功能。 + * 在 Uvicorn 后使用 Starlette,性能会略有下降。实际上,Starlette 使用 Uvicorn 运行。因此,由于必须执行更多的代码,它只会比 Uvicorn 更慢。 + * 但它为你提供了构建简单的网络程序的工具,并具有基于路径的路由等功能。 * 如果想对比与 Starlette 对标的开发框架,请将其与 Sanic,Flask,Django 等网络框架(或微框架)进行比较。 * **FastAPI**: * 与 Starlette 使用 Uvicorn 一样,由于 **FastAPI** 使用 Starlette,因此 FastAPI 不能比 Starlette 更快。 - * FastAPI 在 Starlette 基础上提供了更多功能。例如在开发 API 时,所需的数据验证和序列化功能。FastAPI 可以帮助您自动生成 API文档,(文档在应用程序启动时自动生成,所以不会增加应用程序运行时的开销)。 - * 如果您不使用 FastAPI 而直接使用 Starlette(或诸如 Sanic,Flask,Responder 等其它工具),您则要自己实现所有的数据验证和序列化。那么最终您的应用程序会和使用 FastAPI 构建的程序有相同的开销。一般这种数据验证和序列化的操作在您应用程序的代码中会占很大比重。 - * 因此,通过使用 FastAPI 意味着您可以节省开发时间,减少编码错误,用更少的编码实现其功能,并且相比不使用 FastAPI 您很大可能会获得相同或更好的性能(因为那样您必须在代码中实现所有相同的功能)。 - * 如果您想对比与 FastAPI 对标的开发框架,请与能够提供数据验证,序列化和带有自动文档生成的网络应用程序框架(或工具集)进行对比,例如具有集成自动数据验证,序列化和自动化文档的 Flask-apispec,NestJS,Molten 等。 + * FastAPI 在 Starlette 基础上提供了更多功能。例如在开发 API 时,所需的数据验证和序列化功能。FastAPI 可以帮助你自动生成 API文档,(文档在应用程序启动时自动生成,所以不会增加应用程序运行时的开销)。 + * 如果你不使用 FastAPI 而直接使用 Starlette(或诸如 Sanic,Flask,Responder 等其它工具),你则要自己实现所有的数据验证和序列化。那么最终你的应用程序会和使用 FastAPI 构建的程序有相同的开销。一般这种数据验证和序列化的操作在你应用程序的代码中会占很大比重。 + * 因此,通过使用 FastAPI 意味着你可以节省开发时间,减少编码错误,用更少的编码实现其功能,并且相比不使用 FastAPI 你很大可能会获得相同或更好的性能(因为那样你必须在代码中实现所有相同的功能)。 + * 如果你想对比 FastAPI,请与能够提供数据验证、序列化和文档的网络应用程序框架(或工具集)进行对比,例如具有集成自动数据验证、序列化和自动化文档的 Flask-apispec,NestJS,Molten 等。 diff --git a/docs/zh/docs/deployment/cloud.md b/docs/zh/docs/deployment/cloud.md index 8a892a560..96883bd6b 100644 --- a/docs/zh/docs/deployment/cloud.md +++ b/docs/zh/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# 在云上部署 FastAPI +# 在云服务商上部署 FastAPI { #deploy-fastapi-on-cloud-providers } -您几乎可以使用**任何云服务商**来部署 FastAPI 应用程序。 +你几乎可以使用**任何云服务商**来部署你的 FastAPI 应用。 -在大多数情况下,主要的云服务商都有部署 FastAPI 的指南。 +在大多数情况下,主流云服务商都有部署 FastAPI 的指南。 -## 云服务商 - 赞助商 +## FastAPI Cloud { #fastapi-cloud } -一些云服务商 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,这确保了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/zh/docs/deployment/concepts.md b/docs/zh/docs/deployment/concepts.md index f7208da7c..66d32629c 100644 --- a/docs/zh/docs/deployment/concepts.md +++ b/docs/zh/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# 部署概念 +# 部署概念 { #deployments-concepts } 在部署 **FastAPI** 应用程序或任何类型的 Web API 时,有几个概念值得了解,通过掌握这些概念您可以找到**最合适的**方法来**部署您的应用程序**。 @@ -13,7 +13,7 @@ 我们接下来了解它们将如何影响**部署**。 -我们的最终目标是能够以**安全**的方式**为您的 API 客户端**提供服务,同时要**避免中断**,并且尽可能高效地利用**计算资源**( 例如服务器CPU资源)。 🚀 +我们的最终目标是能够以**安全**的方式**为您的 API 客户端**提供服务,同时要**避免中断**,并且尽可能高效地利用**计算资源**(例如远程服务器/虚拟机)。 🚀 我将在这里告诉您更多关于这些**概念**的信息,希望能给您提供**直觉**来决定如何在非常不同的环境中部署 API,甚至在是尚不存在的**未来**的环境里。 @@ -23,7 +23,7 @@ 但现在,让我们仔细看一下这些重要的**概念**。 这些概念也适用于任何其他类型的 Web API。 💡 -## 安全性 - HTTPS +## 安全性 - HTTPS { #security-https } 在[上一章有关 HTTPS](https.md){.internal-link target=_blank} 中,我们了解了 HTTPS 如何为您的 API 提供加密。 @@ -31,21 +31,20 @@ 并且必须有某个东西负责**更新 HTTPS 证书**,它可以是相同的组件,也可以是不同的组件。 - -### HTTPS 示例工具 +### HTTPS 示例工具 { #example-tools-for-https } 您可以用作 TLS 终止代理的一些工具包括: * Traefik - * 自动处理证书更新 ✨ + * 自动处理证书更新 ✨ * Caddy - * 自动处理证书更新 ✨ + * 自动处理证书更新 ✨ * Nginx - * 使用 Certbot 等外部组件进行证书更新 + * 使用 Certbot 等外部组件进行证书更新 * HAProxy - * 使用 Certbot 等外部组件进行证书更新 -* 带有 Ingress Controller(如Nginx) 的 Kubernetes - * 使用诸如 cert-manager 之类的外部组件来进行证书更新 + * 使用 Certbot 等外部组件进行证书更新 +* 带有 Ingress Controller(如 Nginx) 的 Kubernetes + * 使用诸如 cert-manager 之类的外部组件来进行证书更新 * 由云服务商内部处理,作为其服务的一部分(请阅读下文👇) 另一种选择是您可以使用**云服务**来完成更多工作,包括设置 HTTPS。 它可能有一些限制或向您收取更多费用等。但在这种情况下,您不必自己设置 TLS 终止代理。 @@ -56,11 +55,11 @@ 接下来要考虑的概念都是关于运行实际 API 的程序(例如 Uvicorn)。 -## 程序和进程 +## 程序和进程 { #program-and-process } 我们将讨论很多关于正在运行的“**进程**”的内容,因此弄清楚它的含义以及与“**程序**”这个词有什么区别是很有用的。 -### 什么是程序 +### 什么是程序 { #what-is-a-program } **程序**这个词通常用来描述很多东西: @@ -68,12 +67,12 @@ * 操作系统可以**执行**的**文件**,例如:`python`、`python.exe`或`uvicorn`。 * 在操作系统上**运行**、使用CPU 并将内容存储在内存上的特定程序。 这也被称为**进程**。 -### 什么是进程 +### 什么是进程 { #what-is-a-process } **进程** 这个词通常以更具体的方式使用,仅指在操作系统中运行的东西(如上面的最后一点): * 在操作系统上**运行**的特定程序。 - * 这不是指文件,也不是指代码,它**具体**指的是操作系统正在**执行**和管理的东西。 + * 这不是指文件,也不是指代码,它**具体**指的是操作系统正在**执行**和管理的东西。 * 任何程序,任何代码,**只有在执行时才能做事**。 因此,是当有**进程正在运行**时。 * 该进程可以由您或操作系统**终止**(或“杀死”)。 那时,它停止运行/被执行,并且它可以**不再做事情**。 * 您计算机上运行的每个应用程序背后都有一些进程,每个正在运行的程序,每个窗口等。并且通常在计算机打开时**同时**运行许多进程。 @@ -89,13 +88,13 @@ 现在我们知道了术语“进程”和“程序”之间的区别,让我们继续讨论部署。 -## 启动时运行 +## 启动时运行 { #running-on-startup } 在大多数情况下,当您创建 Web API 时,您希望它**始终运行**、不间断,以便您的客户端始终可以访问它。 这是当然的,除非您有特定原因希望它仅在某些情况下运行,但大多数时候您希望它不断运行并且**可用**。 -### 在远程服务器中 +### 在远程服务器中 { #in-a-remote-server } -当您设置远程服务器(云服务器、虚拟机等)时,您可以做的最简单的事情就是手动运行 Uvicorn(或类似的),就像本地开发时一样。 +当您设置远程服务器(云服务器、虚拟机等)时,您可以做的最简单的事情就是使用 `fastapi run`(它使用 Uvicorn)或类似方式,手动运行,就像本地开发时一样。 它将会在**开发过程中**发挥作用并发挥作用。 @@ -103,16 +102,15 @@ 如果服务器重新启动(例如更新后或从云提供商迁移后),您可能**不会注意到它**。 因此,您甚至不知道必须手动重新启动该进程。 所以,你的 API 将一直处于挂掉的状态。 😱 - -### 启动时自动运行 +### 启动时自动运行 { #run-automatically-on-startup } 一般来说,您可能希望服务器程序(例如 Uvicorn)在服务器启动时自动启动,并且不需要任何**人为干预**,让进程始终与您的 API 一起运行(例如 Uvicorn 运行您的 FastAPI 应用程序) 。 -### 单独的程序 +### 单独的程序 { #separate-program } 为了实现这一点,您通常会有一个**单独的程序**来确保您的应用程序在启动时运行。 在许多情况下,它还可以确保其他组件或应用程序也运行,例如数据库。 -### 启动时运行的示例工具 +### 启动时运行的示例工具 { #example-tools-to-run-at-startup } 可以完成这项工作的工具的一些示例是: @@ -127,44 +125,43 @@ 我将在接下来的章节中为您提供更具体的示例。 - -## 重新启动 +## 重新启动 { #restarts } 与确保应用程序在启动时运行类似,您可能还想确保它在挂掉后**重新启动**。 -### 我们会犯错误 +### 我们会犯错误 { #we-make-mistakes } 作为人类,我们总是会犯**错误**。 软件几乎*总是*在不同的地方隐藏着**bug**。 🐛 作为开发人员,当我们发现这些bug并实现新功能(也可能添加新bug😅)时,我们会不断改进代码。 -### 自动处理小错误 +### 自动处理小错误 { #small-errors-automatically-handled } 使用 FastAPI 构建 Web API 时,如果我们的代码中存在错误,FastAPI 通常会将其包含到触发错误的单个请求中。 🛡 对于该请求,客户端将收到 **500 内部服务器错误**,但应用程序将继续处理下一个请求,而不是完全崩溃。 -### 更大的错误 - 崩溃 +### 更大的错误 - 崩溃 { #bigger-errors-crashes } 尽管如此,在某些情况下,我们编写的一些代码可能会导致整个应用程序崩溃,从而导致 Uvicorn 和 Python 崩溃。 💥 尽管如此,您可能不希望应用程序因为某个地方出现错误而保持死机状态,您可能希望它**继续运行**,至少对于未破坏的*路径操作*。 -### 崩溃后重新启动 +### 崩溃后重新启动 { #restart-after-crash } 但在那些严重错误导致正在运行的**进程**崩溃的情况下,您需要一个外部组件来负责**重新启动**进程,至少尝试几次...... -/// tip +/// tip | 提示 ...尽管如果整个应用程序只是**立即崩溃**,那么永远重新启动它可能没有意义。 但在这些情况下,您可能会在开发过程中注意到它,或者至少在部署后立即注意到它。 - 因此,让我们关注主要情况,在**未来**的某些特定情况下,它可能会完全崩溃,但重新启动它仍然有意义。 +因此,让我们关注主要情况,在**未来**的某些特定情况下,它可能会完全崩溃,但重新启动它仍然有意义。 /// 您可能希望让这个东西作为 **外部组件** 负责重新启动您的应用程序,因为到那时,使用 Uvicorn 和 Python 的同一应用程序已经崩溃了,因此同一应用程序的相同代码中没有东西可以对此做出什么。 -### 自动重新启动的示例工具 +### 自动重新启动的示例工具 { #example-tools-to-restart-automatically } 在大多数情况下,用于**启动时运行程序**的同一工具也用于处理自动**重新启动**。 @@ -173,25 +170,25 @@ * Docker * Kubernetes * Docker Compose -* Docker in Swarm mode +* Docker in Swarm Mode * Systemd * Supervisor * 作为其服务的一部分由云提供商内部处理 * 其他的... -## 复制 - 进程和内存 +## 复制 - 进程和内存 { #replication-processes-and-memory } -对于 FastAPI 应用程序,使用像 Uvicorn 这样的服务器程序,在**一个进程**中运行一次就可以同时为多个客户端提供服务。 +对于 FastAPI 应用程序,使用像 `fastapi` 命令(运行 Uvicorn)这样的服务器程序,在**一个进程**中运行一次就可以同时为多个客户端提供服务。 但在许多情况下,您会希望同时运行多个工作进程。 -### 多进程 - Workers +### 多进程 - Workers { #multiple-processes-workers } -如果您的客户端数量多于单个进程可以处理的数量(例如,如果虚拟机不是太大),并且服务器的 CPU 中有 **多个核心**,那么您可以让 **多个进程** 运行 同时处理同一个应用程序,并在它们之间分发所有请求。 +如果您的客户端数量多于单个进程可以处理的数量(例如,如果虚拟机不是太大),并且服务器的 CPU 中有 **多个核心**,那么您可以让 **多个进程** 同时运行同一个应用程序,并在它们之间分发所有请求。 当您运行同一 API 程序的**多个进程**时,它们通常称为 **workers**。 -### 工作进程和端口 +### 工作进程和端口 { #worker-processes-and-ports } 还记得文档 [About HTTPS](https.md){.internal-link target=_blank} 中只有一个进程可以侦听服务器中的端口和 IP 地址的一种组合吗? @@ -199,20 +196,19 @@ 因此,为了能够同时拥有**多个进程**,必须有一个**单个进程侦听端口**,然后以某种方式将通信传输到每个工作进程。 -### 每个进程的内存 +### 每个进程的内存 { #memory-per-process } 现在,当程序将内容加载到内存中时,例如,将机器学习模型加载到变量中,或者将大文件的内容加载到变量中,所有这些都会消耗服务器的一点内存 (RAM) 。 多个进程通常**不共享任何内存**。 这意味着每个正在运行的进程都有自己的东西、变量和内存。 如果您的代码消耗了大量内存,**每个进程**将消耗等量的内存。 -### 服务器内存 +### 服务器内存 { #server-memory } 例如,如果您的代码加载 **1 GB 大小**的机器学习模型,则当您使用 API 运行一个进程时,它将至少消耗 1 GB RAM。 如果您启动 **4 个进程**(4 个工作进程),每个进程将消耗 1 GB RAM。 因此,您的 API 总共将消耗 **4 GB RAM**。 如果您的远程服务器或虚拟机只有 3 GB RAM,尝试加载超过 4 GB RAM 将导致问题。 🚨 - -### 多进程 - 一个例子 +### 多进程 - 一个例子 { #multiple-processes-an-example } 在此示例中,有一个 **Manager Process** 启动并控制两个 **Worker Processes**。 @@ -224,11 +220,11 @@ 当然,除了您的应用程序之外,同一台机器可能还运行**其他进程**。 -一个有趣的细节是,随着时间的推移,每个进程使用的 **CPU 百分比可能会发生很大变化,但内存 (RAM) 通常会或多或少保持稳定**。 +一个有趣的细节是,随着时间的推移,每个进程使用的 **CPU 百分比**可能会发生很大变化,但**内存 (RAM)** 通常会或多或少保持**稳定**。 如果您有一个每次执行相当数量的计算的 API,并且您有很多客户端,那么 **CPU 利用率** 可能也会保持稳定(而不是不断快速上升和下降)。 -### 复制工具和策略示例 +### 复制工具和策略示例 { #examples-of-replication-tools-and-strategies } 可以通过多种方法来实现这一目标,我将在接下来的章节中向您详细介绍具体策略,例如在谈论 Docker 和容器时。 @@ -236,26 +232,22 @@ 以下是一些可能的组合和策略: -* **Gunicorn** 管理 **Uvicorn workers** - * Gunicorn 将是监听 **IP** 和 **端口** 的 **进程管理器**,复制将通过 **多个 Uvicorn 工作进程** 进行 -* **Uvicorn** 管理 **Uvicorn workers** - * 一个 Uvicorn **进程管理器** 将监听 **IP** 和 **端口**,并且它将启动 **多个 Uvicorn 工作进程** +* 带有 `--workers` 的 **Uvicorn** + * 一个 Uvicorn **进程管理器** 将监听 **IP** 和 **端口**,并且它将启动 **多个 Uvicorn 工作进程**。 * **Kubernetes** 和其他分布式 **容器系统** - * **Kubernetes** 层中的某些东西将侦听 **IP** 和 **端口**。 复制将通过拥有**多个容器**,每个容器运行**一个 Uvicorn 进程** + * **Kubernetes** 层中的某些东西将侦听 **IP** 和 **端口**。 复制将通过拥有**多个容器**,每个容器运行**一个 Uvicorn 进程**。 * **云服务** 为您处理此问题 - * 云服务可能**为您处理复制**。 它可能会让您定义 **要运行的进程**,或要使用的 **容器映像**,在任何情况下,它很可能是 **单个 Uvicorn 进程**,并且云服务将负责复制它。 + * 云服务可能**为您处理复制**。 它可能会让您定义 **要运行的进程**,或要使用的 **容器映像**,在任何情况下,它很可能是 **单个 Uvicorn 进程**,并且云服务将负责复制它。 - - -/// tip +/// tip | 提示 如果这些关于 **容器**、Docker 或 Kubernetes 的内容还没有多大意义,请不要担心。 - 我将在以后的章节中向您详细介绍容器镜像、Docker、Kubernetes 等:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 +我将在以后的章节中向您详细介绍容器镜像、Docker、Kubernetes 等:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 /// -## 启动之前的步骤 +## 启动之前的步骤 { #previous-steps-before-starting } 在很多情况下,您希望在**启动**应用程序之前执行一些步骤。 @@ -269,15 +261,15 @@ 当然,也有一些情况,多次运行前面的步骤也没有问题,这样的话就好办多了。 -/// tip +/// tip | 提示 另外,请记住,根据您的设置,在某些情况下,您在开始应用程序之前**可能甚至不需要任何先前的步骤**。 - 在这种情况下,您就不必担心这些。 🤷 +在这种情况下,您就不必担心这些。 🤷 /// -### 前面步骤策略的示例 +### 前面步骤策略的示例 { #examples-of-previous-steps-strategies } 这将在**很大程度上取决于您部署系统的方式**,并且可能与您启动程序、处理重启等的方式有关。 @@ -285,15 +277,15 @@ * Kubernetes 中的“Init Container”在应用程序容器之前运行 * 一个 bash 脚本,运行前面的步骤,然后启动您的应用程序 - * 您仍然需要一种方法来启动/重新启动 bash 脚本、检测错误等。 + * 您仍然需要一种方法来启动/重新启动 bash 脚本、检测错误等。 -/// tip +/// tip | 提示 我将在以后的章节中为您提供使用容器执行此操作的更具体示例:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 /// -## 资源利用率 +## 资源利用率 { #resource-utilization } 您的服务器是一个**资源**,您可以通过您的程序消耗或**利用**CPU 上的计算时间以及可用的 RAM 内存。 @@ -313,8 +305,7 @@ 您可以使用“htop”等简单工具来查看服务器中使用的 CPU 和 RAM 或每个进程使用的数量。 或者您可以使用更复杂的监控工具,这些工具可能分布在服务器等上。 - -## 回顾 +## 回顾 { #recap } 您在这里阅读了一些在决定如何部署应用程序时可能需要牢记的主要概念: diff --git a/docs/zh/docs/deployment/docker.md b/docs/zh/docs/deployment/docker.md index f120ebfb8..3d0c19903 100644 --- a/docs/zh/docs/deployment/docker.md +++ b/docs/zh/docs/deployment/docker.md @@ -1,17 +1,17 @@ -# 容器中的 FastAPI - Docker +# 容器中的 FastAPI - Docker { #fastapi-in-containers-docker } -部署 FastAPI 应用程序时,常见的方法是构建 **Linux 容器镜像**。 通常使用 **Docker** 完成。 然后,你可以通过几种可能的方式之一部署该容器镜像。 +部署 FastAPI 应用时,常见做法是构建一个**Linux 容器镜像**。通常使用 **Docker** 实现。然后你可以用几种方式之一部署该镜像。 -使用 Linux 容器有几个优点,包括**安全性**、**可复制性**、**简单性**等。 +使用 Linux 容器有多种优势,包括**安全性**、**可复制性**、**简单性**等。 -/// tip +/// tip | 提示 -赶时间并且已经知道这些东西了? 跳转到下面的 [`Dockerfile` 👇](#fastapi-docker_1)。 +赶时间并且已经了解这些?直接跳到下面的 [`Dockerfile` 👇](#build-a-docker-image-for-fastapi)。 ///
-Dockerfile Preview 👀 +Dockerfile 预览 👀 ```Dockerfile FROM python:3.9 @@ -24,140 +24,127 @@ 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 } -容器(主要是 Linux 容器)是一种非常**轻量级**的打包应用程序的方式,其包括所有依赖项和必要的文件,同时它们可以和同一系统中的其他容器(或者其他应用程序/组件)相互隔离。 +容器(主要是 Linux 容器)是一种非常**轻量**的方式,用来打包应用及其所有依赖和必要文件,并让它们与同一系统中的其他容器(其他应用或组件)相互隔离。 -Linux 容器使用宿主机(如物理服务器、虚拟机、云服务器等)的Linux 内核运行。 这意味着它们非常轻量(与模拟整个操作系统的完整虚拟机相比)。 +Linux 容器复用宿主机(物理机、虚拟机、云服务器等)的同一个 Linux 内核。这意味着它们非常轻量(相较于模拟整个操作系统的完整虚拟机)。 -通过这样的方式,容器消耗**很少的资源**,与直接运行进程相当(虚拟机会消耗更多)。 +因此,容器消耗的**资源很少**,大致相当于直接运行进程(而虚拟机会多很多)。 -容器的进程(通常只有一个)、文件系统和网络都运行在隔离的环境,这简化了部署、安全、开发等。 +容器还拥有各自**隔离**的运行进程(通常只有一个)、文件系统和网络,简化了部署、安全、开发等。 -## 什么是容器镜像 +## 什么是容器镜像 { #what-is-a-container-image } **容器**是从**容器镜像**运行的。 -容器镜像是容器中文件、环境变量和默认命令/程序的**静态**版本。 **静态**这里的意思是容器**镜像**还没有运行,只是打包的文件和元数据。 +容器镜像是容器中所有文件、环境变量以及应该运行的默认命令/程序的一个**静态**版本。这里的**静态**指容器**镜像**本身并不在运行,仅仅是被打包的文件和元数据。 -与存储静态内容的“**容器镜像**”相反,“**容器**”通常指正在运行的实例,即正在**执行的**。 +与存放静态内容的“**容器镜像**”相对,“**容器**”通常指一个正在运行的实例,即正在被**执行**的东西。 -当**容器**启动并运行时(从**容器镜像**启动),它可以创建或更改文件、环境变量等。这些更改将仅存在于该容器中,而不会持久化到底层的容器镜像中(不会保存到磁盘)。 +当**容器**启动并运行(从**容器镜像**启动)后,它可以创建或修改文件、环境变量等。这些更改只存在于该容器中,不会持久化到底层的容器镜像中(不会写回磁盘)。 -容器镜像相当于**程序**和文件,例如 `python`命令 和某些文件 如`main.py`。 +容器镜像可类比为**程序**文件及其内容,例如 `python` 和某个文件 `main.py`。 -而**容器**本身(与**容器镜像**相反)是镜像的实际运行实例,相当于**进程**。 事实上,容器仅在有**进程运行**时才运行(通常它只是一个单独的进程)。 当容器中没有进程运行时,容器就会停止。 +而**容器**本身(相对**容器镜像**)就是该镜像的实际运行实例,可类比为**进程**。事实上,容器只有在有**进程在运行**时才处于运行状态(通常只有一个进程)。当容器中没有任何进程在运行时,容器就会停止。 +## 容器镜像 { #container-images } +Docker 一直是创建和管理**容器镜像**与**容器**的主要工具之一。 -## 容器镜像 +还有一个公共的 Docker Hub,其中为许多工具、环境、数据库和应用提供了预制的**官方容器镜像**。 -Docker 一直是创建和管理**容器镜像**和**容器**的主要工具之一。 - -还有一个公共 Docker Hub ,其中包含预制的 **官方容器镜像**, 适用于许多工具、环境、数据库和应用程序。 - -例如,有一个官方的 Python 镜像。 - -还有许多其他镜像用于不同的需要(例如数据库),例如: +例如,有官方的 Python 镜像。 +还有许多用于不同目的(如数据库)的镜像,例如: * PostgreSQL * MySQL * MongoDB -* Redis, etc. +* Redis 等。 +通过使用预制的容器镜像,可以很容易地**组合**并使用不同工具。例如,试用一个新的数据库。在大多数情况下,你可以直接使用**官方镜像**,只需通过环境变量配置即可。 -通过使用预制的容器镜像,可以非常轻松地**组合**并使用不同的工具。 例如,尝试一个新的数据库。 在大多数情况下,你可以使用**官方镜像**,只需为其配置环境变量即可。 +这样,在很多场景中你可以学习容器和 Docker,并将这些知识复用到许多不同的工具和组件中。 -这样,在许多情况下,你可以了解容器和 Docker,并通过许多不同的工具和组件重复使用这些知识。 +因此,你可以运行包含不同内容的**多个容器**,比如一个数据库、一个 Python 应用、一个带 React 前端的 Web 服务器,并通过它们的内部网络连接在一起。 -因此,你可以运行带有不同内容的**多个容器**,例如数据库、Python 应用程序、带有 React 前端应用程序的 Web 服务器,并通过内部网络将它们连接在一起。 +所有容器管理系统(如 Docker 或 Kubernetes)都内置了这些网络功能。 -所有容器管理系统(如 Docker 或 Kubernetes)都集成了这些网络功能。 +## 容器与进程 { #containers-and-processes } -## 容器和进程 +**容器镜像**通常在其元数据中包含在**容器**启动时应运行的默认程序或命令以及要传递给该程序的参数。这与命令行中做的事情非常相似。 -**容器镜像**通常在其元数据中包含启动**容器**时应运行的默认程序或命令以及要传递给该程序的参数。 与在命令行中的情况非常相似。 - -当 **容器** 启动时,它将运行该命令/程序(尽管你可以覆盖它并使其运行不同的命令/程序)。 +当**容器**启动时,它将运行该命令/程序(尽管你可以覆盖它,让其运行不同的命令/程序)。 只要**主进程**(命令或程序)在运行,容器就在运行。 -容器通常有一个**单个进程**,但也可以从主进程启动子进程,这样你就可以在同一个容器中拥有**多个进程**。 +容器通常只有**一个进程**,但也可以由主进程启动子进程,这样同一个容器中就会有**多个进程**。 -但是,如果没有**至少一个正在运行的进程**,就不可能有一个正在运行的容器。 如果主进程停止,容器也会停止。 +但不可能在没有**至少一个运行中的进程**的情况下让容器保持运行。如果主进程停止,容器也会停止。 +## 为 FastAPI 构建 Docker 镜像 { #build-a-docker-image-for-fastapi } -## 为 FastAPI 构建 Docker 镜像 +好啦,现在动手构建点东西!🚀 -好吧,让我们现在构建一些东西! 🚀 +我将演示如何基于**官方 Python** 镜像,**从零开始**为 FastAPI 构建一个**Docker 镜像**。 -我将向你展示如何基于 **官方 Python** 镜像 **从头开始** 为 FastAPI 构建 **Docker 镜像**。 - -这是你在**大多数情况**下想要做的,例如: +这在**大多数情况**下都适用,例如: * 使用 **Kubernetes** 或类似工具 -* 在 **Raspberry Pi** 上运行时 -* 使用可为你运行容器镜像的云服务等。 +* 运行在 **Raspberry Pi** +* 使用某个为你运行容器镜像的云服务,等等 -### 依赖项 +### 包依赖 { #package-requirements } -你通常会在某个文件中包含应用程序的**依赖项**。 +通常你会把应用的**包依赖**放在某个文件里。 -具体做法取决于你**安装**这些依赖时所使用的工具。 +这主要取决于你用来**安装**这些依赖的工具。 -最常见的方法是创建一个`requirements.txt`文件,其中每行包含一个包名称和它的版本。 +最常见的方式是使用 `requirements.txt` 文件,每行一个包名及其版本范围。 -你当然也可以使用在[关于 FastAPI 版本](versions.md){.internal-link target=_blank} 中讲到的方法来设置版本范围。 - -例如,你的`requirements.txt`可能如下所示: +当然,你也可以参考你在[关于 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 +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 +/// info | 信息 -还有其他文件格式和工具来定义和安装依赖项。 - - 我将在下面的部分中向你展示一个使用 Poetry 的示例。 👇 +还有其他格式和工具可以定义并安装包依赖。 /// -### 创建 **FastAPI** 代码 - -* 创建`app`目录并进入。 -* 创建一个空文件`__init__.py`。 -* 创建一个 `main.py` 文件: - +### 编写 **FastAPI** 代码 { #create-the-fastapi-code } +* 创建 `app` 目录并进入 +* 创建空文件 `__init__.py` +* 创建 `main.py`,内容如下: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -169,84 +156,114 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -### Dockerfile +### Dockerfile { #dockerfile } -现在在相同的project目录创建一个名为`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. 从官方Python基础镜像开始。 +1. 从官方 Python 基础镜像开始。 -2. 将当前工作目录设置为`/code`。 +2. 将当前工作目录设置为 `/code`。 - 这是我们放置`requirements.txt`文件和`app`目录的位置。 + 我们会把 `requirements.txt` 文件和 `app` 目录放在这里。 -3. 将符合要求的文件复制到`/code`目录中。 +3. 将依赖文件复制到 `/code` 目录。 - 首先仅复制requirements.txt文件,而不复制其余代码。 + 首先**只**复制依赖文件,不要复制其他代码。 - 由于此文件**不经常更改**,Docker 将检测到它并在这一步中使用**缓存**,从而为下一步启用缓存。 + 因为这个文件**不常变化**,Docker 会检测并在此步骤使用**缓存**,从而也为下一步启用缓存。 -4. 安装需求文件中的包依赖项。 +4. 安装依赖文件中的包依赖。 - `--no-cache-dir` 选项告诉 `pip` 不要在本地保存下载的包,因为只有当 `pip` 再次运行以安装相同的包时才会这样,但在与容器一起工作时情况并非如此。 + `--no-cache-dir` 选项告诉 `pip` 不要在本地保存下载的包,只有当以后还要再次用 `pip` 安装相同包时才需要,但在容器场景下不是这样。 - /// note | 笔记 + /// note | 注意 - `--no-cache-dir` 仅与 `pip` 相关,与 Docker 或容器无关。 + `--no-cache-dir` 只和 `pip` 有关,与 Docker 或容器无关。 - /// + /// - `--upgrade` 选项告诉 `pip` 升级软件包(如果已经安装)。 + `--upgrade` 选项告诉 `pip` 如果包已安装则进行升级。 - 因为上一步复制文件可以被 **Docker 缓存** 检测到,所以此步骤也将 **使用 Docker 缓存**(如果可用)。 + 由于上一步复制文件可能被 **Docker 缓存**检测到,因此这一步在可用时也会**使用 Docker 缓存**。 - 在开发过程中一次又一次构建镜像时,在此步骤中使用缓存将为你节省大量**时间**,而不是**每次**都**下载和安装**所有依赖项。 + 在开发过程中反复构建镜像时,此步骤使用缓存可以为你**节省大量时间**,而不必**每次**都**下载并安装**所有依赖。 +5. 将 `./app` 目录复制到 `/code` 目录。 -5. 将“./app”目录复制到“/code”目录中。 + 这里包含了所有**最常变化**的代码,因此 Docker **缓存**很难用于这一步或**其后的步骤**。 - 由于其中包含**更改最频繁**的所有代码,因此 Docker **缓存**不会轻易用于此操作或任何**后续步骤**。 + 所以,把它放在 `Dockerfile` 的**靠后位置**,有助于优化容器镜像的构建时间。 - 因此,将其放在`Dockerfile`**接近最后**的位置非常重要,以优化容器镜像的构建时间。 +6. 设置使用 `fastapi run` 的**命令**(底层使用 Uvicorn)。 -6. 设置**命令**来运行 `uvicorn` 服务器。 + `CMD` 接受一个字符串列表,每个字符串相当于你在命令行中用空格分隔输入的内容。 - `CMD` 接受一个字符串列表,每个字符串都是你在命令行中输入的内容,并用空格分隔。 + 该命令会从**当前工作目录**运行,也就是你用 `WORKDIR /code` 设置的 `/code` 目录。 - 该命令将从 **当前工作目录** 运行,即你上面使用`WORKDIR /code`设置的同一`/code`目录。 +/// tip | 提示 - 因为程序将从`/code`启动,并且其中包含你的代码的目录`./app`,所以**Uvicorn**将能够从`app.main`中查看并**import**`app`。 - -/// tip - -通过单击代码中的每个数字气泡来查看每行的作用。 👆 +点击代码中的每个编号气泡查看每行的作用。👆 /// -你现在应该具有如下目录结构: +/// warning | 警告 + +务必**始终**使用 `CMD` 指令的**exec 形式**,如下所述。 + +/// + +#### 使用 `CMD` - Exec 形式 { #use-cmd-exec-form } + +`CMD` 指令有两种写法: + +✅ **Exec** 形式: + +```Dockerfile +# ✅ 推荐 +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** 形式: + +```Dockerfile +# ⛔️ 不要这样 +CMD fastapi run app/main.py --port 80 +``` + +务必使用**exec** 形式,以确保 FastAPI 可以优雅停机并触发[生命周期事件](../advanced/events.md){.internal-link target=_blank}。 + +你可以在 Docker 文档(Shell 与 Exec 形式)中了解更多。 + +在使用 `docker compose` 时这一点尤为明显。更多技术细节参见该 FAQ:为什么我的服务需要 10 秒才能重新创建或停止? + +#### 目录结构 { #directory-structure } + +此时你的目录结构应类似: + ``` . ├── app @@ -256,56 +273,53 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] └── requirements.txt ``` +#### 在 TLS 终止代理后面 { #behind-a-tls-termination-proxy } -#### 在 TLS 终止代理后面 - -如果你在 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 缓存 +#### Docker 缓存 { #docker-cache } -这个`Dockerfile`中有一个重要的技巧,我们首先只单独复制**包含依赖项的文件**,而不是其余代码。 让我来告诉你这是为什么。 +这个 `Dockerfile` 里有个重要技巧:我们先**只复制依赖文件**,而不是其他代码。原因如下: ```Dockerfile COPY ./requirements.txt /code/requirements.txt ``` -Docker之类的构建工具是通过**增量**的方式来构建这些容器镜像的。具体做法是从`Dockerfile`顶部开始,每一条指令生成的文件都是镜像的“一层”,同过把这些“层”一层一层地叠加到基础镜像上,最后我们就得到了最终的镜像。 +Docker 等工具是**增量**地**构建**容器镜像的,从 `Dockerfile` 顶部开始,按顺序为每条指令创建**一层叠加层**,并把每步生成的文件加入。 -Docker 和类似工具在构建镜像时也会使用**内部缓存**,如果自上次构建容器镜像以来文件没有更改,那么它将**重新使用上次创建的同一层**,而不是再次复制文件并从头开始创建新层。 +构建镜像时,Docker 等工具也会使用**内部缓存**。如果自上次构建以来某个文件没有变更,它会**重用**上次创建的那一层,而不是再次复制文件并从头创建新层。 -仅仅避免文件的复制不一定会有太多速度提升,但是如果在这一步使用了缓存,那么才可以**在下一步中使用缓存**。 例如,可以使用安装依赖项那条指令的缓存: +仅仅避免复制文件并不会带来太多改进,但因为该步骤使用了缓存,它就可以**在下一步中继续使用缓存**。例如,安装依赖的这条指令也能使用缓存: ```Dockerfile RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt ``` +包含包依赖的文件**不会频繁变更**。仅复制该文件,Docker 就能在这一步**使用缓存**。 -包含包依赖项的文件**不会频繁更改**。 只复制该文件(不复制其他的应用代码),Docker 才能在这一步**使用缓存**。 +随后,Docker 还能**对下一步**(下载并安装依赖)**使用缓存**。这正是我们**节省大量时间**的地方。✨ ...并避免无聊的等待。😪😆 -Docker 进而能**使用缓存进行下一步**,即下载并安装这些依赖项。 这才是我们**节省大量时间**的地方。 ✨ ...可以避免无聊的等待。 😪😆 +下载并安装依赖**可能需要几分钟**,而使用**缓存**则**最多只需几秒**。 -下载和安装依赖项**可能需要几分钟**,但使用**缓存**最多**只需要几秒钟**。 +而且在开发中你会反复构建镜像来验证代码变更是否生效,累计节省的时间会很多。 -由于你在开发过程中会一次又一次地构建容器镜像以检查代码更改是否有效,因此可以累计节省大量时间。 - -在`Dockerfile`末尾附近,我们再添加复制代码的指令。 由于代码是**更改最频繁的**,所以将其放在最后,因为这一步之后的内容基本上都是无法使用缓存的。 +接着,在 `Dockerfile` 的末尾附近我们再复制所有代码。因为这是**变化最频繁**的部分,把它放在后面,这样几乎所有在它之后的步骤都不会使用到缓存。 ```Dockerfile COPY ./app /code/app ``` -### 构建 Docker 镜像 +### 构建 Docker 镜像 { #build-the-docker-image } -现在所有文件都已就位,让我们构建容器镜像。 +现在所有文件都就位了,开始构建容器镜像。 -* 转到项目目录(在`Dockerfile`所在的位置,包含`app`目录)。 +* 进入项目目录(`Dockerfile` 所在位置,包含 `app` 目录) * 构建你的 FastAPI 镜像: -
```console @@ -316,18 +330,17 @@ $ docker build -t myimage .
+/// tip | 提示 -/// tip +注意末尾的 `.`,它等价于 `./`,用于告诉 Docker 使用哪个目录来构建容器镜像。 -注意最后的 `.`,它相当于`./`,它告诉 Docker 用于构建容器镜像的目录。 - -在本例中,它是相同的当前目录(`.`)。 +此处就是当前目录(`.`)。 /// -### 启动 Docker 容器 +### 启动 Docker 容器 { #start-the-docker-container } -* 根据你的镜像运行容器: +* 基于你的镜像运行一个容器:
@@ -337,10 +350,9 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-## 检查一下 +## 检查一下 { #check-it } - -你应该能在Docker容器的URL中检查它,例如: http://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (或其他等价的,使用 Docker 主机). +你应该能在容器暴露的 URL 访问它,例如:http://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery(或其他等价地址,取决于你的 Docker 主机)。 你会看到类似内容: @@ -348,25 +360,25 @@ $ docker run -d --name mycontainer -p 80:80 myimage {"item_id": 5, "q": "somequery"} ``` -## 交互式 API 文档 +## 交互式 API 文档 { #interactive-api-docs } -现在你可以转到 http://192.168.99.100/docshttp://127.0.0.1/docs (或其他等价的,使用 Docker 主机)。 +现在你可以访问 http://192.168.99.100/docshttp://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/redochttp://127.0.0.1/redoc (或其他等价的,使用 Docker 主机)。 +你还可以访问 http://192.168.99.100/redochttp://127.0.0.1/redoc(或其他等价地址,取决于你的 Docker 主机)。 你将看到备选的自动文档(由 ReDoc 提供): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 使用单文件 FastAPI 构建 Docker 镜像 +## 使用单文件 FastAPI 构建 Docker 镜像 { #build-a-docker-image-with-a-single-file-fastapi } -如果你的 FastAPI 是单个文件,例如没有`./app`目录的`main.py`,则你的文件结构可能如下所示: +如果你的 FastAPI 是单个文件,例如没有 `./app` 目录、只有 `main.py`,你的文件结构可能如下: ``` . @@ -375,7 +387,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage └── requirements.txt ``` -然后你只需更改相应的路径即可将文件复制到`Dockerfile`中: +然后你只需要在 `Dockerfile` 中修改相应路径来复制该文件: ```{ .dockerfile .annotate hl_lines="10 13" } FROM python:3.9 @@ -386,375 +398,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 并告诉它从 `main` 导入 `app` 对象(而不是从 `app.main` 导入)。 +2. 使用 `fastapi run` 来运行单文件 `main.py` 中的应用。 -然后调整Uvicorn命令使用新模块`main`而不是`app.main`来导入FastAPI 实例`app`。 +当你把文件传给 `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 通常会由另一个工具在 **外部** 处理。 +它可以是另一个容器,例如使用 Traefik,处理 **HTTPS** 并**自动**获取**证书**。 -它可以是另一个容器,例如使用 Traefik,处理 **HTTPS** 和 **自动**获取**证书**。 +/// tip | 提示 -/// tip - -Traefik可以与 Docker、Kubernetes 等集成,因此使用它为容器设置和配置 HTTPS 非常容易。 +Traefik 与 Docker、Kubernetes 等都有集成,因此为容器设置和配置 HTTPS 非常容易。 /// -或者,HTTPS 可以由云服务商作为其服务之一进行处理(同时仍在容器中运行应用程序)。 +或者,HTTPS 也可能由云服务商作为其服务之一提供(应用仍运行在容器中)。 -## 在启动和重新启动时运行 +## 启动时运行与重启 { #running-on-startup-and-restarts } -通常还有另一个工具负责**启动和运行**你的容器。 +通常会有另一个工具负责**启动并运行**你的容器。 -它可以直接是**Docker**, 或者**Docker Compose**、**Kubernetes**、**云服务**等。 +它可以是直接的 **Docker**、**Docker Compose**、**Kubernetes**、某个**云服务**等。 -在大多数(或所有)情况下,有一个简单的选项可以在启动时运行容器并在失败时重新启动。 例如,在 Docker 中,它是命令行选项 `--restart`。 +在大多数(或全部)情况下,都有简单选项可以在开机时运行容器并在失败时启用重启。例如,在 Docker 中是命令行选项 `--restart`。 -如果不使用容器,让应用程序在启动时运行并重新启动可能会很麻烦且困难。 但在大多数情况下,当**使用容器**时,默认情况下会包含该功能。 ✨ +如果不使用容器,要让应用开机自启并带重启可能繁琐且困难。但在**容器**场景下,这种功能通常默认就包含了。✨ -## 复制 - 进程数 +## 复制 - 进程数 { #replication-number-of-processes } -如果你有一个 集群, 比如 **Kubernetes**、Docker Swarm、Nomad 或其他类似的复杂系统来管理多台机器上的分布式容器,那么你可能希望在**集群级别**处理复制**,而不是在每个容器中使用**进程管理器**(如带有Worker的 Gunicorn) 。 +如果你有一个由 **Kubernetes**、Docker Swarm Mode、Nomad 或其他类似的复杂系统管理的、在多台机器上运行的分布式容器集群,那么你很可能会希望在**集群层面**来**处理复制**,而不是在每个容器中使用**进程管理**(比如让 Uvicorn 运行多个 workers)。 -像 Kubernetes 这样的分布式容器管理系统通常有一些集成的方法来处理**容器的复制**,同时仍然支持传入请求的**负载均衡**。 全部都在**集群级别**。 +像 Kubernetes 这样的分布式容器管理系统通常都有某种内置方式来处理**容器复制**,同时对传入请求进行**负载均衡**。这一切都在**集群层面**完成。 -在这些情况下,你可能希望从头开始构建一个 **Docker 镜像**,如[上面所解释](#dockerfile)的那样,安装依赖项并运行 **单个 Uvicorn 进程**,而不是运行 Gunicorn 和 Uvicorn workers这种。 +在这些情况下,你可能希望如[上文所述](#dockerfile)那样**从头构建 Docker 镜像**,安装依赖,并仅运行**单个 Uvicorn 进程**,而不是使用多个 Uvicorn workers。 +### 负载均衡器 { #load-balancer } -### 负载均衡器 +使用容器时,通常会有某个组件**监听主端口**。它可能是另一个同时充当 **TLS 终止代理**以处理 **HTTPS** 的容器,或类似工具。 -使用容器时,通常会有一些组件**监听主端口**。 它可能是处理 **HTTPS** 的 **TLS 终止代理** 或一些类似的工具的另一个容器。 +由于该组件会承接请求的**负载**并以(期望)**均衡**的方式在 workers 间分发,它也常被称为**负载均衡器**。 -由于该组件将接受请求的**负载**并(希望)以**平衡**的方式在worker之间分配该请求,因此它通常也称为**负载均衡器**。 +/// tip | 提示 -/// tip - -用于 HTTPS **TLS 终止代理** 的相同组件也可能是 **负载均衡器**。 +用于 HTTPS 的**TLS 终止代理**组件通常也会是**负载均衡器**。 /// -当使用容器时,你用来启动和管理容器的同一系统已经具有内部工具来传输来自该**负载均衡器**(也可以是**TLS 终止代理**) 的**网络通信**(例如HTTP请求)到你的应用程序容器。 +使用容器时,你用来启动和管理容器的系统本身就已有内部工具,将来自该**负载均衡器**(也可能是**TLS 终止代理**)的**网络通信**(例如 HTTP 请求)传递到你的应用容器中。 -### 一个负载均衡器 - 多个worker容器 +### 一个负载均衡器 - 多个 worker 容器 { #one-load-balancer-multiple-worker-containers } -当使用 **Kubernetes** 或类似的分布式容器管理系统时,使用其内部网络机制将允许单个在主 **端口** 上侦听的 **负载均衡器** 将通信(请求)传输到可能的 **多个** 运行你应用程序的容器。 +在 **Kubernetes** 等分布式容器管理系统中,使用其内部网络机制,允许在主**端口**上监听的单个**负载均衡器**将通信(请求)转发给可能**多个**运行你应用的容器。 -运行你的应用程序的每个容器通常**只有一个进程**(例如,运行 FastAPI 应用程序的 Uvicorn 进程)。 它们都是**相同的容器**,运行相同的东西,但每个容器都有自己的进程、内存等。这样你就可以在 CPU 的**不同核心**, 甚至在**不同的机器**充分利用**并行化(parallelization)**。 +这些运行你应用的容器通常每个只有**一个进程**(例如,一个运行 FastAPI 应用的 Uvicorn 进程)。它们都是**相同的容器**,运行相同的东西,但每个都有自己的进程、内存等。这样你就能在 CPU 的**不同核心**,甚至在**不同机器**上利用**并行化**。 -具有**负载均衡器**的分布式容器系统将**将请求轮流分配**给你的应用程序的每个容器。 因此,每个请求都可以由运行你的应用程序的多个**复制容器**之一来处理。 +分布式容器系统配合**负载均衡器**会把请求**轮流分配**到每个应用容器。因此,每个请求都可能由多个**副本容器**之一来处理。 -通常,这个**负载均衡器**能够处理发送到集群中的*其他*应用程序的请求(例如发送到不同的域,或在不同的 URL 路径前缀下),并正确地将该通信传输到在集群中运行的*其他*应用程序的对应容器。 +通常,这个**负载均衡器**还能处理发往集群中*其他*应用的请求(例如不同域名,或不同的 URL 路径前缀),并将通信转发到运行*那个其他*应用的正确容器。 +### 每个容器一个进程 { #one-process-per-container } +在这种场景下,你大概率希望**每个容器只有一个(Uvicorn)进程**,因为你已经在集群层面处理了复制。 +因此,这种情况下你**不希望**在容器内再启多个 workers(例如通过 `--workers` 命令行选项)。你会希望每个容器仅有一个**单独的 Uvicorn 进程**(但可能会有多个容器)。 +在容器内再放一个进程管理器(就像启多个 workers 一样)只会引入**不必要的复杂性**,而这些你很可能已经在集群系统中处理了。 +### 具有多个进程和特殊情况的容器 { #containers-with-multiple-processes-and-special-cases } -### 每个容器一个进程 +当然,也有一些**特殊情况**,你可能希望让**一个容器**里运行多个 **Uvicorn worker 进程**。 -在这种类型的场景中,你可能希望**每个容器有一个(Uvicorn)进程**,因为你已经在集群级别处理复制。 +在这些情况下,你可以使用 `--workers` 命令行选项来设置要运行的 worker 数量: -因此,在这种情况下,你**不会**希望拥有像 Gunicorn 和 Uvicorn worker一样的进程管理器,或者 Uvicorn 使用自己的 Uvicorn worker。 你可能希望每个容器(但可能有多个容器)只有一个**单独的 Uvicorn 进程**。 +```{ .dockerfile .annotate } +FROM python:3.9 -在容器内拥有另一个进程管理器(就像使用 Gunicorn 或 Uvicorn 管理 Uvicorn 工作线程一样)只会增加**不必要的复杂性**,而你很可能已经在集群系统中处理这些复杂性了。 +WORKDIR /code -### 具有多个进程的容器 +COPY ./requirements.txt /code/requirements.txt -当然,在某些**特殊情况**,你可能希望拥有 **一个容器**,其中包含 **Gunicorn 进程管理器**,并在其中启动多个 **Uvicorn worker进程**。 +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -在这些情况下,你可以使用 **官方 Docker 镜像**,其中包含 **Gunicorn** 作为运行多个 **Uvicorn 工作进程** 的进程管理器,以及一些默认设置来根据当前情况调整工作进程数量 自动CPU核心。 我将在下面的 [Gunicorn - Uvicorn 官方 Docker 镜像](#official-docker-image-with-gunicorn-uvicorn) 中告诉你更多相关信息。 +COPY ./app /code/app -下面一些什么时候这种做法有意义的示例: +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` +1. 这里我们使用 `--workers` 命令行选项将 worker 数量设置为 4。 -#### 一个简单的应用程序 +以下是这种做法可能合理的一些示例: -如果你的应用程序**足够简单**,你不需要(至少现在不需要)过多地微调进程数量,并且你可以使用自动默认值,那么你可能需要容器中的进程管理器 (使用官方 Docker 镜像),并且你在**单个服务器**而不是集群上运行它。 +#### 一个简单的应用 { #a-simple-app } -#### Docker Compose +如果你的应用**足够简单**,可以在**单台服务器**(不是集群)上运行,你可能会希望在容器内有一个进程管理器。 -你可以使用 **Docker Compose** 部署到**单个服务器**(而不是集群),因此你没有一种简单的方法来管理容器的复制(使用 Docker Compose),同时保留共享网络和 **负载均衡**。 +#### Docker Compose { #docker-compose } -然后,你可能希望拥有一个**单个容器**,其中有一个**进程管理器**,在其中启动**多个worker进程**。 +如果你使用 **Docker Compose** 部署到**单台服务器**(不是集群),那么你不会有一个简单的方法在保留共享网络与**负载均衡**的同时管理容器复制(通过 Docker Compose)。 -#### Prometheus和其他原因 - -你还可能有**其他原因**,这将使你更容易拥有一个带有**多个进程**的**单个容器**,而不是拥有每个容器中都有**单个进程**的**多个容器**。 - -例如(取决于你的设置)你可以在同一个容器中拥有一些工具,例如 Prometheus exporter,该工具应该有权访问**每个请求**。 - -在这种情况下,如果你有**多个容器**,默认情况下,当 Prometheus 来**读取metrics**时,它每次都会获取**单个容器**的metrics(对于处理该特定请求的容器),而不是获取所有复制容器的**累积metrics**。 - -在这种情况, 这种做法会更加简单:让**一个容器**具有**多个进程**,并在同一个容器上使用本地工具(例如 Prometheus exporter)收集所有内部进程的 Prometheus 指标并公开单个容器上的这些指标。 +这种情况下,你可能希望用**单个容器**,由**进程管理器**在容器内启动**多个 worker 进程**。 --- -要点是,这些都**不是**你必须盲目遵循的**一成不变的规则**。 你可以根据这些思路**评估你自己的场景**并决定什么方法是最适合你的的系统,考虑如何管理以下概念: +要点是,这些都**不是**你必须盲目遵循的**铁律**。你可以用这些思路来**评估你自己的场景**,并决定最适合你的系统的方法,看看如何管理以下概念: -* 安全性 - HTTPS +* 安全 - HTTPS * 启动时运行 -* 重新启动 +* 重启 * 复制(运行的进程数) * 内存 -* 开始前的先前步骤 +* 启动前的前置步骤 -## 内存 +## 内存 { #memory } -如果你**每个容器运行一个进程**,那么每个容器所消耗的内存或多或少是定义明确的、稳定的且有限的(如果它们是复制的,则不止一个)。 +如果你**每个容器只运行一个进程**,那么每个容器消耗的内存将更容易定义、较为稳定且有限(如果有复制则为多个容器)。 -然后,你可以在容器管理系统的配置中设置相同的内存限制和要求(例如在 **Kubernetes** 中)。 这样,它将能够在**可用机器**中**复制容器**,同时考虑容器所需的内存量以及集群中机器中的可用内存量。 +接着,你可以在容器管理系统(例如 **Kubernetes**)的配置中设置同样的内存限制与需求。这样它就能在**可用的机器**上**复制容器**,同时考虑容器所需的内存量以及集群中机器可用的内存量。 -如果你的应用程序很**简单**,这可能**不是问题**,并且你可能不需要指定内存限制。 但是,如果你**使用大量内存**(例如使用**机器学习**模型),则应该检查你消耗了多少内存并调整**每台机器**中运行的**容器数量**(也许可以向集群添加更多机器)。 +如果你的应用很**简单**,这可能**不成问题**,你也许不需要设置严格的内存上限。但如果你**使用大量内存**(例如使用**机器学习**模型),你应该检查自己的内存消耗,并调整**每台机器**上运行的**容器数量**(也许还需要为集群增加机器)。 -如果你**每个容器运行多个进程**(例如使用官方 Docker 镜像),你必须确保启动的进程数量不会消耗比可用内存**更多的内存**。 +如果你**每个容器运行多个进程**,你需要确保启动的进程数量不会**消耗超过可用的内存**。 -## 启动之前的步骤和容器 +## 启动前的前置步骤与容器 { #previous-steps-before-starting-and-containers } -如果你使用容器(例如 Docker、Kubernetes),那么你可以使用两种主要方法。 +如果你在使用容器(如 Docker、Kubernetes),你可以采用两种主要方式。 +### 多个容器 { #multiple-containers } -### 多个容器 +如果你有**多个容器**,可能每个容器运行一个**单独进程**(例如在 **Kubernetes** 集群中),那么你可能希望使用一个**单独的容器**来执行**前置步骤**,在一个容器中运行一个进程,**在**启动那些复制的 worker 容器**之前**完成。 -如果你有 **多个容器**,可能每个容器都运行一个 **单个进程**(例如,在 **Kubernetes** 集群中),那么你可能希望有一个 **单独的容器** 执行以下操作: 在单个容器中运行单个进程执行**先前步骤**,即运行复制的worker容器之前。 +/// info | 信息 -/// info - -如果你使用 Kubernetes,这可能是 Init Container。 +如果你使用 Kubernetes,这通常会是一个 Init Container。 /// -如果在你的用例中,运行前面的步骤**并行多次**没有问题(例如,如果你没有运行数据库迁移,而只是检查数据库是否已准备好),那么你也可以将它们放在开始主进程之前在每个容器中。 +如果在你的用例中,**并行多次**运行这些前置步骤没有问题(例如你不是在跑数据库迁移,而只是检查数据库是否就绪),那么你也可以把这些步骤放在每个容器中,在启动主进程之前执行。 -### 单容器 +### 单个容器 { #single-container } -如果你有一个简单的设置,使用一个**单个容器**,然后启动多个**工作进程**(或者也只是一个进程),那么你可以在启动进程之前在应用程序同一个容器中运行先前的步骤。 官方 Docker 镜像内部支持这一点。 +如果你的架构较为简单,使用一个**单个容器**,其后再启动多个**worker 进程**(或者也只有一个进程),那么你可以在同一个容器中,在启动应用进程之前执行这些前置步骤。 -## 带有 Gunicorn 的官方 Docker 镜像 - Uvicorn +### 基础 Docker 镜像 { #base-docker-image } -有一个官方 Docker 镜像,其中包含与 Uvicorn worker一起运行的 Gunicorn,如上一章所述:[服务器工作线程 - Gunicorn 与 Uvicorn](server-workers.md){.internal-link target=_blank}。 +曾经有一个官方的 FastAPI Docker 镜像:tiangolo/uvicorn-gunicorn-fastapi。但它现在已被弃用。⛔️ -该镜像主要在上述情况下有用:[具有多个进程和特殊情况的容器](#containers-with-multiple-processes-and-special-cases)。 +你大概率**不应该**使用这个基础镜像(或任何其它类似的镜像)。 +如果你使用 **Kubernetes**(或其他)并且已经在集群层面设置**复制**、使用多个**容器**,那么在这些情况下,最好如上所述**从头构建镜像**:[为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。 +如果你需要多个 workers,可以直接使用 `--workers` 命令行选项。 -* tiangolo/uvicorn-gunicorn-fastapi. +/// note | 技术细节 +这个 Docker 镜像创建于 Uvicorn 还不支持管理与重启失效 workers 的时期,那时需要用 Gunicorn 搭配 Uvicorn,这引入了不少复杂度,只是为了让 Gunicorn 管理并重启 Uvicorn 的 worker 进程。 -/// warning - -你很有可能不需要此基础镜像或任何其他类似的镜像,最好从头开始构建镜像,如[上面所述:为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。 +但现在 Uvicorn(以及 `fastapi` 命令)已经支持使用 `--workers`,因此没有理由不自己构建基础镜像(代码量几乎一样 😅)。 /// -该镜像包含一个**自动调整**机制,用于根据可用的 CPU 核心设置**worker进程数**。 +## 部署容器镜像 { #deploy-the-container-image } -它具有**合理的默认值**,但你仍然可以使用**环境变量**或配置文件更改和更新所有配置。 - -它还支持通过一个脚本运行**开始前的先前步骤** 。 - -/// tip - -要查看所有配置和选项,请转到 Docker 镜像页面: tiangolo/uvicorn-gunicorn-fastapi。 - -/// - -### 官方 Docker 镜像上的进程数 - -此镜像上的**进程数**是根据可用的 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 -``` - -### 何时使用 - -如果你使用 **Kubernetes** (或其他)并且你已经在集群级别设置 **复制**,并且具有多个 **容器**。 在这些情况下,你最好按照上面的描述 **从头开始构建镜像**:[为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。 - -该镜像主要在[具有多个进程的容器和特殊情况](#containers-with-multiple-processes-and-special-cases)中描述的特殊情况下有用。 例如,如果你的应用程序**足够简单**,基于 CPU 设置默认进程数效果很好,你不想在集群级别手动配置复制,并且不会运行更多进程, 或者你使用 **Docker Compose** 进行部署,在单个服务器上运行等。 - -## 部署容器镜像 - -拥有容器(Docker)镜像后,有多种方法可以部署它。 +得到容器(Docker)镜像后,有多种方式可以部署。 例如: -* 在单个服务器中使用 **Docker Compose** +* 在单台服务器上使用 **Docker Compose** * 使用 **Kubernetes** 集群 -* 使用 Docker Swarm 模式集群 -* 使用Nomad等其他工具 -* 使用云服务获取容器镜像并部署它 +* 使用 Docker Swarm Mode 集群 +* 使用 Nomad 等其他工具 +* 使用云服务,接收你的容器镜像并部署 -## Docker 镜像与Poetry +## 使用 `uv` 的 Docker 镜像 { #docker-image-with-uv } -如果你使用 Poetry 来管理项目的依赖项,你可以使用 Docker 多阶段构建: +如果你使用 uv 来安装和管理项目,可以参考他们的 uv Docker 指南。 +## 回顾 { #recap } - -```{ .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. 在此阶段安装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 - -单击气泡数字可查看每行的作用。 - -/// - -**Docker stage** 是 `Dockerfile` 的一部分,用作 **临时容器镜像**,仅用于生成一些稍后使用的文件。 - -第一阶段仅用于 **安装 Poetry** 并使用 Poetry 的 `pyproject.toml` 文件中的项目依赖项 **生成 `requirements.txt`**。 - -此`requirements.txt`文件将在**下一阶段**与`pip`一起使用。 - -在最终的容器镜像中**仅保留最后阶段**。 之前的阶段将被丢弃。 - -使用 Poetry 时,使用 **Docker 多阶段构建** 是有意义的,因为你实际上并不需要在最终的容器镜像中安装 Poetry 及其依赖项,你 **只需要** 生成用于安装项目依赖项的`requirements.txt`文件。 - -然后,在下一个(也是最后一个)阶段,你将或多或少地以与前面描述的相同的方式构建镜像。 - -### 在TLS 终止代理后面 - Poetry - -同样,如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请将选项`--proxy-headers`添加到命令中: - - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## 回顾 - -使用容器系统(例如使用**Docker**和**Kubernetes**),处理所有**部署概念**变得相当简单: +使用容器系统(例如 **Docker** 与 **Kubernetes**)后,处理所有**部署概念**会变得相当直接: * HTTPS * 启动时运行 -* 重新启动 +* 失败重启 * 复制(运行的进程数) * 内存 -* 开始前的先前步骤 +* 启动前的前置步骤 -在大多数情况下,你可能不想使用任何基础镜像,而是基于官方 Python Docker 镜像 **从头开始构建容器镜像** 。 +在大多数情况下,你可能不想使用任何基础镜像,而是基于官方 Python Docker 镜像**从头构建容器镜像**。 -处理好`Dockerfile`和 **Docker 缓存**中指令的**顺序**,你可以**最小化构建时间**,从而最大限度地提高生产力(并避免无聊)。 😎 - -在某些特殊情况下,你可能需要使用 FastAPI 的官方 Docker 镜像。 🤓 +注意 `Dockerfile` 中指令的**顺序**并利用好**Docker 缓存**,可以**最小化构建时间**,以最大化生产力(并避免无聊)。😎 diff --git a/docs/zh/docs/deployment/https.md b/docs/zh/docs/deployment/https.md index d994c4add..4f60b7bca 100644 --- a/docs/zh/docs/deployment/https.md +++ b/docs/zh/docs/deployment/https.md @@ -1,12 +1,12 @@ -# 关于 HTTPS +# 关于 HTTPS { #about-https } 人们很容易认为 HTTPS 仅仅是“启用”或“未启用”的东西。 但实际情况比这复杂得多。 -/// note | 提示 +/// tip | 提示 -如果你很赶时间或不在乎,请继续阅读下一部分,下一部分会提供一个step-by-step的教程,告诉你怎么使用不同技术来把一切都配置好。 +如果你很赶时间或不在乎,请继续阅读后续章节,它们会提供逐步的教程,告诉你怎么使用不同技术把一切都配置好。 /// @@ -15,24 +15,24 @@ 现在,从**开发人员的视角**,在了解 HTTPS 时需要记住以下几点: * 要使用 HTTPS,**服务器**需要拥有由**第三方**生成的**"证书(certificate)"**。 - * 这些证书实际上是从第三方**获取**的,而不是“生成”的。 + * 这些证书实际上是从第三方**获取**的,而不是“生成”的。 * 证书有**生命周期**。 - * 它们会**过期**。 - * 然后它们需要**更新**,**再次从第三方获取**。 + * 它们会**过期**。 + * 然后它们需要**更新**,**再次从第三方获取**。 * 连接的加密发生在 **TCP 层**。 - * 这是 HTTP 协议**下面的一层**。 - * 因此,**证书和加密**处理是在 **HTTP之前**完成的。 + * 这是 HTTP 协议**下面的一层**。 + * 因此,**证书和加密**处理是在 **HTTP之前**完成的。 * **TCP 不知道域名**。 仅仅知道 IP 地址。 - * 有关所请求的 **特定域名** 的信息位于 **HTTP 数据**中。 + * 有关所请求的 **特定域名** 的信息位于 **HTTP 数据**中。 * **HTTPS 证书**“证明”**某个域名**,但协议和加密发生在 TCP 层,在知道正在处理哪个域名**之前**。 * **默认情况下**,这意味着你**每个 IP 地址只能拥有一个 HTTPS 证书**。 - * 无论你的服务器有多大,或者服务器上的每个应用程序有多小。 - * 不过,对此有一个**解决方案**。 -* **TLS** 协议(在 HTTP 之下的TCP 层处理加密的协议)有一个**扩展**,称为 **SNI**。 - * SNI 扩展允许一台服务器(具有 **单个 IP 地址**)拥有 **多个 HTTPS 证书** 并提供 **多个 HTTPS 域名/应用程序**。 - * 为此,服务器上会有**单独**的一个组件(程序)侦听**公共 IP 地址**,这个组件必须拥有服务器中的**所有 HTTPS 证书**。 + * 无论你的服务器有多大,或者服务器上的每个应用程序有多小。 + * 不过,对此有一个**解决方案**。 +* **TLS** 协议(在 HTTP 之下的 TCP 层处理加密的协议)有一个**扩展**,称为 **SNI**。 + * SNI 扩展允许一台服务器(具有 **单个 IP 地址**)拥有 **多个 HTTPS 证书** 并提供 **多个 HTTPS 域名/应用程序**。 + * 为此,服务器上会有**单独**的一个组件(程序)侦听**公共 IP 地址**,这个组件必须拥有服务器中的**所有 HTTPS 证书**。 * **获得安全连接后**,通信协议**仍然是HTTP**。 - * 内容是 **加密过的**,即使它们是通过 **HTTP 协议** 发送的。 + * 内容是 **加密过的**,即使它们是通过 **HTTP 协议** 发送的。 通常的做法是在服务器上运行**一个程序/HTTP 服务器**并**管理所有 HTTPS 部分**:接收**加密的 HTTPS 请求**, 将 **解密的 HTTP 请求** 发送到在同一服务器中运行的实际 HTTP 应用程序(在本例中为 **FastAPI** 应用程序),从应用程序中获取 **HTTP 响应**, 使用适当的 **HTTPS 证书**对其进行加密并使用 **HTTPS** 将其发送回客户端。 此服务器通常被称为 **TLS 终止代理(TLS Termination Proxy)**。 @@ -43,7 +43,7 @@ * Nginx * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } 在 Let's Encrypt 之前,这些 **HTTPS 证书** 由受信任的第三方出售。 @@ -57,16 +57,15 @@ 我们的想法是自动获取和更新这些证书,以便你可以永远免费拥有**安全的 HTTPS**。 -## 面向开发人员的 HTTPS +## 面向开发人员的 HTTPS { #https-for-developers } 这里有一个 HTTPS API 看起来是什么样的示例,我们会分步说明,并且主要关注对开发人员重要的部分。 - -### 域名 +### 域名 { #domain-name } 第一步我们要先**获取**一些**域名(Domain Name)**。 然后可以在 DNS 服务器(可能是你的同一家云服务商提供的)中配置它。 -你可能拥有一个云服务器(虚拟机)或类似的东西,并且它会有一个固定 **公共IP地址**。 +你可能拥有一个云服务器(虚拟机)或类似的东西,并且它会有一个固定 **公共IP地址**。 在 DNS 服务器中,你可以配置一条记录(“A 记录”)以将 **你的域名** 指向你服务器的公共 **IP 地址**。 @@ -78,7 +77,7 @@ /// -### DNS +### DNS { #dns } 现在让我们关注真正的 HTTPS 部分。 @@ -88,7 +87,7 @@ DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。 这将是 -### TLS 握手开始 +### TLS 握手开始 { #tls-handshake-start } 然后,浏览器将在**端口 443**(HTTPS 端口)上与该 IP 地址进行通信。 @@ -98,7 +97,7 @@ DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。 这将是 客户端和服务器之间建立 TLS 连接的过程称为 **TLS 握手**。 -### 带有 SNI 扩展的 TLS +### 带有 SNI 扩展的 TLS { #tls-with-sni-extension } **服务器中只有一个进程**可以侦听特定 **IP 地址**的特定 **端口**。 可能有其他进程在同一 IP 地址的其他端口上侦听,但每个 IP 地址和端口组合只有一个进程。 @@ -128,7 +127,7 @@ TLS 终止代理可以访问一个或多个 **TLS 证书**(HTTPS 证书)。 /// -### HTTPS 请求 +### HTTPS 请求 { #https-request } 现在客户端和服务器(特别是浏览器和 TLS 终止代理)具有 **加密的 TCP 连接**,它们可以开始 **HTTP 通信**。 @@ -136,19 +135,19 @@ TLS 终止代理可以访问一个或多个 **TLS 证书**(HTTPS 证书)。 -### 解密请求 +### 解密请求 { #decrypt-the-request } TLS 终止代理将使用协商好的加密算法**解密请求**,并将**(解密的)HTTP 请求**传输到运行应用程序的进程(例如运行 FastAPI 应用的 Uvicorn 进程)。 -### HTTP 响应 +### HTTP 响应 { #http-response } 应用程序将处理请求并向 TLS 终止代理发送**(未加密)HTTP 响应**。 -### HTTPS 响应 +### HTTPS 响应 { #https-response } 然后,TLS 终止代理将使用之前协商的加密算法(以`someapp.example.com`的证书开头)对响应进行加密,并将其发送回浏览器。 @@ -158,7 +157,7 @@ TLS 终止代理将使用协商好的加密算法**解密请求**,并将**( 客户端(浏览器)将知道响应来自正确的服务器,因为它使用了他们之前使用 **HTTPS 证书** 协商出的加密算法。 -### 多个应用程序 +### 多个应用程序 { #multiple-applications } 在同一台(或多台)服务器中,可能存在**多个应用程序**,例如其他 API 程序或数据库。 @@ -168,7 +167,7 @@ TLS 终止代理将使用协商好的加密算法**解密请求**,并将**( 这样,TLS 终止代理就可以为多个应用程序处理**多个域名**的 HTTPS 和证书,然后在每种情况下将请求传输到正确的应用程序。 -### 证书更新 +### 证书更新 { #certificate-renewal } 在未来的某个时候,每个证书都会**过期**(大约在获得证书后 3 个月)。 @@ -183,16 +182,48 @@ TLS 终止代理将使用协商好的加密算法**解密请求**,并将**( 有多种方法可以做到这一点。 一些流行的方式是: * **修改一些DNS记录**。 - * 为此,续订程序需要支持 DNS 提供商的 API,因此,要看你使用的 DNS 提供商是否提供这一功能。 + * 为此,续订程序需要支持 DNS 提供商的 API,因此,要看你使用的 DNS 提供商是否提供这一功能。 * **在与域名关联的公共 IP 地址上作为服务器运行**(至少在证书获取过程中)。 - * 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。 - * 这就是当同一个 TLS 终止代理还负责证书续订过程时它非常有用的原因之一。 - * 否则,你可能需要暂时停止 TLS 终止代理,启动续订程序以获取证书,然后使用 TLS 终止代理配置它们,然后重新启动 TLS 终止代理。 这并不理想,因为你的应用程序在 TLS 终止代理关闭期间将不可用。 + * 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。 + * 这就是当同一个 TLS 终止代理还负责证书续订过程时它非常有用的原因之一。 + * 否则,你可能需要暂时停止 TLS 终止代理,启动续订程序以获取证书,然后使用 TLS 终止代理配置它们,然后重新启动 TLS 终止代理。 这并不理想,因为你的应用程序在 TLS 终止代理关闭期间将不可用。 通过拥有一个**单独的系统来使用 TLS 终止代理来处理 HTTPS**, 而不是直接将 TLS 证书与应用程序服务器一起使用 (例如 Uvicorn),你可以在 更新证书的过程中同时保持提供服务。 -## 回顾 +## 代理转发请求头 { #proxy-forwarded-headers } + +当使用代理来处理 HTTPS 时,你的**应用服务器**(例如通过 FastAPI CLI 运行的 Uvicorn)对 HTTPS 过程并不了解,它只通过纯 HTTP 与 **TLS 终止代理**通信。 + +这个**代理**通常会在将请求转发给**应用服务器**之前,临时设置一些 HTTP 请求头,以便让应用服务器知道该请求是由代理**转发**过来的。 + +/// note | 技术细节 + +这些代理请求头包括: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +不过,由于**应用服务器**并不知道自己位于受信任的**代理**之后,默认情况下,它不会信任这些请求头。 + +但你可以配置**应用服务器**去信任由**代理**发送的这些“转发”请求头。如果你在使用 FastAPI CLI,可以使用命令行选项 `--forwarded-allow-ips` 指定它应该信任哪些 IP 发来的这些“转发”请求头。 + +例如,如果**应用服务器**只接收来自受信任**代理**的通信,你可以设置 `--forwarded-allow-ips="*"`,让它信任所有传入的 IP,因为它只会接收来自**代理**所使用 IP 的请求。 + +这样,应用就能知道自己的公共 URL、是否使用 HTTPS、域名等信息。 + +这在需要正确处理重定向等场景时很有用。 + +/// tip + +你可以在文档中了解更多:[在代理之后 - 启用代理转发请求头](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## 回顾 { #recap } 拥有**HTTPS** 非常重要,并且在大多数情况下相当**关键**。 作为开发人员,你围绕 HTTPS 所做的大部分努力就是**理解这些概念**以及它们的工作原理。 diff --git a/docs/zh/docs/deployment/index.md b/docs/zh/docs/deployment/index.md index 1ec0c5c5b..47dcede65 100644 --- a/docs/zh/docs/deployment/index.md +++ b/docs/zh/docs/deployment/index.md @@ -1,21 +1,23 @@ -# 部署 +# 部署 { #deployment } 部署 **FastAPI** 应用程序相对容易。 -## 部署是什么意思 +## 部署是什么意思 { #what-does-deployment-mean } **部署**应用程序意味着执行必要的步骤以使其**可供用户使用**。 -对于**Web API**来说,通常涉及将上传到**云服务器**中,搭配一个性能和稳定性都不错的**服务器程序**,以便你的**用户**可以高效地**访问**你的应用程序,而不会出现中断或其他问题。 +对于**Web API**来说,通常涉及将其放到一台**远程机器**中,搭配一个性能和稳定性都不错的**服务器程序**,以便你的**用户**可以高效地**访问**你的应用程序,而不会出现中断或其他问题。 -这与**开发**阶段形成鲜明对比,在**开发**阶段,你不断更改代码、破坏代码、修复代码, 来回停止和重启服务器等。 +这与**开发**阶段形成鲜明对比,在**开发**阶段,你不断更改代码、破坏代码、修复代码,来回停止和重启开发服务器等。 -## 部署策略 +## 部署策略 { #deployment-strategies } 根据你的使用场景和使用的工具,有多种方法可以实现此目的。 你可以使用一些工具自行**部署服务器**,你也可以使用能为你完成部分工作的**云服务**,或其他可能的选项。 +例如,我们(FastAPI 团队)构建了 **FastAPI Cloud**,让将 FastAPI 应用部署到云端尽可能流畅,并且保持与使用 FastAPI 开发时相同的开发者体验。 + 我将向你展示在部署 **FastAPI** 应用程序时你可能应该记住的一些主要概念(尽管其中大部分适用于任何其他类型的 Web 应用程序)。 在接下来的部分中,你将看到更多需要记住的细节以及一些技巧。 ✨ diff --git a/docs/zh/docs/deployment/manually.md b/docs/zh/docs/deployment/manually.md index 2c2784a64..6f2ad27b2 100644 --- a/docs/zh/docs/deployment/manually.md +++ b/docs/zh/docs/deployment/manually.md @@ -1,6 +1,6 @@ -# 手动运行服务器 +# 手动运行服务器 { #run-a-server-manually } -## 使用 `fastapi run` 命令 +## 使用 `fastapi run` 命令 { #use-the-fastapi-run-command } 简而言之,使用 `fastapi run` 来运行您的 FastAPI 应用程序: @@ -42,11 +42,11 @@ $ fastapi run ASGI。FastAPI 本质上是一个 ASGI Web 框架。 +FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称为 ASGI。FastAPI 本质上是一个 ASGI Web 框架。 要在远程服务器上运行 **FastAPI** 应用(或任何其他 ASGI 应用),您需要一个 ASGI 服务器程序,例如 **Uvicorn**。它是 `fastapi` 命令默认使用的 ASGI 服务器。 @@ -58,7 +58,7 @@ FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称 * Granian:基于 Rust 的 HTTP 服务器,专为 Python 应用设计。 * NGINX Unit:NGINX Unit 是一个轻量级且灵活的 Web 应用运行时环境。 -## 服务器主机和服务器程序 +## 服务器主机和服务器程序 { #server-machine-and-server-program } 关于名称,有一个小细节需要记住。 💡 @@ -68,8 +68,7 @@ FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称 当提到远程主机时,通常将其称为**服务器**,但也称为**机器**(machine)、**VM**(虚拟机)、**节点**。 这些都是指某种类型的远程计算机,通常运行 Linux,您可以在其中运行程序。 - -## 安装服务器程序 +## 安装服务器程序 { #install-the-server-program } 当您安装 FastAPI 时,它自带一个生产环境服务器——Uvicorn,并且您可以使用 `fastapi run` 命令来启动它。 @@ -101,7 +100,7 @@ $ pip install "uvicorn[standard]" /// -## 运行服务器程序 +## 运行服务器程序 { #run-the-server-program } 如果您手动安装了 ASGI 服务器,通常需要以特定格式传递一个导入字符串,以便服务器能够正确导入您的 FastAPI 应用: @@ -142,7 +141,7 @@ Uvicorn 和其他服务器支持 `--reload` 选项,该选项在开发过程中 /// -## 部署概念 +## 部署概念 { #deployment-concepts } 这些示例运行服务器程序(例如 Uvicorn),启动**单个进程**,在所有 IP(`0.0.0.0`)上监听预定义端口(例如`80`)。 diff --git a/docs/zh/docs/deployment/server-workers.md b/docs/zh/docs/deployment/server-workers.md index e46ba7a09..2bbd5d9b6 100644 --- a/docs/zh/docs/deployment/server-workers.md +++ b/docs/zh/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# 服务器工作进程(Workers) - 使用 Uvicorn 的多工作进程模式 +# 服务器工作进程(Workers) - 使用 Uvicorn 的多工作进程模式 { #server-workers-uvicorn-with-workers } 让我们回顾一下之前的部署概念: @@ -17,7 +17,7 @@ 在本章节中,我将向您展示如何使用 `fastapi` 命令或直接使用 `uvicorn` 命令以**多工作进程模式**运行 **Uvicorn**。 -/// info +/// info | 信息 如果您正在使用容器,例如 Docker 或 Kubernetes,我将在下一章中告诉您更多相关信息:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 @@ -25,7 +25,7 @@ /// -## 多个工作进程 +## 多个工作进程 { #multiple-workers } 您可以使用 `--workers` 命令行选项来启动多个工作进程: @@ -111,7 +111,7 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 您还可以看到它显示了每个进程的 **PID**,父进程(这是**进程管理器**)的 PID 为`27365`,每个工作进程的 PID 为:`27368`、`27369`, `27370`和`27367`。 -## 部署概念 +## 部署概念 { #deployment-concepts } 在这里,您学习了如何使用多个**工作进程(workers)**来让应用程序的执行**并行化**,充分利用 CPU 的**多核性能**,并能够处理**更多的请求**。 @@ -124,13 +124,13 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 * **内存** * **启动之前的先前步骤** -## 容器和 Docker +## 容器和 Docker { #containers-and-docker } 在关于 [容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank} 的下一章中,我将介绍一些可用于处理其他**部署概念**的策略。 我将向您展示如何**从零开始构建自己的镜像**,以运行一个单独的 Uvicorn 进程。这个过程相对简单,并且在使用 **Kubernetes** 等分布式容器管理系统时,这通常是您需要采取的方法。 -## 回顾 +## 回顾 { #recap } 您可以在使用 `fastapi` 或 `uvicorn` 命令时,通过 `--workers` CLI 选项启用多个工作进程(workers),以充分利用**多核 CPU**,以**并行运行多个进程**。 diff --git a/docs/zh/docs/deployment/versions.md b/docs/zh/docs/deployment/versions.md index 228bb0765..23c37f3b5 100644 --- a/docs/zh/docs/deployment/versions.md +++ b/docs/zh/docs/deployment/versions.md @@ -1,4 +1,4 @@ -# 关于 FastAPI 版本 +# 关于 FastAPI 版本 { #about-fastapi-versions } **FastAPI** 已在许多应用程序和系统的生产环境中使用。 并且测试覆盖率保持在100%。 但其开发进度仍在快速推进。 @@ -8,41 +8,41 @@ 你现在就可以使用 **FastAPI** 创建生产环境应用程序(你可能已经这样做了一段时间),你只需确保使用的版本可以与其余代码正确配合即可。 -## 固定你的 `fastapi` 版本 +## 固定你的 `fastapi` 版本 { #pin-your-fastapi-version } 你应该做的第一件事是将你正在使用的 **FastAPI** 版本“固定”到你知道适用于你的应用程序的特定最新版本。 -例如,假设你在应用程序中使用版本`0.45.0`。 +例如,假设你在应用程序中使用版本`0.112.0`。 如果你使用`requirements.txt`文件,你可以使用以下命令指定版本: -````txt -fastapi==0.45.0 -```` +```txt +fastapi[standard]==0.112.0 +``` -这意味着你将使用版本`0.45.0`。 +这意味着你将使用版本`0.112.0`。 或者你也可以将其固定为: -````txt -fastapi>=0.45.0,<0.46.0 -```` +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` -这意味着你将使用`0.45.0`或更高版本,但低于`0.46.0`,例如,版本`0.45.2`仍会被接受。 +这意味着你将使用`0.112.0`或更高版本,但低于`0.113.0`,例如,版本`0.112.2`仍会被接受。 -如果你使用任何其他工具来管理你的安装,例如 Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。 +如果你使用任何其他工具来管理你的安装,例如 `uv`、Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。 -## 可用版本 +## 可用版本 { #available-versions } 你可以在[发行说明](../release-notes.md){.internal-link target=_blank}中查看可用版本(例如查看当前最新版本)。 -## 关于版本 +## 关于版本 { #about-versions } 遵循语义版本控制约定,任何低于`1.0.0`的版本都可能会添加 breaking changes。 -FastAPI 还遵循这样的约定:任何`PATCH`版本更改都是为了bug修复和non-breaking changes。 +FastAPI 还遵循这样的约定:任何"PATCH"版本更改都是为了bug修复和non-breaking changes。 -/// tip +/// tip | 提示 "PATCH"是最后一个数字,例如,在`0.2.3`中,PATCH版本是`3`。 @@ -56,13 +56,13 @@ fastapi>=0.45.0,<0.46.0 "MINOR"版本中会添加breaking changes和新功能。 -/// tip +/// tip | 提示 "MINOR"是中间的数字,例如,在`0.2.3`中,MINOR版本是`2`。 /// -## 升级FastAPI版本 +## 升级FastAPI版本 { #upgrading-the-fastapi-versions } 你应该为你的应用程序添加测试。 @@ -72,7 +72,7 @@ fastapi>=0.45.0,<0.46.0 如果一切正常,或者在进行必要的更改之后,并且所有测试都通过了,那么你可以将`fastapi`固定到新的版本。 -## 关于Starlette +## 关于Starlette { #about-starlette } 你不应该固定`starlette`的版本。 @@ -80,14 +80,14 @@ fastapi>=0.45.0,<0.46.0 因此,**FastAPI** 自己可以使用正确的 Starlette 版本。 -## 关于 Pydantic +## 关于 Pydantic { #about-pydantic } Pydantic 包含针对 **FastAPI** 的测试及其自己的测试,因此 Pydantic 的新版本(`1.0.0`以上)始终与 FastAPI 兼容。 -你可以将 Pydantic 固定到适合你的`1.0.0`以上和`2.0.0`以下的任何版本。 +你可以将 Pydantic 固定到任何高于 `1.0.0` 且适合你的版本。 例如: -````txt -pydantic>=1.2.0,<2.0.0 -```` +```txt +pydantic>=2.7.0,<3.0.0 +``` diff --git a/docs/zh/docs/environment-variables.md b/docs/zh/docs/environment-variables.md index 812278051..8729a6306 100644 --- a/docs/zh/docs/environment-variables.md +++ b/docs/zh/docs/environment-variables.md @@ -1,6 +1,6 @@ -# 环境变量 +# 环境变量 { #environment-variables } -/// tip +/// tip | 提示 如果你已经知道什么是“环境变量”并且知道如何使用它们,你可以放心跳过这一部分。 @@ -10,7 +10,7 @@ 环境变量对于处理应用程序**设置**、作为 Python **安装**的一部分等方面非常有用。 -## 创建和使用环境变量 +## 创建和使用环境变量 { #create-and-use-env-vars } 你在 **shell(终端)**中就可以**创建**和使用环境变量,并不需要用到 Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## 在 Python 中读取环境变量 +## 在 Python 中读取环境变量 { #read-env-vars-in-python } 你也可以在 Python **之外**的终端中创建环境变量(或使用任何其他方法),然后在 Python 中**读取**它们。 @@ -63,9 +63,9 @@ name = os.getenv("MY_NAME", "World") print(f"Hello {name} from Python") ``` -/// tip +/// tip | 提示 -第二个参数是 `os.getenv()` 的默认返回值。 +第二个参数是 `os.getenv()` 的默认返回值。 如果没有提供,默认值为 `None`,这里我们提供 `"World"` 作为默认值。 @@ -151,21 +151,21 @@ Hello World from Python -/// tip +/// tip | 提示 -你可以在 The Twelve-Factor App: 配置中了解更多信息。 +你可以在 The Twelve-Factor App: 配置中了解更多信息。 /// -## 类型和验证 +## 类型和验证 { #types-and-validation } 这些环境变量只能处理**文本字符串**,因为它们是处于 Python 范畴之外的,必须与其他程序和操作系统的其余部分兼容(甚至与不同的操作系统兼容,如 Linux、Windows、macOS)。 这意味着从环境变量中读取的**任何值**在 Python 中都将是一个 `str`,任何类型转换或验证都必须在代码中完成。 -你将在[高级用户指南 - 设置和环境变量](./advanced/settings.md)中了解更多关于使用环境变量处理**应用程序设置**的信息。 +你将在[高级用户指南 - 设置和环境变量](./advanced/settings.md){.internal-link target=_blank}中了解更多关于使用环境变量处理**应用程序设置**的信息。 -## `PATH` 环境变量 +## `PATH` 环境变量 { #path-environment-variable } 有一个**特殊的**环境变量称为 **`PATH`**,操作系统(Linux、macOS、Windows)用它来查找要运行的程序。 @@ -209,7 +209,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 如果找到了,那么操作系统将**使用它**;否则,操作系统会继续在**其他目录**中查找。 -### 安装 Python 和更新 `PATH` +### 安装 Python 和更新 `PATH` { #installing-python-and-updating-the-path } 安装 Python 时,可能会询问你是否要更新 `PATH` 环境变量。 @@ -233,7 +233,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 假设你安装 Python 并最终将其安装在了目录 `C:\opt\custompython\bin` 中。 -如果你同意更新 `PATH` 环境变量 (在 Python 安装程序中,这个操作是名为 `Add Python x.xx to PATH` 的复选框 —— 译者注),那么安装程序将会将 `C:\opt\custompython\bin` 添加到 `PATH` 环境变量中。 +如果你同意更新 `PATH` 环境变量,那么安装程序将会将 `C:\opt\custompython\bin` 添加到 `PATH` 环境变量中。 ```plaintext C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin @@ -285,13 +285,13 @@ $ C:\opt\custompython\bin\python //// -当学习[虚拟环境](virtual-environments.md)时,这些信息将会很有用。 +当学习[虚拟环境](virtual-environments.md){.internal-link target=_blank}时,这些信息将会很有用。 -## 结论 +## 结论 { #conclusion } 通过这个教程,你应该对**环境变量**是什么以及如何在 Python 中使用它们有了基本的了解。 -你也可以在环境变量 - 维基百科 (Wikipedia for Environment Variable) 中了解更多关于它们的信息。 +你也可以在环境变量 - 维基百科中了解更多关于它们的信息。 在许多情况下,环境变量的用途和适用性并不是很明显。但是在开发过程中,它们会在许多不同的场景中出现,因此了解它们是很有必要的。 diff --git a/docs/zh/docs/fastapi-cli.md b/docs/zh/docs/fastapi-cli.md index 3b67eb664..4d3b51a57 100644 --- a/docs/zh/docs/fastapi-cli.md +++ b/docs/zh/docs/fastapi-cli.md @@ -1,8 +1,8 @@ -# FastAPI CLI +# FastAPI CLI { #fastapi-cli } **FastAPI CLI** 是一个命令行程序,你可以用它来部署和运行你的 FastAPI 应用程序,管理你的 FastAPI 项目,等等。 -当你安装 FastAPI 时(例如使用 `pip install FastAPI` 命令),会包含一个名为 `fastapi-cli` 的软件包,该软件包在终端中提供 `fastapi` 命令。 +当你安装 FastAPI 时(例如使用 `pip install "fastapi[standard]"`),会包含一个名为 `fastapi-cli` 的软件包,该软件包在终端中提供 `fastapi` 命令。 要在开发环境中运行你的 FastAPI 应用,你可以使用 `fastapi dev` 命令: @@ -48,32 +48,28 @@ $ fastapi dev Uvicorn,这是一个高性能、适用于生产环境的 ASGI 服务器。😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } 当你运行 `fastapi dev` 时,它将以开发模式运行。 -默认情况下,它会启用**自动重载**,因此当你更改代码时,它会自动重新加载服务器。该功能是资源密集型的,且相较不启用时更不稳定,因此你应该仅在开发环境下使用它。 +默认情况下,它会启用**自动重载**,因此当你更改代码时,它会自动重新加载服务器。该功能是资源密集型的,且相较不启用时更不稳定,因此你应该仅在开发环境下使用它。它还会监听 IP 地址 `127.0.0.1`,这是你的机器仅与自身通信的 IP(`localhost`)。 -默认情况下,它将监听 IP 地址 `127.0.0.1`,这是你的机器与自身通信的 IP 地址(`localhost`)。 - -## `fastapi run` +## `fastapi run` { #fastapi-run } 当你运行 `fastapi run` 时,它默认以生产环境模式运行。 -默认情况下,**自动重载是禁用的**。 - -它将监听 IP 地址 `0.0.0.0`,即所有可用的 IP 地址,这样任何能够与该机器通信的人都可以公开访问它。这通常是你在生产环境中运行它的方式,例如在容器中运行。 +默认情况下,**自动重载是禁用的**。它将监听 IP 地址 `0.0.0.0`,即所有可用的 IP 地址,这样任何能够与该机器通信的人都可以公开访问它。这通常是你在生产环境中运行它的方式,例如在容器中运行。 在大多数情况下,你会(且应该)有一个“终止代理”在上层为你处理 HTTPS,这取决于你如何部署应用程序,你的服务提供商可能会为你处理此事,或者你可能需要自己设置。 /// tip | 提示 -你可以在 [deployment documentation](deployment/index.md){.internal-link target=_blank} 获得更多信息。 +你可以在[部署文档](deployment/index.md){.internal-link target=_blank}中了解更多。 /// diff --git a/docs/zh/docs/features.md b/docs/zh/docs/features.md index eaf8daff7..7d7aa19c0 100644 --- a/docs/zh/docs/features.md +++ b/docs/zh/docs/features.md @@ -1,22 +1,21 @@ -# 特性 +# 特性 { #features } -## FastAPI 特性 +## FastAPI 特性 { #fastapi-features } **FastAPI** 提供了以下内容: -### 基于开放标准 +### 基于开放标准 { #based-on-open-standards } - -* 用于创建 API 的 OpenAPI 包含了路径操作,请求参数,请求体,安全性等的声明。 -* 使用 JSON Schema (因为 OpenAPI 本身就是基于 JSON Schema 的)自动生成数据模型文档。 +* 用于创建 API 的 OpenAPI,包含对路径 操作、参数、请求体、安全等的声明。 +* 使用 JSON Schema 自动生成数据模型文档(因为 OpenAPI 本身就是基于 JSON Schema 的)。 * 经过了缜密的研究后围绕这些标准而设计。并非狗尾续貂。 * 这也允许了在很多语言中自动**生成客户端代码**。 -### 自动生成文档 +### 自动生成文档 { #automatic-docs } 交互式 API 文档以及具探索性 web 界面。因为该框架是基于 OpenAPI,所以有很多可选项,FastAPI 默认自带两个交互式 API 文档。 -* Swagger UI,可交互式操作,能在浏览器中直接调用和测试你的 API 。 +* Swagger UI,可交互式操作,能在浏览器中直接调用和测试你的 API。 ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) @@ -24,11 +23,11 @@ ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 更主流的 Python +### 更主流的 Python { #just-modern-python } -全部都基于标准的 **Python 3.6 类型**声明(感谢 Pydantic )。没有新的语法需要学习。只需要标准的 Python 。 +全部都基于标准的 **Python 类型** 声明(感谢 Pydantic)。没有新的语法需要学习。只需要标准的现代 Python。 -如果你需要2分钟来学习如何使用 Python 类型(即使你不使用 FastAPI ),看看这个简短的教程:[Python Types](python-types.md){.internal-link target=_blank}。 +如果你需要2分钟来学习如何使用 Python 类型(即使你不使用 FastAPI),看看这个简短的教程:[Python Types](python-types.md){.internal-link target=_blank}。 编写带有类型标注的标准 Python: @@ -37,13 +36,13 @@ from datetime import date from pydantic import BaseModel -# Declare a variable as a str -# and get editor support inside the function +# 将变量声明为 str +# 并在函数内获得编辑器支持 def main(user_id: str): return user_id -# A Pydantic model +# 一个 Pydantic 模型 class User(BaseModel): id: int name: str @@ -65,19 +64,19 @@ my_second_user: User = User(**second_user_data) ``` -/// info +/// info | 信息 -`**second_user_data` 意思是: +`**second_user_data` 意思是: -直接将`second_user_data`字典的键和值直接作为key-value参数传递,等同于:`User(id=4, name="Mary", joined="2018-11-30")` +直接将 `second_user_data` 字典的键和值作为 key-value 参数传入,等同于:`User(id=4, name="Mary", joined="2018-11-30")` /// -### 编辑器支持 +### 编辑器支持 { #editor-support } 整个框架都被设计得易于使用且直观,所有的决定都在开发之前就在多个编辑器上进行了测试,来确保最佳的开发体验。 -在最近的 Python 开发者调查中,我们能看到 被使用最多的功能是"自动补全"。 +在最近的 Python 开发者调查中,我们能看到 被使用最多的功能是“自动补全”。 整个 **FastAPI** 框架就是基于这一点的。任何地方都可以进行自动补全。 @@ -85,62 +84,58 @@ my_second_user: User = User(**second_user_data) 在这里,你的编辑器可能会这样帮助你: -* Visual Studio Code 中: +* 在 Visual Studio Code 中: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -* PyCharm 中: +* 在 PyCharm 中: ![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) 你将能进行代码补全,这是在之前你可能曾认为不可能的事。例如,在来自请求 JSON 体(可能是嵌套的)中的键 `price`。 -不会再输错键名,来回翻看文档,或者来回滚动寻找你最后使用的 `username` 或者 `user_name` 。 +不会再输错键名,来回翻看文档,或者来回滚动寻找你最后使用的 `username` 或者 `user_name`。 - - -### 简洁 +### 简洁 { #short } 任何类型都有合理的**默认值**,任何和地方都有可选配置。所有的参数被微调,来满足你的需求,定义成你需要的 API。 但是默认情况下,一切都能**“顺利工作”**。 -### 验证 +### 验证 { #validation } * 校验大部分(甚至所有?)的 Python **数据类型**,包括: - * JSON 对象 (`dict`). + * JSON 对象 (`dict`)。 * JSON 数组 (`list`) 定义成员类型。 - * 字符串 (`str`) 字段, 定义最小或最大长度。 - * 数字 (`int`, `float`) 有最大值和最小值, 等等。 + * 字符串 (`str`) 字段,定义最小或最大长度。 + * 数字 (`int`, `float`) 有最大值和最小值,等等。 -* 校验外来类型, 比如: - * URL. - * Email. - * UUID. - * ...及其他. +* 校验外来类型,比如: + * URL。 + * Email。 + * UUID。 + * ...及其他。 所有的校验都由完善且强大的 **Pydantic** 处理。 -### 安全性及身份验证 +### 安全性及身份验证 { #security-and-authentication } 集成了安全性和身份认证。杜绝数据库或者数据模型的渗透风险。 OpenAPI 中定义的安全模式,包括: * HTTP 基本认证。 -* **OAuth2** (也使用 **JWT tokens**)。在 [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}查看教程。 +* **OAuth2**(也使用 **JWT tokens**)。在 [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}查看教程。 * API 密钥,在: * 请求头。 * 查询参数。 - * Cookies, 等等。 + * Cookies,等等。 加上来自 Starlette(包括 **session cookie**)的所有安全特性。 所有的这些都是可复用的工具和组件,可以轻松与你的系统,数据仓库,关系型以及 NoSQL 数据库等等集成。 - - -### 依赖注入 +### 依赖注入 { #dependency-injection } FastAPI 有一个使用非常简单,但是非常强大的依赖注入系统。 @@ -149,48 +144,47 @@ FastAPI 有一个使用非常简单,但是非常强大的IDE/linter/brain** 适配: * 因为 pydantic 数据结构仅仅是你定义的类的实例;自动补全,linting,mypy 以及你的直觉应该可以和你验证的数据一起正常工作。 * 验证**复杂结构**: - * 使用分层的 Pydantic 模型, Python `typing`的 `List` 和 `Dict` 等等。 - * 验证器使我们能够简单清楚的将复杂的数据模式定义、检查并记录为 JSON Schema。 + * 使用分层的 Pydantic 模型,Python `typing` 的 `List` 和 `Dict` 等等。 + * 验证器使我们能够简单清楚地将复杂的数据模式定义、检查并记录为 JSON Schema。 * 你可以拥有深度**嵌套的 JSON** 对象并对它们进行验证和注释。 * **可扩展**: * Pydantic 允许定义自定义数据类型或者你可以用验证器装饰器对被装饰的模型上的方法扩展验证。 diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md index f01eb9eb5..5a5157b5d 100644 --- a/docs/zh/docs/help-fastapi.md +++ b/docs/zh/docs/help-fastapi.md @@ -1,147 +1,254 @@ -# 帮助 FastAPI 与求助 +# 帮助 FastAPI - 获取帮助 { #help-fastapi-get-help } -您喜欢 **FastAPI** 吗? +你喜欢 **FastAPI** 吗? -想帮助 FastAPI?其它用户?还有项目作者? +想帮助 FastAPI、其他用户和作者吗? -或要求助怎么使用 **FastAPI**? +或者你想获取 **FastAPI** 的帮助? -以下几种帮助的方式都非常简单(有些只需要点击一两下鼠标)。 +有很多非常简单的方式可以帮忙(有些只需点一两下)。 -求助的渠道也很多。 +同样,也有多种途径可以获得帮助。 -## 订阅新闻邮件 +## 订阅新闻邮件 { #subscribe-to-the-newsletter } -您可以订阅 [**FastAPI 和它的小伙伴** 新闻邮件](newsletter.md){.internal-link target=_blank}(不会经常收到) +你可以订阅(不频繁的)[**FastAPI and friends** 新闻邮件](newsletter.md){.internal-link target=_blank},获取如下更新: * FastAPI 及其小伙伴的新闻 🚀 * 指南 📝 * 功能 ✨ * 破坏性更改 🚨 -* 开发技巧 ✅ +* 使用技巧 ✅ -## 在推特上关注 FastAPI +## 在 X (Twitter) 上关注 FastAPI { #follow-fastapi-on-x-twitter } -在 **X (Twitter)** 上关注 @fastapi 获取 **FastAPI** 的最新消息。🐦 +在 **X (Twitter)** 上关注 @fastapi 获取 **FastAPI** 的最新动态。🐦 -## 在 GitHub 上为 **FastAPI** 加星 +## 在 GitHub 上为 **FastAPI** 加星 { #star-fastapi-in-github } -您可以在 GitHub 上 **Star** FastAPI(只要点击右上角的星星就可以了): https://github.com/fastapi/fastapi。⭐️ +你可以在 GitHub 上为 FastAPI 点亮「星标」(点击右上角的星形按钮):https://github.com/fastapi/fastapi。⭐️ -**Star** 以后,其它用户就能更容易找到 FastAPI,并了解到已经有其他用户在使用它了。 +点亮星标后,其他用户更容易发现它,并看到它已经对许多人有帮助。 -## 关注 GitHub 资源库的版本发布 +## 关注 GitHub 资源库的版本发布 { #watch-the-github-repository-for-releases } -您还可以在 GitHub 上 **Watch** FastAPI,(点击右上角的 **Watch** 按钮)https://github.com/fastapi/fastapi。👀 +你可以在 GitHub 上「关注」FastAPI(点击右上角的「watch」按钮):https://github.com/fastapi/fastapi。👀 -您可以选择只关注发布(**Releases only**)。 +在那里你可以选择「Releases only」。 -这样,您就可以(在电子邮件里)接收到 **FastAPI** 新版发布的通知,及时了解 bug 修复与新功能。 +这样做之后,每当 **FastAPI** 发布新版本(包含修复和新功能),你都会收到通知(邮件)。 -## 联系作者 +## 联系作者 { #connect-with-the-author } -您可以联系项目作者,就是我(Sebastián Ramírez / `tiangolo`)。 +你可以联系我(Sebastián Ramírez / `tiangolo`),作者本人。 -您可以: +你可以: -* 在 **GitHub** 上关注我 - * 了解其它我创建的开源项目,或许对您会有帮助 - * 关注我什么时候创建新的开源项目 -* 在 **X (Twitter)** 上关注我 - * 告诉我您使用 FastAPI(我非常乐意听到这种消息) - * 接收我发布公告或新工具的消息 - * 您还可以关注@fastapi on X (Twitter),这是个独立的账号 -* 在**领英**上联系我 - * 接收我发布公告或新工具的消息(虽然我用 X (Twitter) 比较多) -* 阅读我在 **Dev.to****Medium** 上的文章,或关注我 - * 阅读我的其它想法、文章,了解我创建的工具 - * 关注我,这样就可以随时看到我发布的新文章 +* 在 **GitHub** 上关注我。 + * 了解我创建的其他开源项目,也许对你有帮助。 + * 关注我何时创建新的开源项目。 +* 关注我在 **X (Twitter)**Mastodon。 + * 告诉我你如何使用 FastAPI(我很喜欢听这些)。 + * 获取我发布公告或新工具的消息。 + * 你也可以关注 @fastapi on X (Twitter)(独立账号)。 +* 在 **LinkedIn** 上关注我。 + * 获取我发布公告或新工具的消息(不过我更常用 X (Twitter) 🤷‍♂)。 +* 阅读我在 **Dev.to****Medium** 上的文章(或关注我)。 + * 阅读我的其他想法、文章,以及我创建的工具。 + * 关注我,这样当我发布新文章时你会第一时间看到。 -## Tweet about **FastAPI** +## 发推谈谈 **FastAPI** { #tweet-about-fastapi } -Tweet about **FastAPI** 让我和大家知道您为什么喜欢 FastAPI。🎉 +Tweet about **FastAPI**,告诉我和大家你为什么喜欢它。🎉 -知道有人使用 **FastAPI**,我会很开心,我也想知道您为什么喜欢 FastAPI,以及您在什么项目/哪些公司使用 FastAPI,等等。 +我很高兴听到 **FastAPI** 的使用情况、你喜欢它的哪些点、你在哪个项目/公司使用它,等等。 -## 为 FastAPI 投票 +## 为 FastAPI 投票 { #vote-for-fastapi } -* 在 Slant 上为 **FastAPI** 投票 -* 在 AlternativeTo 上为 **FastAPI** 投票 +* 在 Slant 上为 **FastAPI** 投票。 +* 在 AlternativeTo 上为 **FastAPI** 投票。 +* 在 StackShare 上标注你在用 **FastAPI**。 -## 在 GitHub 上帮助其他人解决问题 +## 在 GitHub 上帮别人解答问题 { #help-others-with-questions-in-github } -您可以查看现有 issues,并尝试帮助其他人解决问题,说不定您能解决这些问题呢。🤓 +你可以尝试在以下地方帮助他人解答问题: -如果帮助很多人解决了问题,您就有可能成为 [FastAPI 的官方专家](fastapi-people.md#_3){.internal-link target=_blank}。🎉 +* GitHub Discussions +* GitHub Issues -## 监听 GitHub 资源库 +很多情况下,你也许已经知道这些问题的答案了。🤓 -您可以在 GitHub 上「监听」FastAPI(点击右上角的 "watch" 按钮): https://github.com/fastapi/fastapi. 👀 +如果你帮助了很多人解答问题,你会成为官方的 [FastAPI 专家](fastapi-people.md#fastapi-experts){.internal-link target=_blank}。🎉 -如果您选择 "Watching" 而不是 "Releases only",有人创建新 Issue 时,您会接收到通知。 +只要记住,最重要的一点是:尽量友善。人们带着挫败感而来,很多时候他们的提问方式并不理想,但请尽你所能地友好对待。🤗 -然后您就可以尝试并帮助他们解决问题。 +我们的目标是让 **FastAPI** 社区友好且包容。同时,也不要接受对他人的霸凌或不尊重。我们需要彼此照顾。 -## 创建 Issue +--- -您可以在 GitHub 资源库中创建 Issue,例如: +以下是如何帮助他人解答问题(在 Discussions 或 Issues 中): -* 提出**问题**或**意见** -* 提出新**特性**建议 +### 理解问题 { #understand-the-question } -**注意**:如果您创建 Issue,我会要求您也要帮助别的用户。😉 +* 看看你是否能理解提问者的**目的**和使用场景。 -## 创建 PR +* 然后检查问题(绝大多数是提问)是否**清晰**。 -您可以创建 PR 为源代码做[贡献](contributing.md){.internal-link target=_blank},例如: +* 很多时候,问题是围绕提问者想象中的解决方案,但可能有**更好的**方案。如果你更好地理解了问题和使用场景,你就可能提出更**合适的替代方案**。 -* 修改文档错别字 -* 编辑这个文件,分享 FastAPI 的文章、视频、博客,不论是您自己的,还是您看到的都成 - * 注意,添加的链接要放在对应区块的开头 -* [翻译文档](contributing.md#_8){.internal-link target=_blank} - * 审阅别人翻译的文档 -* 添加新的文档内容 -* 修复现有问题/Bug -* 添加新功能 +* 如果你没能理解问题,请请求更多**细节**。 -## 加入聊天 +### 复现问题 { #reproduce-the-problem } -快加入 👥 Discord 聊天服务器 👥 和 FastAPI 社区里的小伙伴一起哈皮吧。 +在大多数情况下与问题相关的都是提问者的**原始代码**。 -/// tip | 提示 +很多时候他们只会粘贴一小段代码,但这不足以**复现问题**。 -如有问题,请在 GitHub Issues 里提问,在这里更容易得到 [FastAPI 专家](fastapi-people.md#_3){.internal-link target=_blank}的帮助。 +* 你可以请他们提供一个可最小复现的示例,你可以**复制粘贴**并在本地运行,看到与他们相同的错误或行为,或者更好地理解他们的用例。 -聊天室仅供闲聊。 +* 如果你非常热心,你也可以尝试仅根据问题描述自己**构造一个示例**。不过要记住,这可能会花很多时间,通常先请他们澄清问题会更好。 + +### 提出解决方案 { #suggest-solutions } + +* 在能够理解问题之后,你可以给出一个可能的**答案**。 + +* 很多情况下,更好的是去理解他们**底层的问题或场景**,因为可能存在比他们尝试的方法更好的解决方式。 + +### 请求关闭问题 { #ask-to-close } + +如果他们回复了,很有可能你已经解决了他们的问题,恭喜,**你是英雄**!🦸 + +* 现在,如果问题已解决,你可以请他们: + * 在 GitHub Discussions 中:将你的评论标记为**答案**。 + * 在 GitHub Issues 中:**关闭**该 issue。 + +## 关注 GitHub 资源库 { #watch-the-github-repository } + +你可以在 GitHub 上「关注」FastAPI(点击右上角的「watch」按钮):https://github.com/fastapi/fastapi。👀 + +如果你选择「Watching」而非「Releases only」,当有人创建新的 issue 或问题时你会收到通知。你也可以指定只通知新 issues、discussions、PR 等。 + +然后你就可以尝试帮助他们解决这些问题。 + +## 提问 { #ask-questions } + +你可以在 GitHub 资源库中创建一个新问题(Question),例如: + +* 提出一个**问题**或关于某个**问题**的求助。 +* 建议一个新的**功能**。 + +**注意**:如果你这么做了,我也会请你去帮助其他人。😉 + +## 审阅 Pull Request { #review-pull-requests } + +你可以帮我审阅他人的 Pull Request。 + +再次提醒,请尽力保持友善。🤗 + +--- + +下面是需要注意的点,以及如何审阅一个 Pull Request: + +### 理解问题 { #understand-the-problem } + +* 首先,确保你**理解这个 PR 要解决的问题**。它可能在 GitHub Discussion 或 issue 中有更长的讨论。 + +* 也有很大可能这个 PR 实际上并不需要,因为问题可以用**不同方式**解决。这种情况下你可以提出或询问该方案。 + +### 不用过分担心风格 { #dont-worry-about-style } + +* 不用太在意提交信息风格等,我会在合并时 squash 并手动调整提交信息。 + +* 也不用过分担心代码风格规则,已经有自动化工具在检查。 + +如果还有其他风格或一致性需求,我会直接提出,或者我会在其上追加提交做必要修改。 + +### 检查代码 { #check-the-code } + +* 检查并阅读代码,看看是否说得通,**在本地运行**并确认它确实解决了问题。 + +* 然后**评论**说明你已经这样做了,这样我就知道你确实检查过。 + +/// info | 信息 + +不幸的是,我不能仅仅信任那些有很多人批准的 PR。 + +多次发生过这样的情况:PR 有 3、5 个甚至更多的批准,可能是因为描述很吸引人,但当我检查时,它们实际上是坏的、有 bug,或者并没有解决它声称要解决的问题。😅 + +所以,真正重要的是你确实读过并运行过代码,并在评论里告诉我你做过这些。🤓 /// -### 别在聊天室里提问 +* 如果 PR 可以在某些方面简化,你可以提出建议,但没必要过分挑剔,很多东西比较主观(我也会有我自己的看法 🙈),因此尽量关注关键点更好。 -注意,聊天室更倾向于“闲聊”,经常有人会提出一些笼统得让人难以回答的问题,所以在这里提问一般没人回答。 +### 测试 { #tests } -GitHub Issues 里提供了模板,指引您提出正确的问题,有利于获得优质的回答,甚至可能解决您还没有想到的问题。而且就算答疑解惑要耗费不少时间,我还是会尽量在 GitHub 里回答问题。但在聊天室里,我就没功夫这么做了。😅 +* 帮我检查 PR 是否包含**测试**。 -聊天室里的聊天内容也不如 GitHub 里好搜索,聊天里的问答很容易就找不到了。只有在 GitHub Issues 里的问答才能帮助您成为 [FastAPI 专家](fastapi-people.md#_3){.internal-link target=_blank},在 GitHub Issues 中为您带来更多关注。 +* 确认在合并 PR 之前,测试**会失败**。🚨 -另一方面,聊天室里有成千上万的用户,在这里,您有很大可能遇到聊得来的人。😄 +* 然后确认合并 PR 之后,测试**能通过**。✅ -## 赞助作者 +* 很多 PR 没有测试,你可以**提醒**他们添加测试,或者你甚至可以自己**建议**一些测试。这是最耗时的部分之一,你能在这方面帮上大忙。 -您还可以通过 GitHub 赞助商资助本项目的作者(就是我)。 +* 然后也请评论你做了哪些验证,这样我就知道你检查过。🤓 -给我买杯咖啡 ☕️ 以示感谢 😄 +## 创建 Pull Request { #create-a-pull-request } -当然您也可以成为 FastAPI 的金牌或银牌赞助商。🏅🎉 +你可以通过 Pull Request 为源代码[做贡献](contributing.md){.internal-link target=_blank},例如: -## 赞助 FastAPI 使用的工具 +* 修正文档中的一个错别字。 +* 通过编辑这个文件分享你创建或发现的关于 FastAPI 的文章、视频或播客。 + * 请确保把你的链接添加到相应区块的开头。 +* 帮助把[文档翻译](contributing.md#translations){.internal-link target=_blank}成你的语言。 + * 你也可以审阅他人创建的翻译。 +* 提议新增文档章节。 +* 修复现有 issue/bug。 + * 记得添加测试。 +* 添加新功能。 + * 记得添加测试。 + * 如果相关,记得补充文档。 -如您在本文档中所见,FastAPI 站在巨人的肩膀上,它们分别是 Starlette 和 Pydantic。 +## 帮忙维护 FastAPI { #help-maintain-fastapi } -您还可以赞助: +帮我一起维护 **FastAPI** 吧!🤓 -* Samuel Colvin (Pydantic) -* Encode (Starlette, Uvicorn) +有很多工作要做,其中大部分其实**你**都能做。 + +你现在就能做的主要事情有: + +* [在 GitHub 上帮别人解答问题](#help-others-with-questions-in-github){.internal-link target=_blank}(见上面的章节)。 +* [审阅 Pull Request](#review-pull-requests){.internal-link target=_blank}(见上面的章节)。 + +这两项工作是**最耗时**的。这也是维护 FastAPI 的主要工作。 + +如果你能在这方面帮我,**你就是在帮我维护 FastAPI**,并确保它**更快更好地前进**。🚀 + +## 加入聊天 { #join-the-chat } + +加入 👥 Discord 聊天服务器 👥,和 FastAPI 社区的小伙伴们一起交流。 + +/// tip | 提示 + +关于提问,请在 GitHub Discussions 中发布,这样更有机会得到 [FastAPI 专家](fastapi-people.md#fastapi-experts){.internal-link target=_blank} 的帮助。 + +聊天仅用于其他日常交流。 + +/// + +### 别在聊天里提问 { #dont-use-the-chat-for-questions } + +请记住,聊天更偏向“自由交流”,很容易提出过于笼统、难以回答的问题,因此你可能收不到解答。 + +在 GitHub 中,模板会引导你写出恰当的问题,从而更容易获得好的回答,甚至在提问之前就能自己解决。而且在 GitHub 里,我能尽量确保最终回复每个问题,即使这需要一些时间。对聊天系统来说,我个人做不到这一点。😅 + +聊天系统中的对话也不像 GitHub 那样容易搜索,因此问答可能在聊天中淹没。而且只有在 GitHub 中的问答才会计入成为 [FastAPI 专家](fastapi-people.md#fastapi-experts){.internal-link target=_blank} 的贡献,所以你在 GitHub 上更可能获得关注。 + +另一方面,聊天系统里有成千上万的用户,你几乎随时都能在那里找到聊得来的人。😄 + +## 赞助作者 { #sponsor-the-author } + +如果你的**产品/公司**依赖或与 **FastAPI** 相关,并且你想触达它的用户,你可以通过 GitHub sponsors 赞助作者(我)。根据赞助层级,你还可能获得一些额外福利,比如在文档中展示徽章。🎁 --- diff --git a/docs/zh/docs/history-design-future.md b/docs/zh/docs/history-design-future.md index 4db5c8472..00945eab5 100644 --- a/docs/zh/docs/history-design-future.md +++ b/docs/zh/docs/history-design-future.md @@ -1,12 +1,12 @@ -# 历史、设计、未来 +# 历史、设计、未来 { #history-design-and-future } 不久前,曾有 **FastAPI** 用户问过: -> 这个项目有怎样的历史?好像它只用了几周就从默默无闻变得众所周知…… +> 这个项目有怎样的历史?好像它只用了几周就从默默无闻变得众所周知... 在此,我们简单回顾一下 **FastAPI** 的历史。 -## 备选方案 +## 备选方案 { #alternatives } 有那么几年,我曾领导数个开发团队为诸多复杂需求创建各种 API,这些需求包括机器学习、分布系统、异步任务、NoSQL 数据库等领域。 @@ -24,10 +24,9 @@ 在那几年中,我一直回避创建新的框架。首先,我尝试使用各种框架、插件、工具解决 **FastAPI** 现在的功能。 但到了一定程度之后,我别无选择,只能从之前的工具中汲取最优思路,并以尽量好的方式把这些思路整合在一起,使用之前甚至是不支持的语言特性(Python 3.6+ 的类型提示),从而创建一个能满足我所有需求的框架。 - -## 调研 +## 调研 { #investigation } 通过使用之前所有的备选方案,我有机会从它们之中学到了很多东西,获取了很多想法,并以我和我的开发团队能想到的最好方式把这些思路整合成一体。 @@ -37,7 +36,7 @@ 因此,甚至在开发 **FastAPI** 前,我就花了几个月的时间研究 OpenAPI、JSON Schema、OAuth2 等规范。深入理解它们之间的关系、重叠及区别之处。 -## 设计 +## 设计 { #design } 然后,我又花了一些时间从用户角度(使用 FastAPI 的开发者)设计了开发者 **API**。 @@ -51,7 +50,7 @@ 所有这些都是为了给开发者提供最佳的开发体验。 -## 需求项 +## 需求项 { #requirements } 经过测试多种备选方案,我最终决定使用 **Pydantic**,并充分利用它的优势。 @@ -59,11 +58,11 @@ 在开发期间,我还为 **Starlette** 做了不少贡献,这是另一个关键需求项。 -## 开发 +## 开发 { #development } 当我启动 **FastAPI** 开发的时候,绝大多数部件都已经就位,设计已经定义,需求项和工具也已经准备就绪,相关标准与规范的知识储备也非常清晰而新鲜。 -## 未来 +## 未来 { #future } 至此,**FastAPI** 及其理念已经为很多人所用。 @@ -73,6 +72,6 @@ 但,**FastAPI** 仍有很多改进的余地,也还需要添加更多的功能。 -总之,**FastAPI** 前景光明。 +**FastAPI** 前景光明。 在此,我们衷心感谢[您的帮助](help-fastapi.md){.internal-link target=_blank}。 diff --git a/docs/zh/docs/how-to/configure-swagger-ui.md b/docs/zh/docs/how-to/configure-swagger-ui.md index 108e0cb95..104baff4b 100644 --- a/docs/zh/docs/how-to/configure-swagger-ui.md +++ b/docs/zh/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` 来禁用 Swagger UI 中的语法高亮: -{* ../../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[7: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 参数,请阅读 docs for Swagger UI parameters。 +查看所有其他可用的配置,请阅读 Swagger UI 参数文档。 -## JavaScript-only 配置 +## JavaScript-only 配置 { #javascript-only-settings } Swagger UI 同样允许使用 **JavaScript-only** 配置对象(例如,JavaScript 函数)。 @@ -67,4 +67,4 @@ presets: [ 这些是 **JavaScript** 对象,而不是字符串,所以你不能直接从 Python 代码中传递它们。 -如果你需要像这样使用 JavaScript-only 配置,你可以使用上述方法之一。覆盖所有 Swagger UI *path operation* 并手动编写任何你需要的 JavaScript。 +如果你需要像这样使用 JavaScript-only 配置,你可以使用上述方法之一。覆盖所有 Swagger UI *路径操作* 并手动编写任何你需要的 JavaScript。 diff --git a/docs/zh/docs/how-to/general.md b/docs/zh/docs/how-to/general.md index e8b6dd3b2..e75ad6c79 100644 --- a/docs/zh/docs/how-to/general.md +++ b/docs/zh/docs/how-to/general.md @@ -1,39 +1,39 @@ -# 通用 - 如何操作 - 诀窍 +# 通用 - 如何操作 - 诀窍 { #general-how-to-recipes } 这里是一些指向文档中其他部分的链接,用于解答一般性或常见问题。 -## 数据过滤 - 安全性 +## 数据过滤 - 安全性 { #filter-data-security } 为确保不返回超过需要的数据,请阅读 [教程 - 响应模型 - 返回类型](../tutorial/response-model.md){.internal-link target=_blank} 文档。 -## 文档的标签 - OpenAPI +## 文档的标签 - OpenAPI { #documentation-tags-openapi } 在文档界面中添加**路径操作**的标签和进行分组,请阅读 [教程 - 路径操作配置 - Tags 参数](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} 文档。 -## 文档的概要和描述 - OpenAPI +## 文档的概要和描述 - OpenAPI { #documentation-summary-and-description-openapi } -在文档界面中添加**路径操作**的概要和描述,请阅读 [教程 - 路径操作配置 - Summary 和 Description 参数](../tutorial/path-operation-configuration.md#summary-description){.internal-link target=_blank} 文档。 +在文档界面中添加**路径操作**的概要和描述,请阅读 [教程 - 路径操作配置 - Summary 和 Description 参数](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} 文档。 -## 文档的响应描述 - OpenAPI +## 文档的响应描述 - OpenAPI { #documentation-response-description-openapi } 在文档界面中定义并显示响应描述,请阅读 [教程 - 路径操作配置 - 响应描述](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} 文档。 -## 文档弃用**路径操作** - OpenAPI +## 文档弃用**路径操作** - OpenAPI { #documentation-deprecate-a-path-operation-openapi } 在文档界面中显示弃用的**路径操作**,请阅读 [教程 - 路径操作配置 - 弃用](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} 文档。 -## 将任何数据转换为 JSON 兼容格式 +## 将任何数据转换为 JSON 兼容格式 { #convert-any-data-to-json-compatible } 要将任何数据转换为 JSON 兼容格式,请阅读 [教程 - JSON 兼容编码器](../tutorial/encoder.md){.internal-link target=_blank} 文档。 -## OpenAPI 元数据 - 文档 +## OpenAPI 元数据 - 文档 { #openapi-metadata-docs } 要添加 OpenAPI 的元数据,包括许可证、版本、联系方式等,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md){.internal-link target=_blank} 文档。 -## OpenAPI 自定义 URL +## OpenAPI 自定义 URL { #openapi-custom-url } 要自定义 OpenAPI 的 URL(或删除它),请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} 文档。 -## OpenAPI 文档 URL +## OpenAPI 文档 URL { #openapi-docs-urls } -要更改用于自动生成文档的 URL,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. +要更改用于自动生成文档的 URL,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}。 diff --git a/docs/zh/docs/how-to/index.md b/docs/zh/docs/how-to/index.md index ac097618b..980dcd1a6 100644 --- a/docs/zh/docs/how-to/index.md +++ b/docs/zh/docs/how-to/index.md @@ -1,4 +1,4 @@ -# 如何操作 - 诀窍 +# 如何操作 - 诀窍 { #how-to-recipes } 在这里,你将看到关于**多个主题**的不同诀窍或“如何操作”指南。 @@ -6,7 +6,7 @@ 如果某些内容看起来对你的项目有用,请继续查阅,否则请直接跳过它们。 -/// tip | 小技巧 +/// tip | 提示 如果你想以系统的方式**学习 FastAPI**(推荐),请阅读 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 的每一章节。 diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 9f4b6d3d5..1c2aea328 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

FastAPI 框架,高性能,易于学习,高效编码,生产可用 @@ -27,135 +27,140 @@ --- -**文档**: https://fastapi.tiangolo.com +**文档**: https://fastapi.tiangolo.com **源码**: https://github.com/fastapi/fastapi --- -FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。 +FastAPI 是一个用于构建 API 的现代、快速(高性能)的 Web 框架,使用 Python 并基于标准的 Python 类型提示。 -关键特性: +关键特性: -* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 +* **快速**:极高性能,可与 **NodeJS** 和 **Go** 并肩(归功于 Starlette 和 Pydantic)。[最快的 Python 框架之一](#performance)。 +* **高效编码**:功能开发速度提升约 200% ~ 300%。* +* **更少 bug**:人为(开发者)错误减少约 40%。* +* **直观**:极佳的编辑器支持。处处皆可自动补全。更少的调试时间。 +* **易用**:为易用和易学而设计。更少的文档阅读时间。 +* **简短**:最小化代码重复。一次参数声明即可获得多种功能。更少的 bug。 +* **健壮**:生产可用级代码。并带有自动生成的交互式文档。 +* **标准化**:基于(并完全兼容)API 的开放标准:OpenAPI(以前称为 Swagger)和 JSON Schema。 -* **高效编码**:提高功能开发速度约 200% 至 300%。* -* **更少 bug**:减少约 40% 的人为(开发者)导致错误。* -* **智能**:极佳的编辑器支持。处处皆可自动补全,减少调试时间。 -* **简单**:设计的易于使用和学习,阅读文档的时间更短。 -* **简短**:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。 -* **健壮**:生产可用级别的代码。还有自动生成的交互式文档。 -* **标准化**:基于(并完全兼容)API 的相关开放标准:OpenAPI (以前被称为 Swagger) 和 JSON Schema。 +* 基于某内部开发团队在构建生产应用时的测试估算。 -* 根据对某个构建线上应用的内部开发团队所进行的测试估算得出。 - -## Sponsors +## 赞助商 { #sponsors } -{% if sponsors %} +### Keystone 赞助商 { #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 } -「_[...] 最近我一直在使用 **FastAPI**。[...] 实际上我正在计划将其用于我所在的**微软**团队的所有**机器学习服务**。其中一些服务正被集成进核心 **Windows** 产品和一些 **Office** 产品。_」 +「_[...] 最近我大量使用 **FastAPI**。[...] 我实际上计划把它用于我团队在 **微软** 的所有 **机器学习服务**。其中一些正在集成进核心 **Windows** 产品以及一些 **Office** 产品。_」 -

Kabir Khan - 微软 (ref)
+
Kabir Khan - Microsoft (ref)
--- -「_我们选择了 **FastAPI** 来创建用于获取**预测结果**的 **REST** 服务。[用于 Ludwig]_」 +「_我们采用 **FastAPI** 来构建可查询以获取**预测结果**的 **REST** 服务器。[用于 Ludwig]_」 -
Piero Molino,Yaroslav Dudin 和 Sai Sumanth Miryala - Uber (ref)
+
Piero Molino,Yaroslav Dudin,Sai Sumanth Miryala - Uber (ref)
--- -「_**Netflix** 非常高兴地宣布,正式开源我们的**危机管理**编排框架:**Dispatch**![使用 **FastAPI** 构建]_」 +「_**Netflix** 很高兴宣布开源我们的**危机管理**编排框架:**Dispatch**![使用 **FastAPI** 构建]_」
Kevin Glisson,Marc Vilanova,Forest Monsen - Netflix (ref)
--- -「_**FastAPI** 让我兴奋的欣喜若狂。它太棒了!_」 +「_我对 **FastAPI** 兴奋到飞起。它太有趣了!_」
Brian Okken - Python Bytes 播客主持人 (ref)
--- -「_老实说,你的作品看起来非常可靠和优美。在很多方面,这就是我想让 **Hug** 成为的样子 - 看到有人实现了它真的很鼓舞人心。_」 +「_老实说,你构建的东西非常稳健而且打磨得很好。从很多方面看,这就是我想让 **Hug** 成为的样子 —— 看到有人把它做出来真的很鼓舞人心。_」
Timothy Crosley - Hug 作者 (ref)
--- -「_如果你正打算学习一个**现代框架**用来构建 REST API,来看下 **FastAPI** [...] 它快速、易用且易于学习 [...]_」 +「_如果你想学一个用于构建 REST API 的**现代框架**,看看 **FastAPI** [...] 它快速、易用且易学 [...]_」 -「_我们已经将 **API** 服务切换到了 **FastAPI** [...] 我认为你会喜欢它的 [...]_」 +「_我们已经把我们的 **API** 切换到 **FastAPI** [...] 我想你会喜欢它 [...]_」
Ines Montani - Matthew Honnibal - Explosion AI 创始人 - spaCy 作者 (ref) - (ref)
--- -## **Typer**,命令行中的 FastAPI +「_如果有人正在构建生产级的 Python API,我强烈推荐 **FastAPI**。它**设计优雅**、**使用简单**且**高度可扩展**,已经成为我们 API 优先开发战略中的**关键组件**,并驱动了许多自动化和服务,比如我们的 Virtual TAC Engineer。_」 + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## FastAPI 迷你纪录片 { #fastapi-mini-documentary } + +在 2025 年末发布了一部FastAPI 迷你纪录片,你可以在线观看: + +FastAPI Mini Documentary + +## **Typer**,命令行中的 FastAPI { #typer-the-fastapi-of-clis } -如果你正在开发一个在终端中运行的命令行应用而不是 web API,不妨试下 **Typer**。 +如果你要开发一个用于终端的 命令行应用而不是 Web API,看看 **Typer**。 -**Typer** 是 FastAPI 的小同胞。它想要成为**命令行中的 FastAPI**。 ⌨️ 🚀 +**Typer** 是 FastAPI 的小同胞。它的目标是成为**命令行中的 FastAPI**。⌨️ 🚀 -## 依赖 +## 依赖 { #requirements } -Python 及更高版本 +FastAPI 站在巨人的肩膀之上: -FastAPI 站在以下巨人的肩膀之上: - -* Starlette 负责 web 部分。 +* Starlette 负责 Web 部分。 * Pydantic 负责数据部分。 -## 安装 +## 安装 { #installation } + +创建并激活一个虚拟环境,然后安装 FastAPI:
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-你还会需要一个 ASGI 服务器,生产环境可以使用 Uvicorn 或者 Hypercorn。 +**Note**: 请确保把 `"fastapi[standard]"` 用引号包起来,以保证在所有终端中都能正常工作。 -
+## 示例 { #example } -```console -$ pip install "uvicorn[standard]" +### 创建 { #create-it } ----> 100% -``` - -
- -## 示例 - -### 创建 - -* 创建一个 `main.py` 文件并写入以下内容: +创建文件 `main.py`,内容如下: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -167,18 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: 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 +如果你的代码里会用到 `async` / `await`,请使用 `async def`: +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -190,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` **Note**: -如果你不知道是否会用到,可以查看文档的 _"In a hurry?"_ 章节中 关于 `async` 和 `await` 的部分。 +如果你不确定,请查看文档中 _"In a hurry?"_ 章节的`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. ``` @@ -219,58 +235,56 @@ 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"} ``` -你已经创建了一个具有以下功能的 API: +你已经创建了一个 API,它可以: -* 通过 _路径_ `/` 和 `/items/{item_id}` 接受 HTTP 请求。 -* 以上 _路径_ 都接受 `GET` 操作(也被称为 HTTP _方法_)。 -* `/items/{item_id}` _路径_ 有一个 _路径参数_ `item_id` 并且应该为 `int` 类型。 -* `/items/{item_id}` _路径_ 有一个可选的 `str` 类型的 _查询参数_ `q`。 +* 在路径 `/` 和 `/items/{item_id}` 接收 HTTP 请求。 +* 以上两个路径都接受 `GET` 操作(也称为 HTTP 方法)。 +* 路径 `/items/{item_id}` 有一个应为 `int` 的路径参数 `item_id`。 +* 路径 `/items/{item_id}` 有一个可选的 `str` 类型查询参数 `q`。 -### 交互式 API 文档 +### 交互式 API 文档 { #interactive-api-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` 请求的请求体。 -我们借助 Pydantic 来使用标准的 Python 类型声明请求体。 - -```Python hl_lines="4 9-12 25-27" -from typing import Union +借助 Pydantic,使用标准的 Python 类型来声明请求体。 +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -280,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -289,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -298,173 +312,248 @@ 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。 +现在访问 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) -### 可选文档升级 +### 可选文档升级 { #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 及更高版本**。 +只需要标准的 **Python**。 -举个例子,比如声明 `int` 类型: +例如,一个 `int`: ```Python item_id: int ``` -或者一个更复杂的 `Item` 模型: +或者更复杂的 `Item` 模型: ```Python item: Item ``` -......在进行一次声明之后,你将获得: +……通过一次声明,你将获得: * 编辑器支持,包括: - * 自动补全 - * 类型检查 + * 自动补全。 + * 类型检查。 * 数据校验: - * 在校验失败时自动生成清晰的错误信息 - * 对多层嵌套的 JSON 对象依然执行校验 -* 转换 来自网络请求的输入数据为 Python 数据类型。包括以下数据: - * JSON - * 路径参数 - * 查询参数 - * Cookies - * 请求头 - * 表单 - * 文件 -* 转换 输出的数据:转换 Python 数据类型为供网络传输的 JSON 数据: - * 转换 Python 基础类型 (`str`、 `int`、 `float`、 `bool`、 `list` 等) - * `datetime` 对象 - * `UUID` 对象 - * 数据库模型 - * ......以及更多其他类型 + * 当数据无效时自动生成清晰的错误信息。 + * 即便是多层嵌套的 JSON 对象也会进行校验。 +* 输入数据的转换:从网络读取到 Python 数据和类型。读取来源: + * JSON。 + * 路径参数。 + * 查询参数。 + * Cookies。 + * Headers。 + * Forms。 + * Files。 +* 输出数据的转换:从 Python 数据和类型转换为网络数据(JSON): + * 转换 Python 类型(`str`、`int`、`float`、`bool`、`list` 等)。 + * `datetime` 对象。 + * `UUID` 对象。 + * 数据库模型。 + * ……以及更多。 * 自动生成的交互式 API 文档,包括两种可选的用户界面: - * Swagger UI - * ReDoc + * Swagger UI。 + * ReDoc。 --- -回到前面的代码示例,**FastAPI** 将会: +回到之前的代码示例,**FastAPI** 将会: -* 校验 `GET` 和 `PUT` 请求的路径中是否含有 `item_id`。 +* 校验 `GET` 和 `PUT` 请求的路径中是否包含 `item_id`。 * 校验 `GET` 和 `PUT` 请求中的 `item_id` 是否为 `int` 类型。 - * 如果不是,客户端将会收到清晰有用的错误信息。 -* 检查 `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` 类型。 - * 以上过程对于多层嵌套的 JSON 对象同样也会执行 -* 自动对 JSON 进行转换或转换成 JSON。 -* 通过 OpenAPI 文档来记录所有内容,可被用于: - * 交互式文档系统 - * 许多编程语言的客户端代码自动生成系统 -* 直接提供 2 种交互式文档 web 界面。 + * 如果不是,客户端会看到清晰有用的错误信息。 +* 对于 `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`。 + * 对于多层嵌套的 JSON 对象,同样适用。 +* 自动完成 JSON 的读取与输出转换。 +* 使用 OpenAPI 记录所有内容,可用于: + * 交互式文档系统。 + * 多语言的客户端代码自动生成系统。 +* 直接提供 2 种交互式文档 Web 界面。 --- -虽然我们才刚刚开始,但其实你已经了解了这一切是如何工作的。 +我们只是浅尝辄止,但你已经大致了解其工作方式了。 -尝试更改下面这行代码: +尝试把这一行: ```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) -教程 - 用户指南 中有包含更多特性的更完整示例。 +更多包含更多特性的完整示例,请参阅 教程 - 用户指南。 -**剧透警告**: 教程 - 用户指南中的内容有: +**剧透警告**:教程 - 用户指南包括: -* 对来自不同地方的参数进行声明,如:**请求头**、**cookies**、**form 表单**以及**上传的文件**。 -* 如何设置**校验约束**如 `maximum_length` 或者 `regex`。 -* 一个强大并易于使用的 **依赖注入** 系统。 -* 安全性和身份验证,包括通过 **JWT 令牌**和 **HTTP 基本身份认证**来支持 **OAuth2**。 -* 更进阶(但同样简单)的技巧来声明 **多层嵌套 JSON 模型** (借助 Pydantic)。 -* 许多额外功能(归功于 Starlette)比如: +* 来自不同位置的**参数**声明:**headers**、**cookies**、**form 字段**和**文件**。 +* 如何设置**校验约束**,如 `maximum_length` 或 `regex`。 +* 功能强大且易用的 **依赖注入** 系统。 +* 安全与认证,包括对 **OAuth2**、**JWT tokens** 和 **HTTP Basic** 认证的支持。 +* 更高级(但同样简单)的 **多层嵌套 JSON 模型** 声明技巧(得益于 Pydantic)。 +* 通过 Strawberry 等库进行 **GraphQL** 集成。 +* 许多额外特性(归功于 Starlette),例如: * **WebSockets** - * **GraphQL** * 基于 HTTPX 和 `pytest` 的极其简单的测试 * **CORS** * **Cookie Sessions** - * ......以及更多 + * ……以及更多。 -## 性能 +### 部署你的应用(可选) { #deploy-your-app-optional } -独立机构 TechEmpower 所作的基准测试结果显示,基于 Uvicorn 运行的 **FastAPI** 程序是 最快的 Python web 框架之一,仅次于 Starlette 和 Uvicorn 本身(FastAPI 内部使用了它们)。(*) +你可以选择把 FastAPI 应用部署到 FastAPI Cloud,如果还没有的话去加入候补名单吧。🚀 -想了解更多,请查阅 基准测试 章节。 +如果你已经有 **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。 + +它把用 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 } + +当你通过 `pip install "fastapi[standard]"` 安装 FastAPI 时,会包含 `standard` 组的一些可选依赖: + +Pydantic 使用: * email-validator - 用于 email 校验。 -用于 Starlette: +Starlette 使用: -* httpx - 使用 `TestClient` 时安装。 -* jinja2 - 使用默认模板配置时安装。 -* python-multipart - 需要通过 `request.form()` 对表单进行「解析」时安装。 -* itsdangerous - 需要 `SessionMiddleware` 支持时安装。 -* pyyaml - 使用 Starlette 提供的 `SchemaGenerator` 时安装(有 FastAPI 你可能并不需要它)。 -* graphene - 需要 `GraphQLApp` 支持时安装。 +* httpx - 使用 `TestClient` 时需要。 +* jinja2 - 使用默认模板配置时需要。 +* python-multipart - 使用 `request.form()` 支持表单「解析」时需要。 -用于 FastAPI / Starlette: +FastAPI 使用: -* uvicorn - 用于加载和运行你的应用程序的服务器。 -* orjson - 使用 `ORJSONResponse` 时安装。 -* ujson - 使用 `UJSONResponse` 时安装。 +* uvicorn - 加载并提供你的应用的服务器。包含 `uvicorn[standard]`,其中包含高性能服务所需的一些依赖(例如 `uvloop`)。 +* `fastapi-cli[standard]` - 提供 `fastapi` 命令。 + * 其中包含 `fastapi-cloud-cli`,它允许你将 FastAPI 应用部署到 FastAPI Cloud。 -你可以通过 `pip install "fastapi[all]"` 命令来安装以上所有依赖。 +### 不包含 `standard` 依赖 { #without-standard-dependencies } -## 许可协议 +如果你不想包含这些 `standard` 可选依赖,可以使用 `pip install fastapi`,而不是 `pip install "fastapi[standard]"`。 + +### 不包含 `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +如果你想安装带有 standard 依赖但不包含 `fastapi-cloud-cli` 的 FastAPI,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"`。 + +### 其他可选依赖 { #additional-optional-dependencies } + +还有一些你可能想安装的可选依赖。 + +额外的 Pydantic 可选依赖: + +* pydantic-settings - 用于配置管理。 +* pydantic-extra-types - 用于在 Pydantic 中使用的额外类型。 + +额外的 FastAPI 可选依赖: + +* orjson - 使用 `ORJSONResponse` 时需要。 +* ujson - 使用 `UJSONResponse` 时需要。 + +## 许可协议 { #license } 该项目遵循 MIT 许可协议。 diff --git a/docs/zh/docs/learn/index.md b/docs/zh/docs/learn/index.md index 38696f6fe..144d5b2a9 100644 --- a/docs/zh/docs/learn/index.md +++ b/docs/zh/docs/learn/index.md @@ -1,4 +1,4 @@ -# 学习 +# 学习 { #learn } 以下是学习 **FastAPI** 的介绍部分和教程。 diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md index 48eb990df..a6ad9f94a 100644 --- a/docs/zh/docs/project-generation.md +++ b/docs/zh/docs/project-generation.md @@ -1,28 +1,28 @@ -# FastAPI全栈模板 +# FastAPI全栈模板 { #full-stack-fastapi-template } -模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁 +模板通常带有特定的设置,但它们被设计为灵活且可定制。这样你可以根据项目需求进行修改和调整,使其成为很好的起点。🏁 -您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。 +你可以使用此模板开始,它已经为你完成了大量的初始设置、安全性、数据库以及一些 API 端点。 -代码仓: Full Stack FastAPI Template +GitHub 仓库: Full Stack FastAPI Template -## FastAPI全栈模板 - 技术栈和特性 +## 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数据库。 +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/zh) 用于 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) 用于前端组件。 - - 🤖 一个自动化生成的前端客户端。 - - 🧪 [Playwright](https://playwright.dev)用于端到端测试。 - - 🦇 支持暗黑主题(Dark mode)。 -- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。 -- 🔒 默认使用密码哈希来保证安全。 -- 🔑 JWT令牌用于权限验证。 -- 📫 使用邮箱来进行密码恢复。 -- ✅ 单元测试用了[Pytest](https://pytest.org). -- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。 -- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。 -- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。 + - 💃 使用 TypeScript、hooks、Vite 以及现代前端技术栈的其他部分。 + - 🎨 [Tailwind CSS](https://tailwindcss.com) 与 [shadcn/ui](https://ui.shadcn.com) 用于前端组件。 + - 🤖 自动生成的前端客户端。 + - 🧪 [Playwright](https://playwright.dev) 用于端到端测试。 + - 🦇 支持暗黑模式。 +- 🐋 [Docker Compose](https://www.docker.com) 用于开发与生产。 +- 🔒 默认启用安全的密码哈希。 +- 🔑 JWT(JSON Web Token)认证。 +- 📫 基于邮箱的密码找回。 +- ✅ 使用 [Pytest](https://pytest.org) 进行测试。 +- 📞 [Traefik](https://traefik.io) 用作反向代理/负载均衡。 +- 🚢 使用 Docker Compose 的部署指南,包括如何设置前端 Traefik 代理以自动处理 HTTPS 证书。 +- 🏭 基于 GitHub Actions 的 CI(持续集成)与 CD(持续部署)。 diff --git a/docs/zh/docs/python-types.md b/docs/zh/docs/python-types.md index a7f76d97f..3e1c593c1 100644 --- a/docs/zh/docs/python-types.md +++ b/docs/zh/docs/python-types.md @@ -1,31 +1,30 @@ -# Python 类型提示简介 +# Python 类型提示简介 { #python-types-intro } -**Python 3.6+ 版本**加入了对"类型提示"的支持。 +Python 支持可选的“类型提示”(也叫“类型注解”)。 -这些**"类型提示"**是一种新的语法(在 Python 3.6 版本加入)用来声明一个变量的类型。 +这些“类型提示”或注解是一种特殊语法,用来声明变量的类型。 -通过声明变量的类型,编辑器和一些工具能给你提供更好的支持。 +通过为变量声明类型,编辑器和工具可以为你提供更好的支持。 -这只是一个关于 Python 类型提示的**快速入门 / 复习**。它仅涵盖与 **FastAPI** 一起使用所需的最少部分...实际上只有很少一点。 +这只是一个关于 Python 类型提示的快速入门/复习。它只涵盖与 **FastAPI** 一起使用所需的最少部分……实际上非常少。 -整个 **FastAPI** 都基于这些类型提示构建,它们带来了许多优点和好处。 +**FastAPI** 完全基于这些类型提示构建,它们带来了许多优势和好处。 -但即使你不会用到 **FastAPI**,了解一下类型提示也会让你从中受益。 +但即使你从不使用 **FastAPI**,了解一些类型提示也会让你受益。 -/// note +/// note | 注意 -如果你已经精通 Python,并且了解关于类型提示的一切知识,直接跳到下一章节吧。 +如果你已经是 Python 专家,并且对类型提示了如指掌,可以跳到下一章。 /// -## 动机 +## 动机 { #motivation } 让我们从一个简单的例子开始: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -运行这段程序将输出: +运行这个程序会输出: ``` John Doe @@ -33,38 +32,37 @@ John Doe 这个函数做了下面这些事情: -* 接收 `first_name` 和 `last_name` 参数。 -* 通过 `title()` 将每个参数的第一个字母转换为大写形式。 -* 中间用一个空格来拼接它们。 +* 接收 `first_name` 和 `last_name`。 +* 通过 `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 @@ -78,227 +76,389 @@ John Doe 就是这样。 -这些就是"类型提示": +这些就是“类型提示”: -{* ../../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] *} +因为编辑器知道变量的类型,你不仅能得到补全,还能获得错误检查: -因为编辑器已经知道了这些变量的类型,所以不仅能对代码进行补全,还能检查其中的错误: + - +现在你知道需要修复它,用 `str(age)` 把 `age` 转成字符串: -现在你知道了必须先修复这个问题,通过 `str(age)` 把 `age` 转换成字符串: +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +## 声明类型 { #declaring-types } +你刚刚看到的是声明类型提示的主要位置:函数参数。 -## 声明类型 +这也是你在 **FastAPI** 中使用它们的主要场景。 -你刚刚看到的就是声明类型提示的主要场景。用于函数的参数。 +### 简单类型 { #simple-types } -这也是你将在 **FastAPI** 中使用它们的主要场景。 +你不仅可以声明 `str`,还可以声明所有标准的 Python 类型。 -### 简单类型 - -不只是 `str`,你能够声明所有的标准 Python 类型。 - -比如以下类型: +例如: * `int` * `float` * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### 带类型参数的泛型类型 { #generic-types-with-type-parameters } -### 嵌套类型 +有些数据结构可以包含其他值,比如 `dict`、`list`、`set` 和 `tuple`。而内部的值也会有自己的类型。 -有些容器数据结构可以包含其他的值,比如 `dict`、`list`、`set` 和 `tuple`。它们内部的值也会拥有自己的类型。 +这些带有内部类型的类型称为“泛型”(generic)类型。可以把它们连同内部类型一起声明出来。 -你可以使用 Python 的 `typing` 标准库来声明这些类型以及子类型。 +要声明这些类型以及内部类型,你可以使用 Python 标准库模块 `typing`。它就是为支持这些类型提示而存在的。 -它专门用来支持这些类型提示。 +#### 更新的 Python 版本 { #newer-versions-of-python } -#### 列表 +使用 `typing` 的语法与所有版本兼容,从 Python 3.6 到最新版本(包括 Python 3.9、Python 3.10 等)。 -例如,让我们来定义一个由 `str` 组成的 `list` 变量。 +随着 Python 的发展,更新的版本对这些类型注解的支持更好,在很多情况下你甚至不需要导入和使用 `typing` 模块来声明类型注解。 -从 `typing` 模块导入 `List`(注意是大写的 `L`): +如果你可以为项目选择更高版本的 Python,你将能享受到这种额外的简化。 -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +在整个文档中,会根据不同 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 } -由于列表是带有"子类型"的类型,所以我们把子类型放在方括号中: +例如,我们来定义一个由 `str` 组成的 `list` 变量。 -{* ../../docs_src/python_types/tutorial006.py hl[4] *} +用同样的冒号(`:`)语法声明变量。 +类型写 `list`。 -这表示:"变量 `items` 是一个 `list`,并且这个列表里的每一个元素都是 `str`"。 +因为 list 是一种包含内部类型的类型,把内部类型写在方括号里: -这样,即使在处理列表中的元素时,你的编辑器也可以提供支持。 +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -没有类型,几乎是不可能实现下面这样: +/// info | 信息 - +方括号中的这些内部类型称为“类型参数”(type parameters)。 -注意,变量 `item` 是列表 `items` 中的元素之一。 +在这个例子中,`str` 是传给 `list` 的类型参数。 -而且,编辑器仍然知道它是一个 `str`,并为此提供了支持。 +/// -#### 元组和集合 +这表示:“变量 `items` 是一个 `list`,并且列表中的每一个元素都是 `str`”。 -声明 `tuple` 和 `set` 的方法也是一样的: +这样,即使是在处理列表中的元素时,编辑器也能给你提供支持: -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} + +没有类型的话,这几乎是不可能做到的。 + +注意,变量 `item` 是列表 `items` 中的一个元素。 + +即便如此,编辑器仍然知道它是 `str`,并为此提供支持。 + +#### 元组和集合 { #tuple-and-set } + +声明 `tuple` 和 `set` 的方式类似: + +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} 这表示: -* 变量 `items_t` 是一个 `tuple`,其中的前两个元素都是 `int` 类型, 最后一个元素是 `str` 类型。 -* 变量 `items_s` 是一个 `set`,其中的每个元素都是 `bytes` 类型。 +* 变量 `items_t` 是一个含有 3 个元素的 `tuple`,分别是一个 `int`、另一个 `int`,以及一个 `str`。 +* 变量 `items_s` 是一个 `set`,其中每个元素的类型是 `bytes`。 -#### 字典 +#### 字典 { #dict } -定义 `dict` 时,需要传入两个子类型,用逗号进行分隔。 +定义 `dict` 时,需要传入 2 个类型参数,用逗号分隔。 -第一个子类型声明 `dict` 的所有键。 +第一个类型参数用于字典的键。 -第二个子类型声明 `dict` 的所有值: - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} +第二个类型参数用于字典的值: +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} 这表示: * 变量 `prices` 是一个 `dict`: - * 这个 `dict` 的所有键为 `str` 类型(可以看作是字典内每个元素的名称)。 - * 这个 `dict` 的所有值为 `float` 类型(可以看作是字典内每个元素的价格)。 + * 这个 `dict` 的键是 `str` 类型(比如,每个条目的名称)。 + * 这个 `dict` 的值是 `float` 类型(比如,每个条目的价格)。 -### 类作为类型 +#### Union { #union } -你也可以将类声明为变量的类型。 +你可以声明一个变量可以是若干种类型中的任意一种,比如既可以是 `int` 也可以是 `str`。 -假设你有一个名为 `Person` 的类,拥有 name 属性: +在 Python 3.6 及以上(包括 Python 3.10),你可以使用 `typing` 中的 `Union`,把可能的类型放到方括号里。 -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - - -接下来,你可以将一个变量声明为 `Person` 类型: - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - - -然后,你将再次获得所有的编辑器支持: - - - -## Pydantic 模型 - -Pydantic 是一个用来执行数据校验的 Python 库。 - -你可以将数据的"结构"声明为具有属性的类。 - -每个属性都拥有类型。 - -接着你用一些值来创建这个类的实例,这些值会被校验,并被转换为适当的类型(在需要的情况下),返回一个包含所有数据的对象。 - -然后,你将获得这个对象的所有编辑器支持。 - -下面的例子来自 Pydantic 官方文档: +在 Python 3.10 中还有一种新的语法,可以用竖线(`|`)把可能的类型分隔开。 //// tab | Python 3.10+ -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} ``` //// //// tab | Python 3.9+ -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} ``` //// -//// tab | Python 3.8+ +两种方式的含义一致:`item` 可以是 `int` 或 `str`。 -```Python -{!> ../../docs_src/python_types/tutorial011.py!} +#### 可能为 `None` { #possibly-none } + +你可以声明一个值的类型是某种类型(比如 `str`),但它也可能是 `None`。 + +在 Python 3.6 及以上(包括 Python 3.10),你可以通过从 `typing` 模块导入并使用 `Optional` 来声明: + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +使用 `Optional[str]` 而不是仅仅 `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+ -/// info +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` -想进一步了解 Pydantic,请阅读其文档. +//// + +//// tab | Python 3.9+ 另一种写法 + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### 使用 `Union` 或 `Optional` { #using-union-or-optional } + +如果你使用的是 3.10 以下的 Python 版本,这里有个来自我非常主观的建议: + +* 🚨 避免使用 `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` 仍然是必填的(不是“可选”),因为它没有默认值。不过,`name` 接受 `None` 作为值: + +```Python +say_hi(name=None) # 这样可以,None 是有效值 🎉 +``` + +好消息是,一旦你使用 Python 3.10,就无需再为此操心,因为你可以直接用 `|` 来定义类型联合: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +这样你就不必再考虑 `Optional` 和 `Union` 这些名字了。😎 + +#### 泛型类型 { #generic-types } + +这些在方括号中接收类型参数的类型称为“泛型类型”(Generic types)或“泛型”(Generics),例如: + +//// tab | Python 3.10+ + +你可以把同样的内建类型作为泛型使用(带方括号和内部类型): + +* `list` +* `tuple` +* `set` +* `dict` + +以及与之前的 Python 版本一样,来自 `typing` 模块的: + +* `Union` +* `Optional` +* ……以及其他。 + +在 Python 3.10 中,作为使用泛型 `Union` 和 `Optional` 的替代,你可以使用竖线(`|`)来声明类型联合,这更好也更简单。 + +//// + +//// tab | Python 3.9+ + +你可以把同样的内建类型作为泛型使用(带方括号和内部类型): + +* `list` +* `tuple` +* `set` +* `dict` + +以及来自 `typing` 模块的泛型: + +* `Union` +* `Optional` +* ……以及其他。 + +//// + +### 类作为类型 { #classes-as-types } + +你也可以把类声明为变量的类型。 + +假设你有一个名为 `Person` 的类,带有 name: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} + +然后你可以声明一个变量是 `Person` 类型: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} + +接着,你会再次获得所有的编辑器支持: + + + +注意,这表示“`one_person` 是类 `Person` 的一个实例(instance)”。 + +它并不表示“`one_person` 是名为 `Person` 的类本身(class)”。 + +## Pydantic 模型 { #pydantic-models } + +Pydantic 是一个用于执行数据校验的 Python 库。 + +你将数据的“结构”声明为带有属性的类。 + +每个属性都有一个类型。 + +然后你用一些值创建这个类的实例,它会校验这些值,并在需要时把它们转换为合适的类型,返回一个包含所有数据的对象。 + +你还能对这个结果对象获得完整的编辑器支持。 + +下面是来自 Pydantic 官方文档的一个示例: + +{* ../../docs_src/python_types/tutorial011_py310.py *} + +/// info | 信息 + +想了解更多关于 Pydantic 的信息,请查看其文档。 /// -整个 **FastAPI** 建立在 Pydantic 的基础之上。 +**FastAPI** 完全建立在 Pydantic 之上。 -实际上你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 看到很多这种情况。 +你会在[教程 - 用户指南](tutorial/index.md){.internal-link target=_blank}中看到更多的实战示例。 -## **FastAPI** 中的类型提示 +/// tip | 提示 -**FastAPI** 利用这些类型提示来做下面几件事。 - -使用 **FastAPI** 时用类型提示声明参数可以获得: - -* **编辑器支持**。 -* **类型检查**。 - -...并且 **FastAPI** 还会用这些类型声明来: - -* **定义参数要求**:声明对请求路径参数、查询参数、请求头、请求体、依赖等的要求。 -* **转换数据**:将来自请求的数据转换为需要的类型。 -* **校验数据**: 对于每一个请求: - * 当数据校验失败时自动生成**错误信息**返回给客户端。 -* 使用 OpenAPI **记录** API: - * 然后用于自动生成交互式文档的用户界面。 - -听上去有点抽象。不过不用担心。你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 中看到所有的实战。 - -最重要的是,通过使用标准的 Python 类型,只需要在一个地方声明(而不是添加更多的类、装饰器等),**FastAPI** 会为你完成很多的工作。 - -/// info - -如果你已经阅读了所有教程,回过头来想了解有关类型的更多信息,来自 `mypy` 的"速查表"是不错的资源。 +当你在没有默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,Pydantic 有一个特殊行为,你可以在 Pydantic 文档的 必填的 Optional 字段 中了解更多。 + +/// + +## 带元数据注解的类型提示 { #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** 中,用类型提示来声明参数,你将获得: + +* 编辑器支持。 +* 类型检查。 + +……并且 **FastAPI** 会使用相同的声明来: + +* 定义要求:从请求路径参数、查询参数、请求头、请求体、依赖等。 +* 转换数据:把请求中的数据转换为所需类型。 +* 校验数据:对于每个请求: + * 当数据无效时,自动生成错误信息返回给客户端。 +* 使用 OpenAPI 记录 API: + * 然后用于自动生成交互式文档界面。 + +这些听起来可能有点抽象。别担心。你会在[教程 - 用户指南](tutorial/index.md){.internal-link target=_blank}中看到所有这些的实际效果。 + +重要的是,通过使用标准的 Python 类型,而且只在一个地方声明(而不是添加更多类、装饰器等),**FastAPI** 会为你完成大量工作。 + +/// info | 信息 + +如果你已经读完所有教程,又回来想进一步了解类型,一个不错的资源是 `mypy` 的“速查表”。 /// diff --git a/docs/zh/docs/tutorial/background-tasks.md b/docs/zh/docs/tutorial/background-tasks.md index b9becd8bf..8398472c3 100644 --- a/docs/zh/docs/tutorial/background-tasks.md +++ b/docs/zh/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# 后台任务 +# 后台任务 { #background-tasks } 你可以定义在返回响应后运行的后台任务。 @@ -11,15 +11,15 @@ * 处理数据: * 例如,假设您收到的文件必须经过一个缓慢的过程,您可以返回一个"Accepted"(HTTP 202)响应并在后台处理它。 -## 使用 `BackgroundTasks` +## 使用 `BackgroundTasks` { #using-backgroundtasks } 首先导入 `BackgroundTasks` 并在 *路径操作函数* 中使用类型声明 `BackgroundTasks` 定义一个参数: -{* ../../docs_src/background_tasks/tutorial001.py hl[1, 13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** 会创建一个 `BackgroundTasks` 类型的对象并作为该参数传入。 -## 创建一个任务函数 +## 创建一个任务函数 { #create-a-task-function } 创建要作为后台任务运行的函数。 @@ -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()` 接收以下参数: @@ -45,53 +45,13 @@ * 应按顺序传递给任务函数的任意参数序列(`email`)。 * 应传递给任务函数的任意关键字参数(`message="some notification"`)。 -## 依赖注入 +## 依赖注入 { #dependency-injection } 使用 `BackgroundTasks` 也适用于依赖注入系统,你可以在多个级别声明 `BackgroundTasks` 类型的参数:在 *路径操作函数* 里,在依赖中(可依赖),在子依赖中,等等。 **FastAPI** 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务被合并在一起并且随后在后台运行: -//// tab | Python 3.10+ - -{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13, 15, 22, 25] *} - -//// - -//// tab | Python 3.9+ - -{* ../../docs_src/background_tasks/tutorial002_an_py39.py hl[13, 15, 22, 25] *} - -//// - -//// tab | Python 3.8+ - -{* ../../docs_src/background_tasks/tutorial002_an.py hl[14, 16, 23, 26] *} - -//// - -//// tab | Python 3.10+ 没Annotated - -/// tip - -尽可能选择使用 `Annotated` 的版本。 - -/// - -{* ../../docs_src/background_tasks/tutorial002_py310.py hl[11, 13, 20, 23] *} - -//// - -//// tab | Python 3.8+ 没Annotated - -/// tip - -尽可能选择使用 `Annotated` 的版本。 - -/// - -{* ../../docs_src/background_tasks/tutorial002.py hl[13, 15, 22, 25] *} - -//// +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} 该示例中,信息会在响应发出 *之后* 被写到 `log.txt` 文件。 @@ -99,7 +59,7 @@ 然后另一个在 *路径操作函数* 生成的后台任务会使用路径参数 `email` 写入一条信息。 -## 技术细节 +## 技术细节 { #technical-details } `BackgroundTasks` 类直接来自 `starlette.background`。 @@ -111,7 +71,7 @@ 更多细节查看 Starlette's official docs for Background Tasks. -## 告诫 +## 告诫 { #caveat } 如果您需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,您不需要共享内存、变量等),那么使用其他更大的工具(如 Celery)可能更好。 @@ -119,6 +79,6 @@ 但是,如果您需要从同一个**FastAPI**应用程序访问变量和对象,或者您需要执行小型后台任务(如发送电子邮件通知),您只需使用 `BackgroundTasks` 即可。 -## 回顾 +## 回顾 { #recap } 导入并使用 `BackgroundTasks` 通过 *路径操作函数* 中的参数和依赖项来添加后台任务。 diff --git a/docs/zh/docs/tutorial/bigger-applications.md b/docs/zh/docs/tutorial/bigger-applications.md index 554bc654f..1ced002dc 100644 --- a/docs/zh/docs/tutorial/bigger-applications.md +++ b/docs/zh/docs/tutorial/bigger-applications.md @@ -1,16 +1,16 @@ -# 更大的应用 - 多个文件 +# 更大的应用 - 多个文件 { #bigger-applications-multiple-files } 如果你正在开发一个应用程序或 Web API,很少会将所有的内容都放在一个文件中。 **FastAPI** 提供了一个方便的工具,可以在保持所有灵活性的同时构建你的应用程序。 -/// info +/// info | 信息 如果你来自 Flask,那这将相当于 Flask 的 Blueprints。 /// -## 一个文件结构示例 +## 一个文件结构示例 { #an-example-file-structure } 假设你的文件结构如下: @@ -29,7 +29,7 @@ │   └── admin.py ``` -/// tip +/// tip | 提示 上面有几个 `__init__.py` 文件:每个目录或子目录中都有一个。 @@ -52,11 +52,11 @@ from app.routers import items * 还有一个子目录 `app/internal/` 包含另一个 `__init__.py` 文件,因此它是又一个「Python 子包」:`app.internal`。 * `app/internal/admin.py` 是另一个子模块:`app.internal.admin`。 - + 带有注释的同一文件结构: -``` +```bash . ├── app # 「app」是一个 Python 包 │   ├── __init__.py # 这个文件使「app」成为一个 Python 包 @@ -71,7 +71,7 @@ from app.routers import items │   └── admin.py # 「admin」子模块,例如 import app.internal.admin ``` -## `APIRouter` +## `APIRouter` { #apirouter } 假设专门用于处理用户逻辑的文件是位于 `/app/routers/users.py` 的子模块。 @@ -81,23 +81,19 @@ from app.routers import items 你可以使用 `APIRouter` 为该模块创建*路径操作*。 -### 导入 `APIRouter` +### 导入 `APIRouter` { #import-apirouter } 你可以导入它并通过与 `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"] *} -### 使用 `APIRouter` 的*路径操作* +### 使用 `APIRouter` 的*路径操作* { #path-operations-with-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`」类。 @@ -105,7 +101,7 @@ from app.routers import items 所有相同的 `parameters`、`responses`、`dependencies`、`tags` 等等。 -/// tip +/// tip | 提示 在此示例中,该变量被命名为 `router`,但你可以根据你的想法自由命名。 @@ -113,7 +109,7 @@ from app.routers import items 我们将在主 `FastAPI` 应用中包含该 `APIRouter`,但首先,让我们来看看依赖项和另一个 `APIRouter`。 -## 依赖项 +## 依赖项 { #dependencies } 我们了解到我们将需要一些在应用程序的好几个地方所使用的依赖项。 @@ -121,11 +117,9 @@ from app.routers import items 现在我们将使用一个简单的依赖项来读取一个自定义的 `X-Token` 请求首部: -```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 +/// tip | 提示 我们正在使用虚构的请求首部来简化此示例。 @@ -133,7 +127,7 @@ from app.routers import items /// -## 其他使用 `APIRouter` 的模块 +## 其他使用 `APIRouter` 的模块 { #another-module-with-apirouter } 假设你在位于 `app/routers/items.py` 的模块中还有专门用于处理应用程序中「项目」的端点。 @@ -155,9 +149,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"] *} 由于每个*路径操作*的路径都必须以 `/` 开头,例如: @@ -175,7 +167,7 @@ async def read_item(item_id: str): 我们可以添加一个 `dependencies` 列表,这些依赖项将被添加到路由器中的所有*路径操作*中,并将针对向它们发起的每个请求执行/解决。 -/// tip +/// tip | 提示 请注意,和[*路径操作装饰器*中的依赖项](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}很类似,没有值会被传递给你的*路径操作函数*。 @@ -196,19 +188,19 @@ async def read_item(item_id: str): * 路由器的依赖项最先执行,然后是[装饰器中的 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank},再然后是普通的参数依赖项。 * 你还可以添加[具有 `scopes` 的 `Security` 依赖项](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}。 -/// tip +/// tip | 提示 在 `APIRouter`中具有 `dependencies` 可以用来,例如,对一整组的*路径操作*要求身份认证。即使这些依赖项并没有分别添加到每个路径操作中。 /// -/// check +/// check | 检查 `prefix`、`tags`、`responses` 以及 `dependencies` 参数只是(和其他很多情况一样)**FastAPI** 的一个用于帮助你避免代码重复的功能。 /// -### 导入依赖项 +### 导入依赖项 { #import-the-dependencies } 这些代码位于 `app.routers.items` 模块,`app/routers/items.py` 文件中。 @@ -216,13 +208,11 @@ 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 } -/// tip +/// tip | 提示 如果你完全了解导入的工作原理,请从下面的下一部分继续。 @@ -244,7 +234,7 @@ from .dependencies import get_token_header 请记住我们的程序/文件结构是怎样的: - + --- @@ -276,24 +266,22 @@ from ...dependencies import get_token_header * 从该模块(`app/routers/items.py` 文件)所在的同一个包(`app/routers/` 目录)开始... * 跳转到其父包(`app/` 目录)... * 然后跳转到该包的父包(该父包并不存在,`app` 已经是最顶层的包 😱)... -* 在该父包中,找到 `dependencies` 模块(位于 `app/` 更上一级目录中的 `dependencies.py` 文件)... +* 在该父包中,找到 `dependencies` 模块(位于 `app/dependencies.py` 的文件)... * 然后从中导入函数 `get_token_header`。 这将引用 `app/` 的往上一级,带有其自己的 `__init __.py` 等文件的某个包。但是我们并没有这个包。因此,这将在我们的示例中引发错误。🚨 但是现在你知道了它的工作原理,因此无论它们多么复杂,你都可以在自己的应用程序中使用相对导入。🤓 -### 添加一些自定义的 `tags`、`responses` 和 `dependencies` +### 添加一些自定义的 `tags`、`responses` 和 `dependencies` { #add-some-custom-tags-responses-and-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!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} -/// tip +/// tip | 提示 最后的这个路径操作将包含标签的组合:`["items","custom"]`。 @@ -301,7 +289,7 @@ from ...dependencies import get_token_header /// -## `FastAPI` 主体 +## `FastAPI` 主体 { #the-main-fastapi } 现在,让我们来看看位于 `app/main.py` 的模块。 @@ -311,27 +299,23 @@ from ...dependencies import get_token_header 并且由于你的大部分逻辑现在都存在于其自己的特定模块中,因此主文件的内容将非常简单。 -### 导入 `FastAPI` +### 导入 `FastAPI` { #import-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!} -``` +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} -### 导入 `APIRouter` +### 导入 `APIRouter` { #import-the-apirouter } 现在,我们导入具有 `APIRouter` 的其他子模块: -```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[4:5] title["app/main.py"] *} 由于文件 `app/routers/users.py` 和 `app/routers/items.py` 是同一 Python 包 `app` 一个部分的子模块,因此我们可以使用单个点 ` .` 通过「相对导入」来导入它们。 -### 导入是如何工作的 +### 导入是如何工作的 { #how-the-importing-works } 这段代码: @@ -355,7 +339,7 @@ from .routers import items, users from app.routers import items, users ``` -/// info +/// info | 信息 第一个版本是「相对导入」: @@ -373,7 +357,7 @@ from app.routers import items, users /// -### 避免名称冲突 +### 避免名称冲突 { #avoid-name-collisions } 我们将直接导入 `items` 子模块,而不是仅导入其 `router` 变量。 @@ -390,19 +374,15 @@ 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"] *} -### 包含 `users` 和 `items` 的 `APIRouter` +### 包含 `users` 和 `items` 的 `APIRouter` { #include-the-apirouters-for-users-and-items } 现在,让我们来包含来自 `users` 和 `items` 子模块的 `router`。 -```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 | 信息 `users.router` 包含了 `app/routers/users.py` 文件中的 `APIRouter`。 @@ -422,7 +402,7 @@ from .routers.users import router /// -/// check +/// check | 检查 包含路由器时,你不必担心性能问题。 @@ -432,7 +412,7 @@ from .routers.users import router /// -### 包含一个有自定义 `prefix`、`tags`、`responses` 和 `dependencies` 的 `APIRouter` +### 包含一个有自定义 `prefix`、`tags`、`responses` 和 `dependencies` 的 `APIRouter` { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } 现在,假设你的组织为你提供了 `app/internal/admin.py` 文件。 @@ -440,17 +420,13 @@ from .routers.users import router 对于此示例,它将非常简单。但是假设由于它是与组织中的其他项目所共享的,因此我们无法对其进行修改,以及直接在 `APIRouter` 中添加 `prefix`、`dependencies`、`tags` 等: -```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"] *} 但是我们仍然希望在包含 `APIRouter` 时设置一个自定义的 `prefix`,以便其所有*路径操作*以 `/admin` 开头,我们希望使用本项目已经有的 `dependencies` 保护它,并且我们希望它包含自定义的 `tags` 和 `responses`。 我们可以通过将这些参数传递给 `app.include_router()` 来完成所有的声明,而不必修改原始的 `APIRouter`: -```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` 文件。 @@ -465,15 +441,13 @@ from .routers.users import router 因此,举例来说,其他项目能够以不同的身份认证方法使用相同的 `APIRouter`。 -### 包含一个*路径操作* +### 包含一个*路径操作* { #include-a-path-operation } 我们还可以直接将*路径操作*添加到 `FastAPI` 应用中。 这里我们这样做了...只是为了表明我们可以做到🤷: -```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()` 添加的所有其他*路径操作*一起正常运行。 @@ -491,14 +465,14 @@ from .routers.users import router /// -## 查看自动化的 API 文档 +## 查看自动化的 API 文档 { #check-the-automatic-api-docs } -现在,使用 `app.main` 模块和 `app` 变量运行 `uvicorn`: +现在,运行你的应用:
```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) ``` @@ -509,9 +483,9 @@ $ uvicorn app.main:app --reload 你将看到使用了正确路径(和前缀)和正确标签的自动化 API 文档,包括了来自所有子模块的路径: - + -## 多次使用不同的 `prefix` 包含同一个路由器 +## 多次使用不同的 `prefix` 包含同一个路由器 { #include-the-same-router-multiple-times-with-different-prefix } 你也可以在*同一*路由器上使用不同的前缀来多次使用 `.include_router()`。 @@ -519,7 +493,7 @@ $ uvicorn app.main:app --reload 这是一个你可能并不真正需要的高级用法,但万一你有需要了就能够用上。 -## 在另一个 `APIRouter` 中包含一个 `APIRouter` +## 在另一个 `APIRouter` 中包含一个 `APIRouter` { #include-an-apirouter-in-another } 与在 `FastAPI` 应用程序中包含 `APIRouter` 的方式相同,你也可以在另一个 `APIRouter` 中包含 `APIRouter`,通过: @@ -527,4 +501,4 @@ $ uvicorn app.main:app --reload router.include_router(other_router) ``` -请确保在你将 `router` 包含到 `FastAPI` 应用程序之前进行此操作,以便 `other_router` 中的`路径操作`也能被包含进来。 +请确保在你将 `router` 包含到 `FastAPI` 应用程序之前进行此操作,以便 `other_router` 中的*路径操作*也能被包含进来。 diff --git a/docs/zh/docs/tutorial/body-fields.md b/docs/zh/docs/tutorial/body-fields.md index 4cff58bfc..36be7c419 100644 --- a/docs/zh/docs/tutorial/body-fields.md +++ b/docs/zh/docs/tutorial/body-fields.md @@ -1,8 +1,8 @@ -# 请求体 - 字段 +# 请求体 - 字段 { #body-fields } 与在*路径操作函数*中使用 `Query`、`Path` 、`Body` 声明校验与元数据的方式一样,可以使用 Pydantic 的 `Field` 在 Pydantic 模型内部声明校验和元数据。 -## 导入 `Field` +## 导入 `Field` { #import-field } 首先,从 Pydantic 中导入 `Field`: @@ -14,7 +14,7 @@ /// -## 声明模型属性 +## 声明模型属性 { #declare-model-attributes } 然后,使用 `Field` 定义模型的属性: @@ -24,7 +24,7 @@ /// note | 技术细节 -实际上,`Query`、`Path` 都是 `Params` 的子类,而 `Params` 类又是 Pydantic 中 `FieldInfo` 的子类。 +实际上,`Query`、`Path` 以及你接下来会看到的其它对象,会创建公共 `Param` 类的子类的对象,而 `Param` 本身是 Pydantic 中 `FieldInfo` 的子类。 Pydantic 的 `Field` 返回也是 `FieldInfo` 的类实例。 @@ -40,13 +40,20 @@ Pydantic 的 `Field` 返回也是 `FieldInfo` 的类实例。 /// -## 添加更多信息 +## 添加更多信息 { #add-extra-information } `Field`、`Query`、`Body` 等对象里可以声明更多信息,并且 JSON Schema 中也会集成这些信息。 *声明示例*一章中将详细介绍添加更多信息的知识。 -## 小结 +/// warning | 警告 + +传递给 `Field` 的额外键也会出现在你的应用生成的 OpenAPI 架构中。 +由于这些键不一定属于 OpenAPI 规范的一部分,某些 OpenAPI 工具(例如 [OpenAPI 验证器](https://validator.swagger.io/))可能无法处理你生成的架构。 + +/// + +## 小结 { #recap } Pydantic 的 `Field` 可以为模型属性声明更多校验和元数据。 diff --git a/docs/zh/docs/tutorial/body-multiple-params.md b/docs/zh/docs/tutorial/body-multiple-params.md index b4356fdcb..7d0ddfc1e 100644 --- a/docs/zh/docs/tutorial/body-multiple-params.md +++ b/docs/zh/docs/tutorial/body-multiple-params.md @@ -1,8 +1,8 @@ -# 请求体 - 多个参数 +# 请求体 - 多个参数 { #body-multiple-parameters } 既然我们已经知道了如何使用 `Path` 和 `Query`,下面让我们来了解一下请求体声明的更高级用法。 -## 混合使用 `Path`、`Query` 和请求体参数 +## 混合使用 `Path`、`Query` 和请求体参数 { #mix-path-query-and-body-parameters } 首先,毫无疑问地,你可以随意地混合使用 `Path`、`Query` 和请求体参数声明,**FastAPI** 会知道该如何处理。 @@ -10,13 +10,13 @@ {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} -/// note +/// note | 注意 请注意,在这种情况下,将从请求体获取的 `item` 是可选的。因为它的默认值为 `None`。 /// -## 多个请求体参数 +## 多个请求体参数 { #multiple-body-parameters } 在上面的示例中,*路径操作*将期望一个具有 `Item` 的属性的 JSON 请求体,就像: @@ -52,7 +52,7 @@ } ``` -/// note +/// note | 注意 请注意,即使 `item` 的声明方式与之前相同,但现在它被期望通过 `item` 键内嵌在请求体中。 @@ -62,7 +62,7 @@ 它将执行对复合数据的校验,并且像现在这样为 OpenAPI 模式和自动化文档对其进行记录。 -## 请求体中的单一值 +## 请求体中的单一值 { #singular-values-in-body } 与使用 `Query` 和 `Path` 为查询参数和路径参数定义额外数据的方式相同,**FastAPI** 提供了一个同等的 `Body`。 @@ -72,12 +72,10 @@ 但是你可以使用 `Body` 指示 **FastAPI** 将其作为请求体的另一个键进行处理。 - {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} 在这种情况下,**FastAPI** 将期望像这样的请求体: - ```JSON { "item": { @@ -96,27 +94,33 @@ 同样的,它将转换数据类型,校验,生成文档等。 -## 多个请求体参数和查询参数 +## 多个请求体参数和查询参数 { #multiple-body-params-and-query } 当然,除了请求体参数外,你还可以在任何需要的时候声明额外的查询参数。 -由于默认情况下单一值被解释为查询参数,因此你不必显式地添加 `Query`,你可以仅执行以下操作: +由于默认情况下单一值会被解释为查询参数,因此你不必显式地添加 `Query`,你可以这样写: ```Python -q: str = None +q: str | None = None +``` + +或者在 Python 3.9 中: + +```Python +q: Union[str, None] = None ``` 比如: -{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[27] *} +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} -/// info +/// info | 信息 `Body` 同样具有与 `Query`、`Path` 以及其他后面将看到的类完全相同的额外校验和元数据参数。 /// -## 嵌入单个请求体参数 +## 嵌入单个请求体参数 { #embed-a-single-body-parameter } 假设你只有一个来自 Pydantic 模型 `Item` 的请求体参数 `item`。 @@ -156,7 +160,7 @@ item: Item = Body(embed=True) } ``` -## 总结 +## 总结 { #recap } 你可以添加多个请求体参数到*路径操作函数*中,即使一个请求只能有一个请求体。 diff --git a/docs/zh/docs/tutorial/body-nested-models.md b/docs/zh/docs/tutorial/body-nested-models.md index df96d96b4..242aa5822 100644 --- a/docs/zh/docs/tutorial/body-nested-models.md +++ b/docs/zh/docs/tutorial/body-nested-models.md @@ -1,53 +1,42 @@ -# 请求体 - 嵌套模型 +# 请求体 - 嵌套模型 { #body-nested-models } 使用 **FastAPI**,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于Pydantic)。 -## List 字段 +## List 字段 { #list-fields } -你可以将一个属性定义为拥有子元素的类型。例如 Python `list`: +你可以将一个属性定义为一个子类型。例如,Python `list`: {* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} 这将使 `tags` 成为一个由元素组成的列表。不过它没有声明每个元素的类型。 -## 具有子类型的 List 字段 +## 带类型参数的 List 字段 { #list-fields-with-type-parameter } -但是 Python 有一种特定的方法来声明具有子类型的列表: +不过,Python 有一种用于声明具有内部类型(类型参数)的列表的特定方式: -### 从 typing 导入 `List` +### 声明带类型参数的 `list` { #declare-a-list-with-a-type-parameter } -首先,从 Python 的标准库 `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] ``` 这完全是用于类型声明的标准 Python 语法。 -对具有子类型的模型属性也使用相同的标准语法。 +对具有内部类型的模型属性也使用相同的标准语法。 因此,在我们的示例中,我们可以将 `tags` 明确地指定为一个「字符串列表」: {* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## Set 类型 +## Set 类型 { #set-types } 但是随后我们考虑了一下,意识到标签不应该重复,它们很大可能会是唯一的字符串。 -Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `set`。 +而 Python 有一种用于保存唯一元素集合的特殊数据类型 `set`。 -然后我们可以导入 `Set` 并将 `tag` 声明为一个由 `str` 组成的 `set`: +然后我们可以将 `tags` 声明为一个由字符串组成的 set: {* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} @@ -57,7 +46,7 @@ Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `se 并且还会被相应地标注 / 记录文档。 -## 嵌套模型 +## 嵌套模型 { #nested-models } Pydantic 模型的每个属性都具有类型。 @@ -67,13 +56,13 @@ Pydantic 模型的每个属性都具有类型。 上述这些都可以任意的嵌套。 -### 定义子模型 +### 定义子模型 { #define-a-submodel } 例如,我们可以定义一个 `Image` 模型: {* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### 将子模型用作类型 +### 将子模型用作类型 { #use-the-submodel-as-a-type } 然后我们可以将其用作一个属性的类型: @@ -102,11 +91,11 @@ Pydantic 模型的每个属性都具有类型。 * 数据校验 * 自动生成文档 -## 特殊的类型和校验 +## 特殊的类型和校验 { #special-types-and-validation } 除了普通的单一值类型(如 `str`、`int`、`float` 等)外,你还可以使用从 `str` 继承的更复杂的单一值类型。 -要了解所有的可用选项,请查看关于 来自 Pydantic 的外部类型 的文档。你将在下一章节中看到一些示例。 +要了解所有的可用选项,请查看 Pydantic 的类型概览。你将在下一章节中看到一些示例。 例如,在 `Image` 模型中我们有一个 `url` 字段,我们可以把它声明为 Pydantic 的 `HttpUrl`,而不是 `str`: @@ -114,7 +103,7 @@ Pydantic 模型的每个属性都具有类型。 该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 文档中进行记录。 -## 带有一组子模型的属性 +## 带有一组子模型的属性 { #attributes-with-lists-of-submodels } 你还可以将 Pydantic 模型用作 `list`、`set` 等的子类型: @@ -146,49 +135,49 @@ Pydantic 模型的每个属性都具有类型。 } ``` -/// info +/// info | 信息 请注意 `images` 键现在具有一组 image 对象是如何发生的。 /// -## 深度嵌套模型 +## 深度嵌套模型 { #deeply-nested-models } 你可以定义任意深度的嵌套模型: {* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} -/// info +/// info | 信息 请注意 `Offer` 拥有一组 `Item` 而反过来 `Item` 又是一个可选的 `Image` 列表是如何发生的。 /// -## 纯列表请求体 +## 纯列表请求体 { #bodies-of-pure-lists } 如果你期望的 JSON 请求体的最外层是一个 JSON `array`(即 Python `list`),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样: ```Python -images: List[Image] +images: list[Image] ``` 例如: {* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## 无处不在的编辑器支持 +## 无处不在的编辑器支持 { #editor-support-everywhere } 你可以随处获得编辑器支持。 即使是列表中的元素: - + 如果你直接使用 `dict` 而不是 Pydantic 模型,那你将无法获得这种编辑器支持。 但是你根本不必担心这两者,传入的字典会自动被转换,你的输出也会自动被转换为 JSON。 -## 任意 `dict` 构成的请求体 +## 任意 `dict` 构成的请求体 { #bodies-of-arbitrary-dicts } 你也可以将请求体声明为使用某类型的键和其他类型值的 `dict`。 @@ -206,7 +195,7 @@ images: List[Image] {* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} -/// tip +/// tip | 提示 请记住 JSON 仅支持将 `str` 作为键。 @@ -218,7 +207,7 @@ images: List[Image] /// -## 总结 +## 总结 { #recap } 使用 **FastAPI** 你可以拥有 Pydantic 模型提供的极高灵活性,同时保持代码的简单、简短和优雅。 diff --git a/docs/zh/docs/tutorial/body-updates.md b/docs/zh/docs/tutorial/body-updates.md index 87f88f255..000201de9 100644 --- a/docs/zh/docs/tutorial/body-updates.md +++ b/docs/zh/docs/tutorial/body-updates.md @@ -1,18 +1,18 @@ -# 请求体 - 更新数据 +# 请求体 - 更新数据 { #body-updates } -## 用 `PUT` 更新数据 +## 用 `PUT` 替换式更新 { #update-replacing-with-put } -更新数据请用 HTTP `PUT` 操作。 +更新数据可以使用 HTTP `PUT` 操作。 把输入数据转换为以 JSON 格式存储的数据(比如,使用 NoSQL 数据库时),可以使用 `jsonable_encoder`。例如,把 `datetime` 转换为 `str`。 -{* ../../docs_src/body_updates/tutorial001.py hl[30:35] *} +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} `PUT` 用于接收替换现有数据的数据。 -### 关于更新数据的警告 +### 关于替换的警告 { #warning-about-replacing } -用 `PUT` 把数据项 `bar` 更新为以下内容时: +用 `PUT` 把数据项 `bar` 更新为以下请求体时: ```Python { @@ -22,78 +22,79 @@ } ``` -因为上述数据未包含已存储的属性 `"tax": 20.2`,新的输入模型会把 `"tax": 10.5` 作为默认值。 +因为其中未包含已存储的属性 `"tax": 20.2`,输入模型会取 `"tax": 10.5` 的默认值。 -因此,本次操作把 `tax` 的值「更新」为 `10.5`。 +因此,保存的数据会带有这个“新的” `tax` 值 `10.5`。 -## 用 `PATCH` 进行部分更新 +## 用 `PATCH` 进行部分更新 { #partial-updates-with-patch } -HTTP `PATCH` 操作用于更新 *部分* 数据。 +也可以使用 HTTP `PATCH` 操作对数据进行*部分*更新。 -即,只发送要更新的数据,其余数据保持不变。 +也就是说,你只需发送想要更新的数据,其余数据保持不变。 -/// note | 笔记 +/// note | 注意 -`PATCH` 没有 `PUT` 知名,也怎么不常用。 +`PATCH` 没有 `PUT` 知名,也没那么常用。 -很多人甚至只用 `PUT` 实现部分更新。 +很多团队甚至只用 `PUT` 实现部分更新。 -**FastAPI** 对此没有任何限制,可以**随意**互换使用这两种操作。 +你可以**随意**选择如何使用它们,**FastAPI** 不做任何限制。 -但本指南也会分别介绍这两种操作各自的用途。 +但本指南会大致展示它们的预期用法。 /// -### 使用 Pydantic 的 `exclude_unset` 参数 +### 使用 Pydantic 的 `exclude_unset` 参数 { #using-pydantics-exclude-unset-parameter } -更新部分数据时,可以在 Pydantic 模型的 `.dict()` 中使用 `exclude_unset` 参数。 +如果要接收部分更新,建议在 Pydantic 模型的 `.model_dump()` 中使用 `exclude_unset` 参数。 -比如,`item.dict(exclude_unset=True)`。 +比如,`item.model_dump(exclude_unset=True)`。 -这段代码生成的 `dict` 只包含创建 `item` 模型时显式设置的数据,而不包括默认值。 +这会生成一个 `dict`,只包含创建 `item` 模型时显式设置的数据,不包含默认值。 -然后再用它生成一个只含已设置(在请求中所发送)数据,且省略了默认值的 `dict`: +然后再用它生成一个只含已设置(在请求中发送)数据、且省略默认值的 `dict`: -{* ../../docs_src/body_updates/tutorial002.py hl[34] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### 使用 Pydantic 的 `update` 参数 +### 使用 Pydantic 的 `update` 参数 { #using-pydantics-update-parameter } -接下来,用 `.copy()` 为已有模型创建调用 `update` 参数的副本,该参数为包含更新数据的 `dict`。 +接下来,用 `.model_copy()` 为已有模型创建副本,并传入 `update` 参数,值为包含更新数据的 `dict`。 -例如,`stored_item_model.copy(update=update_data)`: +例如,`stored_item_model.model_copy(update=update_data)`: -{* ../../docs_src/body_updates/tutorial002.py hl[35] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### 更新部分数据小结 +### 部分更新小结 { #partial-updates-recap } -简而言之,更新部分数据应: +简而言之,应用部分更新应当: -* 使用 `PATCH` 而不是 `PUT` (可选,也可以用 `PUT`); -* 提取存储的数据; -* 把数据放入 Pydantic 模型; -* 生成不含输入模型默认值的 `dict` (使用 `exclude_unset` 参数); - * 只更新用户设置过的值,不用模型中的默认值覆盖已存储过的值。 -* 为已存储的模型创建副本,用接收的数据更新其属性 (使用 `update` 参数)。 +* (可选)使用 `PATCH` 而不是 `PUT`。 +* 提取已存储的数据。 +* 把该数据放入 Pydantic 模型。 +* 生成不含输入模型默认值的 `dict`(使用 `exclude_unset`)。 + * 这样只会更新用户实际设置的值,而不会用模型中的默认值覆盖已存储的值。 +* 为已存储的模型创建副本,用接收到的部分更新数据更新其属性(使用 `update` 参数)。 * 把模型副本转换为可存入数据库的形式(比如,使用 `jsonable_encoder`)。 - * 这种方式与 Pydantic 模型的 `.dict()` 方法类似,但能确保把值转换为适配 JSON 的数据类型,例如, 把 `datetime` 转换为 `str` 。 -* 把数据保存至数据库; + * 这类似于再次调用模型的 `.model_dump()` 方法,但会确保(并转换)值为可转换为 JSON 的数据类型,例如把 `datetime` 转换为 `str`。 +* 把数据保存至数据库。 * 返回更新后的模型。 -{* ../../docs_src/body_updates/tutorial002.py hl[30:37] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} /// tip | 提示 -实际上,HTTP `PUT` 也可以完成相同的操作。 -但本节以 `PATCH` 为例的原因是,该操作就是为了这种用例创建的。 +实际上,HTTP `PUT` 也可以使用同样的技巧。 + +但这里用 `PATCH` 举例,因为它就是为这种用例设计的。 /// -/// note | 笔记 +/// note | 注意 -注意,输入模型仍需验证。 +注意,输入模型仍会被验证。 -因此,如果希望接收的部分更新数据可以省略其他所有属性,则要把模型中所有的属性标记为可选(使用默认值或 `None`)。 +因此,如果希望接收的部分更新可以省略所有属性,则需要一个所有属性都标记为可选(带默认值或 `None`)的模型。 -为了区分用于**更新**所有可选值的模型与用于**创建**包含必选值的模型,请参照[更多模型](extra-models.md){.internal-link target=_blank} 一节中的思路。 +为了区分用于**更新**(全部可选)和用于**创建**(必填)的模型,可以参考[更多模型](extra-models.md){.internal-link target=_blank} 中介绍的思路。 /// diff --git a/docs/zh/docs/tutorial/body.md b/docs/zh/docs/tutorial/body.md index 3820fc747..60088a048 100644 --- a/docs/zh/docs/tutorial/body.md +++ b/docs/zh/docs/tutorial/body.md @@ -1,30 +1,30 @@ -# 请求体 +# 请求体 { #request-body } -FastAPI 使用**请求体**从客户端(例如浏览器)向 API 发送数据。 +当你需要从客户端(比如浏览器)向你的 API 发送数据时,会把它作为**请求体**发送。 -**请求体**是客户端发送给 API 的数据。**响应体**是 API 发送给客户端的数据。 +**请求体**是客户端发送给你的 API 的数据。**响应体**是你的 API 发送给客户端的数据。 -API 基本上肯定要发送**响应体**,但是客户端不一定发送**请求体**。 +你的 API 几乎总是需要发送**响应体**。但客户端不一定总是要发送**请求体**,有时它们只请求某个路径,可能带一些查询参数,但不会发送请求体。 -使用 Pydantic 模型声明**请求体**,能充分利用它的功能和优点。 +使用 Pydantic 模型来声明**请求体**,能充分利用它的功能和优点。 -/// info | 说明 +/// info | 信息 -发送数据使用 `POST`(最常用)、`PUT`、`DELETE`、`PATCH` 等操作。 +发送数据应使用以下之一:`POST`(最常见)、`PUT`、`DELETE` 或 `PATCH`。 -规范中没有定义使用 `GET` 发送请求体的操作,但不管怎样,FastAPI 也支持这种方式,只不过仅用于非常复杂或极端的用例。 +规范中没有定义用 `GET` 请求发送请求体的行为,但 FastAPI 仍支持这种方式,只用于非常复杂/极端的用例。 -我们不建议使用 `GET`,因此,在 Swagger UI 交互文档中不会显示有关 `GET` 的内容,而且代理协议也不一定支持 `GET`。 +由于不推荐,在使用 `GET` 时,Swagger UI 的交互式文档不会显示请求体的文档,而且中间的代理可能也不支持它。 /// -## 导入 Pydantic 的 `BaseModel` +## 导入 Pydantic 的 `BaseModel` { #import-pydantics-basemodel } 从 `pydantic` 中导入 `BaseModel`: {* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## 创建数据模型 +## 创建数据模型 { #create-your-data-model } 把数据模型声明为继承 `BaseModel` 的类。 @@ -32,9 +32,9 @@ API 基本上肯定要发送**响应体**,但是客户端不一定发送**请 {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -与声明查询参数一样,包含默认值的模型属性是可选的,否则就是必选的。默认值为 `None` 的模型属性也是可选的。 +与声明查询参数一样,包含默认值的模型属性是可选的,否则就是必选的。把默认值设为 `None` 可使其变为可选。 -例如,上述模型声明如下 JSON **对象**(即 Python **字典**): +例如,上述模型声明如下 JSON "object"(即 Python `dict`): ```JSON { @@ -45,7 +45,7 @@ API 基本上肯定要发送**响应体**,但是客户端不一定发送**请 } ``` -……由于 `description` 和 `tax` 是可选的(默认值为 `None`),下面的 JSON **对象**也有效: +...由于 `description` 和 `tax` 是可选的(默认值为 `None`),下面的 JSON "object" 也有效: ```JSON { @@ -54,40 +54,40 @@ API 基本上肯定要发送**响应体**,但是客户端不一定发送**请 } ``` -## 声明请求体参数 +## 声明为参数 { #declare-it-as-a-parameter } -使用与声明路径和查询参数相同的方式声明请求体,把请求体添加至*路径操作*: +使用与声明路径和查询参数相同的方式,把它添加至*路径操作*: {* ../../docs_src/body/tutorial001_py310.py hl[16] *} -……此处,请求体参数的类型为 `Item` 模型。 +...并把其类型声明为你创建的模型 `Item`。 -## 结论 +## 结果 { #results } -仅使用 Python 类型声明,**FastAPI** 就可以: +仅使用这些 Python 类型声明,**FastAPI** 就可以: -* 以 JSON 形式读取请求体 -* (在必要时)把请求体转换为对应的类型 -* 校验数据: - * 数据无效时返回错误信息,并指出错误数据的确切位置和内容 -* 把接收的数据赋值给参数 `item` - * 把函数中请求体参数的类型声明为 `Item`,还能获得代码补全等编辑器支持 -* 为模型生成 JSON Schema,在项目中所需的位置使用 -* 这些概图是 OpenAPI 概图的部件,用于 API 文档 UI +* 以 JSON 形式读取请求体。 +* (在必要时)把请求体转换为对应的类型。 +* 校验数据。 + * 数据无效时返回清晰的错误信息,并指出错误数据的确切位置和内容。 +* 把接收的数据赋值给参数 `item`。 + * 因为你把函数中的参数类型声明为 `Item`,所以还能获得所有属性及其类型的编辑器支持(补全等)。 +* 为你的模型生成 JSON Schema 定义,如果对你的项目有意义,还可以在其他地方使用它们。 +* 这些 schema 会成为生成的 OpenAPI Schema 的一部分,并被自动文档的 UIs 使用。 -## API 文档 +## 自动文档 { #automatic-docs } -Pydantic 模型的 JSON 概图是 OpenAPI 生成的概图部件,可在 API 文档中显示: +你的模型的 JSON Schema 会成为生成的 OpenAPI Schema 的一部分,并显示在交互式 API 文档中: -而且,还会用于 API 文档中使用了概图的*路径操作*: +并且,还会用于需要它们的每个*路径操作*的 API 文档中: -## 编辑器支持 +## 编辑器支持 { #editor-support } -在编辑器中,函数内部均可使用类型提示、代码补全(如果接收的不是 Pydantic 模型,而是**字典**,就没有这样的支持): +在编辑器中,函数内部你会在各处得到类型提示与补全(如果接收的不是 Pydantic 模型,而是 `dict`,就不会有这样的支持): @@ -95,23 +95,23 @@ Pydantic 模型的 JSON 概图是 OpenAPI 生成的概图部件,可在 API 文 -这并非偶然,整个 **FastAPI** 框架都是围绕这种思路精心设计的。 +这并非偶然,整个框架都是围绕这种设计构建的。 -并且,在 FastAPI 的设计阶段,我们就已经进行了全面测试,以确保 FastAPI 可以获得所有编辑器的支持。 +并且在设计阶段、实现之前就进行了全面测试,以确保它能在所有编辑器中正常工作。 -我们还改进了 Pydantic,让它也支持这些功能。 +我们甚至对 Pydantic 本身做了一些改动以支持这些功能。 -虽然上面的截图取自 Visual Studio Code。 +上面的截图来自 Visual Studio Code。 -但 PyCharm 和大多数 Python 编辑器也支持同样的功能: +但使用 PyCharm 和大多数其他 Python 编辑器,你也会获得相同的编辑器支持: /// tip | 提示 -使用 PyCharm 编辑器时,推荐安装 Pydantic PyCharm 插件。 +如果你使用 PyCharm 作为编辑器,可以使用 Pydantic PyCharm 插件。 -该插件用于完善 PyCharm 对 Pydantic 模型的支持,优化的功能如下: +它能改进对 Pydantic 模型的编辑器支持,包括: * 自动补全 * 类型检查 @@ -121,42 +121,44 @@ Pydantic 模型的 JSON 概图是 OpenAPI 生成的概图部件,可在 API 文 /// -## 使用模型 +## 使用模型 { #use-the-model } -在*路径操作*函数内部直接访问模型对象的属性: +在*路径操作*函数内部直接访问模型对象的所有属性: -{* ../../docs_src/body/tutorial002_py310.py hl[19] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## 请求体 + 路径参数 +## 请求体 + 路径参数 { #request-body-path-parameters } -**FastAPI** 支持同时声明路径参数和请求体。 +可以同时声明路径参数和请求体。 -**FastAPI** 能识别与**路径参数**匹配的函数参数,还能识别从**请求体**中获取的类型为 Pydantic 模型的函数参数。 +**FastAPI** 能识别与**路径参数**匹配的函数参数应该**从路径中获取**,而声明为 Pydantic 模型的函数参数应该**从请求体中获取**。 {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## 请求体 + 路径参数 + 查询参数 +## 请求体 + 路径 + 查询参数 { #request-body-path-query-parameters } -**FastAPI** 支持同时声明**请求体**、**路径参数**和**查询参数**。 +也可以同时声明**请求体**、**路径**和**查询**参数。 -**FastAPI** 能够正确识别这三种参数,并从正确的位置获取数据。 +**FastAPI** 会分别识别它们,并从正确的位置获取数据。 {* ../../docs_src/body/tutorial004_py310.py hl[16] *} 函数参数按如下规则进行识别: -- **路径**中声明了相同参数的参数,是路径参数 -- 类型是(`int`、`float`、`str`、`bool` 等)**单类型**的参数,是**查询**参数 -- 类型是 **Pydantic 模型**的参数,是**请求体** +* 如果该参数也在**路径**中声明了,它就是路径参数。 +* 如果该参数是(`int`、`float`、`str`、`bool` 等)**单一类型**,它会被当作**查询**参数。 +* 如果该参数的类型声明为 **Pydantic 模型**,它会被当作请求**体**。 -/// note | 笔记 +/// note | 注意 -因为默认值是 `None`, FastAPI 会把 `q` 当作可选参数。 +FastAPI 会根据默认值 `= None` 知道 `q` 的值不是必填的。 -FastAPI 不使用 `Optional[str]` 中的 `Optional`, 但 `Optional` 可以让编辑器提供更好的支持,并检测错误。 +`str | None`(Python 3.10+)或 `Union[str, None]`(Python 3.9+ 中的 `Union`)并不是 FastAPI 用来判断是否必填的依据;是否必填由是否有默认值 `= None` 决定。 + +但添加这些类型注解可以让你的编辑器提供更好的支持并检测错误。 /// -## 不使用 Pydantic +## 不使用 Pydantic { #without-pydantic } -即便不使用 Pydantic 模型也能使用 **Body** 参数。详见[请求体 - 多参数:请求体中的单值](body-multiple-params.md#_2){.internal-link target=\_blank}。 +即便不使用 Pydantic 模型也能使用 **Body** 参数。详见[请求体 - 多参数:请求体中的单值](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}。 diff --git a/docs/zh/docs/tutorial/cookie-param-models.md b/docs/zh/docs/tutorial/cookie-param-models.md index 6a7b09e25..707a6a9c7 100644 --- a/docs/zh/docs/tutorial/cookie-param-models.md +++ b/docs/zh/docs/tutorial/cookie-param-models.md @@ -1,22 +1,22 @@ -# Cookie 参数模型 +# Cookie 参数模型 { #cookie-parameter-models } 如果您有一组相关的 **cookie**,您可以创建一个 **Pydantic 模型**来声明它们。🍪 这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证方式和元数据。😎 -/// note +/// note | 注意 自 FastAPI 版本 `0.115.0` 起支持此功能。🤓 /// -/// tip +/// tip | 提示 此技术同样适用于 `Query` 、 `Cookie` 和 `Header` 。😎 /// -## 带有 Pydantic 模型的 Cookie +## 带有 Pydantic 模型的 Cookie { #cookies-with-a-pydantic-model } 在 **Pydantic** 模型中声明所需的 **cookie** 参数,然后将参数声明为 `Cookie` : @@ -24,7 +24,7 @@ **FastAPI** 将从请求中接收到的 **cookie** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。 -## 查看文档 +## 查看文档 { #check-the-docs } 您可以在文档 UI 的 `/docs` 中查看定义的 cookie: @@ -32,7 +32,7 @@
-/// info +/// info | 信息 请记住,由于**浏览器**以特殊方式**处理 cookie**,并在后台进行操作,因此它们**不会**轻易允许 **JavaScript** 访问这些 cookie。 @@ -42,7 +42,7 @@ /// -## 禁止额外的 Cookie +## 禁止额外的 Cookie { #forbid-extra-cookies } 在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 cookie。 @@ -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] *} 如果客户尝试发送一些**额外的 cookie**,他们将收到**错误**响应。 @@ -71,6 +71,6 @@ } ``` -## 总结 +## 总结 { #summary } 您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 **cookie**。😎 diff --git a/docs/zh/docs/tutorial/cookie-params.md b/docs/zh/docs/tutorial/cookie-params.md index 495600814..ab05cd7d2 100644 --- a/docs/zh/docs/tutorial/cookie-params.md +++ b/docs/zh/docs/tutorial/cookie-params.md @@ -1,20 +1,19 @@ -# 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` 参数的方式与声明 `Query` 和 `Path` 参数相同。 第一个值是默认值,还可以传递所有验证参数或注释参数: - {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | 技术细节 @@ -25,12 +24,22 @@ /// -/// info | 说明 +/// info | 信息 必须使用 `Cookie` 声明 cookie 参数,否则该参数会被解释为查询参数。 /// -## 小结 +/// info | 信息 + +请注意,由于**浏览器会以特殊方式并在幕后处理 cookies**,它们**不会**轻易允许**JavaScript**访问它们。 + +如果你前往位于 `/docs` 的**API 文档界面**,你可以看到你的*路径操作*中有关 cookies 的**文档**。 + +但即使你**填写了数据**并点击 "Execute",由于文档界面依赖于**JavaScript**工作,cookies 也不会被发送,你会看到一个**错误**消息,好像你没有填写任何值一样。 + +/// + +## 小结 { #recap } 使用 `Cookie` 声明 cookie 参数的方式与 `Query` 和 `Path` 相同。 diff --git a/docs/zh/docs/tutorial/cors.md b/docs/zh/docs/tutorial/cors.md index a4f15f647..3a296ca72 100644 --- a/docs/zh/docs/tutorial/cors.md +++ b/docs/zh/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS(跨域资源共享) +# CORS(跨域资源共享) { #cors-cross-origin-resource-sharing } CORS 或者「跨域资源共享」 指浏览器中运行的前端拥有与后端通信的 JavaScript 代码,而后端处于与前端不同的「源」的情况。 -## 源 +## 源 { #origin } 源是协议(`http`,`https`)、域(`myapp.com`,`localhost`,`localhost.tiangolo.com`)以及端口(`80`、`443`、`8080`)的组合。 @@ -14,25 +14,25 @@ 即使它们都在 `localhost` 中,但是它们使用不同的协议或者端口,所以它们都是不同的「源」。 -## 步骤 +## 步骤 { #steps } 假设你的浏览器中有一个前端运行在 `http://localhost:8080`,并且它的 JavaScript 正在尝试与运行在 `http://localhost` 的后端通信(因为我们没有指定端口,浏览器会采用默认的端口 `80`)。 -然后,浏览器会向后端发送一个 HTTP `OPTIONS` 请求,如果后端发送适当的 headers 来授权来自这个不同源(`http://localhost:8080`)的通信,浏览器将允许前端的 JavaScript 向后端发送请求。 +然后,浏览器会向 `:80` 的后端发送一个 HTTP `OPTIONS` 请求,如果后端发送适当的 headers 来授权来自这个不同源(`http://localhost:8080`)的通信,那么运行在 `:8080` 的浏览器就会允许前端中的 JavaScript 向 `:80` 的后端发送请求。 -为此,后端必须有一个「允许的源」列表。 +为此,`:80` 的后端必须有一个「允许的源」列表。 -在这种情况下,它必须包含 `http://localhost:8080`,前端才能正常工作。 +在这种情况下,它必须包含 `http://localhost:8080`,这样 `:8080` 的前端才能正常工作。 -## 通配符 +## 通配符 { #wildcards } 也可以使用 `"*"`(一个「通配符」)声明这个列表,表示全部都是允许的。 -但这仅允许某些类型的通信,不包括所有涉及凭据的内容:像 Cookies 以及那些使用 Bearer 令牌的授权 headers 等。 +但这仅允许某些类型的通信,不包括所有涉及凭据的内容:比如 Cookies,以及那些使用 Bearer 令牌的 Authorization 请求头等。 因此,为了一切都能正常工作,最好显式地指定允许的源。 -## 使用 `CORSMiddleware` +## 使用 `CORSMiddleware` { #use-corsmiddleware } 你可以在 **FastAPI** 应用中使用 `CORSMiddleware` 来配置它。 @@ -42,11 +42,11 @@ 你也可以指定后端是否允许: -* 凭证(授权 headers,Cookies 等)。 +* 凭证(Authorization 请求头、Cookies 等)。 * 特定的 HTTP 方法(`POST`,`PUT`)或者使用通配符 `"*"` 允许所有方法。 -* 特定的 HTTP headers 或者使用通配符 `"*"` 允许所有 headers。 +* 特定的 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` 实现所使用的默认参数较为保守,所以你需要显式地启用特定的源、方法或者 headers,以便浏览器能够在跨域上下文中使用它们。 @@ -55,26 +55,29 @@ * `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` - 指示跨域请求支持 cookies。默认是 `False`。另外,允许凭证时 `allow_origins` 不能设定为 `['*']`,必须指定源。 +* `allow_headers` - 一个允许跨域请求的 HTTP 请求头列表。默认为 `[]`。你可以使用 `['*']` 允许所有的请求头。`Accept`、`Accept-Language`、`Content-Language` 以及 `Content-Type` 这几个请求头在简单 CORS 请求中总是被允许。 +* `allow_credentials` - 指示跨域请求支持 cookies。默认是 `False`。 + + 当 `allow_credentials` 设为 `True` 时,`allow_origins`、`allow_methods` 和 `allow_headers` 都不能设为 `['*']`。它们必须显式指定。 + * `expose_headers` - 指示可以被浏览器访问的响应头。默认为 `[]`。 * `max_age` - 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 `600`。 中间件响应两种特定类型的 HTTP 请求…… -### CORS 预检请求 +### CORS 预检请求 { #cors-preflight-requests } 这是些带有 `Origin` 和 `Access-Control-Request-Method` 请求头的 `OPTIONS` 请求。 在这种情况下,中间件将拦截传入的请求并进行响应,出于提供信息的目的返回一个使用了适当的 CORS headers 的 `200` 或 `400` 响应。 -### 简单请求 +### 简单请求 { #simple-requests } 任何带有 `Origin` 请求头的请求。在这种情况下,中间件将像平常一样传递请求,但是在响应中包含适当的 CORS headers。 -## 更多信息 +## 更多信息 { #more-info } -更多关于 CORS 的信息,请查看 Mozilla CORS 文档。 +更多关于 CORS 的信息,请查看 Mozilla CORS 文档。 /// note | 技术细节 diff --git a/docs/zh/docs/tutorial/debugging.md b/docs/zh/docs/tutorial/debugging.md index 734b85565..6e0cefe5b 100644 --- a/docs/zh/docs/tutorial/debugging.md +++ b/docs/zh/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# 调试 +# 调试 { #debugging } 你可以在编辑器中连接调试器,例如使用 Visual Studio Code 或 PyCharm。 -## 调用 `uvicorn` +## 调用 `uvicorn` { #call-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`。 @@ -57,7 +57,7 @@ $ python myapp.py ```Python from myapp import app -# Some more code +# 其他一些代码 ``` 在这种情况下,`myapp.py` 内部的自动变量不会有值为 `"__main__"` 的变量 `__name__`。 @@ -74,7 +74,7 @@ from myapp import app /// -## 使用你的调试器运行代码 +## 使用你的调试器运行代码 { #run-your-code-with-your-debugger } 由于是从代码直接运行的 Uvicorn 服务器,所以你可以从调试器直接调用 Python 程序(你的 FastAPI 应用)。 diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md index f07280790..d83321ddd 100644 --- a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,12 +1,12 @@ -# 类作为依赖项 +# 类作为依赖项 { #classes-as-dependencies } 在深入探究 **依赖注入** 系统之前,让我们升级之前的例子。 -## 来自前一个例子的`dict` +## 来自前一个例子的`dict` { #a-dict-from-the-previous-example } 在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`: -{* ../../docs_src/dependencies/tutorial001_py310.py hl[7] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} 但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `dict`。 @@ -14,9 +14,9 @@ 对此,我们可以做的更好... -## 什么构成了依赖项? +## 什么构成了依赖项? { #what-makes-a-dependency } -到目前为止,您看到的依赖项都被声明为函数。 +到目前为止,你看到的依赖项都被声明为函数。 但这并不是声明依赖项的唯一方法(尽管它可能是更常见的方法)。 @@ -38,9 +38,9 @@ something(some_argument, some_keyword_argument="foo") 这就是 "可调用对象"。 -## 类作为依赖项 +## 类作为依赖项 { #classes-as-dependencies_1 } -您可能会注意到,要创建一个 Python 类的实例,您可以使用相同的语法。 +你可能会注意到,要创建一个 Python 类的实例,你可以使用相同的语法。 举个例子: @@ -63,21 +63,21 @@ fluffy = Cat(name="Mr Fluffy") 实际上 FastAPI 检查的是它是一个 "可调用对象"(函数,类或其他任何类型)以及定义的参数。 -如果您在 **FastAPI** 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理路径操作函数的参数的方式来处理它们。包括子依赖项。 +如果你在 **FastAPI** 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理路径操作函数的参数的方式来处理它们。包括子依赖项。 这也适用于完全没有参数的可调用对象。这与不带参数的路径操作函数一样。 所以,我们可以将上面的依赖项 "可依赖对象" `common_parameters` 更改为类 `CommonQueryParams`: -{* ../../docs_src/dependencies/tutorial002_py310.py hl[9:13] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} 注意用于创建类实例的 `__init__` 方法: -{* ../../docs_src/dependencies/tutorial002_py310.py hl[10] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} ...它与我们以前的 `common_parameters` 具有相同的参数: -{* ../../docs_src/dependencies/tutorial001_py310.py hl[6] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} 这些参数就是 **FastAPI** 用来 "处理" 依赖项的。 @@ -89,26 +89,44 @@ fluffy = Cat(name="Mr Fluffy") 在两个例子下,数据都将被转换、验证、在 OpenAPI schema 上文档化,等等。 -## 使用它 +## 使用它 { #use-it } -现在,您可以使用这个类来声明你的依赖项了。 +现在,你可以使用这个类来声明你的依赖项了。 -{* ../../docs_src/dependencies/tutorial002_py310.py hl[17] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} **FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。 -## 类型注解 vs `Depends` +## 类型注解 vs `Depends` { #type-annotation-vs-depends } 注意,我们在上面的代码中编写了两次`CommonQueryParams`: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + 最后的 `CommonQueryParams`: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...实际上是 **Fastapi** 用来知道依赖项是什么的。 @@ -119,62 +137,152 @@ FastAPI 将从依赖项中提取声明的参数,这才是 FastAPI 实际调用 在本例中,第一个 `CommonQueryParams` : +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// + ```Python commons: CommonQueryParams ... ``` -...对于 **FastAPI** 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 `= Depends(CommonQueryParams)`)。 +//// + +...对于 **FastAPI** 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 `Depends(CommonQueryParams)`)。 你实际上可以只这样编写: +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// + ```Python commons = Depends(CommonQueryParams) ``` +//// + ..就像: -{* ../../docs_src/dependencies/tutorial003_py310.py hl[17] *} +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} 但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等: -## 快捷方式 +## 快捷方式 { #shortcut } -但是您可以看到,我们在这里有一些代码重复了,编写了`CommonQueryParams`两次: +但是你可以看到,我们在这里有一些代码重复了,编写了`CommonQueryParams`两次: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + **FastAPI** 为这些情况提供了一个快捷方式,在这些情况下,依赖项 *明确地* 是一个类,**FastAPI** 将 "调用" 它来创建类本身的一个实例。 -对于这些特定的情况,您可以跟随以下操作: +对于这些特定的情况,你可以按如下操作: 不是写成这样: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + ...而是这样写: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ 未使用 Annotated + +/// tip | 提示 + +尽可能使用 `Annotated` 版本。 + +/// + ```Python commons: CommonQueryParams = Depends() ``` -您声明依赖项作为参数的类型,并使用 `Depends()` 作为该函数的参数的 "默认" 值(在 `=` 之后),而在 `Depends()` 中没有任何参数,而不是在 `Depends(CommonQueryParams)` 编写完整的类。 +//// + +你声明依赖项作为参数的类型,并使用 `Depends()` 作为该函数的参数的 "默认" 值(在 `=` 之后),而在 `Depends()` 中没有任何参数,而不是在 `Depends(CommonQueryParams)` 中*再次*编写完整的类。 同样的例子看起来像这样: -{* ../../docs_src/dependencies/tutorial004_py310.py hl[17] *} +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} ... **FastAPI** 会知道怎么处理。 -/// tip +/// tip | 提示 如果这看起来更加混乱而不是更加有帮助,那么请忽略它,你不*需要*它。 -这只是一个快捷方式。因为 **FastAPI** 关心的是帮助您减少代码重复。 +这只是一个快捷方式。因为 **FastAPI** 关心的是帮助你减少代码重复。 /// diff --git a/docs/zh/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/zh/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 51b3e9fc3..02fcf62a0 100644 --- a/docs/zh/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/zh/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# 路径操作装饰器依赖项 +# 路径操作装饰器依赖项 { #dependencies-in-path-operation-decorators } 有时,我们并不需要在*路径操作函数*中使用依赖项的返回值。 @@ -8,15 +8,15 @@ 对于这种情况,不必在声明*路径操作函数*的参数时使用 `Depends`,而是可以在*路径操作装饰器*中添加一个由 `dependencies` 组成的 `list`。 -## 在*路径操作装饰器*中添加 `dependencies` 参数 +## 在*路径操作装饰器*中添加 `dependencies` 参数 { #add-dependencies-to-the-path-operation-decorator } -*路径操作装饰器*支持可选参数 ~ `dependencies`。 +*路径操作装饰器*支持可选参数 `dependencies`。 该参数的值是由 `Depends()` 组成的 `list`: -{* ../../docs_src/dependencies/tutorial006.py hl[17] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *} -路径操作装饰器依赖项(以下简称为**“路径装饰器依赖项”**)的执行或解析方式和普通依赖项一样,但就算这些依赖项会返回值,它们的值也不会传递给*路径操作函数*。 +路径操作装饰器依赖项的执行或解析方式和普通依赖项一样,但就算这些依赖项会返回值,它们的值也不会传递给*路径操作函数*。 /// tip | 提示 @@ -36,34 +36,34 @@ /// -## 依赖项错误和返回值 +## 依赖项错误和返回值 { #dependencies-errors-and-return-values } 路径装饰器依赖项也可以使用普通的依赖项*函数*。 -### 依赖项的需求项 +### 依赖项的需求项 { #dependency-requirements } 路径装饰器依赖项可以声明请求的需求项(比如响应头)或其他子依赖项: -{* ../../docs_src/dependencies/tutorial006.py hl[6,11] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### 触发异常 +### 触发异常 { #raise-exceptions } 路径装饰器依赖项与正常的依赖项一样,可以 `raise` 异常: -{* ../../docs_src/dependencies/tutorial006.py hl[8,13] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### 返回值 +### 返回值 { #return-values } 无论路径装饰器依赖项是否返回值,路径操作都不会使用这些值。 因此,可以复用在其他位置使用过的、(能返回值的)普通依赖项,即使没有使用这个值,也会执行该依赖项: -{* ../../docs_src/dependencies/tutorial006.py hl[9,14] *} +{* ../../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` 参数。 +稍后,[大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=_blank}一章中会介绍如何使用多个文件创建大型应用程序,在这一章中,您将了解到如何为一组*路径操作*声明单个 `dependencies` 参数。 -## 全局依赖项 +## 全局依赖项 { #global-dependencies } 接下来,我们将学习如何为 `FastAPI` 应用程序添加全局依赖项,创建应用于每个*路径操作*的依赖项。 diff --git a/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md index a863bb861..bf495c9f3 100644 --- a/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,136 +1,139 @@ -# 使用yield的依赖项 +# 使用 yield 的依赖项 { #dependencies-with-yield } -FastAPI支持在完成后执行一些额外步骤的依赖项. +FastAPI 支持那些在完成后执行一些额外步骤的依赖项。 -为此,你需要使用 `yield` 而不是 `return`,然后再编写这些额外的步骤(代码)。 +为此,使用 `yield` 而不是 `return`,并把这些额外步骤(代码)写在后面。 /// tip | 提示 -确保在每个依赖中只使用一次 `yield`。 +确保在每个依赖里只使用一次 `yield`。 /// /// note | 技术细节 -任何一个可以与以下内容一起使用的函数: +任何可以与以下装饰器一起使用的函数: -* `@contextlib.contextmanager` 或者 +* `@contextlib.contextmanager` 或 * `@contextlib.asynccontextmanager` 都可以作为 **FastAPI** 的依赖项。 -实际上,FastAPI内部就使用了这两个装饰器。 +实际上,FastAPI 在内部就是用的这两个装饰器。 /// -## 使用 `yield` 的数据库依赖项 +## 使用 `yield` 的数据库依赖项 { #a-database-dependency-with-yield } -例如,你可以使用这种方式创建一个数据库会话,并在完成后关闭它。 +例如,你可以用这种方式创建一个数据库会话,并在完成后将其关闭。 -在发送响应之前,只会执行 `yield` 语句及之前的代码: +在创建响应之前,只会执行 `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` 语句后面的代码会在创建响应后,发送响应前执行: +`yield` 语句后面的代码会在响应之后执行: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | 提示 你可以使用 `async` 或普通函数。 -**FastAPI** 会像处理普通依赖一样,对每个依赖做正确的处理。 +**FastAPI** 会像处理普通依赖一样对它们进行正确处理。 /// -## 包含 `yield` 和 `try` 的依赖项 +## 同时使用 `yield` 和 `try` 的依赖项 { #a-dependency-with-yield-and-try } -如果在包含 `yield` 的依赖中使用 `try` 代码块,你会捕获到使用依赖时抛出的任何异常。 +如果你在带有 `yield` 的依赖中使用了 `try` 代码块,那么当使用该依赖时抛出的任何异常你都会收到。 -例如,如果某段代码在另一个依赖中或在 *路由函数* 中使数据库事务"回滚"或产生任何其他错误,你将会在依赖中捕获到异常。 +例如,如果在中间的某处代码中(在另一个依赖或在某个 *路径操作* 中)发生了数据库事务“回滚”或产生了其他异常,你会在你的依赖中收到这个异常。 -因此,你可以使用 `except SomeException` 在依赖中捕获特定的异常。 +因此,你可以在该依赖中用 `except SomeException` 来捕获这个特定异常。 -同样,你也可以使用 `finally` 来确保退出步骤得到执行,无论是否存在异常。 +同样地,你可以使用 `finally` 来确保退出步骤一定会被执行,无论是否发生异常。 -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} -## 使用 `yield` 的子依赖项 +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -你可以声明任意数量和层级的树状依赖,而且它们中的任何一个或所有的都可以使用 `yield`。 +## 使用 `yield` 的子依赖项 { #sub-dependencies-with-yield } -**FastAPI** 会确保每个带有 `yield` 的依赖中的"退出代码"按正确顺序运行。 +你可以声明任意大小和形状的子依赖及其“树”,其中任意一个或全部都可以使用 `yield`。 -例如,`dependency_c` 可以依赖于 `dependency_b`,而 `dependency_b` 则依赖于 `dependency_a`。 +**FastAPI** 会确保每个带有 `yield` 的依赖中的“退出代码”按正确的顺序运行。 + +例如,`dependency_c` 可以依赖 `dependency_b`,而 `dependency_b` 则依赖 `dependency_a`: {* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} -所有这些依赖都可以使用 `yield`。 +并且它们都可以使用 `yield`。 -在这种情况下,`dependency_c` 在执行其退出代码时需要 `dependency_b`(此处称为 `dep_b`)的值仍然可用。 +在这种情况下,`dependency_c` 在执行其退出代码时需要 `dependency_b`(此处命名为 `dep_b`)的值仍然可用。 -而 `dependency_b` 反过来则需要 `dependency_a`(此处称为 `dep_a` )的值在其退出代码中可用。 +而 `dependency_b` 又需要 `dependency_a`(此处命名为 `dep_a`)的值在其退出代码中可用。 {* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} -同样,你可以混合使用带有 `yield` 或 `return` 的依赖。 +同样地,你可以将一些依赖用 `yield`,另一些用 `return`,并让其中一些依赖依赖于另一些。 -你也可以声明一个依赖于多个带有 `yield` 的依赖,等等。 +你也可以有一个依赖需要多个带有 `yield` 的依赖,等等。 你可以拥有任何你想要的依赖组合。 -**FastAPI** 将确保按正确的顺序运行所有内容。 +**FastAPI** 将确保一切都按正确的顺序运行。 /// note | 技术细节 -这是由 Python 的上下文管理器完成的。 +这要归功于 Python 的上下文管理器。 **FastAPI** 在内部使用它们来实现这一点。 /// -## 包含 `yield` 和 `HTTPException` 的依赖项 +## 同时使用 `yield` 和 `HTTPException` 的依赖项 { #dependencies-with-yield-and-httpexception } -你可以使用带有 `yield` 的依赖项,并且可以包含 `try` 代码块用于捕获异常。 +你已经看到可以在带有 `yield` 的依赖中使用 `try` 块尝试执行一些代码,然后在 `finally` 之后运行一些退出代码。 -同样,你可以在 `yield` 之后的退出代码中抛出一个 `HTTPException` 或类似的异常。 +你也可以使用 `except` 来捕获引发的异常并对其进行处理。 + +例如,你可以抛出一个不同的异常,如 `HTTPException`。 /// tip | 提示 -这是一种相对高级的技巧,在大多数情况下你并不需要使用它,因为你可以在其他代码中抛出异常(包括 `HTTPException` ),例如在 *路由函数* 中。 +这是一种相对高级的技巧,在大多数情况下你并不需要使用它,因为你可以在应用的其他代码中(例如在 *路径操作函数* 里)抛出异常(包括 `HTTPException`)。 -但是如果你需要,你也可以在依赖项中做到这一点。🤓 +但是如果你需要,它就在这里。🤓 /// {* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} -你还可以创建一个 [自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} 用于捕获异常(同时也可以抛出另一个 `HTTPException`)。 +如果你想捕获异常并基于它创建一个自定义响应,请创建一个[自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}。 -## 包含 `yield` 和 `except` 的依赖项 +## 同时使用 `yield` 和 `except` 的依赖项 { #dependencies-with-yield-and-except } -如果你在包含 `yield` 的依赖项中使用 `except` 捕获了一个异常,然后你没有重新抛出该异常(或抛出一个新异常),与在普通的Python代码中相同,FastAPI不会注意到发生了异常。 +如果你在带有 `yield` 的依赖中使用 `except` 捕获了一个异常,并且你没有再次抛出它(或抛出一个新异常),FastAPI 将无法察觉发生过异常,就像普通的 Python 代码那样: {* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} -在示例代码的情况下,客户端将会收到 *HTTP 500 Internal Server Error* 的响应,因为我们没有抛出 `HTTPException` 或者类似的异常,并且服务器也 **不会有任何日志** 或者其他提示来告诉我们错误是什么。😱 +在这种情况下,客户端会像预期那样看到一个 *HTTP 500 Internal Server Error* 响应,因为我们没有抛出 `HTTPException` 或类似异常,但服务器将**没有任何日志**或其他关于错误是什么的提示。😱 -### 在包含 `yield` 和 `except` 的依赖项中一定要 `raise` +### 在带有 `yield` 和 `except` 的依赖中务必 `raise` { #always-raise-in-dependencies-with-yield-and-except } -如果你在使用 `yield` 的依赖项中捕获到了一个异常,你应该再次抛出捕获到的异常,除非你抛出 `HTTPException` 或类似的其他异常, +如果你在带有 `yield` 的依赖中捕获到了一个异常,除非你抛出另一个 `HTTPException` 或类似异常,**否则你应该重新抛出原始异常**。 -你可以使用 `raise` 再次抛出捕获到的异常。 +你可以使用 `raise` 重新抛出同一个异常: {* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} -现在客户端同样会得到 *HTTP 500 Internal Server Error* 响应,但是服务器日志会记录下我们自定义的 `InternalError`。 +现在客户端仍会得到同样的 *HTTP 500 Internal Server Error* 响应,但服务器日志中会有我们自定义的 `InternalError`。😎 -## 使用 `yield` 的依赖项的执行 +## 使用 `yield` 的依赖项的执行 { #execution-of-dependencies-with-yield } -执行顺序大致如下时序图所示。时间轴从上到下,每一列都代表交互或者代码执行的一部分。 +执行顺序大致如下图所示。时间轴从上到下,每一列都代表交互或执行代码的一部分。 ```mermaid sequenceDiagram @@ -167,63 +170,78 @@ participant tasks as Background tasks end ``` -/// info | 说明 +/// info | 信息 -只会向客户端发送 **一次响应** ,可能是一个错误响应,也可能是来自 *路由函数* 的响应。 +只会向客户端发送**一次响应**。它可能是某个错误响应,或者是来自 *路径操作* 的响应。 -在发送了其中一个响应之后,就无法再发送其他响应了。 +在其中一个响应发送之后,就不能再发送其他响应了。 /// /// tip | 提示 -这个时序图展示了 `HTTPException`,除此之外你也可以抛出任何你在使用 `yield` 的依赖项中或者[自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}中捕获的异常。 - -如果你引发任何异常,它将传递给使用 `yield` 的依赖项,包括 `HTTPException`。在大多数情况下你应当从使用 `yield` 的依赖项中重新抛出捕获的异常或者一个新的异常来确保它会被正确的处理。 +如果你在 *路径操作函数* 的代码中引发任何异常,它都会被传递给带有 `yield` 的依赖项,包括 `HTTPException`。在大多数情况下,你会希望在带有 `yield` 的依赖中重新抛出相同的异常或一个新的异常,以确保它被正确处理。 /// -## 包含 `yield`, `HTTPException`, `except` 的依赖项和后台任务 +## 提前退出与 `scope` { #early-exit-and-scope } -/// warning | 注意 +通常,带有 `yield` 的依赖的退出代码会在响应发送给客户端**之后**执行。 -你大概率不需要了解这些技术细节,可以跳过这一章节继续阅读后续的内容。 +但如果你知道在从 *路径操作函数* 返回之后不再需要使用该依赖,你可以使用 `Depends(scope="function")` 告诉 FastAPI:应当在 *路径操作函数* 返回后、但在**响应发送之前**关闭该依赖。 -如果你使用的FastAPI的版本早于0.106.0,并且在使用后台任务中使用了包含 `yield` 的依赖项中的资源,那么这些细节会对你有一些用处。 +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} -/// +`Depends()` 接收一个 `scope` 参数,可为: -### 包含 `yield` 和 `except` 的依赖项的技术细节 +* `"function"`:在处理请求的 *路径操作函数* 之前启动依赖,在 *路径操作函数* 结束后结束依赖,但在响应发送给客户端**之前**。因此,依赖函数将围绕这个*路径操作函数*执行。 +* `"request"`:在处理请求的 *路径操作函数* 之前启动依赖(与使用 `"function"` 时类似),但在响应发送给客户端**之后**结束。因此,依赖函数将围绕这个**请求**与响应周期执行。 -在FastAPI 0.110.0版本之前,如果使用了一个包含 `yield` 的依赖项,你在依赖项中使用 `except` 捕获了一个异常,但是你没有再次抛出该异常,这个异常会被自动抛出/转发到异常处理器或者内部服务错误处理器。 +如果未指定且依赖包含 `yield`,则默认 `scope` 为 `"request"`。 -### 后台任务和使用 `yield` 的依赖项的技术细节 +### 子依赖的 `scope` { #scope-for-sub-dependencies } -在FastAPI 0.106.0版本之前,在 `yield` 后面抛出异常是不可行的,因为 `yield` 之后的退出代码是在响应被发送之后再执行,这个时候异常处理器已经执行过了。 +当你声明一个 `scope="request"`(默认)的依赖时,任何子依赖也需要有 `"request"` 的 `scope`。 -这样设计的目的主要是为了允许在后台任务中使用被依赖项`yield`的对象,因为退出代码会在后台任务结束后再执行。 +但一个 `scope` 为 `"function"` 的依赖可以有 `scope` 为 `"function"` 和 `"request"` 的子依赖。 -然而这也意味着在等待响应通过网络传输的同时,非必要的持有一个 `yield` 依赖项中的资源(例如数据库连接),这一行为在FastAPI 0.106.0被改变了。 +这是因为任何依赖都需要能够在子依赖之前运行其退出代码,因为它的退出代码中可能还需要使用这些子依赖。 -/// tip | 提示 +```mermaid +sequenceDiagram -除此之外,后台任务通常是一组独立的逻辑,应该被单独处理,并且使用它自己的资源(例如它自己的数据库连接)。 +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation -这样也会让你的代码更加简洁。 + client ->> 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 +``` -/// +## 包含 `yield`、`HTTPException`、`except` 和后台任务的依赖项 { #dependencies-with-yield-httpexception-except-and-background-tasks } -如果你之前依赖于这一行为,那么现在你应该在后台任务中创建并使用它自己的资源,不要在内部使用属于 `yield` 依赖项的资源。 +带有 `yield` 的依赖项随着时间演进以涵盖不同的用例并修复了一些问题。 -例如,你应该在后台任务中创建一个新的数据库会话用于查询数据,而不是使用相同的会话。你应该将对象的ID作为参数传递给后台任务函数,然后在该函数中重新获取该对象,而不是直接将数据库对象作为参数。 +如果你想了解在不同 FastAPI 版本中发生了哪些变化,可以在进阶指南中阅读更多:[高级依赖项 —— 包含 `yield`、`HTTPException`、`except` 和后台任务的依赖项](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}。 -## 上下文管理器 +## 上下文管理器 { #context-managers } -### 什么是"上下文管理器" +### 什么是“上下文管理器” { #what-are-context-managers } -"上下文管理器"是你可以在 `with` 语句中使用的任何Python对象。 +“上下文管理器”是你可以在 `with` 语句中使用的任意 Python 对象。 -例如,你可以使用`with`读取文件: +例如,你可以用 `with` 来读取文件: ```Python with open("./somefile.txt") as f: @@ -231,37 +249,39 @@ with open("./somefile.txt") as f: print(contents) ``` -在底层,`open("./somefile.txt")`创建了一个被称为"上下文管理器"的对象。 +在底层,`open("./somefile.txt")` 会创建一个“上下文管理器”对象。 -当 `with` 代码块结束时,它会确保关闭文件,即使发生了异常也是如此。 +当 `with` 代码块结束时,它会确保文件被关闭,即使期间发生了异常。 -当你使用 `yield` 创建一个依赖项时,**FastAPI** 会在内部将其转换为上下文管理器,并与其他相关工具结合使用。 +当你用 `yield` 创建一个依赖时,**FastAPI** 会在内部为它创建一个上下文管理器,并与其他相关工具结合使用。 -### 在使用 `yield` 的依赖项中使用上下文管理器 +### 在带有 `yield` 的依赖中使用上下文管理器 { #using-context-managers-in-dependencies-with-yield } -/// warning | 注意 +/// warning | 警告 -这是一个更为"高级"的想法。 +这算是一个“高级”概念。 -如果你刚开始使用 **FastAPI** ,你可以暂时可以跳过它。 +如果你刚开始使用 **FastAPI**,现在可以先跳过。 /// -在Python中,你可以通过创建一个带有`__enter__()`和`__exit__()`方法的类来创建上下文管理器。 +在 Python 中,你可以通过创建一个带有 `__enter__()` 和 `__exit__()` 方法的类来创建上下文管理器。 -你也可以在 **FastAPI** 的 `yield` 依赖项中通过 `with` 或者 `async with` 语句来使用它们: +你也可以在 **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 | 提示 -另一种创建上下文管理器的方法是: +另一种创建上下文管理器的方式是: -* `@contextlib.contextmanager`或者 -* `@contextlib.asynccontextmanager` +* `@contextlib.contextmanager` 或 +* `@contextlib.asynccontextmanager` -使用它们装饰一个只有单个 `yield` 的函数。这就是 **FastAPI** 内部对于 `yield` 依赖项的处理方式。 +用它们去装饰一个只包含单个 `yield` 的函数。 -但是你不需要为FastAPI的依赖项使用这些装饰器(而且也不应该)。FastAPI会在内部为你处理这些。 +这正是 **FastAPI** 在内部处理带有 `yield` 的依赖时所使用的方式。 + +但你不需要(也不应该)为 FastAPI 的依赖去使用这些装饰器。FastAPI 会在内部为你处理好。 /// diff --git a/docs/zh/docs/tutorial/dependencies/global-dependencies.md b/docs/zh/docs/tutorial/dependencies/global-dependencies.md index 797fd76b7..36cf9cf44 100644 --- a/docs/zh/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/global-dependencies.md @@ -1,15 +1,15 @@ -# 全局依赖项 +# 全局依赖项 { #global-dependencies } 有时,我们要为整个应用添加依赖项。 -通过与定义[*路径装饰器依赖项*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 类似的方式,可以把依赖项添加至整个 `FastAPI` 应用。 +通过与[将 `dependencies` 添加到*路径操作装饰器*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 类似的方式,可以把依赖项添加至整个 `FastAPI` 应用。 这样一来,就可以为所有*路径操作*应用该依赖项: -{* ../../docs_src/dependencies/tutorial012.py hl[15] *} +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} -[*路径装饰器依赖项*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 一章的思路均适用于全局依赖项, 在本例中,这些依赖项可以用于应用中的所有*路径操作*。 +[将 `dependencies` 添加到*路径操作装饰器*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 一章的思路均适用于全局依赖项, 在本例中,这些依赖项可以用于应用中的所有*路径操作*。 -## 为一组路径操作定义依赖项 +## 为一组路径操作定义依赖项 { #dependencies-for-groups-of-path-operations } 稍后,[大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=_blank}一章中会介绍如何使用多个文件创建大型应用程序,在这一章中,您将了解到如何为一组*路径操作*声明单个 `dependencies` 参数。 diff --git a/docs/zh/docs/tutorial/dependencies/index.md b/docs/zh/docs/tutorial/dependencies/index.md index 9eec69ed5..20d2c0b64 100644 --- a/docs/zh/docs/tutorial/dependencies/index.md +++ b/docs/zh/docs/tutorial/dependencies/index.md @@ -1,85 +1,97 @@ -# 依赖项 +# 依赖项 { #dependencies } -FastAPI 提供了简单易用,但功能强大的**依赖注入**系统。 +**FastAPI** 提供了简单直观但功能强大的**依赖注入**系统。 -这个依赖系统设计的简单易用,可以让开发人员轻松地把组件集成至 **FastAPI**。 +它被设计得非常易用,能让任何开发者都能轻松把其他组件与 **FastAPI** 集成。 -## 什么是「依赖注入」 +## 什么是「依赖注入」 { #what-is-dependency-injection } -编程中的**「依赖注入」**是声明代码(本文中为*路径操作函数* )运行所需的,或要使用的「依赖」的一种方式。 +在编程中,**「依赖注入」**指的是,你的代码(本文中为*路径操作函数*)声明其运行所需并要使用的东西:“依赖”。 -然后,由系统(本文中为 **FastAPI**)负责执行任意需要的逻辑,为代码提供这些依赖(「注入」依赖项)。 +然后,由该系统(本文中为 **FastAPI**)负责执行所有必要的逻辑,为你的代码提供这些所需的依赖(“注入”依赖)。 -依赖注入常用于以下场景: +当你需要以下内容时,这非常有用: -* 共享业务逻辑(复用相同的代码逻辑) +* 共享业务逻辑(同一段代码逻辑反复复用) * 共享数据库连接 -* 实现安全、验证、角色权限 -* 等…… +* 实施安全、认证、角色权限等要求 +* 以及更多其他内容... -上述场景均可以使用**依赖注入**,将代码重复最小化。 +同时尽量减少代码重复。 -## 第一步 +## 第一步 { #first-steps } -接下来,我们学习一个非常简单的例子,尽管它过于简单,不是很实用。 +先来看一个非常简单的例子。它现在简单到几乎没什么用。 -但通过这个例子,您可以初步了解「依赖注入」的工作机制。 +但这样我们就可以专注于**依赖注入**系统是如何工作的。 -### 创建依赖项 +### 创建依赖项,或“dependable” { #create-a-dependency-or-dependable } -首先,要关注的是依赖项。 +首先关注依赖项。 -依赖项就是一个函数,且可以使用与*路径操作函数*相同的参数: +它只是一个函数,且可以接收与*路径操作函数*相同的所有参数: -{* ../../docs_src/dependencies/tutorial001.py hl[8:11] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} 大功告成。 只用了**2 行**代码。 -依赖项函数的形式和结构与*路径操作函数*一样。 +它的形式和结构与所有*路径操作函数*相同。 -因此,可以把依赖项当作没有「装饰器」(即,没有 `@app.get("/some-path")` )的路径操作函数。 +你可以把它当作没有“装饰器”(没有 `@app.get("/some-path")`)的*路径操作函数*。 -依赖项可以返回各种内容。 +而且它可以返回任何你想要的内容。 -本例中的依赖项预期接收如下参数: +本例中的依赖项预期接收: * 类型为 `str` 的可选查询参数 `q` -* 类型为 `int` 的可选查询参数 `skip`,默认值是 `0` -* 类型为 `int` 的可选查询参数 `limit`,默认值是 `100` +* 类型为 `int` 的可选查询参数 `skip`,默认值 `0` +* 类型为 `int` 的可选查询参数 `limit`,默认值 `100` -然后,依赖项函数返回包含这些值的 `dict`。 +然后它只需返回一个包含这些值的 `dict`。 -### 导入 `Depends` +/// info | 信息 -{* ../../docs_src/dependencies/tutorial001.py hl[3] *} +FastAPI 在 0.95.0 版本中新增了对 `Annotated` 的支持(并开始推荐使用)。 -### 声明依赖项 +如果你的版本较旧,尝试使用 `Annotated` 会报错。 -与在*路径操作函数*参数中使用 `Body`、`Query` 的方式相同,声明依赖项需要使用 `Depends` 和一个新的参数: - -{* ../../docs_src/dependencies/tutorial001.py hl[15,20] *} - -虽然,在路径操作函数的参数中使用 `Depends` 的方式与 `Body`、`Query` 相同,但 `Depends` 的工作方式略有不同。 - -这里只能传给 Depends 一个参数。 - -且该参数必须是可调用对象,比如函数。 - -该函数接收的参数和*路径操作函数*的参数一样。 - -/// tip | 提示 - -下一章介绍,除了函数还有哪些「对象」可以用作依赖项。 +在使用 `Annotated` 之前,请确保[升级 FastAPI 版本](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}到至少 0.95.1。 /// -接收到新的请求时,**FastAPI** 执行如下操作: +### 导入 `Depends` { #import-depends } -* 用正确的参数调用依赖项函数(「可依赖项」) +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} + +### 在“dependant”中声明依赖项 { #declare-the-dependency-in-the-dependant } + +与在*路径操作函数*的参数中使用 `Body`、`Query` 等相同,给参数使用 `Depends` 来声明一个新的依赖项: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} + +虽然你在函数参数中使用 `Depends` 的方式与 `Body`、`Query` 等相同,但 `Depends` 的工作方式略有不同。 + +这里只能给 `Depends` 传入一个参数。 + +这个参数必须是类似函数的可调用对象。 + +你不需要直接调用它(不要在末尾加括号),只需将其作为参数传给 `Depends()`。 + +该函数接收的参数与*路径操作函数*的参数相同。 + +/// tip | 提示 + +下一章会介绍除了函数之外,还有哪些“东西”可以用作依赖项。 + +/// + +接收到新的请求时,**FastAPI** 会负责: + +* 用正确的参数调用你的依赖项(“dependable”)函数 * 获取函数返回的结果 -* 把函数返回的结果赋值给*路径操作函数*的参数 +* 将该结果赋值给你的*路径操作函数*中的参数 ```mermaid graph TB @@ -92,95 +104,121 @@ common_parameters --> read_items common_parameters --> read_users ``` -这样,只编写一次代码,**FastAPI** 就可以为多个*路径操作*共享这段代码 。 +这样,你只需编写一次共享代码,**FastAPI** 会在你的*路径操作*中为你调用它。 /// check | 检查 -注意,无需创建专门的类,并将之传递给 **FastAPI** 以进行「注册」或执行类似的操作。 +注意,无需创建专门的类并传给 **FastAPI** 去“注册”之类的操作。 -只要把它传递给 `Depends`,**FastAPI** 就知道该如何执行后续操作。 +只要把它传给 `Depends`,**FastAPI** 就知道该怎么做了。 /// -## 要不要使用 `async`? +## 共享 `Annotated` 依赖项 { #share-annotated-dependencies } -**FastAPI** 调用依赖项的方式与*路径操作函数*一样,因此,定义依赖项函数,也要应用与路径操作函数相同的规则。 +在上面的示例中,你会发现这里有一点点**代码重复**。 -即,既可以使用异步的 `async def`,也可以使用普通的 `def` 定义依赖项。 +当你需要使用 `common_parameters()` 这个依赖时,你必须写出完整的带类型注解和 `Depends()` 的参数: -在普通的 `def` *路径操作函数*中,可以声明异步的 `async def` 依赖项;也可以在异步的 `async def` *路径操作函数*中声明普通的 `def` 依赖项。 +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` -上述这些操作都是可行的,**FastAPI** 知道该怎么处理。 +但因为我们使用了 `Annotated`,可以把这个 `Annotated` 的值存到一个变量里,在多个地方复用: -/// note | 笔记 +{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} -如里不了解异步,请参阅[异步:*“着急了?”*](../../async.md){.internal-link target=_blank} 一章中 `async` 和 `await` 的内容。 +/// tip | 提示 + +这只是标准的 Python,叫做“类型别名”,并不是 **FastAPI** 特有的。 + +但因为 **FastAPI** 基于 Python 标准(包括 `Annotated`),你就可以在代码里使用这个技巧。😎 /// -## 与 OpenAPI 集成 +这些依赖会照常工作,而**最棒的是**,**类型信息会被保留**,这意味着你的编辑器依然能提供**自动补全**、**行内报错**等。同样适用于 `mypy` 等其他工具。 -依赖项及子依赖项的所有请求声明、验证和需求都可以集成至同一个 OpenAPI 概图。 +当你在**大型代码库**中,在**很多*路径操作***里反复使用**相同的依赖**时,这会特别有用。 -所以,交互文档里也会显示依赖项的所有信息: +## 要不要使用 `async`? { #to-async-or-not-to-async } + +由于依赖项也会由 **FastAPI** 调用(与*路径操作函数*相同),因此定义函数时同样的规则也适用。 + +你可以使用 `async def` 或普通的 `def`。 + +你可以在普通的 `def` *路径操作函数*中声明 `async def` 的依赖项;也可以在异步的 `async def` *路径操作函数*中声明普通的 `def` 依赖项,等等。 + +都没关系,**FastAPI** 知道该怎么处理。 + +/// note | 注意 + +如果不了解异步,请参阅文档中关于 `async` 和 `await` 的章节:[异步:*“着急了?”*](../../async.md#in-a-hurry){.internal-link target=_blank}。 + +/// + +## 与 OpenAPI 集成 { #integrated-with-openapi } + +依赖项及子依赖项中声明的所有请求、验证和需求都会集成到同一个 OpenAPI 模式中。 + +因此,交互式文档中也会包含这些依赖项的所有信息: -## 简单用法 +## 简单用法 { #simple-usage } -观察一下就会发现,只要*路径* 和*操作*匹配,就可以使用声明的路径操作函数。然后,**FastAPI** 会用正确的参数调用函数,并提取请求中的数据。 +观察一下就会发现,只要*路径*和*操作*匹配,就会使用声明的*路径操作函数*。随后,**FastAPI** 会用正确的参数调用该函数,并从请求中提取数据。 -实际上,所有(或大多数)网络框架的工作方式都是这样的。 +事实上,所有(或大多数)Web 框架的工作方式都是这样的。 -开发人员永远都不需要直接调用这些函数,这些函数是由框架(在此为 **FastAPI** )调用的。 +你从不会直接调用这些函数。它们由你的框架(此处为 **FastAPI**)调用。 -通过依赖注入系统,只要告诉 **FastAPI** *路径操作函数* 还要「依赖」其他在*路径操作函数*之前执行的内容,**FastAPI** 就会执行函数代码,并「注入」函数返回的结果。 +通过依赖注入系统,你还可以告诉 **FastAPI**,你的*路径操作函数*还“依赖”某些应在*路径操作函数*之前执行的内容,**FastAPI** 会负责执行它并“注入”结果。 -其他与「依赖注入」概念相同的术语为: +“依赖注入”的其他常见术语包括: -* 资源(Resource) -* 提供方(Provider) -* 服务(Service) -* 可注入(Injectable) -* 组件(Component) +* 资源(resources) +* 提供方(providers) +* 服务(services) +* 可注入(injectables) +* 组件(components) -## **FastAPI** 插件 +## **FastAPI** 插件 { #fastapi-plug-ins } -**依赖注入**系统支持构建集成和「插件」。但实际上,FastAPI 根本**不需要创建「插件」**,因为使用依赖项可以声明不限数量的、可用于*路径操作函数*的集成与交互。 +可以使用**依赖注入**系统构建集成和“插件”。但实际上,根本**不需要创建“插件”**,因为通过依赖项可以声明无限多的集成与交互,使其可用于*路径操作函数*。 -创建依赖项非常简单、直观,并且还支持导入 Python 包。毫不夸张地说,只要几行代码就可以把需要的 Python 包与 API 函数集成在一起。 +依赖项可以用非常简单直观的方式创建,你只需导入所需的 Python 包,用*字面意义上的*几行代码就能把它们与你的 API 函数集成起来。 -下一章将详细介绍在关系型数据库、NoSQL 数据库、安全等方面使用依赖项的例子。 +在接下来的章节中,你会看到关于关系型数据库、NoSQL 数据库、安全等方面的示例。 -## **FastAPI** 兼容性 +## **FastAPI** 兼容性 { #fastapi-compatibility } -依赖注入系统如此简洁的特性,让 **FastAPI** 可以与下列系统兼容: +依赖注入系统的简洁让 **FastAPI** 能与以下内容兼容: -* 关系型数据库 +* 各类关系型数据库 * NoSQL 数据库 -* 外部支持库 +* 外部包 * 外部 API -* 认证和鉴权系统 +* 认证与授权系统 * API 使用监控系统 * 响应数据注入系统 -* 等等…… +* 等等... -## 简单而强大 +## 簡单而强大 { #simple-and-powerful } -虽然,**层级式依赖注入系统**的定义与使用十分简单,但它却非常强大。 +虽然**层级式依赖注入系统**的定义与使用非常简单,但它依然非常强大。 -比如,可以定义依赖其他依赖项的依赖项。 +你可以定义依赖其他依赖项的依赖项。 -最后,依赖项层级树构建后,**依赖注入系统**会处理所有依赖项及其子依赖项,并为每一步操作提供(注入)结果。 +最终会构建出一个依赖项的层级树,**依赖注入**系统会处理所有这些依赖(及其子依赖),并在每一步提供(注入)相应的结果。 -比如,下面有 4 个 API 路径操作(*端点*): +例如,假设你有 4 个 API 路径操作(*端点*): * `/items/public/` * `/items/private/` * `/users/{user_id}/activate` * `/items/pro/` -开发人员可以使用依赖项及其子依赖项为这些路径操作添加不同的权限: +你可以仅通过依赖项及其子依赖项为它们添加不同的权限要求: ```mermaid graph TB @@ -205,8 +243,8 @@ admin_user --> activate_user paying_user --> pro_items ``` -## 与 **OpenAPI** 集成 +## 与 **OpenAPI** 集成 { #integrated-with-openapi_1 } -在声明需求时,所有这些依赖项还会把参数、验证等功能添加至路径操作。 +在声明需求的同时,所有这些依赖项也会为你的*路径操作*添加参数、验证等内容。 -**FastAPI** 负责把上述内容全部添加到 OpenAPI 概图,并显示在交互文档中。 +**FastAPI** 会负责把这些全部添加到 OpenAPI 模式中,以便它们显示在交互式文档系统里。 diff --git a/docs/zh/docs/tutorial/dependencies/sub-dependencies.md b/docs/zh/docs/tutorial/dependencies/sub-dependencies.md index 2e7746433..0b73c392d 100644 --- a/docs/zh/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# 子依赖项 +# 子依赖项 { #sub-dependencies } FastAPI 支持创建含**子依赖项**的依赖项。 @@ -6,34 +6,34 @@ FastAPI 支持创建含**子依赖项**的依赖项。 **FastAPI** 负责处理解析不同深度的子依赖项。 -### 第一层依赖项 +## 第一层依赖项 “dependable” { #first-dependency-dependable } -下列代码创建了第一层依赖项: +你可以创建一个第一层依赖项(“dependable”),如下: -{* ../../docs_src/dependencies/tutorial005.py hl[8:9] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} 这段代码声明了类型为 `str` 的可选查询参数 `q`,然后返回这个查询参数。 这个函数很简单(不过也没什么用),但却有助于让我们专注于了解子依赖项的工作方式。 -### 第二层依赖项 +## 第二层依赖项,“dependable”和“dependant” { #second-dependency-dependable-and-dependant } -接下来,创建另一个依赖项函数,并同时用该依赖项自身再声明一个依赖项(所以这也是一个「依赖项」): +接下来,创建另一个依赖项函数(一个“dependable”),并同时为它自身再声明一个依赖项(因此它同时也是一个“dependant”): -{* ../../docs_src/dependencies/tutorial005.py hl[13] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} 这里重点说明一下声明的参数: -* 尽管该函数自身是依赖项,但还声明了另一个依赖项(它「依赖」于其他对象) +* 尽管该函数自身是依赖项(“dependable”),但还声明了另一个依赖项(它“依赖”于其他对象) * 该函数依赖 `query_extractor`, 并把 `query_extractor` 的返回值赋给参数 `q` * 同时,该函数还声明了类型是 `str` 的可选 cookie(`last_query`) * 用户未提供查询参数 `q` 时,则使用上次使用后保存在 cookie 中的查询 -### 使用依赖项 +## 使用依赖项 { #use-the-dependency } 接下来,就可以使用依赖项: -{* ../../docs_src/dependencies/tutorial005.py hl[22] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} /// info | 信息 @@ -54,20 +54,39 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## 多次使用同一个依赖项 +## 多次使用同一个依赖项 { #using-the-same-dependency-multiple-times } 如果在同一个*路径操作* 多次声明了同一个依赖项,例如,多个依赖项共用一个子依赖项,**FastAPI** 在处理同一请求时,只调用一次该子依赖项。 FastAPI 不会为同一个请求多次调用同一个依赖项,而是把依赖项的返回值进行「缓存」,并把它传递给同一请求中所有需要使用该返回值的「依赖项」。 -在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 `Depends` 的参数 `use_cache` 的值设置为 `False` : +在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 `Depends` 的参数 `use_cache` 的值设置为 `False`: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ 非 Annotated + +/// tip | 提示 + +尽可能优先使用 `Annotated` 版本。 + +/// ```Python hl_lines="1" async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): return {"fresh_value": fresh_value} ``` -## 小结 +//// + +## 小结 { #recap } 千万别被本章里这些花里胡哨的词藻吓倒了,其实**依赖注入**系统非常简单。 diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md index e52aaa2ed..f47a09201 100644 --- a/docs/zh/docs/tutorial/encoder.md +++ b/docs/zh/docs/tutorial/encoder.md @@ -1,4 +1,4 @@ -# JSON 兼容编码器 +# JSON 兼容编码器 { #json-compatible-encoder } 在某些情况下,您可能需要将数据类型(如Pydantic模型)转换为与JSON兼容的数据类型(如`dict`、`list`等)。 @@ -6,7 +6,7 @@ 对于这种要求, **FastAPI**提供了`jsonable_encoder()`函数。 -## 使用`jsonable_encoder` +## 使用`jsonable_encoder` { #using-the-jsonable-encoder } 让我们假设你有一个数据库名为`fake_db`,它只能接收与JSON兼容的数据。 @@ -28,7 +28,7 @@ 这个操作不会返回一个包含JSON格式(作为字符串)数据的庞大的`str`。它将返回一个Python标准数据结构(例如`dict`),其值和子值都与JSON兼容。 -/// note +/// note | 注意 `jsonable_encoder`实际上是FastAPI内部用来转换数据的。但是它在许多其他场景中也很有用。 diff --git a/docs/zh/docs/tutorial/extra-data-types.md b/docs/zh/docs/tutorial/extra-data-types.md index b064ee551..2cefd163d 100644 --- a/docs/zh/docs/tutorial/extra-data-types.md +++ b/docs/zh/docs/tutorial/extra-data-types.md @@ -1,4 +1,4 @@ -# 额外数据类型 +# 额外数据类型 { #extra-data-types } 到目前为止,您一直在使用常见的数据类型,如: @@ -15,9 +15,9 @@ * 传入请求的数据转换。 * 响应数据转换。 * 数据验证。 -* 自动补全和文档。 +* 自动注解和文档。 -## 其他数据类型 +## 其他数据类型 { #other-data-types } 下面是一些你可以使用的其他数据类型: @@ -36,12 +36,12 @@ * `datetime.timedelta`: * 一个 Python `datetime.timedelta`. * 在请求和响应中将表示为 `float` 代表总秒数。 - * Pydantic 也允许将其表示为 "ISO 8601 时间差异编码", 查看文档了解更多信息。 + * Pydantic 也允许将其表示为 "ISO 8601 时间差异编码", 查看文档了解更多信息。 * `frozenset`: * 在请求和响应中,作为 `set` 对待: * 在请求中,列表将被读取,消除重复,并将其转换为一个 `set`。 * 在响应中 `set` 将被转换为 `list` 。 - * 产生的模式将指定那些 `set` 的值是唯一的 (使用 JSON 模式的 `uniqueItems`)。 + * 产生的模式将指定那些 `set` 的值是唯一的 (使用 JSON Schema 的 `uniqueItems`)。 * `bytes`: * 标准的 Python `bytes`。 * 在请求和响应中被当作 `str` 处理。 @@ -49,9 +49,9 @@ * `Decimal`: * 标准的 Python `Decimal`。 * 在请求和响应中被当做 `float` 一样处理。 -* 您可以在这里检查所有有效的pydantic数据类型: Pydantic data types. +* 您可以在这里检查所有有效的 Pydantic 数据类型: Pydantic data types. -## 例子 +## 例子 { #example } 下面是一个*路径操作*的示例,其中的参数使用了上面的一些类型。 diff --git a/docs/zh/docs/tutorial/extra-models.md b/docs/zh/docs/tutorial/extra-models.md index ccfb3aa5a..4d18c76ec 100644 --- a/docs/zh/docs/tutorial/extra-models.md +++ b/docs/zh/docs/tutorial/extra-models.md @@ -1,4 +1,4 @@ -# 更多模型 +# 更多模型 { #extra-models } 书接上文,多个关联模型这种情况很常见。 @@ -6,29 +6,29 @@ * **输入模型**应该含密码 * **输出模型**不应含密码 -* **数据库模型**需要加密的密码 +* **数据库模型**可能需要包含哈希后的密码 -/// danger | 危险 +/// danger -千万不要存储用户的明文密码。始终存储可以进行验证的**安全哈希值**。 +不要存储用户的明文密码。始终只存储之后可用于校验的“安全哈希”。 -如果不了解这方面的知识,请参阅[安全性中的章节](security/simple-oauth2.md#password-hashing){.internal-link target=_blank},了解什么是**密码哈希**。 +如果你还不了解,可以在[安全性章节](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}中学习什么是“密码哈希”。 /// -## 多个模型 +## 多个模型 { #multiple-models } 下面的代码展示了不同模型处理密码字段的方式,及使用位置的大致思路: {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -### `**user_in.dict()` 简介 +### 关于 `**user_in.model_dump()` { #about-user-in-model-dump } -#### Pydantic 的 `.dict()` +#### Pydantic 的 `.model_dump()` { #pydantics-model-dump } `user_in` 是类 `UserIn` 的 Pydantic 模型。 -Pydantic 模型支持 `.dict()` 方法,能返回包含模型数据的**字典**。 +Pydantic 模型有 `.model_dump()` 方法,会返回包含模型数据的 `dict`。 因此,如果使用如下方式创建 Pydantic 对象 `user_in`: @@ -39,10 +39,10 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com 就能以如下方式调用: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` -现在,变量 `user_dict`中的就是包含数据的**字典**(变量 `user_dict` 是字典,不是 Pydantic 模型对象)。 +现在,变量 `user_dict` 中的是包含数据的 `dict`(它是 `dict`,不是 Pydantic 模型对象)。 以如下方式调用: @@ -50,7 +50,7 @@ user_dict = user_in.dict() print(user_dict) ``` -输出的就是 Python **字典**: +输出的就是 Python `dict`: ```Python { @@ -61,9 +61,9 @@ print(user_dict) } ``` -#### 解包 `dict` +#### 解包 `dict` { #unpacking-a-dict } -把**字典** `user_dict` 以 `**user_dict` 形式传递给函数(或类),Python 会执行**解包**操作。它会把 `user_dict` 的键和值作为关键字参数直接传递。 +把 `dict`(如 `user_dict`)以 `**user_dict` 形式传递给函数(或类),Python 会执行“解包”。它会把 `user_dict` 的键和值作为关键字参数直接传递。 因此,接着上面的 `user_dict` 继续编写如下代码: @@ -82,7 +82,7 @@ UserInDB( ) ``` -或更精准,直接把可能会用到的内容与 `user_dict` 一起使用: +或更精准,直接使用 `user_dict`(无论它将来包含什么字段): ```Python UserInDB( @@ -93,31 +93,31 @@ UserInDB( ) ``` -#### 用其它模型中的内容生成 Pydantic 模型 +#### 用另一个模型的内容生成 Pydantic 模型 { #a-pydantic-model-from-the-contents-of-another } -上例中 ,从 `user_in.dict()` 中得到了 `user_dict`,下面的代码: +上例中 ,从 `user_in.model_dump()` 中得到了 `user_dict`,下面的代码: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` 等效于: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -……因为 `user_in.dict()` 是字典,在传递给 `UserInDB` 时,把 `**` 加在 `user_in.dict()` 前,可以让 Python 进行**解包**。 +……因为 `user_in.model_dump()` 是 `dict`,在传递给 `UserInDB` 时,把 `**` 加在 `user_in.model_dump()` 前,可以让 Python 进行解包。 这样,就可以用其它 Pydantic 模型中的数据生成 Pydantic 模型。 -#### 解包 `dict` 和更多关键字 +#### 解包 `dict` 并添加额外关键字参数 { #unpacking-a-dict-and-extra-keywords } 接下来,继续添加关键字参数 `hashed_password=hashed_password`,例如: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ……输出结果如下: @@ -132,68 +132,80 @@ UserInDB( ) ``` -/// warning | 警告 +/// warning -辅助的附加函数只是为了演示可能的数据流,但它们显然不能提供任何真正的安全机制。 +配套的辅助函数 `fake_password_hasher` 和 `fake_save_user` 仅用于演示可能的数据流,当然并不提供真实的安全性。 /// -## 减少重复 +## 减少重复 { #reduce-duplication } -**FastAPI** 的核心思想就是减少代码重复。 +减少代码重复是 **FastAPI** 的核心思想之一。 代码重复会导致 bug、安全问题、代码失步等问题(更新了某个位置的代码,但没有同步更新其它位置的代码)。 上面的这些模型共享了大量数据,拥有重复的属性名和类型。 -FastAPI 可以做得更好。 +我们可以做得更好。 -声明 `UserBase` 模型作为其它模型的基类。然后,用该类衍生出继承其属性(类型声明、验证等)的子类。 +声明 `UserBase` 模型作为其它模型的基类。然后,用该类衍生出继承其属性(类型声明、校验等)的子类。 所有数据转换、校验、文档等功能仍将正常运行。 -这样,就可以仅声明模型之间的差异部分(具有明文的 `password`、具有 `hashed_password` 以及不包括密码)。 - -通过这种方式,可以只声明模型之间的区别(分别包含明文密码、哈希密码,以及无密码的模型)。 +这样,就可以仅声明模型之间的差异部分(具有明文的 `password`、具有 `hashed_password` 以及不包括密码): {* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union` 或者 `anyOf` +## `Union` 或 `anyOf` { #union-or-anyof } -响应可以声明为两种类型的 `Union` 类型,即该响应可以是两种类型中的任意类型。 +响应可以声明为两个或多个类型的 `Union`,即该响应可以是这些类型中的任意一种。 -在 OpenAPI 中可以使用 `anyOf` 定义。 +在 OpenAPI 中会用 `anyOf` 表示。 为此,请使用 Python 标准类型提示 `typing.Union`: -/// note | 笔记 +/// note -定义 `Union` 类型时,要把详细的类型写在前面,然后是不太详细的类型。下例中,更详细的 `PlaneItem` 位于 `Union[PlaneItem,CarItem]` 中的 `CarItem` 之前。 +定义 `Union` 类型时,要把更具体的类型写在前面,然后是不太具体的类型。下例中,更具体的 `PlaneItem` 位于 `Union[PlaneItem, CarItem]` 中的 `CarItem` 之前。 /// {* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} -## 模型列表 +### Python 3.10 中的 `Union` { #union-in-python-3-10 } -使用同样的方式也可以声明由对象列表构成的响应。 +在这个示例中,我们把 `Union[PlaneItem, CarItem]` 作为参数 `response_model` 的值传入。 -为此,请使用标准的 Python `typing.List`: +因为这是作为“参数的值”而不是放在“类型注解”中,所以即使在 Python 3.10 也必须使用 `Union`。 + +如果是在类型注解中,我们就可以使用竖线: + +```Python +some_variable: PlaneItem | CarItem +``` + +但如果把它写成赋值 `response_model=PlaneItem | CarItem`,就会报错,因为 Python 会尝试在 `PlaneItem` 和 `CarItem` 之间执行一个“无效的运算”,而不是把它当作类型注解来解析。 + +## 模型列表 { #list-of-models } + +同样地,你可以声明由对象列表构成的响应。 + +为此,请使用标准的 Python `typing.List`(在 Python 3.9+ 中也可以直接用 `list`): {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} -## 任意 `dict` 构成的响应 +## 任意 `dict` 的响应 { #response-with-arbitrary-dict } -任意的 `dict` 都能用于声明响应,只要声明键和值的类型,无需使用 Pydantic 模型。 +你也可以使用普通的任意 `dict` 来声明响应,只需声明键和值的类型,无需使用 Pydantic 模型。 -事先不知道可用的字段 / 属性名时(Pydantic 模型必须知道字段是什么),这种方式特别有用。 +如果你事先不知道有效的字段/属性名(Pydantic 模型需要预先知道字段)时,这很有用。 -此时,可以使用 `typing.Dict`: +此时,可以使用 `typing.Dict`(在 Python 3.9+ 中也可以直接用 `dict`): {* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} -## 小结 +## 小结 { #recap } -针对不同场景,可以随意使用不同的 Pydantic 模型继承定义的基类。 +针对不同场景,可以随意使用不同的 Pydantic 模型并通过继承复用。 -实体必须具有不同的**状态**时,不必为不同状态的实体单独定义数据模型。例如,用户**实体**就有包含 `password`、包含 `password_hash` 以及不含密码等多种状态。 +当一个实体需要具备不同的“状态”时,无需只为该实体定义一个数据模型。例如,用户“实体”就可能有包含 `password`、包含 `password_hash` 以及不含密码等多种状态。 diff --git a/docs/zh/docs/tutorial/first-steps.md b/docs/zh/docs/tutorial/first-steps.md index 2d7c35c8c..5d01884b8 100644 --- a/docs/zh/docs/tutorial/first-steps.md +++ b/docs/zh/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 第一步 +# 第一步 { #first-steps } 最简单的 FastAPI 文件可能像下面这样: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} 将其复制到 `main.py` 文件中。 @@ -56,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 该行显示了你的应用在本机所提供服务的 URL 地址。 -### 查看 +### 查看 { #check-it } 打开浏览器访问 http://127.0.0.1:8000。 @@ -66,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 交互式 API 文档 +### 交互式 API 文档 { #interactive-api-docs } 跳转到 http://127.0.0.1:8000/docs。 @@ -74,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![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。 @@ -82,35 +82,35 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } **FastAPI** 使用定义 API 的 **OpenAPI** 标准将你的所有 API 转换成「模式」。 -#### 「模式」 +#### 「模式」 { #schema } 「模式」是对事物的一种定义或描述。它并非具体的实现代码,而只是抽象的描述。 -#### API「模式」 +#### API「模式」 { #api-schema } 在这种场景下,OpenAPI 是一种规定如何定义 API 模式的规范。 「模式」的定义包括你的 API 路径,以及它们可能使用的参数等等。 -#### 数据「模式」 +#### 数据「模式」 { #data-schema } 「模式」这个术语也可能指的是某些数据比如 JSON 的结构。 在这种情况下,它可以表示 JSON 的属性及其具有的数据类型,等等。 -#### OpenAPI 和 JSON Schema +#### OpenAPI 和 JSON Schema { #openapi-and-json-schema } OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送和接收的数据的定义(或称为「模式」),这些定义通过 JSON 数据模式标准 **JSON Schema** 所生成。 -#### 查看 `openapi.json` +#### 查看 `openapi.json` { #check-the-openapi-json } 如果你对原始的 OpenAPI 模式长什么样子感到好奇,FastAPI 自动生成了包含所有 API 描述的 JSON(模式)。 -你可以直接在:http://127.0.0.1:8000/openapi.json 看到它。 +你可以直接在:http://127.0.0.1:800api.json 看到它。 它将显示以如下内容开头的 JSON: @@ -135,7 +135,7 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送 ... ``` -#### OpenAPI 的用途 +#### OpenAPI 的用途 { #what-is-openapi-for } 驱动 FastAPI 内置的 2 个交互式文档系统的正是 OpenAPI 模式。 @@ -143,11 +143,47 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送 你还可以使用它自动生成与你的 API 进行通信的客户端代码。例如 web 前端,移动端或物联网嵌入程序。 -## 分步概括 +### 部署你的应用(可选) { #deploy-your-app-optional } -### 步骤 1:导入 `FastAPI` +你可以选择将 FastAPI 应用部署到 FastAPI Cloud,如果还没有,先去加入候补名单。🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +如果你已经拥有 **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_py39.py hl[1] *} `FastAPI` 是一个为你的 API 提供了所有功能的 Python 类。 @@ -159,17 +195,17 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送 /// -### 步骤 2:创建一个 `FastAPI`「实例」 +### 步骤 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` 类的一个「实例」。 这个实例将是创建你所有 API 的主要交互对象。 -### 步骤 3:创建一个*路径操作* +### 步骤 3:创建一个*路径操作* { #step-3-create-a-path-operation } -#### 路径 +#### 路径 { #path } 这里的「路径」指的是 URL 中从第一个 `/` 起的后半部分。 @@ -193,7 +229,7 @@ https://example.com/items/foo 开发 API 时,「路径」是用来分离「关注点」和「资源」的主要手段。 -#### 操作 +#### 操作 { #operation } 这里的「操作」指的是一种 HTTP「方法」。 @@ -228,9 +264,9 @@ https://example.com/items/foo 我们也打算称呼它们为「操作」。 -#### 定义一个*路径操作装饰器* +#### 定义一个*路径操作装饰器* { #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** 在它下方的函数负责处理如下访问请求: @@ -276,7 +312,7 @@ https://example.com/items/foo /// -### 步骤 4:定义**路径操作函数** +### 步骤 4:定义**路径操作函数** { #step-4-define-the-path-operation-function } 这是我们的「**路径操作函数**」: @@ -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,17 +332,17 @@ 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 -如果你不知道两者的区别,请查阅 [并发: *赶时间吗?*](../async.md#_1){.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` 一样的单个值,等等。 @@ -314,10 +350,31 @@ 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 是 *FastAPI 及其朋友们* 开源项目的主要赞助和资金提供方。✨ + +#### 部署到其他云服务商 { #deploy-to-other-cloud-providers } + +FastAPI 是开源并基于标准的。你可以将 FastAPI 应用部署到你选择的任何云服务商。 + +按照你的云服务商的指南部署 FastAPI 应用即可。🤓 + +## 总结 { #recap } * 导入 `FastAPI`。 * 创建一个 `app` 实例。 * 编写一个**路径操作装饰器**,如 `@app.get("/")`。 * 定义一个**路径操作函数**,如 `def root(): ...`。 * 使用命令 `fastapi dev` 运行开发服务器。 +* 可选:使用 `fastapi deploy` 部署你的应用。 diff --git a/docs/zh/docs/tutorial/handling-errors.md b/docs/zh/docs/tutorial/handling-errors.md index ae667b74a..986a84772 100644 --- a/docs/zh/docs/tutorial/handling-errors.md +++ b/docs/zh/docs/tutorial/handling-errors.md @@ -1,64 +1,62 @@ -# 处理错误 +# 处理错误 { #handling-errors } -某些情况下,需要向客户端返回错误提示。 +某些情况下,需要向使用你的 API 的客户端返回错误提示。 -这里所谓的客户端包括前端浏览器、其他应用程序、物联网设备等。 +这里所谓的客户端包括前端浏览器、他人的代码、物联网设备等。 -需要向客户端返回错误提示的场景主要如下: +你可能需要告诉客户端: -- 客户端没有执行操作的权限 -- 客户端没有访问资源的权限 +- 客户端没有执行该操作的权限 +- 客户端没有访问该资源的权限 - 客户端要访问的项目不存在 -- 等等 ... +- 等等 遇到这些情况时,通常要返回 **4XX**(400 至 499)**HTTP 状态码**。 -**4XX** 状态码与表示请求成功的 **2XX**(200 至 299) HTTP 状态码类似。 +这与表示请求成功的 **2XX**(200 至 299)HTTP 状态码类似。那些“200”状态码表示某种程度上的“成功”。 -只不过,**4XX** 状态码表示客户端发生的错误。 +而 **4XX** 状态码表示客户端发生了错误。 大家都知道**「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` 是额外包含了和 API 有关数据的常规 Python 异常。 因为是 Python 异常,所以不能 `return`,只能 `raise`。 -如在调用*路径操作函数*里的工具函数时,触发了 `HTTPException`,FastAPI 就不再继续执行*路径操作函数*中的后续代码,而是立即终止请求,并把 `HTTPException` 的 HTTP 错误发送至客户端。 +这也意味着,如果你在*路径操作函数*里调用的某个工具函数内部触发了 `HTTPException`,那么*路径操作函数*中后续的代码将不会继续执行,请求会立刻终止,并把 `HTTPException` 的 HTTP 错误发送给客户端。 -在介绍依赖项与安全的章节中,您可以了解更多用 `raise` 异常代替 `return` 值的优势。 +在介绍依赖项与安全的章节中,你可以更直观地看到用 `raise` 异常代替 `return` 值的优势。 -本例中,客户端用 `ID` 请求的 `item` 不存在时,触发状态码为 `404` 的异常: +本例中,客户端用不存在的 `ID` 请求 `item` 时,触发状态码为 `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」`)时,客户端会接收到 HTTP 状态码 - 200 及如下 JSON 响应结果: +请求为 `http://example.com/items/foo`(`item_id` 为 `"foo"`)时,客户端会接收到 HTTP 状态码 200 及如下 JSON 响应结果: ```JSON { "item": "The Foo Wrestlers" } - ``` -但如果客户端请求 `http://example.com/items/bar`(`item_id` `「bar」` 不存在时),则会接收到 HTTP 状态码 - 404(「未找到」错误)及如下 JSON 响应结果: +但如果客户端请求 `http://example.com/items/bar`(不存在的 `item_id` `"bar"`),则会接收到 HTTP 状态码 404(“未找到”错误)及如下 JSON 响应结果: ```JSON { "detail": "Item not found" } - ``` /// tip | 提示 @@ -71,68 +69,67 @@ /// -## 添加自定义响应头 +## 添加自定义响应头 { #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 的异常工具](https://www.starlette.dev/exceptions/)。 +可以使用与 Starlette 相同的异常处理工具添加自定义异常处理器。 -假设要触发的自定义异常叫作 `UnicornException`。 +假设有一个自定义异常 `UnicornException`(你自己或你使用的库可能会 `raise` 它)。 -且需要 FastAPI 实现全局处理该异常。 +并且你希望用 FastAPI 在全局处理该异常。 -此时,可以用 `@app.exception_handler()` 添加自定义异常控制器: +此时,可以用 `@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` 时,路径操作会触发 `UnicornException`。 但该异常将会被 `unicorn_exception_handler` 处理。 -接收到的错误信息清晰明了,HTTP 状态码为 `418`,JSON 内容如下: +你会收到清晰的错误信息,HTTP 状态码为 `418`,JSON 内容如下: ```JSON {"message": "Oops! yolo did something. There goes a rainbow..."} - ``` /// note | 技术细节 -`from starlette.requests import Request` 和 `from starlette.responses import JSONResponse` 也可以用于导入 `Request` 和 `JSONResponse`。 +也可以使用 `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** 自带了一些默认异常处理器。 -触发 `HTTPException` 或请求无效数据时,这些处理器返回默认的 JSON 响应结果。 +当你触发 `HTTPException`,或者请求中包含无效数据时,这些处理器负责返回默认的 JSON 响应。 -不过,也可以使用自定义处理器覆盖默认异常处理器。 +你也可以用自己的处理器覆盖它们。 -### 覆盖请求验证异常 +### 覆盖请求验证异常 { #override-request-validation-exceptions } 请求中包含无效数据时,**FastAPI** 内部会触发 `RequestValidationError`。 -该异常也内置了默认异常处理器。 +它也内置了该异常的默认处理器。 -覆盖默认异常处理器时需要导入 `RequestValidationError`,并用 `@app.excption_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 { @@ -147,59 +144,46 @@ } ] } - ``` -被替换为了以下文本格式的错误信息: +将得到如下文本内容: ``` -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` +### 覆盖 `HTTPException` 错误处理器 { #override-the-httpexception-error-handler } -/// warning | 警告 +同理,也可以覆盖 `HTTPException` 的处理器。 -如果您觉得现在还用不到以下技术细节,可以先跳过下面的内容。 +例如,只为这些错误返回纯文本响应,而不是 JSON: -/// - -`RequestValidationError` 是 Pydantic 的 `ValidationError` 的子类。 - -**FastAPI** 调用的就是 `RequestValidationError` 类,因此,如果在 `response_model` 中使用 Pydantic 模型,且数据有错误时,在日志中就会看到这个错误。 - -但客户端或用户看不到这个错误。反之,客户端接收到的是 HTTP 状态码为 `500` 的「内部服务器错误」。 - -这是因为在*响应*或代码(不是在客户端的请求里)中出现的 Pydantic `ValidationError` 是代码的 bug。 - -修复错误时,客户端或用户不能访问错误的内部信息,否则会造成安全隐患。 - -### 覆盖 `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` 包含其接收到的无效数据请求的 `body` 。 +请注意,`RequestValidationError` 包含发生验证错误的文件名和行号信息,你可以在需要时将其记录到日志中以提供相关信息。 -开发时,可以用这个请求体生成日志、调试错误,并返回给用户。 +但这也意味着,如果你只是将其直接转换为字符串并返回,可能会泄露一些关于系统的细节信息。因此,这里的代码会提取并分别显示每个错误。 -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +/// + +### 使用 `RequestValidationError` 的请求体 { #use-the-requestvalidationerror-body } + +`RequestValidationError` 包含其接收到的带有无效数据的请求体 `body`。 + +开发时,你可以用它来记录请求体、调试错误,或返回给用户等。 + +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} 现在试着发送一个无效的 `item`,例如: @@ -208,10 +192,9 @@ path -> item_id "title": "towel", "size": "XL" } - ``` -收到的响应包含 `body` 信息,并说明数据是无效的: +收到的响应会告诉你数据无效,并包含收到的请求体: ```JSON hl_lines="12-15" { @@ -230,40 +213,32 @@ path -> item_id "size": "XL" } } - ``` -### FastAPI `HTTPException` vs Starlette `HTTPException` +#### FastAPI 的 `HTTPException` vs Starlette 的 `HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI** 也提供了自有的 `HTTPException`。 -**FastAPI** 的 `HTTPException` 继承自 Starlette 的 `HTTPException` 错误类。 +**FastAPI** 的 `HTTPException` 错误类继承自 Starlette 的 `HTTPException` 错误类。 -它们之间的唯一区别是,**FastAPI** 的 `HTTPException` 可以在响应中添加响应头。 +它们之间的唯一区别是,**FastAPI** 的 `HTTPException` 在 `detail` 字段中接受任意可转换为 JSON 的数据,而 Starlette 的 `HTTPException` 只接受字符串。 -OAuth 2.0 等安全工具需要在内部调用这些响应头。 - -因此你可以继续像平常一样在代码中触发 **FastAPI** 的 `HTTPException` 。 +因此,你可以继续像平常一样在代码中触发 **FastAPI** 的 `HTTPException`。 但注册异常处理器时,应该注册到来自 Starlette 的 `HTTPException`。 -这样做是为了,当 Starlette 的内部代码、扩展或插件触发 Starlette `HTTPException` 时,处理程序能够捕获、并处理此异常。 +这样做是为了,当 Starlette 的内部代码、扩展或插件触发 Starlette `HTTPException` 时,你的处理器能够捕获并处理它。 -注意,本例代码中同时使用了这两个 `HTTPException`,此时,要把 Starlette 的 `HTTPException` 命名为 `StarletteHTTPException`: +本例中,为了在同一份代码中同时使用两个 `HTTPException`,将 Starlette 的异常重命名为 `StarletteHTTPException`: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException - ``` -### 复用 **FastAPI** 异常处理器 +### 复用 **FastAPI** 的异常处理器 { #reuse-fastapis-exception-handlers } -FastAPI 支持先对异常进行某些处理,然后再使用 **FastAPI** 中处理该异常的默认异常处理器。 +如果你想在自定义处理后仍复用 **FastAPI** 的默认异常处理器,可以从 `fastapi.exception_handlers` 导入并复用这些默认处理器: -从 `fastapi.exception_handlers` 中导入要复用的默认异常处理器: +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} - -虽然,本例只是输出了夸大其词的错误信息。 - -但也足以说明,可以在处理异常之后再复用默认的异常处理器。 +虽然本例只是用非常夸张的信息打印了错误,但足以说明:你可以先处理异常,然后再复用默认的异常处理器。 diff --git a/docs/zh/docs/tutorial/header-param-models.md b/docs/zh/docs/tutorial/header-param-models.md index 13366aebc..e7d548317 100644 --- a/docs/zh/docs/tutorial/header-param-models.md +++ b/docs/zh/docs/tutorial/header-param-models.md @@ -1,16 +1,16 @@ -# Header 参数模型 +# Header 参数模型 { #header-parameter-models } 如果您有一组相关的 **header 参数**,您可以创建一个 **Pydantic 模型**来声明它们。 这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证和元数据。😎 -/// note +/// note | 注意 自 FastAPI 版本 `0.115.0` 起支持此功能。🤓 /// -## 使用 Pydantic 模型的 Header 参数 +## 使用 Pydantic 模型的 Header 参数 { #header-parameters-with-a-pydantic-model } 在 **Pydantic 模型**中声明所需的 **header 参数**,然后将参数声明为 `Header` : @@ -18,7 +18,7 @@ **FastAPI** 将从请求中接收到的 **headers** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。 -## 查看文档 +## 查看文档 { #check-the-docs } 您可以在文档 UI 的 `/docs` 中查看所需的 headers: @@ -26,7 +26,7 @@ -## 禁止额外的 Headers +## 禁止额外的 Headers { #forbid-extra-headers } 在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 headers。 @@ -51,6 +51,22 @@ } ``` -## 总结 +## 禁用下划线转换 { #disable-convert-underscores } + +与常规的 header 参数相同,当参数名中包含下划线时,会**自动转换为连字符**。 + +例如,如果你的代码中有一个名为 `save_data` 的 header 参数,那么预期的 HTTP 头将是 `save-data`,并且在文档中也会以这种形式显示。 + +如果由于某些原因你需要禁用这种自动转换,你也可以在用于 header 参数的 Pydantic 模型中进行设置。 + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | 警告 + +在将 `convert_underscores` 设为 `False` 之前,请注意某些 HTTP 代理和服务器不允许使用带下划线的 headers。 + +/// + +## 总结 { #summary } 您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 **headers**。😎 diff --git a/docs/zh/docs/tutorial/header-params.md b/docs/zh/docs/tutorial/header-params.md index 19bb455cf..ccb88ae7f 100644 --- a/docs/zh/docs/tutorial/header-params.md +++ b/docs/zh/docs/tutorial/header-params.md @@ -1,14 +1,14 @@ -# Header 参数 +# Header 参数 { #header-parameters } 定义 `Header` 参数的方式与定义 `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` 一样的结构定义 header 参数。 @@ -24,13 +24,13 @@ /// -/// info | 说明 +/// info | 信息 必须使用 `Header` 声明 header 参数,否则该参数会被解释为查询参数。 /// -## 自动转换 +## 自动转换 { #automatic-conversion } `Header` 比 `Path`、`Query` 和 `Cookie` 提供了更多功能。 @@ -54,7 +54,7 @@ /// -## 重复的请求头 +## 重复的请求头 { #duplicate-headers } 有时,可能需要接收重复的请求头。即同一个请求头有多个值。 @@ -84,7 +84,7 @@ X-Token: bar } ``` -## 小结 +## 小结 { #recap } 使用 `Header` 声明请求头的方式与 `Query`、`Path` 、`Cookie` 相同。 diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md index 3ca927337..793458302 100644 --- a/docs/zh/docs/tutorial/index.md +++ b/docs/zh/docs/tutorial/index.md @@ -1,12 +1,12 @@ -# 教程 - 用户指南 +# 教程 - 用户指南 { #tutorial-user-guide } 本教程将一步步向您展示如何使用 **FastAPI** 的绝大部分特性。 -各个章节的内容循序渐进,但是又围绕着单独的主题,所以您可以直接跳转到某个章节以解决您的特定需求。 +各个章节的内容循序渐进,但是又围绕着单独的主题,所以您可以直接跳转到某个章节以解决您的特定 API 需求。 本教程同样可以作为将来的参考手册,所以您可以随时回到本教程并查阅您需要的内容。 -## 运行代码 +## 运行代码 { #run-the-code } 所有代码片段都可以复制后直接使用(它们实际上是经过测试的 Python 文件)。 @@ -58,7 +58,7 @@ $ fastapi dev -/// note +/// note | 注意 -当您使用 `pip install "fastapi[standard]"` 进行安装时,它会附带一些默认的可选标准依赖项。 +当您使用 `pip install "fastapi[standard]"` 安装时,它会附带一些默认的可选标准依赖项,其中包括 `fastapi-cloud-cli`,它可以让您部署到 FastAPI Cloud。 如果您不想安装这些可选依赖,可以选择安装 `pip install fastapi`。 +如果您想安装标准依赖但不包含 `fastapi-cloud-cli`,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"` 安装。 + /// -## 进阶用户指南 +## 进阶用户指南 { #advanced-user-guide } 在本**教程-用户指南**之后,您可以阅读**进阶用户指南**。 diff --git a/docs/zh/docs/tutorial/metadata.md b/docs/zh/docs/tutorial/metadata.md index d29a1e6d0..6ec7e9add 100644 --- a/docs/zh/docs/tutorial/metadata.md +++ b/docs/zh/docs/tutorial/metadata.md @@ -1,28 +1,28 @@ -# 元数据和文档 URL +# 元数据和文档 URL { #metadata-and-docs-urls } 你可以在 FastAPI 应用程序中自定义多个元数据配置。 -## API 元数据 +## API 元数据 { #metadata-for-api } 你可以在设置 OpenAPI 规范和自动 API 文档 UI 中使用的以下字段: | 参数 | 类型 | 描述 | |------------|------|-------------| | `title` | `str` | API 的标题。 | -| `summary` | `str` | API 的简短摘要。 自 OpenAPI 3.1.0、FastAPI 0.99.0 起可用。. | -| `description` | `str` | API 的简短描述。可以使用Markdown。 | -| `version` | `string` | API 的版本。这是您自己的应用程序的版本,而不是 OpenAPI 的版本。例如 `2.5.0` 。 | +| `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` | API 服务条款的 URL。如果提供,则必须是 URL。 | -| `contact` | `dict` | 公开的 API 的联系信息。它可以包含多个字段。
contact 字段
参数Type描述
namestr联系人/组织的识别名称。
urlstr指向联系信息的 URL。必须采用 URL 格式。
emailstr联系人/组织的电子邮件地址。必须采用电子邮件地址的格式。
| -| `license_info` | `dict` | 公开的 API 的许可证信息。它可以包含多个字段。
license_info 字段
参数类型描述
namestr必须的 (如果设置了license_info). 用于 API 的许可证名称。
identifierstr一个API的SPDX许可证表达。 The identifier field is mutually exclusive of the url field. 自 OpenAPI 3.1.0、FastAPI 0.99.0 起可用。
urlstr用于 API 的许可证的 URL。必须采用 URL 格式。
| +| `contact` | `dict` | 公开的 API 的联系信息。它可以包含多个字段。
contact 字段
参数类型描述
namestr联系人/组织的识别名称。
urlstr指向联系信息的 URL。必须采用 URL 格式。
emailstr联系人/组织的电子邮件地址。必须采用电子邮件地址的格式。
| +| `license_info` | `dict` | 公开的 API 的许可证信息。它可以包含多个字段。
license_info 字段
参数类型描述
namestr必须(如果设置了 license_info)。用于 API 的许可证名称。
identifierstrAPI 的 SPDX 许可证表达式。字段 identifier 与字段 url 互斥。自 OpenAPI 3.1.0、FastAPI 0.99.0 起可用。
urlstr用于 API 的许可证的 URL。必须采用 URL 格式。
| 你可以按如下方式设置它们: -{* ../../docs_src/metadata/tutorial001.py hl[4:6] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -/// tip +/// tip | 提示 -您可以在 `description` 字段中编写 Markdown,它将在输出中呈现。 +你可以在 `description` 字段中编写 Markdown,它会在输出中渲染。 /// @@ -30,15 +30,35 @@ -## 标签元数据 +## 许可证标识符 { #license-identifier } -### 创建标签元数据 +自 OpenAPI 3.1.0 和 FastAPI 0.99.0 起,你还可以在 `license_info` 中使用 `identifier` 而不是 `url`。 + +例如: + +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} + +## 标签元数据 { #metadata-for-tags } + +你也可以通过参数 `openapi_tags` 为用于分组路径操作的不同标签添加额外的元数据。 + +它接收一个列表,列表中每个标签对应一个字典。 + +每个字典可以包含: + +- `name`(必填):一个 `str`,与在你的*路径操作*和 `APIRouter` 的 `tags` 参数中使用的标签名相同。 +- `description`:一个 `str`,该标签的简短描述。可以使用 Markdown,并会显示在文档 UI 中。 +- `externalDocs`:一个 `dict`,描述外部文档,包含: + - `description`:一个 `str`,该外部文档的简短描述。 + - `url`(必填):一个 `str`,该外部文档的 URL。 + +### 创建标签元数据 { #create-metadata-for-tags } 让我们在带有标签的示例中为 `users` 和 `items` 试一下。 创建标签元数据并把它传递给 `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_)。 @@ -48,11 +68,11 @@ /// -### 使用你的标签 +### 使用你的标签 { #use-your-tags } 将 `tags` 参数和*路径操作*(以及 `APIRouter`)一起使用,将其分配给不同的标签: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | 信息 @@ -60,19 +80,19 @@ /// -### 查看文档 +### 查看文档 { #check-the-docs } 如果你现在查看文档,它们会显示所有附加的元数据: -### 标签顺序 +### 标签顺序 { #order-of-tags } 每个标签元数据字典的顺序也定义了在文档用户界面显示的顺序。 例如按照字母顺序,即使 `users` 排在 `items` 之后,它也会显示在前面,因为我们将它的元数据添加为列表内的第一个字典。 -## OpenAPI URL +## OpenAPI URL { #openapi-url } 默认情况下,OpenAPI 模式服务于 `/openapi.json`。 @@ -80,21 +100,21 @@ 例如,将其设置为服务于 `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} 如果你想完全禁用 OpenAPI 模式,可以将其设置为 `openapi_url=None`,这样也会禁用使用它的文档用户界面。 -## 文档 URLs +## 文档 URLs { #docs-urls } 你可以配置两个文档用户界面,包括: -* **Swagger UI**:服务于 `/docs`。 - * 可以使用参数 `docs_url` 设置它的 URL。 - * 可以通过设置 `docs_url=None` 禁用它。 -* ReDoc:服务于 `/redoc`。 - * 可以使用参数 `redoc_url` 设置它的 URL。 - * 可以通过设置 `redoc_url=None` 禁用它。 +- **Swagger UI**:服务于 `/docs`。 + - 可以使用参数 `docs_url` 设置它的 URL。 + - 可以通过设置 `docs_url=None` 禁用它。 +- **ReDoc**:服务于 `/redoc`。 + - 可以使用参数 `redoc_url` 设置它的 URL。 + - 可以通过设置 `redoc_url=None` 禁用它。 例如,设置 Swagger UI 服务于 `/documentation` 并禁用 ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/zh/docs/tutorial/middleware.md b/docs/zh/docs/tutorial/middleware.md index 5608c4ee1..a3f833d78 100644 --- a/docs/zh/docs/tutorial/middleware.md +++ b/docs/zh/docs/tutorial/middleware.md @@ -1,66 +1,95 @@ -# 中间件 +# 中间件 { #middleware } -你可以向 **FastAPI** 应用添加中间件. +你可以向 **FastAPI** 应用添加中间件。 -"中间件"是一个函数,它在每个**请求**被特定的*路径操作*处理之前,以及在每个**响应**返回之前工作. +“中间件”是一个函数,它会在每个特定的*路径操作*处理每个**请求**之前运行,也会在返回每个**响应**之前运行。 -* 它接收你的应用程序的每一个**请求**. -* 然后它可以对这个**请求**做一些事情或者执行任何需要的代码. -* 然后它将**请求**传递给应用程序的其他部分 (通过某种*路径操作*). -* 然后它获取应用程序生产的**响应** (通过某种*路径操作*). -* 它可以对该**响应**做些什么或者执行任何需要的代码. -* 然后它返回这个 **响应**. +* 它接收你的应用的每一个**请求**。 +* 然后它可以对这个**请求**做一些事情或者执行任何需要的代码。 +* 然后它将这个**请求**传递给应用程序的其他部分(某个*路径操作*)处理。 +* 之后它获取应用程序生成的**响应**(由某个*路径操作*产生)。 +* 它可以对该**响应**做一些事情或者执行任何需要的代码。 +* 然后它返回这个**响应**。 /// note | 技术细节 -如果你使用了 `yield` 关键字依赖, 依赖中的退出代码将在执行中间件*后*执行. +如果你有使用 `yield` 的依赖,依赖中的退出代码会在中间件之后运行。 -如果有任何后台任务(稍后记录), 它们将在执行中间件*后*运行. +如果有任何后台任务(会在[后台任务](background-tasks.md){.internal-link target=_blank}一节中介绍,你稍后会看到),它们会在所有中间件之后运行。 /// -## 创建中间件 +## 创建中间件 { #create-a-middleware } -要创建中间件你可以在函数的顶部使用装饰器 `@app.middleware("http")`. +要创建中间件,你可以在函数的顶部使用装饰器 `@app.middleware("http")`。 -中间件参数接收如下参数: +中间件函数会接收: -* `request`. -* 一个函数 `call_next` 它将接收 `request` 作为参数. - * 这个函数将 `request` 传递给相应的 *路径操作*. - * 然后它将返回由相应的*路径操作*生成的 `response`. -* 然后你可以在返回 `response` 前进一步修改它. +* `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 (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) 的 `expose_headers` 参数中,在 Starlette's CORS docs文档中. +但是如果你有希望让浏览器中的客户端可见的自定义请求头,你需要把它们加到你的 CORS 配置([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})的 `expose_headers` 参数中,参见 Starlette 的 CORS 文档。 /// /// note | 技术细节 -你也可以使用 `from starlette.requests import Request`. +你也可以使用 `from starlette.requests import Request`。 -**FastAPI** 为了开发者方便提供了该对象. 但其实它直接来自于 Starlette. +**FastAPI** 为了开发者方便提供了该对象,但它直接来自 Starlette。 /// -### 在 `response` 的前和后 +### 在 `response` 之前与之后 { #before-and-after-the-response } -在任何*路径操作*收到`request`前,可以添加要和请求一起运行的代码. +你可以在任何*路径操作*接收 `request` 之前,添加要与该 `request` 一起运行的代码。 -也可以在*响应*生成但是返回之前添加代码. +也可以在生成 `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 User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}阅读更多关于中间件的教程. +这里我们使用 `time.perf_counter()` 而不是 `time.time()`,因为在这类场景中它可能更精确。🤓 -你将在下一节中学习如何使用中间件处理 CORS . +/// + +## 多个中间件的执行顺序 { #multiple-middleware-execution-order } + +当你使用 `@app.middleware()` 装饰器或 `app.add_middleware()` 方法添加多个中间件时,每个新中间件都会包裹应用,形成一个栈。最后添加的中间件是“最外层”的,最先添加的是“最内层”的。 + +在请求路径上,最外层的中间件先运行。 + +在响应路径上,它最后运行。 + +例如: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +这会产生如下执行顺序: + +* 请求:MiddlewareB → MiddlewareA → 路由 + +* 响应:路由 → MiddlewareA → MiddlewareB + +这种栈式行为确保中间件按可预测且可控的顺序执行。 + +## 其他中间件 { #other-middlewares } + +你可以稍后在[高级用户指南:高级中间件](../advanced/middleware.md){.internal-link target=_blank}中阅读更多关于其他中间件的内容。 + +你将在下一节中了解如何使用中间件处理 CORS。 diff --git a/docs/zh/docs/tutorial/path-operation-configuration.md b/docs/zh/docs/tutorial/path-operation-configuration.md index adeca2d3f..49b7baabc 100644 --- a/docs/zh/docs/tutorial/path-operation-configuration.md +++ b/docs/zh/docs/tutorial/path-operation-configuration.md @@ -1,66 +1,76 @@ -# 路径操作配置 +# 路径操作配置 { #path-operation-configuration } *路径操作装饰器*支持多种配置参数。 /// warning | 警告 -注意:以下参数应直接传递给**路径操作装饰器**,不能传递给*路径操作函数*。 +注意:以下参数应直接传递给*路径操作装饰器*,不能传递给*路径操作函数*。 /// -## `status_code` 状态码 +## 响应状态码 { #response-status-code } -`status_code` 用于定义*路径操作*响应中的 HTTP 状态码。 +可以在*路径操作*的响应中定义(HTTP)`status_code`。 -可以直接传递 `int` 代码, 比如 `404`。 +可以直接传递 `int` 代码,比如 `404`。 -如果记不住数字码的涵义,也可以用 `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 | 技术细节 也可以使用 `from starlette import status` 导入状态码。 -**FastAPI** 的`fastapi.status` 和 `starlette.status` 一样,只是快捷方式。实际上,`fastapi.status` 直接继承自 Starlette。 +**FastAPI** 提供的 `fastapi.status` 与 `starlette.status` 相同,方便你作为开发者使用。实际上它直接来自 Starlette。 /// -## `tags` 参数 +## 标签 { #tags } -`tags` 参数的值是由 `str` 组成的 `list` (一般只有一个 `str` ),`tags` 用于为*路径操作*添加标签: +可以通过传入由 `str` 组成的 `list`(通常只有一个 `str`)的参数 `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 概图会自动添加标签,供 API 文档接口使用: -## `summary` 和 `description` 参数 +### 使用 Enum 的标签 { #tags-with-enums } -路径装饰器还支持 `summary` 和 `description` 这两个参数: +如果你的应用很大,可能会积累出很多标签,你会希望确保相关的*路径操作*始终使用相同的标签。 -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +这种情况下,把标签存放在 `Enum` 中会更合适。 -## 文档字符串(`docstring`) +**FastAPI** 对此的支持与使用普通字符串相同: -描述内容比较长且占用多行时,可以在函数的 docstring 中声明*路径操作*的描述,**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_py310.py hl[17:18] *} + +## 从 docstring 获取描述 { #description-from-docstring } + +描述内容比较长且占用多行时,可以在函数的 docstring 中声明*路径操作*的描述,**FastAPI** 会从中读取。 文档字符串支持 Markdown,能正确解析和显示 Markdown 的内容,但要注意文档字符串的缩进。 -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} 下图为 Markdown 文本在 API 文档中的显示效果: -## 响应描述 +## 响应描述 { #response-description } `response_description` 参数用于定义响应的描述说明: -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | 说明 @@ -78,11 +88,11 @@ OpenAPI 规定每个*路径操作*都要有响应描述。 -## 弃用*路径操作* +## 弃用*路径操作* { #deprecate-a-path-operation } `deprecated` 参数可以把*路径操作*标记为弃用,无需直接删除: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} API 文档会把该路径操作标记为弃用: @@ -92,6 +102,6 @@ API 文档会把该路径操作标记为弃用: -## 小结 +## 小结 { #recap } -通过传递参数给*路径操作装饰器* ,即可轻松地配置*路径操作*、添加元数据。 +通过传递参数给*路径操作装饰器*,即可轻松地配置*路径操作*、添加元数据。 diff --git a/docs/zh/docs/tutorial/path-params-numeric-validations.md b/docs/zh/docs/tutorial/path-params-numeric-validations.md index ff6242835..ff8b1762c 100644 --- a/docs/zh/docs/tutorial/path-params-numeric-validations.md +++ b/docs/zh/docs/tutorial/path-params-numeric-validations.md @@ -1,91 +1,128 @@ -# 路径参数和数值校验 +# 路径参数和数值校验 { #path-parameters-and-numeric-validations } 与使用 `Query` 为查询参数声明更多的校验和元数据的方式相同,你也可以使用 `Path` 为路径参数声明相同类型的校验和元数据。 -## 导入 Path +## 导入 `Path` { #import-path } -首先,从 `fastapi` 导入 `Path`: +首先,从 `fastapi` 导入 `Path`,并导入 `Annotated`: {* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## 声明元数据 +/// info | 信息 -你可以声明与 `Query` 相同的所有参数。 +FastAPI 在 0.95.0 版本添加了对 `Annotated` 的支持(并开始推荐使用它)。 -例如,要声明路径参数 `item_id`的 `title` 元数据值,你可以输入: +如果你使用的是更旧的版本,尝试使用 `Annotated` 会报错。 -{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} - -/// note - -路径参数总是必需的,因为它必须是路径的一部分。 - -所以,你应该在声明时使用 `...` 将其标记为必需参数。 - -然而,即使你使用 `None` 声明路径参数或设置一个其他默认值也不会有任何影响,它依然会是必需参数。 +请确保在使用 `Annotated` 之前,将 FastAPI 版本[升级](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}到至少 0.95.1。 /// -## 按需对参数排序 +## 声明元数据 { #declare-metadata } -假设你想要声明一个必需的 `str` 类型查询参数 `q`。 +你可以声明与 `Query` 相同的所有参数。 -而且你不需要为该参数声明任何其他内容,所以实际上你并不需要使用 `Query`。 +例如,要为路径参数 `item_id` 声明 `title` 元数据值,你可以这样写: -但是你仍然需要使用 `Path` 来声明路径参数 `item_id`。 +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} -如果你将带有「默认值」的参数放在没有「默认值」的参数之前,Python 将会报错。 +/// note | 注意 -但是你可以对其重新排序,并将不带默认值的值(查询参数 `q`)放到最前面。 +路径参数总是必需的,因为它必须是路径的一部分。即使你将其声明为 `None` 或设置了默认值,也不会产生任何影响,它依然始终是必需参数。 -对 **FastAPI** 来说这无关紧要。它将通过参数的名称、类型和默认值声明(`Query`、`Path` 等)来检测参数,而不在乎参数的顺序。 +/// + +## 按需对参数排序 { #order-the-parameters-as-you-need } + +/// tip | 提示 + +如果你使用 `Annotated`,这点可能不那么重要或必要。 + +/// + +假设你想要将查询参数 `q` 声明为必需的 `str`。 + +并且你不需要为该参数声明其他内容,所以实际上不需要用到 `Query`。 + +但是你仍然需要为路径参数 `item_id` 使用 `Path`。并且出于某些原因你不想使用 `Annotated`。 + +如果你将带有“默认值”的参数放在没有“默认值”的参数之前,Python 会报错。 + +不过你可以重新排序,让没有默认值的参数(查询参数 `q`)放在最前面。 + +对 **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`,并使它们的顺序与上面不同,Python 对此有一些特殊的语法。 +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -传递 `*` 作为函数的第一个参数。 +## 按需对参数排序的技巧 { #order-the-parameters-as-you-need-tricks } -Python 不会对该 `*` 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值。 +/// tip | 提示 -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +如果你使用 `Annotated`,这点可能不那么重要或必要。 -## 数值校验:大于等于 +/// -使用 `Query` 和 `Path`(以及你将在后面看到的其他类)可以声明字符串约束,但也可以声明数值约束。 +这里有一个小技巧,可能会很方便,但你并不会经常需要它。 -像下面这样,添加 `ge=1` 后,`item_id` 将必须是一个大于(`g`reater than)或等于(`e`qual)`1` 的整数。 +如果你想要: -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +* 在没有 `Query` 且没有任何默认值的情况下声明查询参数 `q` +* 使用 `Path` 声明路径参数 `item_id` +* 让它们的顺序与上面不同 +* 不使用 `Annotated` -## 数值校验:大于和小于等于 +...Python 为此有一个小的特殊语法。 -同样的规则适用于: +在函数的第一个参数位置传入 `*`。 + +Python 不会对这个 `*` 做任何事,但它会知道之后的所有参数都应该作为关键字参数(键值对)来调用,也被称为 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` 必须是一个整数,值要「`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) -{* ../../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` 值。 -能够声明 gt 而不仅仅是 ge 在这个前提下变得重要起来。例如,你可以要求一个值必须大于 `0`,即使它小于 `1`。 +能够声明 gt 而不仅仅是 ge 在这里变得很重要。例如,你可以要求一个值必须大于 `0`,即使它小于 `1`。 -因此,`0.5` 将是有效值。但是 `0.0`或 `0` 不是。 +因此,`0.5` 将是有效值。但是 `0.0` 或 `0` 不是。 -对于 lt 也是一样的。 +对于 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-params-str-validations.md){.internal-link target=_blank} 相同的方式使用 `Query`、`Path`(以及其他你还没见过的类)声明元数据和字符串校验。 +你能够以与[查询参数和字符串校验](query-params-str-validations.md){.internal-link target=_blank}相同的方式使用 `Query`、`Path`(以及其他你还没见过的类)声明元数据和字符串校验。 而且你还可以声明数值校验: @@ -94,24 +131,24 @@ Python 不会对该 `*` 做任何事情,但是它将知道之后的所有参 * `lt`:小于(`l`ess `t`han) * `le`:小于等于(`l`ess than or `e`qual) -/// info +/// info | 信息 -`Query`、`Path` 以及你后面会看到的其他类继承自一个共同的 `Param` 类(不需要直接使用它)。 +`Query`、`Path` 以及你后面会看到的其他类,都是一个通用 `Param` 类的子类。 -而且它们都共享相同的所有你已看到并用于添加额外校验和元数据的参数。 +它们都共享相同的参数,用于你已看到的额外校验和元数据。 /// /// note | 技术细节 -当你从 `fastapi` 导入 `Query`、`Path` 和其他同类对象时,它们实际上是函数。 +当你从 `fastapi` 导入 `Query`、`Path` 和其他对象时,它们实际上是函数。 -当被调用时,它们返回同名类的实例。 +当被调用时,它们会返回同名类的实例。 -如此,你导入 `Query` 这个函数。当你调用它时,它将返回一个同样命名为 `Query` 的类的实例。 +也就是说,你导入的是函数 `Query`。当你调用它时,它会返回一个同名的 `Query` 类的实例。 -因为使用了这些函数(而不是直接使用类),所以你的编辑器不会标记有关其类型的错误。 +之所以使用这些函数(而不是直接使用类),是为了让你的编辑器不要因为它们的类型而标记错误。 -这样,你可以使用常规的编辑器和编码工具,而不必添加自定义配置来忽略这些错误。 +这样你就可以使用常规的编辑器和编码工具,而不必添加自定义配置来忽略这些错误。 /// diff --git a/docs/zh/docs/tutorial/path-params.md b/docs/zh/docs/tutorial/path-params.md index ac9df0831..fa4c9514a 100644 --- a/docs/zh/docs/tutorial/path-params.md +++ b/docs/zh/docs/tutorial/path-params.md @@ -1,10 +1,10 @@ -# 路径参数 +# 路径参数 { #path-parameters } -FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(**变量**): +你可以使用与 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,可获得如下响应: @@ -12,11 +12,11 @@ FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(** {"item_id":"foo"} ``` -## 声明路径参数的类型 +## 声明路径参数的类型 { #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`。 @@ -26,7 +26,7 @@ FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(** /// -## 数据转换 +## 数据转换 { #data-conversion } 运行示例并访问 http://127.0.0.1:8000/items/3,返回的响应如下: @@ -42,56 +42,57 @@ FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(** /// -## 数据校验 +## 数据校验 { #data-validation } 通过浏览器访问 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` 的值 (`"foo"`)的类型不是 `int`。 +这是因为路径参数 `item_id` 的值(`"foo"`)的类型不是 `int`。 -值的类型不是 `int ` 而是浮点数(`float`)时也会显示同样的错误,比如: http://127.0.0.1:8000/items/4.2。 +值的类型不是 `int` 而是浮点数(`float`)时也会显示同样的错误,比如: http://127.0.0.1:8000/items/4.2 /// check | 检查 -**FastAPI** 使用 Python 类型声明实现了数据校验。 +**FastAPI** 使用同样的 Python 类型声明实现了数据校验。 -注意,上面的错误清晰地指出了未通过校验的具体原因。 +注意,上面的错误清晰地指出了未通过校验的具体位置。 这在开发调试与 API 交互的代码时非常有用。 /// -## 查看文档 +## 文档 { #documentation } -访问 http://127.0.0.1:8000/docs,查看自动生成的 API 文档: +访问 http://127.0.0.1:8000/docs,查看自动生成的交互式 API 文档: /// check | 检查 -还是使用 Python 类型声明,**FastAPI** 提供了(集成 Swagger UI 的)API 文档。 +还是使用 Python 类型声明,**FastAPI** 提供了(集成 Swagger UI 的)自动交互式文档。 注意,路径参数的类型是整数。 /// -## 基于标准的好处,备选文档 +## 基于标准的好处,备选文档 { #standards-based-benefits-alternative-documentation } -**FastAPI** 使用 OpenAPI 生成概图,所以能兼容很多工具。 +**FastAPI** 使用 OpenAPI 生成概图,所以能兼容很多工具。 因此,**FastAPI** 还内置了 ReDoc 生成的备选 API 文档,可在此查看 http://127.0.0.1:8000/redoc: @@ -99,15 +100,15 @@ FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(** 同样,还有很多兼容工具,包括多种语言的代码生成工具。 -## Pydantic +## Pydantic { #pydantic } FastAPI 充分地利用了 Pydantic 的优势,用它在后台校验数据。众所周知,Pydantic 擅长的就是数据校验。 同样,`str`、`float`、`bool` 以及很多复合数据类型都可以使用类型声明。 -下一章介绍详细内容。 +接下来的章节会介绍其中的好几种。 -## 顺序很重要 +## 顺序很重要 { #order-matters } 有时,*路径操作*中的路径是写死的。 @@ -117,15 +118,21 @@ FastAPI 充分地利用了 `Enum` 类型接收预设的*路径参数*。 +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### 创建 `Enum` 类 +由于路径首先匹配,始终会使用第一个定义的。 + +## 预设值 { #predefined-values } + +路径操作使用 Python 的 `Enum` 类型接收预设的路径参数。 + +### 创建 `Enum` 类 { #create-an-enum-class } 导入 `Enum` 并创建继承自 `str` 和 `Enum` 的子类。 @@ -133,47 +140,41 @@ FastAPI 充分地利用了 枚举(即 enums)。 - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | 提示 -**AlexNet**、**ResNet**、**LeNet** 是机器学习模型。 +**AlexNet**、**ResNet**、**LeNet** 是机器学习模型的名字。 /// -### 声明*路径参数* +### 声明路径参数 { #declare-a-path-parameter } -使用 Enum 类(`ModelName`)创建使用类型注解的*路径参数*: +使用 Enum 类(`ModelName`)创建使用类型注解的路径参数: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### 查看文档 +### 查看文档 { #check-the-docs } - API 文档会显示预定义*路径参数*的可用值: +API 文档会显示预定义路径参数的可用值: -### 使用 Python _枚举类型_ +### 使用 Python 枚举 { #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` 获取实际的值(本例中为**字符串**): +使用 `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,13 +182,13 @@ Python 3.4 及之后版本支持解析** +- 数据 "解析" - 数据校验 -- API 注解和 API 文档 +- API 注解和自动文档 只需要声明一次即可。 diff --git a/docs/zh/docs/tutorial/query-param-models.md b/docs/zh/docs/tutorial/query-param-models.md index c6a79a71a..fc691839d 100644 --- a/docs/zh/docs/tutorial/query-param-models.md +++ b/docs/zh/docs/tutorial/query-param-models.md @@ -1,16 +1,16 @@ -# 查询参数模型 +# 查询参数模型 { #query-parameter-models } 如果你有一组具有相关性的**查询参数**,你可以创建一个 **Pydantic 模型**来声明它们。 这将允许你在**多个地方**去**复用模型**,并且一次性为所有参数声明验证和元数据。😎 -/// note +/// note | 注意 FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓 /// -## 使用 Pydantic 模型的查询参数 +## 使用 Pydantic 模型的查询参数 { #query-parameters-with-a-pydantic-model } 在一个 **Pydantic 模型**中声明你需要的**查询参数**,然后将参数声明为 `Query`: @@ -18,7 +18,7 @@ FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓 **FastAPI** 将会从请求的**查询参数**中**提取**出**每个字段**的数据,并将其提供给你定义的 Pydantic 模型。 -## 查看文档 +## 查看文档 { #check-the-docs } 你可以在 `/docs` 页面的 UI 中查看查询参数: @@ -26,11 +26,11 @@ FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓 -## 禁止额外的查询参数 +## 禁止额外的查询参数 { #forbid-extra-query-parameters } 在一些特殊的使用场景中(可能不是很常见),你可能希望**限制**你要接收的查询参数。 -你可以使用 Pydantic 的模型配置来 `forbid`(意为禁止 —— 译者注)任何 `extra`(意为额外的 —— 译者注)字段: +你可以使用 Pydantic 的模型配置来 `forbid` 任何 `extra` 字段: {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} @@ -57,11 +57,11 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## 总结 +## 总结 { #summary } 你可以使用 **Pydantic 模型**在 **FastAPI** 中声明**查询参数**。😎 -/// tip +/// tip | 提示 剧透警告:你也可以使用 Pydantic 模型来声明 cookie 和 headers,但你将在本教程的后面部分阅读到这部分内容。🤫 diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index c2f9a7e9f..62552e6d0 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -1,142 +1,271 @@ -# 查询参数和字符串校验 +# 查询参数和字符串校验 { #query-parameters-and-string-validations } **FastAPI** 允许你为参数声明额外的信息和校验。 -让我们以下面的应用程序为例: +让我们以下面的应用为例: {* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -查询参数 `q` 的类型为 `str`,默认值为 `None`,因此它是可选的。 +查询参数 `q` 的类型为 `str | None`,这意味着它是 `str` 类型,但也可以是 `None`。其默认值确实为 `None`,所以 FastAPI 会知道它不是必填的。 -## 额外的校验 +/// note | 注意 -我们打算添加约束条件:即使 `q` 是可选的,但只要提供了该参数,则该参数值**不能超过50个字符的长度**。 +FastAPI 会因为默认值 `= None` 而知道 `q` 的值不是必填的。 -### 导入 `Query` - -为此,首先从 `fastapi` 导入 `Query`: - -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[1] *} - -## 使用 `Query` 作为默认值 - -现在,将 `Query` 用作查询参数的默认值,并将它的 `max_length` 参数设置为 50: - -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} - -由于我们必须用 `Query(default=None)` 替换默认值 `None`,`Query` 的第一个参数同样也是用于定义默认值。 - -所以: - -```Python -q: Union[str, None] = Query(default=None) -``` - -...使得参数可选,等同于: - -```Python -q: str = None -``` - -但是 `Query` 显式地将其声明为查询参数。 - -然后,我们可以将更多的参数传递给 `Query`。在本例中,适用于字符串的 `max_length` 参数: - -```Python -q: Union[str, None] = Query(default=None, max_length=50) -``` - -将会校验数据,在数据无效时展示清晰的错误信息,并在 OpenAPI 模式的*路径操作*中记录该参​​数。 - -## 添加更多校验 - -你还可以添加 `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** 中直接使用它们。 - -## 默认值 - -你可以向 `Query` 的第一个参数传入 `None` 用作查询参数的默认值,以同样的方式你也可以传递其他默认值。 - -假设你想要声明查询参数 `q`,使其 `min_length` 为 `3`,并且默认值为 `fixedquery`: - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note - -具有默认值还会使该参数成为可选参数。 +将类型标注为 `str | None` 能让你的编辑器提供更好的辅助和错误检测。 /// -## 声明为必需参数 +## 额外校验 { #additional-validation } -当我们不需要声明额外的校验或元数据时,只需不声明默认值就可以使 `q` 参数成为必需参数,例如: +我们打算添加约束:即使 `q` 是可选的,但只要提供了该参数,**其长度不能超过 50 个字符**。 + +### 导入 `Query` 和 `Annotated` { #import-query-and-annotated } + +为此,先导入: + +- 从 `fastapi` 导入 `Query` +- 从 `typing` 导入 `Annotated` + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} + +/// info | 信息 + +FastAPI 在 0.95.0 版本中添加了对 `Annotated` 的支持(并开始推荐使用)。 + +如果你的版本更旧,使用 `Annotated` 会报错。 + +在使用 `Annotated` 之前,请确保先[升级 FastAPI 版本](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}到至少 0.95.1。 + +/// + +## 在 `q` 参数的类型中使用 `Annotated` { #use-annotated-in-the-type-for-the-q-parameter } + +还记得我之前在[Python 类型简介](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}中说过可以用 `Annotated` 给参数添加元数据吗? + +现在正是与 FastAPI 搭配使用它的时候。🚀 + +我们之前的类型标注是: + +//// tab | Python 3.10+ ```Python -q: str +q: str | None = None ``` -代替: +//// + +//// tab | Python 3.9+ ```Python q: Union[str, None] = None ``` -但是现在我们正在用 `Query` 声明它,例如: +//// + +我们要做的是用 `Annotated` 把它包起来,变成: + +//// tab | Python 3.10+ ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None] = None ``` -因此,当你在使用 `Query` 且需要声明一个值是必需的时,只需不声明默认参数: +//// -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +//// tab | Python 3.9+ -### 使用`None`声明必需参数 +```Python +q: Annotated[Union[str, None]] = None +``` -你可以声明一个参数可以接收`None`值,但它仍然是必需的。这将强制客户端发送一个值,即使该值是`None`。 +//// -为此,你可以声明`None`是一个有效的类型,并仍然使用`default=...`: +这两种写法含义相同,`q` 是一个可以是 `str` 或 `None` 的参数,默认是 `None`。 -{* ../../docs_src/query_params_str_validations/tutorial006c.py hl[9] *} +现在进入更有趣的部分。🎉 -/// tip +## 在 `q` 的 `Annotated` 中添加 `Query` { #add-query-to-annotated-in-the-q-parameter } -Pydantic 是 FastAPI 中所有数据验证和序列化的核心,当你在没有设默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,它具有特殊行为,你可以在 Pydantic 文档中阅读有关必需可选字段的更多信息。 +有了 `Annotated` 之后,我们就可以放入更多信息(本例中是额外的校验)。在 `Annotated` 中添加 `Query`,并把参数 `max_length` 设为 `50`: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +注意默认值依然是 `None`,所以该参数仍是可选的。 + +但现在把 `Query(max_length=50)` 放到 `Annotated` 里,我们就在告诉 FastAPI,这个值需要**额外校验**,最大长度为 50 个字符。😎 + +/// tip | 提示 + +这里用的是 `Query()`,因为这是一个**查询参数**。稍后我们还会看到 `Path()`、`Body()`、`Header()` 和 `Cookie()`,它们也接受与 `Query()` 相同的参数。 /// -## 查询参数列表 / 多个值 +FastAPI 现在会: -当你使用 `Query` 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值。 +- 对数据进行**校验**,确保最大长度为 50 个字符 +- 当数据无效时向客户端展示**清晰的错误** +- 在 OpenAPI 模式的*路径操作*中**记录**该参数(因此会出现在**自动文档 UI** 中) + +## 另一种(旧的)方式:把 `Query` 作为默认值 { #alternative-old-query-as-the-default-value } + +早期版本的 FastAPI(0.95.0 之前)要求你把 `Query` 作为参数的默认值,而不是放在 `Annotated` 里。你很可能会在别处看到这种写法,所以我也给你解释一下。 + +/// 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: str | None = Query(default=None) +``` + +……会让参数变成可选,默认值为 `None`,等同于: + +```Python +q: str | None = None +``` + +但使用 `Query` 的版本会显式把它声明为一个查询参数。 + +然后,我们可以向 `Query` 传入更多参数。本例中是适用于字符串的 `max_length` 参数: + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +这会校验数据、在数据无效时展示清晰的错误,并在 OpenAPI 模式的*路径操作*中记录该参数。 + +### 在默认值中使用 `Query` 或在 `Annotated` 中使用 `Query` { #query-as-the-default-value-or-in-annotated } + +注意,当你在 `Annotated` 中使用 `Query` 时,不能再给 `Query` 传 `default` 参数。 + +相反,应使用函数参数本身的实际默认值。否则会不一致。 + +例如,下面这样是不允许的: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +……因为不清楚默认值应该是 `"rick"` 还是 `"morty"`。 + +因此,你应该这样用(推荐): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +……或者在旧代码库中你会见到: + +```Python +q: str = Query(default="rick") +``` + +### `Annotated` 的优势 { #advantages-of-annotated } + +**推荐使用 `Annotated`**,而不是把 `Query` 放在函数参数的默认值里,这样做在多方面都**更好**。🤓 + +函数参数的**默认值**就是**真正的默认值**,这与 Python 的直觉更一致。😌 + +你可以在**其他地方**不通过 FastAPI **直接调用**这个函数,而且它会**按预期工作**。如果有**必填**参数(没有默认值),你的**编辑器**会报错提示;如果在运行时没有传入必填参数,**Python** 也会报错。 + +当你不使用 `Annotated` 而是使用**(旧的)默认值风格**时,如果你在**其他地方**不通过 FastAPI 调用该函数,你必须**记得**给函数传参,否则得到的值会和预期不同(例如得到 `QueryInfo` 之类的对象而不是 `str`)。而你的编辑器不会报错,Python 也不会在调用时报错,只有在函数内部的操作出错时才会暴露问题。 + +由于 `Annotated` 可以包含多个元数据标注,你甚至可以用同一个函数与其他工具配合,例如 Typer。🚀 + +## 添加更多校验 { #add-more-validations } + +你还可以添加 `min_length` 参数: + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## 添加正则表达式 { #add-regular-expressions } + +你可以定义一个参数必须匹配的 正则表达式 `pattern`: + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +这个特定的正则表达式通过以下规则检查接收到的参数值: + +- `^`:必须以接下来的字符开头,前面没有其他字符。 +- `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` 成为必填参数,例如: + +```Python +q: str +``` + +而不是: + +```Python +q: str | None = None +``` + +但现在我们用 `Query` 来声明它,例如: + +```Python +q: Annotated[str | None, Query(min_length=3)] = None +``` + +因此,在使用 `Query` 的同时需要把某个值声明为必填时,只需不声明默认值: + +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} + +### 必填,但可以为 `None` { #required-can-be-none } + +你可以声明一个参数可以接收 `None`,但它仍然是必填的。这将强制客户端必须发送一个值,即使该值是 `None`。 + +为此,你可以声明 `None` 是有效类型,但不声明默认值: + +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} + +## 查询参数列表 / 多个值 { #query-parameter-list-multiple-values } + +当你用 `Query` 显式地定义查询参数时,你还可以声明它接收一个值列表,换句话说,接收多个值。 例如,要声明一个可在 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: ``` http://localhost:8000/items/?q=foo&q=bar ``` -你会在*路径操作函数*的*函数参数* `q` 中以一个 Python `list` 的形式接收到*查询参数* `q` 的多个值(`foo` 和 `bar`)。 +你会在*路径操作函数*的*函数参数* `q` 中以一个 Python `list` 的形式接收到多个 `q` *查询参数* 的值(`foo` 和 `bar`)。 因此,该 URL 的响应将会是: @@ -149,21 +278,21 @@ http://localhost:8000/items/?q=foo&q=bar } ``` -/// tip +/// tip | 提示 -要声明类型为 `list` 的查询参数,如上例所示,你需要显式地使用 `Query`,否则该参数将被解释为请求体。 +要声明类型为 `list` 的查询参数(如上例),你需要显式地使用 `Query`,否则它会被解释为请求体。 /// -交互式 API 文档将会相应地进行更新,以允许使用多个值: +交互式 API 文档会相应更新,以支持多个值: - + -### 具有默认值的查询参数列表 / 多个值 +### 具有默认值的查询参数列表 / 多个值 { #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] *} 如果你访问: @@ -182,93 +311,163 @@ http://localhost:8000/items/ } ``` -#### 使用 `list` +#### 只使用 `list` { #using-just-list } -你也可以直接使用 `list` 代替 `List [str]`: +你也可以直接使用 `list`,而不是 `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 +/// note | 注意 -请记住,在这种情况下 FastAPI 将不会检查列表的内容。 +请记住,在这种情况下 FastAPI 不会检查列表的内容。 -例如,`List[int]` 将检查(并记录到文档)列表的内容必须是整数。但是单独的 `list` 不会。 +例如,`list[int]` 会检查(并记录到文档)列表的内容必须是整数。但仅用 `list` 不会。 /// -## 声明更多元数据 +## 声明更多元数据 { #declare-more-metadata } 你可以添加更多有关该参数的信息。 -这些信息将包含在生成的 OpenAPI 模式中,并由文档用户界面和外部工具所使用。 +这些信息会包含在生成的 OpenAPI 中,并被文档用户界面和外部工具使用。 -/// note +/// note | 注意 请记住,不同的工具对 OpenAPI 的支持程度可能不同。 -其中一些可能不会展示所有已声明的额外信息,尽管在大多数情况下,缺少的这部分功能已经计划进行开发。 +其中一些可能还不会展示所有已声明的额外信息,尽管在大多数情况下,缺失的功能已经在计划开发中。 /// 你可以添加 `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`。 +假设你想要参数名为 `item-query`。 -像下面这样: +像这样: ``` http://127.0.0.1:8000/items/?item-query=foobaritems ``` -但是 `item-query` 不是一个有效的 Python 变量名称。 +但 `item-query` 不是有效的 Python 变量名。 最接近的有效名称是 `item_query`。 -但是你仍然要求它在 URL 中必须是 `item-query`... +但你仍然需要它在 URL 中就是 `item-query`…… -这时你可以用 `alias` 参数声明一个别名,该别名将用于在 URL 中查找查询参数值: +这时可以用 `alias` 参数声明一个别名,FastAPI 会用该别名在 URL 中查找参数值: -{* ../../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=True` 传入 `Query`: +那么将参数 `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` 等。🤓 + +/// + +例如,这个自定义校验器会检查条目 ID 是否以 `isbn-`(用于 ISBN 书号)或 `imdb-`(用于 IMDB 电影 URL 的 ID)开头: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | 信息 + +这在 Pydantic 2 或更高版本中可用。😎 + +/// + +/// tip | 提示 + +如果你需要进行任何需要与**外部组件**通信的校验,例如数据库或其他 API,你应该改用 **FastAPI 依赖项**,稍后你会学到它们。 + +这些自定义校验器用于只需检查请求中**同一份数据**即可完成的事情。 + +/// + +### 理解这段代码 { #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()` 我们会得到一个包含每个字典项键和值的元组的 可迭代对象。 + +我们用 `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 } + +你可以为参数声明额外的校验和元数据。 通用的校验和元数据: -* `alias` -* `title` -* `description` -* `deprecated` +- `alias` +- `title` +- `description` +- `deprecated` -特定于字符串的校验: +字符串特有的校验: -* `min_length` -* `max_length` -* `regex` +- `min_length` +- `max_length` +- `pattern` -在这些示例中,你了解了如何声明对 `str` 值的校验。 +也可以使用 `AfterValidator` 进行自定义校验。 -请参阅下一章节,以了解如何声明对其他类型例如数值的校验。 +在这些示例中,你看到了如何为 `str` 值声明校验。 + +参阅下一章节,了解如何为其他类型(例如数值)声明校验。 diff --git a/docs/zh/docs/tutorial/query-params.md b/docs/zh/docs/tutorial/query-params.md index cc2e74b9e..9ef998731 100644 --- a/docs/zh/docs/tutorial/query-params.md +++ b/docs/zh/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 的 `?` 之后,以 `&` 分隔。 @@ -26,9 +26,9 @@ http://127.0.0.1:8000/items/?skip=0&limit=10 * (显而易见的)编辑器支持 * 数据**解析** * 数据校验 -* API 文档 +* 自动文档 -## 默认值 +## 默认值 { #defaults } 查询参数不是路径的固定内容,它是可选的,还支持默认值。 @@ -57,7 +57,7 @@ http://127.0.0.1:8000/items/?skip=20 * `skip=20`:在 URL 中设定的值 * `limit=10`:使用默认值 -## 可选参数 +## 可选参数 { #optional-parameters } 同理,把默认值设为 `None` 即可声明**可选的**查询参数: @@ -71,15 +71,7 @@ http://127.0.0.1:8000/items/?skip=20 /// -/// note | 笔记 - -因为默认值为 `= None`,FastAPI 把 `q` 识别为可选参数。 - -FastAPI 不使用 `Optional[str]` 中的 `Optional`(只使用 `str`),但 `Optional[str]` 可以帮助编辑器发现代码中的错误。 - -/// - -## 查询参数类型转换 +## 查询参数类型转换 { #query-parameter-type-conversion } 参数还可以声明为 `bool` 类型,FastAPI 会自动转换参数类型: @@ -116,10 +108,10 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -或其它任意大小写形式(大写、首字母大写等),函数接收的 `short` 参数都是布尔值 `True`。值为 `False` 时也一样。 +或其它任意大小写形式(大写、首字母大写等),函数接收的 `short` 参数都是布尔值 `True`。否则为 `False`。 -## 多个路径和查询参数 +## 多个路径和查询参数 { #multiple-path-and-query-parameters } **FastAPI** 可以识别同时声明的多个路径参数和查询参数。 @@ -129,7 +121,7 @@ FastAPI 通过参数名进行检测: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## 必选查询参数 +## 必选查询参数 { #required-query-parameters } 为不是路径参数的参数声明默认值(至此,仅有查询参数),该参数就**不是必选**的了。 @@ -137,7 +129,7 @@ FastAPI 通过参数名进行检测: 如果要把查询参数设置为**必选**,就不要声明默认值: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} 这里的查询参数 `needy` 是类型为 `str` 的必选查询参数。 @@ -151,16 +143,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 + } + ] } ``` @@ -191,6 +184,6 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy /// tip | 提示 -还可以像在[路径参数](path-params.md#_8){.internal-link target=_blank} 中那样使用 `Enum`。 +还可以像在[路径参数](path-params.md#predefined-values){.internal-link target=_blank} 中那样使用 `Enum`。 /// diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index 81ddc7238..927bd093b 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -1,159 +1,163 @@ -# 请求文件 +# 请求文件 { #request-files } -`File` 用于定义客户端的上传文件。 +你可以使用 `File` 定义由客户端上传的文件。 -/// info | 说明 +/// info | 信息 -因为上传文件以「表单数据」形式发送。 +要接收上传的文件,请先安装 `python-multipart`。 -所以接收上传文件,要预先安装 `python-multipart`。 +请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank}、激活它,然后安装,例如: -例如: `pip install python-multipart`。 +```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 } -创建文件(`File`)参数的方式与 `Body` 和 `Form` 一样: +像为 `Body` 或 `Form` 一样创建文件参数: -{* ../../docs_src/request_files/tutorial001.py hl[7] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} -/// info | 说明 +/// info | 信息 `File` 是直接继承自 `Form` 的类。 -注意,从 `fastapi` 导入的 `Query`、`Path`、`File` 等项,实际上是返回特定类的函数。 +但要注意,从 `fastapi` 导入的 `Query`、`Path`、`File` 等项,实际上是返回特定类的函数。 /// /// tip | 提示 -声明文件体必须使用 `File`,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数。 +声明文件体必须使用 `File`,否则,这些参数会被当作查询参数或请求体(JSON)参数。 /// -文件作为「表单数据」上传。 +文件将作为「表单数据」上传。 -如果把*路径操作函数*参数的类型声明为 `bytes`,**FastAPI** 将以 `bytes` 形式读取和接收文件内容。 +如果把*路径操作函数*参数的类型声明为 `bytes`,**FastAPI** 会为你读取文件,并以 `bytes` 的形式接收其内容。 -这种方式把文件的所有内容都存储在内存里,适用于小型文件。 +请注意,这意味着整个内容会存储在内存中,适用于小型文件。 -不过,很多情况下,`UploadFile` 更好用。 +不过,在很多情况下,使用 `UploadFile` 会更有优势。 -## 含 `UploadFile` 的文件参数 +## 含 `UploadFile` 的文件参数 { #file-parameters-with-uploadfile } -定义文件参数时使用 `UploadFile`: +将文件参数的类型声明为 `UploadFile`: -{* ../../docs_src/request_files/tutorial001.py hl[12] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} -`UploadFile` 与 `bytes` 相比有更多优势: +与 `bytes` 相比,使用 `UploadFile` 有多项优势: -* 使用 `spooled` 文件: - * 存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘; -* 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存; -* 可获取上传文件的元数据; -* 自带 file-like `async` 接口; -* 暴露的 Python `SpooledTemporaryFile` 对象,可直接传递给其他预期「file-like」对象的库。 +* 无需在参数的默认值中使用 `File()`。 +* 它使用“spooled”文件: + * 文件会先存储在内存中,直到达到最大上限,超过该上限后会写入磁盘。 +* 因此,非常适合处理图像、视频、大型二进制等大文件,而不会占用所有内存。 +* 你可以获取上传文件的元数据。 +* 它提供 file-like 的 `async` 接口。 +* 它暴露了一个实际的 Python `SpooledTemporaryFile` 对象,你可以直接传给期望「file-like」对象的其他库。 -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` 的属性如下: -* `filename`:上传文件名字符串(`str`),例如, `myimage.jpg`; -* `content_type`:内容类型(MIME 类型 / 媒体类型)字符串(`str`),例如,`image/jpeg`; -* `file`: `SpooledTemporaryFile`file-like 对象)。其实就是 Python文件,可直接传递给其他预期 `file-like` 对象的函数或支持库。 +* `filename`:上传的原始文件名字符串(`str`),例如 `myimage.jpg`。 +* `content_type`:内容类型(MIME 类型 / 媒体类型)的字符串(`str`),例如 `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) ` 移动到文件开头; - * 执行 `await myfile.read()` 后,需再次读取已读取内容时,这种方法特别好用; +* `write(data)`:将 `data`(`str` 或 `bytes`)写入文件。 +* `read(size)`:读取文件中 `size`(`int`)个字节/字符。 +* `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() ``` -/// note | `async` 技术细节 +/// note | 注意 -使用 `async` 方法时,**FastAPI** 在线程池中执行文件方法,并 `await` 操作完成。 +当你使用这些 `async` 方法时,**FastAPI** 会在线程池中运行相应的文件方法并等待其完成。 /// -/// note | Starlette 技术细节 +/// note | 注意 -**FastAPI** 的 `UploadFile` 直接继承自 **Starlette** 的 `UploadFile`,但添加了一些必要功能,使之与 **Pydantic** 及 FastAPI 的其它部件兼容。 +**FastAPI** 的 `UploadFile` 直接继承自 **Starlette** 的 `UploadFile`,但添加了一些必要的部分,使其与 **Pydantic** 以及 FastAPI 的其他部分兼容。 /// -## 什么是 「表单数据」 +## 什么是「表单数据」 { #what-is-form-data } -与 JSON 不同,HTML 表单(`
`)向服务器发送数据通常使用「特殊」的编码。 +HTML 表单(`
`)向服务器发送数据的方式通常会对数据使用一种「特殊」的编码,这与 JSON 不同。 -**FastAPI** 要确保从正确的位置读取数据,而不是读取 JSON。 +**FastAPI** 会确保从正确的位置读取这些数据,而不是从 JSON 中读取。 -/// note | 技术细节 +/// note | 注意 -不包含文件时,表单数据一般用 `application/x-www-form-urlencoded`「媒体类型」编码。 +当不包含文件时,来自表单的数据通常使用「媒体类型」`application/x-www-form-urlencoded` 编码。 -但表单包含文件时,编码为 `multipart/form-data`。使用了 `File`,**FastAPI** 就知道要从请求体的正确位置获取文件。 +但当表单包含文件时,会编码为 `multipart/form-data`。如果你使用 `File`,**FastAPI** 会知道需要从请求体的正确位置获取文件。 -编码和表单字段详见 MDN Web 文档的 POST 小节。 +如果你想进一步了解这些编码和表单字段,请参阅 MDN web docs for POST。 /// /// warning | 警告 -可在一个*路径操作*中声明多个 `File` 和 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段。因为此时请求体的编码是 `multipart/form-data`,不是 `application/json`。 +你可以在一个*路径操作*中声明多个 `File` 和 `Form` 参数,但不能同时声明希望以 JSON 接收的 `Body` 字段,因为此时请求体会使用 `multipart/form-data` 编码,而不是 `application/json`。 -这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。 +这不是 **FastAPI** 的限制,而是 HTTP 协议的一部分。 /// -## 可选文件上传 +## 可选文件上传 { #optional-file-upload } 您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选: -{* ../../docs_src/request_files/tutorial001_02_py310.py hl[7,14] *} +{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## 带有额外元数据的 `UploadFile` +## 带有额外元数据的 `UploadFile` { #uploadfile-with-additional-metadata } 您也可以将 `File()` 与 `UploadFile` 一起使用,例如,设置额外的元数据: -{* ../../docs_src/request_files/tutorial001_03.py hl[13] *} +{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## 多文件上传 +## 多文件上传 { #multiple-file-uploads } FastAPI 支持同时上传多个文件。 -可用同一个「表单字段」发送含多个文件的「表单数据」。 +它们会被关联到同一个通过「表单数据」发送的「表单字段」。 -上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`): +要实现这一点,声明一个由 `bytes` 或 `UploadFile` 组成的列表(`List`): -{* ../../docs_src/request_files/tutorial002_py39.py hl[8,13] *} +{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} 接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。 - -/// note | 技术细节 +/// note | 注意 也可以使用 `from starlette.responses import HTMLResponse`。 @@ -161,12 +165,12 @@ FastAPI 支持同时上传多个文件。 /// -### 带有额外元数据的多文件上传 +### 带有额外元数据的多文件上传 { #multiple-file-uploads-with-additional-metadata } -和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`: +和之前的方式一样,你可以为 `File()` 设置额外参数,即使是 `UploadFile`: -{* ../../docs_src/request_files/tutorial003_py39.py hl[16] *} +{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## 小结 +## 小结 { #recap } -本节介绍了如何用 `File` 把上传文件声明为(表单数据的)输入参数。 +使用 `File`、`bytes` 和 `UploadFile` 来声明在请求中上传的文件,它们以表单数据发送。 diff --git a/docs/zh/docs/tutorial/request-form-models.md b/docs/zh/docs/tutorial/request-form-models.md index e639e4b9f..4eb98ea22 100644 --- a/docs/zh/docs/tutorial/request-form-models.md +++ b/docs/zh/docs/tutorial/request-form-models.md @@ -1,12 +1,12 @@ -# 表单模型 +# 表单模型 { #form-models } -您可以使用 **Pydantic 模型**在 FastAPI 中声明**表单字段**。 +你可以在 FastAPI 中使用 **Pydantic 模型**声明**表单字段**。 -/// info +/// 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,51 +14,51 @@ $ pip install python-multipart /// -/// note +/// note | 注意 自 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` : +你可以在文档 UI 中验证它,地址为 `/docs`:
-## 禁止额外的表单字段 +## 禁止额外的表单字段 { #forbid-extra-form-fields } -在某些特殊使用情况下(可能并不常见),您可能希望将表单字段**限制**为仅在 Pydantic 模型中声明过的字段,并**禁止**任何**额外**的字段。 +在某些特殊使用情况下(可能并不常见),你可能希望将表单字段**限制**为仅在 Pydantic 模型中声明过的字段,并**禁止**任何**额外**的字段。 -/// note +/// note | 注意 自 FastAPI 版本 `0.114.0` 起支持此功能。🤓 /// -您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段: +你可以使用 Pydantic 的模型配置来 `forbid` 任何 `extra` 字段: {* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} 如果客户端尝试发送一些额外的数据,他们将收到**错误**响应。 -例如,如果客户端尝试发送这样的表单字段: +例如,客户端尝试发送如下表单字段: * `username`: `Rick` * `password`: `Portal Gun` * `extra`: `Mr. Poopybutthole` -他们将收到一条错误响应,表明字段 `extra` 是不被允许的: +他们将收到一条错误响应,表明字段 `extra` 不被允许: ```json { @@ -73,6 +73,6 @@ $ pip install python-multipart } ``` -## 总结 +## 总结 { #summary } -您可以使用 Pydantic 模型在 FastAPI 中声明表单字段。😎 +你可以使用 Pydantic 模型在 FastAPI 中声明表单字段。😎 diff --git a/docs/zh/docs/tutorial/request-forms-and-files.md b/docs/zh/docs/tutorial/request-forms-and-files.md index f72e5047a..3c809868b 100644 --- a/docs/zh/docs/tutorial/request-forms-and-files.md +++ b/docs/zh/docs/tutorial/request-forms-and-files.md @@ -1,24 +1,28 @@ -# 请求表单与文件 +# 请求表单与文件 { #request-forms-and-files } FastAPI 支持同时使用 `File` 和 `Form` 定义文件和表单字段。 -/// info | 说明 +/// 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] *} 文件和表单字段作为表单数据上传与接收。 @@ -32,6 +36,6 @@ FastAPI 支持同时使用 `File` 和 `Form` 定义文件和表单字段。 /// -## 小结 +## 小结 { #recap } 在同一个请求中接收数据和文件时,应同时使用 `File` 和 `Form`。 diff --git a/docs/zh/docs/tutorial/request-forms.md b/docs/zh/docs/tutorial/request-forms.md index ee03e82a7..70eb93a26 100644 --- a/docs/zh/docs/tutorial/request-forms.md +++ b/docs/zh/docs/tutorial/request-forms.md @@ -1,69 +1,73 @@ -# 表单数据 +# 表单数据 { #form-data } -接收的不是 JSON,而是表单字段时,要使用 `Form`。 +当你需要接收表单字段而不是 JSON 时,可以使用 `Form`。 -/// info | 说明 +/// info -要使用表单,需预先安装 `python-multipart`。 +要使用表单,首先安装 `python-multipart`。 -例如,`pip install python-multipart`。 +请先创建并激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后再进行安装,例如: + +```console +$ pip install python-multipart +``` /// -## 导入 `Form` +## 导入 `Form` { #import-form } 从 `fastapi` 导入 `Form`: -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## 定义 `Form` 参数 +## 定义 `Form` 参数 { #define-form-parameters } -创建表单(`Form`)参数的方式与 `Body` 和 `Query` 一样: +创建表单参数的方式与 `Body` 或 `Query` 相同: -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} -例如,OAuth2 规范的 "密码流" 模式规定要通过表单字段发送 `username` 和 `password`。 +例如,在 OAuth2 规范的一种使用方式(称为“密码流”)中,要求将 `username` 和 `password` 作为表单字段发送。 -该规范要求字段必须命名为 `username` 和 `password`,并通过表单字段发送,不能用 JSON。 +spec 要求这些字段必须精确命名为 `username` 和 `password`,并且作为表单字段发送,而不是 JSON。 -使用 `Form` 可以声明与 `Body` (及 `Query`、`Path`、`Cookie`)相同的元数据和验证。 +使用 `Form` 可以像使用 `Body`(以及 `Query`、`Path`、`Cookie`)一样声明相同的配置,包括校验、示例、别名(例如将 `username` 写成 `user-name`)等。 -/// info | 说明 +/// info `Form` 是直接继承自 `Body` 的类。 /// -/// tip | 提示 +/// tip -声明表单体要显式使用 `Form` ,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数。 +要声明表单请求体,必须显式使用 `Form`,否则这些参数会被当作查询参数或请求体(JSON)参数。 /// -## 关于 "表单字段" +## 关于 "表单字段" { #about-form-fields } -与 JSON 不同,HTML 表单(`
`)向服务器发送数据通常使用「特殊」的编码。 +HTML 表单(`
`)向服务器发送数据时通常会对数据使用一种“特殊”的编码方式,这与 JSON 不同。 -**FastAPI** 要确保从正确的位置读取数据,而不是读取 JSON。 +**FastAPI** 会确保从正确的位置读取这些数据,而不是从 JSON 中读取。 /// note | 技术细节 -表单数据的「媒体类型」编码一般为 `application/x-www-form-urlencoded`。 +表单数据通常使用“媒体类型” `application/x-www-form-urlencoded` 进行编码。 -但包含文件的表单编码为 `multipart/form-data`。文件处理详见下节。 +但当表单包含文件时,会编码为 `multipart/form-data`。你将在下一章阅读如何处理文件。 -编码和表单字段详见 MDN Web 文档的 POST小节。 +如果你想了解更多关于这些编码和表单字段的信息,请参阅 MDN Web 文档的 POST。 /// -/// warning | 警告 +/// warning -可在一个*路径操作*中声明多个 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段。因为此时请求体的编码是 `application/x-www-form-urlencoded`,不是 `application/json`。 +你可以在一个路径操作中声明多个 `Form` 参数,但不能同时再声明要接收为 JSON 的 `Body` 字段,因为此时请求体会使用 `application/x-www-form-urlencoded` 而不是 `application/json` 进行编码。 -这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。 +这不是 **FastAPI** 的限制,而是 HTTP 协议的一部分。 /// -## 小结 +## 小结 { #recap } -本节介绍了如何使用 `Form` 声明表单数据输入参数。 +使用 `Form` 来声明表单数据输入参数。 diff --git a/docs/zh/docs/tutorial/response-model.md b/docs/zh/docs/tutorial/response-model.md index 049cd1223..791eb66fb 100644 --- a/docs/zh/docs/tutorial/response-model.md +++ b/docs/zh/docs/tutorial/response-model.md @@ -1,6 +1,35 @@ -# 响应模型 +# 响应模型 - 返回类型 { #response-model-return-type } -你可以在任意的*路径操作*中使用 `response_model` 参数来声明用于响应的模型: +你可以通过为*路径操作函数*的**返回类型**添加注解来声明用于响应的类型。 + +和为输入数据在函数**参数**里做类型注解的方式相同,你可以使用 Pydantic 模型、`list`、`dict`、以及整数、布尔值等标量类型。 + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPI 会使用这个返回类型来: + +* 对返回数据进行**校验**。 + * 如果数据无效(例如缺少某个字段),这意味着你的应用代码有问题,没有返回应有的数据,FastAPI 将返回服务器错误而不是返回错误的数据。这样你和你的客户端都可以确定会收到期望的数据及其结构。 +* 在 OpenAPI 的*路径操作*中为响应添加**JSON Schema**。 + * 它会被**自动文档**使用。 + * 它也会被自动客户端代码生成工具使用。 + +但更重要的是: + +* 它会将输出数据**限制并过滤**为返回类型中定义的内容。 + * 这对**安全性**尤为重要,下面会进一步介绍。 + +## `response_model` 参数 { #response-model-parameter } + +在一些情况下,你需要或希望返回的数据与声明的类型不完全一致。 + +例如,你可能希望**返回一个字典**或数据库对象,但**将其声明为一个 Pydantic 模型**。这样 Pydantic 模型就会为你返回的对象(例如字典或数据库对象)完成文档、校验等工作。 + +如果你添加了返回类型注解,工具和编辑器会(正确地)报错,提示你的函数返回的类型(例如 `dict`)与声明的类型(例如一个 Pydantic 模型)不同。 + +在这些情况下,你可以使用*路径操作装饰器*参数 `response_model`,而不是返回类型。 + +你可以在任意*路径操作*中使用 `response_model` 参数: * `@app.get()` * `@app.post()` @@ -10,102 +39,209 @@ {* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} -/// note +/// note | 注意 -注意,`response_model`是「装饰器」方法(`get`,`post` 等)的一个参数。不像之前的所有参数和请求体,它不属于*路径操作函数*。 +注意,`response_model` 是「装饰器」方法(`get`、`post` 等)的一个参数。不是你的*路径操作函数*的参数,不像所有查询参数和请求体那样。 /// -它接收的类型与你将为 Pydantic 模型属性所声明的类型相同,因此它可以是一个 Pydantic 模型,但也可以是一个由 Pydantic 模型组成的 `list`,例如 `List[Item]`。 +`response_model` 接收的类型与为 Pydantic 模型字段声明的类型相同,因此它可以是一个 Pydantic 模型,也可以是一个由 Pydantic 模型组成的 `list`,例如 `List[Item]`。 -FastAPI 将使用此 `response_model` 来: +FastAPI 会使用这个 `response_model` 来完成数据文档、校验等,并且还会将输出数据**转换并过滤**为其类型声明。 -* 将输出数据转换为其声明的类型。 -* 校验数据。 -* 在 OpenAPI 的*路径操作*中为响应添加一个 JSON Schema。 -* 并在自动生成文档系统中使用。 +/// 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 字段的东西添加类型注解时,可能需要这样做,下面的章节会有示例。 -{* ../../docs_src/response_model/tutorial002.py hl[17:18] *} +## 返回与输入相同的数据 { #return-the-same-input-data } -现在,每当浏览器使用一个密码创建用户时,API 都会在响应中返回相同的密码。 +这里我们声明一个 `UserIn` 模型,它包含一个明文密码: -在这个案例中,这可能不算是问题,因为用户自己正在发送密码。 +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} -但是,如果我们在其他的*路径操作*中使用相同的模型,则可能会将用户的密码发送给每个客户端。 +/// info | 信息 -/// danger +要使用 `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_py310.py hl[16] *} + +现在,每当浏览器使用密码创建用户时,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] *} -...我们已经将 `response_model` 声明为了不包含密码的 `UserOut` 模型: +……我们仍将 `response_model` 声明为不包含密码的 `UserOut` 模型: {* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -因此,**FastAPI** 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)。 +因此,**FastAPI** 会负责过滤掉输出模型中未声明的所有数据(使用 Pydantic)。 -## 在文档中查看 +### `response_model` 还是返回类型 { #response-model-or-return-type } -当你查看自动化文档时,你可以检查输入模型和输出模型是否都具有自己的 JSON Schema: +在这个例子中,因为两个模型不同,如果我们将函数返回类型注解为 `UserOut`,编辑器和工具会抱怨我们返回了无效类型,因为它们是不同的类。 - +这就是为什么在这个例子里我们必须在 `response_model` 参数中声明它。 -并且两种模型都将在交互式 API 文档中使用: +……但继续往下读,看看如何更好地处理这种情况。 - +## 返回类型与数据过滤 { #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` 时,返回 `UserIn` 是*合法*的。 + +### FastAPI 的数据过滤 { #fastapi-data-filtering } + +对于 FastAPI,它会查看返回类型并确保你返回的内容**只**包含该类型中声明的字段。 + +FastAPI 在内部配合 Pydantic 做了多项处理,确保不会把类继承的这些规则用于返回数据的过滤,否则你可能会返回比预期多得多的数据。 + +这样,你就能兼得两方面的优势:带有**工具支持**的类型注解和**数据过滤**。 + +## 在文档中查看 { #see-it-in-the-docs } + +当你查看自动文档时,你会看到输入模型和输出模型都会有各自的 JSON Schema: + + + +并且两个模型都会用于交互式 API 文档: + + + +## 其他返回类型注解 { #other-return-type-annotations } + +有些情况下你会返回一些不是有效 Pydantic 字段的内容,并在函数上做了相应注解,只是为了获得工具链(编辑器、mypy 等)的支持。 + +### 直接返回 Response { #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] *} + +这个简单场景 FastAPI 会自动处理,因为返回类型注解是 `Response`(或其子类)。 + +工具也会满意,因为 `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 类型,也会发生同样的情况,例如这个会失败 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +……它失败是因为该类型注解不是 Pydantic 类型,也不只是单个 `Response` 类或其子类,而是 `Response` 与 `dict` 的联合类型(任意其一)。 + +### 禁用响应模型 { #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: Union[str, None] = None` 具有默认值 `None`。 -* `tax: float = 10.5` 具有默认值 `10.5`. -* `tags: List[str] = []` 具有一个空列表作为默认值: `[]`. +* `description: Union[str, None] = None`(或在 Python 3.10 中的 `str | None = None`)默认值为 `None`。 +* `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.py hl[24] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -然后响应中将不会包含那些默认值,而是仅有实际设置的值。 +这样响应中将不会包含那些默认值,而只包含实际设置的值。 -因此,如果你向*路径操作*发送 ID 为 `foo` 的商品的请求,则响应(不包括默认值)将为: +因此,如果你向该*路径操作*请求 ID 为 `foo` 的商品,响应(不包括默认值)将为: ```JSON { @@ -114,24 +250,18 @@ FastAPI 将使用此 `response_model` 来: } ``` -/// info - -FastAPI 通过 Pydantic 模型的 `.dict()` 配合 该方法的 `exclude_unset` 参数 来实现此功能。 - -/// - -/// info +/// info | 信息 你还可以使用: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -参考 Pydantic 文档 中对 `exclude_defaults` 和 `exclude_none` 的描述。 +详见 Pydantic 文档中对 `exclude_defaults` 和 `exclude_none` 的说明。 /// -#### 默认值字段有实际值的数据 +#### 默认字段有实际值的数据 { #data-with-values-for-fields-with-defaults } 但是,如果你的数据在具有默认值的模型字段中有实际的值,例如 ID 为 `bar` 的项: @@ -146,7 +276,7 @@ FastAPI 通过 Pydantic 模型的 `.dict()` 配合 `http.HTTPStatus`。 @@ -31,7 +31,7 @@ -/// note | 笔记 +/// note | 注意 某些响应状态码表示响应没有响应体(参阅下一章)。 @@ -39,9 +39,9 @@ FastAPI 可以进行识别,并生成表明无响应体的 OpenAPI 文档。 /// -## 关于 HTTP 状态码 +## 关于 HTTP 状态码 { #about-http-status-codes } -/// note | 笔记 +/// note | 注意 如果已经了解 HTTP 状态码,请跳到下一章。 @@ -53,40 +53,40 @@ FastAPI 可以进行识别,并生成表明无响应体的 OpenAPI 文档。 简言之: -* `100` 及以上的状态码用于返回**信息**。这类状态码很少直接使用。具有这些状态码的响应不能包含响应体 -* **`200`** 及以上的状态码用于表示**成功**。这些状态码是最常用的 - * `200` 是默认状态代码,表示一切**正常** - * `201` 表示**已创建**,通常在数据库中创建新记录后使用 - * `204` 是一种特殊的例子,表示**无内容**。该响应在没有为客户端返回内容时使用,因此,该响应不能包含响应体 -* **`300`** 及以上的状态码用于**重定向**。具有这些状态码的响应不一定包含响应体,但 `304`**未修改**是个例外,该响应不得包含响应体 -* **`400`** 及以上的状态码用于表示**客户端错误**。这些可能是第二常用的类型 - * `404`,用于**未找到**响应 +* `100 - 199` 用于返回“信息”。这类状态码很少直接使用。具有这些状态码的响应不能包含响应体 +* **`200 - 299`** 用于表示“成功”。这些状态码是最常用的 + * `200` 是默认状态码,表示一切“OK” + * `201` 表示“已创建”,通常在数据库中创建新记录后使用 + * `204` 是一种特殊的例子,表示“无内容”。该响应在没有为客户端返回内容时使用,因此,该响应不能包含响应体 +* **`300 - 399`** 用于“重定向”。具有这些状态码的响应不一定包含响应体,但 `304`“未修改”是个例外,该响应不得包含响应体 +* **`400 - 499`** 用于表示“客户端错误”。这些可能是第二常用的类型 + * `404`,用于“未找到”响应 * 对于来自客户端的一般错误,可以只使用 `400` -* `500` 及以上的状态码用于表示服务器端错误。几乎永远不会直接使用这些状态码。应用代码或服务器出现问题时,会自动返回这些状态代码 +* `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] *} 这只是一种快捷方式,具有相同的数字代码,但它可以使用编辑器的自动补全功能: - + /// note | 技术细节 @@ -96,6 +96,6 @@ FastAPI 可以进行识别,并生成表明无响应体的 OpenAPI 文档。 /// -## 更改默认状态码 +## 更改默认状态码 { #changing-the-default } [高级用户指南](../advanced/response-change-status-code.md){.internal-link target=_blank}中,将介绍如何返回与在此声明的默认状态码不同的状态码。 diff --git a/docs/zh/docs/tutorial/schema-extra-example.md b/docs/zh/docs/tutorial/schema-extra-example.md index 6c132eed3..818ff5087 100644 --- a/docs/zh/docs/tutorial/schema-extra-example.md +++ b/docs/zh/docs/tutorial/schema-extra-example.md @@ -1,55 +1,202 @@ -# 模式的额外信息 - 例子 +# 声明请求示例数据 { #declare-request-example-data } -您可以在JSON模式中定义额外的信息。 +你可以为你的应用将接收的数据声明示例。 -一个常见的用例是添加一个将在文档中显示的`example`。 +这里有几种实现方式。 -有几种方法可以声明额外的 JSON 模式信息。 +## Pydantic 模型中的额外 JSON Schema 数据 { #extra-json-schema-data-in-pydantic-models } -## Pydantic `schema_extra` +你可以为一个 Pydantic 模型声明 `examples`,它们会被添加到生成的 JSON Schema 中。 -您可以使用 `Config` 和 `schema_extra` 为Pydantic模型声明一个示例,如Pydantic 文档:定制 Schema 中所述: +{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:21] *} +这些额外信息会原样添加到该模型输出的 JSON Schema 中,并会在 API 文档中使用。 -这些额外的信息将按原样添加到输出的JSON模式中。 +你可以使用属性 `model_config`,它接收一个 `dict`,详见 Pydantic 文档:配置。 -## `Field` 的附加参数 +你可以设置 `"json_schema_extra"`,其值为一个 `dict`,包含你希望出现在生成 JSON Schema 中的任意附加数据,包括 `examples`。 -在 `Field`, `Path`, `Query`, `Body` 和其他你之后将会看到的工厂函数,你可以为JSON 模式声明额外信息,你也可以通过给工厂函数传递其他的任意参数来给JSON 模式声明额外信息,比如增加 `example`: +/// tip | 提示 -{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} +你也可以用同样的技巧扩展 JSON Schema,添加你自己的自定义额外信息。 -/// warning - -请记住,传递的那些额外参数不会添加任何验证,只会添加注释,用于文档的目的。 +例如,你可以用它为前端用户界面添加元数据等。 /// -## `Body` 额外参数 +/// info | 信息 -你可以通过传递额外信息给 `Field` 同样的方式操作`Path`, `Query`, `Body`等。 +OpenAPI 3.1.0(自 FastAPI 0.99.0 起使用)增加了对 `examples` 的支持,它是 JSON Schema 标准的一部分。 -比如,你可以将请求体的一个 `example` 传递给 `Body`: +在此之前,只支持使用单个示例的关键字 `example`。OpenAPI 3.1.0 仍然支持它,但它已被弃用,并不属于 JSON Schema 标准。因此,建议你把 `example` 迁移到 `examples`。🤓 -{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:27] *} +你可以在本页末尾阅读更多内容。 -## 文档 UI 中的例子 +/// -使用上面的任何方法,它在 `/docs` 中看起来都是这样的: +## `Field` 的附加参数 { #field-additional-arguments } + +在 Pydantic 模型中使用 `Field()` 时,你也可以声明额外的 `examples`: + +{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} + +## JSON Schema 中的 `examples` - OpenAPI { #examples-in-json-schema-openapi } + +在以下任意场景中使用: + +- `Path()` +- `Query()` +- `Header()` +- `Cookie()` +- `Body()` +- `Form()` +- `File()` + +你也可以声明一组 `examples`,这些带有附加信息的示例将被添加到它们在 OpenAPI 中的 JSON Schema 里。 + +### 带有 `examples` 的 `Body` { #body-with-examples } + +这里我们向 `Body()` 传入 `examples`,其中包含一个期望的数据示例: + +{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} + +### 文档 UI 中的示例 { #example-in-the-docs-ui } + +使用上述任一方法,在 `/docs` 中看起来会是这样: -## 技术细节 +### 带有多个 `examples` 的 `Body` { #body-with-multiple-examples } -关于 `example` 和 `examples`... +当然,你也可以传入多个 `examples`: -JSON Schema在最新的一个版本中定义了一个字段 `examples` ,但是 OpenAPI 基于之前的一个旧版JSON Schema,并没有 `examples`. +{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -所以 OpenAPI为了相似的目的定义了自己的 `example` (使用 `example`, 而不是 `examples`), 这也是文档 UI 所使用的 (使用 Swagger UI). +这样做时,这些示例会成为该请求体数据内部 JSON Schema 的一部分。 -所以,虽然 `example` 不是JSON Schema的一部分,但它是OpenAPI的一部分,这将被文档UI使用。 +不过,在撰写本文时,用于展示文档 UI 的 Swagger UI 并不支持显示 JSON Schema 中数据的多个示例。但请继续阅读,下面有一种变通方法。 -## 其他信息 +### OpenAPI 特定的 `examples` { #openapi-specific-examples } -同样的方法,你可以添加你自己的额外信息,这些信息将被添加到每个模型的JSON模式中,例如定制前端用户界面,等等。 +在 JSON Schema 支持 `examples` 之前,OpenAPI 就已支持一个同名但不同的字段 `examples`。 + +这个面向 OpenAPI 的 `examples` 位于 OpenAPI 规范的另一处。它放在每个路径操作的详细信息中,而不是每个 JSON Schema 里。 + +而 Swagger UI 早就支持这个特定的 `examples` 字段。因此,你可以用它在文档 UI 中展示不同的示例。 + +这个 OpenAPI 特定字段 `examples` 的结构是一个包含多个示例的 `dict`(而不是一个 `list`),每个示例都包含会被添加到 OpenAPI 的额外信息。 + +这不放在 OpenAPI 内部包含的各个 JSON Schema 里,而是直接放在路径操作上。 + +### 使用 `openapi_examples` 参数 { #using-the-openapi-examples-parameter } + +你可以在 FastAPI 中通过参数 `openapi_examples` 来声明这个 OpenAPI 特定的 `examples`,适用于: + +- `Path()` +- `Query()` +- `Header()` +- `Cookie()` +- `Body()` +- `Form()` +- `File()` + +这个 `dict` 的键用于标识每个示例,每个值是另一个 `dict`。 + +`examples` 中每个具体示例的 `dict` 可以包含: + +- `summary`:该示例的简短描述。 +- `description`:较长描述,可以包含 Markdown 文本。 +- `value`:实际展示的示例,例如一个 `dict`。 +- `externalValue`:`value` 的替代项,指向该示例的 URL。不过它的工具支持度可能不如 `value`。 + +你可以这样使用: + +{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} + +### 文档 UI 中的 OpenAPI 示例 { #openapi-examples-in-the-docs-ui } + +当把 `openapi_examples` 添加到 `Body()` 后,`/docs` 会如下所示: + + + +## 技术细节 { #technical-details } + +/// tip | 提示 + +如果你已经在使用 FastAPI 版本 0.99.0 或更高版本,你大概率可以跳过这些细节。 + +它们对更早版本(OpenAPI 3.1.0 尚不可用之前)更相关。 + +你可以把这当作一堂简短的 OpenAPI 和 JSON Schema 历史课。🤓 + +/// + +/// warning | 警告 + +以下是关于 JSON Schema 和 OpenAPI 标准的非常技术性的细节。 + +如果上面的思路对你已经足够可用,你可能不需要这些细节,可以直接跳过。 + +/// + +在 OpenAPI 3.1.0 之前,OpenAPI 使用的是一个更旧且经过修改的 JSON Schema 版本。 + +当时 JSON Schema 没有 `examples`,所以 OpenAPI 在它修改过的版本中添加了自己的 `example` 字段。 + +OpenAPI 还在规范的其他部分添加了 `example` 和 `examples` 字段: + +- `Parameter Object`(规范中),被 FastAPI 的以下内容使用: + - `Path()` + - `Query()` + - `Header()` + - `Cookie()` +- `Request Body Object` 中的 `content` 字段里的 `Media Type Object`(规范中),被 FastAPI 的以下内容使用: + - `Body()` + - `File()` + - `Form()` + +/// info | 信息 + +这个旧的、OpenAPI 特定的 `examples` 参数,自 FastAPI `0.103.0` 起改名为 `openapi_examples`。 + +/// + +### JSON Schema 的 `examples` 字段 { #json-schemas-examples-field } + +后来,JSON Schema 在新版本的规范中添加了 `examples` 字段。 + +随后新的 OpenAPI 3.1.0 基于最新版本(JSON Schema 2020-12),其中包含了这个新的 `examples` 字段。 + +现在,这个新的 `examples` 字段优先于旧的单个(且自定义的)`example` 字段,后者已被弃用。 + +JSON Schema 中这个新的 `examples` 字段只是一个由示例组成的 `list`,而不是像上面提到的 OpenAPI 其他位置那样带有额外元数据的 `dict`。 + +/// info | 信息 + +即使在 OpenAPI 3.1.0 发布、并与 JSON Schema 有了这种更简单的集成之后,有一段时间里,提供自动文档的 Swagger UI 并不支持 OpenAPI 3.1.0(它自 5.0.0 版本起已支持 🎉)。 + +因此,FastAPI 0.99.0 之前的版本仍然使用低于 3.1.0 的 OpenAPI 版本。 + +/// + +### Pydantic 与 FastAPI 的 `examples` { #pydantic-and-fastapi-examples } + +当你在 Pydantic 模型中添加 `examples`,通过 `schema_extra` 或 `Field(examples=["something"])`,这些示例会被添加到该 Pydantic 模型的 JSON Schema 中。 + +这个 Pydantic 模型的 JSON Schema 会被包含到你的 API 的 OpenAPI 中,然后在文档 UI 中使用。 + +在 FastAPI 0.99.0 之前的版本(0.99.0 及以上使用更新的 OpenAPI 3.1.0),当你在其他工具(`Query()`、`Body()` 等)中使用 `example` 或 `examples` 时,这些示例不会被添加到描述该数据的 JSON Schema 中(甚至不会添加到 OpenAPI 自己的 JSON Schema 版本中),而是会直接添加到 OpenAPI 的路径操作声明中(在 OpenAPI 使用 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-and-openapi-specific-examples } + +此前,由于 Swagger UI 不支持多个 JSON Schema 示例(截至 2023-08-26),用户无法在文档中展示多个示例。 + +为了解决这个问题,FastAPI `0.103.0` 通过新增参数 `openapi_examples`,为声明同样的旧式 OpenAPI 特定 `examples` 字段提供了支持。🤓 + +### 总结 { #summary } + +我曾经说我不太喜欢历史……结果现在在这儿上“技术史”课。😅 + +简而言之,升级到 FastAPI 0.99.0 或更高版本,一切会更简单、一致、直观,你也不必了解这些历史细节。😎 diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md index 225eb2695..43b7c6657 100644 --- a/docs/zh/docs/tutorial/security/first-steps.md +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -1,197 +1,203 @@ -# 安全 - 第一步 +# 安全 - 第一步 { #security-first-steps } -假设**后端** API 在某个域。 +假设你的**后端** API 位于某个域名下。 -**前端**在另一个域,或(移动应用中)在同一个域的不同路径下。 +而**前端**在另一个域名,或同一域名的不同路径(或在移动应用中)。 -并且,前端要使用后端的 **username** 与 **password** 验证用户身份。 +你希望前端能通过**username** 和 **password** 与后端进行身份验证。 -固然,**FastAPI** 支持 **OAuth2** 身份验证。 +我们可以用 **OAuth2** 在 **FastAPI** 中实现它。 -但为了节省开发者的时间,不要只为了查找很少的内容,不得不阅读冗长的规范文档。 +但为了节省你的时间,不必为获取少量信息而通读冗长的规范。 -我们建议使用 **FastAPI** 的安全工具。 +我们直接使用 **FastAPI** 提供的安全工具。 -## 概览 +## 效果预览 { #how-it-looks } -首先,看看下面的代码是怎么运行的,然后再回过头来了解其背后的原理。 +先直接运行代码看看效果,之后再回过头理解其背后的原理。 -## 创建 `main.py` +## 创建 `main.py` { #create-main-py } 把下面的示例代码复制到 `main.py`: {* ../../docs_src/security/tutorial001_an_py39.py *} -## 运行 +## 运行 { #run-it } -/// info | 说明 +/// info | 信息 -先安装 `python-multipart`。 +当你使用命令 `pip install "fastapi[standard]"` 安装 **FastAPI** 时,`python-multipart` 包会自动安装。 -安装命令: `pip install python-multipart`。 +但是,如果你使用 `pip install fastapi`,默认不会包含 `python-multipart` 包。 -这是因为 **OAuth2** 使用**表单数据**发送 `username` 与 `password`。 +如需手动安装,请先创建并激活[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},然后执行: + +```console +$ pip install python-multipart +``` + +这是因为 **OAuth2** 使用“表单数据”来发送 `username` 和 `password`。 /// -用下面的命令运行该示例: +用下面的命令运行示例:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-## 查看文档 +## 查看 { #check-it } -打开 API 文档: http://127.0.0.1:8000/docs。 +打开交互式文档:http://127.0.0.1:8000/docs。 -界面如下图所示: +你会看到类似这样的界面: /// check | Authorize 按钮! -页面右上角出现了一个「**Authorize**」按钮。 +页面右上角已经有一个崭新的“Authorize”按钮。 -*路径操作*的右上角也出现了一个可以点击的小锁图标。 +你的*路径操作*右上角还有一个可点击的小锁图标。 /// -点击 **Authorize** 按钮,弹出授权表单,输入 `username` 与 `password` 及其它可选字段: +点击它,会弹出一个授权表单,可输入 `username` 和 `password`(以及其它可选字段): -/// note | 笔记 +/// note | 注意 -目前,在表单中输入内容不会有任何反应,后文会介绍相关内容。 +目前无论在表单中输入什么都不会生效,我们稍后就会实现它。 /// -虽然此文档不是给前端最终用户使用的,但这个自动工具非常实用,可在文档中与所有 API 交互。 +这当然不是面向最终用户的前端,但它是一个很棒的自动化工具,可交互式地为整个 API 提供文档。 -前端团队(可能就是开发者本人)可以使用本工具。 +前端团队(也可能就是你自己)可以使用它。 -第三方应用与系统也可以调用本工具。 +第三方应用和系统也可以使用它。 -开发者也可以用它来调试、检查、测试应用。 +你也可以用它来调试、检查和测试同一个应用。 -## 密码流 +## `password` 流 { #the-password-flow } -现在,我们回过头来介绍这段代码的原理。 +现在回过头来理解这些内容。 -`Password` **流**是 OAuth2 定义的,用于处理安全与身份验证的方式(**流**)。 +`password` “流”(flow)是 OAuth2 定义的处理安全与身份验证的一种方式。 -OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户身份。 +OAuth2 的设计目标是让后端或 API 与负责用户认证的服务器解耦。 -但在本例中,**FastAPI** 应用会处理 API 与身份验证。 +但在这个例子中,**FastAPI** 应用同时处理 API 和认证。 -下面,我们来看一下简化的运行流程: +从这个简化的角度来看看流程: -- 用户在前端输入 `username` 与`password`,并点击**回车** -- (用户浏览器中运行的)前端把 `username` 与`password` 发送至 API 中指定的 URL(使用 `tokenUrl="token"` 声明) -- API 检查 `username` 与`password`,并用令牌(`Token`) 响应(暂未实现此功能): - - 令牌只是用于验证用户的字符串 - - 一般来说,令牌会在一段时间后过期 - - 过时后,用户要再次登录 - - 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,**在绝大多数情况下**不会长期有效 -- 前端临时将令牌存储在某个位置 -- 用户点击前端,前往前端应用的其它部件 -- 前端需要从 API 中提取更多数据: - - 为指定的端点(Endpoint)进行身份验证 - - 因此,用 API 验证身份时,要发送值为 `Bearer` + 令牌的请求头 `Authorization` - - 假如令牌为 `foobar`,`Authorization` 请求头就是: `Bearer foobar` +* 用户在前端输入 `username` 和 `password`,然后按下 `Enter`。 +* 前端(运行在用户浏览器中)把 `username` 和 `password` 发送到我们 API 中的特定 URL(使用 `tokenUrl="token"` 声明)。 +* API 校验 `username` 和 `password`,并返回一个“令牌”(这些我们尚未实现)。 + * “令牌”只是一个字符串,包含一些内容,之后可用来验证该用户。 + * 通常,令牌会在一段时间后过期。 + * 因此,用户过一段时间需要重新登录。 + * 如果令牌被窃取,风险也更小。它不像一把永久有效的钥匙(在大多数情况下)。 +* 前端会把令牌临时存储在某处。 +* 用户在前端中点击跳转到前端应用的其他部分。 +* 前端需要从 API 获取更多数据。 + * 但该端点需要身份验证。 + * 因此,为了与我们的 API 进行身份验证,它会发送一个 `Authorization` 请求头,值为 `Bearer ` 加上令牌。 + * 如果令牌内容是 `foobar`,`Authorization` 请求头的内容就是:`Bearer foobar`。 -## **FastAPI** 的 `OAuth2PasswordBearer` +## **FastAPI** 的 `OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer } -**FastAPI** 提供了不同抽象级别的安全工具。 +**FastAPI** 在不同抽象层级提供了多种安全工具。 -本例使用 **OAuth2** 的 **Password** 流以及 **Bearer** 令牌(`Token`)。为此要使用 `OAuth2PasswordBearer` 类。 +本示例将使用 **OAuth2** 的 **Password** 流程并配合 **Bearer** 令牌,通过 `OAuth2PasswordBearer` 类来实现。 -/// info | 说明 +/// info | 信息 -`Bearer` 令牌不是唯一的选择。 +“Bearer” 令牌并非唯一选项。 -但它是最适合这个用例的方案。 +但它非常适合我们的用例。 -甚至可以说,它是适用于绝大多数用例的最佳方案,除非您是 OAuth2 的专家,知道为什么其它方案更合适。 +对于大多数用例,它也可能是最佳选择,除非你是 OAuth2 专家,并明确知道为何其他方案更适合你的需求。 -本例中,**FastAPI** 还提供了构建工具。 +在那种情况下,**FastAPI** 同样提供了相应的构建工具。 /// -创建 `OAuth2PasswordBearer` 的类实例时,要传递 `tokenUrl` 参数。该参数包含客户端(用户浏览器中运行的前端) 的 URL,用于发送 `username` 与 `password`,并获取令牌。 +创建 `OAuth2PasswordBearer` 类实例时,需要传入 `tokenUrl` 参数。该参数包含客户端(运行在用户浏览器中的前端)用来发送 `username` 和 `password` 以获取令牌的 URL。 -{* ../../docs_src/security/tutorial001.py hl[6] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} /// tip | 提示 -在此,`tokenUrl="token"` 指向的是暂未创建的相对 URL `token`。这个相对 URL 相当于 `./token`。 +这里的 `tokenUrl="token"` 指向的是尚未创建的相对 URL `token`,等价于 `./token`。 -因为使用的是相对 URL,如果 API 位于 `https://example.com/`,则指向 `https://example.com/token`。但如果 API 位于 `https://example.com/api/v1/`,它指向的就是`https://example.com/api/v1/token`。 +因为使用的是相对 URL,若你的 API 位于 `https://example.com/`,它将指向 `https://example.com/token`;若你的 API 位于 `https://example.com/api/v1/`,它将指向 `https://example.com/api/v1/token`。 -使用相对 URL 非常重要,可以确保应用在遇到[使用代理](../../advanced/behind-a-proxy.md){.internal-link target=_blank}这样的高级用例时,也能正常运行。 +使用相对 URL 很重要,这能确保你的应用在诸如[使用代理](../../advanced/behind-a-proxy.md){.internal-link target=_blank}等高级用例中依然正常工作。 /// -该参数不会创建端点或*路径操作*,但会声明客户端用来获取令牌的 URL `/token` 。此信息用于 OpenAPI 及 API 文档。 +这个参数不会创建该端点/*路径操作*,而是声明客户端应使用 `/token` 这个 URL 来获取令牌。这些信息会用于 OpenAPI,进而用于交互式 API 文档系统。 -接下来,学习如何创建实际的路径操作。 +我们很快也会创建对应的实际路径操作。 -/// info | 说明 +/// info | 信息 -严苛的 **Pythonista** 可能不喜欢用 `tokenUrl` 这种命名风格代替 `token_url`。 +如果你是非常严格的 “Pythonista”,可能不喜欢使用参数名 `tokenUrl` 而不是 `token_url`。 -这种命名方式是因为要使用与 OpenAPI 规范中相同的名字。以便在深入校验安全方案时,能通过复制粘贴查找更多相关信息。 +这是因为它使用了与 OpenAPI 规范中相同的名称。这样当你需要深入了解这些安全方案时,可以直接复制粘贴去查找更多信息。 /// -`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的实例,也是**可调用项**。 +`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的一个实例,同时它也是“可调用”的。 -以如下方式调用: +可以像这样调用: ```Python oauth2_scheme(some, parameters) ``` -因此,`Depends` 可以调用 `oauth2_scheme` 变量。 +因此,它可以与 `Depends` 一起使用。 -### 使用 +### 使用 { #use-it } -接下来,使用 `Depends` 把 `oauth2_scheme` 传入依赖项。 +现在你可以通过 `Depends` 将 `oauth2_scheme` 作为依赖传入。 -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -该依赖项使用字符串(`str`)接收*路径操作函数*的参数 `token` 。 +该依赖会提供一个 `str`,赋值给*路径操作函数*的参数 `token`。 -**FastAPI** 使用依赖项在 OpenAPI 概图(及 API 文档)中定义**安全方案**。 +**FastAPI** 会据此在 OpenAPI 架构(以及自动生成的 API 文档)中定义一个“安全方案”。 /// info | 技术细节 -**FastAPI** 使用(在依赖项中声明的)类 `OAuth2PasswordBearer` 在 OpenAPI 中定义安全方案,这是因为它继承自 `fastapi.security.oauth2.OAuth2`,而该类又是继承自`fastapi.security.base.SecurityBase`。 +**FastAPI** 之所以知道可以使用(在依赖中声明的)`OAuth2PasswordBearer` 在 OpenAPI 中定义安全方案,是因为它继承自 `fastapi.security.oauth2.OAuth2`,而后者又继承自 `fastapi.security.base.SecurityBase`。 -所有与 OpenAPI(及 API 文档)集成的安全工具都继承自 `SecurityBase`, 这就是为什么 **FastAPI** 能把它们集成至 OpenAPI 的原因。 +所有与 OpenAPI(以及自动 API 文档)集成的安全工具都继承自 `SecurityBase`,这就是 **FastAPI** 能将它们集成到 OpenAPI 的方式。 /// -## 实现的操作 +## 它做了什么 { #what-it-does } -FastAPI 校验请求中的 `Authorization` 请求头,核对请求头的值是不是由 `Bearer ` + 令牌组成, 并返回令牌字符串(`str`)。 +它会在请求中查找 `Authorization` 请求头,检查其值是否为 `Bearer ` 加上一些令牌,并将该令牌作为 `str` 返回。 -如果没有找到 `Authorization` 请求头,或请求头的值不是 `Bearer ` + 令牌。FastAPI 直接返回 401 错误状态码(`UNAUTHORIZED`)。 +如果没有 `Authorization` 请求头,或者其值不包含 `Bearer ` 令牌,它会直接返回 401 状态码错误(`UNAUTHORIZED`)。 -开发者不需要检查错误信息,查看令牌是否存在,只要该函数能够执行,函数中就会包含令牌字符串。 +你甚至无需检查令牌是否存在即可返回错误;只要你的函数被执行,就可以确定会拿到一个 `str` 类型的令牌。 -正如下图所示,API 文档已经包含了这项功能: +你已经可以在交互式文档中试试了: -目前,暂时还没有实现验证令牌是否有效的功能,不过后文很快就会介绍的。 +我们还没有验证令牌是否有效,但这已经是一个良好的开端。 -## 小结 +## 小结 { #recap } -看到了吧,只要多写三四行代码,就可以添加基础的安全表单。 +只需增加三四行代码,你就已经拥有了一种初步的安全机制。 diff --git a/docs/zh/docs/tutorial/security/get-current-user.md b/docs/zh/docs/tutorial/security/get-current-user.md index 1f254a103..c14bba28a 100644 --- a/docs/zh/docs/tutorial/security/get-current-user.md +++ b/docs/zh/docs/tutorial/security/get-current-user.md @@ -1,23 +1,23 @@ -# 获取当前用户 +# 获取当前用户 { #get-current-user } 上一章中,(基于依赖注入系统的)安全系统向*路径操作函数*传递了 `str` 类型的 `token`: -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} 但这并不实用。 接下来,我们学习如何返回当前用户。 -## 创建用户模型 +## 创建用户模型 { #create-a-user-model } 首先,创建 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` 依赖项。 @@ -27,19 +27,19 @@ 与之前直接在路径操作中的做法相同,新的 `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` 模型: -{* ../../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 } 在*路径操作* 的 `Depends` 中使用 `get_current_user`: -{* ../../docs_src/security/tutorial002.py hl[31] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} 注意,此处把 `current_user` 的类型声明为 Pydantic 的 `User` 模型。 @@ -61,7 +61,7 @@ /// -## 其它模型 +## 其它模型 { #other-models } 接下来,直接在*路径操作函数*中获取当前用户,并用 `Depends` 在**依赖注入**系统中处理安全机制。 @@ -78,7 +78,7 @@ 尽管使用应用所需的任何模型、类、数据库。**FastAPI** 通过依赖注入系统都能帮您搞定。 -## 代码大小 +## 代码大小 { #code-size } 这个示例看起来有些冗长。毕竟这个文件同时包含了安全、数据模型的工具函数,以及路径操作等代码。 @@ -94,9 +94,9 @@ 所有*路径操作*只需 3 行代码就可以了: -{* ../../docs_src/security/tutorial002.py hl[30:32] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} -## 小结 +## 小结 { #recap } 现在,我们可以直接在*路径操作函数*中获取当前用户。 diff --git a/docs/zh/docs/tutorial/security/index.md b/docs/zh/docs/tutorial/security/index.md index 1484b99fd..589f93c3e 100644 --- a/docs/zh/docs/tutorial/security/index.md +++ b/docs/zh/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 完全不同,并且更为复杂,因为它直接包含了有关如何加密通信的规范。 @@ -32,13 +32,13 @@ OAuth2是一个规范,它定义了几种处理身份认证和授权的方法 OAuth2 没有指定如何加密通信,它期望你为应用程序使用 HTTPS 进行通信。 -/// tip +/// tip | 提示 在有关**部署**的章节中,你将了解如何使用 Traefik 和 Let's Encrypt 免费设置 HTTPS。 /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect 是另一个基于 **OAuth2** 的规范。 @@ -48,7 +48,7 @@ OpenID Connect 是另一个基于 **OAuth2** 的规范。 但是 Facebook 登录不支持 OpenID Connect。它具有自己的 OAuth2 风格。 -### OpenID(非「OpenID Connect」) +### OpenID(非「OpenID Connect」) { #openid-not-openid-connect } 还有一个「OpenID」规范。它试图解决与 **OpenID Connect** 相同的问题,但它不是基于 OAuth2。 @@ -56,7 +56,7 @@ OpenID Connect 是另一个基于 **OAuth2** 的规范。 如今它已经不是很流行,没有被广泛使用了。 -## OpenAPI +## OpenAPI { #openapi } OpenAPI(以前称为 Swagger)是用于构建 API 的开放规范(现已成为 Linux Foundation 的一部分)。 @@ -75,7 +75,7 @@ OpenAPI 定义了以下安全方案: * 请求头。 * cookie。 * `http`:标准的 HTTP 身份认证系统,包括: - * `bearer`: 一个值为 `Bearer` 加令牌字符串的 `Authorization` 请求头。这是从 OAuth2 继承的。 + * `bearer`: 一个值为 `Bearer ` 加令牌字符串的 `Authorization` 请求头。这是从 OAuth2 继承的。 * HTTP Basic 认证方式。 * HTTP Digest,等等。 * `oauth2`:所有的 OAuth2 处理安全性的方式(称为「流程」)。 @@ -89,7 +89,7 @@ OpenAPI 定义了以下安全方案: * 此自动发现机制是 OpenID Connect 规范中定义的内容。 -/// tip +/// tip | 提示 集成其他身份认证/授权提供者(例如Google,Facebook,X (Twitter),GitHub等)也是可能的,而且较为容易。 @@ -97,10 +97,10 @@ OpenAPI 定义了以下安全方案: /// -## **FastAPI** 实用工具 +## **FastAPI** 实用工具 { #fastapi-utilities } FastAPI 在 `fastapi.security` 模块中为每个安全方案提供了几种工具,这些工具简化了这些安全机制的使用方法。 -在下一章中,你将看到如何使用 **FastAPI** 所提供的这些工具为你的 API 增加安全性。 +在接下来的章节中,你将看到如何使用 **FastAPI** 所提供的这些工具为你的 API 增加安全性。 而且你还将看到它如何自动地被集成到交互式文档系统中。 diff --git a/docs/zh/docs/tutorial/security/oauth2-jwt.md b/docs/zh/docs/tutorial/security/oauth2-jwt.md index 7d338419b..c7eb9bd90 100644 --- a/docs/zh/docs/tutorial/security/oauth2-jwt.md +++ b/docs/zh/docs/tutorial/security/oauth2-jwt.md @@ -1,34 +1,36 @@ -# OAuth2 实现密码哈希与 Bearer JWT 令牌验证 +# 使用密码(及哈希)的 OAuth2,基于 JWT 的 Bearer 令牌 { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } -至此,我们已经编写了所有安全流,本章学习如何使用 JWT 令牌(Token)和安全密码哈希(Hash)实现真正的安全机制。 +现在我们已经有了完整的安全流程,接下来用 JWT 令牌和安全的密码哈希,让应用真正安全起来。 -本章的示例代码真正实现了在应用的数据库中保存哈希密码等功能。 +这些代码可以直接用于你的应用,你可以把密码哈希保存到数据库中,等等。 -接下来,我们紧接上一章,继续完善安全机制。 +我们将从上一章结束的地方继续,逐步完善。 -## JWT 简介 +## 关于 JWT { #about-jwt } -JWT 即**JSON 网络令牌**(JSON Web Tokens)。 +JWT 意为 “JSON Web Tokens”。 -JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示: +它是一种标准,把一个 JSON 对象编码成没有空格、很密集的一长串字符串。看起来像这样: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` -JWT 字符串没有加密,任何人都能用它恢复原始信息。 +它不是加密的,所以任何人都可以从内容中恢复信息。 -但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。 +但它是“签名”的。因此,当你收到一个自己签发的令牌时,你可以验证它确实是你签发的。 -使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。 +这样你就可以创建一个例如有效期为 1 周的令牌。然后当用户第二天带着这个令牌回来时,你能知道该用户仍然处于登录状态。 -令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。 +一周后令牌过期,用户将不再被授权,需要重新登录以获取新令牌。而如果用户(或第三方)尝试修改令牌来更改过期时间,你也能发现,因为签名将不匹配。 -如需深入了解 JWT 令牌,了解它的工作方式,请参阅 https://jwt.io。 +如果你想动手体验 JWT 令牌并了解它的工作方式,请访问 https://jwt.io。 -## 安装 `PyJWT` +## 安装 `PyJWT` { #install-pyjwt } -安装 `PyJWT`,在 Python 中生成和校验 JWT 令牌: +我们需要安装 `PyJWT`,以便在 Python 中生成和校验 JWT 令牌。 + +请确保创建并激活一个[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},然后安装 `pyjwt`:
@@ -40,42 +42,42 @@ $ pip install pyjwt
-/// info | 说明 +/// info | 信息 -如果您打算使用类似 RSA 或 ECDSA 的数字签名算法,您应该安装加密库依赖项 `pyjwt[crypto]`。 +如果你计划使用类似 RSA 或 ECDSA 的数字签名算法,你应该安装加密库依赖项 `pyjwt[crypto]`。 -您可以在 PyJWT Installation docs 获得更多信息。 +可以在 PyJWT 安装文档中了解更多。 /// -## 密码哈希 +## 密码哈希 { #password-hashing } -**哈希**是指把特定内容(本例中为密码)转换为乱码形式的字节序列(其实就是字符串)。 +“哈希”是指把一些内容(这里是密码)转换成看起来像乱码的一串字节(其实就是字符串)。 -每次传入完全相同的内容时(比如,完全相同的密码),返回的都是完全相同的乱码。 +当你每次传入完全相同的内容(完全相同的密码)时,都会得到完全相同的“乱码”。 -但这个乱码无法转换回传入的密码。 +但你无法从这个“乱码”反向还原出密码。 -### 为什么使用密码哈希 +### 为什么使用密码哈希 { #why-use-password-hashing } -原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。 +如果你的数据库被盗,窃贼拿到的不会是用户的明文密码,而只是哈希值。 -这样一来,窃贼就无法在其它应用中使用窃取的密码(要知道,很多用户在所有系统中都使用相同的密码,风险超大)。 +因此,窃贼无法把该密码拿去尝试登录另一个系统(很多用户在各处都用相同的密码,这将非常危险)。 -## 安装 `passlib` +## 安装 `pwdlib` { #install-pwdlib } -Passlib 是处理密码哈希的 Python 包。 +pwdlib 是一个用于处理密码哈希的优秀 Python 包。 -它支持很多安全哈希算法及配套工具。 +它支持多种安全的哈希算法以及相关工具。 -本教程推荐的算法是 **Bcrypt**。 +推荐的算法是 “Argon2”。 -因此,请先安装附带 Bcrypt 的 PassLib: +请确保创建并激活一个[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},然后安装带 Argon2 的 pwdlib:
```console -$ pip install passlib[bcrypt] +$ pip install "pwdlib[argon2]" ---> 100% ``` @@ -84,51 +86,51 @@ $ pip install passlib[bcrypt] /// tip | 提示 -`passlib` 甚至可以读取 Django、Flask 的安全插件等工具创建的密码。 +使用 `pwdlib`,你甚至可以把它配置为能够读取由 **Django**、**Flask** 安全插件或其他许多工具创建的密码。 -例如,把 Django 应用的数据共享给 FastAPI 应用的数据库。或利用同一个数据库,可以逐步把应用从 Django 迁移到 FastAPI。 +例如,你可以在数据库中让一个 Django 应用和一个 FastAPI 应用共享同一份数据。或者在使用同一个数据库的前提下,逐步迁移一个 Django 应用到 FastAPI。 -并且,用户可以同时从 Django 应用或 FastAPI 应用登录。 +同时,你的用户既可以从 Django 应用登录,也可以从 **FastAPI** 应用登录。 /// -## 密码哈希与校验 +## 哈希并校验密码 { #hash-and-verify-the-passwords } -从 `passlib` 导入所需工具。 +从 `pwdlib` 导入所需工具。 -创建用于密码哈希和身份校验的 PassLib **上下文**。 +用推荐设置创建一个 PasswordHash 实例——它将用于哈希与校验密码。 /// tip | 提示 -PassLib 上下文还支持使用不同哈希算法的功能,包括只能校验的已弃用旧算法等。 +pwdlib 也支持 bcrypt 哈希算法,但不包含遗留算法——如果需要处理过时的哈希,建议使用 passlib 库。 -例如,用它读取和校验其它系统(如 Django)生成的密码,但要使用其它算法,如 Bcrypt,生成新的哈希密码。 +例如,你可以用它读取并校验其他系统(如 Django)生成的密码,但对任何新密码使用不同的算法(如 Argon2 或 Bcrypt)进行哈希。 -同时,这些功能都是兼容的。 +并且能够同时与它们全部兼容。 /// -接下来,创建三个工具函数,其中一个函数用于哈希用户的密码。 +创建一个工具函数来哈希用户传入的密码。 -第一个函数用于校验接收的密码是否匹配存储的哈希值。 +再创建一个工具函数来校验接收的密码是否匹配已存储的哈希。 -第三个函数用于身份验证,并返回用户。 +再创建一个工具函数来进行身份验证并返回用户。 {* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} -/// note | 笔记 +/// 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 令牌进行签名的随机密钥。 -使用以下命令,生成安全的随机密钥: +使用下列命令生成一个安全的随机密钥:
@@ -140,81 +142,82 @@ $ openssl rand -hex 32
-然后,把生成的密钥复制到变量**SECRET_KEY**,注意,不要使用本例所示的密钥。 +把输出复制到变量 `SECRET_KEY`(不要使用示例中的那个)。 -创建指定 JWT 令牌签名算法的变量 **ALGORITHM**,本例中的值为 `"HS256"`。 +创建变量 `ALGORITHM`,设置用于签名 JWT 令牌的算法,这里设为 `"HS256"`。 -创建设置令牌过期时间的变量。 +创建一个变量用于设置令牌的过期时间。 -定义令牌端点响应的 Pydantic 模型。 +定义一个用于令牌端点响应的 Pydantic 模型。 -创建生成新的访问令牌的工具函数。 - -{* ../../docs_src/security/tutorial004.py hl[6,12:14,28:30,78:86] *} - -## 更新依赖项 - -更新 `get_current_user` 以接收与之前相同的令牌,但这里用的是 JWT 令牌。 - -解码并校验接收到的令牌,然后,返回当前用户。 - -如果令牌无效,则直接返回 HTTP 错误。 +创建一个生成新访问令牌的工具函数。 {* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## 更新 `/token` *路径操作* +## 更新依赖项 { #update-the-dependencies } -用令牌过期时间创建 `timedelta` 对象。 +更新 `get_current_user` 以接收与之前相同的令牌,但这次使用的是 JWT 令牌。 -创建并返回真正的 JWT 访问令牌。 +解码接收到的令牌,进行校验,并返回当前用户。 + +如果令牌无效,立即返回一个 HTTP 错误。 + +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} + +## 更新 `/token` 路径操作 { #update-the-token-path-operation } + +用令牌的过期时间创建一个 `timedelta`。 + +创建一个真正的 JWT 访问令牌并返回它。 {* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### JWT `sub` 的技术细节 +### 关于 JWT “主题” `sub` 的技术细节 { #technical-details-about-the-jwt-subject-sub } -JWT 规范还包括 `sub` 键,值是令牌的主题。 +JWT 规范中有一个 `sub` 键,表示令牌的“主题”(subject)。 -该键是可选的,但要把用户标识放在这个键里,所以本例使用了该键。 +使用它是可选的,但通常会把用户的标识放在这里,所以本例中我们使用它。 -除了识别用户与许可用户在 API 上直接执行操作之外,JWT 还可能用于其它事情。 +JWT 除了用于识别用户并允许其直接在你的 API 上执行操作之外,还可能用于其他场景。 -例如,识别**汽车**或**博客**。 +例如,你可以用它来标识一辆“车”或一篇“博客文章”。 -接着,为实体添加权限,比如**驾驶**(汽车)或**编辑**(博客)。 +然后你可以为该实体添加权限,比如“drive”(用于车)或“edit”(用于博客)。 -然后,把 JWT 令牌交给用户(或机器人),他们就可以执行驾驶汽车,或编辑博客等操作。无需注册账户,只要有 API 生成的 JWT 令牌就可以。 +接着,你可以把这个 JWT 令牌交给一个用户(或机器人),他们就可以在没有账户的前提下,仅凭你的 API 生成的 JWT 令牌来执行这些操作(开车、编辑文章)。 -同理,JWT 可以用于更复杂的场景。 +基于这些想法,JWT 可以用于更复杂的场景。 -在这些情况下,多个实体的 ID 可能是相同的,以 ID `foo` 为例,用户的 ID 是 `foo`,车的 ID 是 `foo`,博客的 ID 也是 `foo`。 +在这些情况下,多个实体可能会有相同的 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。 +运行服务器并打开文档:http://127.0.0.1:8000/docs。 -可以看到如下用户界面: +你会看到这样的用户界面: - + -用与上一章同样的方式实现应用授权。 +像之前一样进行授权。 -使用如下凭证: +使用以下凭证: -用户名: `johndoe` 密码: `secret` +用户名: `johndoe` +密码: `secret` /// check | 检查 -注意,代码中没有明文密码**`secret`**,只保存了它的哈希值。 +注意,代码中的任何地方都没有明文密码 “`secret`”,我们只有它的哈希版本。 /// - + -调用 `/users/me/` 端点,收到下面的响应: +调用 `/users/me/` 端点,你将得到如下响应: ```JSON { @@ -225,46 +228,46 @@ JWT 规范还包括 `sub` 键,值是令牌的主题。 } ``` - + -打开浏览器的开发者工具,查看数据是怎么发送的,而且数据里只包含了令牌,只有验证用户的第一个请求才发送密码,并获取访问令牌,但之后不会再发送密码: +如果你打开开发者工具,你会看到发送的数据只包含令牌。密码只会在第一个请求中用于认证用户并获取访问令牌,之后就不会再发送密码了: - + -/// note | 笔记 +/// note | 注意 -注意,请求中 `Authorization` 响应头的值以 `Bearer` 开头。 +注意 `Authorization` 请求头,其值以 `Bearer ` 开头。 /// -## `scopes` 高级用法 +## 使用 `scopes` 的高级用法 { #advanced-usage-with-scopes } -OAuth2 支持**`scopes`**(作用域)。 +OAuth2 支持 “scopes”(作用域)。 -**`scopes`**为 JWT 令牌添加指定权限。 +你可以用它们为 JWT 令牌添加一组特定的权限。 -让持有令牌的用户或第三方在指定限制条件下与 API 交互。 +然后你可以把这个令牌直接交给用户或第三方,在一组限制条件下与 API 交互。 -**高级用户指南**中将介绍如何使用 `scopes`,及如何把 `scopes` 集成至 **FastAPI**。 +在**高级用户指南**中你将学习如何使用它们,以及它们如何集成进 **FastAPI**。 -## 小结 +## 小结 { #recap } -至此,您可以使用 OAuth2 和 JWT 等标准配置安全的 **FastAPI** 应用。 +通过目前所学内容,你可以使用 OAuth2 和 JWT 等标准来搭建一个安全的 **FastAPI** 应用。 -几乎在所有框架中,处理安全问题很快都会变得非常复杂。 +在几乎任何框架中,处理安全问题都会很快变得相当复杂。 -有些包为了简化安全流,不得不在数据模型、数据库和功能上做出妥协。而有些过于简化的软件包其实存在了安全隐患。 +许多把安全流程大幅简化的包,往往要在数据模型、数据库和可用特性上做出大量妥协。而有些过度简化的包实际上在底层存在安全隐患。 --- -**FastAPI** 不向任何数据库、数据模型或工具做妥协。 +**FastAPI** 不会在任何数据库、数据模型或工具上做妥协。 -开发者可以灵活选择最适合项目的安全机制。 +它给予你完全的灵活性,选择最适合你项目的方案。 -还可以直接使用 `passlib` 和 `PyJWT` 等维护良好、使用广泛的包,这是因为 **FastAPI** 不需要任何复杂机制,就能集成外部的包。 +而且你可以直接使用许多维护良好、广泛使用的包,比如 `pwdlib` 和 `PyJWT`,因为 **FastAPI** 不需要复杂机制来集成外部包。 -而且,**FastAPI** 还提供了一些工具,在不影响灵活、稳定和安全的前提下,尽可能地简化安全机制。 +同时它也为你提供尽可能简化流程的工具,而不牺牲灵活性、健壮性或安全性。 -**FastAPI** 还支持以相对简单的方式,使用 OAuth2 等安全、标准的协议。 +你可以以相对简单的方式使用和实现像 OAuth2 这样的安全、标准协议。 -**高级用户指南**中详细介绍了 OAuth2**`scopes`**的内容,遵循同样的标准,实现更精密的权限系统。OAuth2 的作用域是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制,让用户授权第三方应用与 API 交互。 +在**高级用户指南**中,你可以进一步了解如何使用 OAuth2 的 “scopes”,以遵循相同标准实现更细粒度的权限系统。带作用域的 OAuth2 是许多大型身份认证提供商(如 Facebook、Google、GitHub、Microsoft、X(Twitter)等)用来授权第三方应用代表其用户与其 API 交互的机制。 diff --git a/docs/zh/docs/tutorial/security/simple-oauth2.md b/docs/zh/docs/tutorial/security/simple-oauth2.md index f70678df8..3037a983b 100644 --- a/docs/zh/docs/tutorial/security/simple-oauth2.md +++ b/docs/zh/docs/tutorial/security/simple-oauth2.md @@ -1,8 +1,8 @@ -# OAuth2 实现简单的 Password 和 Bearer 验证 +# OAuth2 实现简单的 Password 和 Bearer 验证 { #simple-oauth2-with-password-and-bearer } 本章添加上一章示例中欠缺的部分,实现完整的安全流。 -## 获取 `username` 和 `password` +## 获取 `username` 和 `password` { #get-the-username-and-password } 首先,使用 **FastAPI** 安全工具获取 `username` 和 `password`。 @@ -18,7 +18,7 @@ OAuth2 规范要求使用**密码流**时,客户端或用户必须以表单数 该规范要求必须以表单数据形式发送 `username` 和 `password`,因此,不能使用 JSON 对象。 -### `Scope`(作用域) +### `scope` { #scope } OAuth2 还支持客户端发送**`scope`**表单字段。 @@ -32,7 +32,7 @@ OAuth2 还支持客户端发送**`scope`**表单字段。 * 脸书和 Instagram 使用 `instagram_basic` * 谷歌使用 `https://www.googleapis.com/auth/drive` -/// info | 说明 +/// info | 信息 OAuth2 中,**作用域**只是声明指定权限的字符串。 @@ -44,15 +44,15 @@ OAuth2 中,**作用域**只是声明指定权限的字符串。 /// -## 获取 `username` 和 `password` 的代码 +## 获取 `username` 和 `password` 的代码 { #code-to-get-the-username-and-password } 接下来,使用 **FastAPI** 工具获取用户名与密码。 -### `OAuth2PasswordRequestForm` +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } 首先,导入 `OAuth2PasswordRequestForm`,然后,在 `/token` *路径操作* 中,用 `Depends` 把该类作为依赖项。 -{* ../../docs_src/security/tutorial003.py hl[4,76] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} `OAuth2PasswordRequestForm` 是用以下几项内容声明表单请求体的类依赖项: @@ -72,7 +72,7 @@ OAuth2 中,**作用域**只是声明指定权限的字符串。 * 可选的 `client_id`(本例未使用) * 可选的 `client_secret`(本例未使用) -/// info | 说明 +/// info | 信息 `OAuth2PasswordRequestForm` 与 `OAuth2PasswordBearer` 一样,都不是 FastAPI 的特殊类。 @@ -84,7 +84,7 @@ OAuth2 中,**作用域**只是声明指定权限的字符串。 /// -### 使用表单数据 +### 使用表单数据 { #use-the-form-data } /// tip | 提示 @@ -100,9 +100,9 @@ OAuth2 中,**作用域**只是声明指定权限的字符串。 本例使用 `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-hashing } **哈希**是指,将指定内容(本例中为密码)转换为形似乱码的字节序列(其实就是字符串)。 @@ -120,15 +120,15 @@ OAuth2 中,**作用域**只是声明指定权限的字符串。 但这个乱码无法转换回传入的密码。 -##### 为什么使用密码哈希 +##### 为什么使用密码哈希 { #why-use-password-hashing } 原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。 这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大。 -{* ../../docs_src/security/tutorial003.py hl[80:83] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} -#### 关于 `**user_dict` +#### 关于 `**user_dict` { #about-user-dict } `UserInDB(**user_dict)` 是指: @@ -144,13 +144,13 @@ UserInDB( ) ``` -/// info | 说明 +/// info | 信息 -`user_dict` 的说明,详见[**更多模型**一章](../extra-models.md#user_indict){.internal-link target=_blank}。 +`user_dict` 的说明,详见[**更多模型**一章](../extra-models.md#about-user-in-dict){.internal-link target=_blank}。 /// -## 返回 Token +## 返回 Token { #return-the-token } `token` 端点的响应必须是 JSON 对象。 @@ -162,13 +162,13 @@ UserInDB( /// tip | 提示 -下一章介绍使用哈希密码和 JWT Token 的真正安全机制。 +下一章介绍使用哈希密码和 JWT Token 的真正安全机制。 但现在,仅关注所需的特定细节。 /// -{* ../../docs_src/security/tutorial003.py hl[85] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} /// tip | 提示 @@ -182,7 +182,7 @@ UserInDB( /// -## 更新依赖项 +## 更新依赖项 { #update-the-dependencies } 接下来,更新依赖项。 @@ -194,9 +194,9 @@ UserInDB( 因此,在端点中,只有当用户存在、通过身份验证、且状态为激活时,才能获得该用户: -{* ../../docs_src/security/tutorial003.py hl[58:67,69:72,90] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} -/// info | 说明 +/// info | 信息 此处返回值为 `Bearer` 的响应头 `WWW-Authenticate` 也是规范的一部分。 @@ -210,15 +210,15 @@ UserInDB( 说不定什么时候,就有工具用得上它,而且,开发者或用户也可能用得上。 -这就是遵循标准的好处…… +这就是遵循标准的好处... /// -## 实际效果 +## 实际效果 { #see-it-in-action } 打开 API 文档:http://127.0.0.1:8000/docs。 -### 身份验证 +### 身份验证 { #authenticate } 点击**Authorize**按钮。 @@ -228,13 +228,13 @@ UserInDB( 密码:`secret` - + 通过身份验证后,显示下图所示的内容: - + -### 获取当前用户数据 +### 获取当前用户数据 { #get-your-own-user-data } 使用 `/users/me` 路径的 `GET` 操作。 @@ -250,7 +250,7 @@ UserInDB( } ``` - + 点击小锁图标,注销后,再执行同样的操作,则会得到 HTTP 401 错误: @@ -260,7 +260,7 @@ UserInDB( } ``` -### 未激活用户 +### 未激活用户 { #inactive-user } 测试未激活用户,输入以下信息,进行身份验证: @@ -278,7 +278,7 @@ UserInDB( } ``` -## 小结 +## 小结 { #recap } 使用本章的工具实现基于 `username` 和 `password` 的完整 API 安全系统。 @@ -286,4 +286,4 @@ UserInDB( 唯一欠缺的是,它仍然不是真的**安全**。 -下一章,介绍使用密码哈希支持库与 JWT 令牌实现真正的安全机制。 +下一章,介绍使用密码哈希支持库与 JWT 令牌实现真正的安全机制。 diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index fbdf3be6c..944e960a7 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -1,40 +1,40 @@ -# SQL(关系型)数据库 +# SQL(关系型)数据库 { #sql-relational-databases } -**FastAPI** 并不要求您使用 SQL(关系型)数据库。您可以使用**任何**想用的数据库。 +**FastAPI** 并不要求你使用 SQL(关系型)数据库。你可以使用你想用的**任何数据库**。 这里,我们来看一个使用 SQLModel 的示例。 -**SQLModel** 是基于 SQLAlchemy 和 Pydantic 构建的。它由 **FastAPI** 的同一作者制作,旨在完美匹配需要使用 **SQL 数据库**的 FastAPI 应用程序。 +**SQLModel** 基于 SQLAlchemy 和 Pydantic 构建。它由 **FastAPI** 的同一作者制作,旨在完美匹配需要使用**SQL 数据库**的 FastAPI 应用程序。 -/// tip +/// tip | 提示 -您可以使用任何其他您想要的 SQL 或 NoSQL 数据库(在某些情况下称为 “ORM”),FastAPI 不会强迫您使用任何东西。😎 +你可以使用任意其他你想要的 SQL 或 NoSQL 数据库库(在某些情况下称为 "ORMs"),FastAPI 不会强迫你使用任何东西。😎 /// -由于 SQLModel 基于 SQLAlchemy,因此您可以轻松使用任何由 SQLAlchemy **支持的数据库**(这也让它们被 SQLModel 支持),例如: +由于 SQLModel 基于 SQLAlchemy,因此你可以轻松使用任何由 SQLAlchemy **支持的数据库**(这也让它们被 SQLModel 支持),例如: * PostgreSQL * MySQL * SQLite * Oracle -* Microsoft SQL Server 等. +* Microsoft SQL Server 等 -在这个例子中,我们将使用 **SQLite**,因为它使用单个文件,并且 Python 对其有集成支持。因此,您可以直接复制这个例子并运行。 +在这个示例中,我们将使用 **SQLite**,因为它使用单个文件,并且 Python 对其有集成支持。因此,你可以直接复制这个示例并运行。 -之后,对于您的生产应用程序,您可能会想要使用像 PostgreSQL 这样的数据库服务器。 +之后,对于你的生产应用程序,你可能会想要使用像 **PostgreSQL** 这样的数据库服务器。 -/// tip +/// 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` : +首先,确保你创建并激活了[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后安装 `sqlmodel`:
@@ -45,61 +45,61 @@ $ pip install sqlmodel
-## 创建含有单一模型的应用程序 +## 创建含有单一模型的应用 { #create-the-app-with-a-single-model } -我们首先创建应用程序的最简单的第一个版本,只有一个 **SQLModel** 模型。 +我们先创建应用的最简单的第一个版本,只有一个 **SQLModel** 模型。 稍后我们将通过下面的**多个模型**提高其安全性和多功能性。🤓 -### 创建模型 +### 创建模型 { #create-models } 导入 `SQLModel` 并创建一个数据库模型: {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} -`Hero` 类与 Pydantic 模型非常相似(实际上,从底层来看,它确实*就是一个 Pydantic 模型*)。 +`Hero` 类与 Pydantic 模型非常相似(实际上,从底层来看,它确实就是一个 Pydantic 模型)。 有一些区别: -* `table=True` 会告诉 SQLModel 这是一个*表模型*,它应该表示 SQL 数据库中的一个*表*,而不仅仅是一个*数据模型*(就像其他常规的 Pydantic 类一样)。 +* `table=True` 会告诉 SQLModel 这是一个*表模型*,它应该表示 SQL 数据库中的一个**表**,而不仅仅是一个*数据模型*(就像其他常规的 Pydantic 类一样)。 -* `Field(primary_key=True)` 会告诉 SQLModel `id` 是 SQL 数据库中的**主键**(您可以在 SQLModel 文档中了解更多关于 SQL 主键的信息)。 +* `Field(primary_key=True)` 会告诉 SQLModel `id` 是 SQL 数据库中的**主键**(你可以在 SQLModel 文档中了解更多关于 SQL 主键的信息)。 - 把类型设置为 `int | None` ,SQLModel 就能知道该列在 SQL 数据库中应该是 `INTEGER` 类型,并且应该是 `NULLABLE` 。 + **注意:** 我们为主键字段使用 `int | None`,这样在 Python 代码中我们可以在没有 `id`(`id=None`)的情况下创建对象,并假定数据库在保存时会生成它。SQLModel 会理解数据库会提供 `id`,并在数据库模式中将该列定义为非空的 `INTEGER`。详见 SQLModel 关于主键的文档。 * `Field(index=True)` 会告诉 SQLModel 应该为此列创建一个 **SQL 索引**,这样在读取按此列过滤的数据时,程序能在数据库中进行更快的查找。 - SQLModel 会知道声明为 `str` 的内容将是类型为 `TEXT` (或 `VARCHAR` ,具体取决于数据库)的 SQL 列。 + SQLModel 会知道声明为 `str` 的内容将是类型为 `TEXT`(或 `VARCHAR`,具体取决于数据库)的 SQL 列。 -### 创建引擎(Engine) +### 创建引擎(Engine) { #create-an-engine } -SQLModel 的引擎 `engine`(实际上它是一个 SQLAlchemy `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 数据库。这很有必要,因为**单个请求**可能会使用**多个线程**(例如在依赖项中)。 -不用担心,我们会按照代码结构确保**每个请求使用一个单独的 SQLModel *会话***,这实际上就是 `check_same_thread` 想要实现的。 +不用担心,我们会按照代码结构确保**每个请求使用一个单独的 SQLModel 会话(session)**,这实际上就是 `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] *} -### 创建会话(Session)依赖项 +### 创建会话(Session)依赖项 { #create-a-session-dependency } **`Session`** 会存储**内存中的对象**并跟踪数据中所需更改的内容,然后它**使用 `engine`** 与数据库进行通信。 -我们会使用 `yield` 创建一个 FastAPI **依赖项**,为每个请求提供一个新的 `Session` 。这确保我们每个请求使用一个单独的会话。🤓 +我们会使用 `yield` 创建一个 FastAPI **依赖项**,为每个请求提供一个新的 `Session`。这确保我们每个请求使用一个单独的会话。🤓 然后我们创建一个 `Annotated` 的依赖项 `SessionDep` 来简化其他也会用到此依赖的代码。 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} -### 在启动时创建数据库表 +### 在启动时创建数据库表 { #create-database-tables-on-startup } 我们会在应用程序启动时创建数据库表。 @@ -107,49 +107,47 @@ SQLModel 的引擎 `engine`(实际上它是一个 SQLAlchemy `engine` )是 此处,在应用程序启动事件中,我们创建了表。 -而对于生产环境,您可能会用一个能够在启动应用程序之前运行的迁移脚本。🤓 +在生产环境中,你可能会使用一个在启动应用程序之前运行的迁移脚本。🤓 -/// tip +/// tip | 提示 -SQLModel 将会拥有封装 Alembic 的迁移工具,但目前您可以直接使用 Alembic。 +SQLModel 将会拥有封装 Alembic 的迁移工具,但目前你可以直接使用 Alembic。 /// -### 创建 Hero 类 +### 创建 Hero { #create-a-hero } -因为每个 SQLModel 模型同时也是一个 Pydantic 模型,所以您可以在与 Pydantic 模型相同的**类型注释**中使用它。 +因为每个 SQLModel 模型同时也是一个 Pydantic 模型,所以你可以在与 Pydantic 模型相同的**类型注解**中使用它。 -例如,如果您声明一个类型为 `Hero` 的参数,它将从 **JSON 主体**中读取数据。 +例如,如果你声明一个类型为 `Hero` 的参数,它将从 **JSON 主体**中读取数据。 -同样,您可以将其声明为函数的**返回类型**,然后数据的结构就会显示在自动生成的 API 文档界面中。 +同样,你可以将其声明为函数的**返回类型**,然后数据的结构就会显示在自动生成的 API 文档界面中。 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} - +这里,我们使用 `SessionDep` 依赖项(一个 `Session`)将新的 `Hero` 添加到 `Session` 实例中,提交更改到数据库,刷新 `hero` 中的数据,并返回它。 -这里,我们使用 `SessionDep` 依赖项(一个 `Session` )将新的 `Hero` 添加到 `Session` 实例中,提交更改到数据库,刷新 hero 中的数据,并返回它。 +### 读取 Hero { #read-heroes } -### 读取 Hero 类 - -我们可以使用 `select()` 从数据库中**读取** `Hero` 类,并利用 `limit` 和 `offset` 来对结果进行分页。 +我们可以使用 `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` 。 +我们可以**读取**单个 `Hero`。 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} -### 删除单个 Hero +### 删除单个 Hero { #delete-a-hero } -我们也可以**删除**单个 `Hero` 。 +我们也可以**删除**一个 `Hero`。 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} -### 运行应用程序 +### 运行应用 { #run-the-app } -您可以运行这个应用程序: +你可以运行这个应用:
@@ -161,25 +159,25 @@ $ fastapi dev main.py
-然后在 `/docs` UI 中,您能够看到 **FastAPI** 会用这些**模型**来**记录** API,并且还会用它们来**序列化**和**验证**数据。 +然后在 `/docs` UI 中,你能够看到 **FastAPI** 会用这些**模型**来**记录** API,并且还会用它们来**序列化**和**验证**数据。
-## 更新应用程序以支持多个模型 +## 使用多个模型更新应用 { #update-the-app-with-multiple-models } 现在让我们稍微**重构**一下这个应用,以提高**安全性**和**多功能性**。 -如果您查看之前的应用程序,您可以在 UI 界面中看到,到目前为止,由客户端决定要创建的 `Hero` 的 `id` 值。😱 +如果你查看之前的应用程序,你可以在 UI 界面中看到,到目前为止,它允许客户端决定要创建的 `Hero` 的 `id`。😱 -我们不应该允许这样做,因为他们可能会覆盖我们在数据库中已经分配的 `id` 。决定 `id` 的行为应该由**后端**或**数据库**来完成,**而非客户端**。 +我们不应该允许这样做,因为他们可能会覆盖我们在数据库中已经分配的 `id`。决定 `id` 的行为应该由**后端**或**数据库**来完成,**而非客户端**。 -此外,我们为 hero 创建了一个 `secret_name` ,但到目前为止,我们在各处都返回了它,这就不太**秘密**了……😅 +此外,我们为 hero 创建了一个 `secret_name`,但到目前为止,我们在各处都返回了它,这就不太**秘密**了……😅 我们将通过添加一些**额外的模型**来解决这些问题,而 SQLModel 将在这里大放异彩。✨ -### 创建多个模型 +### 创建多个模型 { #create-multiple-models } 在 **SQLModel** 中,任何含有 `table=True` 属性的模型类都是一个**表模型**。 @@ -187,7 +185,7 @@ $ fastapi dev main.py 有了 SQLModel,我们就可以利用**继承**来在所有情况下**避免重复**所有字段。 -#### `HeroBase` - 基类 +#### `HeroBase` - 基类 { #herobase-the-base-class } 我们从一个 `HeroBase` 模型开始,该模型具有所有模型**共享的字段**: @@ -196,14 +194,14 @@ $ fastapi dev main.py {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} -#### `Hero` - *表模型* +#### `Hero` - *表模型* { #hero-the-table-model } -接下来,我们创建 `Hero` ,实际的*表模型*,并添加那些不总是在其他模型中的**额外字段**: +接下来,我们创建 `Hero`,实际的*表模型*,并添加那些不总是在其他模型中的**额外字段**: * `id` * `secret_name` -因为 `Hero` 继承自 HeroBase ,所以它**也**包含了在 `HeroBase` 中声明过的**字段**。因此 `Hero` 的所有字段为: +因为 `Hero` 继承自 `HeroBase`,所以它**也**包含了在 `HeroBase` 中声明过的**字段**。因此 `Hero` 的所有字段为: * `id` * `name` @@ -212,46 +210,45 @@ $ fastapi dev main.py {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} -#### `HeroPublic` - 公共*数据模型* +#### `HeroPublic` - 公共*数据模型* { #heropublic-the-public-data-model } 接下来,我们创建一个 `HeroPublic` 模型,这是将**返回**给 API 客户端的模型。 -它包含与 `HeroBase` 相同的字段,因此不会包括 `secret_name` 。 +它包含与 `HeroBase` 相同的字段,因此不会包括 `secret_name`。 -终于,我们英雄(hero)的身份得到了保护! 🥷 +终于,我们英雄(hero)的身份得到了保护!🥷 -它还重新声明了 `id: int` 。这样我们便与 API 客户端建立了一种**约定**,使他们始终可以期待 `id` 存在并且是一个整数 `int`(永远不会是 `None` )。 +它还重新声明了 `id: int`。这样我们便与 API 客户端建立了一种**约定**,使他们始终可以期待 `id` 存在并且是一个整数 `int`(永远不会是 `None`)。 -/// tip +/// tip | 提示 -确保返回模型始终提供一个值并且始终是 `int` (而不是 `None` )对 API 客户端非常有用,他们可以在这种确定性下编写更简单的代码。 +确保返回模型始终提供一个值并且始终是 `int`(而不是 `None`)对 API 客户端非常有用,他们可以在这种确定性下编写更简单的代码。 -此外,**自动生成的客户端**将拥有更简洁的接口,这样与您的 API 交互的开发者就能更轻松地使用您的 API。😎 +此外,**自动生成的客户端**将拥有更简洁的接口,这样与你的 API 交互的开发者就能更轻松地使用你的 API。😎 /// -`HeroPublic` 中的所有字段都与 `HeroBase` 中的相同,其中 `id` 声明为 `int` (不是 `None` ): +`HeroPublic` 中的所有字段都与 `HeroBase` 中的相同,其中 `id` 声明为 `int`(不是 `None`): * `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` 。 +它不仅拥有与 `HeroBase` 相同的字段,还有 `secret_name`。 -现在,当客户端**创建一个新的 hero** 时,他们会发送 `secret_name` ,它会被存储到数据库中,但这些 `secret_name` 不会通过 API 返回给客户端。 +现在,当客户端**创建一个新的 hero** 时,他们会发送 `secret_name`,它会被存储到数据库中,但这些 `secret_name` 不会通过 API 返回给客户端。 -/// tip +/// tip | 提示 这应当是**密码**被处理的方式:接收密码,但不要通过 API 返回它们。 -在存储密码之前,您还应该对密码的值进行**哈希**处理,**绝不要以明文形式存储它们**。 +在存储密码之前,你还应该对密码的值进行**哈希**处理,**绝不要以明文形式存储它们**。 /// @@ -263,17 +260,17 @@ $ fastapi dev main.py {* ../../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**,但现在有了**多个模型**,我们便能做到这一点了。🎉 -`HeroUpdate` *数据模型*有些特殊,它包含创建新 hero 所需的**所有相同字段**,但所有字段都是**可选的**(它们都有默认值)。这样,当您更新一个 hero 时,您可以只发送您想要更新的字段。 +`HeroUpdate` *数据模型*有些特殊,它包含创建新 hero 所需的**所有相同字段**,但所有字段都是**可选的**(它们都有默认值)。这样,当你更新一个 hero 时,你可以只发送你想要更新的字段。 -因为所有**字段实际上**都发生了**变化**(类型现在包括 `None` ,并且它们现在有一个默认值 `None` ),我们需要**重新声明**它们。 +因为所有**字段实际上**都发生了**变化**(类型现在包括 `None`,并且它们现在有一个默认值 `None`),我们需要**重新声明**它们。 -我们会重新声明所有字段,因此我们并不是真的需要从 `HeroBase` 继承。我会让它继承只是为了保持一致,但这并不必要。这更多是个人喜好的问题。🤷 +我们并不真的需要从 `HeroBase` 继承,因为我们会重新声明所有字段。我会让它继承只是为了保持一致,但这并不必要。这更多是个人喜好的问题。🤷 -`HeroUpdate` 的字段包括: +`HeroUpdate` 的字段包括: * `name` * `age` @@ -281,51 +278,51 @@ $ fastapi dev main.py {* ../../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` 会包含客户端发送的字段,以及一个由数据库生成的 `id`。 -然后我们将与函数中相同的*表模型* `Hero` 原样返回。但是由于我们使用 `HeroPublic` *数据模型*声明了 `response_model` ,**FastAPI** 会使用 `HeroPublic` 来验证和序列化数据。 +然后我们将与函数中相同的*表模型* `Hero` 原样返回。但是由于我们使用 `HeroPublic` *数据模型*声明了 `response_model`,**FastAPI** 会使用 `HeroPublic` 来验证和序列化数据。 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} -/// tip +/// tip | 提示 -现在我们使用 `response_model=HeroPublic` 来代替**返回类型注释** `-> HeroPublic` ,因为我们返回的值实际上**并不是** `HeroPublic` 类型。 +现在我们使用 `response_model=HeroPublic` 来代替**返回类型注解** `-> HeroPublic`,因为我们返回的值实际上并不是 `HeroPublic`。 -如果我们声明了 `-> HeroPublic` ,您的编辑器和代码检查工具会抱怨(但也确实理所应当)您返回了一个 `Hero` 而不是一个 `HeroPublic` 。 +如果我们声明了 `-> HeroPublic`,你的编辑器和代码检查工具会(理所应当地)抱怨你返回了一个 `Hero` 而不是一个 `HeroPublic`。 通过 `response_model` 的声明,我们让 **FastAPI** 按照它自己的方式处理,而不会干扰类型注解以及编辑器和其他工具提供的帮助。 /// -### 用 `HeroPublic` 读取 Hero +### 使用 `HeroPublic` 读取 Hero { #read-heroes-with-heropublic } -我们可以像之前一样**读取** `Hero` 。同样,使用 `response_model=list[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` 操作。 +我们可以**更新**单个 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 基本保持不变。 @@ -333,9 +330,9 @@ $ fastapi dev main.py {* ../../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 看到它现在已经更新,并且在进行创建 hero 等操作时,它不会再期望从客户端接收 `id` 数据。 +如果你进入 `/docs` API UI,你会看到它现在已经更新,并且在创建 hero 时,它不会再期望从客户端接收 `id` 数据等。
-## 总结 +## 总结 { #recap } -您可以使用 **SQLModel** 与 SQL 数据库进行交互,并通过*数据模型*和*表模型*简化代码。 +你可以使用 **SQLModel** 与 SQL 数据库进行交互,并通过*数据模型*和*表模型*简化代码。 -您可以在 SQLModel 的文档中学习到更多内容,其中有一个更详细的关于如何将 SQLModel 与 FastAPI 一起使用的教程。🚀 +你可以在 **SQLModel** 文档中学习到更多内容,其中有一个更详细的将 SQLModel 与 **FastAPI** 一起使用的迷你教程。🚀 diff --git a/docs/zh/docs/tutorial/static-files.md b/docs/zh/docs/tutorial/static-files.md index 1a0d4504c..5b2f920b2 100644 --- a/docs/zh/docs/tutorial/static-files.md +++ b/docs/zh/docs/tutorial/static-files.md @@ -1,40 +1,40 @@ -# 静态文件 +# 静态文件 { #static-files } -您可以使用 `StaticFiles`从目录中自动提供静态文件。 +你可以使用 `StaticFiles` 从目录中自动提供静态文件。 -## 使用`StaticFiles` +## 使用 `StaticFiles` { #use-staticfiles } -* 导入`StaticFiles`。 -* "挂载"(Mount) 一个 `StaticFiles()` 实例到一个指定路径。 +* 导入 `StaticFiles`。 +* 将一个 `StaticFiles()` 实例“挂载”(Mount)到指定路径。 -{* ../../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`。 -**FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles` ,只是为了方便你,开发者。但它确实来自Starlette。 +**FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles`,只是为了方便你这个开发者。但它确实直接来自 Starlette。 /// -### 什么是"挂载"(Mounting) +### 什么是“挂载”(Mounting) { #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**内部使用的名字。 +`name="static"` 为它提供了一个可被 **FastAPI** 内部使用的名称。 -所有这些参数可以不同于"`static`",根据你应用的需要和具体细节调整它们。 +这些参数都可以不是“`static`”,请根据你的应用需求和具体细节进行调整。 -## 更多信息 +## 更多信息 { #more-info } -更多细节和选择查阅 Starlette's docs about Static Files. +更多细节和选项请查阅 Starlette 的静态文件文档。 diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md index 3877adbac..1aff9f799 100644 --- a/docs/zh/docs/tutorial/testing.md +++ b/docs/zh/docs/tutorial/testing.md @@ -1,4 +1,4 @@ -# 测试 +# 测试 { #testing } 感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 @@ -6,17 +6,21 @@ 有了它,你可以直接与**FastAPI**一起使用 pytest。 -## 使用 `TestClient` +## 使用 `TestClient` { #using-testclient } /// info | 信息 -要使用 `TestClient`,先要安装 `httpx`. +要使用 `TestClient`,先要安装 `httpx`。 -例:`pip install httpx`. +确保你创建并激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后再安装,例如: + +```console +$ pip install httpx +``` /// -导入 `TestClient`. +导入 `TestClient`。 通过传入你的**FastAPI**应用创建一个 `TestClient` 。 @@ -26,7 +30,7 @@ 为你需要检查的地方用标准的Python表达式写个简单的 `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 | 提示 @@ -52,13 +56,13 @@ /// -## 分离测试 +## 分离测试 { #separating-tests } 在实际应用中,你可能会把你的测试放在另一个文件里。 您的**FastAPI**应用程序也可能由一些文件/模块组成等等。 -### **FastAPI** app 文件 +### **FastAPI** app 文件 { #fastapi-app-file } 假设你有一个像 [更大的应用](bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构: @@ -72,9 +76,9 @@ 在 `main.py` 文件中你有一个 **FastAPI** app: -{* ../../docs_src/app_testing/main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} -### 测试文件 +### 测试文件 { #testing-file } 然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在(一样是目录,但有个 `__init__.py` 文件): @@ -88,15 +92,15 @@ 因为这文件在同一个包中,所以你可以通过相对导入从 `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** app 文件 +### 扩展后的 **FastAPI** app 文件 { #extended-fastapi-app-file } 让我们继续之前的文件结构: @@ -116,63 +120,13 @@ 所有*路径操作* 都需要一个`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 | 提示 - -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!} -``` - -//// - -### 扩展后的测试文件 +### 扩展后的测试文件 { #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的设计的。 @@ -196,9 +150,11 @@ Prefer to use the `Annotated` version if possible. /// -## 运行起来 +## 运行起来 { #run-it } -之后,你只需要安装 `pytest`: +之后,你只需要安装 `pytest`。 + +确保你创建并激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后再安装,例如:
diff --git a/docs/zh/docs/virtual-environments.md b/docs/zh/docs/virtual-environments.md index 9b3c0340a..29e272b59 100644 --- a/docs/zh/docs/virtual-environments.md +++ b/docs/zh/docs/virtual-environments.md @@ -1,14 +1,14 @@ -# 虚拟环境 +# 虚拟环境 { #virtual-environments } 当你在 Python 工程中工作时,你可能会有必要用到一个**虚拟环境**(或类似的机制)来隔离你为每个工程安装的包。 -/// info +/// info | 信息 如果你已经了解虚拟环境,知道如何创建和使用它们,你可以考虑跳过这一部分。🤓 /// -/// tip +/// tip | 提示 **虚拟环境**和**环境变量**是不同的。 @@ -18,7 +18,7 @@ /// -/// info +/// info | 信息 这个页面将教你如何使用**虚拟环境**以及了解它们的工作原理。 @@ -26,7 +26,7 @@ /// -## 创建一个工程 +## 创建一个工程 { #create-a-project } 首先,为你的工程创建一个目录。 @@ -51,11 +51,11 @@ $ cd awesome-project
-## 创建一个虚拟环境 +## 创建一个虚拟环境 { #create-a-virtual-environment } 在开始一个 Python 工程的**第一时间**,**在你的工程内部**创建一个虚拟环境。 -/// tip +/// tip | 提示 你只需要 **在每个工程中操作一次**,而不是每次工作时都操作。 @@ -96,7 +96,7 @@ $ uv venv
-/// tip +/// tip | 提示 默认情况下,`uv` 会在一个名为 `.venv` 的目录中创建一个虚拟环境。 @@ -114,11 +114,11 @@ $ uv venv /// -## 激活虚拟环境 +## 激活虚拟环境 { #activate-the-virtual-environment } 激活新的虚拟环境来确保你运行的任何 Python 命令或安装的包都能使用到它。 -/// tip +/// tip | 提示 **每次**开始一个 **新的终端会话** 来工作在这个工程时,你都需要执行这个操作。 @@ -162,19 +162,19 @@ $ source .venv/Scripts/activate //// -/// tip +/// tip | 提示 每次你在这个环境中安装一个 **新的包** 时,都需要 **重新激活** 这个环境。 -这么做确保了当你使用一个由这个包安装的 **终端(CLI)程序** 时,你使用的是你的虚拟环境中的程序,而不是全局安装、可能版本不同的程序。 +这么做确保了当你使用一个由这个包安装的 **终端(CLI)程序** 时,你使用的是你的虚拟环境中的程序,而不是全局安装、可能版本不同的程序。 /// -## 检查虚拟环境是否激活 +## 检查虚拟环境是否激活 { #check-the-virtual-environment-is-active } 检查虚拟环境是否激活 (前面的命令是否生效)。 -/// tip +/// tip | 提示 这是 **可选的**,但这是一个很好的方法,可以 **检查** 一切是否按预期工作,以及你是否使用了你打算使用的虚拟环境。 @@ -212,9 +212,9 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -## 升级 `pip` +## 升级 `pip` { #upgrade-pip } -/// tip +/// tip | 提示 如果你使用 `uv` 来安装内容,而不是 `pip`,那么你就不需要升级 `pip`。😎 @@ -224,7 +224,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 在安装包时出现的许多奇怪的错误都可以通过先升级 `pip` 来解决。 -/// tip +/// tip | 提示 通常你只需要在创建虚拟环境后 **执行一次** 这个操作。 @@ -242,17 +242,37 @@ $ python -m pip install --upgrade pip -## 添加 `.gitignore` +/// tip | 提示 + +有时在尝试升级 pip 时,你可能会遇到 **`No module named pip`** 错误。 + +如果发生这种情况,使用下面的命令来安装并升级 pip: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +该命令会在尚未安装 pip 时进行安装,并确保安装的 pip 版本不早于 `ensurepip` 提供的版本。 + +/// + +## 添加 `.gitignore` { #add-gitignore } 如果你使用 **Git** (这是你应该使用的),添加一个 `.gitignore` 文件来排除你的 `.venv` 中的所有内容。 -/// tip +/// tip | 提示 如果你使用 `uv` 来创建虚拟环境,它会自动为你完成这个操作,你可以跳过这一步。😎 /// -/// tip +/// tip | 提示 通常你只需要在创建虚拟环境后 **执行一次** 这个操作。 @@ -282,11 +302,11 @@ $ echo "*" > .venv/.gitignore /// -## 安装软件包 +## 安装软件包 { #install-packages } 在激活虚拟环境后,你可以在其中安装软件包。 -/// tip +/// tip | 提示 当你需要安装或升级软件包时,执行本操作**一次**; @@ -294,11 +314,11 @@ $ echo "*" > .venv/.gitignore /// -### 直接安装包 +### 直接安装包 { #install-packages-directly } 如果你急于安装,不想使用文件来声明工程的软件包依赖,您可以直接安装它们。 -/// tip +/// tip | 提示 将程序所需的软件包及其版本放在文件中(例如 `requirements.txt` 或 `pyproject.toml`)是个好(并且非常好)的主意。 @@ -333,7 +353,7 @@ $ uv pip install "fastapi[standard]" //// -### 从 `requirements.txt` 安装 +### 从 `requirements.txt` 安装 { #install-from-requirements-txt } 如果你有一个 `requirements.txt` 文件,你可以使用它来安装其中的软件包。 @@ -376,7 +396,7 @@ pydantic==2.8.0 /// -## 运行程序 +## 运行程序 { #run-your-program } 在你激活虚拟环境后,你可以运行你的程序,它将使用虚拟环境中的 Python 和你在其中安装的软件包。 @@ -390,7 +410,7 @@ Hello World -## 配置编辑器 +## 配置编辑器 { #configure-your-editor } 你可能会用到编辑器(即 IDE —— 译者注),请确保配置它使用与你创建的相同的虚拟环境(它可能会自动检测到),以便你可以获得自动补全和内联错误提示。 @@ -399,13 +419,13 @@ Hello World * VS Code * PyCharm -/// tip +/// tip | 提示 通常你只需要在创建虚拟环境时执行此操作**一次**。 /// -## 退出虚拟环境 +## 退出虚拟环境 { #deactivate-the-virtual-environment } 当你完成工作后,你可以**退出**虚拟环境。 @@ -419,13 +439,13 @@ $ deactivate 这样,当你运行 `python` 时,它不会尝试从安装了软件包的虚拟环境中运行。(即,它将不再会尝试从虚拟环境中运行,也不会使用其中安装的软件包。—— 译者注) -## 开始工作 +## 开始工作 { #ready-to-work } 现在你已经准备好开始你的工作了。 -/// tip +/// tip | 提示 你想要理解上面的所有内容吗? @@ -433,7 +453,7 @@ $ deactivate /// -## 为什么要使用虚拟环境 +## 为什么要使用虚拟环境 { #why-virtual-environments } 你需要安装 Python 才能使用 FastAPI。 @@ -443,7 +463,7 @@ $ deactivate 然而,如果你直接使用 `pip`,软件包将被安装在你的**全局 Python 环境**中(即 Python 的全局安装)。 -### 存在的问题 +### 存在的问题 { #the-problem } 那么,在全局 Python 环境中安装软件包有什么问题呢? @@ -516,7 +536,7 @@ flowchart LR end ``` -/// tip +/// tip | 提示 Python 包在推出**新版本**时通常会尽量**避免破坏性更改**,但最好还是要小心,要想清楚再安装新版本,而且在运行测试以确保一切能正常工作时再安装。 @@ -526,7 +546,7 @@ Python 包在推出**新版本**时通常会尽量**避免破坏性更改**, 此外,取决于你的操作系统(例如 Linux、Windows、macOS),它可能已经预先安装了 Python。在这种情况下,它可能已经预先安装了一些软件包,这些软件包的特定版本是**系统所需的**。如果你在全局 Python 环境中安装软件包,你可能会**破坏**一些随操作系统一起安装的程序。 -## 软件包安装在哪里 +## 软件包安装在哪里 { #where-are-packages-installed } 当你安装 Python 时,它会在你的计算机上创建一些目录,并在这些目录中放一些文件。 @@ -552,7 +572,7 @@ $ pip install "fastapi[standard]" 默认情况下,它会将下载并解压的这些文件放在随 Python 安装的目录中,这就是**全局环境**。 -## 什么是虚拟环境 +## 什么是虚拟环境 { #what-are-virtual-environments } 解决软件包都安装在全局环境中的问题的方法是为你所做的每个工程使用一个**虚拟环境**。 @@ -577,7 +597,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 激活虚拟环境意味着什么 +## 激活虚拟环境意味着什么 { #what-does-activating-a-virtual-environment-mean } 当你激活了一个虚拟环境,例如: @@ -623,7 +643,7 @@ $ source .venv/Scripts/activate 其中之一是 `PATH` 变量。 -/// tip +/// tip | 提示 你可以在 [环境变量](environment-variables.md#path-environment-variable){.internal-link target=_blank} 部分了解更多关于 `PATH` 环境变量的内容。 @@ -714,7 +734,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 激活虚拟环境还会改变其他一些东西,但这是它所做的最重要的事情之一。 -## 检查虚拟环境 +## 检查虚拟环境 { #checking-a-virtual-environment } 当你检查虚拟环境是否激活时,例如: @@ -756,7 +776,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 因此,你可以确认你是否在正确的虚拟环境中。 -/// tip +/// tip | 提示 激活一个虚拟环境,获取一个 Python,然后**转到另一个工程**是一件很容易的事情; @@ -766,7 +786,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python /// -## 为什么要停用虚拟环境 +## 为什么要停用虚拟环境 { #why-deactivate-a-virtual-environment } 例如,你可能正在一个工程 `philosophers-stone` 上工作,**激活了该虚拟环境**,安装了包并使用了该环境, @@ -820,7 +840,7 @@ I solemnly swear 🐺 -## 替代方案 +## 替代方案 { #alternatives } 这是一个简单的指南,可以帮助你入门并教会你如何理解一切**底层**的东西。 @@ -837,7 +857,7 @@ I solemnly swear 🐺 * 确保你有一个**确切**的软件包和版本集合来安装,包括它们的依赖项,这样你就可以确保在生产中运行你的工程与在开发时在你的计算机上运行的工程完全相同,这被称为**锁定** * 还有很多其他功能 -## 结论 +## 结论 { #conclusion } 如果你读过并理解了所有这些,现在**你对虚拟环境的了解比很多开发者都要多**。🤓 diff --git a/fastapi-slim/README.md b/fastapi-slim/README.md new file mode 100644 index 000000000..e378a9c8c --- /dev/null +++ b/fastapi-slim/README.md @@ -0,0 +1,54 @@ +

+ FastAPI +

+

+ FastAPI framework, high performance, easy to learn, fast to code, ready for production +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/fastapi/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python based on standard Python type hints. + +## `fastapi-slim` + +⚠️ Do not install this package. ⚠️ + +This package, `fastapi-slim`, does nothing other than depend on `fastapi`. + +All the functionality has been integrated into `fastapi`. + +The only reason this package exists is as a migration path for old projects that used to depend on `fastapi-slim`, so that they can get the latest version of `fastapi`. + +You **should not** install this package. + +Install instead: + +```bash +pip install fastapi +``` + +This package is deprecated and will stop receiving any updates and published versions. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 6133787b0..9931216ee 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.128.0" +__version__ = "0.128.8" from starlette import status as status diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index 3dfaf9b71..4581c38c8 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,8 +1,14 @@ -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 ( + field_annotation_is_scalar_sequence as field_annotation_is_scalar_sequence, +) +from .shared import field_annotation_is_sequence as field_annotation_is_sequence +from .shared import ( + is_bytes_or_nonable_bytes_annotation as is_bytes_or_nonable_bytes_annotation, +) +from .shared import is_bytes_sequence_annotation as is_bytes_sequence_annotation 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, @@ -13,28 +19,21 @@ 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_flat_models_from_fields as get_flat_models_from_fields from .v2 import get_missing_field_error as get_missing_field_error +from .v2 import get_model_name_map as get_model_name_map 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/shared.py b/fastapi/_compat/shared.py index 68b9bbdf1..9d76dabe6 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,4 +1,3 @@ -import sys import types import typing import warnings @@ -8,27 +7,28 @@ from dataclasses import is_dataclass from typing import ( Annotated, Any, + TypeGuard, + TypeVar, Union, + get_args, + get_origin, ) 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 get_args, get_origin + +_T = TypeVar("_T") # Copy from Pydantic: pydantic/_internal/_typing_extra.py -if sys.version_info < (3, 10): - WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] -else: - WithArgsTypes: tuple[Any, ...] = ( - typing._GenericAlias, # type: ignore[attr-defined] - types.GenericAlias, - types.UnionType, - ) # pyright: ignore[reportAttributeAccessIssue] +WithArgsTypes: tuple[Any, ...] = ( + typing._GenericAlias, # type: ignore[attr-defined] + types.GenericAlias, + types.UnionType, +) # pyright: ignore[reportAttributeAccessIssue] PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 sequence_annotation_to_type = { @@ -40,15 +40,13 @@ sequence_annotation_to_type = { deque: deque, } -sequence_types = tuple(sequence_annotation_to_type.keys()) - -Url: type[Any] +sequence_types: tuple[type[Any], ...] = tuple(sequence_annotation_to_type.keys()) -# Copy of Pydantic: pydantic/_internal/_utils.py +# Copy of Pydantic: pydantic/_internal/_utils.py with added TypeGuard def lenient_issubclass( - cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...], None] -) -> bool: + cls: Any, class_or_tuple: type[_T] | tuple[type[_T], ...] | None +) -> TypeGuard[type[_T]]: try: return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] except TypeError: # pragma: no cover @@ -57,13 +55,13 @@ def lenient_issubclass( raise # pragma: no cover -def _annotation_is_sequence(annotation: Union[type[Any], None]) -> bool: +def _annotation_is_sequence(annotation: type[Any] | None) -> bool: if lenient_issubclass(annotation, (str, bytes)): return False return lenient_issubclass(annotation, sequence_types) -def field_annotation_is_sequence(annotation: Union[type[Any], None]) -> bool: +def field_annotation_is_sequence(annotation: type[Any] | None) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): @@ -79,7 +77,7 @@ def value_is_sequence(value: Any) -> bool: 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: type[Any] | None) -> bool: return ( lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) or _annotation_is_sequence(annotation) @@ -87,7 +85,7 @@ def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool: ) -def field_annotation_is_complex(annotation: Union[type[Any], None]) -> bool: +def field_annotation_is_complex(annotation: 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)) @@ -108,7 +106,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: type[Any] | None) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: at_least_one_scalar_sequence = False @@ -178,16 +176,26 @@ 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 + # TODO: remove this function once the required version of Pydantic fully + # removes pydantic.v1 + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + except ImportError: # pragma: no cover + return False 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 + # TODO: remove this function once the required version of Pydantic fully + # removes pydantic.v1 + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + except ImportError: # pragma: no cover + return False return lenient_issubclass(cls, v1.BaseModel) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index dae78a32e..b83bc1b55 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -1,18 +1,21 @@ import re import warnings from collections.abc import Sequence -from copy import copy, deepcopy +from copy import copy from dataclasses import dataclass, is_dataclass from enum import Enum from functools import lru_cache from typing import ( Annotated, Any, + Literal, Union, cast, + get_args, + get_origin, ) -from fastapi._compat import shared +from fastapi._compat import lenient_issubclass, shared from fastapi.openapi.constants import REF_TEMPLATE from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model @@ -23,29 +26,19 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr- GetJsonSchemaHandler as GetJsonSchemaHandler, ) from pydantic._internal._typing_extra import eval_type_lenient -from pydantic._internal._utils import lenient_issubclass as lenient_issubclass from pydantic.fields import FieldInfo as FieldInfo from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema 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 PydanticUndefined from pydantic_core import Url as Url -from typing_extensions import Literal, get_args, get_origin - -try: - from pydantic_core.core_schema import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) -except ImportError: # pragma: no cover - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 - ) +from pydantic_core.core_schema import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) RequiredParam = PydanticUndefined Undefined = PydanticUndefined -UndefinedType = PydanticUndefinedType evaluate_forwardref = eval_type_lenient -Validator = Any # TODO: remove when dropping support for Pydantic < v2.12.3 _Attrs = { @@ -87,20 +80,12 @@ def asdict(field_info: FieldInfo) -> dict[str, Any]: } -class BaseConfig: - pass - - -class ErrorWrapper(Exception): - pass - - @dataclass class ModelField: field_info: FieldInfo name: str mode: Literal["validation", "serialization"] = "validation" - config: Union[ConfigDict, None] = None + config: ConfigDict | None = None @property def alias(self) -> str: @@ -108,29 +93,21 @@ class ModelField: return a if a is not None else self.name @property - def validation_alias(self) -> Union[str, None]: + def validation_alias(self) -> 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]: + def serialization_alias(self) -> str | None: sa = self.field_info.serialization_alias return sa or None - @property - def required(self) -> bool: - return self.field_info.is_required() - @property def default(self) -> Any: return self.get_default() - @property - def type_(self) -> Any: - return self.field_info.annotation - def __post_init__(self) -> None: with warnings.catch_warnings(): # Pydantic >= 2.12.0 warns about field specific metadata that is unused @@ -143,8 +120,8 @@ 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() + # TODO: remove after setting the min Pydantic to v2.12.3 + # that adds asdict(), and use self.field_info.asdict() instead field_dict = asdict(self.field_info) annotated_args = ( field_dict["annotation"], @@ -168,12 +145,12 @@ class ModelField: value: Any, values: dict[str, Any] = {}, # noqa: B006 *, - loc: tuple[Union[int, str], ...] = (), - ) -> tuple[Any, Union[list[dict[str, Any]], None]]: + loc: tuple[int | str, ...] = (), + ) -> tuple[Any, list[dict[str, Any]]]: try: return ( self._type_adapter.validate_python(value, from_attributes=True), - None, + [], ) except ValidationError as exc: return None, _regenerate_error_with_loc( @@ -185,8 +162,8 @@ class ModelField: value: Any, *, mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -227,7 +204,7 @@ def get_schema_from_model_field( ], separate_input_output_schemas: bool = True, ) -> dict[str, Any]: - override_mode: Union[Literal["validation"], None] = ( + override_mode: Literal["validation"] | None = ( None if (separate_input_output_schemas or _has_computed_fields(field)) else "validation" @@ -284,9 +261,9 @@ def get_definitions( for model in flat_serialization_models ] flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields - input_types = {f.type_ for f in fields} + input_types = {f.field_info.annotation for f in fields} unique_flat_model_fields = { - f for f in flat_model_fields if f.type_ not in input_types + f for f in flat_model_fields if f.field_info.annotation not in input_types } inputs = [ ( @@ -305,94 +282,12 @@ def get_definitions( if "description" in item_def: item_description = cast(str, item_def["description"]).split("\f")[0] item_def["description"] = item_description - new_mapping, new_definitions = _remap_definitions_and_field_mappings( - model_name_map=model_name_map, - definitions=definitions, # type: ignore[arg-type] - field_mapping=field_mapping, - ) - return new_mapping, new_definitions - - -def _replace_refs( - *, - 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": - 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( - schema=value, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - elif isinstance(value, list): - new_value = [] - for item in value: - if isinstance(item, dict): - new_item = _replace_refs( - schema=item, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_value.append(new_item) - - else: - new_value.append(item) - new_schema[key] = new_value - return new_schema - - -def _remap_definitions_and_field_mappings( - *, - model_name_map: ModelNameMap, - definitions: dict[str, Any], - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], -) -> 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 or "$ref" not in schema: - continue - new_name = model_name_map[model] - old_name = schema["$ref"].split("/")[-1] - if old_name in {f"{new_name}-Input", f"{new_name}-Output"}: - continue - old_name_to_new_name_map[old_name] = new_name - - new_field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ] = {} - for field_key, schema in field_mapping.items(): - new_schema = _replace_refs( - schema=schema, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_field_mapping[field_key] = new_schema - - new_definitions = {} - for key, value in definitions.items(): - if key in old_name_to_new_name_map: - new_key = old_name_to_new_name_map[key] - else: - new_key = key - new_value = _replace_refs( - schema=value, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_definitions[new_key] = new_value - return new_field_mapping, new_definitions + # definitions: dict[DefsRef, dict[str, Any]] + # but mypy complains about general str in other places that are not declared as + # DefsRef, although DefsRef is just str: + # DefsRef = NewType('DefsRef', str) + # So, a cast to simplify the types here + return field_mapping, cast(dict[str, dict[str, Any]], definitions) def is_scalar_field(field: ModelField) -> bool: @@ -403,22 +298,6 @@ def is_scalar_field(field: ModelField) -> bool: ) and not isinstance(field.field_info, params.Body) -def is_sequence_field(field: ModelField) -> bool: - return shared.field_annotation_is_sequence(field.field_info.annotation) - - -def is_scalar_sequence_field(field: ModelField) -> bool: - return shared.field_annotation_is_scalar_sequence(field.field_info.annotation) - - -def is_bytes_field(field: ModelField) -> bool: - return shared.is_bytes_or_nonable_bytes_annotation(field.type_) - - -def is_bytes_sequence_field(field: ModelField) -> bool: - return shared.is_bytes_sequence_annotation(field.type_) - - def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: cls = type(field_info) merged_field_info = cls.from_annotation(annotation) @@ -441,7 +320,7 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: 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[int | str, ...]) -> dict[str, Any]: error = ValidationError.from_exception_data( "Field required", [{"type": "missing", "loc": loc, "input": {}}] ).errors(include_url=False)[0] @@ -483,7 +362,7 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: # 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]] +TypeModelOrEnum = type["BaseModel"] | type[Enum] TypeModelSet = set[TypeModelOrEnum] @@ -499,19 +378,8 @@ def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str return {v: k for k, v in name_model_map.items()} -def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - all_flat_models: TypeModelSet = 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) - - model_name_map = get_model_name_map(all_flat_models) - return model_name_map - - def get_flat_models_from_model( - model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None + model: type["BaseModel"], known_models: TypeModelSet | None = None ) -> TypeModelSet: known_models = known_models or set() fields = get_model_fields(model) @@ -525,10 +393,11 @@ def get_flat_models_from_annotation( origin = get_origin(annotation) if origin is not None: for arg in get_args(annotation): - if lenient_issubclass(arg, (BaseModel, Enum)) and arg not in known_models: - known_models.add(arg) - if lenient_issubclass(arg, BaseModel): - get_flat_models_from_model(arg, known_models=known_models) + if lenient_issubclass(arg, (BaseModel, Enum)): + if arg not in known_models: + known_models.add(arg) # type: ignore[arg-type] + if lenient_issubclass(arg, BaseModel): + get_flat_models_from_model(arg, known_models=known_models) else: get_flat_models_from_annotation(arg, known_models=known_models) return known_models @@ -537,7 +406,7 @@ def get_flat_models_from_annotation( def get_flat_models_from_field( field: ModelField, known_models: TypeModelSet ) -> TypeModelSet: - field_type = field.type_ + field_type = field.field_info.annotation if lenient_issubclass(field_type, BaseModel): if field_type in known_models: return known_models @@ -559,7 +428,7 @@ def get_flat_models_from_fields( def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] + *, errors: Sequence[Any], loc_prefix: tuple[str | int, ...] ) -> list[dict[str, Any]]: updated_loc_errors: list[Any] = [ {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors diff --git a/fastapi/applications.py b/fastapi/applications.py index 340cabfc2..41d86143e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,12 +1,9 @@ -from collections.abc import Awaitable, Coroutine, Sequence +from collections.abc import Awaitable, Callable, Coroutine, Sequence from enum import Enum from typing import ( Annotated, Any, - Callable, - Optional, TypeVar, - Union, ) from annotated_doc import Doc @@ -77,7 +74,7 @@ class FastAPI(Starlette): ), ] = False, routes: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -120,7 +117,7 @@ class FastAPI(Starlette): ), ] = "FastAPI", summary: Annotated[ - Optional[str], + str | None, Doc( """ A short summary of the API. @@ -203,7 +200,7 @@ class FastAPI(Starlette): ), ] = "0.1.0", openapi_url: Annotated[ - Optional[str], + str | None, Doc( """ The URL where the OpenAPI schema will be served from. @@ -226,7 +223,7 @@ class FastAPI(Starlette): ), ] = "/openapi.json", openapi_tags: Annotated[ - Optional[list[dict[str, Any]]], + list[dict[str, Any]] | None, Doc( """ A list of tags used by OpenAPI, these are the same `tags` you can set @@ -286,7 +283,7 @@ class FastAPI(Starlette): ), ] = None, servers: Annotated[ - Optional[list[dict[str, Union[str, Any]]]], + list[dict[str, str | Any]] | None, Doc( """ A `list` of `dict`s with connectivity information to a target server. @@ -335,7 +332,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of global dependencies, they will be applied to each @@ -402,7 +399,7 @@ class FastAPI(Starlette): ), ] = True, docs_url: Annotated[ - Optional[str], + str | None, Doc( """ The path to the automatic interactive API documentation. @@ -426,7 +423,7 @@ class FastAPI(Starlette): ), ] = "/docs", redoc_url: Annotated[ - Optional[str], + str | None, Doc( """ The path to the alternative automatic interactive API documentation @@ -450,7 +447,7 @@ class FastAPI(Starlette): ), ] = "/redoc", swagger_ui_oauth2_redirect_url: Annotated[ - Optional[str], + str | None, Doc( """ The OAuth2 redirect endpoint for the Swagger UI. @@ -463,7 +460,7 @@ class FastAPI(Starlette): ), ] = "/docs/oauth2-redirect", swagger_ui_init_oauth: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ OAuth2 configuration for the Swagger UI, by default shown at `/docs`. @@ -474,7 +471,7 @@ class FastAPI(Starlette): ), ] = None, middleware: Annotated[ - Optional[Sequence[Middleware]], + Sequence[Middleware] | None, Doc( """ List of middleware to be added when creating the application. @@ -488,12 +485,11 @@ class FastAPI(Starlette): ), ] = None, exception_handlers: Annotated[ - Optional[ - dict[ - Union[int, type[Exception]], - Callable[[Request, Any], Coroutine[Any, Any, Response]], - ] - ], + dict[ + int | type[Exception], + Callable[[Request, Any], Coroutine[Any, Any, Response]], + ] + | None, Doc( """ A dictionary with handlers for exceptions. @@ -507,7 +503,7 @@ class FastAPI(Starlette): ), ] = None, on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of startup event handler functions. @@ -519,7 +515,7 @@ class FastAPI(Starlette): ), ] = None, on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of shutdown event handler functions. @@ -532,7 +528,7 @@ class FastAPI(Starlette): ), ] = None, lifespan: Annotated[ - Optional[Lifespan[AppType]], + Lifespan[AppType] | None, Doc( """ A `Lifespan` context manager handler. This replaces `startup` and @@ -544,7 +540,7 @@ class FastAPI(Starlette): ), ] = None, terms_of_service: Annotated[ - Optional[str], + str | None, Doc( """ A URL to the Terms of Service for your API. @@ -563,7 +559,7 @@ class FastAPI(Starlette): ), ] = None, contact: Annotated[ - Optional[dict[str, Union[str, Any]]], + dict[str, str | Any] | None, Doc( """ A dictionary with the contact information for the exposed API. @@ -596,7 +592,7 @@ class FastAPI(Starlette): ), ] = None, license_info: Annotated[ - Optional[dict[str, Union[str, Any]]], + dict[str, str | Any] | None, Doc( """ A dictionary with the license information for the exposed API. @@ -685,7 +681,7 @@ class FastAPI(Starlette): ), ] = True, responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -701,7 +697,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations*. @@ -714,7 +710,7 @@ class FastAPI(Starlette): ), ] = None, webhooks: Annotated[ - Optional[routing.APIRouter], + routing.APIRouter | None, Doc( """ Add OpenAPI webhooks. This is similar to `callbacks` but it doesn't @@ -730,7 +726,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* as deprecated. You probably don't need it, @@ -758,7 +754,7 @@ class FastAPI(Starlette): ), ] = True, swagger_ui_parameters: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Parameters to configure Swagger UI, the autogenerated interactive API @@ -819,7 +815,7 @@ class FastAPI(Starlette): ), ] = True, openapi_external_docs: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ This field allows you to provide additional external documentation links. @@ -905,7 +901,7 @@ class FastAPI(Starlette): """ ), ] = "3.1.0" - self.openapi_schema: Optional[dict[str, Any]] = None + self.openapi_schema: dict[str, Any] | None = 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'" @@ -980,7 +976,7 @@ class FastAPI(Starlette): generate_unique_id_function=generate_unique_id_function, ) self.exception_handlers: dict[ - Any, Callable[[Request, Any], Union[Response, Awaitable[Response]]] + Any, Callable[[Request, Any], Response | Awaitable[Response]] ] = {} if exception_handlers is None else dict(exception_handlers) self.exception_handlers.setdefault(HTTPException, http_exception_handler) self.exception_handlers.setdefault( @@ -995,7 +991,7 @@ class FastAPI(Starlette): self.user_middleware: list[Middleware] = ( [] if middleware is None else list(middleware) ) - self.middleware_stack: Union[ASGIApp, None] = None + self.middleware_stack: ASGIApp | None = None self.setup() def build_middleware_stack(self) -> ASGIApp: @@ -1143,28 +1139,26 @@ class FastAPI(Starlette): endpoint: Callable[..., Any], *, response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[list[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[Depends] | None = None, + summary: str | None = None, + description: str | None = None, response_description: str = "Successful Response", - responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - methods: Optional[list[str]] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + methods: list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - response_class: Union[type[Response], DefaultPlaceholder] = Default( - JSONResponse - ), - name: Optional[str] = None, - openapi_extra: Optional[dict[str, Any]] = None, + response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), + name: str | None = None, + openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), @@ -1201,26 +1195,26 @@ class FastAPI(Starlette): path: str, *, response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[list[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[Depends] | None = None, + summary: str | None = None, + description: str | None = None, response_description: str = "Successful Response", - responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - methods: Optional[list[str]] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + methods: list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: type[Response] = Default(JSONResponse), - name: Optional[str] = None, - openapi_extra: Optional[dict[str, Any]] = None, + name: str | None = None, + openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), @@ -1260,9 +1254,9 @@ class FastAPI(Starlette): self, path: str, endpoint: Callable[..., Any], - name: Optional[str] = None, + name: str | None = None, *, - dependencies: Optional[Sequence[Depends]] = None, + dependencies: Sequence[Depends] | None = None, ) -> None: self.router.add_api_websocket_route( path, @@ -1282,7 +1276,7 @@ class FastAPI(Starlette): ), ], name: Annotated[ - Optional[str], + str | None, Doc( """ A name for the WebSocket. Only used internally. @@ -1291,7 +1285,7 @@ class FastAPI(Starlette): ] = None, *, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be used for this @@ -1342,7 +1336,7 @@ class FastAPI(Starlette): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -1356,7 +1350,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -1384,7 +1378,7 @@ class FastAPI(Starlette): ), ] = None, responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -1400,7 +1394,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all the *path operations* in this router as deprecated. @@ -1479,7 +1473,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1589,7 +1583,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -1602,7 +1596,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -1615,7 +1609,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -1627,7 +1621,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -1640,7 +1634,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -1668,7 +1662,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -1678,7 +1672,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -1688,7 +1682,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -1708,7 +1702,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -1720,7 +1714,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -1822,7 +1816,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -1830,7 +1824,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1846,7 +1840,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -1962,7 +1956,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -1975,7 +1969,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -1988,7 +1982,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -2000,7 +1994,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -2013,7 +2007,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -2041,7 +2035,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -2051,7 +2045,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -2061,7 +2055,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -2081,7 +2075,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -2093,7 +2087,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2195,7 +2189,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2203,7 +2197,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2219,7 +2213,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2340,7 +2334,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -2353,7 +2347,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -2366,7 +2360,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -2378,7 +2372,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -2391,7 +2385,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -2419,7 +2413,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -2429,7 +2423,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -2439,7 +2433,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -2459,7 +2453,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -2471,7 +2465,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2573,7 +2567,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2581,7 +2575,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2597,7 +2591,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2718,7 +2712,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -2731,7 +2725,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -2744,7 +2738,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -2756,7 +2750,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -2769,7 +2763,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -2797,7 +2791,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -2807,7 +2801,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -2817,7 +2811,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -2837,7 +2831,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -2849,7 +2843,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2951,7 +2945,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2959,7 +2953,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2975,7 +2969,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3091,7 +3085,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -3104,7 +3098,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3117,7 +3111,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3129,7 +3123,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3142,7 +3136,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3170,7 +3164,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3180,7 +3174,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3190,7 +3184,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3210,7 +3204,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3222,7 +3216,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -3324,7 +3318,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -3332,7 +3326,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3348,7 +3342,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3464,7 +3458,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -3477,7 +3471,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3490,7 +3484,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3502,7 +3496,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3515,7 +3509,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3543,7 +3537,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3553,7 +3547,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3563,7 +3557,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3583,7 +3577,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3595,7 +3589,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -3697,7 +3691,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -3705,7 +3699,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3721,7 +3715,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3837,7 +3831,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -3850,7 +3844,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3863,7 +3857,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3875,7 +3869,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3888,7 +3882,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3916,7 +3910,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3926,7 +3920,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3936,7 +3930,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3956,7 +3950,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3968,7 +3962,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -4070,7 +4064,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -4078,7 +4072,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4094,7 +4088,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4215,7 +4209,7 @@ class FastAPI(Starlette): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -4228,7 +4222,7 @@ class FastAPI(Starlette): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -4241,7 +4235,7 @@ class FastAPI(Starlette): ), ] = None, dependencies: Annotated[ - Optional[Sequence[Depends]], + Sequence[Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -4253,7 +4247,7 @@ class FastAPI(Starlette): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -4266,7 +4260,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -4294,7 +4288,7 @@ class FastAPI(Starlette): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -4304,7 +4298,7 @@ class FastAPI(Starlette): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -4314,7 +4308,7 @@ class FastAPI(Starlette): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -4334,7 +4328,7 @@ class FastAPI(Starlette): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -4346,7 +4340,7 @@ class FastAPI(Starlette): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -4448,7 +4442,7 @@ class FastAPI(Starlette): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -4456,7 +4450,7 @@ class FastAPI(Starlette): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4472,7 +4466,7 @@ class FastAPI(Starlette): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4541,7 +4535,7 @@ class FastAPI(Starlette): ) def websocket_route( - self, path: str, name: Union[str, None] = None + self, path: str, name: str | None = None ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.router.add_websocket_route(path, func, name=name) @@ -4627,7 +4621,7 @@ class FastAPI(Starlette): def exception_handler( self, exc_class_or_status_code: Annotated[ - Union[int, type[Exception]], + 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 20803ba67..7677058c4 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1,4 +1,5 @@ -from typing import Annotated, Any, Callable +from collections.abc import Callable +from typing import Annotated, Any from annotated_doc import Doc from starlette.background import BackgroundTasks as StarletteBackgroundTasks diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 2bf5fdb26..c04b5f0f3 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,10 +1,8 @@ -from collections.abc import Mapping +from collections.abc import Callable, Mapping from typing import ( Annotated, Any, BinaryIO, - Callable, - Optional, TypeVar, cast, ) @@ -58,11 +56,11 @@ class UploadFile(StarletteUploadFile): BinaryIO, Doc("The standard Python file object (non-async)."), ] - filename: Annotated[Optional[str], Doc("The original file name.")] - size: Annotated[Optional[int], Doc("The size of the file in bytes.")] + filename: Annotated[str | None, Doc("The original file name.")] + size: Annotated[int | None, Doc("The size of the file in bytes.")] headers: Annotated[Headers, Doc("The headers of the request.")] content_type: Annotated[ - Optional[str], Doc("The content type of the request, from the headers.") + str | None, Doc("The content type of the request, from the headers.") ] async def write( diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 58392326d..25ffb0d2d 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,13 +1,13 @@ import inspect import sys +from collections.abc import Callable from dataclasses import dataclass, field from functools import cached_property, partial -from typing import Any, Callable, Optional, Union +from typing import Any, Literal from fastapi._compat import ModelField from fastapi.security.base import SecurityBase from fastapi.types import DependencyCacheKey -from typing_extensions import Literal if sys.version_info >= (3, 13): # pragma: no cover from inspect import iscoroutinefunction @@ -15,7 +15,7 @@ else: # pragma: no cover from asyncio import iscoroutinefunction -def _unwrapped_call(call: Optional[Callable[..., Any]]) -> Any: +def _unwrapped_call(call: Callable[..., Any] | None) -> Any: if call is None: return call # pragma: no cover unwrapped = inspect.unwrap(_impartial(call)) @@ -36,19 +36,19 @@ class Dependant: 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 - websocket_param_name: Optional[str] = None - http_connection_param_name: Optional[str] = None - response_param_name: Optional[str] = None - background_tasks_param_name: Optional[str] = None - security_scopes_param_name: Optional[str] = None - own_oauth_scopes: Optional[list[str]] = None - parent_oauth_scopes: Optional[list[str]] = None + name: str | None = None + call: Callable[..., Any] | None = None + request_param_name: str | None = None + websocket_param_name: str | None = None + http_connection_param_name: str | None = None + response_param_name: str | None = None + background_tasks_param_name: str | None = None + security_scopes_param_name: str | None = None + own_oauth_scopes: list[str] | None = None + parent_oauth_scopes: list[str] | None = None use_cache: bool = True - path: Optional[str] = None - scope: Union[Literal["function", "request"], None] = None + path: str | None = None + scope: Literal["function", "request"] | None = None @cached_property def oauth_scopes(self) -> list[str]: @@ -185,7 +185,7 @@ class Dependant: return False @cached_property - def computed_scope(self) -> Union[str, None]: + def computed_scope(self) -> str | None: if self.scope: return self.scope if self.is_gen_callable or self.is_async_gen_callable: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index fc5dfed85..ab18ec2db 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,38 +1,37 @@ import dataclasses import inspect import sys -from collections.abc import Coroutine, Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass from typing import ( Annotated, Any, - Callable, ForwardRef, - Optional, + Literal, Union, cast, + get_args, + get_origin, ) -import anyio from fastapi import params from fastapi._compat import ( ModelField, RequiredParam, Undefined, - _regenerate_error_with_loc, copy_field_info, create_body_model, evaluate_forwardref, field_annotation_is_scalar, + field_annotation_is_scalar_sequence, + field_annotation_is_sequence, get_cached_model_fields, get_missing_field_error, - is_bytes_field, - is_bytes_sequence_field, + is_bytes_or_nonable_bytes_annotation, + is_bytes_sequence_annotation, is_scalar_field, - is_scalar_sequence_field, - is_sequence_field, is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, lenient_issubclass, @@ -51,7 +50,7 @@ from fastapi.logger import logger 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 +from pydantic import BaseModel, Json from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -65,7 +64,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 Literal, get_args, get_origin +from typing_inspection.typing_objects import is_typealiastype multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -128,8 +127,8 @@ def get_flat_dependant( dependant: Dependant, *, skip_repeats: bool = False, - visited: Optional[list[DependencyCacheKey]] = None, - parent_oauth_scopes: Optional[list[str]] = None, + visited: list[DependencyCacheKey] | None = None, + parent_oauth_scopes: list[str] | None = None, ) -> Dependant: if visited is None: visited = [] @@ -182,8 +181,10 @@ 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 lenient_issubclass(first_field.type_, BaseModel): - fields_to_extract = get_cached_model_fields(first_field.type_) + if len(fields) == 1 and lenient_issubclass( + first_field.field_info.annotation, BaseModel + ): + fields_to_extract = get_cached_model_fields(first_field.field_info.annotation) return fields_to_extract return fields @@ -198,20 +199,17 @@ def get_flat_params(dependant: Dependant) -> list[ModelField]: 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 - if sys.version_info >= (3, 14): - from annotationlib import Format + 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 + if sys.version_info >= (3, 14): + from annotationlib import Format - signature = inspect.signature(call, annotation_format=Format.FORWARDREF) - else: - signature = inspect.signature(call) - else: - signature = inspect.signature(call) + signature = inspect.signature(call, annotation_format=Format.FORWARDREF) + else: + signature = inspect.signature(call) return signature @@ -257,11 +255,11 @@ def get_dependant( *, path: str, call: Callable[..., Any], - name: Optional[str] = None, - own_oauth_scopes: Optional[list[str]] = None, - parent_oauth_scopes: Optional[list[str]] = None, + name: str | None = None, + own_oauth_scopes: list[str] | None = None, + parent_oauth_scopes: list[str] | None = None, use_cache: bool = True, - scope: Union[Literal["function", "request"], None] = None, + scope: Literal["function", "request"] | None = None, ) -> Dependant: dependant = Dependant( call=call, @@ -330,7 +328,7 @@ def get_dependant( def add_non_field_param_to_dependency( *, param_name: str, type_annotation: Any, dependant: Dependant -) -> Optional[bool]: +) -> bool | None: if lenient_issubclass(type_annotation, Request): dependant.request_param_name = param_name return True @@ -355,8 +353,8 @@ def add_non_field_param_to_dependency( @dataclass class ParamDetails: type_annotation: Any - depends: Optional[params.Depends] - field: Optional[ModelField] + depends: params.Depends | None + field: ModelField | None def analyze_param( @@ -370,6 +368,9 @@ def analyze_param( depends = None type_annotation: Any = Any use_annotation: Any = Any + if is_typealiastype(annotation): + # unpack in case PEP 695 type syntax is used + annotation = annotation.__value__ if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation @@ -395,7 +396,7 @@ def analyze_param( ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_annotation: FieldInfo | params.Depends | None = ( fastapi_specific_annotations[-1] ) else: @@ -449,7 +450,9 @@ def analyze_param( depends = dataclasses.replace(depends, dependency=type_annotation) # Handle non-param type annotations like Request - if lenient_issubclass( + # Only apply special handling when there's no explicit Depends - if there's a Depends, + # the dependency will be called and its return value used instead of the special injection + if depends is None and lenient_issubclass( type_annotation, ( Request, @@ -460,7 +463,6 @@ def analyze_param( SecurityScopes, ), ): - assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}" assert field_info is None, ( f"Cannot specify FastAPI annotation for type {type_annotation!r}" ) @@ -508,7 +510,6 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default in (RequiredParam, Undefined), field_info=field_info, ) if is_path_param: @@ -518,12 +519,8 @@ def analyze_param( elif isinstance(field_info, params.Query): assert ( is_scalar_field(field) - or is_scalar_sequence_field(field) - or ( - lenient_issubclass(field.type_, BaseModel) - # For Pydantic v1 - and getattr(field, "shape", 1) == 1 - ) + or field_annotation_is_scalar_sequence(field.field_info.annotation) + or lenient_issubclass(field.field_info.annotation, BaseModel) ), f"Query parameter {param_name!r} must be one of the supported types" return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) @@ -560,20 +557,20 @@ async def _solve_generator( class SolvedDependency: values: dict[str, Any] errors: list[Any] - background_tasks: Optional[StarletteBackgroundTasks] + background_tasks: StarletteBackgroundTasks | None response: Response dependency_cache: dict[DependencyCacheKey, Any] async def solve_dependencies( *, - request: Union[Request, WebSocket], + request: Request | WebSocket, dependant: Dependant, - 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, + body: dict[str, Any] | FormData | None = None, + background_tasks: StarletteBackgroundTasks | None = None, + response: Response | None = None, + dependency_overrides_provider: Any | None = None, + dependency_cache: dict[DependencyCacheKey, Any] | None = 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, @@ -709,23 +706,26 @@ def _validate_value_with_model_field( *, field: ModelField, value: Any, values: dict[str, Any], loc: tuple[str, ...] ) -> tuple[Any, list[Any]]: if value is None: - if field.required: + if field.field_info.is_required(): return None, [get_missing_field_error(loc=loc)] else: return deepcopy(field.default), [] - v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) - return None, new_errors - else: - return v_, [] + return field.validate(value, values, loc=loc) + + +def _is_json_field(field: ModelField) -> bool: + return any(type(item) is Json for item in field.field_info.metadata) def _get_multidict_value( - field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None + field: ModelField, values: Mapping[str, Any], alias: str | None = None ) -> Any: alias = alias or get_validation_alias(field) - if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): + if ( + (not _is_json_field(field)) + and field_annotation_is_sequence(field.field_info.annotation) + and isinstance(values, (ImmutableMultiDict, Headers)) + ): value = values.getlist(alias) else: value = values.get(alias, None) @@ -736,9 +736,12 @@ def _get_multidict_value( and isinstance(value, str) # For type checks and value == "" ) - or (is_sequence_field(field) and len(value) == 0) + or ( + field_annotation_is_sequence(field.field_info.annotation) + and len(value) == 0 + ) ): - if field.required: + if field.field_info.is_required(): return else: return deepcopy(field.default) @@ -747,7 +750,7 @@ def _get_multidict_value( def request_params_to_args( fields: Sequence[ModelField], - received_params: Union[Mapping[str, Any], QueryParams, Headers], + received_params: Mapping[str, Any] | QueryParams | Headers, ) -> tuple[dict[str, Any], list[Any]]: values: dict[str, Any] = {} errors: list[dict[str, Any]] = [] @@ -759,8 +762,10 @@ def request_params_to_args( fields_to_extract = fields single_not_embedded_field = False default_convert_underscores = True - if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): - fields_to_extract = get_cached_model_fields(first_field.type_) + if len(fields) == 1 and lenient_issubclass( + first_field.field_info.annotation, BaseModel + ): + fields_to_extract = get_cached_model_fields(first_field.field_info.annotation) single_not_embedded_field = True # If headers are in a Pydantic model, the way to disable convert_underscores # would be with Header(convert_underscores=False) at the Pydantic model level @@ -864,8 +869,8 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool: # otherwise it has to be embedded, so that the key value pair can be extracted if ( 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_) + and not lenient_issubclass(first_field.field_info.annotation, BaseModel) + and not is_union_of_base_models(first_field.field_info.annotation) ): return True return False @@ -882,28 +887,20 @@ async def _extract_form_body( field_info = field.field_info if ( isinstance(field_info, params.File) - and is_bytes_field(field) + and is_bytes_or_nonable_bytes_annotation(field.field_info.annotation) and isinstance(value, UploadFile) ): value = await value.read() elif ( - is_bytes_sequence_field(field) + is_bytes_sequence_annotation(field.field_info.annotation) and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types assert isinstance(value, sequence_types) - results: list[Union[bytes, str]] = [] - - async def process_fn( - fn: Callable[[], Coroutine[Any, Any, Any]], - ) -> None: - result = await fn() - results.append(result) # noqa: B023 - - async with anyio.create_task_group() as tg: - for sub_value in value: - tg.start_soon(process_fn, sub_value.read) + results: list[bytes | str] = [] + for sub_value in value: + results.append(await sub_value.read()) value = serialize_sequence_value(field=field, value=results) if value is not None: values[get_validation_alias(field)] = value @@ -920,7 +917,7 @@ async def _extract_form_body( async def request_body_to_args( body_fields: list[ModelField], - received_body: Optional[Union[dict[str, Any], FormData]], + received_body: dict[str, Any] | FormData | None, embed_body_fields: bool, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: values: dict[str, Any] = {} @@ -934,10 +931,10 @@ async def request_body_to_args( if ( single_not_embedded_field - and lenient_issubclass(first_field.type_, BaseModel) + and lenient_issubclass(first_field.field_info.annotation, BaseModel) and isinstance(received_body, FormData) ): - fields_to_extract = get_cached_model_fields(first_field.type_) + fields_to_extract = get_cached_model_fields(first_field.field_info.annotation) if isinstance(received_body, FormData): body_to_process = await _extract_form_body(fields_to_extract, received_body) @@ -950,7 +947,7 @@ async def request_body_to_args( return {first_field.name: v_}, errors_ for field in body_fields: loc = ("body", get_validation_alias(field)) - value: Optional[Any] = None + value: Any | None = None if body_to_process is not None: try: value = body_to_process.get(get_validation_alias(field)) @@ -970,7 +967,7 @@ async def request_body_to_args( def get_body_field( *, flat_dependant: Dependant, name: str, embed_body_fields: bool -) -> Optional[ModelField]: +) -> ModelField | None: """ Get a ModelField representing the request body for a path operation, combining all body parameters into a single field if necessary. @@ -990,7 +987,9 @@ def get_body_field( BodyModel = create_body_model( fields=flat_dependant.body_params, model_name=model_name ) - required = any(True for f in flat_dependant.body_params if f.required) + required = any( + True for f in flat_dependant.body_params if f.field_info.is_required() + ) BodyFieldInfo_kwargs: dict[str, Any] = { "annotation": BodyModel, "alias": "body", @@ -1014,7 +1013,6 @@ def get_body_field( final_field = create_model_field( name="body", type_=BodyModel, - required=required, alias="body", field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index b4661be4f..e20255c11 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,6 +1,7 @@ import dataclasses import datetime from collections import defaultdict, deque +from collections.abc import Callable from decimal import Decimal from enum import Enum from ipaddress import ( @@ -14,7 +15,7 @@ from ipaddress import ( from pathlib import Path, PurePath from re import Pattern from types import GeneratorType -from typing import Annotated, Any, Callable, Optional, Union +from typing import Annotated, Any from uuid import UUID from annotated_doc import Doc @@ -33,13 +34,13 @@ from ._compat import ( # Taken from Pydantic v1 as is -def isoformat(o: Union[datetime.date, datetime.time]) -> str: +def isoformat(o: datetime.date | datetime.time) -> str: return o.isoformat() # Adapted from Pydantic v1 # TODO: pv2 should this return strings instead? -def decimal_encoder(dec_value: Decimal) -> Union[int, float]: +def decimal_encoder(dec_value: Decimal) -> int | float: """ Encodes a Decimal as int if there's no exponent, otherwise float @@ -118,7 +119,7 @@ def jsonable_encoder( ), ], include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Pydantic's `include` parameter, passed to Pydantic models to set the @@ -127,7 +128,7 @@ def jsonable_encoder( ), ] = None, exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Pydantic's `exclude` parameter, passed to Pydantic models to set the @@ -177,7 +178,7 @@ def jsonable_encoder( ), ] = False, custom_encoder: Annotated[ - Optional[dict[Any, Callable[[Any], Any]]], + dict[Any, Callable[[Any], Any]] | None, Doc( """ Pydantic's `custom_encoder` parameter, passed to Pydantic models to define diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 62b4674de..d7065c52f 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,5 +1,5 @@ -from collections.abc import Sequence -from typing import Annotated, Any, Optional, TypedDict, Union +from collections.abc import Mapping, Sequence +from typing import Annotated, Any, TypedDict from annotated_doc import Doc from pydantic import BaseModel, create_model @@ -68,7 +68,7 @@ class HTTPException(StarletteHTTPException): ), ] = None, headers: Annotated[ - Optional[dict[str, str]], + Mapping[str, str] | None, Doc( """ Any headers to send to the client in the response. @@ -137,7 +137,7 @@ class WebSocketException(StarletteWebSocketException): ), ], reason: Annotated[ - Union[str, None], + str | None, Doc( """ The reason to close the WebSocket connection. @@ -176,7 +176,7 @@ class ValidationException(Exception): self, errors: Sequence[Any], *, - endpoint_ctx: Optional[EndpointContext] = None, + endpoint_ctx: EndpointContext | None = None, ) -> None: self._errors = errors self.endpoint_ctx = endpoint_ctx @@ -215,7 +215,7 @@ class RequestValidationError(ValidationException): errors: Sequence[Any], *, body: Any = None, - endpoint_ctx: Optional[EndpointContext] = None, + endpoint_ctx: EndpointContext | None = None, ) -> None: super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body @@ -226,7 +226,7 @@ class WebSocketRequestValidationError(ValidationException): self, errors: Sequence[Any], *, - endpoint_ctx: Optional[EndpointContext] = None, + endpoint_ctx: EndpointContext | None = None, ) -> None: super().__init__(errors, endpoint_ctx=endpoint_ctx) @@ -237,7 +237,7 @@ class ResponseValidationError(ValidationException): errors: Sequence[Any], *, body: Any = None, - endpoint_ctx: Optional[EndpointContext] = None, + endpoint_ctx: EndpointContext | None = None, ) -> None: super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index bb387c609..b845f87c1 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -1,5 +1,5 @@ import json -from typing import Annotated, Any, Optional +from typing import Annotated, Any from annotated_doc import Doc from fastapi.encoders import jsonable_encoder @@ -85,7 +85,7 @@ def get_swagger_ui_html( ), ] = "https://fastapi.tiangolo.com/img/favicon.png", oauth2_redirect_url: Annotated[ - Optional[str], + str | None, Doc( """ The OAuth2 redirect URL, it is normally automatically handled by FastAPI. @@ -96,7 +96,7 @@ def get_swagger_ui_html( ), ] = None, init_oauth: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ A dictionary with Swagger UI OAuth2 initialization configurations. @@ -107,7 +107,7 @@ def get_swagger_ui_html( ), ] = None, swagger_ui_parameters: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Configuration parameters for Swagger UI. diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index ac6a6d52c..d7950241f 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,6 +1,6 @@ -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping from enum import Enum -from typing import Annotated, Any, Callable, Optional, Union +from typing import Annotated, Any, Literal, Optional, Union from fastapi._compat import with_info_plain_validator_function from fastapi.logger import logger @@ -10,7 +10,7 @@ from pydantic import ( Field, GetJsonSchemaHandler, ) -from typing_extensions import Literal, TypedDict +from typing_extensions import TypedDict from typing_extensions import deprecated as typing_deprecated try: @@ -59,37 +59,37 @@ class BaseModelWithConfig(BaseModel): class Contact(BaseModelWithConfig): - name: Optional[str] = None - url: Optional[AnyUrl] = None - email: Optional[EmailStr] = None + name: str | None = None + url: AnyUrl | None = None + email: EmailStr | None = None class License(BaseModelWithConfig): name: str - identifier: Optional[str] = None - url: Optional[AnyUrl] = None + identifier: str | None = None + url: AnyUrl | None = None class Info(BaseModelWithConfig): title: str - summary: Optional[str] = None - description: Optional[str] = None - termsOfService: Optional[str] = None - contact: Optional[Contact] = None - license: Optional[License] = None + summary: str | None = None + description: str | None = None + termsOfService: str | None = None + contact: Contact | None = None + license: License | None = None version: str class ServerVariable(BaseModelWithConfig): - enum: Annotated[Optional[list[str]], Field(min_length=1)] = None + enum: Annotated[list[str] | None, Field(min_length=1)] = None default: str - description: Optional[str] = None + description: str | None = None class Server(BaseModelWithConfig): - url: Union[AnyUrl, str] - description: Optional[str] = None - variables: Optional[dict[str, ServerVariable]] = None + url: AnyUrl | str + description: str | None = None + variables: dict[str, ServerVariable] | None = None class Reference(BaseModel): @@ -98,19 +98,19 @@ class Reference(BaseModel): class Discriminator(BaseModel): propertyName: str - mapping: Optional[dict[str, str]] = None + mapping: dict[str, str] | None = None class XML(BaseModelWithConfig): - name: Optional[str] = None - namespace: Optional[str] = None - prefix: Optional[str] = None - attribute: Optional[bool] = None - wrapped: Optional[bool] = None + name: str | None = None + namespace: str | None = None + prefix: str | None = None + attribute: bool | None = None + wrapped: bool | None = None class ExternalDocumentation(BaseModelWithConfig): - description: Optional[str] = None + description: str | None = None url: AnyUrl @@ -123,83 +123,80 @@ SchemaType = Literal[ class Schema(BaseModelWithConfig): # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu # Core Vocabulary - schema_: Optional[str] = Field(default=None, alias="$schema") - vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") - id: Optional[str] = Field(default=None, alias="$id") - anchor: Optional[str] = Field(default=None, alias="$anchor") - 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") - comment: Optional[str] = Field(default=None, alias="$comment") + schema_: str | None = Field(default=None, alias="$schema") + vocabulary: str | None = Field(default=None, alias="$vocabulary") + id: str | None = Field(default=None, alias="$id") + anchor: str | None = Field(default=None, alias="$anchor") + dynamicAnchor: str | None = Field(default=None, alias="$dynamicAnchor") + ref: str | None = Field(default=None, alias="$ref") + dynamicRef: str | None = Field(default=None, alias="$dynamicRef") + defs: dict[str, "SchemaOrBool"] | None = Field(default=None, alias="$defs") + comment: str | None = 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: list["SchemaOrBool"] | None = None + anyOf: list["SchemaOrBool"] | None = None + oneOf: list["SchemaOrBool"] | None = 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 - # 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 + dependentSchemas: dict[str, "SchemaOrBool"] | None = None + prefixItems: list["SchemaOrBool"] | None = None + items: Optional["SchemaOrBool"] = None contains: Optional["SchemaOrBool"] = None - properties: Optional[dict[str, "SchemaOrBool"]] = None - patternProperties: Optional[dict[str, "SchemaOrBool"]] = None + properties: dict[str, "SchemaOrBool"] | None = None + patternProperties: dict[str, "SchemaOrBool"] | None = 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 - const: Optional[Any] = None - multipleOf: Optional[float] = Field(default=None, gt=0) - maximum: Optional[float] = None - exclusiveMaximum: Optional[float] = None - minimum: Optional[float] = None - exclusiveMinimum: Optional[float] = None - maxLength: Optional[int] = Field(default=None, ge=0) - minLength: Optional[int] = Field(default=None, ge=0) - pattern: Optional[str] = None - maxItems: Optional[int] = Field(default=None, ge=0) - minItems: Optional[int] = Field(default=None, ge=0) - uniqueItems: Optional[bool] = None - maxContains: Optional[int] = Field(default=None, ge=0) - 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 + type: SchemaType | list[SchemaType] | None = None + enum: list[Any] | None = None + const: Any | None = None + multipleOf: float | None = Field(default=None, gt=0) + maximum: float | None = None + exclusiveMaximum: float | None = None + minimum: float | None = None + exclusiveMinimum: float | None = None + maxLength: int | None = Field(default=None, ge=0) + minLength: int | None = Field(default=None, ge=0) + pattern: str | None = None + maxItems: int | None = Field(default=None, ge=0) + minItems: int | None = Field(default=None, ge=0) + uniqueItems: bool | None = None + maxContains: int | None = Field(default=None, ge=0) + minContains: int | None = Field(default=None, ge=0) + maxProperties: int | None = Field(default=None, ge=0) + minProperties: int | None = Field(default=None, ge=0) + required: list[str] | None = None + dependentRequired: dict[str, set[str]] | None = 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 + format: str | None = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten # A Vocabulary for the Contents of String-Encoded Data - contentEncoding: Optional[str] = None - contentMediaType: Optional[str] = None + contentEncoding: str | None = None + contentMediaType: str | None = None contentSchema: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta # A Vocabulary for Basic Meta-Data Annotations - title: Optional[str] = None - description: Optional[str] = None - default: Optional[Any] = None - deprecated: Optional[bool] = None - readOnly: Optional[bool] = None - writeOnly: Optional[bool] = None - examples: Optional[list[Any]] = None + title: str | None = None + description: str | None = None + default: Any | None = None + deprecated: bool | None = None + readOnly: bool | None = None + writeOnly: bool | None = None + examples: list[Any] | None = 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 - xml: Optional[XML] = None - externalDocs: Optional[ExternalDocumentation] = None + discriminator: Discriminator | None = None + xml: XML | None = None + externalDocs: ExternalDocumentation | None = None example: Annotated[ - Optional[Any], + Any | None, typing_deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." @@ -209,14 +206,14 @@ class Schema(BaseModelWithConfig): # Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents # A JSON Schema MUST be an object or a boolean. -SchemaOrBool = Union[Schema, bool] +SchemaOrBool = Schema | bool class Example(TypedDict, total=False): - summary: Optional[str] - description: Optional[str] - value: Optional[Any] - externalValue: Optional[AnyUrl] + summary: str | None + description: str | None + value: Any | None + externalValue: AnyUrl | None __pydantic_config__ = {"extra": "allow"} # type: ignore[misc] @@ -229,33 +226,33 @@ class ParameterInType(Enum): class Encoding(BaseModelWithConfig): - contentType: Optional[str] = None - headers: Optional[dict[str, Union["Header", Reference]]] = None - style: Optional[str] = None - explode: Optional[bool] = None - allowReserved: Optional[bool] = None + contentType: str | None = None + headers: dict[str, Union["Header", Reference]] | None = None + style: str | None = None + explode: bool | None = None + allowReserved: bool | None = None 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 + schema_: Schema | Reference | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[str, Example | Reference] | None = None + encoding: dict[str, Encoding] | None = None class ParameterBase(BaseModelWithConfig): - description: Optional[str] = None - required: Optional[bool] = None - deprecated: Optional[bool] = None + description: str | None = None + required: bool | None = None + deprecated: bool | None = None # Serialization rules for simple scenarios - style: Optional[str] = None - explode: Optional[bool] = None - 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 + style: str | None = None + explode: bool | None = None + allowReserved: bool | None = None + schema_: Schema | Reference | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[str, Example | Reference] | None = None # Serialization rules for more complex scenarios - content: Optional[dict[str, MediaType]] = None + content: dict[str, MediaType] | None = None class Parameter(ParameterBase): @@ -268,57 +265,57 @@ class Header(ParameterBase): class RequestBody(BaseModelWithConfig): - description: Optional[str] = None + description: str | None = None content: dict[str, MediaType] - required: Optional[bool] = None + required: bool | None = None class Link(BaseModelWithConfig): - operationRef: Optional[str] = None - operationId: Optional[str] = None - parameters: Optional[dict[str, Union[Any, str]]] = None - requestBody: Optional[Union[Any, str]] = None - description: Optional[str] = None - server: Optional[Server] = None + operationRef: str | None = None + operationId: str | None = None + parameters: dict[str, Any | str] | None = None + requestBody: Any | str | None = None + description: str | None = None + server: Server | None = None 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: dict[str, Header | Reference] | None = None + content: dict[str, MediaType] | None = None + links: dict[str, Link | Reference] | None = None class Operation(BaseModelWithConfig): - 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 - requestBody: Optional[Union[RequestBody, Reference]] = None + tags: list[str] | None = None + summary: str | None = None + description: str | None = None + externalDocs: ExternalDocumentation | None = None + operationId: str | None = None + parameters: list[Parameter | Reference] | None = None + requestBody: RequestBody | Reference | None = None # Using Any for Specification Extensions - 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 + responses: dict[str, Response | Any] | None = None + callbacks: dict[str, dict[str, "PathItem"] | Reference] | None = None + deprecated: bool | None = None + security: list[dict[str, list[str]]] | None = None + servers: list[Server] | None = None class PathItem(BaseModelWithConfig): - ref: Optional[str] = Field(default=None, alias="$ref") - summary: Optional[str] = None - description: Optional[str] = None - get: Optional[Operation] = None - put: Optional[Operation] = None - post: Optional[Operation] = None - delete: Optional[Operation] = None - options: Optional[Operation] = None - head: Optional[Operation] = None - patch: Optional[Operation] = None - trace: Optional[Operation] = None - servers: Optional[list[Server]] = None - parameters: Optional[list[Union[Parameter, Reference]]] = None + ref: str | None = Field(default=None, alias="$ref") + summary: str | None = None + description: str | None = None + get: Operation | None = None + put: Operation | None = None + post: Operation | None = None + delete: Operation | None = None + options: Operation | None = None + head: Operation | None = None + patch: Operation | None = None + trace: Operation | None = None + servers: list[Server] | None = None + parameters: list[Parameter | Reference] | None = None class SecuritySchemeType(Enum): @@ -330,7 +327,7 @@ class SecuritySchemeType(Enum): class SecurityBase(BaseModelWithConfig): type_: SecuritySchemeType = Field(alias="type") - description: Optional[str] = None + description: str | None = None class APIKeyIn(Enum): @@ -352,11 +349,11 @@ class HTTPBase(SecurityBase): class HTTPBearer(HTTPBase): scheme: Literal["bearer"] = "bearer" - bearerFormat: Optional[str] = None + bearerFormat: str | None = None class OAuthFlow(BaseModelWithConfig): - refreshUrl: Optional[str] = None + refreshUrl: str | None = None scopes: dict[str, str] = {} @@ -378,10 +375,10 @@ class OAuthFlowAuthorizationCode(OAuthFlow): class OAuthFlows(BaseModelWithConfig): - implicit: Optional[OAuthFlowImplicit] = None - password: Optional[OAuthFlowPassword] = None - clientCredentials: Optional[OAuthFlowClientCredentials] = None - authorizationCode: Optional[OAuthFlowAuthorizationCode] = None + implicit: OAuthFlowImplicit | None = None + password: OAuthFlowPassword | None = None + clientCredentials: OAuthFlowClientCredentials | None = None + authorizationCode: OAuthFlowAuthorizationCode | None = None class OAuth2(SecurityBase): @@ -396,41 +393,41 @@ class OpenIdConnect(SecurityBase): openIdConnectUrl: str -SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer] +SecurityScheme = 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: dict[str, Schema | Reference] | None = None + responses: dict[str, Response | Reference] | None = None + parameters: dict[str, Parameter | Reference] | None = None + examples: dict[str, Example | Reference] | None = None + requestBodies: dict[str, RequestBody | Reference] | None = None + headers: dict[str, Header | Reference] | None = None + securitySchemes: dict[str, SecurityScheme | Reference] | None = None + links: dict[str, Link | Reference] | None = 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: dict[str, dict[str, PathItem] | Reference | Any] | None = None + pathItems: dict[str, PathItem | Reference] | None = None class Tag(BaseModelWithConfig): name: str - description: Optional[str] = None - externalDocs: Optional[ExternalDocumentation] = None + description: str | None = None + externalDocs: ExternalDocumentation | None = None class OpenAPI(BaseModelWithConfig): openapi: str info: Info - jsonSchemaDialect: Optional[str] = None - servers: Optional[list[Server]] = None + jsonSchemaDialect: str | None = None + servers: list[Server] | None = None # Using Any for Specification Extensions - 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 - externalDocs: Optional[ExternalDocumentation] = None + paths: dict[str, PathItem | Any] | None = None + webhooks: dict[str, PathItem | Reference] | None = None + components: Components | None = None + security: list[dict[str, list[str]]] | None = None + tags: list[Tag] | None = None + externalDocs: ExternalDocumentation | None = None Schema.model_rebuild() diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index d56027b50..812003aee 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -3,14 +3,15 @@ import http.client import inspect import warnings from collections.abc import Sequence -from typing import Any, Optional, Union, cast +from typing import Any, Literal, cast from fastapi import routing from fastapi._compat import ( ModelField, Undefined, - get_compat_model_name_map, get_definitions, + get_flat_models_from_fields, + get_model_name_map, get_schema_from_model_field, lenient_issubclass, ) @@ -37,7 +38,6 @@ from fastapi.utils import ( from pydantic import BaseModel from starlette.responses import JSONResponse from starlette.routing import BaseRoute -from typing_extensions import Literal validation_error_definition = { "title": "ValidationError", @@ -50,6 +50,8 @@ validation_error_definition = { }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, "required": ["loc", "msg", "type"], } @@ -126,7 +128,7 @@ def _get_openapi_operation_parameters( default_convert_underscores = True if len(flat_dependant.header_params) == 1: first_field = flat_dependant.header_params[0] - if lenient_issubclass(first_field.type_, BaseModel): + if lenient_issubclass(first_field.field_info.annotation, BaseModel): default_convert_underscores = getattr( first_field.field_info, "convert_underscores", True ) @@ -158,7 +160,7 @@ def _get_openapi_operation_parameters( parameter = { "name": name, "in": param_type.value, - "required": param.required, + "required": param.field_info.is_required(), "schema": param_schema, } if field_info.description: @@ -177,13 +179,13 @@ def _get_openapi_operation_parameters( def get_openapi_operation_request_body( *, - body_field: Optional[ModelField], + body_field: ModelField | None, model_name_map: ModelNameMap, field_mapping: dict[ tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, -) -> Optional[dict[str, Any]]: +) -> dict[str, Any] | None: if not body_field: return None assert isinstance(body_field, ModelField) @@ -195,7 +197,7 @@ 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 + required = body_field.field_info.is_required() request_body_oai: dict[str, Any] = {} if required: request_body_oai["required"] = required @@ -276,7 +278,7 @@ def get_openapi_path( else: current_response_class = route.response_class assert current_response_class, "A response class is needed to generate OpenAPI" - route_response_media_type: Optional[str] = current_response_class.media_type + route_response_media_type: str | None = current_response_class.media_type if route.include_in_schema: for method in route.methods: operation = get_openapi_operation_metadata( @@ -390,7 +392,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: dict[str, Any] | None = None if field: additional_field_schema = get_schema_from_model_field( field=field, @@ -405,7 +407,7 @@ def get_openapi_path( .setdefault("schema", {}) ) deep_dict_update(additional_schema, additional_field_schema) - status_text: Optional[str] = status_code_ranges.get( + status_text: str | None = status_code_ranges.get( str(additional_status_code).upper() ) or http.client.responses.get(int(additional_status_code)) description = ( @@ -479,17 +481,17 @@ def get_openapi( title: str, version: str, openapi_version: str = "3.1.0", - summary: Optional[str] = None, - description: Optional[str] = None, + summary: str | None = None, + description: str | None = 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, - terms_of_service: Optional[str] = None, - contact: Optional[dict[str, Union[str, Any]]] = None, - license_info: Optional[dict[str, Union[str, Any]]] = None, + webhooks: Sequence[BaseRoute] | None = None, + tags: list[dict[str, Any]] | None = None, + servers: list[dict[str, str | Any]] | None = None, + terms_of_service: str | None = None, + contact: dict[str, str | Any] | None = None, + license_info: dict[str, str | Any] | None = None, separate_input_output_schemas: bool = True, - external_docs: Optional[dict[str, Any]] = None, + external_docs: dict[str, Any] | None = None, ) -> dict[str, Any]: info: dict[str, Any] = {"title": title, "version": version} if summary: @@ -510,7 +512,8 @@ def get_openapi( 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) + flat_models = get_flat_models_from_fields(all_fields, known_models=set()) + model_name_map = get_model_name_map(flat_models) field_mapping, definitions = get_definitions( fields=all_fields, model_name_map=model_name_map, diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 9bd92be4c..4be504f43 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,12 +1,12 @@ -from collections.abc import Sequence -from typing import Annotated, Any, Callable, Optional, Union +from collections.abc import Callable, Sequence +from typing import Annotated, Any, Literal from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example from pydantic import AliasChoices, AliasPath -from typing_extensions import Literal, deprecated +from typing_extensions import deprecated _Unset: Any = Undefined @@ -25,7 +25,7 @@ def Path( # noqa: N802 ] = ..., *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -36,7 +36,7 @@ def Path( # noqa: N802 ), ] = _Unset, alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -48,7 +48,7 @@ def Path( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -56,7 +56,7 @@ def Path( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -65,7 +65,7 @@ def Path( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -75,7 +75,7 @@ def Path( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -86,7 +86,7 @@ def Path( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -94,7 +94,7 @@ def Path( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -106,7 +106,7 @@ def Path( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -118,7 +118,7 @@ def Path( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -129,7 +129,7 @@ def Path( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -141,7 +141,7 @@ def Path( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -149,7 +149,7 @@ def Path( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -157,7 +157,7 @@ def Path( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -165,7 +165,7 @@ def Path( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -176,7 +176,7 @@ def Path( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -184,7 +184,7 @@ def Path( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -192,7 +192,7 @@ def Path( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -200,7 +200,7 @@ def Path( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -208,7 +208,7 @@ def Path( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -216,7 +216,7 @@ def Path( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -224,7 +224,7 @@ def Path( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -235,14 +235,14 @@ def Path( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -259,7 +259,7 @@ def Path( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -280,7 +280,7 @@ def Path( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -369,7 +369,7 @@ def Query( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -380,7 +380,7 @@ def Query( # noqa: N802 ), ] = _Unset, alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -395,7 +395,7 @@ def Query( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -403,7 +403,7 @@ def Query( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -412,7 +412,7 @@ def Query( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -422,7 +422,7 @@ def Query( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -433,7 +433,7 @@ def Query( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -444,7 +444,7 @@ def Query( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -456,7 +456,7 @@ def Query( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -468,7 +468,7 @@ def Query( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -479,7 +479,7 @@ def Query( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -491,7 +491,7 @@ def Query( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -502,7 +502,7 @@ def Query( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -513,7 +513,7 @@ def Query( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -524,7 +524,7 @@ def Query( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -535,7 +535,7 @@ def Query( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -543,7 +543,7 @@ def Query( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -551,7 +551,7 @@ def Query( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -559,7 +559,7 @@ def Query( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -567,7 +567,7 @@ def Query( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -575,7 +575,7 @@ def Query( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -583,7 +583,7 @@ def Query( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -594,14 +594,14 @@ def Query( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -618,7 +618,7 @@ def Query( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -645,7 +645,7 @@ def Query( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -710,7 +710,7 @@ def Header( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -721,7 +721,7 @@ def Header( # noqa: N802 ), ] = _Unset, alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -733,7 +733,7 @@ def Header( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -741,7 +741,7 @@ def Header( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -750,7 +750,7 @@ def Header( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -771,7 +771,7 @@ def Header( # noqa: N802 ), ] = True, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -779,7 +779,7 @@ def Header( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -787,7 +787,7 @@ def Header( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -796,7 +796,7 @@ def Header( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -805,7 +805,7 @@ def Header( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -813,7 +813,7 @@ def Header( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -822,7 +822,7 @@ def Header( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -830,7 +830,7 @@ def Header( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -838,7 +838,7 @@ def Header( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -846,7 +846,7 @@ def Header( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -857,7 +857,7 @@ def Header( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -865,7 +865,7 @@ def Header( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -873,7 +873,7 @@ def Header( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -881,7 +881,7 @@ def Header( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -889,7 +889,7 @@ def Header( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -897,7 +897,7 @@ def Header( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -905,7 +905,7 @@ def Header( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -916,14 +916,14 @@ def Header( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -940,7 +940,7 @@ def Header( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -961,7 +961,7 @@ def Header( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -1027,7 +1027,7 @@ def Cookie( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -1038,7 +1038,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -1050,7 +1050,7 @@ def Cookie( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -1058,7 +1058,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1067,7 +1067,7 @@ def Cookie( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -1077,7 +1077,7 @@ def Cookie( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -1085,7 +1085,7 @@ def Cookie( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -1093,7 +1093,7 @@ def Cookie( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -1102,7 +1102,7 @@ def Cookie( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -1111,7 +1111,7 @@ def Cookie( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -1119,7 +1119,7 @@ def Cookie( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -1128,7 +1128,7 @@ def Cookie( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -1136,7 +1136,7 @@ def Cookie( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -1144,7 +1144,7 @@ def Cookie( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1152,7 +1152,7 @@ def Cookie( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1163,7 +1163,7 @@ def Cookie( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -1171,7 +1171,7 @@ def Cookie( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -1179,7 +1179,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -1187,7 +1187,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -1195,7 +1195,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -1203,7 +1203,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -1211,7 +1211,7 @@ def Cookie( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -1222,14 +1222,14 @@ def Cookie( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -1246,7 +1246,7 @@ def Cookie( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -1267,7 +1267,7 @@ def Cookie( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -1332,7 +1332,7 @@ def Body( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -1343,7 +1343,7 @@ def Body( # noqa: N802 ), ] = _Unset, embed: Annotated[ - Union[bool, None], + bool | None, Doc( """ When `embed` is `True`, the parameter will be expected in a JSON body as a @@ -1366,7 +1366,7 @@ def Body( # noqa: N802 ), ] = "application/json", alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -1378,7 +1378,7 @@ def Body( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -1386,7 +1386,7 @@ def Body( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1395,7 +1395,7 @@ def Body( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -1405,7 +1405,7 @@ def Body( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -1413,7 +1413,7 @@ def Body( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -1421,7 +1421,7 @@ def Body( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -1430,7 +1430,7 @@ def Body( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -1439,7 +1439,7 @@ def Body( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -1447,7 +1447,7 @@ def Body( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -1456,7 +1456,7 @@ def Body( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -1464,7 +1464,7 @@ def Body( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -1472,7 +1472,7 @@ def Body( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1480,7 +1480,7 @@ def Body( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1491,7 +1491,7 @@ def Body( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -1499,7 +1499,7 @@ def Body( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -1507,7 +1507,7 @@ def Body( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -1515,7 +1515,7 @@ def Body( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -1523,7 +1523,7 @@ def Body( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -1531,7 +1531,7 @@ def Body( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -1539,7 +1539,7 @@ def Body( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -1550,14 +1550,14 @@ def Body( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -1574,7 +1574,7 @@ def Body( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -1595,7 +1595,7 @@ def Body( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -1662,7 +1662,7 @@ def Form( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -1682,7 +1682,7 @@ def Form( # noqa: N802 ), ] = "application/x-www-form-urlencoded", alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -1694,7 +1694,7 @@ def Form( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -1702,7 +1702,7 @@ def Form( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1711,7 +1711,7 @@ def Form( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -1721,7 +1721,7 @@ def Form( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -1729,7 +1729,7 @@ def Form( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -1737,7 +1737,7 @@ def Form( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -1746,7 +1746,7 @@ def Form( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -1755,7 +1755,7 @@ def Form( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -1763,7 +1763,7 @@ def Form( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -1772,7 +1772,7 @@ def Form( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -1780,7 +1780,7 @@ def Form( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -1788,7 +1788,7 @@ def Form( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1796,7 +1796,7 @@ def Form( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -1807,7 +1807,7 @@ def Form( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -1815,7 +1815,7 @@ def Form( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -1823,7 +1823,7 @@ def Form( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -1831,7 +1831,7 @@ def Form( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -1839,7 +1839,7 @@ def Form( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -1847,7 +1847,7 @@ def Form( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -1855,7 +1855,7 @@ def Form( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -1866,14 +1866,14 @@ def Form( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -1890,7 +1890,7 @@ def Form( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -1911,7 +1911,7 @@ def Form( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -1977,7 +1977,7 @@ def File( # noqa: N802 ] = Undefined, *, default_factory: Annotated[ - Union[Callable[[], Any], None], + Callable[[], Any] | None, Doc( """ A callable to generate the default value. @@ -1997,7 +1997,7 @@ def File( # noqa: N802 ), ] = "multipart/form-data", alias: Annotated[ - Optional[str], + str | None, Doc( """ An alternative name for the parameter field. @@ -2009,7 +2009,7 @@ def File( # noqa: N802 ), ] = None, alias_priority: Annotated[ - Union[int, None], + int | None, Doc( """ Priority of the alias. This affects whether an alias generator is used. @@ -2017,7 +2017,7 @@ def File( # noqa: N802 ), ] = _Unset, validation_alias: Annotated[ - Union[str, AliasPath, AliasChoices, None], + str | AliasPath | AliasChoices | None, Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -2026,7 +2026,7 @@ def File( # noqa: N802 ), ] = None, serialization_alias: Annotated[ - Union[str, None], + str | None, Doc( """ 'Blacklist' validation step. The vanilla parameter field will be the @@ -2036,7 +2036,7 @@ def File( # noqa: N802 ), ] = None, title: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable title. @@ -2044,7 +2044,7 @@ def File( # noqa: N802 ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Human-readable description. @@ -2052,7 +2052,7 @@ def File( # noqa: N802 ), ] = None, gt: Annotated[ - Optional[float], + float | None, Doc( """ Greater than. If set, value must be greater than this. Only applicable to @@ -2061,7 +2061,7 @@ def File( # noqa: N802 ), ] = None, ge: Annotated[ - Optional[float], + float | None, Doc( """ Greater than or equal. If set, value must be greater than or equal to @@ -2070,7 +2070,7 @@ def File( # noqa: N802 ), ] = None, lt: Annotated[ - Optional[float], + float | None, Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. @@ -2078,7 +2078,7 @@ def File( # noqa: N802 ), ] = None, le: Annotated[ - Optional[float], + float | None, Doc( """ Less than or equal. If set, value must be less than or equal to this. @@ -2087,7 +2087,7 @@ def File( # noqa: N802 ), ] = None, min_length: Annotated[ - Optional[int], + int | None, Doc( """ Minimum length for strings. @@ -2095,7 +2095,7 @@ def File( # noqa: N802 ), ] = None, max_length: Annotated[ - Optional[int], + int | None, Doc( """ Maximum length for strings. @@ -2103,7 +2103,7 @@ def File( # noqa: N802 ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -2111,7 +2111,7 @@ def File( # noqa: N802 ), ] = None, regex: Annotated[ - Optional[str], + str | None, Doc( """ RegEx pattern for strings. @@ -2122,7 +2122,7 @@ def File( # noqa: N802 ), ] = None, discriminator: Annotated[ - Union[str, None], + str | None, Doc( """ Parameter field name for discriminating the type in a tagged union. @@ -2130,7 +2130,7 @@ def File( # noqa: N802 ), ] = None, strict: Annotated[ - Union[bool, None], + bool | None, Doc( """ If `True`, strict validation is applied to the field. @@ -2138,7 +2138,7 @@ def File( # noqa: N802 ), ] = _Unset, multiple_of: Annotated[ - Union[float, None], + float | None, Doc( """ Value must be a multiple of this. Only applicable to numbers. @@ -2146,7 +2146,7 @@ def File( # noqa: N802 ), ] = _Unset, allow_inf_nan: Annotated[ - Union[bool, None], + bool | None, Doc( """ Allow `inf`, `-inf`, `nan`. Only applicable to numbers. @@ -2154,7 +2154,7 @@ def File( # noqa: N802 ), ] = _Unset, max_digits: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of allow digits for strings. @@ -2162,7 +2162,7 @@ def File( # noqa: N802 ), ] = _Unset, decimal_places: Annotated[ - Union[int, None], + int | None, Doc( """ Maximum number of decimal places allowed for numbers. @@ -2170,7 +2170,7 @@ def File( # noqa: N802 ), ] = _Unset, examples: Annotated[ - Optional[list[Any]], + list[Any] | None, Doc( """ Example values for this field. @@ -2181,14 +2181,14 @@ def File( # noqa: N802 ), ] = None, example: Annotated[ - Optional[Any], + Any | None, deprecated( "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), ] = _Unset, openapi_examples: Annotated[ - Optional[dict[str, Example]], + dict[str, Example] | None, Doc( """ OpenAPI-specific examples. @@ -2205,7 +2205,7 @@ def File( # noqa: N802 ), ] = None, deprecated: Annotated[ - Union[deprecated, str, bool, None], + deprecated | str | bool | None, Doc( """ Mark this parameter field as deprecated. @@ -2226,7 +2226,7 @@ def File( # noqa: N802 ), ] = True, json_schema_extra: Annotated[ - Union[dict[str, Any], None], + dict[str, Any] | None, Doc( """ Any additional JSON schema data. @@ -2283,7 +2283,7 @@ def File( # noqa: N802 def Depends( # noqa: N802 dependency: Annotated[ - Optional[Callable[..., Any]], + Callable[..., Any] | None, Doc( """ A "dependable" callable (like a function). @@ -2315,7 +2315,7 @@ def Depends( # noqa: N802 ), ] = True, scope: Annotated[ - Union[Literal["function", "request"], None], + Literal["function", "request"] | None, Doc( """ Mainly for dependencies with `yield`, define when the dependency function @@ -2372,7 +2372,7 @@ def Depends( # noqa: N802 def Security( # noqa: N802 dependency: Annotated[ - Optional[Callable[..., Any]], + Callable[..., Any] | None, Doc( """ A "dependable" callable (like a function). @@ -2387,7 +2387,7 @@ def Security( # noqa: N802 ] = None, *, scopes: Annotated[ - Optional[Sequence[str]], + Sequence[str] | None, Doc( """ OAuth2 scopes required for the *path operation* that uses this Security diff --git a/fastapi/params.py b/fastapi/params.py index 72e797f83..68f987081 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,14 +1,14 @@ import warnings -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from enum import Enum -from typing import Annotated, Any, Callable, Optional, Union +from typing import Annotated, Any, Literal 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 Literal, deprecated +from typing_extensions import deprecated from ._compat import ( Undefined, @@ -31,45 +31,45 @@ class Param(FieldInfo): # type: ignore[misc] 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, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): if example is not _Unset: @@ -142,45 +142,45 @@ class Path(Param): # type: ignore[misc] self, default: Any = ..., *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): assert default is ..., "Path parameters cannot have a default value" @@ -226,45 +226,45 @@ class Query(Param): # type: ignore[misc] 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, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): super().__init__( @@ -308,46 +308,46 @@ class Header(Param): # type: ignore[misc] 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, - validation_alias: Union[str, AliasPath, AliasChoices, None] = None, - serialization_alias: Union[str, None] = None, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: 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, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): self.convert_underscores = convert_underscores @@ -392,45 +392,45 @@ class Cookie(Param): # type: ignore[misc] 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, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): super().__init__( @@ -472,47 +472,47 @@ class Body(FieldInfo): # type: ignore[misc] self, default: Any = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - embed: Union[bool, None] = None, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + embed: bool | None = None, media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): self.embed = embed @@ -584,46 +584,46 @@ class Form(Body): # type: ignore[misc] self, default: Any = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): super().__init__( @@ -666,46 +666,46 @@ class File(Form): # type: ignore[misc] self, default: Any = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - validation_alias: Union[str, AliasPath, AliasChoices, 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, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, regex: Annotated[ - Optional[str], + str | None, 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, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, example: Annotated[ - Optional[Any], + Any | None, 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, + openapi_examples: dict[str, Example] | None = None, + deprecated: deprecated | str | bool | None = None, include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, + json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): super().__init__( @@ -745,11 +745,11 @@ class File(Form): # type: ignore[misc] @dataclass(frozen=True) class Depends: - dependency: Optional[Callable[..., Any]] = None + dependency: Callable[..., Any] | None = None use_cache: bool = True - scope: Union[Literal["function", "request"], None] = None + scope: Literal["function", "request"] | None = None @dataclass(frozen=True) class Security(Depends): - scopes: Optional[Sequence[str]] = None + scopes: Sequence[str] | None = None diff --git a/fastapi/routing.py b/fastapi/routing.py index dc05f38d3..5e7c18bfa 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,23 +1,30 @@ +import contextlib import email.message import functools import inspect import json +import types from collections.abc import ( AsyncIterator, Awaitable, + Callable, Collection, Coroutine, + Generator, Mapping, Sequence, ) -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import ( + AbstractAsyncContextManager, + AbstractContextManager, + AsyncExitStack, + asynccontextmanager, +) from enum import Enum, IntEnum from typing import ( Annotated, Any, - Callable, - Optional, - Union, + TypeVar, ) from annotated_doc import Doc @@ -25,7 +32,6 @@ from fastapi import params from fastapi._compat import ( ModelField, Undefined, - annotation_is_pydantic_v1, lenient_issubclass, ) from fastapi.datastructures import Default, DefaultPlaceholder @@ -43,14 +49,12 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, FastAPIError, - PydanticV1NotSupportedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, ) from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( - create_cloned_field, create_model_field, generate_unique_id, get_value_or_default, @@ -78,7 +82,7 @@ from typing_extensions import deprecated # Copy of starlette.routing.request_response modified to include the # dependencies' AsyncExitStack def request_response( - func: Callable[[Request], Union[Awaitable[Response], Response]], + func: Callable[[Request], Awaitable[Response] | Response], ) -> ASGIApp: """ Takes a function or coroutine `func(request) -> response`, @@ -143,13 +147,57 @@ def websocket_session( return app +_T = TypeVar("_T") + + +# Vendored from starlette.routing to avoid importing private symbols +class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]): + """ + Wraps a synchronous context manager to make it async. + + This is vendored from Starlette to avoid importing private symbols. + """ + + def __init__(self, cm: AbstractContextManager[_T]) -> None: + self._cm = cm + + async def __aenter__(self) -> _T: + return self._cm.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_value, traceback) + + +# Vendored from starlette.routing to avoid importing private symbols +def _wrap_gen_lifespan_context( + lifespan_context: Callable[[Any], Generator[Any, Any, Any]], +) -> Callable[[Any], AbstractAsyncContextManager[Any]]: + """ + Wrap a generator-based lifespan context into an async context manager. + + This is vendored from Starlette to avoid importing private symbols. + """ + cmgr = contextlib.contextmanager(lifespan_context) + + @functools.wraps(cmgr) + def wrapper(app: Any) -> _AsyncLiftContextManager[Any]: + return _AsyncLiftContextManager(cmgr(app)) + + return wrapper + + def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @asynccontextmanager async def merged_lifespan( app: AppType, - ) -> AsyncIterator[Optional[Mapping[str, Any]]]: + ) -> AsyncIterator[Mapping[str, Any] | None]: async with original_context(app) as maybe_original_state: async with nested_context(app) as maybe_nested_state: if maybe_nested_state is None and maybe_original_state is None: @@ -160,6 +208,30 @@ def _merge_lifespan_context( return merged_lifespan # type: ignore[return-value] +class _DefaultLifespan: + """ + Default lifespan context manager that runs on_startup and on_shutdown handlers. + + This is a copy of the Starlette _DefaultLifespan class that was removed + in Starlette. FastAPI keeps it to maintain backward compatibility with + on_startup and on_shutdown event handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + + def __init__(self, router: "APIRouter") -> None: + self._router = router + + async def __aenter__(self) -> None: + await self._router._startup() + + async def __aexit__(self, *exc_info: object) -> None: + await self._router._shutdown() + + def __call__(self: _T, app: object) -> _T: + return self + + # Cache for endpoint context to avoid re-extracting on every request _endpoint_context_cache: dict[int, EndpointContext] = {} @@ -189,27 +261,24 @@ def _extract_endpoint_context(func: Any) -> EndpointContext: async def serialize_response( *, - field: Optional[ModelField] = None, + field: ModelField | None = None, response_content: Any, - include: Optional[IncEx] = None, - exclude: Optional[IncEx] = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, - endpoint_ctx: Optional[EndpointContext] = None, + endpoint_ctx: EndpointContext | None = None, ) -> Any: if field: - errors = [] if is_coroutine: - value, errors_ = field.validate(response_content, {}, loc=("response",)) + value, errors = field.validate(response_content, {}, loc=("response",)) else: - value, errors_ = await run_in_threadpool( + value, errors = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) - if isinstance(errors_, list): - errors.extend(errors_) if errors: ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( @@ -247,17 +316,17 @@ async def run_endpoint_function( 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_field: Optional[ModelField] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, + body_field: ModelField | None = None, + status_code: int | None = None, + response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), + response_field: ModelField | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, - dependency_overrides_provider: Optional[Any] = None, + dependency_overrides_provider: Any | None = None, embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" @@ -392,7 +461,7 @@ def get_request_handler( def get_websocket_app( dependant: Dependant, - dependency_overrides_provider: Optional[Any] = None, + dependency_overrides_provider: Any | None = None, embed_body_fields: bool = False, ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: @@ -433,9 +502,9 @@ class APIWebSocketRoute(routing.WebSocketRoute): path: str, endpoint: Callable[..., Any], *, - name: Optional[str] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - dependency_overrides_provider: Optional[Any] = None, + name: str | None = None, + dependencies: Sequence[params.Depends] | None = None, + dependency_overrides_provider: Any | None = None, ) -> None: self.path = path self.endpoint = endpoint @@ -476,33 +545,30 @@ class APIRoute(routing.Route): endpoint: Callable[..., Any], *, response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[list[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[params.Depends] | None = None, + summary: str | None = None, + description: str | None = None, response_description: str = "Successful Response", - 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, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + name: str | None = None, + methods: set[str] | list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - 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, - generate_unique_id_function: Union[ - Callable[["APIRoute"], str], DefaultPlaceholder - ] = Default(generate_unique_id), + response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), + dependency_overrides_provider: Any | None = None, + callbacks: list[BaseRoute] | None = None, + openapi_extra: dict[str, Any] | None = None, + generate_unique_id_function: Callable[["APIRoute"], str] + | DefaultPlaceholder = Default(generate_unique_id), ) -> None: self.path = path self.endpoint = endpoint @@ -552,30 +618,13 @@ 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, mode="serialization", ) - # Create a clone of the field, so that a Pydantic submodel is not returned - # as is just because it's an instance of a subclass of a more limited class - # e.g. UserInDB (containing hashed_password) could be a subclass of User - # that doesn't have the hashed_password. But because it's a subclass, it - # would pass the validation and be returned as is. - # By being a new field, no inheritance will be passed as is. A new model - # will always be created. - # TODO: remove when deprecating Pydantic v1 - self.secure_cloned_response_field: Optional[ModelField] = ( - create_cloned_field(self.response_field) - ) else: self.response_field = None # type: ignore - self.secure_cloned_response_field = None self.dependencies = list(dependencies or []) self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") # if a "form feed" character (page break) is found in the description text, @@ -590,17 +639,12 @@ 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[int | str, ModelField] = response_fields else: self.response_fields = {} @@ -630,7 +674,7 @@ class APIRoute(routing.Route): body_field=self.body_field, status_code=self.status_code, response_class=self.response_class, - response_field=self.secure_cloned_response_field, + response_field=self.response_field, response_model_include=self.response_model_include, response_model_exclude=self.response_model_exclude, response_model_by_alias=self.response_model_by_alias, @@ -680,7 +724,7 @@ class APIRouter(routing.Router): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -694,7 +738,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -717,7 +761,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -733,7 +777,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -747,7 +791,7 @@ class APIRouter(routing.Router): ), ] = None, routes: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -778,7 +822,7 @@ class APIRouter(routing.Router): ), ] = True, default: Annotated[ - Optional[ASGIApp], + ASGIApp | None, Doc( """ Default function handler for this router. Used to handle @@ -787,7 +831,7 @@ class APIRouter(routing.Router): ), ] = None, dependency_overrides_provider: Annotated[ - Optional[Any], + Any | None, Doc( """ Only used internally by FastAPI to handle dependency overrides. @@ -809,7 +853,7 @@ class APIRouter(routing.Router): ), ] = APIRoute, on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of startup event handler functions. @@ -821,7 +865,7 @@ class APIRouter(routing.Router): ), ] = None, on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of shutdown event handler functions. @@ -836,7 +880,7 @@ class APIRouter(routing.Router): # the generic to Lifespan[AppType] is the type of the top level application # which the router cannot know statically, so we use typing.Any lifespan: Annotated[ - Optional[Lifespan[Any]], + Lifespan[Any] | None, Doc( """ A `Lifespan` context manager handler. This replaces `startup` and @@ -848,7 +892,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* in this router as deprecated. @@ -890,21 +934,42 @@ class APIRouter(routing.Router): ), ] = Default(generate_unique_id), ) -> None: + # Determine the lifespan context to use + if lifespan is None: + # Use the default lifespan that runs on_startup/on_shutdown handlers + lifespan_context: Lifespan[Any] = _DefaultLifespan(self) + elif inspect.isasyncgenfunction(lifespan): + lifespan_context = asynccontextmanager(lifespan) + elif inspect.isgeneratorfunction(lifespan): + lifespan_context = _wrap_gen_lifespan_context(lifespan) + else: + lifespan_context = lifespan + self.lifespan_context = lifespan_context + super().__init__( routes=routes, redirect_slashes=redirect_slashes, default=default, - on_startup=on_startup, - on_shutdown=on_shutdown, - lifespan=lifespan, + lifespan=lifespan_context, ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( "A path prefix must not end with '/', as the routes will start with '/'" ) + + # Handle on_startup/on_shutdown locally since Starlette removed support + # Ref: https://github.com/Kludex/starlette/pull/3117 + # TODO: deprecate this once the lifespan (or alternative) interface is improved + self.on_startup: list[Callable[[], Any]] = ( + [] if on_startup is None else list(on_startup) + ) + self.on_shutdown: list[Callable[[], Any]] = ( + [] if on_shutdown is None else list(on_shutdown) + ) + self.prefix = prefix - self.tags: list[Union[str, Enum]] = tags or [] + self.tags: list[str | Enum] = tags or [] self.dependencies = list(dependencies or []) self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -918,8 +983,8 @@ class APIRouter(routing.Router): def route( self, path: str, - methods: Optional[Collection[str]] = None, - name: Optional[str] = None, + methods: Collection[str] | None = None, + name: str | None = None, include_in_schema: bool = True, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: @@ -940,33 +1005,30 @@ class APIRouter(routing.Router): endpoint: Callable[..., Any], *, response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[list[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[params.Depends] | None = None, + summary: str | None = None, + description: str | None = None, response_description: str = "Successful Response", - responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, - deprecated: Optional[bool] = 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, + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + methods: set[str] | list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, - 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, - generate_unique_id_function: Union[ - Callable[[APIRoute], str], DefaultPlaceholder - ] = Default(generate_unique_id), + response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), + name: str | None = None, + route_class_override: type[APIRoute] | None = None, + callbacks: list[BaseRoute] | None = None, + openapi_extra: dict[str, Any] | None = None, + generate_unique_id_function: Callable[[APIRoute], str] + | DefaultPlaceholder = Default(generate_unique_id), ) -> None: route_class = route_class_override or self.route_class responses = responses or {} @@ -1021,27 +1083,27 @@ class APIRouter(routing.Router): path: str, *, response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[list[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[params.Depends] | None = None, + summary: str | None = None, + description: str | None = None, response_description: str = "Successful Response", - responses: Optional[dict[Union[int, str], dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - methods: Optional[list[str]] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + methods: list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[list[BaseRoute]] = None, - openapi_extra: Optional[dict[str, Any]] = None, + name: str | None = None, + callbacks: list[BaseRoute] | None = None, + openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[[APIRoute], str] = Default( generate_unique_id ), @@ -1082,9 +1144,9 @@ class APIRouter(routing.Router): self, path: str, endpoint: Callable[..., Any], - name: Optional[str] = None, + name: str | None = None, *, - dependencies: Optional[Sequence[params.Depends]] = None, + dependencies: Sequence[params.Depends] | None = None, ) -> None: current_dependencies = self.dependencies.copy() if dependencies: @@ -1110,7 +1172,7 @@ class APIRouter(routing.Router): ), ], name: Annotated[ - Optional[str], + str | None, Doc( """ A name for the WebSocket. Only used internally. @@ -1119,7 +1181,7 @@ class APIRouter(routing.Router): ] = None, *, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be used for this @@ -1167,7 +1229,7 @@ class APIRouter(routing.Router): return decorator def websocket_route( - self, path: str, name: Union[str, None] = None + self, path: str, name: str | None = None ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_websocket_route(path, func, name=name) @@ -1181,7 +1243,7 @@ class APIRouter(routing.Router): *, prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -1195,7 +1257,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -1218,7 +1280,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -1234,7 +1296,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -1248,7 +1310,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* in this router as deprecated. @@ -1310,6 +1372,10 @@ class APIRouter(routing.Router): app.include_router(internal_router) ``` """ + assert self is not router, ( + "Cannot include the same APIRouter instance into itself. " + "Did you mean to include a different router?" + ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( @@ -1467,7 +1533,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -1480,7 +1546,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -1493,7 +1559,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -1505,7 +1571,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -1518,7 +1584,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -1546,7 +1612,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -1556,7 +1622,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -1566,7 +1632,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -1586,7 +1652,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -1598,7 +1664,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -1700,7 +1766,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -1708,7 +1774,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -1724,7 +1790,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -1844,7 +1910,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -1857,7 +1923,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -1870,7 +1936,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -1882,7 +1948,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -1895,7 +1961,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -1923,7 +1989,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -1933,7 +1999,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -1943,7 +2009,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -1963,7 +2029,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -1975,7 +2041,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2077,7 +2143,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2085,7 +2151,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2101,7 +2167,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2226,7 +2292,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -2239,7 +2305,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -2252,7 +2318,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -2264,7 +2330,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -2277,7 +2343,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -2305,7 +2371,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -2315,7 +2381,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -2325,7 +2391,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -2345,7 +2411,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -2357,7 +2423,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2459,7 +2525,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2467,7 +2533,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2483,7 +2549,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2608,7 +2674,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -2621,7 +2687,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -2634,7 +2700,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -2646,7 +2712,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -2659,7 +2725,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -2687,7 +2753,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -2697,7 +2763,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -2707,7 +2773,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -2727,7 +2793,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -2739,7 +2805,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -2841,7 +2907,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -2849,7 +2915,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -2865,7 +2931,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -2985,7 +3051,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -2998,7 +3064,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3011,7 +3077,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3023,7 +3089,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3036,7 +3102,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3064,7 +3130,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3074,7 +3140,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3084,7 +3150,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3104,7 +3170,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3116,7 +3182,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -3218,7 +3284,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -3226,7 +3292,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3242,7 +3308,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3362,7 +3428,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -3375,7 +3441,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3388,7 +3454,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3400,7 +3466,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3413,7 +3479,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3441,7 +3507,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3451,7 +3517,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3461,7 +3527,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3481,7 +3547,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3493,7 +3559,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -3595,7 +3661,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -3603,7 +3669,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -3619,7 +3685,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -3744,7 +3810,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -3757,7 +3823,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -3770,7 +3836,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -3782,7 +3848,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -3795,7 +3861,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -3823,7 +3889,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -3833,7 +3899,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -3843,7 +3909,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -3863,7 +3929,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -3875,7 +3941,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -3977,7 +4043,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -3985,7 +4051,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4001,7 +4067,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4126,7 +4192,7 @@ class APIRouter(routing.Router): ), ] = Default(None), status_code: Annotated[ - Optional[int], + int | None, Doc( """ The default status code to be used for the response. @@ -4139,7 +4205,7 @@ class APIRouter(routing.Router): ), ] = None, tags: Annotated[ - Optional[list[Union[str, Enum]]], + list[str | Enum] | None, Doc( """ A list of tags to be applied to the *path operation*. @@ -4152,7 +4218,7 @@ class APIRouter(routing.Router): ), ] = None, dependencies: Annotated[ - Optional[Sequence[params.Depends]], + Sequence[params.Depends] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to the @@ -4164,7 +4230,7 @@ class APIRouter(routing.Router): ), ] = None, summary: Annotated[ - Optional[str], + str | None, Doc( """ A summary for the *path operation*. @@ -4177,7 +4243,7 @@ class APIRouter(routing.Router): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ A description for the *path operation*. @@ -4205,7 +4271,7 @@ class APIRouter(routing.Router): ), ] = "Successful Response", responses: Annotated[ - Optional[dict[Union[int, str], dict[str, Any]]], + dict[int | str, dict[str, Any]] | None, Doc( """ Additional responses that could be returned by this *path operation*. @@ -4215,7 +4281,7 @@ class APIRouter(routing.Router): ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark this *path operation* as deprecated. @@ -4225,7 +4291,7 @@ class APIRouter(routing.Router): ), ] = None, operation_id: Annotated[ - Optional[str], + str | None, Doc( """ Custom operation ID to be used by this *path operation*. @@ -4245,7 +4311,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_include: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to include only certain fields in the @@ -4257,7 +4323,7 @@ class APIRouter(routing.Router): ), ] = None, response_model_exclude: Annotated[ - Optional[IncEx], + IncEx | None, Doc( """ Configuration passed to Pydantic to exclude certain fields in the @@ -4359,7 +4425,7 @@ class APIRouter(routing.Router): ), ] = Default(JSONResponse), name: Annotated[ - Optional[str], + str | None, Doc( """ Name for this *path operation*. Only used internally. @@ -4367,7 +4433,7 @@ class APIRouter(routing.Router): ), ] = None, callbacks: Annotated[ - Optional[list[BaseRoute]], + list[BaseRoute] | None, Doc( """ List of *path operations* that will be used as OpenAPI callbacks. @@ -4383,7 +4449,7 @@ class APIRouter(routing.Router): ), ] = None, openapi_extra: Annotated[ - Optional[dict[str, Any]], + dict[str, Any] | None, Doc( """ Extra metadata to be included in the OpenAPI schema for this *path @@ -4460,6 +4526,58 @@ class APIRouter(routing.Router): generate_unique_id_function=generate_unique_id_function, ) + # TODO: remove this once the lifespan (or alternative) interface is improved + async def _startup(self) -> None: + """ + Run any `.on_startup` event handlers. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + for handler in self.on_startup: + if is_async_callable(handler): + await handler() + else: + handler() + + # TODO: remove this once the lifespan (or alternative) interface is improved + async def _shutdown(self) -> None: + """ + Run any `.on_shutdown` event handlers. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + for handler in self.on_shutdown: + if is_async_callable(handler): + await handler() + else: + handler() + + # TODO: remove this once the lifespan (or alternative) interface is improved + def add_event_handler( + self, + event_type: str, + func: Callable[[], Any], + ) -> None: + """ + Add an event handler function for startup or shutdown. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + assert event_type in ("startup", "shutdown") + if event_type == "startup": + self.on_startup.append(func) + else: + self.on_shutdown.append(func) + @deprecated( """ on_event is deprecated, use lifespan event handlers instead. diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 18dfb8e61..89358a913 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional, Union +from typing import Annotated from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn @@ -13,8 +13,8 @@ class APIKeyBase(SecurityBase): self, location: APIKeyIn, name: str, - description: Union[str, None], - scheme_name: Union[str, None], + description: str | None, + scheme_name: str | None, auto_error: bool, ): self.auto_error = auto_error @@ -42,7 +42,7 @@ class APIKeyBase(SecurityBase): headers={"WWW-Authenticate": "APIKey"}, ) - def check_api_key(self, api_key: Optional[str]) -> Optional[str]: + def check_api_key(self, api_key: str | None) -> str | None: if not api_key: if self.auto_error: raise self.make_not_authenticated_error() @@ -90,7 +90,7 @@ class APIKeyQuery(APIKeyBase): Doc("Query parameter name."), ], scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -100,7 +100,7 @@ class APIKeyQuery(APIKeyBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -137,7 +137,7 @@ class APIKeyQuery(APIKeyBase): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: api_key = request.query_params.get(self.model.name) return self.check_api_key(api_key) @@ -179,7 +179,7 @@ class APIKeyHeader(APIKeyBase): *, name: Annotated[str, Doc("Header name.")], scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -189,7 +189,7 @@ class APIKeyHeader(APIKeyBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -225,7 +225,7 @@ class APIKeyHeader(APIKeyBase): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: api_key = request.headers.get(self.model.name) return self.check_api_key(api_key) @@ -267,7 +267,7 @@ class APIKeyCookie(APIKeyBase): *, name: Annotated[str, Doc("Cookie name.")], scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -277,7 +277,7 @@ class APIKeyCookie(APIKeyBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -313,6 +313,6 @@ class APIKeyCookie(APIKeyBase): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: api_key = request.cookies.get(self.model.name) return self.check_api_key(api_key) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index b4c3bc6d8..05299323c 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Annotated, Optional +from typing import Annotated from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -71,8 +71,8 @@ class HTTPBase(SecurityBase): self, *, scheme: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, + scheme_name: str | None = None, + description: str | None = None, auto_error: bool = True, ): self.model: HTTPBaseModel = HTTPBaseModel( @@ -91,9 +91,7 @@ class HTTPBase(SecurityBase): headers=self.make_authenticate_headers(), ) - async def __call__( - self, request: Request - ) -> Optional[HTTPAuthorizationCredentials]: + async def __call__(self, request: Request) -> HTTPAuthorizationCredentials | None: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): @@ -143,7 +141,7 @@ class HTTPBasic(HTTPBase): self, *, scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -153,7 +151,7 @@ class HTTPBasic(HTTPBase): ), ] = None, realm: Annotated[ - Optional[str], + str | None, Doc( """ HTTP Basic authentication realm. @@ -161,7 +159,7 @@ class HTTPBasic(HTTPBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -203,7 +201,7 @@ class HTTPBasic(HTTPBase): async def __call__( # type: ignore self, request: Request - ) -> Optional[HTTPBasicCredentials]: + ) -> HTTPBasicCredentials | None: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "basic": @@ -256,9 +254,9 @@ class HTTPBearer(HTTPBase): def __init__( self, *, - bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None, + bearerFormat: Annotated[str | None, Doc("Bearer token format.")] = None, scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -268,7 +266,7 @@ class HTTPBearer(HTTPBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -302,9 +300,7 @@ class HTTPBearer(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__( - self, request: Request - ) -> Optional[HTTPAuthorizationCredentials]: + async def __call__(self, request: Request) -> HTTPAuthorizationCredentials | None: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): @@ -362,7 +358,7 @@ class HTTPDigest(HTTPBase): self, *, scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -372,7 +368,7 @@ class HTTPDigest(HTTPBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -405,9 +401,7 @@ class HTTPDigest(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__( - self, request: Request - ) -> Optional[HTTPAuthorizationCredentials]: + async def __call__(self, request: Request) -> HTTPAuthorizationCredentials | None: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 58ffc5c76..661043ce7 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, Optional, Union, cast +from typing import Annotated, Any, cast from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -60,7 +60,7 @@ class OAuth2PasswordRequestForm: self, *, grant_type: Annotated[ - Union[str, None], + str | None, Form(pattern="^password$"), Doc( """ @@ -128,7 +128,7 @@ class OAuth2PasswordRequestForm: ), ] = "", client_id: Annotated[ - Union[str, None], + str | None, Form(), Doc( """ @@ -139,7 +139,7 @@ class OAuth2PasswordRequestForm: ), ] = None, client_secret: Annotated[ - Union[str, None], + str | None, Form(json_schema_extra={"format": "password"}), Doc( """ @@ -294,7 +294,7 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): ), ] = "", client_id: Annotated[ - Union[str, None], + str | None, Form(), Doc( """ @@ -305,7 +305,7 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): ), ] = None, client_secret: Annotated[ - Union[str, None], + str | None, Form(), Doc( """ @@ -344,7 +344,7 @@ class OAuth2(SecurityBase): self, *, flows: Annotated[ - Union[OAuthFlowsModel, dict[str, dict[str, Any]]], + OAuthFlowsModel | dict[str, dict[str, Any]], Doc( """ The dictionary of OAuth2 flows. @@ -352,7 +352,7 @@ class OAuth2(SecurityBase): ), ] = OAuthFlowsModel(), scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -362,7 +362,7 @@ class OAuth2(SecurityBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -420,7 +420,7 @@ class OAuth2(SecurityBase): headers={"WWW-Authenticate": "Bearer"}, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: @@ -454,7 +454,7 @@ class OAuth2PasswordBearer(OAuth2): ), ], scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -464,7 +464,7 @@ class OAuth2PasswordBearer(OAuth2): ), ] = None, scopes: Annotated[ - Optional[dict[str, str]], + dict[str, str] | None, Doc( """ The OAuth2 scopes that would be required by the *path operations* that @@ -476,7 +476,7 @@ class OAuth2PasswordBearer(OAuth2): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -506,7 +506,7 @@ class OAuth2PasswordBearer(OAuth2): ), ] = True, refreshUrl: Annotated[ - Optional[str], + str | None, Doc( """ The URL to refresh the token and obtain a new one. @@ -533,7 +533,7 @@ class OAuth2PasswordBearer(OAuth2): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": @@ -562,7 +562,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ), ], refreshUrl: Annotated[ - Optional[str], + str | None, Doc( """ The URL to refresh the token and obtain a new one. @@ -570,7 +570,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ), ] = None, scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -580,7 +580,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ), ] = None, scopes: Annotated[ - Optional[dict[str, str]], + dict[str, str] | None, Doc( """ The OAuth2 scopes that would be required by the *path operations* that @@ -589,7 +589,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -639,7 +639,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": @@ -666,7 +666,7 @@ class SecurityScopes: def __init__( self, scopes: Annotated[ - Optional[list[str]], + list[str] | None, Doc( """ This will be filled by FastAPI. diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index f4d953351..1c6fcc744 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel @@ -31,7 +31,7 @@ class OpenIdConnect(SecurityBase): ), ], scheme_name: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme name. @@ -41,7 +41,7 @@ class OpenIdConnect(SecurityBase): ), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( """ Security scheme description. @@ -84,7 +84,7 @@ class OpenIdConnect(SecurityBase): headers={"WWW-Authenticate": "Bearer"}, ) - async def __call__(self, request: Request) -> Optional[str]: + async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index fd349aec7..8ee66fd38 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -1,8 +1,5 @@ -from typing import Optional - - def get_authorization_scheme_param( - authorization_header_value: Optional[str], + authorization_header_value: str | None, ) -> tuple[str, str]: if not authorization_header_value: return "", "" diff --git a/fastapi/types.py b/fastapi/types.py index 1c3a6de74..1fb86e13b 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,11 +1,12 @@ import types +from collections.abc import Callable from enum import Enum -from typing import Any, Callable, Optional, TypeVar, Union +from typing import Any, TypeVar, Union from pydantic import BaseModel from pydantic.main import IncEx as IncEx DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) -ModelNameMap = dict[Union[type[BaseModel], type[Enum]], str] -DependencyCacheKey = tuple[Optional[Callable[..., Any]], tuple[str, ...], str] +ModelNameMap = dict[type[BaseModel] | type[Enum], str] +DependencyCacheKey = tuple[Callable[..., Any] | None, tuple[str, ...], str] diff --git a/fastapi/utils.py b/fastapi/utils.py index 1c3a0881f..12eaa2bf0 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,42 +1,29 @@ import re import warnings -from collections.abc import MutableMapping from typing import ( TYPE_CHECKING, Any, - Optional, - Union, + Literal, ) -from weakref import WeakKeyDictionary import fastapi from fastapi._compat import ( - BaseConfig, ModelField, PydanticSchemaGenerationError, Undefined, - UndefinedType, - Validator, annotation_is_pydantic_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]] = ( - WeakKeyDictionary() -) - -def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: +def is_body_allowed_for_status_code(status_code: int | str | None) -> bool: if status_code is None: return True # Ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#patterned-fields-1 @@ -71,40 +58,25 @@ _invalid_args_message = ( def create_model_field( name: str, type_: Any, - class_validators: Optional[dict[str, Validator]] = None, - default: Optional[Any] = Undefined, - required: Union[bool, UndefinedType] = Undefined, - model_config: Union[type[BaseConfig], None] = None, - field_info: Optional[FieldInfo] = None, - alias: Optional[str] = None, + default: Any | None = Undefined, + field_info: FieldInfo | None = None, + alias: str | None = 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 {} - 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[arg-type] + return v2.ModelField(mode=mode, name=name, field_info=field_info) 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, -) -> ModelField: - return field - - def generate_operation_id_for_path( *, name: str, path: str, method: str ) -> str: # pragma: nocover @@ -147,9 +119,9 @@ def deep_dict_update(main_dict: dict[Any, Any], update_dict: dict[Any, Any]) -> def get_value_or_default( - first_item: Union[DefaultPlaceholder, DefaultType], - *extra_items: Union[DefaultPlaceholder, DefaultType], -) -> Union[DefaultPlaceholder, DefaultType]: + first_item: DefaultPlaceholder | DefaultType, + *extra_items: DefaultPlaceholder | DefaultType, +) -> DefaultPlaceholder | DefaultType: """ Pass items or `DefaultPlaceholder`s by descending priority. diff --git a/pdm_build.py b/pdm_build.py index a0eb88eeb..b1b662bd3 100644 --- a/pdm_build.py +++ b/pdm_build.py @@ -3,18 +3,38 @@ from typing import Any from pdm.backend.hooks import Context -TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE", "fastapi") +TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE") def pdm_build_initialize(context: Context) -> None: metadata = context.config.metadata + # Get main version + version = metadata["version"] # Get custom config for the current package, from the env var - config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ + all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ "_internal-slim-build" - ]["packages"].get(TIANGOLO_BUILD_PACKAGE) - if not config: + ]["packages"] + + if TIANGOLO_BUILD_PACKAGE not in all_configs_config: return + + config = all_configs_config[TIANGOLO_BUILD_PACKAGE] 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 + # Get custom build config for the current package + build_config: dict[str, Any] = ( + config.get("tool", {}).get("pdm", {}).get("build", {}) + ) + # Override PDM build config with custom build config for this package + for key, value in build_config.items(): + context.config.build_config[key] = value + # Get main dependencies + dependencies: list[str] = metadata.get("dependencies", []) + # Sync versions in dependencies + new_dependencies = [] + for dep in dependencies: + new_dep = f"{dep}>={version}" + new_dependencies.append(new_dep) + metadata["dependencies"] = new_dependencies diff --git a/pyproject.toml b/pyproject.toml index 0f6bf1e4a..e1ce30507 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.9" +requires-python = ">=3.10" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, ] @@ -33,7 +33,6 @@ classifiers = [ "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -43,9 +42,10 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.51.0", + "starlette>=0.40.0,<1.0.0", "pydantic>=2.7.0", "typing-extensions>=4.8.0", + "typing-inspection>=0.4.2", "annotated-doc>=0.0.2", ] @@ -107,9 +107,9 @@ all = [ # For Starlette's schema generation, would not be used with FastAPI "pyyaml >=5.3.1", # For UJSONResponse - "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", + "ujson >=5.8.0", # For ORJSONResponse - "orjson >=3.2.1", + "orjson >=3.9.3", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -128,62 +128,62 @@ dev = [ { include-group = "tests" }, { include-group = "docs" }, { include-group = "translations" }, - "playwright>=1.57.0", - "prek==0.2.22", + "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.5.0", - "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.21.1", + "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.5.0", + "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.21.1", ] docs-tests = [ - "httpx>=0.23.0,<1.0.0", - "ruff==0.14.14", + "httpx >=0.23.0,<1.0.0", + "ruff >=0.14.14", ] 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", + "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.31", - "strawberry-graphql>=0.200.0,<1.0.0", - "types-orjson==3.6.2", - "types-ujson==5.10.0.20240515", - "a2wsgi>=1.9.0,<=2.0.0", + "anyio[trio] >=3.2.1,<5.0.0", + "coverage[toml] >=6.5.0,<8.0", + "dirty-equals >=0.9.0", + "flask >=3.0.0,<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.31", + "strawberry-graphql >=0.200.0,<1.0.0", + "types-orjson >=3.6.2", + "types-ujson >=5.10.0.20240515", + "a2wsgi >=1.9.0,<=2.0.0", ] translations = [ - "gitpython==3.1.46", - "pydantic-ai==0.4.10", - "pygithub==2.8.1", + "gitpython >=3.1.46", + "pydantic-ai >=0.4.10", + "pygithub >=2.8.1", ] [tool.pdm] @@ -201,6 +201,29 @@ source-includes = [ [tool.tiangolo._internal-slim-build.packages.fastapi-slim.project] name = "fastapi-slim" +readme = "fastapi-slim/README.md" +dependencies = [ + "fastapi", +] +optional-dependencies = {} +scripts = {} + +[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build] +# excludes needs to explicitly exclude the top level python packages, +# otherwise PDM includes them by default +# A "*" glob pattern can't be used here because in PDM internals, the patterns are put +# in a set (unordered, order varies) and each excluded file is assigned one of the +# glob patterns that matches, as the set is unordered, the matched pattern could be "*" +# independent of the order here. And then the internal code would give it a lower score +# than the one for a default included file. +# By not using "*" and explicitly excluding the top level packages, they get a higher +# score than the default inclusion +excludes = ["fastapi", "tests", "pdm_build.py"] +# source-includes needs to explicitly define some value because PDM will check the +# truthy value of the list, and if empty, will include some defaults, including "tests", +# an empty string doesn't match anything, but makes the list truthy, so that PDM +# doesn't override it during the build. +source-includes = [""] [tool.mypy] plugins = ["pydantic.mypy"] diff --git a/scripts/docs.py b/scripts/docs.py index 20bf1c168..3cf62f084 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -20,13 +20,18 @@ from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) SUPPORTED_LANGS = { - "en", "de", + "en", "es", + "fr", + "ja", "ko", "pt", "ru", + "tr", "uk", + "zh", + "zh-hant", } diff --git a/scripts/general-llm-prompt.md b/scripts/general-llm-prompt.md index d45ab8eb0..23926bd8e 100644 --- a/scripts/general-llm-prompt.md +++ b/scripts/general-llm-prompt.md @@ -353,7 +353,9 @@ Erstelle eine [Virtuelle Umgebung](../virtual-environments.md){.internal-link ta Translate HTML abbr elements (`text`) 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 text 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. +- The text inside abbr tag may be surrounded by further HTML or Markdown markup or quotes, for example text or `text` or "text". Preserve markup and only translate visible text inside the abbr element. + +- 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: @@ -421,45 +423,7 @@ Result (German): ASGI ``` -- 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): - -``` -{text} -``` - -Result: - -``` -{translation of text} -``` - -Examples: - - Source (English): - -``` -path -linter -parsing -0.95.0 -at the time of writing this -``` - -Result (German): - -``` -Pfad -Linter -Parsen -0.95.0 -zum Zeitpunkt als das hier geschrieben wurde -``` - -- 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. +- If the title of abbr element contains a 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: @@ -526,3 +490,23 @@ Result (German): - 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. + +### HTML dfn elements + +For HTML dfn elements (`text`), translate the text inside the dfn element and the title attribute. Do not include the original English text in the title attribute. + +Examples: + +Source (English): + +``` +path +linter +``` + +Result (German): + +``` +Pfad +Linter +``` diff --git a/scripts/translate.py b/scripts/translate.py index 9eda7b390..ddcfa311d 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -86,7 +86,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.2") + agent = Agent("openai:gpt-5") prompt_segments = [ general_prompt, @@ -347,9 +347,12 @@ def list_outdated(language: str) -> list[Path]: @app.command() -def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def update_outdated( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: outdated_paths = list_outdated(language) - for path in outdated_paths: + for path in outdated_paths[:max]: print(f"Updating lang: {language} path: {path}") translate_page(language=language, en_path=path) print(f"Done updating: {path}") @@ -357,9 +360,12 @@ def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) - @app.command() -def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def add_missing( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: missing_paths = list_missing(language) - for path in missing_paths: + for path in missing_paths[:max]: print(f"Adding lang: {language} path: {path}") translate_page(language=language, en_path=path) print(f"Done adding: {path}") @@ -367,11 +373,14 @@ def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> No @app.command() -def update_and_add(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def update_and_add( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: print(f"Updating outdated translations for {language}") - update_outdated(language=language) + update_outdated(language=language, max=max) print(f"Adding missing translations for {language}") - add_missing(language=language) + add_missing(language=language, max=max) print(f"Done updating and adding for {language}") @@ -411,7 +420,8 @@ def make_pr( 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}") + branch_name = current_branch + print(f"Committing in place on branch {branch_name}") print("Adding updated files") git_path = Path("docs") subprocess.run(["git", "add", str(git_path)], check=True) diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 262236640..130a6662a 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -26,82 +27,88 @@ def test_additional_properties_post(): 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": { - "/foo": { - "post": { - "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": { + "/foo": { + "post": { + "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": "Foo", + "operationId": "foo_foo_post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Items"} } }, + "required": True, }, - }, - "summary": "Foo", - "operationId": "foo_foo_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Items"} + } + } + }, + "components": { + "schemas": { + "Items": { + "title": "Items", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "object", + "additionalProperties": {"type": "integer"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Items": { - "title": "Items", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "object", - "additionalProperties": {"type": "integer"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index 063297a3f..c02841cde 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -2,6 +2,7 @@ from typing import Union from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, ConfigDict @@ -40,81 +41,87 @@ def test_call_valid(): 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": "Post", - "operationId": "post__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - {"$ref": "#/components/schemas/Foo"}, - {"type": "null"}, - ], - "title": "Foo", - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post", + "operationId": "post__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, + } + } + }, + "components": { + "schemas": { + "Foo": { + "properties": {}, + "additionalProperties": False, + "type": "object", + "title": "Foo", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "components": { - "schemas": { - "Foo": { - "properties": {}, - "additionalProperties": False, - "type": "object", - "title": "Foo", - }, - "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_additional_response_extra.py b/tests/test_additional_response_extra.py index 55be19bad..70a9a7331 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot router = APIRouter() @@ -29,21 +30,23 @@ def test_path_operation(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Item", - "operationId": "read_item_items__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Item", + "operationId": "read_item_items__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 376d7714e..3a37c924d 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -128,6 +128,8 @@ def test_openapi_schema(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 8724e5ecb..9b468c621 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -34,64 +35,66 @@ 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": { - "/a/{id}": { - "get": { - "responses": { - "422": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/JsonApiError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } } - } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, }, }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } }, }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], } - } - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index 153f04f57..09e03959e 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -15,70 +16,76 @@ 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": { - "/a/{id}": { - "get": { - "responses": { - "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": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index fecc3ee16..3faab2797 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -39,76 +40,78 @@ 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": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/JsonApiError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } } - } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, }, }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } }, }, - "summary": "B", - "operationId": "b_b_get", } }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, - } + } + ) diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index 71cabc7c3..b6c359ba8 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel @@ -84,94 +85,98 @@ def test_d(): 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": { - "/a": { - "get": { - "responses": { - "501": {"description": "Error 1"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "502": {"description": "Error 2"}, - "4XX": {"description": "Error with range, upper"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - "/c": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": {"description": "Error with range, lower"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": {"description": "A default response"}, - }, - "summary": "C", - "operationId": "c_c_get", - } - }, - "/d": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": { - "description": "Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseModel" - } - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "501": {"description": "Error 1"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseModel" - } - } + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "502": {"description": "Error 2"}, + "4XX": {"description": "Error with range, upper"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - }, - "summary": "D", - "operationId": "d_d_get", + "summary": "B", + "operationId": "b_b_get", + } + }, + "/c": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": {"description": "Error with range, lower"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": {"description": "A default response"}, + }, + "summary": "C", + "operationId": "c_c_get", + } + }, + "/d": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": { + "description": "Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + }, + "summary": "D", + "operationId": "d_d_get", + } + }, + }, + "components": { + "schemas": { + "ResponseModel": { + "title": "ResponseModel", + "required": ["message"], + "type": "object", + "properties": { + "message": {"title": "Message", "type": "string"} + }, + } } }, - }, - "components": { - "schemas": { - "ResponseModel": { - "title": "ResponseModel", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - } - } - }, - } + } + ) diff --git a/tests/test_additional_responses_union_duplicate_anyof.py b/tests/test_additional_responses_union_duplicate_anyof.py index f5d987ca3..5d833fce4 100644 --- a/tests/test_additional_responses_union_duplicate_anyof.py +++ b/tests/test_additional_responses_union_duplicate_anyof.py @@ -8,6 +8,7 @@ from typing import Union from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel @@ -45,79 +46,81 @@ 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": { - "/route1": { - "get": { - "summary": "Route1", - "operationId": "route1_route1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "anyOf": [ - {"$ref": "#/components/schemas/ModelA"}, - {"$ref": "#/components/schemas/ModelB"}, - ], - "title": "Response 500 Route1 Route1 Get", - }, - "examples": {"Case A": {"value": "a"}}, - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/route1": { + "get": { + "summary": "Route1", + "operationId": "route1_route1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/ModelA"}, + {"$ref": "#/components/schemas/ModelB"}, + ], + "title": "Response 500 Route1 Route1 Get", + }, + "examples": {"Case A": {"value": "a"}}, + } + }, }, }, + } + }, + "/route2": { + "get": { + "summary": "Route2", + "operationId": "route2_route2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/ModelA"}, + {"$ref": "#/components/schemas/ModelB"}, + ], + "title": "Response 500 Route2 Route2 Get", + }, + "examples": {"Case A": {"value": "a"}}, + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "ModelA": { + "properties": {"a": {"type": "string", "title": "A"}}, + "type": "object", + "required": ["a"], + "title": "ModelA", + }, + "ModelB": { + "properties": {"b": {"type": "string", "title": "B"}}, + "type": "object", + "required": ["b"], + "title": "ModelB", }, } }, - "/route2": { - "get": { - "summary": "Route2", - "operationId": "route2_route2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "anyOf": [ - {"$ref": "#/components/schemas/ModelA"}, - {"$ref": "#/components/schemas/ModelB"}, - ], - "title": "Response 500 Route2 Route2 Get", - }, - "examples": {"Case A": {"value": "a"}}, - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "ModelA": { - "properties": {"a": {"type": "string", "title": "A"}}, - "type": "object", - "required": ["a"], - "title": "ModelA", - }, - "ModelB": { - "properties": {"b": {"type": "string", "title": "B"}}, - "type": "object", - "required": ["b"], - "title": "ModelB", - }, - } - }, - } + } + ) diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 39f6f83b2..68e2ea884 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -120,172 +121,178 @@ def test_nested_router(): 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"}, - "paths": { - "/default": { - "get": { - "summary": "Default", - "operationId": "default_default_get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Foo", - "type": "string", - "default": "foo", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/default": { + "get": { + "summary": "Default", + "operationId": "default_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Foo", + "type": "string", + "default": "foo", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - "name": "foo", - "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" + } } - } + }, }, }, - }, - } - }, - "/required": { - "get": { - "summary": "Required", - "operationId": "required_required_get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Foo", - "minLength": 1, - "type": "string", - }, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/multiple": { - "get": { - "summary": "Multiple", - "operationId": "multiple_multiple_get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Foo", - "minLength": 1, - "type": "string", - }, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/unrelated": { - "get": { - "summary": "Unrelated", - "operationId": "unrelated_unrelated_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Foo", "type": "string"}, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "/required": { + "get": { + "summary": "Required", + "operationId": "required_required_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + } }, - } - }, - } + "/multiple": { + "get": { + "summary": "Multiple", + "operationId": "multiple_multiple_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/unrelated": { + "get": { + "summary": "Unrelated", + "operationId": "unrelated_unrelated_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_application.py b/tests/test_application.py index 001586ff7..675866298 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,5 +1,6 @@ import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from .main import app @@ -54,1225 +55,1231 @@ def test_enum_status_code_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"}, - "externalDocs": { - "description": "External API documentation.", - "url": "https://docs.example.com/api-general", - }, - "paths": { - "/api_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Operation", - "operationId": "non_operation_api_route_get", - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "externalDocs": { + "description": "External API documentation.", + "url": "https://docs.example.com/api-general", }, - "/non_decorated_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Decorated Route", - "operationId": "non_decorated_route_non_decorated_route_get", - } - }, - "/text": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get Text", - "operationId": "get_text_text_get", - } - }, - "/path/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Id", - "operationId": "get_id_path__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/str/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Str Id", - "operationId": "get_str_id_path_str__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Int Id", - "operationId": "get_int_id_path_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/float/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Float Id", - "operationId": "get_float_id_path_float__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "number"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/bool/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Bool Id", - "operationId": "get_bool_id_path_bool__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "boolean"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Id", - "operationId": "get_path_param_id_path_param__item_id__get", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Item Id", - }, - } - ], - } - }, - "/path/param-minlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Length", - "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Max Length", - "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-min_maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Max Length", - "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "minLength": 2, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt", - "operationId": "get_path_param_gt_path_param_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt0", - "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge", - "operationId": "get_path_param_ge_path_param_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt", - "operationId": "get_path_param_lt_path_param_lt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt0", - "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le", - "operationId": "get_path_param_le_path_param_le__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt", - "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge", - "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Int", - "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt Int", - "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Int", - "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge Int", - "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt Int", - "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge Int", - "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/query": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query", - "operationId": "get_query_query_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Optional", - "operationId": "get_query_optional_query_optional_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type", - "operationId": "get_query_type_query_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Optional", - "operationId": "get_query_type_optional_query_int_optional_get", - "parameters": [ - { - "name": "query", - "in": "query", - "required": False, - "schema": { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Query", - }, - } - ], - } - }, - "/query/int/default": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Int Default", - "operationId": "get_query_type_int_default_query_int_default_get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Query", - "type": "integer", - "default": 10, - }, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param", - "operationId": "get_query_param_query_param_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required", - "operationId": "get_query_param_required_query_param_required_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required Type", - "operationId": "get_query_param_required_type_query_param_required_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/enum-status-code": { - "get": { - "responses": { - "201": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "Get Enum Status Code", - "operationId": "get_enum_status_code_enum_status_code_get", - } - }, - "/query/frozenset": { - "get": { - "summary": "Get Query Type Frozenset", - "operationId": "get_query_type_frozenset_query_frozenset_get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Query", - "uniqueItems": True, - "type": "array", - "items": {"type": "integer"}, - }, - "name": "query", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query/list": { - "get": { - "summary": "Get Query List", - "operationId": "get_query_list_query_list_get", - "parameters": [ - { - "name": "device_ids", - "in": "query", - "required": True, - "schema": { - "type": "array", - "items": {"type": "integer"}, - "title": "Device Ids", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": {"type": "integer"}, - "title": "Response Get Query List Query List Get", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query/list-default": { - "get": { - "summary": "Get Query List Default", - "operationId": "get_query_list_default_query_list_default_get", - "parameters": [ - { - "name": "device_ids", - "in": "query", - "required": False, - "schema": { - "type": "array", - "items": {"type": "integer"}, - "default": [], - "title": "Device Ids", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": {"type": "integer"}, - "title": "Response Get Query List Default Query List Default Get", - } - } - }, - }, - "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"}, - }, + "paths": { + "/api_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Operation", + "operationId": "non_operation_api_route_get", + } }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + "/non_decorated_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Decorated Route", + "operationId": "non_decorated_route_non_decorated_route_get", + } }, - } - }, - } + "/text": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get Text", + "operationId": "get_text_text_get", + } + }, + "/path/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Id", + "operationId": "get_id_path__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/str/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Str Id", + "operationId": "get_str_id_path_str__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Int Id", + "operationId": "get_int_id_path_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/float/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Float Id", + "operationId": "get_float_id_path_float__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "number"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/bool/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Bool Id", + "operationId": "get_bool_id_path_bool__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "boolean"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Id", + "operationId": "get_path_param_id_path_param__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + }, + } + ], + } + }, + "/path/param-minlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Length", + "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Max Length", + "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-min_maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Max Length", + "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "minLength": 2, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt", + "operationId": "get_path_param_gt_path_param_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt0", + "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge", + "operationId": "get_path_param_ge_path_param_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt", + "operationId": "get_path_param_lt_path_param_lt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt0", + "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le", + "operationId": "get_path_param_le_path_param_le__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt", + "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge", + "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Int", + "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt Int", + "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Int", + "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge Int", + "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt Int", + "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge Int", + "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/query": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query", + "operationId": "get_query_query_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Optional", + "operationId": "get_query_optional_query_optional_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type", + "operationId": "get_query_type_query_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Optional", + "operationId": "get_query_type_optional_query_int_optional_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": False, + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + }, + } + ], + } + }, + "/query/int/default": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Int Default", + "operationId": "get_query_type_int_default_query_int_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Query", + "type": "integer", + "default": 10, + }, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param", + "operationId": "get_query_param_query_param_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required", + "operationId": "get_query_param_required_query_param_required_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required Type", + "operationId": "get_query_param_required_type_query_param_required_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/enum-status-code": { + "get": { + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Get Enum Status Code", + "operationId": "get_enum_status_code_enum_status_code_get", + } + }, + "/query/frozenset": { + "get": { + "summary": "Get Query Type Frozenset", + "operationId": "get_query_type_frozenset_query_frozenset_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Query", + "uniqueItems": True, + "type": "array", + "items": {"type": "integer"}, + }, + "name": "query", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query/list": { + "get": { + "summary": "Get Query List", + "operationId": "get_query_list_query_list_get", + "parameters": [ + { + "name": "device_ids", + "in": "query", + "required": True, + "schema": { + "type": "array", + "items": {"type": "integer"}, + "title": "Device Ids", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"type": "integer"}, + "title": "Response Get Query List Query List Get", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query/list-default": { + "get": { + "summary": "Get Query List Default", + "operationId": "get_query_list_default_query_list_default_get", + "parameters": [ + { + "name": "device_ids", + "in": "query", + "required": False, + "schema": { + "type": "array", + "items": {"type": "integer"}, + "default": [], + "title": "Device Ids", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"type": "integer"}, + "title": "Response Get Query List Default Query List Default Get", + } + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py index e7f969f7c..af6654a63 100644 --- a/tests/test_computed_fields.py +++ b/tests/test_computed_fields.py @@ -1,6 +1,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture(name="client") @@ -43,55 +44,65 @@ def test_get(client: TestClient, path: str): 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": { - "/": { - "get": { - "summary": "Read Root", - "operationId": "read_root__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Rectangle"} - } + 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/Rectangle" + } + } + }, + } + }, + } + }, + "/responses": { + "get": { + "summary": "Read Responses", + "operationId": "read_responses_responses_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rectangle" + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Rectangle": { + "properties": { + "width": {"type": "integer", "title": "Width"}, + "length": {"type": "integer", "title": "Length"}, + "area": { + "type": "integer", + "title": "Area", + "readOnly": True, }, - } - }, + }, + "type": "object", + "required": ["width", "length", "area"], + "title": "Rectangle", + } } }, - "/responses": { - "get": { - "summary": "Read Responses", - "operationId": "read_responses_responses_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Rectangle"} - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Rectangle": { - "properties": { - "width": {"type": "integer", "title": "Width"}, - "length": {"type": "integer", "title": "Length"}, - "area": {"type": "integer", "title": "Area", "readOnly": True}, - }, - "type": "object", - "required": ["width", "length", "area"], - "title": "Rectangle", - } - } - }, - } + } + ) diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index 55374584b..786c1efc3 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -2,6 +2,7 @@ import pytest from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute from fastapi.testclient import TestClient +from inline_snapshot import snapshot from starlette.routing import Route app = FastAPI() @@ -74,45 +75,47 @@ def test_route_classes(): 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": { - "/a/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get A", - "operationId": "get_a_a__get", - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get A", + "operationId": "get_a_a__get", + } + }, + "/a/b/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get B", + "operationId": "get_b_a_b__get", + } + }, + "/a/b/c/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get C", + "operationId": "get_c_a_b_c__get", + } + }, }, - "/a/b/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get B", - "operationId": "get_b_a_b__get", - } - }, - "/a/b/c/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get C", - "operationId": "get_c_a_b_c__get", - } - }, - }, - } + } + ) diff --git a/tests/test_dependencies_utils.py b/tests/test_dependencies_utils.py new file mode 100644 index 000000000..9257d1c9e --- /dev/null +++ b/tests/test_dependencies_utils.py @@ -0,0 +1,8 @@ +from fastapi.dependencies.utils import get_typed_annotation + + +def test_get_typed_annotation(): + # For coverage + annotation = "None" + typed_annotation = get_typed_annotation(annotation, globals()) + assert typed_annotation is None diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index a8658e03b..81490e68b 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,5 +1,6 @@ from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -84,147 +85,153 @@ def test_sub_duplicates(): 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": { - "/with-duplicates": { - "post": { - "summary": "With Duplicates", - "operationId": "with_duplicates_with_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/with-duplicates": { + "post": { + "summary": "With Duplicates", + "operationId": "with_duplicates_with_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "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", + } + }, + "/no-duplicates": { + "post": { + "summary": "No Duplicates", + "operationId": "no_duplicates_no_duplicates_post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" } } }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/with-duplicates-sub": { + "post": { + "summary": "No Duplicates Sub", + "operationId": "no_duplicates_sub_with_duplicates_sub_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "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_no_duplicates_no_duplicates_post": { + "title": "Body_no_duplicates_no_duplicates_post", + "required": ["item", "item2"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/no-duplicates": { - "post": { - "summary": "No Duplicates", - "operationId": "no_duplicates_no_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/with-duplicates-sub": { - "post": { - "summary": "No Duplicates Sub", - "operationId": "no_duplicates_sub_with_duplicates_sub_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "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_no_duplicates_no_duplicates_post": { - "title": "Body_no_duplicates_no_duplicates_post", - "required": ["item", "item2"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "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_dependency_pep695.py b/tests/test_dependency_pep695.py new file mode 100644 index 000000000..ef5636638 --- /dev/null +++ b/tests/test_dependency_pep695.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import TypeAliasType + + +async def some_value() -> int: + return 123 + + +DependedValue = TypeAliasType( + "DependedValue", Annotated[int, Depends(some_value)], type_params=() +) + + +def test_pep695_type_dependencies(): + app = FastAPI() + + @app.get("/") + async def get_with_dep(value: DependedValue) -> str: # noqa + return f"value: {value}" + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.text == '"value: 123"' diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py index ec7366d2a..24198af34 100644 --- a/tests/test_deprecated_openapi_prefix.py +++ b/tests/test_deprecated_openapi_prefix.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, Request from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI(openapi_prefix="/api/v1") @@ -21,22 +22,24 @@ def test_main(): def test_openapi(): 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": { - "/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"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - "servers": [{"url": "/api/v1"}], - } + }, + "servers": [{"url": "/api/v1"}], + } + ) diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index 83e86d231..d5a6d1fec 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -35,45 +36,49 @@ def test_get_api_route(): 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": { - "/": { - "get": { - "summary": "F", - "operationId": "f__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model3"} - } - }, - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "F", + "operationId": "f__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Model3" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Model": {"title": "Model", "type": "object", "properties": {}}, + "Model2": { + "title": "Model2", + "required": ["a"], + "type": "object", + "properties": {"a": {"$ref": "#/components/schemas/Model"}}, + }, + "Model3": { + "title": "Model3", + "required": ["c", "d"], + "type": "object", + "properties": { + "c": {"$ref": "#/components/schemas/Model"}, + "d": {"$ref": "#/components/schemas/Model2"}, + }, }, } - } - }, - "components": { - "schemas": { - "Model": {"title": "Model", "type": "object", "properties": {}}, - "Model2": { - "title": "Model2", - "required": ["a"], - "type": "object", - "properties": {"a": {"$ref": "#/components/schemas/Model"}}, - }, - "Model3": { - "title": "Model3", - "required": ["c", "d"], - "type": "object", - "properties": { - "c": {"$ref": "#/components/schemas/Model"}, - "d": {"$ref": "#/components/schemas/Model2"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index 2e5ac6c06..0dee15c25 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -1,7 +1,8 @@ from typing import Optional -from fastapi import Depends, FastAPI, Query, status +from fastapi import Depends, FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -26,79 +27,6 @@ def foo_handler( client = TestClient(app) -expected_schema = { - "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", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.1.0", - "paths": { - "/foo": { - "get": { - "operationId": "foo_handler_foo_get", - "parameters": [ - { - "in": "query", - "name": "client_id", - "required": True, - "schema": {"title": "Client 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": "Foo Handler", - } - } - }, -} - - -def test_schema(): - response = client.get("/openapi.json") - assert response.status_code == status.HTTP_200_OK - actual_schema = response.json() - assert actual_schema == expected_schema - def test_get_invalid(): response = client.get("/foo") @@ -109,3 +37,81 @@ def test_get_valid(): response = client.get("/foo", params={"client_id": "bar"}) assert response.status_code == 200 assert response.json() == {"client_id": "bar_key", "client_tag": "bar_tag"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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": { + "/foo": { + "get": { + "operationId": "foo_handler_foo_get", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": True, + "schema": {"title": "Client 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": "Foo Handler", + } + } + }, + } + ) diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index 45734ec28..96f85b446 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -98,268 +99,274 @@ def test_trace(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Get Items", - "operationId": "get_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "delete": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Delete Item", - "operationId": "delete_item_items__item_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "summary": "Get Items", + "operationId": "get_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", } - }, - "required": True, + ], }, - }, - "options": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "delete": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Options Item", - "operationId": "options_item_items__item_id__options", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "head": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Head Item", - "operationId": "head_item_items__item_id__head", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "patch": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Patch Item", - "operationId": "patch_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"} + "summary": "Delete Item", + "operationId": "delete_item_items__item_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", } - }, - "required": True, - }, - }, - "trace": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, }, - "summary": "Trace Item", - "operationId": "trace_item_items__item_id__trace", - "parameters": [ - { + "options": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Options Item", + "operationId": "options_item_items__item_id__options", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "head": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Head Item", + "operationId": "head_item_items__item_id__head", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Patch Item", + "operationId": "patch_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, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + }, + }, + "trace": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Trace Item", + "operationId": "trace_item_items__item_id__trace", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + }, + "/items-not-decorated/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Not Decorated", + "operationId": "get_not_decorated_items_not_decorated__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } }, }, - "/items-not-decorated/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], }, }, }, - "summary": "Get Not Decorated", - "operationId": "get_not_decorated_items_not_decorated__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, } }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": { - "title": "Price", - "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_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index fc5876410..1de2b50f7 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -165,6 +165,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py index 67f054b34..e759b006a 100644 --- a/tests/test_forms_single_param.py +++ b/tests/test_forms_single_param.py @@ -2,6 +2,7 @@ from typing import Annotated from fastapi import FastAPI, Form from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -23,78 +24,86 @@ def test_single_form_field(): 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": { - "/form/": { - "post": { - "summary": "Post Form", - "operationId": "post_form_form__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_post_form_form__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/form/": { + "post": { + "summary": "Post Form", + "operationId": "post_form_form__post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_post_form_form__post" } } }, + "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_post_form_form__post": { + "properties": { + "username": {"type": "string", "title": "Username"} + }, + "type": "object", + "required": ["username"], + "title": "Body_post_form_form__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "components": { - "schemas": { - "Body_post_form_form__post": { - "properties": {"username": {"type": "string", "title": "Username"}}, - "type": "object", - "required": ["username"], - "title": "Body_post_form_form__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", - }, - } - }, - } + }, + } + ) diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 62ebfbc96..c56e6d579 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -3,6 +3,7 @@ import warnings from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel @@ -45,188 +46,197 @@ def test_top_level_generate_unique_id(): app.include_router(router) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "foo_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "foo_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "foo_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_foo_post_root": { + "title": "Body_foo_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_router": { + "title": "Body_foo_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "foo_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Foo Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_foo_post_root": { - "title": "Body_foo_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_router": { - "title": "Body_foo_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_router_overrides_generate_unique_id(): @@ -246,188 +256,197 @@ def test_router_overrides_generate_unique_id(): app.include_router(router) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "foo_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "foo_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "bar_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_bar_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_bar_post_router": { + "title": "Body_bar_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_root": { + "title": "Body_foo_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "bar_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_bar_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Bar Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Bar Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_bar_post_router": { - "title": "Body_bar_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_root": { - "title": "Body_foo_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_router_include_overrides_generate_unique_id(): @@ -447,188 +466,197 @@ def test_router_include_overrides_generate_unique_id(): app.include_router(router, generate_unique_id_function=custom_generate_unique_id3) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "foo_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "foo_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "bar_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_bar_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_bar_post_router": { + "title": "Body_bar_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_root": { + "title": "Body_foo_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "bar_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_bar_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Bar Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Bar Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_bar_post_router": { - "title": "Body_bar_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_root": { - "title": "Body_foo_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_subrouter_top_level_include_overrides_generate_unique_id(): @@ -658,251 +686,262 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): app.include_router(router, generate_unique_id_function=custom_generate_unique_id3) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "foo_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "foo_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "baz_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_baz_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Baz Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Baz Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/subrouter": { + "post": { + "summary": "Post Subrouter", + "operationId": "bar_post_subrouter", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_bar_post_subrouter" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Bar Post Subrouter", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Bar Post Subrouter", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_bar_post_subrouter": { + "title": "Body_bar_post_subrouter", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_baz_post_router": { + "title": "Body_baz_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_root": { + "title": "Body_foo_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "baz_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_baz_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Baz Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Baz Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/subrouter": { - "post": { - "summary": "Post Subrouter", - "operationId": "bar_post_subrouter", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_bar_post_subrouter" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Bar Post Subrouter", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Bar Post Subrouter", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_bar_post_subrouter": { - "title": "Body_bar_post_subrouter", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_baz_post_router": { - "title": "Body_baz_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_root": { - "title": "Body_foo_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_router_path_operation_overrides_generate_unique_id(): @@ -925,188 +964,197 @@ def test_router_path_operation_overrides_generate_unique_id(): app.include_router(router) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "foo_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "foo_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_foo_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "baz_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_baz_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Baz Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Baz Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_baz_post_router": { + "title": "Body_baz_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_root": { + "title": "Body_foo_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "baz_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_baz_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Baz Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Baz Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_baz_post_router": { - "title": "Body_baz_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_root": { - "title": "Body_foo_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_app_path_operation_overrides_generate_unique_id(): @@ -1133,188 +1181,197 @@ def test_app_path_operation_overrides_generate_unique_id(): app.include_router(router) client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "baz_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_baz_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "baz_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_baz_post_root" + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Baz Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Baz Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/router": { + "post": { + "summary": "Post Router", + "operationId": "bar_post_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_bar_post_router" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Bar Post Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_bar_post_router": { + "title": "Body_bar_post_router", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_baz_post_root": { + "title": "Body_baz_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Baz Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Baz Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - "/router": { - "post": { - "summary": "Post Router", - "operationId": "bar_post_router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_bar_post_router" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Bar Post Router", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Bar Post Router", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_bar_post_router": { - "title": "Body_bar_post_router", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_baz_post_root": { - "title": "Body_baz_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_callback_override_generate_unique_id(): @@ -1350,257 +1407,266 @@ def test_callback_override_generate_unique_id(): client = TestClient(app) response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Post Root", - "operationId": "baz_post_root", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_baz_post_root" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post Root", + "operationId": "baz_post_root", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_baz_post_root" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Baz Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Baz Post Root", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "post_callback": { + "/post-callback": { + "post": { + "summary": "Post Callback", + "operationId": "baz_post_callback", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_baz_post_callback" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Baz Post Callback", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } + } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Baz Post Callback", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } } } }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", + } + }, + "/tocallback": { + "post": { + "summary": "Post With Callback", + "operationId": "foo_post_with_callback", + "requestBody": { "content": { "application/json": { "schema": { - "title": "Response Baz Post Root", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, + "$ref": "#/components/schemas/Body_foo_post_with_callback" } } }, + "required": True, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Baz Post Root", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Foo Post With Callback", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } } - } + }, + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "title": "Response 404 Foo Post With Callback", + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + } + } + }, + }, + "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": { - "post_callback": { - "/post-callback": { - "post": { - "summary": "Post Callback", - "operationId": "baz_post_callback", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_baz_post_callback" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Baz Post Callback", - "type": "array", - "items": { - "$ref": "#/components/schemas/Item" - }, - } - } - }, - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Baz Post Callback", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } + } + }, }, - "/tocallback": { - "post": { - "summary": "Post With Callback", - "operationId": "foo_post_with_callback", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_foo_post_with_callback" - } + "components": { + "schemas": { + "Body_baz_post_callback": { + "title": "Body_baz_post_callback", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_baz_post_root": { + "title": "Body_baz_post_root", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "Body_foo_post_with_callback": { + "title": "Body_foo_post_with_callback", + "required": ["item1", "item2"], + "type": "object", + "properties": { + "item1": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "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": { - "title": "Response Foo Post With Callback", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "title": "Response 404 Foo Post With Callback", - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - } - } - }, + }, + "Message": { + "title": "Message", + "required": ["title", "description"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": {"title": "Description", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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_baz_post_callback": { - "title": "Body_baz_post_callback", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_baz_post_root": { - "title": "Body_baz_post_root", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "Body_foo_post_with_callback": { - "title": "Body_foo_post_with_callback", - "required": ["item1", "item2"], - "type": "object", - "properties": { - "item1": {"$ref": "#/components/schemas/Item"}, - "item2": {"$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", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Message": { - "title": "Message", - "required": ["title", "description"], - "type": "object", - "properties": { - "title": {"title": "Title", "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"}, - }, - }, - } - }, - } + } + ) def test_warn_duplicate_operation_id(): diff --git a/tests/test_generic_parameterless_depends.py b/tests/test_generic_parameterless_depends.py index 93b72ad24..96c7a7126 100644 --- a/tests/test_generic_parameterless_depends.py +++ b/tests/test_generic_parameterless_depends.py @@ -2,6 +2,7 @@ from typing import Annotated, TypeVar from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -44,33 +45,35 @@ def test_generic_parameterless_depends(): def test_openapi_schema(): 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": { - "/a": { - "get": { - "operationId": "a_a_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "A", - } + assert response.json() == snapshot( + { + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.1.0", + "paths": { + "/a": { + "get": { + "operationId": "a_a_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "A", + } + }, + "/b": { + "get": { + "operationId": "b_b_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "B", + } + }, }, - "/b": { - "get": { - "operationId": "b_b_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "B", - } - }, - }, - } + } + ) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index eb7939b69..46f8aa595 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -98,6 +98,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index cc567b88f..1b0f4970b 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -28,80 +29,86 @@ def test_get_with_body(): 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": { - "/product": { - "get": { - "summary": "Create Item", - "operationId": "create_item_product_get", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Product"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/product": { + "get": { + "summary": "Create Item", + "operationId": "create_item_product_get", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Product"} } }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Product": { - "title": "Product", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "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_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index 33baa25e6..e8303c821 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -4,6 +4,7 @@ import pytest from fastapi import APIRouter, Depends, FastAPI, Response from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot class ResponseLevel0(JSONResponse): @@ -442,6856 +443,6862 @@ def test_openapi(): response = client.get("/openapi.json") assert issubclass(w[-1].category, UserWarning) assert "Duplicate Operation ID" in str(w[-1].message) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/override1": { - "get": { - "tags": ["path1a", "path1b"], - "summary": "Path1 Override", - "operationId": "path1_override_override1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/override1": { + "get": { + "tags": ["path1a", "path1b"], + "summary": "Path1 Override", + "operationId": "path1_override_override1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } } }, }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default1": { - "get": { - "summary": "Path1 Default", - "operationId": "path1_default_default1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level1/level2/override3": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path3a", - "path3b", - ], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level1_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default3": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level1_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - ], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default5": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override3": { - "get": { - "tags": ["level1a", "level1b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_level1_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default3": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_level1_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/default5": { - "get": { - "tags": ["level1a", "level1b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level4/default5": { - "get": { - "tags": ["level1a", "level1b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override5": { - "get": { - "tags": ["level1a", "level1b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default5": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": { - "title": "Level1", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level2/override3": { - "get": { - "tags": ["level2a", "level2b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default3": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/default5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/default5": { - "get": { - "tags": ["level2a", "level2b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/default5": { - "get": { - "tags": ["level2a", "level2b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/override5": { - "get": { - "tags": ["level2a", "level2b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default5": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": { - "title": "Level2", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override3": { - "get": { - "tags": ["path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default3": { - "get": { - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level3/level4/override5": { - "get": { - "tags": [ - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/level4/default5": { - "get": { - "tags": ["level3a", "level3b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/override5": { - "get": { - "tags": ["level3a", "level3b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/default5": { - "get": { - "tags": ["level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": { - "title": "Level3", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level4/override5": { - "get": { - "tags": ["level4a", "level4b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level4/default5": { - "get": { - "tags": ["level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": { - "title": "Level4", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override5": { - "get": { - "tags": ["path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": { - "title": "Level5", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default5": { - "get": { - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": { - "title": "Level0", - "type": "string", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + "deprecated": True, + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "/default1": { + "get": { + "summary": "Path1 Default", + "operationId": "path1_default_default1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + "/level1/level2/override3": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path3a", + "path3b", + ], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level1_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + "deprecated": True, + } }, - } - }, - } + "/level1/level2/default3": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level1_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + ], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/default5": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override3": { + "get": { + "tags": ["level1a", "level1b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_level1_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default3": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_level1_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/default5": { + "get": { + "tags": ["level1a", "level1b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level4/default5": { + "get": { + "tags": ["level1a", "level1b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override5": { + "get": { + "tags": ["level1a", "level1b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default5": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level2/override3": { + "get": { + "tags": ["level2a", "level2b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default3": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/default5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/default5": { + "get": { + "tags": ["level2a", "level2b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/default5": { + "get": { + "tags": ["level2a", "level2b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/override5": { + "get": { + "tags": ["level2a", "level2b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default5": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override3": { + "get": { + "tags": ["path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default3": { + "get": { + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + "/level3/level4/override5": { + "get": { + "tags": [ + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/level4/default5": { + "get": { + "tags": ["level3a", "level3b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/override5": { + "get": { + "tags": ["level3a", "level3b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/default5": { + "get": { + "tags": ["level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level4/override5": { + "get": { + "tags": ["level4a", "level4b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level4/default5": { + "get": { + "tags": ["level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override5": { + "get": { + "tags": ["path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default5": { + "get": { + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 147018996..bb20a4a1a 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -2,6 +2,7 @@ from typing import Optional from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -107,226 +108,232 @@ def test_get_users_item(): 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": { - "summary": "Get Users", - "operationId": "get_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/users/{user_id}": { - "get": { - "summary": "Get User", - "operationId": "get_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "path", - } - ], - "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": { + "/users/": { + "get": { + "summary": "Get Users", + "operationId": "get_users_users__get", + "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", - "operationId": "get_items_items__get", - "parameters": [ - { - "required": False, - "name": "user_id", - "in": "query", - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "summary": "Get Item", - "operationId": "get_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "name": "user_id", - "in": "query", - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{user_id}/items/": { - "get": { - "summary": "Get Items", - "operationId": "get_items_users__user_id__items__get", - "parameters": [ - { - "required": True, - "name": "user_id", - "in": "path", - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{user_id}/items/{item_id}": { - "get": { - "summary": "Get Item", - "operationId": "get_item_users__user_id__items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "name": "user_id", - "in": "path", - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "/users/{user_id}": { + "get": { + "summary": "Get User", + "operationId": "get_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "string"}, + "name": "user_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + } }, - } - }, - } + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "parameters": [ + { + "required": False, + "name": "user_id", + "in": "query", + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "name": "user_id", + "in": "query", + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_users__user_id__items__get", + "parameters": [ + { + "required": True, + "name": "user_id", + "in": "path", + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "name": "user_id", + "in": "path", + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_json_type.py b/tests/test_json_type.py new file mode 100644 index 000000000..3e213eaca --- /dev/null +++ b/tests/test_json_type.py @@ -0,0 +1,63 @@ +import json +from typing import Annotated + +from fastapi import Cookie, FastAPI, Form, Header, Query +from fastapi.testclient import TestClient +from pydantic import Json + +app = FastAPI() + + +@app.post("/form-json-list") +def form_json_list(items: Annotated[Json[list[str]], Form()]) -> list[str]: + return items + + +@app.get("/query-json-list") +def query_json_list(items: Annotated[Json[list[str]], Query()]) -> list[str]: + return items + + +@app.get("/header-json-list") +def header_json_list(x_items: Annotated[Json[list[str]], Header()]) -> list[str]: + return x_items + + +@app.get("/cookie-json-list") +def cookie_json_list(items: Annotated[Json[list[str]], Cookie()]) -> list[str]: + return items + + +client = TestClient(app) + + +def test_form_json_list(): + response = client.post( + "/form-json-list", data={"items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_query_json_list(): + response = client.get( + "/query-json-list", params={"items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_header_json_list(): + response = client.get( + "/header-json-list", headers={"x-items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_cookie_json_list(): + client.cookies.set("items", json.dumps(["abc", "def"])) + response = client.get("/cookie-json-list") + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + client.cookies.clear() diff --git a/tests/test_list_bytes_file_order_preserved_issue_14811.py b/tests/test_list_bytes_file_order_preserved_issue_14811.py new file mode 100644 index 000000000..dbf0a7818 --- /dev/null +++ b/tests/test_list_bytes_file_order_preserved_issue_14811.py @@ -0,0 +1,46 @@ +""" +Regression test: preserve order when using list[bytes] + File() +See https://github.com/fastapi/fastapi/discussions/14811 +Fixed in PR: https://github.com/fastapi/fastapi/pull/14884 +""" + +from typing import Annotated + +import anyio +import pytest +from fastapi import FastAPI, File +from fastapi.testclient import TestClient +from starlette.datastructures import UploadFile as StarletteUploadFile + + +def test_list_bytes_file_preserves_order( + monkeypatch: pytest.MonkeyPatch, +) -> None: + app = FastAPI() + + @app.post("/upload") + async def upload(files: Annotated[list[bytes], File()]): + # return something that makes order obvious + return [b[0] for b in files] + + original_read = StarletteUploadFile.read + + async def patched_read(self: StarletteUploadFile, size: int = -1) -> bytes: + # Make the FIRST file slower *deterministically* + if self.filename == "slow.txt": + await anyio.sleep(0.05) + return await original_read(self, size) + + monkeypatch.setattr(StarletteUploadFile, "read", patched_read) + + client = TestClient(app) + + files = [ + ("files", ("slow.txt", b"A" * 10, "text/plain")), + ("files", ("fast.txt", b"B" * 10, "text/plain")), + ] + r = client.post("/upload", files=files) + assert r.status_code == 200, r.text + + # Must preserve request order: slow first, fast second + assert r.json() == [ord("A"), ord("B")] diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 263d87df2..72707993e 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -1,5 +1,6 @@ import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from .app.main import app @@ -28,122 +29,128 @@ def test_post_invalid(path): 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": { - "/a/compute": { - "post": { - "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": { + "/a/compute": { + "post": { + "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": "Compute", + "operationId": "compute_a_compute_post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_compute_a_compute_post" } } }, + "required": True, }, - }, - "summary": "Compute", - "operationId": "compute_a_compute_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_a_compute_post" - } - } + } + }, + "/b/compute/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "required": True, - }, - } - }, - "/b/compute/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Compute", + "operationId": "compute_b_compute__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_compute_b_compute__post" } } }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_compute_b_compute__post": { + "title": "Body_compute_b_compute__post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "type": "string"}, }, }, - "summary": "Compute", - "operationId": "compute_b_compute__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_b_compute__post" - } + "Body_compute_a_compute_post": { + "title": "Body_compute_a_compute_post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } }, - }, - "components": { - "schemas": { - "Body_compute_b_compute__post": { - "title": "Body_compute_b_compute__post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "type": "string"}, - }, - }, - "Body_compute_a_compute_post": { - "title": "Body_compute_a_compute_post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "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_multi_body_errors.py b/tests/test_multi_body_errors.py index 4418c77cb..792471b5c 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -89,97 +89,103 @@ def test_put_incorrect_body_multiple(): 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/": { - "post": { - "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/": { + "post": { + "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": "Save Item No Body", + "operationId": "save_item_no_body_items__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Item", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, } } }, + "required": True, }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, + } } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "age"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "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": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "age"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "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*$", + }, + ), + ], }, }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 5df51ba18..060951efa 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -42,74 +43,80 @@ def test_multi_query_incorrect(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": { - "title": "Q", - "type": "array", - "items": {"type": "integer"}, - }, - "name": "q", - "in": "query", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "integer"}, + }, + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py index 131a3755e..66bb89902 100644 --- a/tests/test_no_schema_split.py +++ b/tests/test_no_schema_split.py @@ -154,6 +154,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index bd0d55452..deb74d8a0 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -2,6 +2,7 @@ from typing import Union from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -143,275 +144,281 @@ def test_call_api(): 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": { - "/examples/": { - "post": { - "summary": "Examples", - "operationId": "examples_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - }, - "examples": { - "Example One": { - "summary": "Example One Summary", - "description": "Example One Description", - "value": { - "data": "Data in Body examples, example1" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, + "examples": { + "Example One": { + "summary": "Example One Summary", + "description": "Example One Description", + "value": { + "data": "Data in Body examples, example1" + }, + }, + "Example Two": { + "value": { + "data": "Data in Body examples, example2" + } }, }, - "Example Two": { - "value": { - "data": "Data in Body examples, example2" + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "type": "string", + "examples": [ + "json_schema_item_1", + "json_schema_item_2", + ], + "title": "Item Id", + }, + "examples": { + "Path One": { + "summary": "Path One Summary", + "description": "Path One Description", + "value": "item_1", }, + "Path Two": {"value": "item_2"}, }, } - }, - "required": True, - }, - "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" + } } - } - }, - }, - }, - } - }, - "/path_examples/{item_id}": { - "get": { - "summary": "Path Examples", - "operationId": "path_examples_path_examples__item_id__get", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "type": "string", - "examples": [ - "json_schema_item_1", - "json_schema_item_2", - ], - "title": "Item Id", - }, - "examples": { - "Path One": { - "summary": "Path One Summary", - "description": "Path One Description", - "value": "item_1", }, - "Path Two": {"value": "item_2"}, - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } }, }, - }, - } - }, - "/query_examples/": { - "get": { - "summary": "Query Examples", - "operationId": "query_examples_query_examples__get", - "parameters": [ - { - "name": "data", - "in": "query", - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "title": "Data", - }, - "examples": { - "Query One": { - "summary": "Query One Summary", - "description": "Query One Description", - "value": "query1", - }, - "Query Two": {"value": "query2"}, - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_examples/": { - "get": { - "summary": "Header Examples", - "operationId": "header_examples_header_examples__get", - "parameters": [ - { - "name": "data", - "in": "header", - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - }, - "examples": { - "Header One": { - "summary": "Header One Summary", - "description": "Header One Description", - "value": "header1", - }, - "Header Two": {"value": "header2"}, - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_examples/": { - "get": { - "summary": "Cookie Examples", - "operationId": "cookie_examples_cookie_examples__get", - "parameters": [ - { - "name": "data", - "in": "cookie", - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - }, - "examples": { - "Cookie One": { - "summary": "Cookie One Summary", - "description": "Cookie One Description", - "value": "cookie1", - }, - "Cookie Two": {"value": "cookie2"}, - }, - } - ], - "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": {"data": {"type": "string", "title": "Data"}}, - "type": "object", - "required": ["data"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "name": "data", + "in": "query", + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + }, + "examples": { + "Query One": { + "summary": "Query One Summary", + "description": "Query One Description", + "value": "query1", + }, + "Query Two": {"value": "query2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", + } }, - } - }, - } + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "name": "data", + "in": "header", + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + }, + "examples": { + "Header One": { + "summary": "Header One Summary", + "description": "Header One Description", + "value": "header1", + }, + "Header Two": {"value": "header2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "name": "data", + "in": "cookie", + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + }, + "examples": { + "Cookie One": { + "summary": "Cookie One Summary", + "description": "Cookie One Description", + "value": "cookie1", + }, + "Cookie Two": {"value": "cookie2"}, + }, + } + ], + "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": {"data": {"type": "string", "title": "Data"}}, + "type": "object", + "required": ["data"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 084cb695d..836a0a7ee 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -2,6 +2,7 @@ from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -41,86 +42,92 @@ def test_get_route(): def test_openapi(): 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": { - "/": { - "get": { - "summary": "Route With Extra Query Parameters", - "operationId": "route_with_extra_query_parameters__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": 50, - "title": "Standard Query Param", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Route With Extra Query Parameters", + "operationId": "route_with_extra_query_parameters__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + }, + "name": "standard_query_param", + "in": "query", }, - "name": "standard_query_param", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Extra Param 1"}, - "name": "extra_param_1", - "in": "query", - }, - { - "required": True, - "schema": {"title": "Extra Param 2"}, - "name": "extra_param_2", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + { + "required": False, + "schema": {"title": "Extra Param 1"}, + "name": "extra_param_1", + "in": "query", + }, + { + "required": True, + "schema": {"title": "Extra Param 2"}, + "name": "extra_param_2", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "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_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py index 3a3099436..fc11b69c8 100644 --- a/tests/test_openapi_route_extensions.py +++ b/tests/test_openapi_route_extensions.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -18,25 +19,27 @@ def test_get_route(): assert response.json() == {} -def test_openapi(): +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": { - "/": { - "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"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, }, - }, - "summary": "Route With Extras", - "operationId": "route_with_extras__get", - "x-custom-extension": "value", - } + "summary": "Route With Extras", + "operationId": "route_with_extras__get", + "x-custom-extension": "value", + } + }, }, - }, - } + } + ) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index 1891f0bde..0efeece01 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -418,6 +418,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] @@ -442,227 +444,235 @@ def test_openapi_schema_no_separate(): client = get_app_client(separate_input_output_schemas=False) 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", + assert response.json() == snapshot( + { + "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": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "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/Item"} + } + }, + }, + "402": { + "description": "Payment Required", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { "content": { "application/json": { "schema": { "items": {"$ref": "#/components/schemas/Item"}, "type": "array", - "title": "Response Read Items Items Get", + "title": "Item", } } }, - } - }, + "required": True, + }, + "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": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "402": { - "description": "Payment Required", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", + "/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/HTTPValidationError" + "$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" + } + } + }, + }, }, }, }, }, - "/items-list/": { - "post": { - "summary": "Create Item List", - "operationId": "create_item_list_items_list__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Item", - } + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", } }, - "required": True, + "type": "object", + "title": "HTTPValidationError", }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem"}, + {"type": "null"}, + ] }, }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "SubItem": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } }, - "/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": { - "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", - }, - "sub": { - "anyOf": [ - {"$ref": "#/components/schemas/SubItem"}, - {"type": "null"}, - ] - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "SubItem": { - "properties": { - "subname": {"type": "string", "title": "Subname"}, - "sub_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Sub Description", - }, - "tags": { - "items": {"type": "string"}, - "type": "array", - "title": "Tags", - "default": [], - }, - }, - "type": "object", - "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": { - "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_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 08eb0f40f..81178924b 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -1,5 +1,6 @@ from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -23,71 +24,76 @@ def test_read_users(): def test_openapi_schema(): response = client.get("/openapi.json") - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/{user_id}": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/users/{user_id}": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "components": { - "schemas": { - "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_param_include_in_schema.py b/tests/test_param_include_in_schema.py index f461947c9..463fb51b1 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -3,6 +3,7 @@ from typing import Optional import pytest from fastapi import Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -33,138 +34,6 @@ async def hidden_query( return {"hidden_query": hidden_query} -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/hidden_cookie": { - "get": { - "summary": "Hidden Cookie", - "operationId": "hidden_cookie_hidden_cookie_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/hidden_header": { - "get": { - "summary": "Hidden Header", - "operationId": "hidden_header_hidden_header_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/hidden_path/{hidden_path}": { - "get": { - "summary": "Hidden Path", - "operationId": "hidden_path_hidden_path__hidden_path__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/hidden_query": { - "get": { - "summary": "Hidden Query", - "operationId": "hidden_query_hidden_query_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "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"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - - @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ @@ -240,3 +109,140 @@ def test_hidden_query(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + client = TestClient(app) + 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": { + "/hidden_cookie": { + "get": { + "summary": "Hidden Cookie", + "operationId": "hidden_cookie_hidden_cookie_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_header": { + "get": { + "summary": "Hidden Header", + "operationId": "hidden_header_hidden_header_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_path/{hidden_path}": { + "get": { + "summary": "Hidden Path", + "operationId": "hidden_path_hidden_path__hidden_path__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_query": { + "get": { + "summary": "Hidden Query", + "operationId": "hidden_query_hidden_query_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index 8f4c82532..5759a3f9f 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -27,70 +28,76 @@ def test_put_no_body_with_body(): 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}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Save Item No Body", + "operationId": "save_item_no_body_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index 5b4daa450..6074206ff 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -132,6 +132,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index d6eaa45fb..2069004f3 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -4,6 +4,7 @@ import pytest from fastapi import FastAPI, Query from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient +from inline_snapshot import snapshot from .utils import needs_py310 @@ -64,77 +65,82 @@ 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/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "name": "q", - "in": "query", - "required": False, - "schema": { - "anyOf": [ - {"type": "string", "pattern": "^fixedquery$"}, - {"type": "null"}, - ], - "title": "Q", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - } - ], - "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" + } } - } + }, }, }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "components": { - "schemas": { - "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_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index c21829bd9..304052dd1 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -1,5 +1,6 @@ from fastapi import Depends, FastAPI, Header, status from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -19,84 +20,90 @@ def get_deps(dep1: str = Depends(get_header), dep2: str = Depends(get_something_ client = TestClient(app) -schema = { - "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", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.1.0", - "paths": { - "/": { - "get": { - "operationId": "get_deps__get", - "parameters": [ - { - "in": "header", - "name": "someheader", - "required": True, - "schema": {"title": "Someheader", "type": "string"}, - } - ], - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Deps", - } - } - }, -} - - -def test_schema(): - response = client.get("/openapi.json") - assert response.status_code == status.HTTP_200_OK - actual_schema = response.json() - assert actual_schema == schema - assert ( - len(actual_schema["paths"]["/"]["get"]["parameters"]) == 1 - ) # primary goal of this test - def test_response(): response = client.get("/", headers={"someheader": "hello"}) assert response.status_code == status.HTTP_200_OK assert response.json() == {"dep1": "hello", "dep2": "hello123"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == status.HTTP_200_OK + actual_schema = response.json() + assert ( + len(actual_schema["paths"]["/"]["get"]["parameters"]) == 1 + ) # primary goal of this test + assert actual_schema == snapshot( + { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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": { + "/": { + "get": { + "operationId": "get_deps__get", + "parameters": [ + { + "in": "header", + "name": "someheader", + "required": True, + "schema": {"title": "Someheader", "type": "string"}, + } + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Deps", + } + } + }, + } + ) diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index fd72eaab2..32cd55ec3 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, Path, Query, status from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -24,77 +25,82 @@ def test_get_parameters(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == status.HTTP_200_OK - actual_schema = response.json() - assert actual_schema == { - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "title": "Detail", - "type": "array", - } + assert response.json() == snapshot( + { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "title": "Location", + "type": "array", }, - "title": "Location", - "type": "array", + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", }, - "required": ["loc", "msg", "type"], - "title": "ValidationError", - "type": "object", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.1.0", - "paths": { - "/{repeated_alias}": { - "get": { - "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", - "parameters": [ - { - "in": "path", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - { - "in": "query", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - ], - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Parameters With Repeated Aliases", } - } - }, - } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.1.0", + "paths": { + "/{repeated_alias}": { + "get": { + "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", + "parameters": [ + { + "in": "path", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + { + "in": "query", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Parameters With Repeated Aliases", + } + } + }, + } + ) diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index bf3aa758c..77da6aafc 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -2,6 +2,7 @@ from typing import Any from fastapi import FastAPI, Response from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -31,67 +32,73 @@ def test_dependency_set_status_code(): 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": { - "/{id}": { - "delete": { - "summary": "Delete Deployment", - "operationId": "delete_deployment__id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Id", "type": "integer"}, - "name": "id", - "in": "path", - } - ], - "responses": { - "204": {"description": "Successful Response"}, - "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": { + "/{id}": { + "delete": { + "summary": "Delete Deployment", + "operationId": "delete_deployment__id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Id", "type": "integer"}, + "name": "id", + "in": "path", + } + ], + "responses": { + "204": {"description": "Successful Response"}, + "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" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "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_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index d1bff9ddf..10e7be50c 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -1,5 +1,6 @@ from fastapi import Body, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -37,139 +38,147 @@ 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": { - "/products": { - "post": { - "summary": "Create Product", - "operationId": "create_product_products_post", - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/Body_create_product_products_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/products": { + "post": { + "summary": "Create Product", + "operationId": "create_product_products_post", + "requestBody": { "content": { - "application/json": { + "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_product_products_post" } } }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/shops": { + "post": { + "summary": "Create Shop", + "operationId": "create_shop_shops_post", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/Body_create_shop_shops_post" + } + } + }, + "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_product_products_post": { + "title": "Body_create_product_products_post", + "required": ["data"], + "type": "object", + "properties": { + "data": {"$ref": "#/components/schemas/Product"} + }, + }, + "Body_create_shop_shops_post": { + "title": "Body_create_shop_shops_post", + "required": ["data"], + "type": "object", + "properties": { + "data": {"$ref": "#/components/schemas/Shop"}, + "included": { + "title": "Included", + "type": "array", + "items": {"$ref": "#/components/schemas/Product"}, + "default": [], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "Shop": { + "title": "Shop", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/shops": { - "post": { - "summary": "Create Shop", - "operationId": "create_shop_shops_post", - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/Body_create_shop_shops_post" - } - } - }, - "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_product_products_post": { - "title": "Body_create_product_products_post", - "required": ["data"], - "type": "object", - "properties": {"data": {"$ref": "#/components/schemas/Product"}}, - }, - "Body_create_shop_shops_post": { - "title": "Body_create_shop_shops_post", - "required": ["data"], - "type": "object", - "properties": { - "data": {"$ref": "#/components/schemas/Shop"}, - "included": { - "title": "Included", - "type": "array", - "items": {"$ref": "#/components/schemas/Product"}, - "default": [], - }, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Product": { - "title": "Product", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "Shop": { - "title": "Shop", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "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"}, - }, - }, - } - }, - } + } + ) diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py index 6f381c8b8..1b2a18b07 100644 --- a/tests/test_request_params/test_cookie/test_optional_str.py +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import pytest from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -30,17 +31,19 @@ async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()] ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -91,17 +94,19 @@ async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cooki ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -169,17 +174,19 @@ def read_model_optional_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -255,17 +262,19 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py index 3e877b3e3..25f93bba6 100644 --- a/tests/test_request_params/test_cookie/test_required_str.py +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -4,6 +4,7 @@ import pytest from dirty_equals import IsOneOf from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -31,14 +32,16 @@ async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()] ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -96,14 +99,16 @@ async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cooki ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -195,14 +200,16 @@ def read_model_required_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( @@ -302,14 +309,16 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "cookie", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 489a6b3e7..4b8e2adb4 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -4,6 +4,7 @@ import pytest from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -31,18 +32,20 @@ def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -101,18 +104,20 @@ async def read_model_required_list_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -199,18 +204,20 @@ async def read_model_required_list_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -307,18 +314,20 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py index 5dd4ea9ad..3bbb73d54 100644 --- a/tests/test_request_params/test_header/test_optional_list.py +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import pytest from fastapi import FastAPI, Header from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -34,20 +35,22 @@ async def read_model_optional_list_str( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -99,20 +102,22 @@ async def read_model_optional_list_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -178,20 +183,22 @@ def read_model_optional_list_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -266,20 +273,22 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py index 0bd0eddc1..a5174e59a 100644 --- a/tests/test_request_params/test_header/test_optional_str.py +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import pytest from fastapi import FastAPI, Header from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -30,17 +31,19 @@ async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()] ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -90,17 +93,19 @@ async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Heade ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -166,17 +171,19 @@ def read_model_optional_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -250,17 +257,19 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py index 20dd29657..2df9b5f2f 100644 --- a/tests/test_request_params/test_header/test_required_str.py +++ b/tests/test_request_params/test_header/test_required_str.py @@ -4,6 +4,7 @@ import pytest from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -31,14 +32,16 @@ async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()] ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -95,14 +98,16 @@ async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Heade ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -189,14 +194,16 @@ def read_model_required_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( @@ -294,14 +301,16 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "header", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py index b2d63667e..aecc2eb6c 100644 --- a/tests/test_request_params/test_path/test_required_str.py +++ b/tests/test_request_params/test_path/test_required_str.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from fastapi import FastAPI, Path from fastapi.testclient import TestClient +from inline_snapshot import Is, snapshot app = FastAPI() @@ -53,14 +54,16 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": Is(expected_title), "type": "string"}, + "name": Is(expected_name), + "in": "path", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index e933da214..a5cd03021 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -4,6 +4,7 @@ import pytest from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -31,18 +32,20 @@ def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query() ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -101,18 +104,20 @@ async def read_model_required_list_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -199,18 +204,20 @@ async def read_model_required_list_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -305,18 +312,20 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": { + "title": "P Val Alias", + "type": "array", + "items": {"type": "string"}, + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py index 351e03a71..5608c6499 100644 --- a/tests/test_request_params/test_query/test_optional_list.py +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import pytest from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -34,20 +35,22 @@ async def read_model_optional_list_str( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -99,20 +102,22 @@ async def read_model_optional_list_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -178,20 +183,22 @@ def read_model_optional_list_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -264,20 +271,22 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py index 12e1b465a..b181686b0 100644 --- a/tests/test_request_params/test_query/test_optional_str.py +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import pytest from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -30,17 +31,19 @@ async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]): ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -90,17 +93,19 @@ async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -166,17 +171,19 @@ def read_model_optional_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -250,17 +257,19 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", + }, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py index 9e7b96145..c60e5ca02 100644 --- a/tests/test_request_params/test_query/test_required_str.py +++ b/tests/test_request_params/test_query/test_required_str.py @@ -4,6 +4,7 @@ import pytest from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, Field app = FastAPI() @@ -31,14 +32,16 @@ async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]): ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P", "type": "string"}, + "name": "p", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -95,14 +98,16 @@ async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Alias", "type": "string"}, + "name": "p_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -192,14 +197,16 @@ def read_model_required_validation_alias( ["/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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( @@ -297,14 +304,16 @@ def read_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", - } - ] + assert app.openapi()["paths"][path]["get"]["parameters"] == snapshot( + [ + { + "required": True, + "schema": {"title": "P Val Alias", "type": "string"}, + "name": "p_val_alias", + "in": "query", + } + ] + ) @pytest.mark.parametrize( diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index 807d2600b..cf97934a9 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, ConfigDict, Field app = FastAPI() @@ -136,188 +137,194 @@ def test_read_list_no_alias(): 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": { - "/dict": { - "get": { - "summary": "Read Dict", - "operationId": "read_dict_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/model": { - "get": { - "summary": "Read Model", - "operationId": "read_model_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/list": { - "get": { - "summary": "Read List", - "operationId": "read_list_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read List List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/dict": { + "get": { + "summary": "Read Dict", + "operationId": "read_dict_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} } - } - }, - } - }, - } - }, - "/by-alias/dict": { - "get": { - "summary": "By Alias Dict", - "operationId": "by_alias_dict_by_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/model": { - "get": { - "summary": "By Alias Model", - "operationId": "by_alias_model_by_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/list": { - "get": { - "summary": "By Alias List", - "operationId": "by_alias_list_by_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response By Alias List By Alias List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, + }, + } + }, + } + }, + "/model": { + "get": { + "summary": "Read Model", + "operationId": "read_model_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} } - } - }, - } - }, - } - }, - "/no-alias/dict": { - "get": { - "summary": "No Alias Dict", - "operationId": "no_alias_dict_no_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelNoAlias" + }, + } + }, + } + }, + "/list": { + "get": { + "summary": "Read List", + "operationId": "read_list_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read List List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Model" + }, + } } - } - }, - } - }, - } - }, - "/no-alias/model": { - "get": { - "summary": "No Alias Model", - "operationId": "no_alias_model_no_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelNoAlias" + }, + } + }, + } + }, + "/by-alias/dict": { + "get": { + "summary": "By Alias Dict", + "operationId": "by_alias_dict_by_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} } - } - }, - } - }, - } - }, - "/no-alias/list": { - "get": { - "summary": "No Alias List", - "operationId": "no_alias_list_no_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Alias List No Alias List Get", - "type": "array", - "items": { + }, + } + }, + } + }, + "/by-alias/model": { + "get": { + "summary": "By Alias Model", + "operationId": "by_alias_model_by_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/list": { + "get": { + "summary": "By Alias List", + "operationId": "by_alias_list_by_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response By Alias List By Alias List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Model" + }, + } + } + }, + } + }, + } + }, + "/no-alias/dict": { + "get": { + "summary": "No Alias Dict", + "operationId": "no_alias_dict_no_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ModelNoAlias" - }, + } } - } - }, - } + }, + } + }, + } + }, + "/no-alias/model": { + "get": { + "summary": "No Alias Model", + "operationId": "no_alias_model_no_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelNoAlias" + } + } + }, + } + }, + } + }, + "/no-alias/list": { + "get": { + "summary": "No Alias List", + "operationId": "no_alias_list_no_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Alias List No Alias List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelNoAlias" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["alias"], + "type": "object", + "properties": {"alias": {"title": "Alias", "type": "string"}}, + }, + "ModelNoAlias": { + "title": "ModelNoAlias", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", }, } }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["alias"], - "type": "object", - "properties": {"alias": {"title": "Alias", "type": "string"}}, - }, - "ModelNoAlias": { - "title": "ModelNoAlias", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", - }, - } - }, - } + } + ) diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index 4dc164bf9..75582cd60 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Response from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -39,73 +40,75 @@ 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": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JsonApiError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } } - } + }, + }, + "200": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - "200": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } + "summary": "B", + "operationId": "b_b_get", + } + }, }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } }, }, - "summary": "B", - "operationId": "b_b_get", } }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, - } + } + ) diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 70456a746..215621065 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -47,66 +48,68 @@ def test_get_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": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/JsonApiError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } } - } + }, + }, + "204": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "204": {"description": "No Content"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, }, - "204": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } + "summary": "B", + "operationId": "b_b_get", + } + }, }, - "/b": { - "get": { - "responses": { - "204": {"description": "No Content"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } }, }, - "summary": "B", - "operationId": "b_b_get", } }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, - } + } + ) diff --git a/tests/test_response_dependency.py b/tests/test_response_dependency.py new file mode 100644 index 000000000..38c359594 --- /dev/null +++ b/tests/test_response_dependency.py @@ -0,0 +1,173 @@ +"""Test using special types (Response, Request, BackgroundTasks) as dependency annotations. + +These tests verify that special FastAPI types can be used with Depends() annotations +and that the dependency injection system properly handles them. +""" + +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI, Request, Response +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient + + +def test_response_with_depends_annotated(): + """Response type hint should work with Annotated[Response, Depends(...)].""" + app = FastAPI() + + def modify_response(response: Response) -> Response: + response.headers["X-Custom"] = "modified" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(modify_response)]): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +def test_response_with_depends_default(): + """Response type hint should work with Response = Depends(...).""" + app = FastAPI() + + def modify_response(response: Response) -> Response: + response.headers["X-Custom"] = "modified" + return response + + @app.get("/") + def endpoint(response: Response = Depends(modify_response)): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +def test_response_without_depends(): + """Regular Response injection should still work.""" + app = FastAPI() + + @app.get("/") + def endpoint(response: Response): + response.headers["X-Direct"] = "set" + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Direct") == "set" + + +def test_response_dependency_chain(): + """Response dependency should work in a chain of dependencies.""" + app = FastAPI() + + def first_modifier(response: Response) -> Response: + response.headers["X-First"] = "1" + return response + + def second_modifier( + response: Annotated[Response, Depends(first_modifier)], + ) -> Response: + response.headers["X-Second"] = "2" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(second_modifier)]): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.headers.get("X-First") == "1" + assert resp.headers.get("X-Second") == "2" + + +def test_response_dependency_returns_different_response_instance(): + """Dependency that returns a different Response instance should work. + + When a dependency returns a new Response object (e.g., JSONResponse) instead + of modifying the injected one, the returned response should be used and any + modifications to it in the endpoint should be preserved. + """ + app = FastAPI() + + def default_response() -> Response: + response = JSONResponse(content={"status": "ok"}) + response.headers["X-Custom"] = "initial" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(default_response)]): + response.headers["X-Custom"] = "modified" + return response + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +# Tests for Request type hint with Depends +def test_request_with_depends_annotated(): + """Request type hint should work in dependency chain.""" + app = FastAPI() + + def extract_request_info(request: Request) -> dict: + return { + "path": request.url.path, + "user_agent": request.headers.get("user-agent", "unknown"), + } + + @app.get("/") + def endpoint( + info: Annotated[dict, Depends(extract_request_info)], + ): + return info + + client = TestClient(app) + resp = client.get("/", headers={"user-agent": "test-agent"}) + + assert resp.status_code == 200 + assert resp.json() == {"path": "/", "user_agent": "test-agent"} + + +# Tests for BackgroundTasks type hint with Depends +def test_background_tasks_with_depends_annotated(): + """BackgroundTasks type hint should work with Annotated[BackgroundTasks, Depends(...)].""" + app = FastAPI() + task_results = [] + + def background_task(message: str): + task_results.append(message) + + def add_background_task(background_tasks: BackgroundTasks) -> BackgroundTasks: + background_tasks.add_task(background_task, "from dependency") + return background_tasks + + @app.get("/") + def endpoint( + background_tasks: Annotated[BackgroundTasks, Depends(add_background_task)], + ): + background_tasks.add_task(background_task, "from endpoint") + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert "from dependency" in task_results + assert "from endpoint" in task_results diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 58fba89f1..ded597102 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -5,6 +5,7 @@ from fastapi import FastAPI from fastapi.exceptions import FastAPIError, ResponseValidationError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel @@ -512,603 +513,611 @@ def test_invalid_response_model_field(): 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": { - "/no_response_model-no_annotation-return_model": { - "get": { - "summary": "No Response Model No Annotation Return Model", - "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/no_response_model-no_annotation-return_dict": { - "get": { - "summary": "No Response Model No Annotation Return Dict", - "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model-no_annotation-return_same_model": { - "get": { - "summary": "Response Model No Annotation Return Same Model", - "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_exact_dict": { - "get": { - "summary": "Response Model No Annotation Return Exact Dict", - "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_dict": { - "get": { - "summary": "Response Model No Annotation Return Invalid Dict", - "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_model": { - "get": { - "summary": "Response Model No Annotation Return Invalid Model", - "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Dict With Extra Data", - "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Submodel With Extra Data", - "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_same_model": { - "get": { - "summary": "No Response Model Annotation Return Same Model", - "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_exact_dict": { - "get": { - "summary": "No Response Model Annotation Return Exact Dict", - "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_dict": { - "get": { - "summary": "No Response Model Annotation Return Invalid Dict", - "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_model": { - "get": { - "summary": "No Response Model Annotation Return Invalid Model", - "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_dict_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Dict With Extra Data", - "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Submodel With Extra Data", - "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_none-annotation-return_same_model": { - "get": { - "summary": "Response Model None Annotation Return Same Model", - "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_exact_dict": { - "get": { - "summary": "Response Model None Annotation Return Exact Dict", - "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_dict": { - "get": { - "summary": "Response Model None Annotation Return Invalid Dict", - "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_model": { - "get": { - "summary": "Response Model None Annotation Return Invalid Model", - "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Dict With Extra Data", - "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Submodel With Extra Data", - "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_same_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Same Model", - "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_exact_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", - "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", - "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", - "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_dict_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_filtering_model-annotation_submodel-return_submodel": { - "get": { - "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", - "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_list_of_model-no_annotation": { - "get": { - "summary": "Response Model List Of Model No Annotation", - "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_list_of_model": { - "get": { - "summary": "No Response Model Annotation List Of Model", - "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_forward_ref_list_of_model": { - "get": { - "summary": "No Response Model Annotation Forward Ref List Of Model", - "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model1": { - "get": { - "summary": "Response Model Union No Annotation Return Model1", - "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model2": { - "get": { - "summary": "Response Model Union No Annotation Return Model2", - "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model1": { - "get": { - "summary": "No Response Model Annotation Union Return Model1", - "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model2": { - "get": { - "summary": "No Response Model Annotation Union Return Model2", - "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_response_class": { - "get": { - "summary": "No Response Model Annotation Response Class", - "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/no_response_model-annotation_json_response_class": { - "get": { - "summary": "No Response Model Annotation Json Response Class", - "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/no_response_model-no_annotation-return_model": { + "get": { + "summary": "No Response Model No Annotation Return Model", + "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } }, - "User": { - "title": "User", - "required": ["name", "surname"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "surname": {"title": "Surname", "type": "string"}, - }, + "/no_response_model-no_annotation-return_dict": { + "get": { + "summary": "No Response Model No Annotation Return Dict", + "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } }, - } - }, - } + "/response_model-no_annotation-return_same_model": { + "get": { + "summary": "Response Model No Annotation Return Same Model", + "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_exact_dict": { + "get": { + "summary": "Response Model No Annotation Return Exact Dict", + "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_dict": { + "get": { + "summary": "Response Model No Annotation Return Invalid Dict", + "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_model": { + "get": { + "summary": "Response Model No Annotation Return Invalid Model", + "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Dict With Extra Data", + "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Submodel With Extra Data", + "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_same_model": { + "get": { + "summary": "No Response Model Annotation Return Same Model", + "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_exact_dict": { + "get": { + "summary": "No Response Model Annotation Return Exact Dict", + "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_dict": { + "get": { + "summary": "No Response Model Annotation Return Invalid Dict", + "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_model": { + "get": { + "summary": "No Response Model Annotation Return Invalid Model", + "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_dict_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Dict With Extra Data", + "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Submodel With Extra Data", + "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_none-annotation-return_same_model": { + "get": { + "summary": "Response Model None Annotation Return Same Model", + "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_exact_dict": { + "get": { + "summary": "Response Model None Annotation Return Exact Dict", + "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_dict": { + "get": { + "summary": "Response Model None Annotation Return Invalid Dict", + "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_model": { + "get": { + "summary": "Response Model None Annotation Return Invalid Model", + "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Dict With Extra Data", + "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Submodel With Extra Data", + "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_same_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Same Model", + "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_exact_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", + "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", + "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", + "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_dict_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_filtering_model-annotation_submodel-return_submodel": { + "get": { + "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", + "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_list_of_model-no_annotation": { + "get": { + "summary": "Response Model List Of Model No Annotation", + "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + }, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_list_of_model": { + "get": { + "summary": "No Response Model Annotation List Of Model", + "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + }, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_forward_ref_list_of_model": { + "get": { + "summary": "No Response Model Annotation Forward Ref List Of Model", + "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + }, + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model1": { + "get": { + "summary": "Response Model Union No Annotation Return Model1", + "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model2": { + "get": { + "summary": "Response Model Union No Annotation Return Model2", + "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model1": { + "get": { + "summary": "No Response Model Annotation Union Return Model1", + "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model2": { + "get": { + "summary": "No Response Model Annotation Union Return Model2", + "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_response_class": { + "get": { + "summary": "No Response Model Annotation Response Class", + "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-annotation_json_response_class": { + "get": { + "summary": "No Response Model Annotation Json Response Class", + "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["name", "surname"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "surname": {"title": "Surname", "type": "string"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index 8036bcb07..9c7096c5f 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel @@ -47,110 +48,114 @@ def test_path_operations(): 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": { - "/valid1": { - "get": { - "summary": "Valid1", - "operationId": "valid1_valid1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid1 Valid1 Get", - "type": "integer", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/valid1": { + "get": { + "summary": "Valid1", + "operationId": "valid1_valid1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid1 Valid1 Get", + "type": "integer", + } } - } + }, }, }, - }, - } - }, - "/valid2": { - "get": { - "summary": "Valid2", - "operationId": "valid2_valid2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid2 Valid2 Get", - "type": "array", - "items": {"type": "integer"}, + } + }, + "/valid2": { + "get": { + "summary": "Valid2", + "operationId": "valid2_valid2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid2 Valid2 Get", + "type": "array", + "items": {"type": "integer"}, + } } - } + }, }, }, - }, - } - }, - "/valid3": { - "get": { - "summary": "Valid3", - "operationId": "valid3_valid3_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } + } + }, + "/valid3": { + "get": { + "summary": "Valid3", + "operationId": "valid3_valid3_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - }, - }, - } - }, - "/valid4": { - "get": { - "summary": "Valid4", - "operationId": "valid4_valid4_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid4 Valid4 Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} } - } + }, }, }, - }, + } + }, + "/valid4": { + "get": { + "summary": "Valid4", + "operationId": "valid4_valid4_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid4 Valid4 Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Model" + }, + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + } } }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - } - } - }, - } + } + ) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py new file mode 100644 index 000000000..492a26d00 --- /dev/null +++ b/tests/test_router_circular_import.py @@ -0,0 +1,12 @@ +import pytest +from fastapi import APIRouter + + +def test_router_circular_import(): + router = APIRouter() + + with pytest.raises( + AssertionError, + match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?", + ): + router.include_router(router) diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 9df299cda..a47d11913 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -241,3 +241,139 @@ def test_merged_mixed_state_lifespans() -> None: with TestClient(app) as client: assert client.app_state == {"router": True} + + +@pytest.mark.filterwarnings( + r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" +) +def test_router_async_shutdown_handler(state: State) -> None: + """Test that async on_shutdown event handlers are called correctly, for coverage.""" + app = FastAPI() + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + @app.on_event("shutdown") + async def app_shutdown() -> None: + state.app_shutdown = True + + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert state.app_shutdown is True + + +def test_router_sync_generator_lifespan(state: State) -> None: + """Test that a sync generator lifespan works via _wrap_gen_lifespan_context.""" + from collections.abc import Generator + + def lifespan(app: FastAPI) -> Generator[None, None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True + + +def test_router_async_generator_lifespan(state: State) -> None: + """Test that an async generator lifespan (not wrapped) works.""" + + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True + + +def test_startup_shutdown_handlers_as_parameters(state: State) -> None: + """Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly.""" + + def app_startup() -> None: + state.app_startup = True + + def app_shutdown() -> None: + state.app_shutdown = True + + app = FastAPI(on_startup=[app_startup], on_shutdown=[app_shutdown]) + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + def router_startup() -> None: + state.router_startup = True + + def router_shutdown() -> None: + state.router_shutdown = True + + router = APIRouter(on_startup=[router_startup], on_shutdown=[router_shutdown]) + + def sub_router_startup() -> None: + state.sub_router_startup = True + + def sub_router_shutdown() -> None: + state.sub_router_shutdown = True + + sub_router = APIRouter( + on_startup=[sub_router_startup], on_shutdown=[sub_router_shutdown] + ) + + router.include_router(sub_router) + app.include_router(router) + + assert state.app_startup is False + assert state.router_startup is False + assert state.sub_router_startup is False + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is True + assert state.router_shutdown is True + assert state.sub_router_shutdown is True diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index ac8999c90..9ec41e7e8 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -4,6 +4,7 @@ import pytest from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, ConfigDict @@ -263,590 +264,596 @@ def test_openapi_schema(): client = TestClient(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": { - "/schema_extra/": { - "post": { - "summary": "Schema Extra", - "operationId": "schema_extra_schema_extra__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/schema_extra/": { + "post": { + "summary": "Schema Extra", + "operationId": "schema_extra_schema_extra__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - } - }, - "/example/": { - "post": { - "summary": "Example", - "operationId": "example_example__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": {"data": "Data in Body example"}, - } - }, - "required": True, - }, - "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": {}}}, }, - }, - }, - } - }, - "/examples/": { - "post": { - "summary": "Examples", - "operationId": "examples_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - } - }, - "required": True, - }, - "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" + } } - } - }, - }, - }, - } - }, - "/example_examples/": { - "post": { - "summary": "Example Examples", - "operationId": "example_examples_example_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], }, - "example": {"data": "Overridden example"}, + }, + }, + } + }, + "/example/": { + "post": { + "summary": "Example", + "operationId": "example_example__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": {"data": "Data in Body example"}, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/example_examples/": { + "post": { + "summary": "Example Examples", + "operationId": "example_examples_example_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, + "example": {"data": "Overridden example"}, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example/{item_id}": { + "get": { + "summary": "Path Example", + "operationId": "path_example_path_example__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "example": "item_1", + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example_examples/{item_id}": { + "get": { + "summary": "Path Example Examples", + "operationId": "path_example_examples_path_example_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], + }, + "example": "item_overridden", + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example/": { + "get": { + "summary": "Query Example", + "operationId": "query_example_query_example__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, + "example": "query1", + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example_examples/": { + "get": { + "summary": "Query Example Examples", + "operationId": "query_example_examples_query_example_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, + "example": "query_overridden", + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example/": { + "get": { + "summary": "Header Example", + "operationId": "header_example_header_example__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, + "example": "header1", + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example_examples/": { + "get": { + "summary": "Header Example Examples", + "operationId": "header_example_examples_header_example_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, + "example": "header_overridden", + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example/": { + "get": { + "summary": "Cookie Example", + "operationId": "cookie_example_cookie_example__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, + "example": "cookie1", + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example_examples/": { + "get": { + "summary": "Cookie Example Examples", + "operationId": "cookie_example_examples_cookie_example_examples__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, + "example": "cookie_overridden", + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + "example": {"data": "Data in schema_extra"}, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/path_example/{item_id}": { - "get": { - "summary": "Path Example", - "operationId": "path_example_path_example__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "example": "item_1", - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_examples/{item_id}": { - "get": { - "summary": "Path Examples", - "operationId": "path_examples_path_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "examples": ["item_1", "item_2"], - }, - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_example_examples/{item_id}": { - "get": { - "summary": "Path Example Examples", - "operationId": "path_example_examples_path_example_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "examples": ["item_1", "item_2"], - }, - "example": "item_overridden", - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example/": { - "get": { - "summary": "Query Example", - "operationId": "query_example_query_example__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - }, - "example": "query1", - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_examples/": { - "get": { - "summary": "Query Examples", - "operationId": "query_examples_query_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - }, - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example_examples/": { - "get": { - "summary": "Query Example Examples", - "operationId": "query_example_examples_query_example_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - }, - "example": "query_overridden", - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example/": { - "get": { - "summary": "Header Example", - "operationId": "header_example_header_example__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - }, - "example": "header1", - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_examples/": { - "get": { - "summary": "Header Examples", - "operationId": "header_examples_header_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - }, - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example_examples/": { - "get": { - "summary": "Header Example Examples", - "operationId": "header_example_examples_header_example_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - }, - "example": "header_overridden", - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example/": { - "get": { - "summary": "Cookie Example", - "operationId": "cookie_example_cookie_example__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - }, - "example": "cookie1", - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_examples/": { - "get": { - "summary": "Cookie Examples", - "operationId": "cookie_examples_cookie_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example_examples/": { - "get": { - "summary": "Cookie Example Examples", - "operationId": "cookie_example_examples_cookie_example_examples__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, - "example": "cookie_overridden", - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "type": "string"}}, - "example": {"data": "Data in schema_extra"}, - }, - "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_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 9bacfc56e..e37e113b8 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyCookie from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,27 +42,29 @@ def test_openapi_schema(): client = TestClient(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": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyCookie": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } + ) diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index d0cab324e..b12dce4bf 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyCookie from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,32 +42,34 @@ def test_openapi_schema(): client = TestClient(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": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyCookie": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": { - "type": "apiKey", - "name": "key", - "in": "cookie", - "description": "An API Cookie Key", + }, + "components": { + "securitySchemes": { + "APIKeyCookie": { + "type": "apiKey", + "name": "key", + "in": "cookie", + "description": "An API Cookie Key", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index cb5590168..7988d8044 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyCookie from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -47,27 +48,29 @@ def test_openapi_schema(): client = TestClient(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": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyCookie": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } + ) diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 3e761b150..a53a2a227 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyHeader from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,27 +42,29 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyHeader": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } + ) diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 38a1a8881..0052dbcfc 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyHeader from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,32 +42,34 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyHeader": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": { - "type": "apiKey", - "name": "key", - "in": "header", - "description": "An API Key Header", + }, + "components": { + "securitySchemes": { + "APIKeyHeader": { + "type": "apiKey", + "name": "key", + "in": "header", + "description": "An API Key Header", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index 6f9682a64..51abd0bb9 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyHeader from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -46,27 +47,29 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyHeader": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } + ) diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index 11ed19468..c3cb855fc 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyQuery from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,27 +42,29 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyQuery": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } + ) diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 658798326..f775a4fea 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyQuery from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -41,32 +42,34 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyQuery": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": { - "type": "apiKey", - "name": "key", - "in": "query", - "description": "API Key Query", + }, + "components": { + "securitySchemes": { + "APIKeyQuery": { + "type": "apiKey", + "name": "key", + "in": "query", + "description": "API Key Query", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 4cc134bd4..26fbb9ee4 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyQuery from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -46,27 +47,29 @@ def test_security_api_key_no_key(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], + assert response.json() == snapshot( + { + "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": [{"APIKeyQuery": []}], + } } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, - } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } + ) diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index ac38ee718..fff6f0ee5 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -37,25 +38,27 @@ def test_security_http_base_no_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBase": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } + ) diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index 791ea59f4..b03636a61 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -31,31 +32,33 @@ def test_security_http_base_no_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBase": []}], + } } - } - }, - "components": { - "securitySchemes": { - "HTTPBase": { - "type": "http", - "scheme": "Other", - "description": "Other Security Scheme", + }, + "components": { + "securitySchemes": { + "HTTPBase": { + "type": "http", + "scheme": "Other", + "description": "Other Security Scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index dd4d76843..612a7909f 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -36,25 +37,27 @@ def test_security_http_base_no_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBase": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } + ) diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 7071f381a..e94565c7b 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -4,6 +4,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -53,25 +54,27 @@ def test_security_http_basic_non_basic_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], + assert response.json() == snapshot( + { + "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"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } + ) diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index ec7371f90..0e328cdd9 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -3,6 +3,7 @@ from base64 import b64encode from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -51,25 +52,27 @@ def test_security_http_basic_non_basic_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], + assert response.json() == snapshot( + { + "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"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } + ) diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index a93d5fc86..5423d9b8d 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -3,6 +3,7 @@ from base64 import b64encode from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -51,31 +52,33 @@ def test_security_http_basic_non_basic_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], + assert response.json() == snapshot( + { + "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", - "description": "HTTPBasic scheme", + }, + "components": { + "securitySchemes": { + "HTTPBasic": { + "type": "http", + "scheme": "basic", + "description": "HTTPBasic scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 961b42f4d..eb94ddc3d 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -38,25 +39,27 @@ def test_security_http_bearer_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBearer": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } + ) diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index e16994abc..47fc14107 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -38,31 +39,33 @@ def test_security_http_bearer_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "HTTPBearer": { - "type": "http", - "scheme": "bearer", - "description": "HTTP Bearer token scheme", + }, + "components": { + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer", + "description": "HTTP Bearer token scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index 943da2ee2..b49a6593e 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -42,25 +43,27 @@ def test_security_http_bearer_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPBearer": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } + ) diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 3fad4c7a5..345ae9c17 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -40,25 +41,27 @@ def test_security_http_digest_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPDigest": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } + ) diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 319416a07..1219bd40e 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -40,31 +41,33 @@ def test_security_http_digest_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPDigest": []}], + } } - } - }, - "components": { - "securitySchemes": { - "HTTPDigest": { - "type": "http", - "scheme": "digest", - "description": "HTTPDigest scheme", + }, + "components": { + "securitySchemes": { + "HTTPDigest": { + "type": "http", + "scheme": "digest", + "description": "HTTPDigest scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index 0d66f9c72..97e62634d 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -44,25 +45,27 @@ def test_security_http_digest_incorrect_scheme_credentials(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], + assert response.json() == snapshot( + { + "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": [{"HTTPDigest": []}], + } } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } + ) diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 7ad936995..1c216e95d 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -2,6 +2,7 @@ import pytest from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -148,122 +149,132 @@ def test_strict_login_correct_grant_type(): 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": { - "/login": { - "post": { - "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": { + "/login": { + "post": { + "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": "Login", + "operationId": "login_login_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_login_post" } } }, + "required": True, }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "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"}], - }, - }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OAuth2": []}], + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "^password$", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "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"}], }, }, - "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": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, }, - }, - } + } + ) diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index 66f53ab00..1ba577e9f 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -46,36 +47,38 @@ def test_token_with_whitespaces(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index 5386fbbd9..73807c31a 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -43,37 +44,39 @@ def test_token(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, - "description": "OAuth2 Code Bearer", + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + "description": "OAuth2 Code Bearer", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 57c16058a..cb79afdb8 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -4,6 +4,7 @@ import pytest from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -151,122 +152,132 @@ def test_strict_login_correct_data(): 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": { - "/login": { - "post": { - "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": { + "/login": { + "post": { + "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": "Login", + "operationId": "login_login_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_login_post" } } }, + "required": True, }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "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"}], - }, - }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "^password$", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "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"}], }, }, - "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": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, }, - }, - } + } + ) diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 60c6c242e..b3fae37a1 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -4,6 +4,7 @@ import pytest from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -152,123 +153,133 @@ def test_strict_login_correct_correct_grant_type(): 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": { - "/login": { - "post": { - "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": { + "/login": { + "post": { + "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": "Login", + "operationId": "login_login_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_login_post" } } }, + "required": True, }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "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"}], - }, - }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "^password$", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "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"}], }, }, - "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": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - "description": "OAuth2 security scheme", - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + "description": "OAuth2 security scheme", + } + }, }, - }, - } + } + ) diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 4c9362c3e..01e2f65ed 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import OAuth2PasswordBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -40,30 +41,32 @@ def test_incorrect_token(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index 6e6ea846c..fec8d03a7 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import OAuth2PasswordBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI() @@ -44,31 +45,33 @@ def test_incorrect_token(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, - "description": "OAuth2PasswordBearer security scheme", + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + "description": "OAuth2PasswordBearer security scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index c9a0a8db7..2c8bcd494 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -47,30 +48,32 @@ def test_security_oauth2_password_bearer_no_header(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], + assert response.json() == snapshot( + { + "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": [{"OpenIdConnect": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "/openid", + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index d008cbc63..4f69c82ff 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -1,6 +1,7 @@ from fastapi import Depends, FastAPI, Security from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -49,31 +50,33 @@ def test_security_oauth2_password_bearer_no_header(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], + assert response.json() == snapshot( + { + "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": [{"OpenIdConnect": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "/openid", - "description": "OpenIdConnect security scheme", + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + "description": "OpenIdConnect security scheme", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index e817434b0..ebaf394dc 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -3,6 +3,7 @@ from typing import Optional from fastapi import Depends, FastAPI, Security from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -52,30 +53,32 @@ def test_security_oauth2_password_bearer_no_header(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], + assert response.json() == snapshot( + { + "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": [{"OpenIdConnect": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "/openid", + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 229fe8016..95c780c0d 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient +from inline_snapshot import snapshot from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @@ -79,124 +80,130 @@ def test_no_body_status_code_with_detail_exception_handlers(): 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": { - "/http-no-body-statuscode-exception": { - "get": { - "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code Exception", - } - }, - "/http-no-body-statuscode-with-detail-exception": { - "get": { - "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code With Detail Exception", - } - }, - "/items/{item_id}": { - "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"}, + "paths": { + "/http-no-body-statuscode-exception": { + "get": { + "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } }, - "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", - } - ], - } - }, - "/starlette-items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Starlette Item", - "operationId": "read_starlette_item_starlette_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "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"}, - }, + "summary": "No Body Status Code Exception", + } }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + "/http-no-body-statuscode-with-detail-exception": { + "get": { + "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "No Body Status Code With Detail Exception", + } }, - } - }, - } + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "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", + } + ], + } + }, + "/starlette-items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Starlette Item", + "operationId": "read_starlette_item_starlette_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index cc7e5f5c6..86dc4d00e 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -2,6 +2,7 @@ from typing import Optional from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, HttpUrl app = FastAPI() @@ -86,208 +87,222 @@ def test_get(): def test_openapi_schema(): with client: response = client.get("/openapi.json") - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "title": "Callback Url", - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + }, + "name": "callback_url", + "in": "query", } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Invoice" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "event_callback": { + "{$callback_url}/events/{$request.body.title}": { + "get": { + "summary": "Event Callback", + "operationId": "event_callback__callback_url__events___request_body_title__get", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } } }, }, + } + } + }, + "components": { + "schemas": { + "Event": { + "title": "Event", + "required": ["name", "total"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, }, - "callbacks": { - "event_callback": { - "{$callback_url}/events/{$request.body.title}": { - "get": { - "summary": "Event Callback", - "operationId": "event_callback__callback_url__events___request_body_title__get", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } + }, + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": { + "title": "Description", + "type": "string", + }, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } - } - }, - "components": { - "schemas": { - "Event": { - "title": "Event", - "required": ["name", "total"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "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_tuples.py b/tests/test_tuples.py index d3c89045b..265388587 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, Form from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -83,187 +84,193 @@ def test_tuple_form_invalid(): 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": { - "/model-with-tuple/": { - "post": { - "summary": "Post Model With Tuple", - "operationId": "post_model_with_tuple_model_with_tuple__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemGroup"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model-with-tuple/": { + "post": { + "summary": "Post Model With Tuple", + "operationId": "post_model_with_tuple_model_with_tuple__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemGroup"} + } + }, + "required": True, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "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", + } + }, + "/tuple-of-models/": { + "post": { + "summary": "Post Tuple Of Models", + "operationId": "post_tuple_of_models_tuple_of_models__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], } } }, + "required": True, }, - }, - } - }, - "/tuple-of-models/": { - "post": { - "summary": "Post Tuple Of Models", - "operationId": "post_tuple_of_models_tuple_of_models__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - } + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + } + }, + "/tuple-form/": { + "post": { + "summary": "Hello", + "operationId": "hello_tuple_form__post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_hello_tuple_form__post" } } }, + "required": True, }, - }, - } - }, - "/tuple-form/": { - "post": { - "summary": "Hello", - "operationId": "hello_tuple_form__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_hello_tuple_form__post" - } - } - }, - "required": True, - }, - "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_hello_tuple_form__post": { - "title": "Body_hello_tuple_form__post", - "required": ["values"], - "type": "object", - "properties": { - "values": { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "integer"}, - {"type": "integer"}, - ], - } - }, - }, - "Coordinate": { - "title": "Coordinate", - "required": ["x", "y"], - "type": "object", - "properties": { - "x": {"title": "X", "type": "number"}, - "y": {"title": "Y", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ItemGroup": { - "title": "ItemGroup", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "array", - "items": { + "components": { + "schemas": { + "Body_hello_tuple_form__post": { + "title": "Body_hello_tuple_form__post", + "required": ["values"], + "type": "object", + "properties": { + "values": { + "title": "Values", "maxItems": 2, "minItems": 2, "type": "array", "prefixItems": [ - {"type": "string"}, - {"type": "string"}, + {"type": "integer"}, + {"type": "integer"}, ], - }, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, + } }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - } - }, - } + "Coordinate": { + "title": "Coordinate", + "required": ["x", "y"], + "type": "object", + "properties": { + "x": {"title": "X", "type": "number"}, + "y": {"title": "Y", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ItemGroup": { + "title": "ItemGroup", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 1a18db75c..d8d9d4130 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.additional_responses.tutorial001_py39 import app @@ -20,97 +21,107 @@ def test_path_operation_not_found(): 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": { - "responses": { - "404": { - "description": "Not Found", - "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" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": { + "description": "Not Found", + "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" + } + } + }, }, }, - }, - "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": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "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"}] - }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "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"}, - } + "Message": { + "title": "Message", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 820860595..4fae59d22 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -4,6 +4,7 @@ import shutil import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -41,93 +42,99 @@ def test_path_operation_img(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": "Return the JSON item or an image.", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Return the JSON item or an image.", + "content": { + "image/png": {}, + "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", + "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", - }, - { - "required": False, - "schema": { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", + { + "required": False, + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, + "name": "img", + "in": "query", }, - "name": "img", - "in": "query", - }, - ], + ], + } } - } - }, - "components": { - "schemas": { - "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": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "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"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 90dc4e371..e888819df 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.additional_responses.tutorial003_py39 import app @@ -20,101 +21,111 @@ def test_path_operation_not_found(): 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": { - "responses": { - "404": { - "description": "The item was not found", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Message"} - } - }, - }, - "200": { - "description": "Item requested by ID", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": { - "id": "bar", - "value": "The bar tenders", - }, - } - }, - }, - "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": { + "404": { + "description": "The item was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } } - } + }, + }, + "200": { + "description": "Item requested by ID", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": { + "id": "bar", + "value": "The bar tenders", + }, + } + }, + }, + "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": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "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"}] - }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "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"}, - } + "Message": { + "title": "Message", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index c6abf5e46..9df326a5c 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -4,6 +4,7 @@ import shutil import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -41,96 +42,102 @@ def test_path_operation_img(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": { - "404": {"description": "Item not found"}, - "302": {"description": "The item was moved"}, - "403": {"description": "Not enough privileges"}, - "200": { - "description": "Successful Response", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": {"description": "Item not found"}, + "302": {"description": "The item was moved"}, + "403": {"description": "Not enough privileges"}, + "200": { + "description": "Successful Response", + "content": { + "image/png": {}, + "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", + "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", - }, - { - "required": False, - "schema": { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", + { + "required": False, + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, + "name": "img", + "in": "query", }, - "name": "img", - "in": "query", - }, - ], + ], + } } - } - }, - "components": { - "schemas": { - "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": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "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"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) 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 00574b5b0..31adaa56a 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial001_py39 import app @@ -14,22 +15,24 @@ def test_main(): def test_openapi(): 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": { - "/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"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - "servers": [{"url": "/api/v1"}], - } + }, + "servers": [{"url": "/api/v1"}], + } + ) 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 1a49c0dfe..56e6f2f9d 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial002_py39 import app @@ -14,22 +15,24 @@ def test_main(): def test_openapi(): 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": { - "/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"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - "servers": [{"url": "/api/v1"}], - } + }, + "servers": [{"url": "/api/v1"}], + } + ) diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index f5e243b95..18845e470 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -269,332 +270,338 @@ def test_admin_invalid_header(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": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "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": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "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": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, }, }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } }, }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "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_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 5a7cae160..c1877a72e 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -260,87 +261,93 @@ def test_other_exceptions(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/": { - "post": { - "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/": { + "post": { + "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": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - "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"}] + }, + "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"}], }, }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "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_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py index b6d51d523..94bf213e3 100644 --- a/tests/test_tutorial/test_body/test_tutorial002.py +++ b/tests/test_tutorial/test_body/test_tutorial002.py @@ -3,6 +3,7 @@ from typing import Union import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -75,87 +76,93 @@ def test_post_with_no_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/": { - "post": { - "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/": { + "post": { + "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": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - "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"}] + }, + "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"}], }, }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "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 index 227a125e7..832c211f6 100644 --- a/tests/test_tutorial/test_body/test_tutorial003.py +++ b/tests/test_tutorial/test_body/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -74,98 +75,104 @@ def test_put_with_no_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}": { - "put": { - "parameters": [ - { - "in": "path", - "name": "item_id", - "required": True, - "schema": { - "title": "Item Id", - "type": "integer", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - "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"}] + }, + "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"}], }, }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "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 index 10212843e..1019a168c 100644 --- a/tests/test_tutorial/test_body/test_tutorial004.py +++ b/tests/test_tutorial/test_body/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -76,107 +77,113 @@ def test_put_with_no_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}": { - "put": { - "parameters": [ - { - "in": "path", - "name": "item_id", - "required": True, - "schema": { - "title": "Item Id", - "type": "integer", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - { - "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", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - "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"}] + }, + "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"}], }, }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "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 0ecadbb66..0116dcb09 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -74,111 +75,117 @@ def test_invalid_price(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": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" } } }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "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": "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": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "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": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], }, }, - "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"}, - } + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, }, - }, - } - }, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 63c9c16d6..2e8ba457b 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -67,114 +68,120 @@ def test_post_id_foo(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": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "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/HTTPValidationError" + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", } } + } + }, + } + } + }, + "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"}], }, }, }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, - "name": "item_id", - "in": "path", + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - }, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } - } + }, }, } - } - }, - "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"}], - }, - }, - }, - "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_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py index e98d5860f..0c94e9dd1 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -197,165 +198,169 @@ def test_post_id_foo(client: TestClient): 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": { + assert response.json() == snapshot( + { + "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": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + "title": "Item Id", + "type": "integer", }, }, - }, - "required": True, - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, - }, - }, - "description": "Successful Response", - }, - "422": { + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", }, }, }, - "description": "Validation Error", + "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", }, - "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", + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", }, - "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", - }, - ], + "user": { + "$ref": "#/components/schemas/User", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 76b7ff709..c27f3d5ba 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -102,119 +103,125 @@ def test_post_body_empty_list(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": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" } } }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "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"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}], }, }, - "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"}, - } + "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"}, + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py index 979c054cd..2a39f3d71 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -159,132 +160,138 @@ def test_put_invalid_importance(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": "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/HTTPValidationError" + "$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"}], + }, }, }, - "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": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": { + "title": "Full Name", "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" - } + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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 index d47aa1b4f..d600e0767 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -131,142 +132,146 @@ def test_post_missing_required_field_in_item(client: TestClient): 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": { + assert response.json() == snapshot( + { + "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": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + "title": "Item Id", + "type": "integer", }, }, - }, - "required": True, - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, - }, - }, - "description": "Successful Response", - }, - "422": { + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", }, }, }, - "description": "Validation Error", + "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", }, - "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", + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", }, - "title": "Detail", - "type": "array", }, + "required": ["item"], + "title": "Body_update_item_items__item_id__put", + "type": "object", }, - "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", - }, - ], + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 index d452929c3..18bce279b 100644 --- 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 @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import Is, snapshot from ...utils import needs_py310 @@ -142,110 +143,116 @@ def test_openapi_schema(client: TestClient, mod_name: str): 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", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$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": Is(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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item", - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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 index ff9596943..6a70779b6 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -139,137 +140,143 @@ def test_put_missing_required_in_image(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": { - "parameters": [ - { - "in": "path", - "name": "item_id", - "required": True, - "schema": { - "title": "Item Id", - "type": "integer", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Item", } } }, + "required": True, }, - }, - "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"}] + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + }, + "name": { + "title": "Name", + "type": "string", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["url", "name"], + "title": "Image", + "type": "object", }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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 index 9a07a904e..2ff3d4f22 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -162,140 +163,146 @@ def test_put_wrong_url(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": { - "parameters": [ - { - "in": "path", - "name": "item_id", - "required": True, - "schema": { - "title": "Item Id", - "type": "integer", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Item", } } }, + "required": True, }, - }, - "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"}] + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["url", "name"], + "title": "Image", + "type": "object", }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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 index 088177cb9..229216fc5 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -122,148 +123,154 @@ def test_put_images_not_list(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": { - "parameters": [ - { - "in": "path", - "name": "item_id", - "required": True, - "schema": { - "title": "Item Id", - "type": "integer", + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Item", } } }, + "required": True, }, - }, - "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"}] + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["url", "name"], + "title": "Image", + "type": "object", }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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 index a30281950..5a7763f59 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -184,161 +185,167 @@ def test_put_missing_required_in_images(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": { - "/offers/": { - "post": { - "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": { + "/offers/": { + "post": { + "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": "Create Offer", + "operationId": "create_offer_offers__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Offer", } } }, + "required": True, }, - }, - "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"}] + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["url", "name"], + "title": "Image", + "type": "object", }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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 index 32eb8ee75..26f48f1d5 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -66,92 +67,98 @@ def test_post_not_a_list(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": { - "/images/multiple/": { - "post": { - "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": { + "/images/multiple/": { + "post": { + "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": "Create Multiple Images", + "operationId": "create_multiple_images_images_multiple__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Images", + "type": "array", + "items": {"$ref": "#/components/schemas/Image"}, } } }, + "required": True, }, - }, - "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"}] + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": ["url", "name"], + "title": "Image", + "type": "object", }, - }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, - }, - } - }, - } + "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 f2e56d40f..492dcecd2 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -43,74 +44,80 @@ def test_post_invalid_body(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": { - "/index-weights/": { - "post": { - "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": { + "/index-weights/": { + "post": { + "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": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, } } }, + "required": True, + }, + } + } + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "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_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 0401eb7d0..feb07b859 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -48,139 +49,145 @@ def test_put(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", + 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", + } + ], + }, + "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, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + } + }, + "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": [], }, }, }, - "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" - } - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "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"} + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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_body_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py index 466e6af8f..a34d08b52 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial002.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -69,139 +70,145 @@ def test_patch_name(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", + 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", + } + ], + }, + "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, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + } + }, + "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": [], }, }, }, - "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" - } - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "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"} + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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 ddc282d85..644b82ad9 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -1,6 +1,7 @@ import importlib from fastapi.testclient import TestClient +from inline_snapshot import snapshot def get_client() -> TestClient: @@ -38,21 +39,23 @@ def test_default_openapi(): response = client.get("/redoc") assert response.status_code == 200, response.text response = client.get("/openapi.json") - 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": {}}}, - } - }, + 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": {}}}, + } + }, + } } - } - }, - } + }, + } + ) 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 ac8e7bdae..f391c569a 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -151,6 +151,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] 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 d7c3d15f1..6583045dc 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -161,6 +161,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 9b47cbc67..4d7adf0d6 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -3,6 +3,7 @@ from types import ModuleType import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -47,73 +48,79 @@ def test_openapi_schema(mod: ModuleType): client = TestClient(mod.app) 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": "Ads Id", - }, - "name": "ads_id", - "in": "cookie", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + }, + "name": "ads_id", + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index f1d2accef..20244bef4 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -26,21 +27,23 @@ def test_get_custom_response(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 3337f47d5..746801df1 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.custom_response.tutorial001b_py39 import app @@ -14,21 +15,23 @@ def test_get_custom_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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } } - } - }, - } + }, + } + ) 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 index 22e2e0254..68e046ac5 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import Is, snapshot @pytest.fixture( @@ -48,21 +49,23 @@ def test_openapi_schema(client: TestClient, mod_name: str): 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", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": Is(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 fea105c28..b4919af6c 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.custom_response.tutorial005_py39 import app @@ -14,21 +15,25 @@ def test_get(): 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": { - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"text/plain": {"schema": {"type": "string"}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/plain": {"schema": {"type": "string"}} + }, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index e9e6ca108..ea2d366aa 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.custom_response.tutorial006_py39 import app @@ -14,21 +15,23 @@ def test_get(): 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": { - "/typer": { - "get": { - "summary": "Redirect Typer", - "operationId": "redirect_typer_typer_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"}, + "paths": { + "/typer": { + "get": { + "summary": "Redirect Typer", + "operationId": "redirect_typer_typer_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index 7ca727d2c..133a591b1 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.custom_response.tutorial006b_py39 import app @@ -14,16 +15,18 @@ def test_redirect_response_class(): 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": { - "/fastapi": { - "get": { - "summary": "Redirect Fastapi", - "operationId": "redirect_fastapi_fastapi_get", - "responses": {"307": {"description": "Successful Response"}}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/fastapi": { + "get": { + "summary": "Redirect Fastapi", + "operationId": "redirect_fastapi_fastapi_get", + "responses": {"307": {"description": "Successful Response"}}, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index e3f76c840..5b17d815d 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.custom_response.tutorial006c_py39 import app @@ -14,16 +15,18 @@ def test_redirect_status_code(): 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": { - "/pydantic": { - "get": { - "summary": "Redirect Pydantic", - "operationId": "redirect_pydantic_pydantic_get", - "responses": {"302": {"description": "Successful Response"}}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/pydantic": { + "get": { + "summary": "Redirect Pydantic", + "operationId": "redirect_pydantic_pydantic_get", + "responses": {"302": {"description": "Successful Response"}}, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 4683062f5..a3d2cf515 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -50,87 +51,93 @@ def test_post_invalid_item(client: TestClient): 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/": { - "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": {}}}, - }, - "422": { - "description": "Validation Error", + 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": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "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"}, - "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"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index a6a9fc1c7..e023271bc 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -70,133 +71,141 @@ def test_get_authors(client: TestClient): 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": { - "/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"}, + assert response.json() == snapshot( + { + "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" + }, + } + } + }, } }, - "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": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", + "components": { + "schemas": { + "Author": { + "title": "Author", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, }, }, - "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" + }, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_debugging/test_tutorial001.py b/tests/test_tutorial/test_debugging/test_tutorial001.py index cf62c3b19..1a0ca6cad 100644 --- a/tests/test_tutorial/test_debugging/test_tutorial001.py +++ b/tests/test_tutorial/test_debugging/test_tutorial001.py @@ -5,6 +5,7 @@ import unittest import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot MOD_NAME = "docs_src.debugging.tutorial001_py39" @@ -44,21 +45,23 @@ def test_uvicorn_run_called_when_run_as_main(): # Just for coverage 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": {}}}, + 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": {}}}, + }, }, - }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py index 50d7c4108..74f4a8f3a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -43,146 +44,152 @@ def test_get(path, expected_status, expected_response, 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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - }, - "name": "q", - "in": "query", + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], + }, } }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - }, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "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_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py index f09d6f268..6e1cea5a0 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -87,93 +88,99 @@ def test_get(path, expected_status, expected_response, 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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", }, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", }, - "name": "limit", - "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py index a914936ba..31054744a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial005.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -58,82 +59,88 @@ def test_get(path, cookie, expected_status, expected_response, client: TestClien 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" + assert response.json() == snapshot( + { + "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", + "summary": "Read Query", + "operationId": "read_query_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", }, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Last Query", + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Last Query", + }, + "name": "last_query", + "in": "cookie", }, - "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 59202df3b..8d6cfa2d6 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -68,76 +69,82 @@ def test_get_valid_headers(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py index 4868254c0..383422a7e 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial011.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial011.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -47,74 +48,80 @@ def test_get(path, expected_status, expected_response, 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": { - "/query-checker/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/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", + "summary": "Read Query Check", + "operationId": "read_query_check_query_checker__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "", + "title": "Q", + }, + "name": "q", + "in": "query", }, - "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 d5599ac73..a15688924 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -115,112 +116,118 @@ def test_get_valid_headers_users(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/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "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_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py index 5c8ee054d..a3d672333 100644 --- a/tests/test_tutorial/test_encoder/test_tutorial001.py +++ b/tests/test_tutorial/test_encoder/test_tutorial001.py @@ -3,6 +3,7 @@ from types import ModuleType import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -73,136 +74,140 @@ def test_put_invalid_data(client: TestClient, mod: ModuleType): 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": { + assert response.json() == snapshot( + { + "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": { - "$ref": "#/components/schemas/Item", + "title": "Id", + "type": "string", }, }, - }, - "required": True, - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, - }, - }, - "description": "Successful Response", - }, - "422": { + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + "$ref": "#/components/schemas/Item", }, }, }, - "description": "Validation Error", + "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", }, - "summary": "Update Item", }, }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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": { + "Item": { + "properties": { + "description": { "anyOf": [ { "type": "string", }, { - "type": "integer", + "type": "null", }, ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": { + "title": "Title", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 5fe99d50d..63215c00d 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -1,6 +1,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture(name="app", scope="module") @@ -21,72 +22,79 @@ def test_openapi_schema(app: FastAPI): with TestClient(app) as client: 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": {}}}, - }, - "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": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index 0612899b5..f98d8921f 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -1,6 +1,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture(name="app", scope="module") @@ -23,21 +24,23 @@ def test_openapi_schema(app: FastAPI): with TestClient(app) as client: 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": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index 38710edfe..4fc848e11 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.events.tutorial003_py39 import ( app, @@ -21,72 +22,79 @@ def test_openapi_schema(): with TestClient(app) as client: 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": { - "/predict": { - "get": { - "summary": "Predict", - "operationId": "predict_predict_get", - "parameters": [ - { - "required": True, - "schema": {"title": "X", "type": "number"}, - "name": "x", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/predict": { + "get": { + "summary": "Predict", + "operationId": "predict_predict_get", + "parameters": [ + { + "required": True, + "schema": {"title": "X", "type": "number"}, + "name": "x", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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" - }, - } - }, - }, - "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_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index 83ecb300e..bc10c888c 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.extending_openapi.tutorial001_py39 import app @@ -14,32 +15,34 @@ def test(): 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": "Custom title", - "summary": "This is a very custom OpenAPI schema", - "description": "Here's a longer description of the custom **OpenAPI** schema", - "version": "2.5.0", - "x-logo": { - "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": { + "title": "Custom title", + "summary": "This is a very custom OpenAPI schema", + "description": "Here's a longer description of the custom **OpenAPI** schema", + "version": "2.5.0", + "x-logo": { + "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" + }, }, - }, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } } - } - }, - } + }, + } + ) openapi_schema = response.json() # Request again to test the custom cache response = client.get("/openapi.json") 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 5479e2925..28fe68f28 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -133,6 +133,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py index 3f2f508a1..4248878b5 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -44,113 +45,119 @@ def test_post(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": { + "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"}], + }, }, }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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 872af5383..38e874158 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -127,6 +127,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 073375ccc..c64371e3b 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -29,42 +30,46 @@ def test_get_items(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index 8e52d8d4b..43b8bae8e 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -26,29 +27,31 @@ def test_get_items(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": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py index aa65218cd..5457ad132 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -33,21 +34,23 @@ def test_get_path(client: TestClient, path, expected_status, expected_response): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Root", - "operationId": "root__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Root", + "operationId": "root__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py index bbb66b451..62799d259 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial001.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -34,109 +35,119 @@ def test_get_items(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/": { - "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"}, + assert response.json() == snapshot( + { + "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", + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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"}] - }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - } - }, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py index ab8bc4c11..f64f5f866 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial002.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial002.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.generate_clients.tutorial002_py39 import app @@ -31,157 +32,167 @@ def test_get_items(): 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"}, + assert response.json() == snapshot( + { + "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", + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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" + } + } + }, + }, + }, + } + }, }, - "/users/": { - "post": { - "tags": ["users"], - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} + "components": { + "schemas": { + "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/ResponseMessage" - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - }, - "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 bac52e4fd..34ede6194 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.generate_clients.tutorial003_py39 import app @@ -31,157 +32,167 @@ def test_get_items(): 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": "items-get_items", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Items-Get Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "items-get_items", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Items-Get Items", + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + } } - } - }, - } - }, - }, - "post": { - "tags": ["items"], - "summary": "Create Item", - "operationId": "items-create_item", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "items-create_item", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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": "users-create_user", + "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" + } + } + }, + }, + }, + } + }, }, - "/users/": { - "post": { - "tags": ["users"], - "summary": "Create User", - "operationId": "users-create_user", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} + "components": { + "schemas": { + "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/ResponseMessage" - } - } - }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - }, - "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_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py index e66f6d2a1..d04bfbfe8 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial004.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial004.py @@ -3,6 +3,8 @@ import json import pathlib from unittest.mock import patch +from inline_snapshot import snapshot + from docs_src.generate_clients import tutorial003_py39 @@ -15,216 +17,220 @@ def test_remove_tags(tmp_path: pathlib.Path): 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", + assert modified_openapi == snapshot( + { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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", - }, - ], + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", }, - "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", + "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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", }, - "title": "Response Items-Get Items", - "type": "array", - }, + { + "type": "integer", + }, + ], }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "description": "Successful Response", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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", + "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", }, }, - "required": True, + "summary": "Get Items", + "tags": [ + "items", + ], }, - "responses": { - "200": { + "post": { + "operationId": "create_item", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResponseMessage", + "$ref": "#/components/schemas/Item", }, }, }, - "description": "Successful Response", + "required": True, }, - "422": { + "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/HTTPValidationError", + "$ref": "#/components/schemas/User", }, }, }, - "description": "Validation Error", + "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", + ], }, - "summary": "Create User", - "tags": [ - "users", - ], }, }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_graphql/test_tutorial001.py b/tests/test_tutorial/test_graphql/test_tutorial001.py index 9ba7147b5..cc3be6a19 100644 --- a/tests/test_tutorial/test_graphql/test_tutorial001.py +++ b/tests/test_tutorial/test_graphql/test_tutorial001.py @@ -1,6 +1,7 @@ import warnings import pytest +from inline_snapshot import snapshot from starlette.testclient import TestClient warnings.filterwarnings( @@ -26,45 +27,47 @@ def test_query(client: TestClient): 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": {}, + assert response.json() == snapshot( + { + "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.", }, - "description": "The GraphiQL integrated development environment.", - }, - "404": { - "description": "Not found if GraphiQL or query via GET are not enabled.", }, + "summary": "Handle Http Get", }, - "summary": "Handle Http Get", - }, - "post": { - "operationId": "handle_http_post_graphql_post", - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, + "post": { + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, }, + "description": "Successful Response", }, - "description": "Successful Response", }, + "summary": "Handle Http Post", }, - "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 c01850fae..8283cdc73 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial001_py39 import app @@ -21,70 +22,76 @@ def test_get_item_not_found(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "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": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index 09366a86f..c437693d3 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial002_py39 import app @@ -21,70 +22,76 @@ def test_get_item_not_found_header(): 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-header/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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-header/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Header", - "operationId": "read_item_header_items_header__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Item Header", + "operationId": "read_item_header_items_header__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index 51ac3e7b2..959729e53 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial003_py39 import app @@ -22,70 +23,76 @@ def test_get_exception(): 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": { - "/unicorns/{name}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/unicorns/{name}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Unicorn", - "operationId": "read_unicorn_unicorns__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], + "summary": "Read Unicorn", + "operationId": "read_unicorn_unicorns__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 376bc8266..16165bb3d 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial004_py39 import app @@ -27,70 +28,76 @@ def test_get(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "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": "integer"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index 7bd947f19..af924a9f7 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial005_py39 import app @@ -31,79 +32,85 @@ def test_post(): 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/": { - "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": {}}}, - }, - "422": { - "description": "Validation Error", + 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": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Item": { + "title": "Item", + "required": ["title", "size"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["title", "size"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index e95e53d5e..4c069d81e 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.handling_errors.tutorial006_py39 import app @@ -35,70 +36,76 @@ def test_get(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "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": "integer"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) 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 1fa8aee46..2d14c698e 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -187,6 +187,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] 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 079a8f540..478ac8408 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -184,6 +184,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] 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 4c89d80ee..00636c2b5 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -224,6 +224,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 88591b822..2e97a8322 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -39,73 +40,79 @@ def test(path, headers, expected_status, expected_response, client: TestClient): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": "User-Agent", - }, - "name": "user-agent", - "in": "header", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + }, + "name": "user-agent", + "in": "header", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 229f96c1f..ba86c55bf 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -50,73 +51,79 @@ def test(path, headers, expected_status, expected_response, client: TestClient): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": "Strange Header", - }, - "name": "strange_header", - "in": "header", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + }, + "name": "strange_header", + "in": "header", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index cf067ccf9..30e5133c0 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -44,76 +45,82 @@ def test(path, headers, expected_status, expected_response, client: TestClient): 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 Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - "name": "x-token", - "in": "header", - } - ], - "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" + } } - } + }, }, }, + } + } + }, + "components": { + "schemas": { + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "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_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index ead48577d..ba3c09ce4 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.metadata.tutorial001_py39 import app @@ -14,36 +15,38 @@ def test_items(): 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": "ChimichangApp", - "summary": "Deadpool's favorite app. Nuff said.", - "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", - "termsOfService": "http://example.com/terms/", - "contact": { - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + "version": "0.0.1", }, - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html", - }, - "version": "0.0.1", - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py index 10cb35c54..933954973 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001_1.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.metadata.tutorial001_1_py39 import app @@ -14,36 +15,38 @@ def test_items(): 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": "ChimichangApp", - "summary": "Deadpool's favorite app. Nuff said.", - "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", - "termsOfService": "http://example.com/terms/", - "contact": { - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "identifier": "Apache-2.0", + }, + "version": "0.0.1", }, - "license": { - "name": "Apache 2.0", - "identifier": "Apache-2.0", - }, - "version": "0.0.1", - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_metadata/test_tutorial002.py b/tests/test_tutorial/test_metadata/test_tutorial002.py index e2814c88f..5a252475e 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial002.py +++ b/tests/test_tutorial/test_metadata/test_tutorial002.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.metadata.tutorial002_py39 import app @@ -19,24 +20,26 @@ def test_get_openapi_json_default_url(): def test_openapi_schema(): 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", - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__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", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_metadata/test_tutorial003.py b/tests/test_tutorial/test_metadata/test_tutorial003.py index 085c271cd..778781e66 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial003.py +++ b/tests/test_tutorial/test_metadata/test_tutorial003.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.metadata.tutorial003_py39 import app @@ -14,27 +15,29 @@ def test_items(): 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": {}}}, - } - }, + assert response.json() == snapshot( + { + "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(): diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index 4ef93fd5e..5c309a830 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.metadata.tutorial004_py39 import app @@ -15,49 +16,51 @@ def test_path_operations(): 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": { - "tags": ["users"], - "summary": "Get Users", - "operationId": "get_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "get_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "tags": [ - { - "name": "users", - "description": "Operations with users. The **login** logic is also here.", - }, - { - "name": "items", - "description": "Manage items. So _fancy_ they have their own docs.", - "externalDocs": { - "description": "Items external docs", - "url": "https://fastapi.tiangolo.com/", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } }, }, - ], - } + "tags": [ + { + "name": "users", + "description": "Operations with users. The **login** logic is also here.", + }, + { + "name": "items", + "description": "Manage items. So _fancy_ they have their own docs.", + "externalDocs": { + "description": "Items external docs", + "url": "https://fastapi.tiangolo.com/", + }, + }, + ], + } + ) diff --git a/tests/test_tutorial/test_middleware/test_tutorial001.py b/tests/test_tutorial/test_middleware/test_tutorial001.py index cbcfd4146..18047f343 100644 --- a/tests/test_tutorial/test_middleware/test_tutorial001.py +++ b/tests/test_tutorial/test_middleware/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.middleware.tutorial001_py39 import app @@ -14,11 +15,13 @@ def test_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": {}, - } + assert response.json() == snapshot( + { + "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 6fde96cb5..1e937d0c9 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -3,6 +3,7 @@ from types import ModuleType import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -42,161 +43,167 @@ def test_dummy_callback(mod: ModuleType): 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": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - "title": "Callback Url", - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + }, + "name": "callback_url", + "in": "query", } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "schema": {"$ref": "#/components/schemas/Invoice"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "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" + }, + } }, }, - "callbacks": { - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "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_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py index 27619489f..0482c94bf 100644 --- a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.openapi_webhooks.tutorial001_py39 import app @@ -19,99 +20,107 @@ def test_dummy_webhook(): 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": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "webhooks": { - "new-subscription": { - "post": { - "summary": "New Subscription", - "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", - "operationId": "new_subscriptionnew_subscription_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Subscription"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + } + } + }, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Subscription" } } }, + "required": True, }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "monthly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Subscription": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "monthly_fee": {"type": "number", "title": "Monthly Fee"}, - "start_date": { - "type": "string", - "format": "date-time", - "title": "Start Date", - }, - }, - "type": "object", - "required": ["username", "monthly_fee", "start_date"], - "title": "Subscription", - }, - "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_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index ee0b70710..3a4f64824 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,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_advanced_configuration.tutorial001_py39 import app @@ -14,21 +15,23 @@ def test_get(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "some_specific_id_you_define", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "some_specific_id_you_define", + } } - } - }, - } + }, + } + ) 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 f6580d72e..c26c36030 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,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_advanced_configuration.tutorial002_py39 import app @@ -14,21 +15,23 @@ def test_get(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items", + } } - } - }, - } + }, + } + ) 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 104554fce..f060647b2 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,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_advanced_configuration.tutorial003_py39 import app @@ -14,8 +15,10 @@ def test_get(): 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": {}, - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + } + ) 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 a95540731..bf7934544 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -38,99 +39,105 @@ def test_query_params_str_validations(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/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", + assert response.json() == snapshot( + { + "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, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + } + } + }, + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], }, }, }, - "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"} + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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"}], - }, - "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 e2a71236f..779e54f9e 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,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_advanced_configuration.tutorial005_py39 import app @@ -13,22 +14,24 @@ def test_get(): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "x-aperture-labs-portal": "blue", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "x-aperture-labs-portal": "blue", + } } - } - }, - } + }, + } + ) 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 9484f7f57..d63b5f912 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,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_advanced_configuration.tutorial006_py39 import app @@ -21,37 +22,39 @@ def test_post(): 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/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"type": "string"}, - "price": {"type": "number"}, - "description": {"type": "string"}, - }, + 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": { + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"type": "string"}, + "price": {"type": "number"}, + "description": {"type": "string"}, + }, + } } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + } } - } - }, - } + }, + } + ) 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 d5f284e3b..ec0c91bda 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -75,41 +76,43 @@ def test_post_invalid(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/": { - "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"}, + 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/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": {}}}, } }, - "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 index 085d1f5e1..500cb057c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -45,142 +46,146 @@ def test_post_items(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/": { - "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", + 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": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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": { + "Item": { + "properties": { + "description": { "anyOf": [ { "type": "string", }, { - "type": "integer", + "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", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 index c7414d756..1cc560cb9 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py @@ -3,6 +3,7 @@ import importlib import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -57,167 +58,171 @@ def test_get_users(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/": { - "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"} + assert response.json() == snapshot( + { + "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": {}}}, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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": {}}}, + } + }, + } + }, }, - "/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", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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": { + "Item": { + "properties": { + "description": { "anyOf": [ { "type": "string", }, { - "type": "integer", + "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", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 5a0193adf..98319645c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_configuration.tutorial002b_py39 import app @@ -20,35 +21,37 @@ def test_get_users(): 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": {}}}, - } - }, - } + assert response.json() == snapshot( + { + "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": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, }, - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - } + } + ) 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 index 791db2462..c13a3c38c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -4,6 +4,7 @@ from textwrap import dedent import pytest from dirty_equals import IsList from fastapi.testclient import TestClient +from inline_snapshot import Is, snapshot from ...utils import needs_py310 @@ -66,143 +67,147 @@ def test_openapi_schema(client: TestClient, mod_name: str): 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", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create an item", + "description": Is(DESCRIPTIONS[mod_name]), + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "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", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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": { + "Item": { + "properties": { + "description": { "anyOf": [ { "type": "string", }, { - "type": "integer", + "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", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 c5a3aec1d..07152755f 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -37,99 +38,105 @@ def test_query_params_str_validations(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/": { - "post": { - "responses": { - "200": { - "description": "The created item", + assert response.json() == snapshot( + { + "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, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + } + } + }, + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], }, }, }, - "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"} + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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"}], - }, - "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 5d9c55675..5f56ab929 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -1,5 +1,6 @@ import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_operation_configuration.tutorial006_py39 import app @@ -23,49 +24,51 @@ def test_query_params_str_validations(path, expected_status, 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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + } + }, + "/elements/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Elements", + "operationId": "read_elements_elements__get", + "deprecated": True, + } + }, }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - } - }, - "/elements/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Elements", - "operationId": "read_elements_elements__get", - "deprecated": True, - } - }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py index a898e386f..ec598832b 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial001.py +++ b/tests/test_tutorial/test_path_params/test_tutorial001.py @@ -1,5 +1,6 @@ import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial001_py39 import app @@ -22,95 +23,99 @@ def test_get_items(item_id, 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": {}, + assert response.json() == snapshot( + { + "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", }, }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, }, }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", }, - "description": "Validation Error", }, + "summary": "Read Item", }, - "summary": "Read Item", }, }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index 0bfc9f807..8384ec8ef 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial002.py +++ b/tests/test_tutorial/test_path_params/test_tutorial002.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial002_py39 import app @@ -29,96 +30,100 @@ def test_get_items_invalid_id(): 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": {}, + assert response.json() == snapshot( + { + "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", }, }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, }, }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", }, - "description": "Validation Error", }, + "summary": "Read Item", }, - "summary": "Read Item", }, }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index cd2c39ab0..432ccde49 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial003.py +++ b/tests/test_tutorial/test_path_params/test_tutorial003.py @@ -1,5 +1,6 @@ import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial003_py39 import app @@ -22,112 +23,116 @@ def test_get_users(user_id: str, expected_response: dict): 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": {}, + assert response.json() == snapshot( + { + "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", }, }, - "description": "Successful Response", + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, }, + "summary": "Read User", }, - "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", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", "type": "string", }, }, - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, - }, - }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, - }, - }, - "description": "Validation Error", - }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index 8e4a26a1c..1cf39eca9 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial003b.py +++ b/tests/test_tutorial/test_path_params/test_tutorial003b.py @@ -1,6 +1,7 @@ import asyncio from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial003b_py39 import app, read_users2 @@ -20,25 +21,27 @@ def test_read_users2(): # Just for coverage 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": {}, + assert response.json() == snapshot( + { + "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", }, - "description": "Successful Response", }, + "summary": "Read Users2", }, - "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 f7f233ccf..b691e821d 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial004_py39 import app @@ -22,70 +23,76 @@ def test_root_file_path(): 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": { - "/files/{file_path}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/files/{file_path}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read File", - "operationId": "read_file_files__file_path__get", - "parameters": [ - { - "required": True, - "schema": {"title": "File Path", "type": "string"}, - "name": "file_path", - "in": "path", - } - ], + "summary": "Read File", + "operationId": "read_file_files__file_path__get", + "parameters": [ + { + "required": True, + "schema": {"title": "File Path", "type": "string"}, + "name": "file_path", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, - }, - } - }, - } + } + }, + } + ) diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 86ccce7b6..c60fee3f0 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.path_params.tutorial005_py39 import app @@ -42,76 +43,81 @@ def test_get_enums_invalid(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - data = response.json() - assert data == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/models/{model_name}": { - "get": { - "summary": "Get Model", - "operationId": "get_model_models__model_name__get", - "parameters": [ - { - "required": True, - "schema": {"$ref": "#/components/schemas/ModelName"}, - "name": "model_name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/models/{model_name}": { + "get": { + "summary": "Get Model", + "operationId": "get_model_models__model_name__get", + "parameters": [ + { + "required": True, + "schema": {"$ref": "#/components/schemas/ModelName"}, + "name": "model_name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelName": { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "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_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py index f1e304103..b43de7032 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -53,112 +54,116 @@ def test_read_items_invalid_item_id(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": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "type": "integer", + assert response.json() == snapshot( + { + "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", }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "null", - }, - ], - "title": "Item-Query", + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Item-Query", + }, + "name": "item-query", + "in": "query", }, - "name": "item-query", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {}, - } - }, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } }, }, - "description": "Validation Error", + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, }, - }, + } } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index 467c915dc..1bb2a3ea8 100644 --- 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -66,105 +67,109 @@ def test_read_items_missing_q(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": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "type": "integer", + assert response.json() == snapshot( + { + "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", }, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": { - "type": "string", - "title": "Q", + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", }, - "name": "q", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {}, - } - }, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } }, }, - "description": "Validation Error", + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, }, - }, + } } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index d3593c984..37c16d295 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -80,106 +81,110 @@ def test_read_items_missing_q(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": { - "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, + assert response.json() == snapshot( + { + "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", }, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": { - "type": "string", - "title": "Q", + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", }, - "name": "q", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {}, - } - }, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } }, }, - "description": "Validation Error", + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, }, - }, + } } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index 296192593..0afc9dcbc 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -96,107 +97,111 @@ def test_read_items_missing_q(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": { - "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, + assert response.json() == snapshot( + { + "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", }, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": { - "type": "string", - "title": "Q", + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", }, - "name": "q", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {}, - } - }, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } }, }, - "description": "Validation Error", + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, }, - }, + } } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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 index 9dc7d7aac..e0a9694c1 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -104,118 +105,122 @@ def test_read_items_size_too_large(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": { - "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, + assert response.json() == snapshot( + { + "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", }, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": { - "type": "string", - "title": "Q", + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", }, - "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", - }, + { + "in": "query", + "name": "size", + "required": True, + "schema": { + "exclusiveMaximum": 10.5, + "exclusiveMinimum": 0, + "title": "Size", + "type": "number", }, }, - "description": "Validation Error", + ], + "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", + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "integer", - }, - ], + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", }, - "title": "Location", - "type": "array", - }, - "msg": { - "title": "Message", - "type": "string", - }, - "type": { - "title": "Error Type", - "type": "string", }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "required": [ - "loc", - "msg", - "type", - ], - "title": "ValidationError", - "type": "object", }, }, - }, - } + } + ) 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 d3ce57121..38b767154 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -207,6 +207,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] 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 96abce6ab..b173a2df4 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -213,6 +213,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py index 4c92b57b8..40ada0e8e 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial001.py +++ b/tests/test_tutorial/test_query_params/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -43,84 +44,90 @@ def test_read_user_item(client: TestClient, path, 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, + assert response.json() == snapshot( + { + "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", }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 10, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 10, + }, + "name": "limit", + "in": "query", }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, }, }, + "description": "Validation Error", }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, } - } - }, - "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 index ae3ee7613..1da308a7e 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial002.py +++ b/tests/test_tutorial/test_query_params/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,86 +43,92 @@ def test_read_user_item(client: TestClient, path, 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", - }, - ], + assert response.json() == snapshot( + { + "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", }, - "name": "q", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + { + "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", }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, } - } - }, - "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 index c0b7e3b13..9bb58ff9f 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial003.py +++ b/tests/test_tutorial/test_query_params/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -53,96 +54,102 @@ def test_read_user_item(client: TestClient, path, 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", - }, - ], + assert response.json() == snapshot( + { + "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", }, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Short", - "type": "boolean", - "default": False, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", }, - "name": "short", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + { + "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", }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, } - } - }, - "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 index 9be18b74d..20aadb3ac 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -55,102 +56,108 @@ def test_read_user_item(client: TestClient, path, 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", - }, - ], + assert response.json() == snapshot( + { + "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", }, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Short", - "type": "boolean", - "default": False, + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", }, - "name": "short", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + { + "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", }, - "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, } - } - }, - "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 103078147..e023fe6d8 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.query_params.tutorial005_py39 import app @@ -29,76 +30,82 @@ def test_foo_no_needy(): 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"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - ], + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 157322c7e..b28e12655 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -61,95 +62,101 @@ def test_foo_no_needy(client: TestClient): 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": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Limit", + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", }, - "name": "limit", - "in": "query", - }, - ], + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + }, + "name": "limit", + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py index f1af7e08c..ed73b7329 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -46,76 +47,82 @@ def test_query_params_str_validations_q_query(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index 62018b80b..3eac1f2b3 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -64,79 +65,85 @@ def test_query_params_str_validations_q_too_long(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index a4ad7a63b..59d5160ac 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -74,80 +75,86 @@ def test_query_params_str_validations_q_too_long(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index 585989a82..abf08c932 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -67,81 +68,87 @@ def test_query_params_str_validations_q_nonregexquery(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index 52462fe33..7b5368abc 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -57,75 +58,81 @@ def test_query_params_str_validations_q_short(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index 640cedce1..2c1df2c08 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -63,74 +64,80 @@ def test_query_params_str_validations_q_fixedquery_too_short(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index f287b5dcd..3df9efa83 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -73,76 +74,82 @@ def test_query_params_str_validations_q_short(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index b17bc2771..874002b17 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -58,79 +59,85 @@ def test_query_params_str_validations_q_fixedquery_too_short(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index c63111574..b9613a17b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -58,81 +59,87 @@ def test_query_params_str_validations_q_fixedquery_too_short(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", + "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", - }, - "name": "q", - "in": "query", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 index 7e9d69d41..d749d85f7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -48,76 +49,82 @@ def test_query_params_str_validations_q_fixedquery(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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", - } - ], + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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 00889c5bf..efe9f1fa6 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 @@ -3,6 +3,7 @@ import importlib import pytest from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient +from inline_snapshot import Is, snapshot from ...utils import needs_py310 @@ -65,90 +66,95 @@ def test_query_params_str_validations_item_query_nonregexquery(client: TestClien 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, - "deprecated": True, - "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", - } - ], - } - } - }, - "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"}, - } - }, - }, - } - }, + + parameters_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 {}), } + + assert response.json() == snapshot( + { + "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, + "deprecated": True, + "schema": Is(parameters_schema), + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "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_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index 11de33ae1..c25357fdd 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -41,76 +42,82 @@ def test_query_no_values(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - }, - "name": "q", - "in": "query", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + {"type": "array", "items": {"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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index 182692861..c3d01e90e 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -37,75 +38,81 @@ def test_multi_query_values(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 46c367c86..74efa4695 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -37,75 +38,81 @@ def test_query_no_values(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "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": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "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"}] + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 0feaccfa4..6dff18b78 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 @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -39,62 +40,68 @@ def test_no_hidden_query(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/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "components": { - "schemas": { - "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_query_params_str_validations/test_tutorial015.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py index 82bb606a9..32a990e74 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 @@ -122,6 +122,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index e0e1bbe63..fd5c3b055 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -82,120 +83,134 @@ def test_post_upload_file(tmp_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": { - "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": { + "/files/": { + "post": { + "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": "Create File", + "operationId": "create_file_files__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_file_files__post" } } }, + "required": True, }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" } } }, + "required": True, }, - }, - "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" - } + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "format": "binary", + } + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "format": "binary", + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "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_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 18948c544..feeb5363e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -173,6 +173,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", 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 53a7a0cf8..5ef8df178 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -41,130 +42,136 @@ def test_post_upload_file(tmp_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": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + 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": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_file_files__post" } } }, + "required": True, + }, + "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": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "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_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } }, - "/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" - } - } - }, - "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_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "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_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 03772419a..52655fd63 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -3,6 +3,7 @@ import importlib import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -102,140 +103,146 @@ def test_get_root(app: FastAPI): 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": { - "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": { + "/files/": { + "post": { + "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": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_files_files__post" } } }, + "required": True, }, - }, - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "required": True, - }, - } - }, - "/uploadfiles/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" } } }, + "required": True, }, - }, - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - } - }, - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Main", - "operationId": "main__get", - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, + "summary": "Main", + "operationId": "main__get", + } }, - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } }, - "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"}, - } + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, }, - }, - } - }, - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index fa4bfd569..a1425ba33 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -3,6 +3,7 @@ import importlib import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -72,142 +73,148 @@ def test_get_root(app: FastAPI): 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 Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_files_files__post" } } }, + "required": True, }, - }, - } - }, - "/uploadfiles/": { - "post": { - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" } } }, + "required": True, }, - }, - } - }, - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as bytes", - } - }, - }, - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as UploadFile", - } - }, - }, - "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"}] + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + } }, - } - }, - } + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + } + }, + } + ) 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 0c43dd7b2..68fa074dc 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -99,79 +100,85 @@ def test_post_body_json(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": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/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"}, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "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_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index 238f8fa2e..ed5ae109a 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -116,80 +117,86 @@ def test_post_body_json(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": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "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": { + "/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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "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 4276414fc..d1596f137 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -99,81 +100,87 @@ def test_post_body_json(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": { - "/login/": { - "post": { - "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": { + "/login/": { + "post": { + "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": "Login", + "operationId": "login_login__post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_login__post" } } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_login_login__post": { + "title": "Body_login_login__post", + "required": ["username", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, }, }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login__post" - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Body_login_login__post": { - "title": "Body_login_login__post", - "required": ["username", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "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"}, - }, - }, - "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_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index 7fa4c3de5..6e9165c42 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 @@ -3,6 +3,7 @@ import importlib import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -149,86 +150,96 @@ def test_post_files_and_token(tmp_path, app: FastAPI): 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": { - "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": { + "/files/": { + "post": { + "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": "Create File", + "operationId": "create_file_files__post", + "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_create_file_files__post" } } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "format": "binary", + }, + "fileb": { + "title": "Fileb", + "type": "string", + "format": "binary", + }, + "token": {"title": "Token", "type": "string"}, }, }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file", "fileb", "token"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"}, - "fileb": { - "title": "Fileb", - "type": "string", - "format": "binary", - }, - "token": {"title": "Token", "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_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py index 2d0c38719..2b034f1c9 100644 --- a/tests/test_tutorial/test_response_directly/test_tutorial001.py +++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -40,113 +41,117 @@ def test_path_operation(client: TestClient): 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", - }, + assert response.json() == snapshot( + { + "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"}, }, - }, - "required": True, - }, - "responses": { - "200": { - "content": { - "application/json": {"schema": {}}, - }, - "description": "Successful Response", - }, - "422": { + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError", + "$ref": "#/components/schemas/Item", }, }, }, - "description": "Validation Error", + "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", }, - "summary": "Update Item", }, }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError", + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", }, - "title": "Detail", - "type": "array", }, + "title": "HTTPValidationError", + "type": "object", }, - "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": { + "Item": { + "properties": { + "description": { "anyOf": [ {"type": "string"}, - {"type": "integer"}, + {"type": "null"}, ], + "title": "Description", }, - "title": "Location", - "type": "array", + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": {"title": "Title", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, - "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 index ef8457572..047e82c14 100644 --- a/tests/test_tutorial/test_response_directly/test_tutorial002.py +++ b/tests/test_tutorial/test_response_directly/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -38,28 +39,30 @@ def test_path_operation(client: TestClient): 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": {}, + assert response.json() == snapshot( + { + "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", }, - "description": "Successful Response", }, + "summary": "Get Legacy Data", }, - "summary": "Get Legacy Data", }, }, - }, - } + } + ) 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 index 10692f990..914f53783 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -77,117 +78,125 @@ def test_create_item_only_required(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "title": "Response Read Items Items Get", + assert response.json() == snapshot( + { + "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, + "summary": "Read Items", + "operationId": "read_items_items__get", }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - } - }, - }, - "422": { - "description": "Validation Error", + "post": { + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$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": [], }, }, }, - "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"}] + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, - "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"}, - } + "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_tutorial002.py b/tests/test_tutorial/test_response_model/test_tutorial002.py index 216d4c420..10a4e37cd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial002.py +++ b/tests/test_tutorial/test_response_model/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -38,92 +39,100 @@ 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", + 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/UserIn" + } + } + }, + }, + "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, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + } + } + }, + "components": { + "schemas": { + "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"}], }, }, }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "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"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 35ed5572d..a1477b7df 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -126,6 +126,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", 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 fa1eb6277..a60a14ae8 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -139,6 +139,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", 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 b7507b711..460c88251 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_02.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.response_model.tutorial003_02_py39 import app @@ -20,74 +21,80 @@ def test_get_redirect(): 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": { - "/portal": { - "get": { - "summary": "Get Portal", - "operationId": "get_portal_portal_get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Teleport", - "type": "boolean", - "default": False, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - "name": "teleport", - "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" + } } - } + }, }, }, + } + } + }, + "components": { + "schemas": { + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "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_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py index ea3c733b2..c83d78c4f 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_03.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.response_model.tutorial003_03_py39 import app @@ -14,21 +15,23 @@ def test_get_portal(): 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": { - "/teleport": { - "get": { - "summary": "Get Teleport", - "operationId": "get_teleport_teleport_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"}, + "paths": { + "/teleport": { + "get": { + "summary": "Get Teleport", + "operationId": "get_teleport_teleport_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) 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 19a7c601b..187b6491f 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -35,74 +36,80 @@ def test_get_redirect(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": { - "/portal": { - "get": { - "summary": "Get Portal", - "operationId": "get_portal_portal_get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Teleport", - "type": "boolean", - "default": False, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, - "name": "teleport", - "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" + } } - } + }, }, }, + } + } + }, + "components": { + "schemas": { + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, }, } - } - }, - "components": { - "schemas": { - "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_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 9c0d95ebd..d40bce261 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -117,6 +117,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 63e8535db..55b2334d4 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 08ab65952..5d6f542b5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", 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 index ddf55a045..7199ceed6 100644 --- a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -27,70 +28,76 @@ def test_create_item(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/": { - "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" - } - } + assert response.json() == snapshot( + { + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, } - } - }, - "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 82f69fd46..f19346a3a 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -36,104 +37,109 @@ def test_post_body_example(client: TestClient): 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"} + assert response.json() == snapshot( + { + "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"}, } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { + "required": True, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$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" + } + } + }, + }, + }, + } + } + }, + "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", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "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", - "examples": [ - { - "description": "A very nice Item", - "name": "Foo", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "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_tutorial002.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py index 4f5240860..8313cb14f 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -36,106 +37,111 @@ def test_post_body_example(client: TestClient): 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"} + assert response.json() == snapshot( + { + "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"}, } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { + "required": True, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$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" + } + } + }, + }, + }, + } + } + }, + "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", + "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", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "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", - "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", - }, - "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_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py index 3529a9bf0..3c45a64bc 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -38,106 +39,111 @@ def test_post_body_example(client: TestClient): 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, - } - ], - }, + assert response.json() == snapshot( + { + "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"}, } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { + "required": True, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "$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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "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 9326e0629..4b8e15fa1 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -38,110 +39,116 @@ def test_post_body_example(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": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "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", - }, - ], - } + assert response.json() == snapshot( + { + "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": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$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, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "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"}], + }, + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "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"}, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": {"title": "Price", "type": "number"}, - "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"}, - }, - }, - } - }, - } + }, + } + ) 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 2d0dee48c..7f8e36df2 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -38,119 +39,125 @@ def test_post_body_example(client: TestClient): def test_openapi_schema(client: TestClient) -> None: 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": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { + assert response.json() == snapshot( + { + "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": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "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", + }, + }, + }, + } + }, "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "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", - }, - }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "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"}], + }, + }, + }, + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } - } - }, - "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"}, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": {"title": "Price", "type": "number"}, - "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"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index cdaa50b19..5742b51bf 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -41,30 +42,32 @@ def test_incorrect_token(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/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial002.py b/tests/test_tutorial/test_security/test_tutorial002.py index 85c076b1d..1e8c6e018 100644 --- a/tests/test_tutorial/test_security/test_tutorial002.py +++ b/tests/test_tutorial/test_security/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,30 +43,32 @@ def test_token(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": { - "/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"}}, + assert response.json() == snapshot( + { + "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 6a786348c..acd910c71 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -86,121 +87,131 @@ def test_inactive_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": { - "/token": { - "post": { - "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": { + "/token": { + "post": { + "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": "Login", + "operationId": "login_token_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_token_post" } } }, + "required": True, }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, } }, - "required": True, - }, - } - }, - "/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": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_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", - }, - }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_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", }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, }, - "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"}}, + } }, }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py index b5e3d39ef..e842c92a1 100644 --- a/tests/test_tutorial/test_security/test_tutorial004.py +++ b/tests/test_tutorial/test_security/test_tutorial004.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -188,176 +189,186 @@ 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": []}], + } + }, + "/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": []}], + } + }, }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], }, - } - }, - "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"}] + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "disabled": { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], }, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error 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": { + "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, }, }, - "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", + } + }, + } }, }, - "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 25b47f0ad..76b08860f 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -379,6 +379,8 @@ def test_openapi_schema(mod: ModuleType): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index a4b3104bb..1c96f62ae 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -3,6 +3,7 @@ from base64 import b64encode import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -53,25 +54,27 @@ def test_security_http_basic_non_basic_credentials(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": { - "/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": []}], + assert response.json() == snapshot( + { + "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"}} - }, - } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial007.py b/tests/test_tutorial/test_security/test_tutorial007.py index 28b70a2d4..24667422f 100644 --- a/tests/test_tutorial/test_security/test_tutorial007.py +++ b/tests/test_tutorial/test_security/test_tutorial007.py @@ -3,6 +3,7 @@ from base64 import b64encode import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture( @@ -65,25 +66,27 @@ def test_security_http_basic_invalid_password(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": { - "/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": []}], + assert response.json() == snapshot( + { + "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"}} - }, - } + }, + "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 275b23487..0be3b59f9 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -41,100 +42,108 @@ def test_read_items(client: TestClient) -> None: def test_openapi_schema(client: TestClient) -> None: 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": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", + assert response.json() == snapshot( + { + "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": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Read Items Items Get", + } } - } - }, - } - }, - }, - "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": {}}}, - }, - "422": { - "description": "Validation Error", + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - }, - } - }, - "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", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + } + }, + "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", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index 8230e3922..87c63f69e 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -2,6 +2,7 @@ import importlib import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -41,100 +42,108 @@ def test_read_items(client: TestClient) -> None: def test_openapi_schema(client: TestClient) -> None: 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": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", + assert response.json() == snapshot( + { + "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": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Read Items Items Get", + } } - } - }, - } - }, - }, - "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": {}}}, - }, - "422": { - "description": "Validation Error", + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, + "required": True, }, - }, - }, - } - }, - "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", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + } + }, + "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", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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_settings/test_app01.py b/tests/test_tutorial/test_settings/test_app01.py index 0c5e440f1..52b9b1e62 100644 --- a/tests/test_tutorial/test_settings/test_app01.py +++ b/tests/test_tutorial/test_settings/test_app01.py @@ -4,6 +4,7 @@ import sys import pytest from dirty_equals import IsAnyStr from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import ValidationError from pytest import MonkeyPatch @@ -58,21 +59,23 @@ def test_app(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": { - "/info": { - "get": { - "operationId": "info_info_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Info", + assert response.json() == snapshot( + { + "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_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index 2c628f525..aec20e42e 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -335,6 +335,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index c72c16e9a..4ea7d5f64 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -416,6 +416,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_static_files/test_tutorial001.py b/tests/test_tutorial/test_static_files/test_tutorial001.py index 4fbf19ae8..dd93fee79 100644 --- a/tests/test_tutorial/test_static_files/test_tutorial001.py +++ b/tests/test_tutorial/test_static_files/test_tutorial001.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot @pytest.fixture(scope="module") @@ -33,8 +34,10 @@ def test_static_files_not_found(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": {}, - } + assert response.json() == snapshot( + { + "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 ef1f80164..a8fb3dc36 100644 --- a/tests/test_tutorial/test_sub_applications/test_tutorial001.py +++ b/tests/test_tutorial/test_sub_applications/test_tutorial001.py @@ -1,53 +1,10 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.sub_applications.tutorial001_py39 import app client = TestClient(app) -openapi_schema_main = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Main", - "operationId": "read_main_app_get", - } - } - }, -} -openapi_schema_sub = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/sub": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Sub", - "operationId": "read_sub_sub_get", - } - } - }, - "servers": [{"url": "/subapi"}], -} - - -def test_openapi_schema_main(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema_main - def test_main(): response = client.get("/app") @@ -55,13 +12,58 @@ def test_main(): assert response.json() == {"message": "Hello World from main app"} -def test_openapi_schema_sub(): - response = client.get("/subapi/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema_sub - - def test_sub(): response = client.get("/subapi/sub") assert response.status_code == 200, response.text assert response.json() == {"message": "Hello World from sub API"} + + +def test_openapi_schema_main(): + 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": { + "/app": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main", + "operationId": "read_main_app_get", + } + } + }, + } + ) + + +def test_openapi_schema_sub(): + response = client.get("/subapi/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": { + "/sub": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Sub", + "operationId": "read_sub_sub_get", + } + } + }, + "servers": [{"url": "/subapi"}], + } + ) diff --git a/tests/test_tutorial/test_testing/test_main_a.py b/tests/test_tutorial/test_testing/test_main_a.py index 9b3c796bd..528488805 100644 --- a/tests/test_tutorial/test_testing/test_main_a.py +++ b/tests/test_tutorial/test_testing/test_main_a.py @@ -1,3 +1,5 @@ +from inline_snapshot import snapshot + from docs_src.app_testing.app_a_py39.test_main import client, test_read_main @@ -8,21 +10,23 @@ def test_main(): 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": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Main", - "operationId": "read_main__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main", + "operationId": "read_main__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index 5f6533306..2b501d36a 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -1,3 +1,5 @@ +from inline_snapshot import snapshot + from docs_src.app_testing.tutorial001_py39 import client, test_read_main @@ -8,21 +10,23 @@ def test_main(): 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": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Main", - "operationId": "read_main__get", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main", + "operationId": "read_main__get", + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py index 33e661b16..cb8ae8b05 100644 --- a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py +++ b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py @@ -1,4 +1,5 @@ from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.using_request_directly.tutorial001_py39 import app @@ -14,99 +15,103 @@ def test_path_operation(): 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", + assert response.json() == snapshot( + { + "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": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", "type": "string", }, }, - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {}, - }, - }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError", - }, - }, - }, - "description": "Validation Error", - }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", }, - "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_union_body.py b/tests/test_union_body.py index ee7fcc423..e333e2499 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -2,6 +2,7 @@ from typing import Optional, Union from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -38,92 +39,98 @@ def test_post_item(): 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/": { - "post": { - "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/": { + "post": { + "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": "Save Union Body", + "operationId": "save_union_body_items__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Item", + "anyOf": [ + {"$ref": "#/components/schemas/OtherItem"}, + {"$ref": "#/components/schemas/Item"}, + ], } } }, + "required": True, }, + } + } + }, + "components": { + "schemas": { + "OtherItem": { + "title": "OtherItem", + "required": ["price"], + "type": "object", + "properties": {"price": {"title": "Price", "type": "integer"}}, }, - "summary": "Save Union Body", - "operationId": "save_union_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "anyOf": [ - {"$ref": "#/components/schemas/OtherItem"}, - {"$ref": "#/components/schemas/Item"}, - ], - } + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "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"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "OtherItem": { - "title": "OtherItem", - "required": ["price"], - "type": "object", - "properties": {"price": {"title": "Price", "type": "integer"}}, - }, - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": { - "title": "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_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 6c31649bc..4afe7be4b 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -154,6 +154,8 @@ def test_discriminator_pydantic_v2() -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py index 42a6aed24..6644d106c 100644 --- a/tests/test_union_body_discriminator_annotated.py +++ b/tests/test_union_body_discriminator_annotated.py @@ -181,6 +181,8 @@ def test_openapi_schema(client: TestClient) -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_union_forms.py b/tests/test_union_forms.py index 018949f0c..f6c2658f9 100644 --- a/tests/test_union_forms.py +++ b/tests/test_union_forms.py @@ -2,6 +2,7 @@ from typing import Annotated, Union from fastapi import FastAPI, Form from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -61,95 +62,102 @@ def test_empty_form(): 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": { - "/form-union/": { - "post": { - "summary": "Post Union Form", - "operationId": "post_union_form_form_union__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "anyOf": [ - {"$ref": "#/components/schemas/UserForm"}, - {"$ref": "#/components/schemas/CompanyForm"}, - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/form-union/": { + "post": { + "summary": "Post Union Form", + "operationId": "post_union_form_form_union__post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "anyOf": [ + {"$ref": "#/components/schemas/UserForm"}, + { + "$ref": "#/components/schemas/CompanyForm" + }, + ], + "title": "Data", } } }, + "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": { + "CompanyForm": { + "properties": { + "company_name": {"type": "string", "title": "Company Name"}, + "industry": {"type": "string", "title": "Industry"}, + }, + "type": "object", + "required": ["company_name", "industry"], + "title": "CompanyForm", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "UserForm": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "email": {"type": "string", "title": "Email"}, + }, + "type": "object", + "required": ["name", "email"], + "title": "UserForm", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", }, } - } - }, - "components": { - "schemas": { - "CompanyForm": { - "properties": { - "company_name": {"type": "string", "title": "Company Name"}, - "industry": {"type": "string", "title": "Industry"}, - }, - "type": "object", - "required": ["company_name", "industry"], - "title": "CompanyForm", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "UserForm": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "email": {"type": "string", "title": "Email"}, - }, - "type": "object", - "required": ["name", "email"], - "title": "UserForm", - }, - "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_inherited_body.py b/tests/test_union_inherited_body.py index 3c062e7f5..5378880a4 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -2,6 +2,7 @@ from typing import Optional, Union from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -38,98 +39,106 @@ def test_post_item(): 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/": { - "post": { - "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/": { + "post": { + "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": "Save Union Different Body", + "operationId": "save_union_different_body_items__post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Item", + "anyOf": [ + { + "$ref": "#/components/schemas/ExtendedItem" + }, + {"$ref": "#/components/schemas/Item"}, + ], } } }, + "required": True, }, - }, - "summary": "Save Union Different Body", - "operationId": "save_union_different_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "anyOf": [ - {"$ref": "#/components/schemas/ExtendedItem"}, - {"$ref": "#/components/schemas/Item"}, - ], - } + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + }, + }, + "ExtendedItem": { + "title": "ExtendedItem", + "required": ["age"], + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - }, - }, - "ExtendedItem": { - "title": "ExtendedItem", - "required": ["age"], - "type": "object", - "properties": { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py index 982ae1e21..267e450d1 100644 --- a/tests/test_webhooks_security.py +++ b/tests/test_webhooks_security.py @@ -4,6 +4,7 @@ from typing import Annotated from fastapi import FastAPI, Security from fastapi.security import HTTPBearer from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel app = FastAPI() @@ -38,89 +39,96 @@ def test_dummy_webhook(): def test_openapi_schema(): 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": {}, - "webhooks": { - "new-subscription": { - "post": { - "summary": "New Subscription", - "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", - "operationId": "new_subscriptionnew_subscription_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Subscription"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Subscription" } } }, + "required": True, }, - }, - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Subscription": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "monthly_fee": {"type": "number", "title": "Monthly Fee"}, - "start_date": { - "type": "string", - "format": "date-time", - "title": "Start Date", - }, - }, - "type": "object", - "required": ["username", "monthly_fee", "start_date"], - "title": "Subscription", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, + "security": [{"HTTPBearer": []}], + } + } }, - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}, - }, - } + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "monthly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "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", + }, + }, + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}, + }, + } + ) diff --git a/uv.lock b/uv.lock index 931a27021..736b0e145 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version >= '3.10' and python_full_version < '3.14'", - "python_full_version < '3.10'", + "python_full_version < '3.14'", ] [[package]] @@ -31,6 +30,148 @@ 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 = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -51,7 +192,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.75.0" +version = "0.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -63,9 +204,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" }, ] [[package]] @@ -84,8 +225,7 @@ wheels = [ [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'" }, + { name = "trio" }, ] [[package]] @@ -97,31 +237,12 @@ 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'" }, + { name = "argon2-cffi-bindings" }, ] 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 = [ @@ -173,6 +294,15 @@ 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 = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -183,12 +313,33 @@ wheels = [ ] [[package]] -name = "babel" -version = "2.17.0" +name = "authlib" +version = "1.6.7" 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" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] @@ -205,44 +356,57 @@ wheels = [ { 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 = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "black" -version = "25.1.0" +version = "26.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 = "click" }, { 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 = "platformdirs" }, + { name = "pytokens" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, + { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] @@ -256,31 +420,39 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.24" +version = "1.42.43" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/47/29afb754de7df0a0ebceaa9d83e209136ef7b62744259a6c09862fef4765/boto3-1.42.43.tar.gz", hash = "sha256:01fc5501209b23849fb30b01c6c086583ac91c40842a76083662fbfb84a82491", size = 112844, upload-time = "2026-02-05T20:31:44.974Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/80/92/584447b14ae70f57f133a4bc64393902a72a3087486a7c09ce1bab25263c/boto3-1.42.43-py3-none-any.whl", hash = "sha256:44ddcaa37c350333c5a4799f533e786a595a97f1ee2fd7fc3e394cdebeb15e44", size = 140603, upload-time = "2026-02-05T20:31:43.698Z" }, ] [[package]] name = "botocore" -version = "1.42.24" +version = "1.42.43" 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'" }, + { name = "urllib3" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/af/d6/def916ad1d13de5d511074afcde538a958e2e8a7c7020fb698d1f392f63b/botocore-1.42.43.tar.gz", hash = "sha256:41d04ead0b0862eec21f841811fb5764fe370a2df9b319e0d5297325c50fba1b", size = 14934077, upload-time = "2026-02-05T20:31:35.15Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/95656f91b795eb47b73a00d36c51c7a5729eafa632c7348caa068ff63e50/botocore-1.42.43-py3-none-any.whl", hash = "sha256:1c0e30f62e274978ac3bcab253e3a859febea634b72b5e343589db7d17f83cd6", size = 14610179, upload-time = "2026-02-05T20:31:32.727Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" }, ] [[package]] @@ -304,8 +476,7 @@ dependencies = [ { 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'" }, + { name = "tinycss2" }, ] 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 = [ @@ -401,18 +572,6 @@ wheels = [ { 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]] @@ -501,59 +660,33 @@ wheels = [ { 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'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "cohere" -version = "5.20.1" +version = "5.20.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastavro" }, @@ -562,13 +695,12 @@ dependencies = [ { 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 = "types-requests" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/35/37/65af9c50b5d5772a5528c6a949799f98ae286b8ebb924e0fac0619b3ae88/cohere-5.20.4.tar.gz", hash = "sha256:3b3017048ff5d5b4f113180947d538ca3d0f274de5875f0345be4c8cb3d5119a", size = 180737, upload-time = "2026-02-05T14:47:54.639Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/6d/14/5c5077c6b01aed7a18dfc5ab775a35c7a6cb118e5bc1dafcfc06abdb9d9e/cohere-5.20.4-py3-none-any.whl", hash = "sha256:9cc6ebb0adac3d9f96ac0efffde6a2484534fb0aec3684a62c250d49da958f29", size = 318987, upload-time = "2026-02-05T14:47:53.505Z" }, ] [[package]] @@ -582,306 +714,191 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.7" +version = "7.13.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, + { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, + { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, + { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] -name = "coverage" -version = "7.13.1" +name = "croniter" +version = "6.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 = "python-dateutil" }, + { name = "pytz" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } 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'" }, + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, ] [[package]] name = "cross-web" -version = "0.4.0" +version = "0.4.1" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/58/e688e99d1493c565d1587e64b499268d0a3129ae59f4efe440aac395f803/cross_web-0.4.1.tar.gz", hash = "sha256:0466295028dcae98c9ab3d18757f90b0e74fac2ff90efbe87e74657546d9993d", size = 157385, upload-time = "2026-01-09T18:17:41.534Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/67/49/92b46b6e65f09b717a66c4e5a9bc47a45ebc83dd0e0ed126f8258363479d/cross_web-0.4.1-py3-none-any.whl", hash = "sha256:41b07c3a38253c517ec0603c1a366353aff77538946092b0f9a2235033f192c2", size = 14320, upload-time = "2026-01-09T18:17:40.325Z" }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.4" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] @@ -889,8 +906,7 @@ 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 = "tinycss2" }, { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } @@ -907,6 +923,23 @@ 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 = "cyclopts" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -918,11 +951,20 @@ wheels = [ [[package]] name = "dirty-equals" -version = "0.9.0" +version = "0.11" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] @@ -934,26 +976,10 @@ 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" }, @@ -968,13 +994,21 @@ 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 = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[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 = "dnspython" }, { 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" } @@ -1012,15 +1046,34 @@ 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 = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[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 = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] [package.optional-dependencies] @@ -1032,14 +1085,11 @@ all = [ { 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 = "pydantic-settings" }, + { name = "python-multipart" }, { 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'" }, + { name = "uvicorn", extra = ["standard"] }, ] standard = [ { name = "email-validator" }, @@ -1047,12 +1097,9 @@ 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'" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, ] standard-no-fastapi-cloud-cli = [ { name = "email-validator" }, @@ -1060,12 +1107,9 @@ 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'" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.dev-dependencies] @@ -1074,8 +1118,7 @@ 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 = "coverage", extra = ["toml"] }, { name = "dirty-equals" }, { name = "flask" }, { name = "gitpython" }, @@ -1094,8 +1137,7 @@ dev = [ { 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 = "pwdlib", extra = ["argon2"] }, { name = "pydantic-ai" }, { name = "pygithub" }, { name = "pyjwt" }, @@ -1105,8 +1147,7 @@ dev = [ { 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 = "strawberry-graphql" }, { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, @@ -1137,8 +1178,7 @@ docs-tests = [ 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 = "pydantic-settings" }, { name = "pygithub" }, { name = "pyyaml" }, { name = "smokeshow" }, @@ -1146,23 +1186,20 @@ github-actions = [ tests = [ { name = "a2wsgi" }, { 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 = "coverage", extra = ["toml"] }, { 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 = "pwdlib", extra = ["argon2"] }, { 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 = "strawberry-graphql" }, { name = "types-orjson" }, { name = "types-ujson" }, ] @@ -1188,7 +1225,7 @@ requires-dist = [ { 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 = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" }, { 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" }, @@ -1200,9 +1237,10 @@ requires-dist = [ { 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 = "starlette", specifier = ">=0.40.0,<1.0.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 = "typing-inspection", specifier = ">=0.4.2" }, + { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.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" }, @@ -1213,64 +1251,64 @@ provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"] dev = [ { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, - { name = "black", specifier = "==25.1.0" }, - { name = "cairosvg", specifier = "==2.8.2" }, + { 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.46" }, - { name = "griffe-typingdoc", specifier = "==0.3.0" }, - { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "dirty-equals", specifier = ">=0.9.0" }, + { name = "flask", specifier = ">=3.0.0,<4.0.0" }, + { name = "gitpython", specifier = ">=3.1.46" }, + { 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 = "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.5.0" }, - { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { 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 = "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 = "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 = "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 = "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.14" }, - { name = "sqlmodel", specifier = "==0.0.31" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "sqlmodel", specifier = ">=0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, - { name = "typer", specifier = "==0.21.1" }, - { name = "types-orjson", specifier = "==3.6.2" }, - { name = "types-ujson", specifier = "==5.10.0.20240515" }, + { name = "typer", specifier = ">=0.21.1" }, + { 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 = "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 = "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.5.0" }, - { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { 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 = "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.14" }, - { name = "typer", specifier = "==0.21.1" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "typer", specifier = ">=0.21.1" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, + { name = "ruff", specifier = ">=0.14.14" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1284,26 +1322,26 @@ tests = [ { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { 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 = "dirty-equals", specifier = ">=0.9.0" }, + { name = "flask", specifier = ">=3.0.0,<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 = "mypy", specifier = ">=1.14.1" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, - { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pyjwt", specifier = ">=2.9.0" }, { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, - { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, - { name = "sqlmodel", specifier = "==0.0.31" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "sqlmodel", specifier = ">=0.0.31" }, { 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" }, + { name = "types-orjson", specifier = ">=3.6.2" }, + { name = "types-ujson", specifier = ">=5.10.0.20240515" }, ] translations = [ - { name = "gitpython", specifier = "==3.1.46" }, - { name = "pydantic-ai", specifier = "==0.4.10" }, - { name = "pygithub", specifier = "==2.8.1" }, + { name = "gitpython", specifier = ">=3.1.46" }, + { name = "pydantic-ai", specifier = ">=0.4.10" }, + { name = "pygithub", specifier = ">=2.8.1" }, ] [[package]] @@ -1314,8 +1352,7 @@ 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'" }, + { name = "uvicorn", extra = ["standard"] }, ] 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 = [ @@ -1325,17 +1362,15 @@ wheels = [ [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'" }, + { name = "uvicorn", extra = ["standard"] }, ] 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'" }, + { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cloud-cli" -version = "0.8.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -1345,12 +1380,11 @@ dependencies = [ { 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'" }, + { name = "uvicorn", extra = ["standard"] }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/11/15/6c3d85d63964340fde6f36cc80f3f365d35f371e6a918d68ff3a3d588ef2/fastapi_cloud_cli-0.11.0.tar.gz", hash = "sha256:ecc83a5db106be35af528eccb01aa9bced1d29783efd48c8c1c831cf111eea99", size = 36170, upload-time = "2026-01-15T09:51:33.681Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/1a/07/60f79270a3320780be7e2ae8a1740cb98a692920b569ba420b97bcc6e175/fastapi_cloud_cli-0.11.0-py3-none-any.whl", hash = "sha256:76857b0f09d918acfcb50ade34682ba3b2079ca0c43fda10215de301f185a7f8", size = 26884, upload-time = "2026-01-15T09:51:34.471Z" }, ] [[package]] @@ -1448,20 +1482,6 @@ wheels = [ { 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" }, @@ -1486,18 +1506,6 @@ wheels = [ { 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]] @@ -1545,37 +1553,44 @@ wheels = [ { 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 = "fastmcp" +version = "2.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/32/982678d44f13849530a74ab101ed80e060c2ee6cf87471f062dcf61705fd/fastmcp-2.14.5.tar.gz", hash = "sha256:38944dc582c541d55357082bda2241cedb42cd3a78faea8a9d6a2662c62a42d7", size = 8296329, upload-time = "2026-02-03T15:35:21.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c1/1a35ec68ff76ea8443aa115b18bcdee748a4ada2124537ee90522899ff9f/fastmcp-2.14.5-py3-none-any.whl", hash = "sha256:d81e8ec813f5089d3624bec93944beaefa86c0c3a4ef1111cbef676a761ebccf", size = 417784, upload-time = "2026-02-03T15:35:18.489Z" }, ] [[package]] name = "filelock" -version = "3.19.1" +version = "3.20.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -1584,9 +1599,7 @@ 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 = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, @@ -1598,28 +1611,146 @@ wheels = [ ] [[package]] -name = "fsspec" -version = "2025.10.0" +name = "frozenlist" +version = "1.8.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" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" -version = "2025.12.0" +version = "2026.2.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" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/87/bdc11c1671e3a3fe701c3c4aaae4aa2bb7a84a6bb1812dfb5693c87d3872/genai_prices-0.0.52.tar.gz", hash = "sha256:0df7420b555fa3a48d09e5c7802ba35b5dfa9fd49b0c3bb2c150c59060d83f52", size = 58364, upload-time = "2026-01-28T12:07:49.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/33/6316b4907a0bffc1bcc99074c7e2d01184fdfeee401c864146a40d55ad10/genai_prices-0.0.52-py3-none-any.whl", hash = "sha256:639e7a2ae7eddf5710febb9779b9c9e31ff5acf464b4eb1f6018798ea642e6d3", size = 60937, upload-time = "2026-01-28T12:07:47.921Z" }, ] [[package]] @@ -1652,7 +1783,6 @@ version = "3.1.46" 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/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ @@ -1661,76 +1791,60 @@ wheels = [ [[package]] name = "google-auth" -version = "2.47.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cryptography" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, ] [package.optional-dependencies] requests = [ - { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "requests" }, ] [[package]] name = "google-genai" -version = "1.47.0" +version = "1.62.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'" }, + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/94/4c/71b32b5c8db420cf2fd0d5ef8a672adbde97d85e5d44a0b4fca712264ef1/google_genai-1.62.0.tar.gz", hash = "sha256:709468a14c739a080bc240a4f3191df597bf64485b1ca3728e0fb67517774c18", size = 490888, upload-time = "2026-02-04T22:48:41.989Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, ] [[package]] -name = "google-genai" -version = "1.57.0" +name = "googleapis-common-protos" +version = "1.72.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'" }, + { name = "protobuf" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [[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" }, @@ -1738,164 +1852,70 @@ wheels = [ [[package]] name = "greenlet" -version = "3.2.4" +version = "3.3.1" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, ] [[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'" }, + { name = "colorama" }, ] 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 = [ @@ -1907,8 +1927,7 @@ 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 = "griffe" }, { 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" } @@ -1921,8 +1940,7 @@ 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'" }, + { name = "griffe" }, ] 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 = [ @@ -1946,6 +1964,67 @@ 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 = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -2047,13 +2126,6 @@ wheels = [ { 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]] @@ -2082,25 +2154,26 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.3.0" +version = "0.36.2" 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 = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "shellingham" }, + { name = "requests" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp" }, ] [[package]] @@ -2124,26 +2197,10 @@ 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" }, @@ -2183,6 +2240,51 @@ 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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jieba" version = "0.42.1" @@ -2203,120 +2305,117 @@ wheels = [ [[package]] name = "jiter" -version = "0.12.0" +version = "0.13.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" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, ] [[package]] @@ -2324,22 +2423,37 @@ 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'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, ] 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-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, ] 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 = [ @@ -2347,52 +2461,210 @@ wheels = [ ] [[package]] -name = "lia-web" -version = "0.3.1" +name = "keyring" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cross-web", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, +] + +[[package]] +name = "logfire" +version = "4.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/29/4386b331b8aecad429c043bbd1b7684cdb56ff9e11a5bd96845483dc0968/logfire-4.22.0.tar.gz", hash = "sha256:284b3b955c7515d4428dbc1e04784c3e652e62acf7597bd64a0aa9ecb6a7dedd", size = 654771, upload-time = "2026-02-04T12:17:57.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/ce/24a598c271d9631b09fb42f86f1e6d63b50647ecf0ce438d368a1e1af66b/logfire-4.22.0-py3-none-any.whl", hash = "sha256:9a0d20885613efe4bc9efcfa19a7943a6c642bc21339d926c51fcc74c0d083d6", size = 242637, upload-time = "2026-02-04T12:17:54.787Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx" }, ] [[package]] name = "logfire-api" -version = "4.17.0" +version = "4.22.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" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c2/38871722347486f59f48dbe407541ff1e02398637b800778a2602f2353a7/logfire_api-4.22.0.tar.gz", hash = "sha256:b22a038e76c58cba1ab2ce7ce1a26aa5f3df8c63e84d6ad01fb22f75273ca830", size = 59723, upload-time = "2026-02-04T12:17:59.07Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/1e/93/4e8395f3e4b1951b1de8d06cb7d7ef1105efd294ea3443bf39b3594390af/logfire_api-4.22.0-py3-none-any.whl", hash = "sha256:9fd2a2dac9cc3adf71ad4dac7bc9d26af326a5aa87445be90026048d22b92a8e", size = 98543, upload-time = "2026-02-04T12:17:56.365Z" }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, ] [[package]] name = "markdown" -version = "3.9" +version = "3.10.1" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] @@ -2400,39 +2672,19 @@ 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'" }, + { name = "markdown" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/47/ec9eae4a6d2f336d95681df43720e2b25b045dc3ed44ae6d30a5ce2f5dff/markdown_include_variants-0.0.8.tar.gz", hash = "sha256:46d812340c64dcd3646b1eaa356bafb31626dd7b4955d15c44ff8c48c6357227", size = 46882, upload-time = "2025-12-12T16:11:04.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/0e/130958e7ec50d13f2ee7b6c142df5c352520a9251baf1652e41262703857/markdown_include_variants-0.0.8-py3-none-any.whl", hash = "sha256:425a300ae25fbcd598506cba67859a9dfa047333e869e0ff2e11a5e354b326dc", size = 8120, upload-time = "2025-12-12T16:11:02.881Z" }, ] -[[package]] -name = "markdown-it-py" -version = "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'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -2522,39 +2774,31 @@ wheels = [ { 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" +version = "1.26.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'" }, + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "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" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -2572,8 +2816,7 @@ 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 = "markdown" }, { name = "rcslice" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051, upload-time = "2022-07-26T05:46:14.129Z" } @@ -2613,14 +2856,11 @@ 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 = "click" }, { 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 = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, @@ -2640,8 +2880,7 @@ 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 = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] @@ -2655,10 +2894,8 @@ 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 = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } @@ -2680,8 +2917,7 @@ dependencies = [ { 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'" }, + { name = "termcolor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" } wheels = [ @@ -2690,15 +2926,14 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" 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 = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, @@ -2706,9 +2941,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -2734,60 +2969,35 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.30.1" +version = "1.0.2" 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 = "markdown" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/63/4d/1ca8a9432579184599714aaeb36591414cc3d3bfd9d494f6db540c995ae4/mkdocstrings-1.0.2.tar.gz", hash = "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8", size = 101048, upload-time = "2026-01-24T15:57:25.735Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/57/32/407a9a5fdd7d8ecb4af8d830b9bcdf47ea68f916869b3f44bac31f081250/mkdocstrings-1.0.2-py3-none-any.whl", hash = "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817", size = 35443, upload-time = "2026-01-24T15:57:23.933Z" }, ] [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" }, + { name = "mkdocstrings-python" }, ] [[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.*'" }, + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] 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 = [ @@ -2795,47 +3005,196 @@ wheels = [ ] [[package]] -name = "mypy" -version = "1.14.1" +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, + { name = "pathspec" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -2847,9 +3206,21 @@ 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 = "nexus-rpc" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142, upload-time = "2025-11-17T19:17:06.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, +] + [[package]] name = "openai" -version = "2.14.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2861,9 +3232,21 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] [[package]] @@ -2880,97 +3263,193 @@ wheels = [ ] [[package]] -name = "orjson" -version = "3.11.5" +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" 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" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] [[package]] @@ -3004,147 +3483,134 @@ wheels = [ ] [[package]] -name = "pathspec" -version = "1.0.2" +name = "pathable" +version = "0.4.4" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, ] [[package]] name = "pillow" -version = "11.3.0" +version = "12.1.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" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, ] [[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" }, @@ -3152,22 +3618,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.57.0" +version = "1.58.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 = "greenlet" }, { 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" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" }, ] [[package]] @@ -3181,28 +3646,35 @@ wheels = [ [[package]] name = "prek" -version = "0.2.22" +version = "0.3.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" }, + { url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" }, + { url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" }, + { url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" }, + { url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/73/42/1bb4bba3ff47897df11e9dfd774027cdfa135482c961a54e079af0faf45a/prek-0.3.2-py3-none-win32.whl", hash = "sha256:58c806bd1344becd480ef5a5ba348846cc000af0e1fbe854fef91181a2e06461", size = 4267619, upload-time = "2026-02-06T13:49:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/97/11/6665f47a7c350d83de17403c90bbf7a762ef50876ece456a86f64f46fbfb/prek-0.3.2-py3-none-win_amd64.whl", hash = "sha256:70114b48e9eb8048b2c11b4c7715ce618529c6af71acc84dd8877871a2ef71a6", size = 4624324, upload-time = "2026-02-06T13:49:45.922Z" }, + { url = "https://files.pythonhosted.org/packages/22/e7/740997ca82574d03426f897fd88afe3fc8a7306b8c7ea342a8bc1c538488/prek-0.3.2-py3-none-win_arm64.whl", hash = "sha256:9144d176d0daa2469a25c303ef6f6fa95a8df015eb275232f5cb53551ecefef0", size = 4336008, upload-time = "2026-02-06T13:49:52.27Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -3218,30 +3690,138 @@ wheels = [ ] [[package]] -name = "pwdlib" -version = "0.2.1" +name = "propcache" +version = "0.4.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" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[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 = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[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" }, @@ -3249,7 +3829,48 @@ wheels = [ [package.optional-dependencies] argon2 = [ - { name = "argon2-cffi", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "argon2-cffi" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, ] [[package]] @@ -3275,11 +3896,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -3304,41 +3925,39 @@ email = [ [[package]] name = "pydantic-ai" -version = "0.4.10" +version = "1.56.0" 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"] }, + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "0.4.10" +version = "1.56.0" 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 = "genai-prices" }, + { name = "griffe" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" }, ] [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'" }, + { name = "starlette" }, ] anthropic = [ { name = "anthropic" }, @@ -3349,6 +3968,7 @@ bedrock = [ cli = [ { name = "argcomplete" }, { name = "prompt-toolkit" }, + { name = "pyperclip" }, { name = "rich" }, ] cohere = [ @@ -3357,32 +3977,47 @@ cohere = [ evals = [ { name = "pydantic-evals" }, ] +fastmcp = [ + { name = "fastmcp" }, +] 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'" }, + { name = "google-genai" }, ] groq = [ { name = "groq" }, ] huggingface = [ - { name = "huggingface-hub" }, + { name = "huggingface-hub", extra = ["inference"] }, +] +logfire = [ + { name = "logfire", extra = ["httpx"] }, ] mcp = [ - { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "mcp" }, ] mistral = [ { name = "mistralai" }, ] openai = [ { name = "openai" }, + { name = "tiktoken" }, ] retries = [ { name = "tenacity" }, ] +temporal = [ + { name = "temporalio" }, +] +ui = [ + { name = "starlette" }, +] vertexai = [ { name = "google-auth" }, { name = "requests" }, ] +xai = [ + { name = "xai-sdk" }, +] [[package]] name = "pydantic-core" @@ -3476,19 +4111,6 @@ wheels = [ { 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" }, @@ -3517,20 +4139,19 @@ wheels = [ [[package]] name = "pydantic-evals" -version = "0.4.10" +version = "1.56.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/98/f2/8c59284a2978af3fbda45ae3217218eaf8b071207a9290b54b7613983e5d/pydantic_evals-1.56.0.tar.gz", hash = "sha256:206635107127af6a3ee4b1fc8f77af6afb14683615a2d6b3609f79467c1c0d28", size = 47210, upload-time = "2026-02-06T01:13:25.714Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/89/51/9875d19ff6d584aaeb574aba76b49d931b822546fc60b29c4fc0da98170d/pydantic_evals-1.56.0-py3-none-any.whl", hash = "sha256:d1efb410c97135aabd2a22453b10c981b2b9851985e9354713af67ae0973b7a9", size = 56407, upload-time = "2026-02-06T01:13:17.098Z" }, ] [[package]] @@ -3548,7 +4169,7 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "0.4.10" +version = "1.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3556,46 +4177,49 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" }, ] [[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'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] 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 = "pydocket" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "croniter" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "taskgroup", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/26/ac23ead3725475468b50b486939bf5feda27180050a614a7407344a0af0e/pydocket-0.17.5.tar.gz", hash = "sha256:19a6976d8fd11c1acf62feb0291a339e06beaefa100f73dd38c6499760ad3e62", size = 334829, upload-time = "2026-01-30T18:44:39.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/98/73427d065c067a99de6afbe24df3d90cf20d63152ceb42edff2b6e829d4c/pydocket-0.17.5-py3-none-any.whl", hash = "sha256:544d7c2625a33e52528ac24db25794841427dfc2cf30b9c558ac387c77746241", size = 93355, upload-time = "2026-01-30T18:44:37.972Z" }, +] + [[package]] name = "pyee" version = "13.0.0" @@ -3617,8 +4241,7 @@ dependencies = [ { 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'" }, + { name = "urllib3" }, ] 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 = [ @@ -3636,11 +4259,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.11.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" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] @@ -3650,16 +4273,15 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.20" +version = "10.20.1" 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 = "markdown" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, ] [[package]] @@ -3697,6 +4319,15 @@ wheels = [ { 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 = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -3704,8 +4335,7 @@ 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 = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, @@ -3722,7 +4352,6 @@ 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" }, ] @@ -3740,8 +4369,6 @@ wheels = [ { 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" }, ] @@ -3767,28 +4394,21 @@ wheels = [ ] [[package]] -name = "python-multipart" -version = "0.0.20" +name = "python-json-logger" +version = "4.0.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -3803,6 +4423,54 @@ 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 = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -3823,9 +4491,15 @@ wheels = [ { 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 = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] @@ -3890,15 +4564,6 @@ wheels = [ { 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]] @@ -3923,17 +4588,150 @@ wheels = [ ] [[package]] -name = "referencing" -version = "0.37.0" +name = "redis" +version = "7.1.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'" }, + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, ] [[package]] @@ -3944,8 +4742,7 @@ 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'" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ @@ -3954,31 +4751,42 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.2" 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 = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, ] [[package]] name = "rich-toolkit" -version = "0.17.1" +version = "0.18.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 = "click" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/da/f1/bcfbde3ca38db54b5dcf7ee3d0caf3ed9133a169aec5a58ad9ec50ba12e8/rich_toolkit-0.18.1.tar.gz", hash = "sha256:bf104f1945a7252debeda7d7138118eaf848fff5ea81d9eda556cbc5f911122c", size = 192514, upload-time = "2026-02-01T10:56:31.857Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/da/43/6f9860c4bfb1f181c347941542a8955ce24b228f84550253765aa1854d53/rich_toolkit-0.18.1-py3-none-any.whl", hash = "sha256:04011a9751f4c2becdf44bd1aaff8562d4b00caf04f14e483a9873c15fbe3154", size = 32255, upload-time = "2026-02-01T10:56:33.071Z" }, ] [[package]] @@ -4076,20 +4884,6 @@ wheels = [ { 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" }, @@ -4114,18 +4908,6 @@ wheels = [ { 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]] @@ -4264,28 +5046,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] @@ -4300,18 +5081,30 @@ 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 = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.52.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'" }, + { name = "urllib3" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, ] [[package]] @@ -4374,157 +5167,113 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" 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 = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [[package]] name = "sqlmodel" -version = "0.0.31" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/b8/e7cd6def4a773f25d6e29ffce63ccbfd6cf9488b804ab6fb9b80d334b39d/sqlmodel-0.0.31.tar.gz", hash = "sha256:2d41a8a9ee05e40736e2f9db8ea28cbfe9b5d4e5a18dd139e80605025e0c516c", size = 94952, upload-time = "2025-12-28T12:35:01.436Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/89/67f8964f3b2ed073fa4e95201e708291935d00e3600f36f09c1be3e279fe/sqlmodel-0.0.32.tar.gz", hash = "sha256:48e8fe4c8c3d7d8bf8468db17fa92ca680421e86cfec8b352217ef40736767be", size = 94140, upload-time = "2026-02-01T18:19:14.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/72/5aa5be921800f6418a949a73c9bb7054890881143e6bc604a93d228a95a3/sqlmodel-0.0.31-py3-none-any.whl", hash = "sha256:6d946d56cac4c2db296ba1541357cee2e795d68174e2043cd138b916794b1513", size = 27093, upload-time = "2025-12-28T12:35:00.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/d9b40ed2c570fd612c2abd57e4d9084a9d8eb1797447e2ce897b77b1c4b2/sqlmodel-0.0.32-py3-none-any.whl", hash = "sha256:d62f0702599592046c1a136d3512feab3d5a80e2988642ef0ed2c89b9b8b297b", size = 27416, upload-time = "2026-02-01T18:19:15.992Z" }, ] [[package]] name = "sse-starlette" -version = "3.1.2" +version = "3.2.0" 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'" }, + { name = "anyio" }, + { name = "starlette" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, ] [[package]] name = "starlette" -version = "0.49.3" +version = "0.52.1" 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'" }, + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] name = "strawberry-graphql" -version = "0.283.3" +version = "0.291.2" 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'" }, + { name = "cross-web" }, + { name = "graphql-core" }, + { name = "packaging" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/33/dd/e0e68f4b17da6ff5773fcd4bebf86fc4ff8620c854be816d047e9af8c4aa/strawberry_graphql-0.291.2.tar.gz", hash = "sha256:e6076604a786e8437bc64a27348584c082113442f072daf757b56e4863543a97", size = 217730, upload-time = "2026-02-06T14:40:51.173Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/6f/14/93908a029605955e3411cdb0f2e8cfe170e5331da23357ed71d5c51b15bc/strawberry_graphql-0.291.2-py3-none-any.whl", hash = "sha256:f71d3669086c6747fd4760e6fafe3605d9a33f7d168886e5edd2b61a04972e56", size = 316389, upload-time = "2026-02-06T14:40:53.482Z" }, ] [[package]] @@ -4540,34 +5289,51 @@ wheels = [ ] [[package]] -name = "tenacity" -version = "9.1.2" +name = "taskgroup" +version = "0.2.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" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" }, ] [[package]] -name = "termcolor" -version = "3.1.0" +name = "temporalio" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498, upload-time = "2025-11-25T21:25:20.225Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638, upload-time = "2025-11-25T21:24:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603, upload-time = "2025-11-25T21:25:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016, upload-time = "2025-11-25T21:25:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521, upload-time = "2025-11-25T21:25:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027, upload-time = "2025-11-25T21:25:16.827Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/4a/c3357c8742f361785e3702bb4c9c68c4cb37a80aa657640b820669be5af1/tenacity-9.1.3.tar.gz", hash = "sha256:a6724c947aa717087e2531f883bde5c9188f603f6669a9b8d54eb998e604c12a", size = 49002, upload-time = "2026-02-05T06:33:12.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/cdc85edb15e384d8e934aad89638cc8646e118c80de94c60125d0fc0a185/tenacity-9.1.3-py3-none-any.whl", hash = "sha256:51171cfc6b8a7826551e2f029426b10a6af189c5ac6986adcd7eb36d42f17954", size = 28858, upload-time = "2026-02-05T06:33:11.219Z" }, ] [[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" }, @@ -4583,30 +5349,72 @@ wheels = [ ] [[package]] -name = "tinycss2" -version = "1.4.0" +name = "tiktoken" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "webencodings", marker = "python_full_version < '3.10'" }, + { name = "regex" }, + { name = "requests" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] [[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'" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ @@ -4641,110 +5449,86 @@ wheels = [ { 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" +version = "2.4.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" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[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'" }, + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, ] 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 = [ @@ -4756,8 +5540,7 @@ name = "typer" 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 = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, @@ -4767,20 +5550,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] -[[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" @@ -4791,30 +5560,20 @@ wheels = [ ] [[package]] -name = "types-requests" -version = "2.31.0.6" +name = "types-protobuf" +version = "6.32.1.20251210" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, ] [[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'" }, + { name = "urllib3" }, ] 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 = [ @@ -4823,20 +5582,11 @@ wheels = [ [[package]] name = "types-ujson" -version = "5.10.0.20240515" +version = "5.10.0.20250822" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/bd/d372d44534f84864a96c19a7059d9b4d29db8541828b8b9dc3040f7a46d0/types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006", size = 8437, upload-time = "2025-08-22T03:02:19.433Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d7/f2/d812543c350674d8b3f6e17c8922248ee3bb752c2a76f64beb8c538b40cf/types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080", size = 7657, upload-time = "2025-08-22T03:02:18.699Z" }, ] [[package]] @@ -4932,17 +5682,6 @@ wheels = [ { 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" }, @@ -4951,71 +5690,23 @@ wheels = [ { 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.*'" }, + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] 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 = [ @@ -5024,13 +5715,13 @@ wheels = [ [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'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, ] [[package]] @@ -5075,12 +5766,6 @@ wheels = [ { 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]] @@ -5101,13 +5786,8 @@ wheels = [ { 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" }, @@ -5213,18 +5893,6 @@ wheels = [ { 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" }, @@ -5233,19 +5901,15 @@ wheels = [ { 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" +version = "0.5.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, ] [[package]] @@ -5307,29 +5971,12 @@ wheels = [ { 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" }, ] @@ -5345,6 +5992,220 @@ 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 = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xai-sdk" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/66/1e0163eac090733d0ed0836a0cd3c14f5b59abeaa6fdba71c7b56b1916e4/xai_sdk-1.6.1.tar.gz", hash = "sha256:b55528df188f8c8448484021d735f75b0e7d71719ddeb432c5f187ac67e3c983", size = 388223, upload-time = "2026-01-29T03:13:07.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/98/8b4019b35f2200295c5eec8176da4b779ec3a0fd60eba7196b618f437e1f/xai_sdk-1.6.1-py3-none-any.whl", hash = "sha256:f478dee9bd8839b8d341bd075277d0432aff5cd7120a4284547d25c6c9e7ab3b", size = 240917, upload-time = "2026-01-29T03:13:05.626Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + [[package]] name = "zipp" version = "3.23.0"