mirror of https://github.com/tiangolo/fastapi.git
✨ Add support for Pydantic v2 (#9816)
* ✨ Pydantic v2 migration, initial implementation (#9500) * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 👷 Add CI for both Pydantic v1 and v2 (#9688) * 👷 Test and install Pydantic v1 and v2 in CI * 💚 Tweak CI config for Pydantic v1 and v2 * 💚 Fix Pydantic v2 specification in CI * 🐛 Fix type annotations for compatibility with Python 3.7 * 💚 Install Pydantic v2 for lints * 🐛 Fix type annotations for Pydantic v2 * 💚 Re-use test cache for lint * ♻️ Refactor internals for test coverage and performance (#9691) * ♻️ Tweak import of Annotated from typing_extensions, they are installed anyway * ♻️ Refactor _compat to define functions for Pydantic v1 or v2 once instead of checking inside * ✅ Add test for UploadFile for Pydantic v2 * ♻️ Refactor types and remove logic for impossible cases * ✅ Add missing tests from test refactor for path params * ✅ Add tests for new decimal encoder * 💡 Add TODO comment for decimals in encoders * 🔥 Remove unneeded dummy function * 🔥 Remove section of code in field_annotation_is_scalar covered by sub-call to field_annotation_is_complex * ♻️ Refactor and tweak variables and types in _compat * ✅ Add tests for corner cases and compat with Pydantic v1 and v2 * ♻️ Refactor type annotations * 🔖 Release version 0.100.0-beta1 * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them (#9707) * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them * ♻️ Update JSON Schema for email field without email-validator installed * 🐛 Fix support for Pydantic v2.0, small changes in their final release (#9771) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> * 🔖 Release version 0.100.0-beta2 * ✨ OpenAPI 3.1.0 with Pydantic v2, merge `master` (#9773) * ➕ Add dirty-equals as a testing dependency (#9778) ➕ Add dirty-equals as a testing dependency, it seems it got lsot at some point * 🔀 Merge master, fix valid JSON Schema accepting bools (#9782) * ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on the Pydantic side (#9787) ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on Pydantic side * ♻️ Deprecate parameter `regex`, use `pattern` instead (#9786) * 📝 Update docs to deprecate regex, recommend pattern * ♻️ Update examples to use new pattern instead of regex * 📝 Add new example with deprecated regex * ♻️ Add deprecation notes and warnings for regex * ✅ Add tests for regex deprecation * ✅ Update tests for compatibility with Pydantic v1 * ✨ Update docs to use Pydantic v2 settings and add note and example about v1 (#9788) * ➕ Add pydantic-settings to all extras * 📝 Update docs for Pydantic settings * 📝 Update Settings source examples to use Pydantic v2, and add a Pydantic v1 version * ✅ Add tests for settings with Pydantic v1 and v2 * 🔥 Remove solved TODO comment * ♻️ Update conditional OpenAPI to use new Pydantic v2 settings * ✅ Update tests to import Annotated from typing_extensions for Python < 3.9 (#9795) * ➕ Add pydantic-extra-types to fastapi[extra] * ➕ temp: Install Pydantic from source to test JSON Schema metadata fixes (#9777) * ➕ Install Pydantic from source, from branch for JSON Schema with metadata * ➕ Update dependencies, install Pydantic main * ➕ Fix dependency URL for Pydantic from source * ➕ Add pydantic-settings for test requirements * 💡 Add TODO comments to re-enable Pydantic main (not from source) (#9796) * ✨ Add new Pydantic Field param options to Query, Cookie, Body, etc. (#9797) * 📝 Add docs for Pydantic v2 for `docs/en/docs/advanced/path-operation-advanced-configuration.md` (#9798) * 📝 Update docs in examples for settings with Pydantic v2 (#9799) * 📝 Update JSON Schema `examples` docs with Pydantic v2 (#9800) * ♻️ Use new Pydantic v2 JSON Schema generator (#9813) Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> * ♻️ Tweak type annotations and Pydantic version range (#9801) * 📌 Re-enable GA Pydantic, for v2, require minimum 2.0.2 (#9814) * 🔖 Release version 0.100.0-beta3 * 🔥 Remove duplicate type declaration from merge conflicts (#9832) * 👷♂️ Run tests with Pydantic v2 GA (#9830) 👷 Run tests for Pydantic v2 GA * 📝 Add notes to docs expecting Pydantic v2 and future updates (#9833) * 📝 Update index with new extras * 📝 Update release notes --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pastukhov Nikita <diementros@yandex.ru>
This commit is contained in:
parent
dd4e78ca7b
commit
0976185af9
|
|
@ -25,10 +25,12 @@ jobs:
|
|||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
- name: Install Pydantic v2
|
||||
run: pip install "pydantic>=2.0.2,<3.0.0"
|
||||
- name: Lint
|
||||
run: bash scripts/lint.sh
|
||||
|
||||
|
|
@ -37,6 +39,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
pydantic-version: ["pydantic-v1", "pydantic-v2"]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
@ -51,10 +54,16 @@ jobs:
|
|||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
- name: Install Pydantic v1
|
||||
if: matrix.pydantic-version == 'pydantic-v1'
|
||||
run: pip install "pydantic>=1.10.0,<2.0.0"
|
||||
- name: Install Pydantic v2
|
||||
if: matrix.pydantic-version == 'pydantic-v2'
|
||||
run: pip install "pydantic>=2.0.2,<3.0.0"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
run: bash scripts/test.sh
|
||||
|
|
|
|||
|
|
@ -447,6 +447,8 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
|
|||
Used by Pydantic:
|
||||
|
||||
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
|
||||
|
||||
Used by Starlette:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# Async SQL (Relational) Databases
|
||||
|
||||
!!! info
|
||||
These docs are about to be updated. 🎉
|
||||
|
||||
The current version assumes Pydantic v1.
|
||||
|
||||
The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> once it is updated to use Pydantic v2 as well.
|
||||
|
||||
You can also use <a href="https://github.com/encode/databases" class="external-link" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`.
|
||||
|
||||
It is compatible with:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# NoSQL (Distributed / Big Data) Databases
|
||||
|
||||
!!! info
|
||||
These docs are about to be updated. 🎉
|
||||
|
||||
The current version assumes Pydantic v1.
|
||||
|
||||
The new docs will hopefully use Pydantic v2 and will use <a href="https://art049.github.io/odmantic/" class="external-link" target="_blank">ODMantic</a> with MongoDB.
|
||||
|
||||
**FastAPI** can also be integrated with any <abbr title="Distributed database (Big Data), also 'Not Only SQL'">NoSQL</abbr>.
|
||||
|
||||
Here we'll see an example using **<a href="https://www.couchbase.com/" class="external-link" target="_blank">Couchbase</a>**, a <abbr title="Document here refers to a JSON object (a dict), with keys and values, and those values can also be other JSON objects, arrays (lists), numbers, strings, booleans, etc.">document</abbr> based NoSQL database.
|
||||
|
|
|
|||
|
|
@ -150,9 +150,20 @@ And you could do this even if the data type in the request is not JSON.
|
|||
|
||||
For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON:
|
||||
|
||||
```Python hl_lines="17-22 24"
|
||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
|
||||
```
|
||||
=== "Pydantic v2"
|
||||
|
||||
```Python hl_lines="17-22 24"
|
||||
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
|
||||
```
|
||||
|
||||
=== "Pydantic v1"
|
||||
|
||||
```Python hl_lines="17-22 24"
|
||||
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`.
|
||||
|
||||
Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML.
|
||||
|
||||
|
|
@ -160,9 +171,20 @@ Then we use the request directly, and extract the body as `bytes`. This means th
|
|||
|
||||
And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content:
|
||||
|
||||
```Python hl_lines="26-33"
|
||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
|
||||
```
|
||||
=== "Pydantic v2"
|
||||
|
||||
```Python hl_lines="26-33"
|
||||
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
|
||||
```
|
||||
|
||||
=== "Pydantic v1"
|
||||
|
||||
```Python hl_lines="26-33"
|
||||
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`.
|
||||
|
||||
!!! tip
|
||||
Here we re-use the same Pydantic model.
|
||||
|
|
|
|||
|
|
@ -125,7 +125,34 @@ That means that any value read in Python from an environment variable will be a
|
|||
|
||||
## Pydantic `Settings`
|
||||
|
||||
Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
|
||||
Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
|
||||
|
||||
### Install `pydantic-settings`
|
||||
|
||||
First, install the `pydantic-settings` package:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install pydantic-settings
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
It also comes included when you install the `all` extras with:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install "fastapi[all]"
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
!!! info
|
||||
In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
|
||||
|
||||
### Create the `Settings` object
|
||||
|
||||
|
|
@ -135,9 +162,20 @@ The same way as with Pydantic models, you declare class attributes with type ann
|
|||
|
||||
You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
|
||||
|
||||
```Python hl_lines="2 5-8 11"
|
||||
{!../../../docs_src/settings/tutorial001.py!}
|
||||
```
|
||||
=== "Pydantic v2"
|
||||
|
||||
```Python hl_lines="2 5-8 11"
|
||||
{!> ../../../docs_src/settings/tutorial001.py!}
|
||||
```
|
||||
|
||||
=== "Pydantic v1"
|
||||
|
||||
!!! info
|
||||
In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`.
|
||||
|
||||
```Python hl_lines="2 5-8 11"
|
||||
{!> ../../../docs_src/settings/tutorial001_pv1.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
If you want something quick to copy and paste, don't use this example, use the last one below.
|
||||
|
|
@ -306,14 +344,28 @@ APP_NAME="ChimichangApp"
|
|||
|
||||
And then update your `config.py` with:
|
||||
|
||||
```Python hl_lines="9-10"
|
||||
{!../../../docs_src/settings/app03/config.py!}
|
||||
```
|
||||
=== "Pydantic v2"
|
||||
|
||||
Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use.
|
||||
```Python hl_lines="9"
|
||||
{!> ../../../docs_src/settings/app03_an/config.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
|
||||
!!! tip
|
||||
The `model_config` attribute is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
|
||||
|
||||
=== "Pydantic v1"
|
||||
|
||||
```Python hl_lines="9-10"
|
||||
{!> ../../../docs_src/settings/app03_an/config_pv1.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
|
||||
|
||||
!!! info
|
||||
In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`.
|
||||
|
||||
Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use.
|
||||
|
||||
### Creating the `Settings` only once with `lru_cache`
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@
|
|||
|
||||
Feel free to skip this.
|
||||
|
||||
Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives.
|
||||
|
||||
!!! info
|
||||
These docs assume Pydantic v1.
|
||||
|
||||
Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes.
|
||||
|
||||
If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM.
|
||||
|
||||
If you already have a code base that uses <a href="https://docs.peewee-orm.com/en/latest/" class="external-link" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# Testing a Database
|
||||
|
||||
!!! info
|
||||
These docs are about to be updated. 🎉
|
||||
|
||||
The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
|
||||
|
||||
The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
|
||||
|
||||
You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing.
|
||||
|
||||
You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc.
|
||||
|
|
|
|||
|
|
@ -446,6 +446,8 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
|
|||
Used by Pydantic:
|
||||
|
||||
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
|
||||
|
||||
Used by Starlette:
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,79 @@
|
|||
|
||||
## Latest Changes
|
||||
|
||||
✨ Support for **Pydantic v2** ✨
|
||||
|
||||
Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example:
|
||||
|
||||
* Improved **correctness** in corner cases.
|
||||
* **Safer** types.
|
||||
* Better **performance** and **less energy** consumption.
|
||||
* Better **extensibility**.
|
||||
* etc.
|
||||
|
||||
...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀
|
||||
|
||||
In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯
|
||||
|
||||
When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade.
|
||||
|
||||
The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign.
|
||||
|
||||
### Migration
|
||||
|
||||
Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/).
|
||||
|
||||
For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic).
|
||||
|
||||
A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes.
|
||||
|
||||
### Pydantic v1
|
||||
|
||||
**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while.
|
||||
|
||||
This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI.
|
||||
|
||||
There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**.
|
||||
|
||||
### Changes
|
||||
|
||||
* There are **new parameter** fields supported by Pydantic `Field()` for:
|
||||
|
||||
* `Path()`
|
||||
* `Query()`
|
||||
* `Header()`
|
||||
* `Cookie()`
|
||||
* `Body()`
|
||||
* `Form()`
|
||||
* `File()`
|
||||
|
||||
* The new parameter fields are:
|
||||
|
||||
* `default_factory`
|
||||
* `alias_priority`
|
||||
* `validation_alias`
|
||||
* `serialization_alias`
|
||||
* `discriminator`
|
||||
* `strict`
|
||||
* `multiple_of`
|
||||
* `allow_inf_nan`
|
||||
* `max_digits`
|
||||
* `decimal_places`
|
||||
* `json_schema_extra`
|
||||
|
||||
...you can read about them in the Pydantic docs.
|
||||
|
||||
* The parameter `regex` has been deprecated and replaced by `pattern`.
|
||||
* You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions).
|
||||
* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration.
|
||||
* You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
|
||||
* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict.
|
||||
* You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
|
||||
* When you install `"fastapi[all]"` it now also includes:
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
|
||||
* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly.
|
||||
* You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/).
|
||||
|
||||
## 0.99.1
|
||||
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ You can also add a parameter `min_length`:
|
|||
|
||||
## Add regular expressions
|
||||
|
||||
You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> that the parameter should match:
|
||||
You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> `pattern` that the parameter should match:
|
||||
|
||||
=== "Python 3.10+"
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ You can define a <abbr title="A regular expression, regex or regexp is a sequenc
|
|||
{!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
|
||||
```
|
||||
|
||||
This specific regular expression checks that the received parameter value:
|
||||
This specific regular expression pattern checks that the received parameter value:
|
||||
|
||||
* `^`: starts with the following characters, doesn't have characters before.
|
||||
* `fixedquery`: has the exact value `fixedquery`.
|
||||
|
|
@ -325,6 +325,20 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The
|
|||
|
||||
But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**.
|
||||
|
||||
### Pydantic v1 `regex` instead of `pattern`
|
||||
|
||||
Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated.
|
||||
|
||||
You could still see some code using it:
|
||||
|
||||
=== "Python 3.10+ Pydantic v1"
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!}
|
||||
```
|
||||
|
||||
But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓
|
||||
|
||||
## Default values
|
||||
|
||||
You can, of course, use default values other than `None`.
|
||||
|
|
|
|||
|
|
@ -4,24 +4,48 @@ You can declare examples of the data your app can receive.
|
|||
|
||||
Here are several ways to do it.
|
||||
|
||||
## Pydantic `schema_extra`
|
||||
## Extra JSON Schema data in Pydantic models
|
||||
|
||||
You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:
|
||||
You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema.
|
||||
|
||||
=== "Python 3.10+"
|
||||
=== "Python 3.10+ Pydantic v2"
|
||||
|
||||
```Python hl_lines="13-23"
|
||||
```Python hl_lines="13-24"
|
||||
{!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.6+"
|
||||
=== "Python 3.10+ Pydantic v1"
|
||||
|
||||
```Python hl_lines="15-25"
|
||||
```Python hl_lines="13-23"
|
||||
{!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.6+ Pydantic v2"
|
||||
|
||||
```Python hl_lines="15-26"
|
||||
{!> ../../../docs_src/schema_extra_example/tutorial001.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.6+ Pydantic v1"
|
||||
|
||||
```Python hl_lines="15-25"
|
||||
{!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!}
|
||||
```
|
||||
|
||||
That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.
|
||||
|
||||
=== "Pydantic v2"
|
||||
|
||||
In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic's docs: Model Config</a>.
|
||||
|
||||
You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
|
||||
|
||||
=== "Pydantic v1"
|
||||
|
||||
In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>.
|
||||
|
||||
You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
|
||||
|
||||
!!! tip
|
||||
You could use the same technique to extend the JSON Schema and add your own custom extra info.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# SQL (Relational) Databases
|
||||
|
||||
!!! info
|
||||
These docs are about to be updated. 🎉
|
||||
|
||||
The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
|
||||
|
||||
The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
|
||||
|
||||
**FastAPI** doesn't require you to use a SQL (relational) database.
|
||||
|
||||
But you can use any relational database that you want.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from fastapi import FastAPI
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
|||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
|||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Item(BaseModel):
|
|||
"/items/",
|
||||
openapi_extra={
|
||||
"requestBody": {
|
||||
"content": {"application/x-yaml": {"schema": Item.schema()}},
|
||||
"content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
|
|
@ -28,7 +28,7 @@ async def create_item(request: Request):
|
|||
except yaml.YAMLError:
|
||||
raise HTTPException(status_code=422, detail="Invalid YAML")
|
||||
try:
|
||||
item = Item.parse_obj(data)
|
||||
item = Item.model_validate(data)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=e.errors())
|
||||
return item
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
from typing import List
|
||||
|
||||
import yaml
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
tags: List[str]
|
||||
|
||||
|
||||
@app.post(
|
||||
"/items/",
|
||||
openapi_extra={
|
||||
"requestBody": {
|
||||
"content": {"application/x-yaml": {"schema": Item.schema()}},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
async def create_item(request: Request):
|
||||
raw_body = await request.body()
|
||||
try:
|
||||
data = yaml.safe_load(raw_body)
|
||||
except yaml.YAMLError:
|
||||
raise HTTPException(status_code=422, detail="Invalid YAML")
|
||||
try:
|
||||
item = Item.parse_obj(data)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=e.errors())
|
||||
return item
|
||||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Union[str, None] = Query(
|
||||
default=None, min_length=3, max_length=50, regex="^fixedquery$"
|
||||
default=None, min_length=3, max_length=50, pattern="^fixedquery$"
|
||||
)
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ app = FastAPI()
|
|||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[
|
||||
Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
|
||||
Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
|
||||
] = None
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[
|
||||
str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
|
||||
str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
|
||||
] = None
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[
|
||||
str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
|
||||
] = None
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
if q:
|
||||
results.update({"q": q})
|
||||
return results
|
||||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[
|
||||
Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
|
||||
Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
|
||||
] = None
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ app = FastAPI()
|
|||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: str
|
||||
| None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$")
|
||||
| None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$")
|
||||
):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
if q:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ async def read_items(
|
|||
description="Query string for the items to search in the database that have a good match",
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
regex="^fixedquery$",
|
||||
pattern="^fixedquery$",
|
||||
deprecated=True,
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async def read_items(
|
|||
description="Query string for the items to search in the database that have a good match",
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
regex="^fixedquery$",
|
||||
pattern="^fixedquery$",
|
||||
deprecated=True,
|
||||
),
|
||||
] = None
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async def read_items(
|
|||
description="Query string for the items to search in the database that have a good match",
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
regex="^fixedquery$",
|
||||
pattern="^fixedquery$",
|
||||
deprecated=True,
|
||||
),
|
||||
] = None
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async def read_items(
|
|||
description="Query string for the items to search in the database that have a good match",
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
regex="^fixedquery$",
|
||||
pattern="^fixedquery$",
|
||||
deprecated=True,
|
||||
),
|
||||
] = None
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async def read_items(
|
|||
description="Query string for the items to search in the database that have a good match",
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
regex="^fixedquery$",
|
||||
pattern="^fixedquery$",
|
||||
deprecated=True,
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ class Item(BaseModel):
|
|||
price: float
|
||||
tax: Union[float, None] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"name": "Foo",
|
||||
|
|
@ -23,6 +23,7 @@ class Item(BaseModel):
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: Union[str, None] = None
|
||||
price: float
|
||||
tax: Union[float, None] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"examples": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"description": "A very nice Item",
|
||||
"price": 35.4,
|
||||
"tax": 3.2,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
async def update_item(item_id: int, item: Item):
|
||||
results = {"item_id": item_id, "item": item}
|
||||
return results
|
||||
|
|
@ -10,8 +10,8 @@ class Item(BaseModel):
|
|||
price: float
|
||||
tax: float | None = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"name": "Foo",
|
||||
|
|
@ -21,6 +21,7 @@ class Item(BaseModel):
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
price: float
|
||||
tax: float | None = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"examples": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"description": "A very nice Item",
|
||||
"price": 35.4,
|
||||
"tax": 3.2,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
async def update_item(item_id: int, item: Item):
|
||||
results = {"item_id": item_id, "item": item}
|
||||
return results
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
@ -6,5 +6,4 @@ class Settings(BaseSettings):
|
|||
admin_email: str
|
||||
items_per_user: int = 50
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "Awesome API"
|
||||
admin_email: str
|
||||
items_per_user: int = 50
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from fastapi import FastAPI
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
from fastapi import FastAPI
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "Awesome API"
|
||||
admin_email: str
|
||||
items_per_user: int = 50
|
||||
|
||||
|
||||
settings = Settings()
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/info")
|
||||
async def info():
|
||||
return {
|
||||
"app_name": settings.app_name,
|
||||
"admin_email": settings.admin_email,
|
||||
"items_per_user": settings.items_per_user,
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.99.1"
|
||||
__version__ = "0.100.0-beta3"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,616 @@
|
|||
from collections import deque
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Deque,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
List,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi.exceptions import RequestErrorModel
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.version import VERSION as PYDANTIC_VERSION
|
||||
from starlette.datastructures import UploadFile
|
||||
from typing_extensions import Annotated, Literal, get_args, get_origin
|
||||
|
||||
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
|
||||
|
||||
|
||||
sequence_annotation_to_type = {
|
||||
Sequence: list,
|
||||
List: list,
|
||||
list: list,
|
||||
Tuple: tuple,
|
||||
tuple: tuple,
|
||||
Set: set,
|
||||
set: set,
|
||||
FrozenSet: frozenset,
|
||||
frozenset: frozenset,
|
||||
Deque: deque,
|
||||
deque: deque,
|
||||
}
|
||||
|
||||
sequence_types = tuple(sequence_annotation_to_type.keys())
|
||||
|
||||
if PYDANTIC_V2:
|
||||
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import ValidationError as ValidationError
|
||||
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
|
||||
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
|
||||
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 MultiHostUrl as MultiHostUrl
|
||||
from pydantic_core import PydanticUndefined, PydanticUndefinedType
|
||||
from pydantic_core import Url as Url
|
||||
from pydantic_core.core_schema import (
|
||||
general_plain_validator_function as general_plain_validator_function,
|
||||
)
|
||||
|
||||
Required = PydanticUndefined
|
||||
Undefined = PydanticUndefined
|
||||
UndefinedType = PydanticUndefinedType
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
Validator = Any
|
||||
|
||||
class BaseConfig:
|
||||
pass
|
||||
|
||||
class ErrorWrapper(Exception):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ModelField:
|
||||
field_info: FieldInfo
|
||||
name: str
|
||||
mode: Literal["validation", "serialization"] = "validation"
|
||||
|
||||
@property
|
||||
def alias(self) -> str:
|
||||
a = self.field_info.alias
|
||||
return a if a is not None else self.name
|
||||
|
||||
@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:
|
||||
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
|
||||
Annotated[self.field_info.annotation, self.field_info]
|
||||
)
|
||||
|
||||
def get_default(self) -> Any:
|
||||
if self.field_info.is_required():
|
||||
return Undefined
|
||||
return self.field_info.get_default(call_default_factory=True)
|
||||
|
||||
def validate(
|
||||
self,
|
||||
value: Any,
|
||||
values: Dict[str, Any] = {}, # noqa: B006
|
||||
*,
|
||||
loc: Tuple[Union[int, str], ...] = (),
|
||||
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
|
||||
try:
|
||||
return (
|
||||
self._type_adapter.validate_python(value, from_attributes=True),
|
||||
None,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return None, _regenerate_error_with_loc(
|
||||
errors=exc.errors(), loc_prefix=loc
|
||||
)
|
||||
|
||||
def serialize(
|
||||
self,
|
||||
value: Any,
|
||||
*,
|
||||
mode: Literal["json", "python"] = "json",
|
||||
include: Union[IncEx, None] = None,
|
||||
exclude: Union[IncEx, None] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
# What calls this code passes a value that already called
|
||||
# self._type_adapter.validate_python(value)
|
||||
return self._type_adapter.dump_python(
|
||||
value,
|
||||
mode=mode,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||
# ModelField to its JSON Schema.
|
||||
return id(self)
|
||||
|
||||
def get_annotation_from_field_info(
|
||||
annotation: Any, field_info: FieldInfo, field_name: str
|
||||
) -> Any:
|
||||
return annotation
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
return errors # type: ignore[return-value]
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.model_rebuild()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.model_dump(mode=mode, **kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.model_config
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
) -> Dict[str, Any]:
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
json_schema = field_mapping[(field, field.mode)]
|
||||
if "$ref" not in json_schema:
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
|
||||
json_schema[
|
||||
"title"
|
||||
] = field.field_info.title or field.alias.title().replace("_", " ")
|
||||
return json_schema
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
return {}
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Tuple[
|
||||
Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
inputs = [
|
||||
(field, field.mode, field._type_adapter.core_schema) for field in fields
|
||||
]
|
||||
field_mapping, definitions = schema_generator.generate_definitions(
|
||||
inputs=inputs
|
||||
)
|
||||
return field_mapping, definitions # type: ignore[return-value]
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
return field_annotation_is_scalar(
|
||||
field.field_info.annotation
|
||||
) and not isinstance(field.field_info, params.Body)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_sequence(field.field_info.annotation)
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_scalar_sequence(field.field_info.annotation)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return is_bytes_or_nonable_bytes_annotation(field.type_)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return is_bytes_sequence_annotation(field.type_)
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
return type(field_info).from_annotation(annotation)
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
origin_type = (
|
||||
get_origin(field.field_info.annotation) or field.field_info.annotation
|
||||
)
|
||||
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
|
||||
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
error = ValidationError.from_exception_data(
|
||||
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
|
||||
).errors()[0]
|
||||
error["input"] = None
|
||||
return error # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
|
||||
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
|
||||
return BodyModel
|
||||
|
||||
else:
|
||||
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
|
||||
from pydantic import AnyUrl as Url # noqa: F401
|
||||
from pydantic import ( # type: ignore[assignment]
|
||||
BaseConfig as BaseConfig, # noqa: F401
|
||||
)
|
||||
from pydantic import ValidationError as ValidationError # noqa: F401
|
||||
from pydantic.class_validators import ( # type: ignore[no-redef]
|
||||
Validator as Validator, # noqa: F401
|
||||
)
|
||||
from pydantic.error_wrappers import ( # type: ignore[no-redef]
|
||||
ErrorWrapper as ErrorWrapper, # noqa: F401
|
||||
)
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import ( # type: ignore[attr-defined]
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
)
|
||||
from pydantic.fields import FieldInfo as FieldInfo
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
ModelField as ModelField, # noqa: F401
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Required as Required, # noqa: F401
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Undefined as Undefined,
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
|
||||
UndefinedType as UndefinedType, # noqa: F401
|
||||
)
|
||||
from pydantic.networks import ( # type: ignore[no-redef]
|
||||
MultiHostDsn as MultiHostUrl, # noqa: F401
|
||||
)
|
||||
from pydantic.schema import (
|
||||
field_schema,
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
model_process_schema,
|
||||
)
|
||||
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
|
||||
get_annotation_from_field_info as get_annotation_from_field_info,
|
||||
)
|
||||
from pydantic.typing import ( # type: ignore[no-redef]
|
||||
evaluate_forwardref as evaluate_forwardref, # noqa: F401
|
||||
)
|
||||
from pydantic.utils import ( # type: ignore[no-redef]
|
||||
lenient_issubclass as lenient_issubclass, # noqa: F401
|
||||
)
|
||||
|
||||
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
|
||||
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
|
||||
CoreSchema = Any # type: ignore[assignment,misc]
|
||||
|
||||
sequence_shapes = {
|
||||
SHAPE_LIST,
|
||||
SHAPE_SET,
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
SHAPE_TUPLE: tuple,
|
||||
SHAPE_SEQUENCE: list,
|
||||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class GenerateJsonSchema: # type: ignore[no-redef]
|
||||
ref_template: str
|
||||
|
||||
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
def general_plain_validator_function( # type: ignore[misc]
|
||||
function: Callable[..., Any],
|
||||
*,
|
||||
ref: Union[str, None] = None,
|
||||
metadata: Any = None,
|
||||
serialization: Any = None,
|
||||
) -> Any:
|
||||
return {}
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict[str, Any]] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
model_name = model_name_map[model]
|
||||
if "description" in m_schema:
|
||||
m_schema["description"] = m_schema["description"].split("\f")[0]
|
||||
definitions[model_name] = m_schema
|
||||
return definitions
|
||||
|
||||
def is_pv1_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
field_info = field.field_info
|
||||
if not (
|
||||
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, dict)
|
||||
and not field_annotation_is_sequence(field.type_)
|
||||
and not is_dataclass(field.type_)
|
||||
and not isinstance(field_info, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
if not all(
|
||||
is_pv1_scalar_field(f)
|
||||
for f in field.sub_fields # type: ignore[attr-defined]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None: # type: ignore[attr-defined]
|
||||
for sub_field in field.sub_fields: # type: ignore[attr-defined]
|
||||
if not is_pv1_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if _annotation_is_sequence(field.type_):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
use_errors: List[Any] = []
|
||||
for error in errors:
|
||||
if isinstance(error, ErrorWrapper):
|
||||
new_errors = ValidationError( # type: ignore[call-arg]
|
||||
errors=[error], model=RequestErrorModel
|
||||
).errors()
|
||||
use_errors.extend(new_errors)
|
||||
elif isinstance(error, list):
|
||||
use_errors.extend(_normalize_errors(error))
|
||||
else:
|
||||
use_errors.append(error)
|
||||
return use_errors
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.update_forward_refs()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.dict(**kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.__config__ # type: ignore[attr-defined]
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
) -> Dict[str, Any]:
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
return field_schema( # type: ignore[no-any-return]
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0]
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return get_model_name_map(models) # type: ignore[no-any-return]
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Tuple[
|
||||
Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return {}, get_model_definitions(
|
||||
flat_models=models, model_name_map=model_name_map
|
||||
)
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_field(field)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_sequence_field(field)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return lenient_issubclass(field.type_, bytes)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
return copy(field_info)
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
|
||||
new_error = ValidationError([missing_field_error], RequestErrorModel)
|
||||
return new_error.errors()[0] # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
BodyModel = create_model(model_name)
|
||||
for f in fields:
|
||||
BodyModel.__fields__[f.name] = f # type: ignore[index]
|
||||
return BodyModel
|
||||
|
||||
|
||||
def _regenerate_error_with_loc(
|
||||
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
|
||||
) -> List[Dict[str, Any]]:
|
||||
updated_loc_errors: List[Any] = [
|
||||
{**err, "loc": loc_prefix + err.get("loc", ())}
|
||||
for err in _normalize_errors(errors)
|
||||
]
|
||||
|
||||
return updated_loc_errors
|
||||
|
||||
|
||||
def _annotation_is_sequence(annotation: Union[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:
|
||||
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
|
||||
get_origin(annotation)
|
||||
)
|
||||
|
||||
|
||||
def value_is_sequence(value: Any) -> bool:
|
||||
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
return (
|
||||
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
|
||||
or _annotation_is_sequence(annotation)
|
||||
or is_dataclass(annotation)
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
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))
|
||||
|
||||
return (
|
||||
_annotation_is_complex(annotation)
|
||||
or _annotation_is_complex(origin)
|
||||
or hasattr(origin, "__pydantic_core_schema__")
|
||||
or hasattr(origin, "__get_pydantic_core_schema__")
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_scalar(annotation: Any) -> bool:
|
||||
# handle Ellipsis here to make tuple[int, ...] work nicely
|
||||
return annotation is Ellipsis or not field_annotation_is_complex(annotation)
|
||||
|
||||
|
||||
def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one_scalar_sequence = False
|
||||
for arg in get_args(annotation):
|
||||
if field_annotation_is_scalar_sequence(arg):
|
||||
at_least_one_scalar_sequence = True
|
||||
continue
|
||||
elif not field_annotation_is_scalar(arg):
|
||||
return False
|
||||
return at_least_one_scalar_sequence
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
field_annotation_is_scalar(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, bytes):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, bytes):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, UploadFile):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, UploadFile):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_bytes_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_bytes_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_bytes_or_nonable_bytes_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_uploadfile_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
|
@ -15,7 +15,6 @@ from typing import (
|
|||
|
||||
from fastapi import routing
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
|
|
@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
|
|||
)
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import generate_unique_id
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
|
|
@ -305,8 +304,8 @@ class FastAPI(Starlette):
|
|||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -363,8 +362,8 @@ class FastAPI(Starlette):
|
|||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -484,8 +483,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -539,8 +538,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -594,8 +593,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -649,8 +648,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -704,8 +703,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -759,8 +758,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -814,8 +813,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -869,8 +868,8 @@ class FastAPI(Starlette):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
general_plain_validator_function,
|
||||
)
|
||||
from starlette.datastructures import URL as URL # noqa: F401
|
||||
from starlette.datastructures import Address as Address # noqa: F401
|
||||
from starlette.datastructures import FormData as FormData # noqa: F401
|
||||
|
|
@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
|
|||
return v
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
|
||||
if not isinstance(__input_value, StarletteUploadFile):
|
||||
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
|
||||
return cast(UploadFile, __input_value)
|
||||
|
||||
if not PYDANTIC_V2:
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "binary"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return general_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any, Callable, List, Optional, Sequence
|
||||
|
||||
from fastapi._compat import ModelField
|
||||
from fastapi.security.base import SecurityBase
|
||||
from pydantic.fields import ModelField
|
||||
|
||||
|
||||
class SecurityRequirement:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import dataclasses
|
||||
import inspect
|
||||
from contextlib import contextmanager
|
||||
from copy import copy, deepcopy
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
|
|
@ -20,6 +19,31 @@ from typing import (
|
|||
|
||||
import anyio
|
||||
from fastapi import params
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
ErrorWrapper,
|
||||
ModelField,
|
||||
Required,
|
||||
Undefined,
|
||||
_regenerate_error_with_loc,
|
||||
copy_field_info,
|
||||
create_body_model,
|
||||
evaluate_forwardref,
|
||||
field_annotation_is_scalar,
|
||||
get_annotation_from_field_info,
|
||||
get_missing_field_error,
|
||||
is_bytes_field,
|
||||
is_bytes_sequence_field,
|
||||
is_scalar_field,
|
||||
is_scalar_sequence_field,
|
||||
is_sequence_field,
|
||||
is_uploadfile_or_nonable_uploadfile_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
lenient_issubclass,
|
||||
sequence_types,
|
||||
serialize_sequence_value,
|
||||
value_is_sequence,
|
||||
)
|
||||
from fastapi.concurrency import (
|
||||
AsyncExitStack,
|
||||
asynccontextmanager,
|
||||
|
|
@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase
|
|||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.utils import create_response_field, get_path_param_names
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import (
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
FieldInfo,
|
||||
ModelField,
|
||||
Required,
|
||||
Undefined,
|
||||
)
|
||||
from pydantic.schema import get_annotation_from_field_info
|
||||
from pydantic.typing import evaluate_forwardref, get_args, get_origin
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
from starlette.requests import HTTPConnection, Request
|
||||
from starlette.responses import Response
|
||||
from starlette.websockets import WebSocket
|
||||
from typing_extensions import Annotated
|
||||
|
||||
sequence_shapes = {
|
||||
SHAPE_LIST,
|
||||
SHAPE_SET,
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
sequence_types = (list, set, tuple)
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
SHAPE_TUPLE: tuple,
|
||||
SHAPE_SEQUENCE: list,
|
||||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
multipart_not_installed_error = (
|
||||
'Form data requires "python-multipart" to be installed. \n'
|
||||
|
|
@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
|||
)
|
||||
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
field_info = field.field_info
|
||||
if not (
|
||||
field.shape == SHAPE_SINGLETON
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||||
and not dataclasses.is_dataclass(field.type_)
|
||||
and not isinstance(field_info, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields:
|
||||
if not all(is_scalar_field(f) for f in field.sub_fields):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None:
|
||||
for sub_field in field.sub_fields:
|
||||
if not is_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if lenient_issubclass(field.type_, sequence_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
|
|
@ -364,12 +322,11 @@ def analyze_param(
|
|||
is_path_param: bool,
|
||||
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
|
||||
field_info = None
|
||||
used_default_field_info = False
|
||||
depends = None
|
||||
type_annotation: Any = Any
|
||||
if (
|
||||
annotation is not inspect.Signature.empty
|
||||
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
|
||||
and get_origin(annotation) is Annotated
|
||||
):
|
||||
annotated_args = get_args(annotation)
|
||||
type_annotation = annotated_args[0]
|
||||
|
|
@ -384,7 +341,9 @@ def analyze_param(
|
|||
fastapi_annotation = next(iter(fastapi_annotations), None)
|
||||
if isinstance(fastapi_annotation, FieldInfo):
|
||||
# Copy `field_info` because we mutate `field_info.default` below.
|
||||
field_info = copy(fastapi_annotation)
|
||||
field_info = copy_field_info(
|
||||
field_info=fastapi_annotation, annotation=annotation
|
||||
)
|
||||
assert field_info.default is Undefined or field_info.default is Required, (
|
||||
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
||||
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
||||
|
|
@ -415,6 +374,8 @@ def analyze_param(
|
|||
f" together for {param_name!r}"
|
||||
)
|
||||
field_info = value
|
||||
if PYDANTIC_V2:
|
||||
field_info.annotation = type_annotation
|
||||
|
||||
if depends is not None and depends.dependency is None:
|
||||
depends.dependency = type_annotation
|
||||
|
|
@ -433,10 +394,15 @@ def analyze_param(
|
|||
# We might check here that `default_value is Required`, but the fact is that the same
|
||||
# parameter might sometimes be a path parameter and sometimes not. See
|
||||
# `tests/test_infer_param_optionality.py` for an example.
|
||||
field_info = params.Path()
|
||||
field_info = params.Path(annotation=type_annotation)
|
||||
elif is_uploadfile_or_nonable_uploadfile_annotation(
|
||||
type_annotation
|
||||
) or is_uploadfile_sequence_annotation(type_annotation):
|
||||
field_info = params.File(annotation=type_annotation, default=default_value)
|
||||
elif not field_annotation_is_scalar(annotation=type_annotation):
|
||||
field_info = params.Body(annotation=type_annotation, default=default_value)
|
||||
else:
|
||||
field_info = params.Query(default=default_value)
|
||||
used_default_field_info = True
|
||||
field_info = params.Query(annotation=type_annotation, default=default_value)
|
||||
|
||||
field = None
|
||||
if field_info is not None:
|
||||
|
|
@ -450,8 +416,8 @@ def analyze_param(
|
|||
and getattr(field_info, "in_", None) is None
|
||||
):
|
||||
field_info.in_ = params.ParamTypes.query
|
||||
annotation = get_annotation_from_field_info(
|
||||
annotation if annotation is not inspect.Signature.empty else Any,
|
||||
use_annotation = get_annotation_from_field_info(
|
||||
type_annotation,
|
||||
field_info,
|
||||
param_name,
|
||||
)
|
||||
|
|
@ -459,19 +425,15 @@ def analyze_param(
|
|||
alias = param_name.replace("_", "-")
|
||||
else:
|
||||
alias = field_info.alias or param_name
|
||||
field_info.alias = alias
|
||||
field = create_response_field(
|
||||
name=param_name,
|
||||
type_=annotation,
|
||||
type_=use_annotation,
|
||||
default=field_info.default,
|
||||
alias=alias,
|
||||
required=field_info.default in (Required, Undefined),
|
||||
field_info=field_info,
|
||||
)
|
||||
if used_default_field_info:
|
||||
if lenient_issubclass(field.type_, UploadFile):
|
||||
field.field_info = params.File(field_info.default)
|
||||
elif not is_scalar_field(field=field):
|
||||
field.field_info = params.Body(field_info.default)
|
||||
|
||||
return type_annotation, depends, field
|
||||
|
||||
|
|
@ -554,13 +516,13 @@ async def solve_dependencies(
|
|||
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
||||
) -> Tuple[
|
||||
Dict[str, Any],
|
||||
List[ErrorWrapper],
|
||||
List[Any],
|
||||
Optional[BackgroundTasks],
|
||||
Response,
|
||||
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
errors: List[Any] = []
|
||||
if response is None:
|
||||
response = Response()
|
||||
del response.headers["content-length"]
|
||||
|
|
@ -674,7 +636,7 @@ async def solve_dependencies(
|
|||
def request_params_to_args(
|
||||
required_params: Sequence[ModelField],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
|
|
@ -688,23 +650,19 @@ def request_params_to_args(
|
|||
assert isinstance(
|
||||
field_info, params.Param
|
||||
), "Params must be subclasses of Param"
|
||||
loc = (field_info.in_.value, field.alias)
|
||||
if value is None:
|
||||
if field.required:
|
||||
errors.append(
|
||||
ErrorWrapper(
|
||||
MissingError(), loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
)
|
||||
errors.append(get_missing_field_error(loc=loc))
|
||||
else:
|
||||
values[field.name] = deepcopy(field.default)
|
||||
continue
|
||||
v_, errors_ = field.validate(
|
||||
value, values, loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
errors.extend(new_errors)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
|
|
@ -713,9 +671,9 @@ def request_params_to_args(
|
|||
async def request_body_to_args(
|
||||
required_params: List[ModelField],
|
||||
received_body: Optional[Union[Dict[str, Any], FormData]],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
values = {}
|
||||
errors = []
|
||||
errors: List[Dict[str, Any]] = []
|
||||
if required_params:
|
||||
field = required_params[0]
|
||||
field_info = field.field_info
|
||||
|
|
@ -733,9 +691,7 @@ async def request_body_to_args(
|
|||
|
||||
value: Optional[Any] = None
|
||||
if received_body is not None:
|
||||
if (
|
||||
field.shape in sequence_shapes or field.type_ in sequence_types
|
||||
) and isinstance(received_body, FormData):
|
||||
if (is_sequence_field(field)) and isinstance(received_body, FormData):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
try:
|
||||
|
|
@ -748,7 +704,7 @@ async def request_body_to_args(
|
|||
or (isinstance(field_info, params.Form) and value == "")
|
||||
or (
|
||||
isinstance(field_info, params.Form)
|
||||
and field.shape in sequence_shapes
|
||||
and is_sequence_field(field)
|
||||
and len(value) == 0
|
||||
)
|
||||
):
|
||||
|
|
@ -759,16 +715,17 @@ async def request_body_to_args(
|
|||
continue
|
||||
if (
|
||||
isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and is_bytes_field(field)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
field.shape in sequence_shapes
|
||||
is_bytes_sequence_field(field)
|
||||
and isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and isinstance(value, sequence_types)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
||||
results: List[Union[bytes, str]] = []
|
||||
|
||||
async def process_fn(
|
||||
|
|
@ -780,24 +737,19 @@ async def request_body_to_args(
|
|||
async with anyio.create_task_group() as tg:
|
||||
for sub_value in value:
|
||||
tg.start_soon(process_fn, sub_value.read)
|
||||
value = sequence_shape_to_type[field.shape](results)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(errors_)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
|
||||
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
|
||||
return missing_field_error
|
||||
|
||||
|
||||
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
if not flat_dependant.body_params:
|
||||
|
|
@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|||
for param in flat_dependant.body_params:
|
||||
setattr(param.field_info, "embed", True) # noqa: B010
|
||||
model_name = "Body_" + name
|
||||
BodyModel: Type[BaseModel] = create_model(model_name)
|
||||
for f in flat_dependant.body_params:
|
||||
BodyModel.__fields__[f.name] = f
|
||||
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)
|
||||
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
||||
"annotation": BodyModel,
|
||||
"alias": "body",
|
||||
}
|
||||
if not required:
|
||||
BodyFieldInfo_kwargs["default"] = None
|
||||
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
|
||||
BodyFieldInfo: Type[params.Body] = params.File
|
||||
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
|
||||
|
|
|
|||
|
|
@ -1,15 +1,87 @@
|
|||
import dataclasses
|
||||
import datetime
|
||||
from collections import defaultdict, deque
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import PurePath
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Interface,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from pathlib import Path, PurePath
|
||||
from re import Pattern
|
||||
from types import GeneratorType
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.types import IncEx
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
from pydantic.color import Color
|
||||
from pydantic.networks import NameEmail
|
||||
from pydantic.types import SecretBytes, SecretStr
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
|
||||
return o.isoformat()
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
# TODO: pv2 should this return strings instead?
|
||||
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
|
||||
"""
|
||||
Encodes a Decimal as int of there's no exponent, otherwise float
|
||||
|
||||
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
|
||||
where a integer (but not int typed) is used. Encoding this as a float
|
||||
results in failed round-tripping between encode and parse.
|
||||
Our Id type is a prime example of this.
|
||||
|
||||
>>> decimal_encoder(Decimal("1.0"))
|
||||
1.0
|
||||
|
||||
>>> decimal_encoder(Decimal("1"))
|
||||
1
|
||||
"""
|
||||
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
|
||||
return int(dec_value)
|
||||
else:
|
||||
return float(dec_value)
|
||||
|
||||
|
||||
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
|
||||
bytes: lambda o: o.decode(),
|
||||
Color: str,
|
||||
datetime.date: isoformat,
|
||||
datetime.datetime: isoformat,
|
||||
datetime.time: isoformat,
|
||||
datetime.timedelta: lambda td: td.total_seconds(),
|
||||
Decimal: decimal_encoder,
|
||||
Enum: lambda o: o.value,
|
||||
frozenset: list,
|
||||
deque: list,
|
||||
GeneratorType: list,
|
||||
IPv4Address: str,
|
||||
IPv4Interface: str,
|
||||
IPv4Network: str,
|
||||
IPv6Address: str,
|
||||
IPv6Interface: str,
|
||||
IPv6Network: str,
|
||||
NameEmail: str,
|
||||
Path: str,
|
||||
Pattern: lambda o: o.pattern,
|
||||
SecretBytes: str,
|
||||
SecretStr: str,
|
||||
set: list,
|
||||
UUID: str,
|
||||
Url: str,
|
||||
MultiHostUrl: str,
|
||||
}
|
||||
|
||||
|
||||
def generate_encoders_by_class_tuples(
|
||||
|
|
@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
|||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
|
|
@ -50,10 +122,15 @@ def jsonable_encoder(
|
|||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||
exclude = set(exclude)
|
||||
if isinstance(obj, BaseModel):
|
||||
encoder = getattr(obj.__config__, "json_encoders", {})
|
||||
if custom_encoder:
|
||||
encoder.update(custom_encoder)
|
||||
obj_dict = obj.dict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
encoders: Dict[Any, Any] = {}
|
||||
if not PYDANTIC_V2:
|
||||
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
|
||||
if custom_encoder:
|
||||
encoders.update(custom_encoder)
|
||||
obj_dict = _model_dump(
|
||||
obj,
|
||||
mode="json",
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
|
|
@ -67,7 +144,8 @@ def jsonable_encoder(
|
|||
obj_dict,
|
||||
exclude_none=exclude_none,
|
||||
exclude_defaults=exclude_defaults,
|
||||
custom_encoder=encoder,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
custom_encoder=encoders,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if dataclasses.is_dataclass(obj):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Any, Dict, Optional, Sequence, Type
|
||||
|
||||
from pydantic import BaseModel, ValidationError, create_model
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from pydantic import BaseModel, create_model
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
|
||||
|
||||
|
|
@ -26,12 +25,25 @@ class FastAPIError(RuntimeError):
|
|||
"""
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
|
||||
class ValidationException(Exception):
|
||||
def __init__(self, errors: Sequence[Any]) -> None:
|
||||
self._errors = errors
|
||||
|
||||
def errors(self) -> Sequence[Any]:
|
||||
return self._errors
|
||||
|
||||
|
||||
class RequestValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
super().__init__(errors, RequestErrorModel)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocketErrorModel)
|
||||
class WebSocketRequestValidationError(ValidationException):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
|
||||
REF_PREFIX = "#/components/schemas/"
|
||||
REF_TEMPLATE = "#/components/schemas/{model}"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
_model_rebuild,
|
||||
general_plain_validator_function,
|
||||
)
|
||||
from fastapi.logger import logger
|
||||
from pydantic import AnyUrl, BaseModel, Field
|
||||
from typing_extensions import Annotated, Literal
|
||||
from typing_extensions import deprecated as typing_deprecated
|
||||
|
||||
try:
|
||||
import email_validator # type: ignore
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic import EmailStr
|
||||
|
|
@ -26,14 +34,39 @@ except ImportError: # pragma: no cover
|
|||
)
|
||||
return str(v)
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, __input_value: Any, _: Any) -> str:
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
"To install, run: pip install email-validator"
|
||||
)
|
||||
return str(__input_value)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "email"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return general_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
name: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
email: Optional[EmailStr] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class License(BaseModel):
|
||||
|
|
@ -41,8 +74,13 @@ class License(BaseModel):
|
|||
identifier: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
|
|
@ -54,17 +92,27 @@ class Info(BaseModel):
|
|||
license: Optional[License] = None
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ServerVariable(BaseModel):
|
||||
enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
|
||||
enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
|
||||
default: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Server(BaseModel):
|
||||
|
|
@ -72,8 +120,13 @@ class Server(BaseModel):
|
|||
description: Optional[str] = None
|
||||
variables: Optional[Dict[str, ServerVariable]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Reference(BaseModel):
|
||||
|
|
@ -92,16 +145,26 @@ class XML(BaseModel):
|
|||
attribute: Optional[bool] = None
|
||||
wrapped: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ExternalDocumentation(BaseModel):
|
||||
description: Optional[str] = None
|
||||
url: AnyUrl
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Schema(BaseModel):
|
||||
|
|
@ -190,8 +253,13 @@ class Schema(BaseModel):
|
|||
),
|
||||
] = None
|
||||
|
||||
class Config:
|
||||
extra: str = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
|
||||
|
|
@ -205,8 +273,13 @@ class Example(BaseModel):
|
|||
value: Optional[Any] = None
|
||||
externalValue: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterInType(Enum):
|
||||
|
|
@ -223,8 +296,13 @@ class Encoding(BaseModel):
|
|||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class MediaType(BaseModel):
|
||||
|
|
@ -233,8 +311,13 @@ class MediaType(BaseModel):
|
|||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
encoding: Optional[Dict[str, Encoding]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterBase(BaseModel):
|
||||
|
|
@ -251,8 +334,13 @@ class ParameterBase(BaseModel):
|
|||
# Serialization rules for more complex scenarios
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Parameter(ParameterBase):
|
||||
|
|
@ -269,8 +357,13 @@ class RequestBody(BaseModel):
|
|||
content: Dict[str, MediaType]
|
||||
required: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
|
|
@ -281,8 +374,13 @@ class Link(BaseModel):
|
|||
description: Optional[str] = None
|
||||
server: Optional[Server] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
|
|
@ -291,8 +389,13 @@ class Response(BaseModel):
|
|||
content: Optional[Dict[str, MediaType]] = None
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Operation(BaseModel):
|
||||
|
|
@ -310,8 +413,13 @@ class Operation(BaseModel):
|
|||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class PathItem(BaseModel):
|
||||
|
|
@ -329,8 +437,13 @@ class PathItem(BaseModel):
|
|||
servers: Optional[List[Server]] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class SecuritySchemeType(Enum):
|
||||
|
|
@ -344,8 +457,13 @@ class SecurityBase(BaseModel):
|
|||
type_: SecuritySchemeType = Field(alias="type")
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class APIKeyIn(Enum):
|
||||
|
|
@ -374,8 +492,13 @@ class OAuthFlow(BaseModel):
|
|||
refreshUrl: Optional[str] = None
|
||||
scopes: Dict[str, str] = {}
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuthFlowImplicit(OAuthFlow):
|
||||
|
|
@ -401,8 +524,13 @@ class OAuthFlows(BaseModel):
|
|||
clientCredentials: Optional[OAuthFlowClientCredentials] = None
|
||||
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
|
|
@ -433,8 +561,13 @@ class Components(BaseModel):
|
|||
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
||||
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
|
|
@ -442,8 +575,13 @@ class Tag(BaseModel):
|
|||
description: Optional[str] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OpenAPI(BaseModel):
|
||||
|
|
@ -459,10 +597,15 @@ class OpenAPI(BaseModel):
|
|||
tags: Optional[List[Tag]] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
Schema.update_forward_refs()
|
||||
Operation.update_forward_refs()
|
||||
Encoding.update_forward_refs()
|
||||
_model_rebuild(Schema)
|
||||
_model_rebuild(Operation)
|
||||
_model_rebuild(Encoding)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,37 @@
|
|||
import http.client
|
||||
import inspect
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi._compat import (
|
||||
GenerateJsonSchema,
|
||||
JsonSchemaValue,
|
||||
ModelField,
|
||||
Undefined,
|
||||
get_compat_model_name_map,
|
||||
get_definitions,
|
||||
get_schema_from_model_field,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, Param
|
||||
from fastapi.responses import Response
|
||||
from fastapi.types import ModelNameMap
|
||||
from fastapi.utils import (
|
||||
deep_dict_update,
|
||||
generate_operation_id_for_path,
|
||||
get_model_definitions,
|
||||
is_body_allowed_for_status_code,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import ModelField, Undefined
|
||||
from pydantic.schema import (
|
||||
field_schema,
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
)
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import BaseRoute
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
from typing_extensions import Literal
|
||||
|
||||
validation_error_definition = {
|
||||
"title": "ValidationError",
|
||||
|
|
@ -88,7 +90,11 @@ def get_openapi_security_definitions(
|
|||
def get_openapi_operation_parameters(
|
||||
*,
|
||||
all_route_params: Sequence[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = []
|
||||
for param in all_route_params:
|
||||
|
|
@ -96,13 +102,17 @@ def get_openapi_operation_parameters(
|
|||
field_info = cast(Param, field_info)
|
||||
if not field_info.include_in_schema:
|
||||
continue
|
||||
param_schema = get_schema_from_model_field(
|
||||
field=param,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
parameter = {
|
||||
"name": param.alias,
|
||||
"in": field_info.in_.value,
|
||||
"required": param.required,
|
||||
"schema": field_schema(
|
||||
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0],
|
||||
"schema": param_schema,
|
||||
}
|
||||
if field_info.description:
|
||||
parameter["description"] = field_info.description
|
||||
|
|
@ -117,13 +127,20 @@ def get_openapi_operation_parameters(
|
|||
def get_openapi_operation_request_body(
|
||||
*,
|
||||
body_field: Optional[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not body_field:
|
||||
return None
|
||||
assert isinstance(body_field, ModelField)
|
||||
body_schema, _, _ = field_schema(
|
||||
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
body_schema = get_schema_from_model_field(
|
||||
field=body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
field_info = cast(Body, body_field.field_info)
|
||||
request_media_type = field_info.media_type
|
||||
|
|
@ -186,7 +203,14 @@ def get_openapi_operation_metadata(
|
|||
|
||||
|
||||
def get_openapi_path(
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
|
||||
*,
|
||||
route: routing.APIRoute,
|
||||
operation_ids: Set[str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
|
|
@ -214,7 +238,10 @@ def get_openapi_path(
|
|||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_flat_params(route.dependant)
|
||||
operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params, model_name_map=model_name_map
|
||||
all_route_params=all_route_params,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
|
|
@ -232,7 +259,10 @@ def get_openapi_path(
|
|||
operation["parameters"] = list(all_parameters.values())
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
body_field=route.body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
|
|
@ -246,8 +276,10 @@ def get_openapi_path(
|
|||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
|
|
@ -273,10 +305,11 @@ def get_openapi_path(
|
|||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(current_response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _, _ = field_schema(
|
||||
route.response_field,
|
||||
response_schema = get_schema_from_model_field(
|
||||
field=route.response_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
|
|
@ -305,8 +338,11 @@ def get_openapi_path(
|
|||
field = route.response_fields.get(additional_status_code)
|
||||
additional_field_schema: Optional[Dict[str, Any]] = None
|
||||
if field:
|
||||
additional_field_schema, _, _ = field_schema(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
additional_field_schema = get_schema_from_model_field(
|
||||
field=field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
media_type = route_response_media_type or "application/json"
|
||||
additional_schema = (
|
||||
|
|
@ -352,13 +388,13 @@ def get_openapi_path(
|
|||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
def get_fields_from_routes(
|
||||
routes: Sequence[BaseRoute],
|
||||
) -> Set[Union[Type[BaseModel], Type[Enum]]]:
|
||||
) -> List[ModelField]:
|
||||
body_fields_from_routes: List[ModelField] = []
|
||||
responses_from_routes: List[ModelField] = []
|
||||
request_fields_from_routes: List[ModelField] = []
|
||||
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
|
||||
callback_flat_models: List[ModelField] = []
|
||||
for route in routes:
|
||||
if getattr(route, "include_in_schema", None) and isinstance(
|
||||
route, routing.APIRoute
|
||||
|
|
@ -373,13 +409,12 @@ def get_flat_models_from_routes(
|
|||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
if route.callbacks:
|
||||
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
|
||||
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
||||
params = get_flat_params(route.dependant)
|
||||
request_fields_from_routes.extend(params)
|
||||
|
||||
flat_models = callback_flat_models | get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes,
|
||||
known_models=set(),
|
||||
flat_models = callback_flat_models + list(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
||||
)
|
||||
return flat_models
|
||||
|
||||
|
|
@ -417,15 +452,22 @@ def get_openapi(
|
|||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
webhook_paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
definitions = get_model_definitions(
|
||||
flat_models=flat_models, model_name_map=model_name_map
|
||||
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
|
||||
model_name_map = get_compat_model_name_map(all_fields)
|
||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||
field_mapping, definitions = get_definitions(
|
||||
fields=all_fields,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
for route in routes or []:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
route=route,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
|
|
@ -441,8 +483,10 @@ def get_openapi(
|
|||
if isinstance(webhook, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=webhook,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
from typing import Any, Callable, List, Optional, Sequence
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
from fastapi import params
|
||||
from pydantic.fields import Undefined
|
||||
from fastapi._compat import Undefined
|
||||
from typing_extensions import Annotated, deprecated
|
||||
|
||||
_Unset: Any = Undefined
|
||||
|
||||
|
||||
def Path( # noqa: N802
|
||||
default: Any = ...,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -17,7 +25,19 @@ def Path( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -25,14 +45,19 @@ def Path( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Path(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -41,11 +66,19 @@ def Path( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -53,7 +86,13 @@ def Path( # noqa: N802
|
|||
def Query( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -62,7 +101,19 @@ def Query( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -70,14 +121,19 @@ def Query( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Query(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -86,11 +142,19 @@ def Query( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -98,7 +162,13 @@ def Query( # noqa: N802
|
|||
def Header( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
convert_underscores: bool = True,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
|
|
@ -108,7 +178,19 @@ def Header( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -116,14 +198,19 @@ def Header( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Header(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
convert_underscores=convert_underscores,
|
||||
title=title,
|
||||
description=description,
|
||||
|
|
@ -133,11 +220,19 @@ def Header( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -145,7 +240,13 @@ def Header( # noqa: N802
|
|||
def Cookie( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -154,7 +255,19 @@ def Cookie( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -162,14 +275,19 @@ def Cookie( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Cookie(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -178,11 +296,19 @@ def Cookie( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -190,9 +316,15 @@ def Cookie( # noqa: N802
|
|||
def Body( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
embed: bool = False,
|
||||
media_type: str = "application/json",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -201,7 +333,19 @@ def Body( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -209,14 +353,21 @@ def Body( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Body(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
embed=embed,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -225,9 +376,19 @@ def Body( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -235,8 +396,14 @@ def Body( # noqa: N802
|
|||
def Form( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -245,7 +412,19 @@ def Form( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -253,13 +432,20 @@ def Form( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.Form(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -268,9 +454,19 @@ def Form( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -278,8 +474,14 @@ def Form( # noqa: N802
|
|||
def File( # noqa: N802
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -288,7 +490,19 @@ def File( # noqa: N802
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -296,13 +510,20 @@ def File( # noqa: N802
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return params.File(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -311,9 +532,19 @@ def File( # noqa: N802
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
example=example,
|
||||
examples=examples,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, List, Optional, Sequence
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
from pydantic.fields import FieldInfo, Undefined
|
||||
from pydantic.fields import FieldInfo
|
||||
from typing_extensions import Annotated, deprecated
|
||||
|
||||
from ._compat import PYDANTIC_V2, Undefined
|
||||
|
||||
_Unset: Any = Undefined
|
||||
|
||||
|
||||
class ParamTypes(Enum):
|
||||
query = "query"
|
||||
|
|
@ -20,7 +24,14 @@ class Param(FieldInfo):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -29,7 +40,19 @@ class Param(FieldInfo):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -37,25 +60,24 @@ class Param(FieldInfo):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
self.deprecated = deprecated
|
||||
if example is not Undefined:
|
||||
if example is not _Unset:
|
||||
warnings.warn(
|
||||
"`example` has been depreacated, please use `examples` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=1,
|
||||
stacklevel=4,
|
||||
)
|
||||
self.example = example
|
||||
self.include_in_schema = include_in_schema
|
||||
extra_kwargs = {**extra}
|
||||
if examples:
|
||||
extra_kwargs["examples"] = examples
|
||||
super().__init__(
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
|
|
@ -65,9 +87,40 @@ class Param(FieldInfo):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
regex=regex,
|
||||
**extra_kwargs,
|
||||
discriminator=discriminator,
|
||||
multiple_of=multiple_of,
|
||||
allow_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
**extra,
|
||||
)
|
||||
if examples is not None:
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
warnings.warn(
|
||||
"`regex` has been depreacated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
current_json_schema_extra = json_schema_extra or extra
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
"alias_priority": alias_priority,
|
||||
"validation_alias": validation_alias,
|
||||
"serialization_alias": serialization_alias,
|
||||
"strict": strict,
|
||||
"json_schema_extra": current_json_schema_extra,
|
||||
}
|
||||
)
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
kwargs["regex"] = pattern or regex
|
||||
kwargs.update(**current_json_schema_extra)
|
||||
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
|
||||
|
||||
super().__init__(**use_kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
|
|
@ -80,7 +133,14 @@ class Path(Param):
|
|||
self,
|
||||
default: Any = ...,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -89,7 +149,19 @@ class Path(Param):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -97,16 +169,22 @@ class Path(Param):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
assert default is ..., "Path parameters cannot have a default value"
|
||||
self.in_ = self.in_
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -115,11 +193,19 @@ class Path(Param):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -131,7 +217,14 @@ class Query(Param):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -140,7 +233,19 @@ class Query(Param):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -148,14 +253,20 @@ class Query(Param):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -164,11 +275,19 @@ class Query(Param):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -180,7 +299,14 @@ class Header(Param):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
convert_underscores: bool = True,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
|
|
@ -190,7 +316,19 @@ class Header(Param):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -198,15 +336,21 @@ class Header(Param):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
self.convert_underscores = convert_underscores
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -215,11 +359,19 @@ class Header(Param):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -231,7 +383,14 @@ class Cookie(Param):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -240,7 +399,19 @@ class Cookie(Param):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -248,14 +419,20 @@ class Cookie(Param):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -264,11 +441,19 @@ class Cookie(Param):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -278,9 +463,16 @@ class Body(FieldInfo):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
embed: bool = False,
|
||||
media_type: str = "application/json",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -289,7 +481,19 @@ class Body(FieldInfo):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -297,23 +501,26 @@ class Body(FieldInfo):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
self.embed = embed
|
||||
self.media_type = media_type
|
||||
if example is not Undefined:
|
||||
self.deprecated = deprecated
|
||||
if example is not _Unset:
|
||||
warnings.warn(
|
||||
"`example` has been depreacated, please use `examples` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=1,
|
||||
stacklevel=4,
|
||||
)
|
||||
self.example = example
|
||||
extra_kwargs = {**extra}
|
||||
if examples is not None:
|
||||
extra_kwargs["examples"] = examples
|
||||
super().__init__(
|
||||
self.include_in_schema = include_in_schema
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
|
|
@ -323,9 +530,41 @@ class Body(FieldInfo):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
regex=regex,
|
||||
**extra_kwargs,
|
||||
discriminator=discriminator,
|
||||
multiple_of=multiple_of,
|
||||
allow_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
**extra,
|
||||
)
|
||||
if examples is not None:
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
warnings.warn(
|
||||
"`regex` has been depreacated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
current_json_schema_extra = json_schema_extra or extra
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
"alias_priority": alias_priority,
|
||||
"validation_alias": validation_alias,
|
||||
"serialization_alias": serialization_alias,
|
||||
"strict": strict,
|
||||
"json_schema_extra": current_json_schema_extra,
|
||||
}
|
||||
)
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
kwargs["regex"] = pattern or regex
|
||||
kwargs.update(**current_json_schema_extra)
|
||||
|
||||
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
|
||||
|
||||
super().__init__(**use_kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
|
|
@ -336,8 +575,15 @@ class Form(Body):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -346,7 +592,19 @@ class Form(Body):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -354,14 +612,22 @@ class Form(Body):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
embed=True,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -370,9 +636,19 @@ class Form(Body):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
@ -382,8 +658,15 @@ class File(Form):
|
|||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
|
|
@ -392,7 +675,19 @@ class File(Form):
|
|||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
regex: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -400,13 +695,21 @@ class File(Form):
|
|||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = Undefined,
|
||||
] = _Unset,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
|
|
@ -415,9 +718,19 @@ class File(Form):
|
|||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ from typing import (
|
|||
)
|
||||
|
||||
from fastapi import params
|
||||
from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
_get_model_config,
|
||||
_model_dump,
|
||||
_normalize_errors,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import (
|
||||
|
|
@ -29,13 +37,14 @@ from fastapi.dependencies.utils import (
|
|||
get_typed_return_annotation,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import (
|
||||
FastAPIError,
|
||||
RequestValidationError,
|
||||
ResponseValidationError,
|
||||
WebSocketRequestValidationError,
|
||||
)
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_response_field,
|
||||
|
|
@ -44,9 +53,6 @@ from fastapi.utils import (
|
|||
is_body_allowed_for_status_code,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import ModelField, Undefined
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette import routing
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.exceptions import HTTPException
|
||||
|
|
@ -73,14 +79,15 @@ def _prepare_response_content(
|
|||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
if isinstance(res, BaseModel):
|
||||
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
|
||||
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
|
||||
if read_with_orm_mode:
|
||||
# Let from_orm extract the data from this model instead of converting
|
||||
# it now to a dict.
|
||||
# Otherwise there's no way to extract lazy data that requires attribute
|
||||
# access instead of dict iteration, e.g. lazy relationships.
|
||||
return res
|
||||
return res.dict(
|
||||
return _model_dump(
|
||||
res,
|
||||
by_alias=True,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
|
|
@ -115,8 +122,8 @@ async def serialize_response(
|
|||
*,
|
||||
field: Optional[ModelField] = None,
|
||||
response_content: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
|
|
@ -125,24 +132,40 @@ async def serialize_response(
|
|||
) -> Any:
|
||||
if field:
|
||||
errors = []
|
||||
response_content = _prepare_response_content(
|
||||
response_content,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
if not hasattr(field, "serialize"):
|
||||
# pydantic v1
|
||||
response_content = _prepare_response_content(
|
||||
response_content,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
if is_coroutine:
|
||||
value, errors_ = field.validate(response_content, {}, loc=("response",))
|
||||
else:
|
||||
value, errors_ = await run_in_threadpool(
|
||||
field.validate, response_content, {}, loc=("response",)
|
||||
)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors, field.type_)
|
||||
raise ResponseValidationError(
|
||||
errors=_normalize_errors(errors), body=response_content
|
||||
)
|
||||
|
||||
if hasattr(field, "serialize"):
|
||||
return field.serialize(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
|
|
@ -175,8 +198,8 @@ def get_request_handler(
|
|||
status_code: Optional[int] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
|
||||
response_field: Optional[ModelField] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -220,7 +243,16 @@ def get_request_handler(
|
|||
body = body_bytes
|
||||
except json.JSONDecodeError as e:
|
||||
raise RequestValidationError(
|
||||
[ErrorWrapper(e, ("body", e.pos))], body=e.doc
|
||||
[
|
||||
{
|
||||
"type": "json_invalid",
|
||||
"loc": ("body", e.pos),
|
||||
"msg": "JSON decode error",
|
||||
"input": {},
|
||||
"ctx": {"error": e.msg},
|
||||
}
|
||||
],
|
||||
body=e.doc,
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -236,7 +268,7 @@ def get_request_handler(
|
|||
)
|
||||
values, errors, background_tasks, sub_response, _ = solved_result
|
||||
if errors:
|
||||
raise RequestValidationError(errors, body=body)
|
||||
raise RequestValidationError(_normalize_errors(errors), body=body)
|
||||
else:
|
||||
raw_response = await run_endpoint_function(
|
||||
dependant=dependant, values=values, is_coroutine=is_coroutine
|
||||
|
|
@ -287,7 +319,7 @@ def get_websocket_app(
|
|||
)
|
||||
values, errors, _, _2, _3 = solved_result
|
||||
if errors:
|
||||
raise WebSocketRequestValidationError(errors)
|
||||
raise WebSocketRequestValidationError(_normalize_errors(errors))
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
await dependant.call(**values)
|
||||
|
||||
|
|
@ -348,8 +380,8 @@ class APIRoute(routing.Route):
|
|||
name: Optional[str] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -414,7 +446,11 @@ class APIRoute(routing.Route):
|
|||
), f"Status code {status_code} must not have a response body"
|
||||
response_name = "Response_" + self.unique_id
|
||||
self.response_field = create_response_field(
|
||||
name=response_name, type_=self.response_model
|
||||
name=response_name,
|
||||
type_=self.response_model,
|
||||
# TODO: This should actually set mode='serialization', just, that changes the schemas
|
||||
# mode="serialization",
|
||||
mode="validation",
|
||||
)
|
||||
# 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
|
||||
|
|
@ -423,6 +459,7 @@ class APIRoute(routing.Route):
|
|||
# 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 be always created.
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
self.secure_cloned_response_field: Optional[
|
||||
ModelField
|
||||
] = create_cloned_field(self.response_field)
|
||||
|
|
@ -569,8 +606,8 @@ class APIRouter(routing.Router):
|
|||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -650,8 +687,8 @@ class APIRouter(routing.Router):
|
|||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -877,8 +914,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -933,8 +970,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -989,8 +1026,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -1045,8 +1082,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -1101,8 +1138,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -1157,8 +1194,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -1213,8 +1250,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
@ -1269,8 +1306,8 @@ class APIRouter(routing.Router):
|
|||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ from fastapi.security.utils import get_authorization_scheme_param
|
|||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
|
||||
# TODO: import from typing when deprecating Python 3.9
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
class OAuth2PasswordRequestForm:
|
||||
"""
|
||||
|
|
@ -45,12 +48,13 @@ class OAuth2PasswordRequestForm:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(default=None, regex="password"),
|
||||
username: str = Form(),
|
||||
password: str = Form(),
|
||||
scope: str = Form(default=""),
|
||||
client_id: Optional[str] = Form(default=None),
|
||||
client_secret: Optional[str] = Form(default=None),
|
||||
*,
|
||||
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
|
||||
username: Annotated[str, Form()],
|
||||
password: Annotated[str, Form()],
|
||||
scope: Annotated[str, Form()] = "",
|
||||
client_id: Annotated[Union[str, None], Form()] = None,
|
||||
client_secret: Annotated[Union[str, None], Form()] = None,
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
|
|
@ -95,12 +99,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(regex="password"),
|
||||
username: str = Form(),
|
||||
password: str = Form(),
|
||||
scope: str = Form(default=""),
|
||||
client_id: Optional[str] = Form(default=None),
|
||||
client_secret: Optional[str] = Form(default=None),
|
||||
grant_type: Annotated[str, Form(pattern="password")],
|
||||
username: Annotated[str, Form()],
|
||||
password: Annotated[str, Form()],
|
||||
scope: Annotated[str, Form()] = "",
|
||||
client_id: Annotated[Union[str, None], Form()] = None,
|
||||
client_secret: Annotated[Union[str, None], Form()] = None,
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
from typing import Any, Callable, TypeVar
|
||||
import types
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
|
||||
UnionType = getattr(types, "UnionType", Union)
|
||||
NoneType = getattr(types, "UnionType", None)
|
||||
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
|
||||
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
|
||||
|
|
|
|||
126
fastapi/utils.py
126
fastapi/utils.py
|
|
@ -1,7 +1,6 @@
|
|||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
|
@ -16,13 +15,20 @@ from typing import (
|
|||
from weakref import WeakKeyDictionary
|
||||
|
||||
import fastapi
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
BaseConfig,
|
||||
ModelField,
|
||||
PydanticSchemaGenerationError,
|
||||
Undefined,
|
||||
UndefinedType,
|
||||
Validator,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseConfig, BaseModel, create_model
|
||||
from pydantic.class_validators import Validator
|
||||
from pydantic.fields import FieldInfo, ModelField, UndefinedType
|
||||
from pydantic.schema import model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.fields import FieldInfo
|
||||
from typing_extensions import Literal
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
|
|
@ -50,24 +56,6 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
|
|||
return not (current_status_code < 200 or current_status_code in {204, 304})
|
||||
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict[str, Any]] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
model_name = model_name_map[model]
|
||||
if "description" in m_schema:
|
||||
m_schema["description"] = m_schema["description"].split("\f")[0]
|
||||
definitions[model_name] = m_schema
|
||||
return definitions
|
||||
|
||||
|
||||
def get_path_param_names(path: str) -> Set[str]:
|
||||
return set(re.findall("{(.*?)}", path))
|
||||
|
||||
|
|
@ -76,30 +64,40 @@ def create_response_field(
|
|||
name: str,
|
||||
type_: Type[Any],
|
||||
class_validators: Optional[Dict[str, Validator]] = None,
|
||||
default: Optional[Any] = None,
|
||||
required: Union[bool, UndefinedType] = True,
|
||||
default: Optional[Any] = Undefined,
|
||||
required: Union[bool, UndefinedType] = Undefined,
|
||||
model_config: Type[BaseConfig] = BaseConfig,
|
||||
field_info: Optional[FieldInfo] = None,
|
||||
alias: Optional[str] = None,
|
||||
mode: Literal["validation", "serialization"] = "validation",
|
||||
) -> ModelField:
|
||||
"""
|
||||
Create a new response field. Raises if type_ is invalid.
|
||||
"""
|
||||
class_validators = class_validators or {}
|
||||
field_info = field_info or FieldInfo()
|
||||
|
||||
try:
|
||||
return ModelField(
|
||||
name=name,
|
||||
type_=type_,
|
||||
class_validators=class_validators,
|
||||
default=default,
|
||||
required=required,
|
||||
model_config=model_config,
|
||||
alias=alias,
|
||||
field_info=field_info,
|
||||
if PYDANTIC_V2:
|
||||
field_info = field_info or FieldInfo(
|
||||
annotation=type_, default=default, alias=alias
|
||||
)
|
||||
except RuntimeError:
|
||||
else:
|
||||
field_info = field_info or FieldInfo()
|
||||
kwargs = {"name": name, "field_info": field_info}
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update({"mode": mode})
|
||||
else:
|
||||
kwargs.update(
|
||||
{
|
||||
"type_": type_,
|
||||
"class_validators": class_validators,
|
||||
"default": default,
|
||||
"required": required,
|
||||
"model_config": model_config,
|
||||
"alias": alias,
|
||||
}
|
||||
)
|
||||
try:
|
||||
return ModelField(**kwargs) # type: ignore[arg-type]
|
||||
except (RuntimeError, PydanticSchemaGenerationError):
|
||||
raise fastapi.exceptions.FastAPIError(
|
||||
"Invalid args for response field! Hint: "
|
||||
f"check that {type_} is a valid Pydantic field type. "
|
||||
|
|
@ -116,6 +114,8 @@ def create_cloned_field(
|
|||
*,
|
||||
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
|
||||
) -> ModelField:
|
||||
if PYDANTIC_V2:
|
||||
return field
|
||||
# cloned_types caches already cloned types to support recursive models and improve
|
||||
# performance by avoiding unecessary cloning
|
||||
if cloned_types is None:
|
||||
|
|
@ -136,30 +136,30 @@ def create_cloned_field(
|
|||
f, cloned_types=cloned_types
|
||||
)
|
||||
new_field = create_response_field(name=field.name, type_=use_type)
|
||||
new_field.has_alias = field.has_alias
|
||||
new_field.alias = field.alias
|
||||
new_field.class_validators = field.class_validators
|
||||
new_field.default = field.default
|
||||
new_field.required = field.required
|
||||
new_field.model_config = field.model_config
|
||||
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
|
||||
new_field.alias = field.alias # type: ignore[misc]
|
||||
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
|
||||
new_field.default = field.default # type: ignore[misc]
|
||||
new_field.required = field.required # type: ignore[misc]
|
||||
new_field.model_config = field.model_config # type: ignore[attr-defined]
|
||||
new_field.field_info = field.field_info
|
||||
new_field.allow_none = field.allow_none
|
||||
new_field.validate_always = field.validate_always
|
||||
if field.sub_fields:
|
||||
new_field.sub_fields = [
|
||||
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
|
||||
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
new_field.sub_fields = [ # type: ignore[attr-defined]
|
||||
create_cloned_field(sub_field, cloned_types=cloned_types)
|
||||
for sub_field in field.sub_fields
|
||||
for sub_field in field.sub_fields # type: ignore[attr-defined]
|
||||
]
|
||||
if field.key_field:
|
||||
new_field.key_field = create_cloned_field(
|
||||
field.key_field, cloned_types=cloned_types
|
||||
if field.key_field: # type: ignore[attr-defined]
|
||||
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
|
||||
field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
|
||||
)
|
||||
new_field.validators = field.validators
|
||||
new_field.pre_validators = field.pre_validators
|
||||
new_field.post_validators = field.post_validators
|
||||
new_field.parse_json = field.parse_json
|
||||
new_field.shape = field.shape
|
||||
new_field.populate_validators()
|
||||
new_field.validators = field.validators # type: ignore[attr-defined]
|
||||
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
|
||||
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
|
||||
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
|
||||
new_field.shape = field.shape # type: ignore[attr-defined]
|
||||
new_field.populate_validators() # type: ignore[attr-defined]
|
||||
return new_field
|
||||
|
||||
|
||||
|
|
@ -220,3 +220,9 @@ def get_value_or_default(
|
|||
if not isinstance(item, DefaultPlaceholder):
|
||||
return item
|
||||
return first_item
|
||||
|
||||
|
||||
def match_pydantic_error_url(error_type: str) -> Any:
|
||||
from dirty_equals import IsStr
|
||||
|
||||
return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ classifiers = [
|
|||
]
|
||||
dependencies = [
|
||||
"starlette>=0.27.0,<0.28.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
|
||||
"typing-extensions>=4.5.0"
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0",
|
||||
"typing-extensions>=4.5.0",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
|
|
@ -61,8 +61,10 @@ all = [
|
|||
"pyyaml >=5.3.1",
|
||||
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
|
||||
"orjson >=3.2.1",
|
||||
"email_validator >=1.1.1",
|
||||
"email_validator >=2.0.0",
|
||||
"uvicorn[standard] >=0.12.0",
|
||||
"pydantic-settings >=2.0.0",
|
||||
"pydantic-extra-types >=2.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
|
|
@ -85,6 +87,7 @@ check_untyped_defs = true
|
|||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
|
|
@ -142,6 +145,7 @@ ignore = [
|
|||
"docs_src/custom_response/tutorial007.py" = ["B007"]
|
||||
"docs_src/dataclasses/tutorial003.py" = ["I001"]
|
||||
"docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
|
||||
"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
|
||||
"docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
|
||||
"docs_src/dependencies/tutorial008_an.py" = ["F821"]
|
||||
"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
-e .
|
||||
pydantic-settings >=2.0.0
|
||||
pytest >=7.1.3,<8.0.0
|
||||
coverage[toml] >= 6.5.0,< 8.0
|
||||
mypy ==1.4.0
|
||||
ruff ==0.0.275
|
||||
black == 23.3.0
|
||||
httpx >=0.23.0,<0.25.0
|
||||
email_validator >=1.1.1,<2.0.0
|
||||
email_validator >=1.1.1,<3.0.0
|
||||
dirty-equals ==0.6.0
|
||||
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
|
||||
# probably when including SQLModel
|
||||
sqlalchemy >=1.3.18,<1.4.43
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
from typing import Union
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class FooBaseModel(BaseModel):
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class Foo(FooBaseModel):
|
||||
|
|
@ -52,7 +58,19 @@ def test_openapi_schema():
|
|||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Foo"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Foo"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Foo",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Foo"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
|
@ -42,13 +43,24 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
),
|
||||
"name": "callback_url",
|
||||
"in": "query",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from typing_extensions import Annotated
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -30,21 +32,46 @@ client = TestClient(app)
|
|||
|
||||
foo_is_missing = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
IsDict(
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
foo_is_short = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "ensure this value has at least 1 characters",
|
||||
"type": "value_error.any_str.min_length",
|
||||
}
|
||||
IsDict(
|
||||
{
|
||||
"ctx": {"min_length": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "String should have at least 1 characters",
|
||||
"type": "string_too_short",
|
||||
"input": "",
|
||||
"url": match_pydantic_error_url("string_too_short"),
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"ctx": {"limit_value": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "ensure this value has at least 1 characters",
|
||||
"type": "value_error.any_str.min_length",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
|
|
@ -266,10 +267,17 @@ def test_openapi_schema():
|
|||
"operationId": "get_path_param_id_path_param__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
"required": True,
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Item Id",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Item Id", "type": "string"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -969,10 +977,17 @@ def test_openapi_schema():
|
|||
"operationId": "get_query_type_optional_query_int_optional_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Query", "type": "integer"},
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
"required": False,
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"title": "Query",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Query", "type": "integer"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
from typing import List, Union
|
||||
|
||||
from fastapi import FastAPI, UploadFile
|
||||
from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
_get_model_config,
|
||||
is_bytes_sequence_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
)
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseConfig, BaseModel, ConfigDict
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_field_default_required():
|
||||
# For coverage
|
||||
field_info = FieldInfo(annotation=str)
|
||||
field = ModelField(name="foo", field_info=field_info)
|
||||
assert field.default is Undefined
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_upload_file_dummy_general_plain_validator_function():
|
||||
# For coverage
|
||||
assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_union_scalar_list():
|
||||
# For coverage
|
||||
# TODO: there might not be a current valid code path that uses this, it would
|
||||
# potentially enable query parameters defined as both a scalar and a list
|
||||
# but that would require more refactors, also not sure it's really useful
|
||||
from fastapi._compat import is_pv1_scalar_field
|
||||
|
||||
field_info = FieldInfo()
|
||||
field = ModelField(
|
||||
name="foo",
|
||||
field_info=field_info,
|
||||
type_=Union[str, List[int]],
|
||||
class_validators={},
|
||||
model_config=BaseConfig,
|
||||
)
|
||||
assert not is_pv1_scalar_field(field)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_get_model_config():
|
||||
# For coverage in Pydantic v2
|
||||
class Foo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
foo = Foo()
|
||||
config = _get_model_config(foo)
|
||||
assert config == {"from_attributes": True}
|
||||
|
||||
|
||||
def test_complex():
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/")
|
||||
def foo(foo: Union[str, List[int]]):
|
||||
return foo
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/", json="bar")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == "bar"
|
||||
|
||||
response2 = client.post("/", json=[1, 2])
|
||||
assert response2.status_code == 200, response2.text
|
||||
assert response2.json() == [1, 2]
|
||||
|
||||
|
||||
def test_is_bytes_sequence_annotation_union():
|
||||
# For coverage
|
||||
# TODO: in theory this would allow declaring types that could be lists of bytes
|
||||
# to be read from files and other types, but I'm not even sure it's a good idea
|
||||
# to support it as a first class "feature"
|
||||
assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
|
||||
|
||||
|
||||
def test_is_uploadfile_sequence_annotation():
|
||||
# For coverage
|
||||
# TODO: in theory this would allow declaring types that could be lists of UploadFile
|
||||
# and other types, but I'm not even sure it's a good idea to support it as a first
|
||||
# class "feature"
|
||||
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -8,10 +9,18 @@ app = FastAPI()
|
|||
class Item(BaseModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
if PYDANTIC_V2:
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/foo", response_model=Item)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,17 @@ from fastapi.datastructures import Default
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
def test_upload_file_invalid():
|
||||
with pytest.raises(ValueError):
|
||||
UploadFile.validate("not a Starlette UploadFile")
|
||||
|
||||
|
||||
def test_upload_file_invalid_pydantic_v2():
|
||||
with pytest.raises(ValueError):
|
||||
UploadFile._validate("not a Starlette UploadFile", {})
|
||||
|
||||
|
||||
def test_default_placeholder_equals():
|
||||
placeholder_1 = Default("a")
|
||||
placeholder_2 = Default("a")
|
||||
|
|
|
|||
|
|
@ -4,31 +4,54 @@ from fastapi import FastAPI
|
|||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
@field_serializer("dt_field")
|
||||
def serialize_datetime(self, dt_field: datetime):
|
||||
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
client = TestClient(app)
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
|
||||
def test_dt():
|
||||
client = TestClient(app)
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_pydanticv1():
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
client = TestClient(app)
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -47,15 +49,30 @@ async def no_duplicates_sub(
|
|||
def test_no_duplicates_invalid():
|
||||
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_no_duplicates():
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
|
|||
return msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "decorator-depends"},
|
||||
),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-decorator-depends"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_normal_app(url, status_code, expected):
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
def test_main_depends():
|
||||
response = client.get("/main-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_main_depends_q_foo():
|
||||
response = client.get("/main-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "main-depends",
|
||||
"params": {"q": "foo", "skip": 0, "limit": 100},
|
||||
}
|
||||
|
||||
|
||||
def test_main_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/main-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "main-depends",
|
||||
"params": {"q": "foo", "skip": 100, "limit": 200},
|
||||
}
|
||||
|
||||
|
||||
def test_decorator_depends():
|
||||
response = client.get("/decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_decorator_depends_q_foo():
|
||||
response = client.get("/decorator-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
|
||||
|
||||
def test_decorator_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
|
||||
|
||||
def test_router_depends():
|
||||
response = client.get("/router-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_router_depends_q_foo():
|
||||
response = client.get("/router-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "router-depends",
|
||||
"params": {"q": "foo", "skip": 0, "limit": 100},
|
||||
}
|
||||
|
||||
|
||||
def test_router_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/router-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "router-depends",
|
||||
"params": {"q": "foo", "skip": 100, "limit": 200},
|
||||
}
|
||||
|
||||
|
||||
def test_router_decorator_depends():
|
||||
response = client.get("/router-decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_router_decorator_depends_q_foo():
|
||||
response = client.get("/router-decorator-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
|
||||
|
||||
def test_router_decorator_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected):
|
|||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?k=bar",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"k": "bar"}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
|
||||
],
|
||||
)
|
||||
def test_override_with_sub(url, status_code, expected):
|
||||
def test_override_with_sub_main_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
response = client.get("/main-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub__main_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/main-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_main_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/main-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
app.dependency_overrides = {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
|
|
@ -327,7 +328,14 @@ def test_openapi_schema():
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Price", "type": "number"}),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model_b: ModelB
|
||||
|
||||
@validator("name")
|
||||
def lower_username(cls, name: str, values):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "model_b": model_c}
|
||||
|
|
@ -1,46 +1,20 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
app = FastAPI()
|
||||
from ..utils import needs_pydanticv1
|
||||
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from .app_pv1 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model_b: ModelB
|
||||
|
||||
@validator("name")
|
||||
def lower_username(cls, name: str, values):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "model_b": model_c}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_sub_model():
|
||||
@needs_pydanticv1
|
||||
def test_filter_sub_model(client: TestClient):
|
||||
response = client.get("/model/modelA")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
|
|
@ -50,8 +24,9 @@ def test_filter_sub_model():
|
|||
}
|
||||
|
||||
|
||||
def test_validator_is_cloned():
|
||||
with pytest.raises(ValidationError) as err:
|
||||
@needs_pydanticv1
|
||||
def test_validator_is_cloned(client: TestClient):
|
||||
with pytest.raises(ResponseValidationError) as err:
|
||||
client.get("/model/modelX")
|
||||
assert err.value.errors() == [
|
||||
{
|
||||
|
|
@ -62,7 +37,8 @@ def test_validator_is_cloned():
|
|||
]
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@needs_pydanticv1
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from .utils import needs_pydanticv2
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from pydantic import BaseModel, FieldValidationInfo, field_validator
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
foo: ModelB
|
||||
|
||||
@field_validator("name")
|
||||
def lower_username(cls, name: str, info: FieldValidationInfo):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "foo": model_c}
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_filter_sub_model(client: TestClient):
|
||||
response = client.get("/model/modelA")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"name": "modelA",
|
||||
"description": "model-a-desc",
|
||||
"foo": {"username": "test-user"},
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_validator_is_cloned(client: TestClient):
|
||||
with pytest.raises(ResponseValidationError) as err:
|
||||
client.get("/model/modelX")
|
||||
assert err.value.errors() == [
|
||||
IsDict(
|
||||
{
|
||||
"type": "value_error",
|
||||
"loc": ("response", "name"),
|
||||
"msg": "Value error, name must end in A",
|
||||
"input": "modelX",
|
||||
"ctx": {"error": "name must end in A"},
|
||||
"url": match_pydantic_error_url("value_error"),
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ("response", "name"),
|
||||
"msg": "name must end in A",
|
||||
"type": "value_error",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/model/{name}": {
|
||||
"get": {
|
||||
"summary": "Get Model A",
|
||||
"operationId": "get_model_a_model__name__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Name", "type": "string"},
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/ModelA"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"ModelA": {
|
||||
"title": "ModelA",
|
||||
"required": ["name", "foo"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
|
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
IsDict({"title": "Description", "type": "string"}),
|
||||
"foo": {"$ref": "#/components/schemas/ModelB"},
|
||||
},
|
||||
},
|
||||
"ModelB": {
|
||||
"title": "ModelB",
|
||||
"required": ["username"],
|
||||
"type": "object",
|
||||
"properties": {"username": {"title": "Username", "type": "string"}},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
|
@ -104,35 +105,253 @@ def test_get_users_item():
|
|||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
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": {}}},
|
||||
},
|
||||
"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": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
}
|
||||
],
|
||||
"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": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
},
|
||||
],
|
||||
"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": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
}
|
||||
],
|
||||
"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": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
|
||||
|
||||
|
||||
def test_schema_2():
|
||||
"""Check that the user_id is an optional query parameter under /items"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": False,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/items/"]["get"]["parameters"]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from fastapi import FastAPI
|
|||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class MyUuid:
|
||||
|
|
@ -26,40 +26,78 @@ class MyUuid:
|
|||
raise TypeError("vars() argument must have __dict__ attribute")
|
||||
|
||||
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
# I don't want to import asyncpg for this test so I made my own UUID
|
||||
# Import asyncpg and uncomment the two lines below for the actual bug
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
# from asyncpg.pgproto import pgproto
|
||||
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
app = FastAPI()
|
||||
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
a_uuid: MyUuid
|
||||
|
||||
a_uuid: MyUuid
|
||||
@field_serializer("a_uuid")
|
||||
def serialize_a_uuid(self, v):
|
||||
return str(v)
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
client = TestClient(app)
|
||||
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
||||
assert response_simple.json() == {
|
||||
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
|
||||
}
|
||||
|
||||
assert response_pydantic.json() == {
|
||||
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
|
||||
}
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_pydanticv1():
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
|
||||
a_uuid: MyUuid
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_dt():
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import PurePath, PurePosixPath, PureWindowsPath
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel, Field, ValidationError, create_model
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class Person:
|
||||
|
|
@ -46,22 +50,6 @@ class Unserializable:
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
class Config:
|
||||
pass
|
||||
|
||||
|
||||
class RoleEnum(Enum):
|
||||
admin = "admin"
|
||||
normal = "normal"
|
||||
|
|
@ -70,8 +58,12 @@ class RoleEnum(Enum):
|
|||
class ModelWithConfig(BaseModel):
|
||||
role: Optional[RoleEnum] = None
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"use_enum_values": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ModelWithAlias(BaseModel):
|
||||
|
|
@ -84,23 +76,6 @@ class ModelWithDefault(BaseModel):
|
|||
bla: str = "bla"
|
||||
|
||||
|
||||
class ModelWithRoot(BaseModel):
|
||||
__root__: str
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
|
||||
)
|
||||
def fixture_model_with_path(request):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
ModelWithPath = create_model(
|
||||
"ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
|
||||
)
|
||||
return ModelWithPath(path=request.param("/foo", "bar"))
|
||||
|
||||
|
||||
def test_encode_dict():
|
||||
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
|
|
@ -154,14 +129,47 @@ def test_encode_unsupported():
|
|||
jsonable_encoder(unserializable)
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model():
|
||||
@needs_pydanticv2
|
||||
def test_encode_custom_json_encoders_model_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
@field_serializer("dt_field")
|
||||
def serialize_dt_field(self, dt):
|
||||
return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
pass
|
||||
|
||||
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model_subclass():
|
||||
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_encode_custom_json_encoders_model_pydanticv1():
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
class Config:
|
||||
pass
|
||||
|
||||
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
def test_encode_model_with_config():
|
||||
|
|
@ -197,6 +205,7 @@ def test_encode_model_with_default():
|
|||
}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_custom_encoders():
|
||||
class safe_datetime(datetime):
|
||||
pass
|
||||
|
|
@ -227,19 +236,72 @@ def test_custom_enum_encoders():
|
|||
assert encoded_instance == custom_enum_encoder(instance)
|
||||
|
||||
|
||||
def test_encode_model_with_path(model_with_path):
|
||||
if isinstance(model_with_path.path, PureWindowsPath):
|
||||
expected = "\\foo\\bar"
|
||||
else:
|
||||
expected = "/foo/bar"
|
||||
assert jsonable_encoder(model_with_path) == {"path": expected}
|
||||
def test_encode_model_with_pure_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PurePath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PurePath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
|
||||
|
||||
|
||||
def test_encode_model_with_pure_posix_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PurePosixPath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
|
||||
|
||||
|
||||
def test_encode_model_with_pure_windows_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PureWindowsPath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_encode_root():
|
||||
class ModelWithRoot(BaseModel):
|
||||
__root__: str
|
||||
|
||||
model = ModelWithRoot(__root__="Foo")
|
||||
assert jsonable_encoder(model) == "Foo"
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_decimal_encoder_float():
|
||||
data = {"value": Decimal(1.23)}
|
||||
assert jsonable_encoder(data) == {"value": 1.23}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_decimal_encoder_int():
|
||||
data = {"value": Decimal(2)}
|
||||
assert jsonable_encoder(data) == {"value": 2}
|
||||
|
||||
|
||||
def test_encode_deque_encodes_child_models():
|
||||
class Model(BaseModel):
|
||||
test: str
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from decimal import Decimal
|
||||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel, condecimal
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]):
|
|||
client = TestClient(app)
|
||||
|
||||
|
||||
single_error = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0.0},
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_put_correct_body():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
|
||||
assert response.json() == {
|
||||
"item": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"age": IsOneOf(
|
||||
5,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"5",
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_jsonable_encoder_requiring_error():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == single_error
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -1.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0.0},
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_incorrect_body_multiple():
|
||||
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == multiple_errors
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "Field required",
|
||||
"input": {"age": "five"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "decimal_parsing",
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "Input should be a valid decimal",
|
||||
"input": "five",
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "Field required",
|
||||
"input": {"age": "six"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "decimal_parsing",
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "Input should be a valid decimal",
|
||||
"input": "six",
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
@ -126,11 +184,23 @@ def test_openapi_schema():
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {
|
||||
"title": "Age",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
},
|
||||
"age": IsDict(
|
||||
{
|
||||
"title": "Age",
|
||||
"anyOf": [
|
||||
{"exclusiveMinimum": 0.0, "type": "number"},
|
||||
{"type": "string"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Age",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)):
|
|||
client = TestClient(app)
|
||||
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_multi_query():
|
||||
response = client.get("/items/?q=5&q=6")
|
||||
assert response.status_code == 200, response.text
|
||||
|
|
@ -39,7 +25,42 @@ def test_multi_query():
|
|||
def test_multi_query_incorrect():
|
||||
response = client.get("/items/?q=five&q=six")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == multiple_errors
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "five",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
},
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "six",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
|
@ -52,11 +53,21 @@ def test_openapi():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Standard Query Param",
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"default": 50,
|
||||
"title": "Standard Query Param",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Standard Query Param",
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
}
|
||||
),
|
||||
"name": "standard_query_param",
|
||||
"in": "query",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from dirty_equals import IsOneOf
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
|
@ -35,10 +36,20 @@ def test_openapi_schema():
|
|||
"servers": [
|
||||
{"url": "/", "description": "Default, relative server"},
|
||||
{
|
||||
"url": "http://staging.localhost.tiangolo.com:8000",
|
||||
"url": IsOneOf(
|
||||
"http://staging.localhost.tiangolo.com:8000/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"http://staging.localhost.tiangolo.com:8000",
|
||||
),
|
||||
"description": "Staging but actually localhost still",
|
||||
},
|
||||
{"url": "https://prod.example.com"},
|
||||
{
|
||||
"url": IsOneOf(
|
||||
"https://prod.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://prod.example.com",
|
||||
)
|
||||
},
|
||||
],
|
||||
"paths": {
|
||||
"/foo": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Any, List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
|
||||
|
||||
test_data: List[Any] = ["teststr", None, ..., 1, []]
|
||||
|
|
@ -10,34 +10,137 @@ def get_user():
|
|||
return {} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", params=test_data)
|
||||
def params(request):
|
||||
return request.param
|
||||
def test_param_repr_str():
|
||||
assert repr(Param("teststr")) == "Param(teststr)"
|
||||
|
||||
|
||||
def test_param_repr(params):
|
||||
assert repr(Param(params)) == "Param(" + str(params) + ")"
|
||||
def test_param_repr_none():
|
||||
assert repr(Param(None)) == "Param(None)"
|
||||
|
||||
|
||||
def test_param_repr_ellipsis():
|
||||
assert repr(Param(...)) == IsOneOf(
|
||||
"Param(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Param(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_param_repr_number():
|
||||
assert repr(Param(1)) == "Param(1)"
|
||||
|
||||
|
||||
def test_param_repr_list():
|
||||
assert repr(Param([])) == "Param([])"
|
||||
|
||||
|
||||
def test_path_repr():
|
||||
assert repr(Path()) == "Path(Ellipsis)"
|
||||
assert repr(Path(...)) == "Path(Ellipsis)"
|
||||
assert repr(Path()) == IsOneOf(
|
||||
"Path(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Path(Ellipsis)",
|
||||
)
|
||||
assert repr(Path(...)) == IsOneOf(
|
||||
"Path(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Path(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_query_repr(params):
|
||||
assert repr(Query(params)) == "Query(" + str(params) + ")"
|
||||
def test_query_repr_str():
|
||||
assert repr(Query("teststr")) == "Query(teststr)"
|
||||
|
||||
|
||||
def test_header_repr(params):
|
||||
assert repr(Header(params)) == "Header(" + str(params) + ")"
|
||||
def test_query_repr_none():
|
||||
assert repr(Query(None)) == "Query(None)"
|
||||
|
||||
|
||||
def test_cookie_repr(params):
|
||||
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
|
||||
def test_query_repr_ellipsis():
|
||||
assert repr(Query(...)) == IsOneOf(
|
||||
"Query(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Query(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_body_repr(params):
|
||||
assert repr(Body(params)) == "Body(" + str(params) + ")"
|
||||
def test_query_repr_number():
|
||||
assert repr(Query(1)) == "Query(1)"
|
||||
|
||||
|
||||
def test_query_repr_list():
|
||||
assert repr(Query([])) == "Query([])"
|
||||
|
||||
|
||||
def test_header_repr_str():
|
||||
assert repr(Header("teststr")) == "Header(teststr)"
|
||||
|
||||
|
||||
def test_header_repr_none():
|
||||
assert repr(Header(None)) == "Header(None)"
|
||||
|
||||
|
||||
def test_header_repr_ellipsis():
|
||||
assert repr(Header(...)) == IsOneOf(
|
||||
"Header(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Header(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_header_repr_number():
|
||||
assert repr(Header(1)) == "Header(1)"
|
||||
|
||||
|
||||
def test_header_repr_list():
|
||||
assert repr(Header([])) == "Header([])"
|
||||
|
||||
|
||||
def test_cookie_repr_str():
|
||||
assert repr(Cookie("teststr")) == "Cookie(teststr)"
|
||||
|
||||
|
||||
def test_cookie_repr_none():
|
||||
assert repr(Cookie(None)) == "Cookie(None)"
|
||||
|
||||
|
||||
def test_cookie_repr_ellipsis():
|
||||
assert repr(Cookie(...)) == IsOneOf(
|
||||
"Cookie(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Cookie(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_cookie_repr_number():
|
||||
assert repr(Cookie(1)) == "Cookie(1)"
|
||||
|
||||
|
||||
def test_cookie_repr_list():
|
||||
assert repr(Cookie([])) == "Cookie([])"
|
||||
|
||||
|
||||
def test_body_repr_str():
|
||||
assert repr(Body("teststr")) == "Body(teststr)"
|
||||
|
||||
|
||||
def test_body_repr_none():
|
||||
assert repr(Body(None)) == "Body(None)"
|
||||
|
||||
|
||||
def test_body_repr_ellipsis():
|
||||
assert repr(Body(...)) == IsOneOf(
|
||||
"Body(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Body(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_body_repr_number():
|
||||
assert repr(Body(1)) == "Body(1)"
|
||||
|
||||
|
||||
def test_body_repr_list():
|
||||
assert repr(Body([])) == "Body([])"
|
||||
|
||||
|
||||
def test_depends_repr():
|
||||
|
|
|
|||
1413
tests/test_path.py
1413
tests/test_path.py
File diff suppressed because it is too large
Load Diff
|
|
@ -1,62 +1,410 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response_missing = {
|
||||
"detail": [
|
||||
|
||||
def test_query():
|
||||
response = client.get("/query")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response_not_valid_int = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/query", 422, response_missing),
|
||||
("/query?query=baz", 200, "foo bar baz"),
|
||||
("/query?not_declared=baz", 422, response_missing),
|
||||
("/query/optional", 200, "foo bar"),
|
||||
("/query/optional?query=baz", 200, "foo bar baz"),
|
||||
("/query/optional?not_declared=baz", 200, "foo bar"),
|
||||
("/query/int", 422, response_missing),
|
||||
("/query/int?query=42", 200, "foo bar 42"),
|
||||
("/query/int?query=42.5", 422, response_not_valid_int),
|
||||
("/query/int?query=baz", 422, response_not_valid_int),
|
||||
("/query/int?not_declared=baz", 422, response_missing),
|
||||
("/query/int/optional", 200, "foo bar"),
|
||||
("/query/int/optional?query=50", 200, "foo bar 50"),
|
||||
("/query/int/optional?query=foo", 422, response_not_valid_int),
|
||||
("/query/int/default", 200, "foo bar 10"),
|
||||
("/query/int/default?query=50", 200, "foo bar 50"),
|
||||
("/query/int/default?query=foo", 422, response_not_valid_int),
|
||||
("/query/param", 200, "foo bar"),
|
||||
("/query/param?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required", 422, response_missing),
|
||||
("/query/param-required?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int", 422, response_missing),
|
||||
("/query/param-required/int?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int?query=foo", 422, response_not_valid_int),
|
||||
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_query_query_baz():
|
||||
response = client.get("/query?query=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar baz"
|
||||
|
||||
|
||||
def test_query_not_declared_baz():
|
||||
response = client.get("/query?not_declared=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_optional():
|
||||
response = client.get("/query/optional")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_optional_query_baz():
|
||||
response = client.get("/query/optional?query=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar baz"
|
||||
|
||||
|
||||
def test_query_optional_not_declared_baz():
|
||||
response = client.get("/query/optional?not_declared=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_int():
|
||||
response = client.get("/query/int")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_query_42():
|
||||
response = client.get("/query/int?query=42")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 42"
|
||||
|
||||
|
||||
def test_query_int_query_42_5():
|
||||
response = client.get("/query/int?query=42.5")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "42.5",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_query_baz():
|
||||
response = client.get("/query/int?query=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "baz",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_not_declared_baz():
|
||||
response = client.get("/query/int?not_declared=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_optional():
|
||||
response = client.get("/query/int/optional")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_int_optional_query_50():
|
||||
response = client.get("/query/int/optional?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_int_optional_query_foo():
|
||||
response = client.get("/query/int/optional?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_default():
|
||||
response = client.get("/query/int/default")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 10"
|
||||
|
||||
|
||||
def test_query_int_default_query_50():
|
||||
response = client.get("/query/int/default?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_int_default_query_foo():
|
||||
response = client.get("/query/int/default?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param():
|
||||
response = client.get("/query/param")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_param_query_50():
|
||||
response = client.get("/query/param?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required():
|
||||
response = client.get("/query/param-required")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param_required_query_50():
|
||||
response = client.get("/query/param-required?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required_int():
|
||||
response = client.get("/query/param-required/int")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param_required_int_query_50():
|
||||
response = client.get("/query/param-required/int?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required_int_query_foo():
|
||||
response = client.get("/query/param-required/int?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_frozenset_query_1_query_1_query_2():
|
||||
response = client.get("/query/frozenset/?query=1&query=1&query=2")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "1,2"
|
||||
|
|
|
|||
|
|
@ -2,48 +2,83 @@ from typing import Any
|
|||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
read_with_orm_mode = True
|
||||
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.from_orm(person)
|
||||
return db_person
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_read_with_orm_mode() -> None:
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.model_validate(person)
|
||||
return db_person
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
person_data = {"name": "Dive", "lastname": "Wilson"}
|
||||
response = client.post("/people/", json=person_data)
|
||||
data = response.json()
|
||||
assert response.status_code == 200, response.text
|
||||
assert data["name"] == person_data["name"]
|
||||
assert data["lastname"] == person_data["lastname"]
|
||||
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_read_with_orm_mode_pv1() -> None:
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
read_with_orm_mode = True
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.from_orm(person)
|
||||
return db_person
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
person_data = {"name": "Dive", "lastname": "Wilson"}
|
||||
response = client.post("/people/", json=person_data)
|
||||
data = response.json()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from .utils import needs_py310
|
||||
|
||||
|
||||
def get_client():
|
||||
app = FastAPI()
|
||||
with pytest.warns(DeprecationWarning):
|
||||
|
||||
@app.post("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[str | None, Form(regex="^fixedquery$")] = None
|
||||
):
|
||||
if q:
|
||||
return f"Hello {q}"
|
||||
else:
|
||||
return "Hello World"
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_no_query():
|
||||
client = get_client()
|
||||
response = client.post("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "Hello World"
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_q_fixedquery():
|
||||
client = get_client()
|
||||
response = client.post("/items/", data={"q": "fixedquery"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "Hello fixedquery"
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_query_nonregexquery():
|
||||
client = get_client()
|
||||
response = client.post("/items/", data={"q": "nonregexquery"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "q"],
|
||||
"msg": "String should match pattern '^fixedquery$'",
|
||||
"input": "nonregexquery",
|
||||
"ctx": {"pattern": "^fixedquery$"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"pattern": "^fixedquery$"},
|
||||
"loc": ["body", "q"],
|
||||
"msg": 'string does not match regex "^fixedquery$"',
|
||||
"type": "value_error.str.regex",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_openapi_schema():
|
||||
client = get_client()
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
# insert_assert(response.json())
|
||||
assert response.json() == {
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": IsDict(
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Body_read_items_items__post"
|
||||
}
|
||||
],
|
||||
"title": "Body",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"$ref": "#/components/schemas/Body_read_items_items__post"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_items_items__post": {
|
||||
"properties": {
|
||||
"q": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "pattern": "^fixedquery$"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"type": "string", "pattern": "^fixedquery$", "title": "Q"}
|
||||
)
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Body_read_items_items__post",
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"type": "array",
|
||||
"title": "Detail",
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location",
|
||||
},
|
||||
"msg": {"type": "string", "title": "Message"},
|
||||
"type": {"type": "string", "title": "Error Type"},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from .utils import needs_py310
|
||||
|
||||
|
||||
def get_client():
|
||||
app = FastAPI()
|
||||
with pytest.warns(DeprecationWarning):
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(
|
||||
q: Annotated[str | None, Query(regex="^fixedquery$")] = None
|
||||
):
|
||||
if q:
|
||||
return f"Hello {q}"
|
||||
else:
|
||||
return "Hello World"
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_query_params_str_validations_no_query():
|
||||
client = get_client()
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "Hello World"
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_query_params_str_validations_q_fixedquery():
|
||||
client = get_client()
|
||||
response = client.get("/items/", params={"q": "fixedquery"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "Hello fixedquery"
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_query_params_str_validations_item_query_nonregexquery():
|
||||
client = get_client()
|
||||
response = client.get("/items/", params={"q": "nonregexquery"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "String should match pattern '^fixedquery$'",
|
||||
"input": "nonregexquery",
|
||||
"ctx": {"pattern": "^fixedquery$"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"pattern": "^fixedquery$"},
|
||||
"loc": ["query", "q"],
|
||||
"msg": 'string does not match regex "^fixedquery$"',
|
||||
"type": "value_error.str.regex",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
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": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "pattern": "^fixedquery$"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^fixedquery$",
|
||||
"title": "Q",
|
||||
}
|
||||
),
|
||||
}
|
||||
],
|
||||
"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",
|
||||
},
|
||||
"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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ client = TestClient(app)
|
|||
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"},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -14,13 +15,24 @@ class Model(BaseModel):
|
|||
class ModelNoAlias(BaseModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/dict", response_model=Model, response_model_by_alias=False)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ from typing import List, Union
|
|||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIError
|
||||
from fastapi.exceptions import FastAPIError, ResponseValidationError
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BaseUser(BaseModel):
|
||||
|
|
@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict():
|
|||
|
||||
|
||||
def test_response_model_no_annotation_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model-no_annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_no_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model-no_annotation-return_invalid_model")
|
||||
|
||||
|
||||
|
|
@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict():
|
|||
|
||||
|
||||
def test_no_response_model_annotation_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/no_response_model-annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_no_response_model_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/no_response_model-annotation-return_invalid_model")
|
||||
|
||||
|
||||
|
|
@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict():
|
|||
|
||||
|
||||
def test_response_model_model1_annotation_model2_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_model1_annotation_model2_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model_model1-annotation_model2-return_invalid_model")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserDB(UserBase):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class PetDB(BaseModel):
|
||||
name: str
|
||||
owner: UserDB
|
||||
|
||||
|
||||
class PetOut(BaseModel):
|
||||
name: str
|
||||
owner: UserBase
|
||||
|
||||
|
||||
@app.post("/users/", response_model=UserBase)
|
||||
async def create_user(user: UserCreate):
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/pets/{pet_id}", response_model=PetOut)
|
||||
async def read_pet(pet_id: int):
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet = PetDB(name="Nibbler", owner=user)
|
||||
return pet
|
||||
|
||||
|
||||
@app.get("/pets/", response_model=List[PetOut])
|
||||
async def read_pets():
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet1 = PetDB(name="Nibbler", owner=user)
|
||||
pet2 = PetDB(name="Zoidberg", owner=user)
|
||||
return [pet1, pet2]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_top_level_model():
|
||||
response = client.post(
|
||||
"/users", json={"email": "johndoe@example.com", "password": "secret"}
|
||||
)
|
||||
assert response.json() == {"email": "johndoe@example.com"}
|
||||
|
||||
|
||||
def test_filter_second_level_model():
|
||||
response = client.get("/pets/1")
|
||||
assert response.json() == {
|
||||
"name": "Nibbler",
|
||||
"owner": {"email": "johndoe@example.com"},
|
||||
}
|
||||
|
||||
|
||||
def test_list_of_models():
|
||||
response = client.get("/pets/")
|
||||
assert response.json() == [
|
||||
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
|
||||
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
|
||||
]
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserDB(BaseModel):
|
||||
email: str
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class PetDB(BaseModel):
|
||||
name: str
|
||||
owner: UserDB
|
||||
|
||||
|
||||
class PetOut(BaseModel):
|
||||
name: str
|
||||
owner: User
|
||||
|
||||
|
||||
@app.post("/users/", response_model=User)
|
||||
async def create_user(user: UserCreate):
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/pets/{pet_id}", response_model=PetOut)
|
||||
async def read_pet(pet_id: int):
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet = PetDB(name="Nibbler", owner=user)
|
||||
return pet
|
||||
|
||||
|
||||
@app.get("/pets/", response_model=List[PetOut])
|
||||
async def read_pets():
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet1 = PetDB(name="Nibbler", owner=user)
|
||||
pet2 = PetDB(name="Zoidberg", owner=user)
|
||||
return [pet1, pet2]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_top_level_model():
|
||||
response = client.post(
|
||||
"/users", json={"email": "johndoe@example.com", "password": "secret"}
|
||||
)
|
||||
assert response.json() == {"email": "johndoe@example.com"}
|
||||
|
||||
|
||||
def test_filter_second_level_model():
|
||||
response = client.get("/pets/1")
|
||||
assert response.json() == {
|
||||
"name": "Nibbler",
|
||||
"owner": {"email": "johndoe@example.com"},
|
||||
}
|
||||
|
||||
|
||||
def test_list_of_models():
|
||||
response = client.get("/pets/")
|
||||
assert response.json() == [
|
||||
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
|
||||
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
|
||||
]
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
def create_app():
|
||||
|
|
@ -12,8 +14,14 @@ def create_app():
|
|||
class Item(BaseModel):
|
||||
data: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"data": "Data in schema_extra"}}
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={"example": {"data": "Data in schema_extra"}}
|
||||
)
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"data": "Data in schema_extra"}}
|
||||
|
||||
@app.post("/schema_extra/")
|
||||
def schema_extra(item: Item):
|
||||
|
|
@ -333,14 +341,28 @@ def test_openapi_schema():
|
|||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [{"$ref": "#/components/schemas/Item"}],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"},
|
||||
{"data": "Data in Body examples, example2"},
|
||||
],
|
||||
}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"$ref": "#/components/schemas/Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"},
|
||||
{"data": "Data in Body examples, example2"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove this when deprecating Pydantic v1
|
||||
{
|
||||
"allOf": [
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"},
|
||||
{"data": "Data in Body examples, example2"},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
|
|
@ -370,14 +392,28 @@ def test_openapi_schema():
|
|||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [{"$ref": "#/components/schemas/Item"}],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "examples example_examples 1"},
|
||||
{"data": "examples example_examples 2"},
|
||||
],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"$ref": "#/components/schemas/Item",
|
||||
"examples": [
|
||||
{"data": "examples example_examples 1"},
|
||||
{"data": "examples example_examples 2"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove this when deprecating Pydantic v1
|
||||
{
|
||||
"allOf": [
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "examples example_examples 1"},
|
||||
{"data": "examples example_examples 2"},
|
||||
],
|
||||
},
|
||||
),
|
||||
"example": {"data": "Overridden example"},
|
||||
}
|
||||
},
|
||||
|
|
@ -508,7 +544,16 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "query1",
|
||||
"name": "data",
|
||||
"in": "query",
|
||||
|
|
@ -539,11 +584,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
}
|
||||
),
|
||||
"name": "data",
|
||||
"in": "query",
|
||||
}
|
||||
|
|
@ -573,11 +628,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["query1", "query2"],
|
||||
}
|
||||
),
|
||||
"example": "query_overridden",
|
||||
"name": "data",
|
||||
"in": "query",
|
||||
|
|
@ -608,7 +673,16 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"type": "string", "title": "Data"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "header1",
|
||||
"name": "data",
|
||||
"in": "header",
|
||||
|
|
@ -639,11 +713,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["header1", "header2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["header1", "header2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["header1", "header2"],
|
||||
}
|
||||
),
|
||||
"name": "data",
|
||||
"in": "header",
|
||||
}
|
||||
|
|
@ -673,11 +757,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["header1", "header2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["header1", "header2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Data",
|
||||
"type": "string",
|
||||
"examples": ["header1", "header2"],
|
||||
}
|
||||
),
|
||||
"example": "header_overridden",
|
||||
"name": "data",
|
||||
"in": "header",
|
||||
|
|
@ -708,7 +802,16 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"type": "string", "title": "Data"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "cookie1",
|
||||
"name": "data",
|
||||
"in": "cookie",
|
||||
|
|
@ -739,11 +842,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Data",
|
||||
"type": "string",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
}
|
||||
),
|
||||
"name": "data",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
|
@ -773,11 +886,21 @@ def test_openapi_schema():
|
|||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Data",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Data",
|
||||
"type": "string",
|
||||
"examples": ["cookie1", "cookie2"],
|
||||
}
|
||||
),
|
||||
"example": "cookie_overridden",
|
||||
"name": "data",
|
||||
"in": "cookie",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header():
|
|||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_no_data():
|
||||
response = client.post("/login")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
@ -199,8 +260,26 @@ def test_openapi_schema():
|
|||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -63,76 +64,136 @@ def test_security_oauth2_password_bearer_no_header():
|
|||
assert response.json() == {"msg": "Create an account first"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_no_data():
|
||||
response = client.post("/login")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_data():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
@ -203,8 +264,26 @@ def test_openapi_schema():
|
|||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -64,76 +65,136 @@ def test_security_oauth2_password_bearer_no_header():
|
|||
assert response.json() == {"msg": "Create an account first"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_None():
|
||||
response = client.post("/login", data=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_correct_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
|
@ -204,8 +265,26 @@ def test_openapi_schema():
|
|||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class SubModel(BaseModel):
|
|||
|
||||
|
||||
class Model(BaseModel):
|
||||
x: Optional[int]
|
||||
x: Optional[int] = None
|
||||
sub: SubModel
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue