Compare commits

..

90 Commits

Author SHA1 Message Date
Sebastián Ramírez
1d8d8925fc 🔖 Release version 0.100.0-beta2 2023-06-30 21:50:18 +02:00
Pastukhov Nikita
d5952d6db5 🐛 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>
2023-06-30 21:49:10 +02:00
Sebastián Ramírez
6dc975da9d ♻️ 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
2023-06-20 18:22:03 +02:00
Sebastián Ramírez
e7b3d47af3 🔖 Release version 0.100.0-beta1 2023-06-20 17:51:16 +02:00
Sebastián Ramírez
cfb00b2119 ♻️ 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
2023-06-20 17:07:53 +02:00
Sebastián Ramírez
c58e2b2d1e 👷 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
2023-06-16 18:37:08 +02:00
Sebastián Ramírez
5301cfff52 🔀 Merge branch 'master' into main-pv2 2023-06-16 17:07:15 +02:00
github-actions
49bc3e0873 📝 Update release notes 2023-06-16 14:49:35 +00:00
Sebastián Ramírez
8767634932 👷 Lint in CI only once, only with one version of Python, run tests with all of them (#9686) 2023-06-16 16:49:01 +02:00
Sebastián Ramírez
bd32fecaf6 Pydantic v2 migration, initial implementation (#9500)
* WIP

*  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>
2023-06-16 16:30:28 +02:00
Sebastián Ramírez
32935103b1 🔖 Release version 0.97.0 2023-06-12 00:50:06 +02:00
Sebastián Ramírez
395ece75aa 📝 Update release notes 2023-06-12 00:49:35 +02:00
github-actions
e958d30d1d 📝 Update release notes 2023-06-11 22:47:16 +00:00
Sebastián Ramírez
34fca99b28 ⬆️ Upgrade Black (#9661) 2023-06-11 22:46:44 +00:00
github-actions
3289796286 📝 Update release notes 2023-06-11 22:38:17 +00:00
Sebastián Ramírez
7167c77a18 ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff (#9660) 2023-06-12 00:37:34 +02:00
github-actions
ba882c10fe 📝 Update release notes 2023-06-11 22:16:38 +00:00
Sebastián Ramírez
4ac55af283 ♻️ Update internal type annotations and upgrade mypy (#9658) 2023-06-11 22:16:01 +00:00
github-actions
3390a82832 📝 Update release notes 2023-06-11 22:09:33 +00:00
Sebastián Ramírez
f5844e76b5 💚 Update CI cache to fix installs when dependencies change (#9659) 2023-06-11 22:08:56 +00:00
github-actions
32cefb9bff 📝 Update release notes 2023-06-11 21:49:52 +00:00
Sebastián Ramírez
17e49bc9f7 ♻️ Simplify AsyncExitStackMiddleware as without Python 3.6 AsyncExitStack is always available (#9657)
♻️ Simplify AsyncExitStackMiddleware as without Python 3.6 AsyncExitStack is always available
2023-06-11 21:49:18 +00:00
github-actions
df58ecdee2 📝 Update release notes 2023-06-11 21:38:54 +00:00
Sebastián Ramírez
6595658324 ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras (#9655) 2023-06-11 23:38:15 +02:00
github-actions
c8b729aea7 📝 Update release notes 2023-06-11 20:36:12 +00:00
Paulo Costa
d8b8f211e8 Add support for dependencies in WebSocket routes (#4534)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-11 20:35:39 +00:00
github-actions
ee96a099d8 📝 Update release notes 2023-06-11 19:08:50 +00:00
Kristján Valur Jónsson
ab03f22635 Add exception handler for WebSocketRequestValidationError (which also allows to override it) (#6030)
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>
2023-06-11 21:08:14 +02:00
Sebastián Ramírez
f5e2dd8025 📝 Update release notes 2023-06-11 00:03:27 +02:00
Sebastián Ramírez
19347bfc3c 🔖 Release version 0.96.1 2023-06-10 23:51:40 +02:00
Sebastián Ramírez
20d93fad94 📝 Update release notes 2023-06-10 23:50:09 +02:00
github-actions
58e50622de 📝 Update release notes 2023-06-10 20:58:55 +00:00
Sebastián Ramírez
4ac8b8e443 🔧 Add sponsor Platform.sh (#9650) 2023-06-10 22:58:15 +02:00
github-actions
3d162455a7 📝 Update release notes 2023-06-10 20:49:25 +00:00
Purwo Widodo
edc939eb3a 🌐 Fix spelling in Indonesian translation of docs/id/docs/tutorial/index.md (#5635)
Co-authored-by: Purwo Widodo <id.purwowd@gmail.comm>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-10 22:48:51 +02:00
github-actions
4c64c15ead 📝 Update release notes 2023-06-10 20:37:02 +00:00
Ildar Ramazanov
e3d67a150c 🌐 Add Russian translation for docs/ru/docs/tutorial/index.md (#5896)
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>
2023-06-10 20:36:25 +00:00
github-actions
9b14107695 📝 Update release notes 2023-06-10 20:31:03 +00:00
雨过初晴
57679e8370 🌐 Add Chinese translations for docs/zh/docs/advanced/response-change-status-code.md and docs/zh/docs/advanced/response-headers.md (#9544) 2023-06-10 22:30:28 +02:00
github-actions
6fe26b5689 📝 Update release notes 2023-06-10 20:29:47 +00:00
Alexandr
510fa5b7fe 🌐 Add Russian translation for docs/ru/docs/tutorial/schema-extra-example.md (#9621)
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
2023-06-10 22:29:08 +02:00
github-actions
ca8ddb2893 📝 Update release notes 2023-06-10 17:23:47 +00:00
Sebastián Ramírez
6dd8e567cc 🐛 Fix HTTPException header type annotations (#9648) 2023-06-10 19:23:12 +02:00
github-actions
ae5c51afa6 📝 Update release notes 2023-06-10 17:06:14 +00:00
Marcelo Trylesinski
19757d1859 🔥 Remove link to Pydantic's benchmark, as it was removed there (#5811)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-10 19:05:42 +02:00
github-actions
e645a2db1b 📝 Update release notes 2023-06-10 17:05:03 +00:00
Marcelo Trylesinski
52fd0afc94 ♻ Remove media_type from ORJSONResponse as it's inherited from the parent class (#5805)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-10 19:04:29 +02:00
github-actions
503cec5649 📝 Update release notes 2023-06-10 16:03:40 +00:00
github-actions
2c7a0aca95 📝 Update release notes 2023-06-10 14:26:29 +00:00
Sebastián Ramírez
8d29e494e0 👷 Add custom tokens for GitHub Actions to avoid rate limits (#9647) 2023-06-10 16:25:54 +02:00
Sebastián Ramírez
4c23c0644b 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits (#9646) 2023-06-10 14:39:34 +02:00
github-actions
d189c38aaf 📝 Update release notes 2023-06-10 12:06:21 +00:00
Timothée Mazzucotelli
010d44ee1b ♻ Instantiate HTTPException only when needed, optimization refactor (#5356)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-10 14:05:35 +02:00
github-actions
4b31beef35 📝 Update release notes 2023-06-08 18:31:33 +00:00
Marcelo Trylesinski
61a8d6720c 📌 Update minimum version of Pydantic to >=1.7.4 (#9567) 2023-06-08 20:30:49 +02:00
github-actions
155fc5e24e 📝 Update release notes 2023-06-07 20:44:47 +00:00
Sebastián Ramírez
2d35651a5a 🐛 Fix OpenAPI model fields int validations, change gte to ge (#9635)
🐛 Fix OpenAPI model fields int validations, change `gte` to `ge`
2023-06-07 22:44:12 +02:00
Sebastián Ramírez
1574c96231 🔖 Release version 0.96.0 2023-06-03 16:29:23 +02:00
Sebastián Ramírez
99ed2a227f 📝 Update release notes 2023-06-03 16:28:37 +02:00
github-actions
6b72d54136 📝 Update release notes 2023-06-03 14:20:32 +00:00
Sergei Solomein
8474bae744 🌐 Add Russian translation for docs/tutorial/body.md (#3885)
Co-authored-by: Teregov_Ruslan <48125303+RuslanTer@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: FedorGN <66411909+FedorGN@users.noreply.github.com>
2023-06-03 14:19:58 +00:00
github-actions
4d5e40190b 📝 Update release notes 2023-06-03 14:19:04 +00:00
Alexandr
1309f67f64 🌐 Add Russian translation for docs/ru/docs/tutorial/static-files.md (#9580)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-03 14:18:30 +00:00
github-actions
b086b6580d 📝 Update release notes 2023-06-03 14:15:41 +00:00
github-actions
47c13874a0 📝 Update release notes 2023-06-03 14:06:48 +00:00
github-actions
ede2b53a0f 📝 Update release notes 2023-06-03 14:06:02 +00:00
Artem Golicyn
918d96f6ad 🌐 Add Russian translation for docs/ru/docs/tutorial/first-steps.md (#9471)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-03 14:05:53 +00:00
Alexandr
4c9ac66554 🌐 Add Russian translation for docs/ru/docs/tutorial/query-params.md (#9584)
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-03 14:05:30 +00:00
Alexandr
1f92ad349c 🌐 Add Russian translation for docs/ru/docs/tutorial/debugging.md (#9579)
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-03 14:05:22 +00:00
github-actions
8e1280bf87 📝 Update release notes 2023-06-03 14:01:51 +00:00
github-actions
5d2942f8fd 📝 Update release notes 2023-06-03 13:58:16 +00:00
github-actions
ee017fdffa 📝 Update release notes 2023-06-03 13:58:09 +00:00
github-actions
d5b588f246 📝 Update release notes 2023-06-03 13:57:42 +00:00
github-actions
1ecc9a1810 📝 Update release notes 2023-06-03 13:56:26 +00:00
github-actions
f0b4d590af 📝 Update release notes 2023-06-03 13:56:16 +00:00
github-actions
beedcd90c7 📝 Update release notes 2023-06-03 13:56:02 +00:00
github-actions
f2b0670f04 📝 Update release notes 2023-06-03 13:55:50 +00:00
github-actions
795419ceee 📝 Update release notes 2023-06-03 13:54:48 +00:00
github-actions
2c091aa0a4 📝 Update release notes 2023-06-03 13:52:14 +00:00
Sebastián Ramírez
68809d6f97 🔧 Update sponsors, remove InvestSuite (#9612) 2023-06-03 13:51:39 +00:00
github-actions
3c7a4b568c 📝 Update release notes 2023-06-03 13:38:23 +00:00
Zanie Adkins
27618aa2e8 Update create_cloned_field to use a global cache and improve startup performance (#4645)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: Huon Wilson <wilson.huon@gmail.com>
2023-06-03 13:37:41 +00:00
吴定焕
d057294de1 🌐 Add Chinese translation for docs/zh/docs/tutorial/static-files.md (#9436)
Add Chinese translation for docs/zh/docs/tutorial/static-files.md
2023-06-03 14:49:32 +02:00
Andres Bermeo
7cdee0eb63 🌐 Update Spanish translation including new illustrations in docs/es/docs/async.md (#9483) 2023-06-03 14:37:15 +02:00
Lemonyte
ffb818970f ✏️ Fix typo in Deta deployment tutorial (#9501)
Fix typo (Data -> Deta)
2023-06-03 14:32:40 +02:00
Artem Golicyn
5017949010 🌐 Add Russian translation for docs/ru/docs/tutorial/path-params.md (#9519)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-03 14:31:44 +02:00
ivan-abc
ad77d7f926 🌐 Add Russian translation for docs/ru/docs/tutorial/path-params-numeric-validations.md (#9563)
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-03 14:22:10 +02:00
Vladislav Kramorenko
061e912ccf 🌐 Add Russian translation for docs/ru/docs/deployment/concepts.md (#9577)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-03 14:21:05 +02:00
Alexandr
72c72774c5 🌐 Add Russian translation for docs/ru/docs/tutorial/body-multiple-params.md (#9586)
* 🌐 Add Russian translation for docs/tutorial/body-multiple-params.md

Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-03 14:13:10 +02:00
github-actions[bot]
e0961cbd1c 👥 Update FastAPI People (#9602)
Co-authored-by: github-actions <github-actions@github.com>
2023-06-03 14:09:57 +02:00
283 changed files with 17788 additions and 4933 deletions

View File

@@ -22,10 +22,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03
- name: Install docs extras
if: steps.cache.outputs.cache-hit != 'true'
run: pip install .[doc]
run: pip install -r requirements-docs.txt
- name: Install Material for MkDocs Insiders
if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true'
run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git
@@ -42,7 +42,7 @@ jobs:
with:
publish-dir: './site'
production-branch: master
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.FASTAPI_BUILD_DOCS_NETLIFY }}
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -20,7 +20,7 @@ jobs:
steps:
- uses: tiangolo/issue-manager@0.4.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.FASTAPI_ISSUE_MANAGER }}
config: >
{
"answered": {

View File

@@ -10,4 +10,4 @@ jobs:
steps:
- uses: docker://tiangolo/label-approved:0.0.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.FASTAPI_LABEL_APPROVED }}

View File

@@ -30,11 +30,9 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
with:
limit-access-to-actor: true
token: ${{ secrets.ACTIONS_TOKEN }}
standard_token: ${{ secrets.GITHUB_TOKEN }}
- uses: docker://tiangolo/latest-changes:0.0.3
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.FASTAPI_LATEST_CHANGES }}
latest_changes_file: docs/en/docs/release-notes.md
latest_changes_header: '## Latest Changes\n\n'
debug_logs: true

View File

@@ -19,4 +19,4 @@ jobs:
limit-access-to-actor: true
- uses: ./.github/actions/notify-translations
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.FASTAPI_NOTIFY_TRANSLATIONS }}

View File

@@ -24,9 +24,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
with:
limit-access-to-actor: true
token: ${{ secrets.ACTIONS_TOKEN }}
standard_token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/people
with:
token: ${{ secrets.ACTIONS_TOKEN }}
standard_token: ${{ secrets.GITHUB_TOKEN }}
standard_token: ${{ secrets.FASTAPI_PEOPLE }}

View File

@@ -18,7 +18,7 @@ jobs:
- name: Download Artifact Docs
uses: dawidd6/action-download-artifact@v2.27.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }}
workflow: build-docs.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs-zip
@@ -34,7 +34,7 @@ jobs:
with:
publish-dir: './site'
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.FASTAPI_PREVIEW_DOCS_NETLIFY }}
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -42,5 +42,5 @@ jobs:
- name: Comment Deploy
uses: ./.github/actions/comment-docs-preview-in-pr
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }}
deploy_url: "${{ steps.netlify.outputs.deploy-url }}"

View File

@@ -39,9 +39,3 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
# - name: Notify
# env:
# GITTER_TOKEN: ${{ secrets.GITTER_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# TAG: ${{ github.event.release.name }}
# run: bash scripts/notify.sh

View File

@@ -22,6 +22,7 @@ jobs:
- uses: dawidd6/action-download-artifact@v2.27.0
with:
github_token: ${{ secrets.FASTAPI_SMOKESHOW_DOWNLOAD_ARTIFACTS }}
workflow: test.yml
commit: ${{ github.event.workflow_run.head_sha }}
@@ -30,6 +31,6 @@ jobs:
SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100
SMOKESHOW_GITHUB_CONTEXT: coverage
SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SMOKESHOW_GITHUB_TOKEN: ${{ secrets.FASTAPI_SMOKESHOW_UPLOAD }}
SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }}

View File

@@ -5,16 +5,42 @@ on:
branches:
- master
pull_request:
types: [opened, synchronize]
types:
- opened
- synchronize
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
# cache-dependency-path: pyproject.toml
- uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
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 --pre "pydantic>=2.0.0b2,<3.0.0"
- name: Lint
run: bash scripts/lint.sh
test:
runs-on: ubuntu-latest
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
- name: Set up Python
@@ -23,17 +49,21 @@ jobs:
python-version: ${{ matrix.python-version }}
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
cache-dependency-path: pyproject.toml
# cache-dependency-path: pyproject.toml
- uses: actions/cache@v3
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-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 -e .[all,dev,doc,test]
- name: Lint
run: bash scripts/lint.sh
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 --pre "pydantic>=2.0.0b2,<3.0.0"
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh
@@ -45,33 +75,28 @@ jobs:
with:
name: coverage
path: coverage
coverage-combine:
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.8'
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
cache-dependency-path: pyproject.toml
# cache-dependency-path: pyproject.toml
- name: Get coverage files
uses: actions/download-artifact@v3
with:
name: coverage
path: coverage
- run: pip install coverage[toml]
- run: ls -la coverage
- run: coverage combine coverage
- run: coverage report
- run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}"
- name: Store coverage HTML
uses: actions/upload-artifact@v3
with:
@@ -80,14 +105,10 @@ jobs:
# https://github.com/marketplace/actions/alls-green#why
check: # This job does nothing and is only used for the branch protection
if: always()
needs:
- coverage-combine
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1

View File

@@ -21,24 +21,13 @@ repos:
- --py3-plus
- --keep-runtime-typing
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.254
rev: v0.0.272
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- id: isort
name: isort (cython)
types: [cython]
- id: isort
name: isort (pyi)
types: [pyi]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
ci:

View File

@@ -47,8 +47,8 @@ The key features are:
<!-- sponsors -->
<a href="https://cryptapi.io/" target="_blank" title="CryptAPI: Your easy to use, secure and privacy oriented payment gateway."><img src="https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg"></a>
<a href="https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" target="_blank" title="Build, run and scale your apps on a modern, reliable, and secure PaaS."><img src="https://fastapi.tiangolo.com/img/sponsors/platform-sh.png"></a>
<a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a>
<a href="https://www.investsuite.com/jobs" target="_blank" title="Wealthtech jobs with FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/investsuite.svg"></a>
<a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a>
<a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a>
<a href="https://github.com/deepset-ai/haystack/" target="_blank" title="Build powerful search from composable, open source building blocks"><img src="https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg"></a>

View File

@@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

View File

@@ -1,10 +1,4 @@
sponsors:
- - login: jina-ai
avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4
url: https://github.com/jina-ai
- - login: armand-sauzay
avatarUrl: https://avatars.githubusercontent.com/u/35524799?u=56e3e944bfe62770d1709c09552d2efc6d285ca6&v=4
url: https://github.com/armand-sauzay
- - login: cryptapi
avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4
url: https://github.com/cryptapi
@@ -14,9 +8,6 @@ sponsors:
- login: ObliviousAI
avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4
url: https://github.com/ObliviousAI
- login: chaserowbotham
avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4
url: https://github.com/chaserowbotham
- - login: mikeckennedy
avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4
url: https://github.com/mikeckennedy
@@ -26,48 +17,42 @@ sponsors:
- login: deepset-ai
avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4
url: https://github.com/deepset-ai
- login: investsuite
avatarUrl: https://avatars.githubusercontent.com/u/73833632?v=4
url: https://github.com/investsuite
- login: svix
avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4
url: https://github.com/svix
- login: databento-bot
avatarUrl: https://avatars.githubusercontent.com/u/98378480?u=494f679996e39427f7ddb1a7de8441b7c96fb670&v=4
url: https://github.com/databento-bot
- login: VincentParedes
avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4
url: https://github.com/VincentParedes
- - login: getsentry
avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4
url: https://github.com/getsentry
- - login: InesIvanova
avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4
url: https://github.com/InesIvanova
- - login: vyos
avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4
url: https://github.com/vyos
- login: takashi-yoneya
- - login: takashi-yoneya
avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4
url: https://github.com/takashi-yoneya
- login: mercedes-benz
avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4
url: https://github.com/mercedes-benz
- login: xoflare
avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4
url: https://github.com/xoflare
- login: marvin-robot
avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4
url: https://github.com/marvin-robot
- login: BoostryJP
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
url: https://github.com/BoostryJP
- - login: johnadjei
avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4
url: https://github.com/johnadjei
- login: HiredScore
- - login: HiredScore
avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4
url: https://github.com/HiredScore
- login: ianshan0915
avatarUrl: https://avatars.githubusercontent.com/u/5893101?u=a178d247d882578b1d1ef214b2494e52eb28634c&v=4
url: https://github.com/ianshan0915
- login: Trivie
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie
- login: Lovage-Labs
avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4
url: https://github.com/Lovage-Labs
- - login: JonasKs
avatarUrl: https://avatars.githubusercontent.com/u/5310116?u=98a049f3e1491bffb91e1feb7e93def6881a9389&v=4
url: https://github.com/JonasKs
- - login: moellenbeck
avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4
url: https://github.com/moellenbeck
@@ -83,12 +68,9 @@ sponsors:
- login: tizz98
avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4
url: https://github.com/tizz98
- login: dorianturba
avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4
url: https://github.com/dorianturba
- login: jmaralc
avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4
url: https://github.com/jmaralc
- login: americanair
avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4
url: https://github.com/americanair
- login: mainframeindustries
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
url: https://github.com/mainframeindustries
@@ -132,7 +114,7 @@ sponsors:
avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4
url: https://github.com/koxudaxi
- login: falkben
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4
url: https://github.com/falkben
- login: jqueguiner
avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4
@@ -146,24 +128,15 @@ sponsors:
- login: mrkmcknz
avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4
url: https://github.com/mrkmcknz
- login: coffeewasmyidea
avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4
url: https://github.com/coffeewasmyidea
- login: mickaelandrieu
avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4
url: https://github.com/mickaelandrieu
- login: jonakoudijs
avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4
url: https://github.com/jonakoudijs
- login: corleyma
avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=c61f9a4bbc45a45f5d855f93e5f6e0fc8b32c468&v=4
url: https://github.com/corleyma
- login: andre1sk
avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4
url: https://github.com/andre1sk
- login: Shark009
avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4
url: https://github.com/Shark009
- login: ColliotL
avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4
url: https://github.com/ColliotL
- login: dblackrun
avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4
url: https://github.com/dblackrun
@@ -203,69 +176,48 @@ sponsors:
- login: simw
avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4
url: https://github.com/simw
- login: pkucmus
avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4
url: https://github.com/pkucmus
- login: s3ich4n
avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=6690c5403bc1d9a1837886defdc5256e9a43b1db&v=4
url: https://github.com/s3ich4n
- login: Rehket
avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4
url: https://github.com/Rehket
- login: ValentinCalomme
avatarUrl: https://avatars.githubusercontent.com/u/7288672?u=e09758c7a36c49f0fb3574abe919cbd344fdc2d6&v=4
url: https://github.com/ValentinCalomme
- login: hiancdtrsnm
avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4
url: https://github.com/hiancdtrsnm
- login: Shackelford-Arden
avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4
url: https://github.com/Shackelford-Arden
- login: savannahostrowski
avatarUrl: https://avatars.githubusercontent.com/u/8949415?u=c3177aa099fb2b8c36aeba349278b77f9a8df211&v=4
url: https://github.com/savannahostrowski
- login: wdwinslow
avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4
url: https://github.com/wdwinslow
- login: svats2k
avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4
url: https://github.com/svats2k
- login: dannywade
avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4
url: https://github.com/dannywade
- login: khadrawy
avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4
url: https://github.com/khadrawy
- login: pablonnaoji
avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=7480e0eaf959e9c5dfe3a05286f2ea4588c0a3c6&v=4
url: https://github.com/pablonnaoji
- login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey
- login: abdalla19977
avatarUrl: https://avatars.githubusercontent.com/u/17257234?v=4
url: https://github.com/abdalla19977
- login: wedwardbeck
avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4
url: https://github.com/wedwardbeck
- login: RaamEEIL
avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4
url: https://github.com/RaamEEIL
- login: Filimoa
avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4
url: https://github.com/Filimoa
- login: shuheng-liu
avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4
url: https://github.com/shuheng-liu
- login: Pablongo24
avatarUrl: https://avatars.githubusercontent.com/u/24843427?u=78a6798469889d7a0690449fc667c39e13d5c6a9&v=4
url: https://github.com/Pablongo24
- login: Joeriksson
avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4
url: https://github.com/Joeriksson
- login: cometa-haley
avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4
url: https://github.com/cometa-haley
- login: SebTota
avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4
url: https://github.com/SebTota
- login: LarryGF
avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4
url: https://github.com/LarryGF
- login: veprimk
avatarUrl: https://avatars.githubusercontent.com/u/29689749?u=f8cb5a15a286e522e5b189bc572d5a1a90217fb2&v=4
url: https://github.com/veprimk
- login: BrettskiPy
avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4
url: https://github.com/BrettskiPy
@@ -290,27 +242,21 @@ sponsors:
- login: arleybri18
avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4
url: https://github.com/arleybri18
- login: thenickben
avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4
url: https://github.com/thenickben
- login: ybressler
avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4
url: https://github.com/ybressler
- login: ddilidili
avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4
url: https://github.com/ddilidili
- login: VictorCalderon
avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4
url: https://github.com/VictorCalderon
- login: rafsaf
avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4
url: https://github.com/rafsaf
- login: dudikbender
avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4
url: https://github.com/dudikbender
- login: thisistheplace
avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4
url: https://github.com/thisistheplace
- login: kyjoconn
avatarUrl: https://avatars.githubusercontent.com/u/58443406?u=a3e9c2acfb7ba62edda9334aba61cf027f41f789&v=4
url: https://github.com/kyjoconn
- login: A-Edge
avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4
url: https://github.com/A-Edge
@@ -320,9 +266,6 @@ sponsors:
- login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia
- login: predictionmachine
avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4
url: https://github.com/predictionmachine
- login: daverin
avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4
url: https://github.com/daverin
@@ -341,24 +284,21 @@ sponsors:
- login: Dagmaara
avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4
url: https://github.com/Dagmaara
- - login: Yarden-zamir
avatarUrl: https://avatars.githubusercontent.com/u/8178413?u=ee177a8b0f87ea56747f4d96f34cd4e9604a8217&v=4
url: https://github.com/Yarden-zamir
- - login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
url: https://github.com/pawamoy
- login: linux-china
avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4
url: https://github.com/linux-china
- login: ddanier
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
url: https://github.com/ddanier
- login: jhb
avatarUrl: https://avatars.githubusercontent.com/u/142217?v=4
url: https://github.com/jhb
- login: justinrmiller
avatarUrl: https://avatars.githubusercontent.com/u/143998?u=b507a940394d4fc2bc1c27cea2ca9c22538874bd&v=4
url: https://github.com/justinrmiller
- login: bryanculbertson
avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4
url: https://github.com/bryanculbertson
- login: slafs
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
url: https://github.com/slafs
- login: adamghill
avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4
url: https://github.com/adamghill
@@ -378,11 +318,8 @@ sponsors:
avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4
url: https://github.com/browniebroke
- login: janfilips
avatarUrl: https://avatars.githubusercontent.com/u/870699?u=50de77b93d3a0b06887e672d4e8c7b9d643085aa&v=4
avatarUrl: https://avatars.githubusercontent.com/u/870699?u=96df18ad355e58b9397accc55f4eeb7a86e959b0&v=4
url: https://github.com/janfilips
- login: allen0125
avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4
url: https://github.com/allen0125
- login: WillHogan
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4
url: https://github.com/WillHogan
@@ -392,17 +329,20 @@ sponsors:
- login: cbonoz
avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4
url: https://github.com/cbonoz
- login: paul121
avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4
url: https://github.com/paul121
- login: Patechoc
avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4
url: https://github.com/Patechoc
- login: larsvik
avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4
url: https://github.com/larsvik
- login: anthonycorletti
avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4
url: https://github.com/anthonycorletti
- login: jonathanhle
avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4
url: https://github.com/jonathanhle
- login: nikeee
avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=63f8eee593f25138e0f1032ef442e9ad24907d4c&v=4
avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=bbe73151f2b409c120160d032dc9aa6875ef0c4b&v=4
url: https://github.com/nikeee
- login: Alisa-lisa
avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4
@@ -410,6 +350,12 @@ sponsors:
- login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood
- login: yuawn
avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4
url: https://github.com/yuawn
- login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota
- login: unredundant
avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4
url: https://github.com/unredundant
@@ -419,11 +365,11 @@ sponsors:
- login: KentShikama
avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4
url: https://github.com/KentShikama
- login: holec
avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4
url: https://github.com/holec
- login: katnoria
avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4
url: https://github.com/katnoria
- login: mattwelke
avatarUrl: https://avatars.githubusercontent.com/u/7719209?v=4
avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=80f02a799323b1472b389b836d95957c93a6d856&v=4
url: https://github.com/mattwelke
- login: hcristea
avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4
@@ -431,6 +377,9 @@ sponsors:
- login: moonape1226
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
url: https://github.com/moonape1226
- login: albertkun
avatarUrl: https://avatars.githubusercontent.com/u/8574425?u=aad2a9674273c9275fe414d99269b7418d144089&v=4
url: https://github.com/albertkun
- login: xncbf
avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4
url: https://github.com/xncbf
@@ -440,6 +389,9 @@ sponsors:
- login: hard-coders
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders
- login: supdann
avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4
url: https://github.com/supdann
- login: satwikkansal
avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4
url: https://github.com/satwikkansal
@@ -456,38 +408,32 @@ sponsors:
avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4
url: https://github.com/giuliano-oliveira
- login: TheR1D
avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b2923ac17fe6e2a7c9ea14800351ddb92f79b100&v=4
avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4
url: https://github.com/TheR1D
- login: cdsre
avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4
url: https://github.com/cdsre
- login: jangia
avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4
url: https://github.com/jangia
- login: paulowiz
avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4
url: https://github.com/paulowiz
- login: ghandic
avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4
url: https://github.com/ghandic
- login: pers0n4
avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4
url: https://github.com/pers0n4
- login: SebTota
avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4
url: https://github.com/SebTota
- login: kadekillary
avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4
url: https://github.com/kadekillary
- login: hoenie-ams
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
url: https://github.com/hoenie-ams
- login: joerambo
avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4
url: https://github.com/joerambo
- login: rlnchow
avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4
url: https://github.com/rlnchow
- login: mertguvencli
avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4
url: https://github.com/mertguvencli
- login: ruizdiazever
avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4
url: https://github.com/ruizdiazever
- login: HosamAlmoghraby
avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4
url: https://github.com/HosamAlmoghraby
@@ -495,53 +441,56 @@ sponsors:
avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4
url: https://github.com/engineerjoe440
- login: bnkc
avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=76cdc0a8b4e88c7d3e58dccb4b2670839e1247b4&v=4
avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=9fbf76b9bf7786275e2900efa51d1394bcf1f06a&v=4
url: https://github.com/bnkc
- login: declon
avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4
url: https://github.com/declon
- login: alvarobartt
avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=9b38695807eb981d452989699ff72ec2d8f6508e&v=4
url: https://github.com/alvarobartt
- login: d-e-h-i-o
avatarUrl: https://avatars.githubusercontent.com/u/36816716?v=4
url: https://github.com/d-e-h-i-o
- login: ww-daniel-mora
avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4
url: https://github.com/ww-daniel-mora
- login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd
- login: miraedbswo
avatarUrl: https://avatars.githubusercontent.com/u/36796047?u=9e7a5b3e558edc61d35d0f9dfac37541bae7f56d&v=4
url: https://github.com/miraedbswo
- login: kristiangronberg
avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4
url: https://github.com/kristiangronberg
- login: arrrrrmin
avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=5265858add14a6822bd145f7547323cf078563e6&v=4
avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4
url: https://github.com/arrrrrmin
- login: ArtyomVancyan
avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4
url: https://github.com/ArtyomVancyan
- login: hgalytoby
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4
url: https://github.com/hgalytoby
- login: data-djinn
avatarUrl: https://avatars.githubusercontent.com/u/56449985?u=42146e140806908d49bd59ccc96f222abf587886&v=4
url: https://github.com/data-djinn
- login: eladgunders
avatarUrl: https://avatars.githubusercontent.com/u/52347338?u=83d454817cf991a035c8827d46ade050c813e2d6&v=4
url: https://github.com/eladgunders
- login: conservative-dude
avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4
url: https://github.com/conservative-dude
- login: leo-jp-edwards
avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4
url: https://github.com/leo-jp-edwards
- login: apar-tiwari
avatarUrl: https://avatars.githubusercontent.com/u/61064197?v=4
url: https://github.com/apar-tiwari
- login: Vyvy-vi
avatarUrl: https://avatars.githubusercontent.com/u/62864373?u=1a9b0b28779abc2bc9b62cb4d2e44d453973c9c3&v=4
url: https://github.com/Vyvy-vi
- login: tamtam-fitness
avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4
url: https://github.com/tamtam-fitness
- login: 0417taehyun
avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4
url: https://github.com/0417taehyun
- login: realabja
avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4
url: https://github.com/realabja
- login: garydsong
avatarUrl: https://avatars.githubusercontent.com/u/105745865?u=03cc1aa9c978be0020e5a1ce1ecca323dd6c8d65&v=4
url: https://github.com/garydsong
- - login: Leon0824
avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4
url: https://github.com/Leon0824
- - login: ssbarnea
avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4
url: https://github.com/ssbarnea
- login: sadikkuzu
avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4
url: https://github.com/sadikkuzu
- login: ruizdiazever
avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4
url: https://github.com/ruizdiazever
- login: danburonline
avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4
url: https://github.com/danburonline
- login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd
- login: xNykram
avatarUrl: https://avatars.githubusercontent.com/u/55030025?u=2c1ba313fd79d29273b5ff7c9c5cf4edfb271b29&v=4
url: https://github.com/xNykram

View File

@@ -1,12 +1,12 @@
maintainers:
- login: tiangolo
answers: 1827
prs: 384
answers: 1839
prs: 398
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4
url: https://github.com/tiangolo
experts:
- login: Kludex
count: 376
count: 410
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4
url: https://github.com/Kludex
- login: dmontagu
@@ -22,69 +22,73 @@ experts:
avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4
url: https://github.com/ycd
- login: JarroVGIT
count: 192
count: 193
avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4
url: https://github.com/JarroVGIT
- login: euri10
count: 151
count: 152
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10
- login: phy25
count: 126
avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4
url: https://github.com/phy25
- login: iudeen
count: 116
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4
url: https://github.com/iudeen
- login: jgould22
count: 101
count: 124
avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4
url: https://github.com/jgould22
- login: iudeen
count: 118
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4
url: https://github.com/iudeen
- login: raphaelauv
count: 83
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
url: https://github.com/raphaelauv
- login: ArcLightSlavik
count: 71
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4
url: https://github.com/ArcLightSlavik
- login: ghandic
count: 71
avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4
url: https://github.com/ghandic
- login: ArcLightSlavik
count: 71
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4
url: https://github.com/ArcLightSlavik
- login: falkben
count: 57
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4
url: https://github.com/falkben
- login: sm-Fifteen
count: 49
avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4
url: https://github.com/sm-Fifteen
- login: Dustyposa
- login: yinziyan1206
count: 45
avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4
url: https://github.com/Dustyposa
avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4
url: https://github.com/yinziyan1206
- login: insomnes
count: 45
avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4
url: https://github.com/insomnes
- login: acidjunk
count: 45
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
url: https://github.com/acidjunk
- login: Dustyposa
count: 45
avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4
url: https://github.com/Dustyposa
- login: adriangb
count: 43
avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4
url: https://github.com/adriangb
- login: frankie567
count: 43
avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4
url: https://github.com/frankie567
- login: acidjunk
count: 43
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
url: https://github.com/acidjunk
- login: odiseo0
count: 42
avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4
avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4
url: https://github.com/odiseo0
- login: adriangb
count: 40
avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4
url: https://github.com/adriangb
- login: includeamin
count: 40
avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4
@@ -97,12 +101,8 @@ experts:
count: 35
avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4
url: https://github.com/krishnardt
- login: yinziyan1206
count: 34
avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4
url: https://github.com/yinziyan1206
- login: chbndrhnns
count: 34
count: 35
avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4
url: https://github.com/chbndrhnns
- login: panla
@@ -125,10 +125,10 @@ experts:
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4
url: https://github.com/SirTelemak
- login: caeser1996
count: 21
avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4
url: https://github.com/caeser1996
- login: acnebs
count: 22
avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4
url: https://github.com/acnebs
- login: rafsaf
count: 21
avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4
@@ -137,34 +137,38 @@ experts:
count: 20
avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4
url: https://github.com/nsidnev
- login: acnebs
count: 20
avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4
url: https://github.com/acnebs
- login: chris-allnutt
count: 20
avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4
url: https://github.com/chris-allnutt
- login: retnikt
count: 18
avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4
url: https://github.com/retnikt
- login: zoliknemet
count: 18
avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4
url: https://github.com/zoliknemet
- login: nkhitrov
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4
url: https://github.com/nkhitrov
- login: harunyasar
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4
url: https://github.com/harunyasar
- login: retnikt
count: 18
avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4
url: https://github.com/retnikt
- login: Hultner
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4
url: https://github.com/Hultner
- login: n8sty
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4
url: https://github.com/n8sty
- login: harunyasar
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4
url: https://github.com/harunyasar
- login: nkhitrov
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4
url: https://github.com/nkhitrov
- login: caeser1996
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4
url: https://github.com/caeser1996
- login: jonatasoli
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4
@@ -173,10 +177,6 @@ experts:
count: 16
avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4
url: https://github.com/dstlny
- login: jorgerpo
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4
url: https://github.com/jorgerpo
- login: ghost
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4
@@ -185,55 +185,43 @@ experts:
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4
url: https://github.com/simondale00
- login: jorgerpo
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4
url: https://github.com/jorgerpo
- login: ebottos94
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4
url: https://github.com/ebottos94
- login: hellocoldworld
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4
url: https://github.com/hellocoldworld
- login: waynerv
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv
- login: mbroton
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4
url: https://github.com/mbroton
last_month_active:
- login: mr-st0rm
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/48455163?u=6b83550e4e70bea57cd2fdb41e717aeab7f64a91&v=4
url: https://github.com/mr-st0rm
- login: caeser1996
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4
url: https://github.com/caeser1996
- login: ebottos94
count: 6
avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4
url: https://github.com/ebottos94
- login: jgould22
count: 6
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4
url: https://github.com/jgould22
- login: Kludex
count: 5
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4
url: https://github.com/Kludex
- login: clemens-tolboom
- login: abhint
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=5b9f9f6192c83ca86a411eafd4be46d9e5828585&v=4
url: https://github.com/abhint
- login: chrisK824
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/371014?v=4
url: https://github.com/clemens-tolboom
- login: williamjamir
avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4
url: https://github.com/chrisK824
- login: djimontyp
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/5083518?u=b76ca8e08b906a86fa195fb817dd94e8d9d3d8f6&v=4
url: https://github.com/williamjamir
- login: nymous
avatarUrl: https://avatars.githubusercontent.com/u/53098395?u=583bade70950b277c322d35f1be2b75c7b0f189c&v=4
url: https://github.com/djimontyp
- login: JavierSanchezCastro
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4
url: https://github.com/nymous
- login: frankie567
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4
url: https://github.com/frankie567
avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4
url: https://github.com/JavierSanchezCastro
top_contributors:
- login: waynerv
count: 25
@@ -263,6 +251,10 @@ top_contributors:
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
url: https://github.com/mariacamilagl
- login: Xewus
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4
url: https://github.com/Xewus
- login: Smlep
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4
@@ -271,6 +263,10 @@ top_contributors:
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4
url: https://github.com/Serrones
- login: rjNemo
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4
url: https://github.com/rjNemo
- login: RunningIkkyu
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4
@@ -279,10 +275,6 @@ top_contributors:
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders
- login: rjNemo
count: 7
avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4
url: https://github.com/rjNemo
- login: batlopes
count: 6
avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4
@@ -291,6 +283,10 @@ top_contributors:
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
url: https://github.com/wshayes
- login: samuelcolvin
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4
url: https://github.com/samuelcolvin
- login: SwftAlpc
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4
@@ -307,18 +303,10 @@ top_contributors:
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4
url: https://github.com/NinaHwang
- login: Xewus
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4
url: https://github.com/Xewus
- login: jekirl
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4
url: https://github.com/jekirl
- login: samuelcolvin
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4
url: https://github.com/samuelcolvin
- login: jfunez
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4
@@ -339,9 +327,13 @@ top_contributors:
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4
url: https://github.com/lsglucas
- login: axel584
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4
url: https://github.com/axel584
top_reviewers:
- login: Kludex
count: 111
count: 117
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4
url: https://github.com/Kludex
- login: BilalAlpaslan
@@ -349,8 +341,8 @@ top_reviewers:
avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4
url: https://github.com/BilalAlpaslan
- login: yezz123
count: 71
avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4
count: 74
avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4
url: https://github.com/yezz123
- login: tokusumi
count: 51
@@ -384,6 +376,10 @@ top_reviewers:
count: 33
avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4
url: https://github.com/AdrianDeAnda
- login: Xewus
count: 32
avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4
url: https://github.com/Xewus
- login: ArcLightSlavik
count: 31
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4
@@ -400,30 +396,34 @@ top_reviewers:
count: 26
avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4
url: https://github.com/lsglucas
- login: Ryandaydev
count: 24
avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4
url: https://github.com/Ryandaydev
- login: dmontagu
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
url: https://github.com/dmontagu
- login: LorhanSohaky
count: 22
count: 23
avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4
url: https://github.com/LorhanSohaky
- login: rjNemo
count: 20
count: 21
avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4
url: https://github.com/rjNemo
- login: hard-coders
count: 20
count: 21
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders
- login: odiseo0
count: 20
avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4
url: https://github.com/odiseo0
- login: 0417taehyun
count: 19
avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4
url: https://github.com/0417taehyun
- login: odiseo0
count: 19
avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4
url: https://github.com/odiseo0
- login: Smlep
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4
@@ -452,34 +452,38 @@ top_reviewers:
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4
url: https://github.com/delhi09
- login: Ryandaydev
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4
url: https://github.com/Ryandaydev
- login: Xewus
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4
url: https://github.com/Xewus
- login: sh0nk
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4
url: https://github.com/sh0nk
- login: peidrao
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4
avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5b94b548ef0002ef3219d7c07ac0fac17c6201a2&v=4
url: https://github.com/peidrao
- login: r0b2g1t
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4
url: https://github.com/r0b2g1t
- login: RunningIkkyu
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4
url: https://github.com/RunningIkkyu
- login: axel584
count: 12
avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4
url: https://github.com/axel584
- login: solomein-sv
count: 11
avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4
avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4
url: https://github.com/solomein-sv
- login: mariacamilagl
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
url: https://github.com/mariacamilagl
- login: raphaelauv
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
url: https://github.com/raphaelauv
- login: Attsun1031
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4
@@ -492,10 +496,10 @@ top_reviewers:
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4
url: https://github.com/ComicShrimp
- login: r0b2g1t
- login: Alexandrhub
count: 10
avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4
url: https://github.com/r0b2g1t
avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4
url: https://github.com/Alexandrhub
- login: izaguerreiro
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4
@@ -516,23 +520,11 @@ top_reviewers:
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4
url: https://github.com/bezaca
- login: dimaqq
- login: oandersonmagalhaes
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4
url: https://github.com/dimaqq
- login: raphaelauv
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
url: https://github.com/raphaelauv
- login: axel584
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/1334088?v=4
url: https://github.com/axel584
- login: blt232018
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4
url: https://github.com/blt232018
- login: rogerbrinkmann
count: 8
avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4
url: https://github.com/rogerbrinkmann
avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4
url: https://github.com/oandersonmagalhaes
- login: NinaHwang
count: 9
avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4
url: https://github.com/NinaHwang

View File

@@ -2,13 +2,13 @@ gold:
- url: https://cryptapi.io/
title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway."
img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg
- url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023
title: "Build, run and scale your apps on a modern, reliable, and secure PaaS."
img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png
silver:
- url: https://www.deta.sh/?ref=fastapi
title: The launchpad for all your (team's) ideas
img: https://fastapi.tiangolo.com/img/sponsors/deta.svg
- url: https://www.investsuite.com/jobs
title: Wealthtech jobs with FastAPI
img: https://fastapi.tiangolo.com/img/sponsors/investsuite.svg
- url: https://training.talkpython.fm/fastapi-courses
title: FastAPI video courses on demand from people you trust
img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png

View File

@@ -15,3 +15,4 @@ logins:
- svix
- armand-sauzay
- databento-bot
- nanram22

View File

@@ -108,7 +108,7 @@ After activating the environment as described above:
<div class="termy">
```console
$ pip install -e ".[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```
@@ -121,10 +121,15 @@ It will install all the dependencies and your local FastAPI in your local enviro
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
And if you update that local FastAPI source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastAPI you just edited.
And if you update that local FastAPI source code when you run that Python file again, it will use the fresh version of FastAPI you just edited.
That way, you don't have to "install" your local version to be able to test every change.
!!! note "Technical Details"
This only happens when you install using this included `requiements.txt` instead of installing `pip install fastapi` directly.
That is because inside of the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option.
### Format
There is a script that you can run that will format and clean all your code:

View File

@@ -276,7 +276,7 @@ Also, notice that Deta Space correctly handles HTTPS for you, so you don't have
## Create a release
Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Data Space cloud.
Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Deta Space cloud.
To do so, run `space release` in the Space CLI to create an **unlisted release**:

View File

@@ -189,8 +189,6 @@ With **FastAPI** you get all of **Pydantic**'s features (as FastAPI is based on
* If you know Python types you know how to use Pydantic.
* Plays nicely with your **<abbr title="Integrated Development Environment, similar to a code editor">IDE</abbr>/<abbr title="A program that checks for code errors">linter</abbr>/brain**:
* Because pydantic data structures are just instances of classes you define; auto-completion, linting, mypy and your intuition should all work properly with your validated data.
* **Fast**:
* in <a href="https://pydantic-docs.helpmanual.io/benchmarks/" class="external-link" target="_blank">benchmarks</a> Pydantic is faster than all other tested libraries.
* Validate **complex structures**:
* Use of hierarchical Pydantic models, Python `typing`s `List` and `Dict`, etc.
* And validators allow complex data schemas to be clearly and easily defined, checked and documented as JSON Schema.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -2,6 +2,92 @@
## Latest Changes
* 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo).
## 0.97.0
### Features
* ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca).
* ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur).
### Refactors
* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo).
* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo).
## 0.96.1
### Fixes
* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* 📌 Update minimum version of Pydantic to >=1.7.4. This fixes an issue when trying to use an old version of Pydantic. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex).
### Refactors
* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex).
* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy).
### Docs
* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex).
### Translations
* 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon).
* 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub).
### Internal
* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo).
## 0.96.0
### Features
* ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz) and previous original PR by [@huonw](https://github.com/huonw).
### Docs
* 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98).
* ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte).
### Translations
* 🌐 Add Russian translation for `docs/tutorial/body.md`. PR [#3885](https://github.com/tiangolo/fastapi/pull/3885) by [@solomein-sv](https://github.com/solomein-sv).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/static-files.md`. PR [#9580](https://github.com/tiangolo/fastapi/pull/9580) by [@Alexandrhub](https://github.com/Alexandrhub).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn).
* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99).
* 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc).
* 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus).
* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub).
### Internal
* 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions).
* 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo).
## 0.95.2

View File

@@ -28,6 +28,12 @@
<img class="sponsor-image" src="/img/sponsors/cryptapi-banner.svg" />
</a>
</div>
<div class="item">
<a title="Build, run and scale your apps on a modern, reliable, and secure PaaS." style="display: block; position: relative;" href="https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/platform-sh-banner.png" />
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -104,24 +104,40 @@ Para entender las diferencias, imagina la siguiente historia sobre hamburguesas:
Vas con la persona que te gusta 😍 a pedir comida rápida 🍔, haces cola mientras el cajero 💁 recoge los pedidos de las personas de delante tuyo.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-01.png" alt="illustration">
Llega tu turno, haces tu pedido de 2 hamburguesas impresionantes para esa persona 😍 y para ti.
Pagas 💸.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-02.png" alt="illustration">
El cajero 💁 le dice algo al chico de la cocina 👨‍🍳 para que sepa que tiene que preparar tus hamburguesas 🍔 (a pesar de que actualmente está preparando las de los clientes anteriores).
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-03.png" alt="illustration">
Pagas 💸.
El cajero 💁 te da el número de tu turno.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-04.png" alt="illustration">
Mientras esperas, vas con esa persona 😍 y eliges una mesa, se sientan y hablan durante un rato largo (ya que las hamburguesas son muy impresionantes y necesitan un rato para prepararse ✨🍔✨).
Mientras te sientas en la mesa con esa persona 😍, esperando las hamburguesas 🍔, puedes disfrutar ese tiempo admirando lo increíble, inteligente, y bien que se ve ✨😍✨.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-05.png" alt="illustration">
Mientras esperas y hablas con esa persona 😍, de vez en cuando, verificas el número del mostrador para ver si ya es tu turno.
Al final, en algún momento, llega tu turno. Vas al mostrador, coges tus hamburguesas 🍔 y vuelves a la mesa.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-06.png" alt="illustration">
Tú y esa persona 😍 se comen las hamburguesas 🍔 y la pasan genial ✨.
<img src="https://fastapi.tiangolo.com/img/async/concurrent-burgers/concurrent-burgers-07.png" alt="illustration">
!!! info
Las ilustraciones fueron creados por <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨
---
Imagina que eres el sistema / programa 🤖 en esa historia.
@@ -150,26 +166,41 @@ Haces la cola mientras varios cajeros (digamos 8) que a la vez son cocineros
Todos los que están antes de ti están esperando 🕙 que sus hamburguesas 🍔 estén listas antes de dejar el mostrador porque cada uno de los 8 cajeros prepara la hamburguesa de inmediato antes de recibir el siguiente pedido.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-01.png" alt="illustration">
Entonces finalmente es tu turno, haces tu pedido de 2 hamburguesas 🍔 impresionantes para esa persona 😍 y para ti.
Pagas 💸.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-02.png" alt="illustration">
El cajero va a la cocina 👨‍🍳.
Esperas, de pie frente al mostrador 🕙, para que nadie más recoja tus hamburguesas 🍔, ya que no hay números para los turnos.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-03.png" alt="illustration">
Como tu y esa persona 😍 están ocupados en impedir que alguien se ponga delante y recoja tus hamburguesas apenas llegan 🕙, tampoco puedes prestarle atención a esa persona 😞.
Este es un trabajo "síncrono", estás "sincronizado" con el cajero / cocinero 👨‍🍳. Tienes que esperar y estar allí en el momento exacto en que el cajero / cocinero 👨‍🍳 termina las hamburguesas 🍔 y te las da, o de lo contrario, alguien más podría cogerlas.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-04.png" alt="illustration">
Luego, el cajero / cocinero 👨‍🍳 finalmente regresa con tus hamburguesas 🍔, después de mucho tiempo esperando 🕙 frente al mostrador.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-05.png" alt="illustration">
Cojes tus hamburguesas 🍔 y vas a la mesa con esa persona 😍.
Sólo las comes y listo 🍔 ⏹.
<img src="https://fastapi.tiangolo.com/img/async/parallel-burgers/parallel-burgers-06.png" alt="illustration">
No has hablado ni coqueteado mucho, ya que has pasado la mayor parte del tiempo esperando 🕙 frente al mostrador 😞.
!!! info
Las ilustraciones fueron creados por <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨
---
En este escenario de las hamburguesas paralelas, tú eres un sistema / programa 🤖 con dos procesadores (tú y la persona que te gusta 😍), ambos esperando 🕙 y dedicando su atención ⏯ a estar "esperando en el mostrador" 🕙 durante mucho tiempo.
@@ -240,7 +271,7 @@ Pero en este caso, si pudieras traer a los 8 ex cajeros / cocineros / ahora limp
En este escenario, cada uno de los limpiadores (incluido tú) sería un procesador, haciendo su parte del trabajo.
Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una <abbr title = "Central Processing Unit. En español: Unidad Central de Procesamiento."> CPU </abbr>, a estos problemas se les llama "<abbr title="En español: atado a CPU.">CPU bond</abbr>".
Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una <abbr title = "Central Processing Unit. En español: Unidad Central de Procesamiento."> CPU </abbr>, a estos problemas se les llama "<abbr title="En español: atado a CPU.">CPU bound</abbr>".
---
@@ -257,7 +288,7 @@ Por ejemplo:
Con **FastAPI** puedes aprovechar la concurrencia que es muy común para el desarrollo web (atractivo principal de NodeJS).
Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bond** como las de los sistemas de Machine Learning.
Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bound** como las de los sistemas de Machine Learning.
Eso, más el simple hecho de que Python es el lenguaje principal para **Data Science**, Machine Learning y especialmente Deep Learning, hacen de FastAPI una muy buena combinación para las API y aplicaciones web de Data Science / Machine Learning (entre muchas otras).

View File

@@ -10,9 +10,9 @@ Sehingga kamu dapat kembali lagi dan mencari apa yang kamu butuhkan dengan tepat
## Jalankan kode
Semua blok-blok kode dapat dicopy dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji).
Semua blok-blok kode dapat disalin dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji).
Untuk menjalankan setiap contoh, copy kode ke file `main.py`, dan jalankan `uvicorn` dengan:
Untuk menjalankan setiap contoh, salin kode ke file `main.py`, dan jalankan `uvicorn` dengan:
<div class="termy">
@@ -28,7 +28,7 @@ $ uvicorn main:app --reload
</div>
**SANGAT disarankan** agar kamu menulis atau meng-copy kode, meng-editnya dan menjalankannya secara lokal.
**SANGAT disarankan** agar kamu menulis atau menyalin kode, mengubahnya dan menjalankannya secara lokal.
Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari FastAPI, melihat bagaimana sedikitnya kode yang harus kamu tulis, semua pengecekan tipe, pelengkapan otomatis, dll.
@@ -38,7 +38,7 @@ Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari F
Langkah pertama adalah dengan meng-install FastAPI.
Untuk tutorial, kamu mungkin hendak meng-instalnya dengan semua pilihan fitur dan dependensinya:
Untuk tutorial, kamu mungkin hendak meng-installnya dengan semua pilihan fitur dan dependensinya:
<div class="termy">
@@ -53,15 +53,15 @@ $ pip install "fastapi[all]"
...yang juga termasuk `uvicorn`, yang dapat kamu gunakan sebagai server yang menjalankan kodemu.
!!! catatan
Kamu juga dapat meng-instalnya bagian demi bagian.
Kamu juga dapat meng-installnya bagian demi bagian.
Hal ini mungkin yang akan kamu lakukan ketika kamu hendak men-deploy aplikasimu ke tahap produksi:
Hal ini mungkin yang akan kamu lakukan ketika kamu hendak menyebarkan (men-deploy) aplikasimu ke tahap produksi:
```
pip install fastapi
```
Juga install `uvicorn` untk menjalankan server"
Juga install `uvicorn` untuk menjalankan server"
```
pip install "uvicorn[standard]"
@@ -77,4 +77,4 @@ Tersedia juga **Pedoman Pengguna Lanjutan** yang dapat kamu baca nanti setelah *
Tetapi kamu harus membaca terlebih dahulu **Tutorial - Pedoman Pengguna** (apa yang sedang kamu baca sekarang).
Hal ini didesain sehingga kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**.
Hal ini dirancang supaya kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**.

View File

@@ -97,7 +97,7 @@ $ python -m venv env
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

View File

@@ -98,7 +98,7 @@ Após ativar o ambiente como descrito acima:
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

View File

@@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

View File

@@ -0,0 +1,311 @@
# Концепции развёртывания
Существует несколько концепций, применяемых для развёртывания приложений **FastAPI**, равно как и для любых других типов веб-приложений, среди которых Вы можете выбрать **наиболее подходящий** способ.
Самые важные из них:
* Использование более безопасного протокола HTTPS
* Настройки запуска приложения
* Перезагрузка приложения
* Запуск нескольких экземпляров приложения
* Управление памятью
* Использование перечисленных функций перед запуском приложения.
Рассмотрим ниже влияние каждого из них на процесс **развёртывания**.
Наша конечная цель - **обслуживать клиентов Вашего API безопасно** и **бесперебойно**, с максимально эффективным использованием **вычислительных ресурсов** (например, удалённых серверов/виртуальных машин). 🚀
Здесь я немного расскажу Вам об этих **концепциях** и надеюсь, что у Вас сложится **интуитивное понимание**, какой способ выбрать при развертывании Вашего API в различных окружениях, возможно, даже **ещё не существующих**.
Ознакомившись с этими концепциями, Вы сможете **оценить и выбрать** лучший способ развёртывании **Вашего API**.
В последующих главах я предоставлю Вам **конкретные рецепты** развёртывания приложения FastAPI.
А сейчас давайте остановимся на важных **идеях этих концепций**. Эти идеи можно также применить и к другим типам веб-приложений. 💡
## Использование более безопасного протокола HTTPS
В [предыдущей главе об HTTPS](./https.md){.internal-link target=_blank} мы рассмотрели, как HTTPS обеспечивает шифрование для Вашего API.
Также мы заметили, что обычно для работы с HTTPS Вашему приложению нужен **дополнительный** компонент - **прокси-сервер завершения работы TLS**.
И если прокси-сервер не умеет сам **обновлять сертификаты HTTPS**, то нужен ещё один компонент для этого действия.
### Примеры инструментов для работы с HTTPS
Вот некоторые инструменты, которые Вы можете применять как прокси-серверы:
* Traefik
* С автоматическим обновлением сертификатов ✨
* Caddy
* С автоматическим обновлением сертификатов ✨
* Nginx
* С дополнительным компонентом типа Certbot для обновления сертификатов
* HAProxy
* С дополнительным компонентом типа Certbot для обновления сертификатов
* Kubernetes с Ingress Controller похожим на Nginx
* С дополнительным компонентом типа cert-manager для обновления сертификатов
* Использование услуг облачного провайдера (читайте ниже 👇)
В последнем варианте Вы можете воспользоваться услугами **облачного сервиса**, который сделает большую часть работы, включая настройку HTTPS. Это может наложить дополнительные ограничения или потребовать дополнительную плату и т.п. Зато Вам не понадобится самостоятельно заниматься настройками прокси-сервера.
В дальнейшем я покажу Вам некоторые конкретные примеры их применения.
---
Следующие концепции рассматривают применение программы, запускающей Ваш API (такой как Uvicorn).
## Программа и процесс
Мы часто будем встречать слова **процесс** и **программа**, потому следует уяснить отличия между ними.
### Что такое программа
Термином **программа** обычно описывают множество вещей:
* **Код**, который Вы написали, в нашем случае **Python-файлы**.
* **Файл**, который может быть **исполнен** операционной системой, например `python`, `python.exe` или `uvicorn`.
* Конкретная программа, **запущенная** операционной системой и использующая центральный процессор и память. В таком случае это также называется **процесс**.
### Что такое процесс
Термин **процесс** имеет более узкое толкование, подразумевая что-то, запущенное операционной системой (как в последнем пункте из вышестоящего абзаца):
* Конкретная программа, **запущенная** операционной системой.
* Это не имеет отношения к какому-либо файлу или коду, но нечто **определённое**, управляемое и **выполняемое** операционной системой.
* Любая программа, любой код, **могут делать что-то** только когда они **выполняются**. То есть, когда являются **работающим процессом**.
* Процесс может быть **прерван** (или "убит") Вами или Вашей операционной системой. В результате чего он перестанет исполняться и **не будет продолжать делать что-либо**.
* Каждое приложение, которое Вы запустили на своём компьютере, каждая программа, каждое "окно" запускает какой-то процесс. И обычно на включенном компьютере **одновременно** запущено множество процессов.
* И **одна программа** может запустить **несколько параллельных процессов**.
Если Вы заглянете в "диспетчер задач" или "системный монитор" (или аналогичные инструменты) Вашей операционной системы, то увидите множество работающих процессов.
Вполне вероятно, что Вы увидите несколько процессов с одним и тем же названием браузерной программы (Firefox, Chrome, Edge и т. Д.). Обычно браузеры запускают один процесс на вкладку и вдобавок некоторые дополнительные процессы.
<img class="shadow" src="/img/deployment/concepts/image01.png">
---
Теперь, когда нам известна разница между **процессом** и **программой**, давайте продолжим обсуждение развёртывания.
## Настройки запуска приложения
В большинстве случаев когда Вы создаёте веб-приложение, то желаете, чтоб оно **работало постоянно** и непрерывно, предоставляя клиентам доступ в любое время. Хотя иногда у Вас могут быть причины, чтоб оно запускалось только при определённых условиях.
### Удалённый сервер
Когда Вы настраиваете удалённый сервер (облачный сервер, виртуальную машину и т.п.), самое простое, что можно сделать, запустить Uvicorn (или его аналог) вручную, как Вы делаете при локальной разработке.
Это рабочий способ и он полезен **во время разработки**.
Но если Вы потеряете соединение с сервером, то не сможете отслеживать - работает ли всё ещё **запущенный Вами процесс**.
И если сервер перезагрузится (например, после обновления или каких-то действий облачного провайдера), Вы скорее всего **этого не заметите**, чтобы снова запустить процесс вручную. Вследствие этого Ваш API останется мёртвым. 😱
### Автоматический запуск программ
Вероятно Вы пожелаете, чтоб Ваша серверная программа (такая как Uvicorn) стартовала автоматически при включении сервера, без **человеческого вмешательства** и всегда могла управлять Вашим API (так как Uvicorn запускает приложение FastAPI).
### Отдельная программа
Для этого у обычно используют отдельную программу, которая следит за тем, чтобы Ваши приложения запускались при включении сервера. Такой подход гарантирует, что другие компоненты или приложения также будут запущены, например, база данных
### Примеры инструментов, управляющих запуском программ
Вот несколько примеров, которые могут справиться с такой задачей:
* Docker
* Kubernetes
* Docker Compose
* Docker в режиме Swarm
* Systemd
* Supervisor
* Использование услуг облачного провайдера
* Прочие...
Я покажу Вам некоторые примеры их использования в следующих главах.
## Перезапуск
Вы, вероятно, также пожелаете, чтоб Ваше приложение **перезапускалось**, если в нём произошёл сбой.
### Мы ошибаемся
Все люди совершают **ошибки**. Программное обеспечение почти *всегда* содержит **баги** спрятавшиеся в разных местах. 🐛
И мы, будучи разработчиками, продолжаем улучшать код, когда обнаруживаем в нём баги или добавляем новый функционал (возможно, добавляя при этом баги 😅).
### Небольшие ошибки обрабатываются автоматически
Когда Вы создаёте свои API на основе FastAPI и допускаете в коде ошибку, то FastAPI обычно остановит её распространение внутри одного запроса, при обработке которого она возникла. 🛡
Клиент получит ошибку **500 Internal Server Error** в ответ на свой запрос, но приложение не сломается и будет продолжать работать с последующими запросами.
### Большие ошибки - Падение приложений
Тем не менее, может случиться так, что ошибка вызовет **сбой всего приложения** или даже сбой в Uvicorn, а то и в самом Python. 💥
Но мы всё ещё хотим, чтобы приложение **продолжало работать** несмотря на эту единственную ошибку, обрабатывая, как минимум, запросы к *операциям пути* не имеющим ошибок.
### Перезапуск после падения
Для случаев, когда ошибки приводят к сбою в запущенном **процессе**, Вам понадобится добавить компонент, который **перезапустит** процесс хотя бы пару раз...
!!! tip "Заметка"
... Если приложение падает сразу же после запуска, вероятно бесполезно его бесконечно перезапускать. Но полагаю, Вы заметите такое поведение во время разработки или, по крайней мере, сразу после развёртывания.
Так что давайте сосредоточимся на конкретных случаях, когда приложение может полностью выйти из строя, но всё ещё есть смысл его запустить заново.
Возможно Вы захотите, чтоб был некий **внешний компонент**, ответственный за перезапуск Вашего приложения даже если уже не работает Uvicorn или Python. То есть ничего из того, что написано в Вашем коде внутри приложения, не может быть выполнено в принципе.
### Примеры инструментов для автоматического перезапуска
В большинстве случаев инструменты **запускающие программы при старте сервера** умеют **перезапускать** эти программы.
В качестве примера можно взять те же:
* Docker
* Kubernetes
* Docker Compose
* Docker в режиме Swarm
* Systemd
* Supervisor
* Использование услуг облачного провайдера
* Прочие...
## Запуск нескольких экземпляров приложения (Репликация) - Процессы и память
Приложение FastAPI, управляемое серверной программой (такой как Uvicorn), запускается как **один процесс** и может обслуживать множество клиентов одновременно.
Но часто Вам может понадобиться несколько одновременно работающих одинаковых процессов.
### Множество процессов - Воркеры (Workers)
Если количество Ваших клиентов больше, чем может обслужить один процесс (допустим, что виртуальная машина не слишком мощная), но при этом Вам доступно **несколько ядер процессора**, то Вы можете запустить **несколько процессов** одного и того же приложения параллельно и распределить запросы между этими процессами.
**Несколько запущенных процессов** одной и той же API-программы часто называют **воркерами**.
### Процессы и порты́
Помните ли Вы, как на странице [Об HTTPS](./https.md){.internal-link target=_blank} мы обсуждали, что на сервере только один процесс может слушать одну комбинацию IP-адреса и порта?
С тех пор ничего не изменилось.
Соответственно, чтобы иметь возможность работать с **несколькими процессами** одновременно, должен быть **один процесс, прослушивающий порт** и затем каким-либо образом передающий данные каждому рабочему процессу.
### У каждого процесса своя память
Работающая программа загружает в память данные, необходимые для её работы, например, переменные содержащие модели машинного обучения или большие файлы. Каждая переменная **потребляет некоторое количество оперативной памяти (RAM)** сервера.
Обычно процессы **не делятся памятью друг с другом**. Сие означает, что каждый работающий процесс имеет свои данные, переменные и свой кусок памяти. И если для выполнения Вашего кода процессу нужно много памяти, то **каждый такой же процесс** запущенный дополнительно, потребует такого же количества памяти.
### Память сервера
Допустим, что Ваш код загружает модель машинного обучения **размером 1 ГБ**. Когда Вы запустите своё API как один процесс, он займёт в оперативной памяти не менее 1 ГБ. А если Вы запустите **4 таких же процесса** (4 воркера), то каждый из них займёт 1 ГБ оперативной памяти. В результате Вашему API потребуется **4 ГБ оперативной памяти (RAM)**.
И если Ваш удалённый сервер или виртуальная машина располагает только 3 ГБ памяти, то попытка загрузить в неё 4 ГБ данных вызовет проблемы. 🚨
### Множество процессов - Пример
В этом примере **менеджер процессов** запустит и будет управлять двумя **воркерами**.
Менеджер процессов будет слушать определённый **сокет** (IP:порт) и передавать данные работающим процессам.
Каждый из этих процессов будет запускать Ваше приложение для обработки полученного **запроса** и возвращения вычисленного **ответа** и они будут использовать оперативную память.
<img src="/img/deployment/concepts/process-ram.svg">
Безусловно, на этом же сервере будут работать и **другие процессы**, которые не относятся к Вашему приложению.
Интересная деталь - обычно в течение времени процент **использования центрального процессора (CPU)** каждым процессом может очень сильно **изменяться**, но объём занимаемой **оперативной памяти (RAM)** остаётся относительно **стабильным**.
Если у Вас есть API, который каждый раз выполняет сопоставимый объем вычислений, и у Вас много клиентов, то **загрузка процессора**, вероятно, *также будет стабильной* (вместо того, чтобы постоянно быстро увеличиваться и уменьшаться).
### Примеры стратегий и инструментов для запуска нескольких экземпляров приложения
Существует несколько подходов для достижения целей репликации и я расскажу Вам больше о конкретных стратегиях в следующих главах, например, когда речь пойдет о Docker и контейнерах.
Основное ограничение при этом - только **один** компонент может работать с определённым **портом публичного IP**. И должен быть способ **передачи** данных между этим компонентом и копиями **процессов/воркеров**.
Вот некоторые возможные комбинации и стратегии:
* **Gunicorn** управляющий **воркерами Uvicorn**
* Gunicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **множества работающих процессов Uvicorn**.
* **Uvicorn** управляющий **воркерами Uvicorn**
* Один процесс Uvicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Он будет запускать **множество работающих процессов Uvicorn**.
* **Kubernetes** и аналогичные **контейнерные системы**
* Какой-то компонент в **Kubernetes** будет слушать **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **нескольких контейнеров**, в каждом из которых работает **один процесс Uvicorn**.
* **Облачные сервисы**, которые позаботятся обо всём за Вас
* Возможно, что облачный сервис умеет **управлять запуском дополнительных экземпляров приложения**. Вероятно, он потребует, чтоб Вы указали - какой **процесс** или **образ** следует клонировать. Скорее всего, Вы укажете **один процесс Uvicorn** и облачный сервис будет запускать его копии при необходимости.
!!! tip "Заметка"
Если Вы не знаете, что такое **контейнеры**, Docker или Kubernetes, не переживайте.
Я поведаю Вам о контейнерах, образах, Docker, Kubernetes и т.п. в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}.
## Шаги, предшествующие запуску
Часто бывает, что Вам необходимо произвести какие-то подготовительные шаги **перед запуском** своего приложения.
Например, запустить **миграции базы данных**.
Но в большинстве случаев такие действия достаточно произвести **однократно**.
Поэтому Вам нужен будет **один процесс**, выполняющий эти **подготовительные шаги** до запуска приложения.
Также Вам нужно будет убедиться, что этот процесс выполнил подготовительные шаги *даже* если впоследствии Вы запустите **несколько процессов** (несколько воркеров) самого приложения. Если бы эти шаги выполнялись в каждом **клонированном процессе**, они бы **дублировали** работу, пытаясь выполнить её **параллельно**. И если бы эта работа была бы чем-то деликатным, вроде миграции базы данных, то это может вызвать конфликты между ними.
Безусловно, возможны случаи, когда нет проблем при выполнении предварительной подготовки параллельно или несколько раз. Тогда Вам повезло, работать с ними намного проще.
!!! tip "Заметка"
Имейте в виду, что в некоторых случаях запуск Вашего приложения **может не требовать каких-либо предварительных шагов вовсе**.
Что ж, тогда Вам не нужно беспокоиться об этом. 🤷
### Примеры стратегий запуска предварительных шагов
Существует **сильная зависимость** от того, как Вы **развёртываете свою систему**, запускаете программы, обрабатываете перезапуски и т.д.
Вот некоторые возможные идеи:
* При использовании Kubernetes нужно предусмотреть "инициализирующий контейнер", запускаемый до контейнера с приложением.
* Bash-скрипт, выполняющий предварительные шаги, а затем запускающий приложение.
* При этом Вам всё ещё нужно найти способ - как запускать/перезапускать *такой* bash-скрипт, обнаруживать ошибки и т.п.
!!! tip "Заметка"
Я приведу Вам больше конкретных примеров работы с контейнерами в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}.
## Утилизация ресурсов
Ваш сервер располагает ресурсами, которые Ваши программы могут потреблять или **утилизировать**, а именно - время работы центрального процессора и объём оперативной памяти.
Как много системных ресурсов Вы предполагаете потребить/утилизировать? Если не задумываться, то можно ответить - "немного", но на самом деле Вы, вероятно, пожелаете использовать **максимально возможное количество**.
Если Вы платите за содержание трёх серверов, но используете лишь малую часть системных ресурсов каждого из них, то Вы **выбрасываете деньги на ветер**, а также **впустую тратите электроэнергию** и т.п.
В таком случае было бы лучше обойтись двумя серверами, но более полно утилизировать их ресурсы (центральный процессор, оперативную память, жёсткий диск, сети передачи данных и т.д).
С другой стороны, если Вы располагаете только двумя серверами и используете **на 100% их процессоры и память**, но какой-либо процесс запросит дополнительную память, то операционная система сервера будет использовать жёсткий диск для расширения оперативной памяти (а диск работает в тысячи раз медленнее), а то вовсе **упадёт**. Или если какому-то процессу понадобится произвести вычисления, то ему придётся подождать, пока процессор освободится.
В такой ситуации лучше подключить **ещё один сервер** и перераспределить процессы между серверами, чтоб всем **хватало памяти и процессорного времени**.
Также есть вероятность, что по какой-то причине возник **всплеск** запросов к Вашему API. Возможно, это был вирус, боты или другие сервисы начали пользоваться им. И для таких происшествий Вы можете захотеть иметь дополнительные ресурсы.
При настройке логики развёртываний, Вы можете указать **целевое значение** утилизации ресурсов, допустим, **от 50% до 90%**. Обычно эти метрики и используют.
Вы можете использовать простые инструменты, такие как `htop`, для отслеживания загрузки центрального процессора и оперативной памяти сервера, в том числе каждым процессом. Или более сложные системы мониторинга нескольких серверов.
## Резюме
Вы прочитали некоторые из основных концепций, которые необходимо иметь в виду при принятии решения о развертывании приложений:
* Использование более безопасного протокола HTTPS
* Настройки запуска приложения
* Перезагрузка приложения
* Запуск нескольких экземпляров приложения
* Управление памятью
* Использование перечисленных функций перед запуском приложения.
Осознание этих идей и того, как их применять, должно дать Вам интуитивное понимание, необходимое для принятия решений при настройке развертываний. 🤓
В следующих разделах я приведу более конкретные примеры возможных стратегий, которым Вы можете следовать. 🚀

View File

@@ -0,0 +1,309 @@
# Body - Множество параметров
Теперь, когда мы увидели, как использовать `Path` и `Query` параметры, давайте рассмотрим более продвинутые примеры обьявления тела запроса.
## Обьединение `Path`, `Query` и параметров тела запроса
Во-первых, конечно, вы можете объединять параметры `Path`, `Query` и объявления тела запроса в своих функциях обработки, **FastAPI** автоматически определит, что с ними нужно делать.
Вы также можете объявить параметры тела запроса как необязательные, установив значение по умолчанию, равное `None`:
=== "Python 3.10+"
```Python hl_lines="18-20"
{!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="18-20"
{!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="19-21"
{!> ../../../docs_src/body_multiple_params/tutorial001_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="17-19"
{!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="19-21"
{!> ../../../docs_src/body_multiple_params/tutorial001.py!}
```
!!! Заметка
Заметьте, что в данном случае параметр `item`, который будет взят из тела запроса, необязателен. Так как было установлено значение `None` по умолчанию.
## Несколько параметров тела запроса
В предыдущем примере, *операции пути* ожидали тело запроса в формате JSON-тело с параметрами, соответствующими атрибутам `Item`, например:
```JSON
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
```
Но вы также можете объявить множество параметров тела запроса, например `item` и `user`:
=== "Python 3.10+"
```Python hl_lines="20"
{!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="22"
{!> ../../../docs_src/body_multiple_params/tutorial002.py!}
```
В этом случае **FastAPI** заметит, что в функции есть более одного параметра тела (два параметра, которые являются моделями Pydantic).
Таким образом, имена параметров будут использоваться в качестве ключей (имён полей) в теле запроса, и будет ожидаться запрос следующего формата:
```JSON
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}
```
!!! Внимание
Обратите внимание, что хотя параметр `item` был объявлен таким же способом, как и раньше, теперь предпологается, что он находится внутри тела с ключом `item`.
**FastAPI** сделает автоматические преобразование из запроса, так что параметр `item` получит своё конкретное содержимое, и то же самое происходит с пользователем `user`.
Произойдёт проверка составных данных, и создание документации в схеме OpenAPI и автоматических документах.
## Отдельные значения в теле запроса
Точно так же, как `Query` и `Path` используются для определения дополнительных данных для query и path параметров, **FastAPI** предоставляет аналогичный инструмент - `Body`.
Например, расширяя предыдущую модель, вы можете решить, что вам нужен еще один ключ `importance` в том же теле запроса, помимо параметров `item` и `user`.
Если вы объявите его без указания, какой именно объект (Path, Query, Body и .т.п.) ожидаете, то, поскольку это является простым типом данных, **FastAPI** будет считать, что это query-параметр.
Но вы можете указать **FastAPI** обрабатывать его, как ещё один ключ тела запроса, используя `Body`:
=== "Python 3.10+"
```Python hl_lines="23"
{!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="23"
{!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="24"
{!> ../../../docs_src/body_multiple_params/tutorial003_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="20"
{!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="22"
{!> ../../../docs_src/body_multiple_params/tutorial003.py!}
```
В этом случае, **FastAPI** будет ожидать тело запроса в формате:
```JSON
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
},
"importance": 5
}
```
И всё будет работать так же - преобразование типов данных, валидация, документирование и т.д.
## Множество body и query параметров
Конечно, вы также можете объявлять query-параметры в любое время, дополнительно к любым body-параметрам.
Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так:
```Python
q: Union[str, None] = None
```
Или в Python 3.10 и выше:
```Python
q: str | None = None
```
Например:
=== "Python 3.10+"
```Python hl_lines="27"
{!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="27"
{!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="28"
{!> ../../../docs_src/body_multiple_params/tutorial004_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="25"
{!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="27"
{!> ../../../docs_src/body_multiple_params/tutorial004.py!}
```
!!! Информация
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже.
## Добавление одного body-параметра
Предположим, у вас есть только один body-параметр `item` из Pydantic модели `Item`.
По умолчанию, **FastAPI** ожидает получить тело запроса напрямую.
Но если вы хотите чтобы он ожидал JSON с ключом `item` с содержимым модели внутри, также как это происходит при объявлении дополнительных body-параметров, вы можете использовать специальный параметр `embed` у типа `Body`:
```Python
item: Item = Body(embed=True)
```
так же, как в этом примере:
=== "Python 3.10+"
```Python hl_lines="17"
{!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="17"
{!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="18"
{!> ../../../docs_src/body_multiple_params/tutorial005_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="15"
{!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! Заметка
Рекомендуется использовать `Annotated` версию, если это возможно.
```Python hl_lines="17"
{!> ../../../docs_src/body_multiple_params/tutorial005.py!}
```
В этом случае **FastAPI** будет ожидать тело запроса в формате:
```JSON hl_lines="2"
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
}
```
вместо этого:
```JSON
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
```
## Резюме
Вы можете добавлять несколько body-параметров вашей *функции операции пути*, несмотря даже на то, что запрос может содержать только одно тело.
Но **FastAPI** справится с этим, предоставит правильные данные в вашей функции, а также сделает валидацию и документацию правильной схемы *операции пути*.
Вы также можете объявить отдельные значения для получения в рамках тела запроса.
И вы можете настроить **FastAPI** таким образом, чтобы включить тело запроса в ключ, даже если объявлен только один параметр.

View File

@@ -0,0 +1,165 @@
# Тело запроса
Когда вам необходимо отправить данные из клиента (допустим, браузера) в ваш API, вы отправляете их как **тело запроса**.
Тело **запроса** --- это данные, отправляемые клиентом в ваш API. Тело **ответа** --- это данные, которые ваш API отправляет клиенту.
Ваш API почти всегда отправляет тело **ответа**. Но клиентам не обязательно всегда отправлять тело **запроса**.
Чтобы объявить тело **запроса**, необходимо использовать модели <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>, со всей их мощью и преимуществами.
!!! info "Информация"
Чтобы отправить данные, необходимо использовать один из методов: `POST` (обычно), `PUT`, `DELETE` или `PATCH`.
Отправка тела с запросом `GET` имеет неопределенное поведение в спецификациях, тем не менее, оно поддерживается FastAPI только для очень сложных/экстремальных случаев использования.
Поскольку это не рекомендуется, интерактивная документация со Swagger UI не будет отображать информацию для тела при использовании метода GET, а промежуточные прокси-серверы могут не поддерживать такой вариант запроса.
## Импортирование `BaseModel` из Pydantic
Первое, что вам необходимо сделать, это импортировать `BaseModel` из пакета `pydantic`:
```Python hl_lines="4"
{!../../../docs_src/body/tutorial001.py!}
```
## Создание вашей собственной модели
После этого вы описываете вашу модель данных как класс, наследующий от `BaseModel`.
Используйте аннотации типов Python для всех атрибутов:
```Python hl_lines="7-11"
{!../../../docs_src/body/tutorial001.py!}
```
Также как и при описании параметров запроса, когда атрибут модели имеет значение по умолчанию, он является необязательным. Иначе он обязателен. Используйте `None`, чтобы сделать его необязательным без использования конкретных значений по умолчанию.
Например, модель выше описывает вот такой JSON "объект" (или словарь Python):
```JSON
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5
}
```
...поскольку `description` и `tax` являются необязательными (с `None` в качестве значения по умолчанию), вот такой JSON "объект" также подходит:
```JSON
{
"name": "Foo",
"price": 45.2
}
```
## Объявление как параметра функции
Чтобы добавить параметр к вашему *обработчику*, объявите его также, как вы объявляли параметры пути или параметры запроса:
```Python hl_lines="18"
{!../../../docs_src/body/tutorial001.py!}
```
...и укажите созданную модель в качестве типа параметра, `Item`.
## Результаты
Всего лишь с помощью аннотации типов Python, **FastAPI**:
* Читает тело запроса как JSON.
* Приводит к соответствующим типам (если есть необходимость).
* Проверяет корректность данных.
* Если данные некорректны, будет возращена читаемая и понятная ошибка, показывающая что именно и в каком месте некорректно в данных.
* Складывает полученные данные в параметр `item`.
* Поскольку внутри функции вы объявили его с типом `Item`, то теперь у вас есть поддержка со стороны редактора (автодополнение и т.п.) для всех атрибутов и их типов.
* Генерирует декларативное описание модели в виде <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a>, так что вы можете его использовать где угодно, если это имеет значение для вашего проекта.
* Эти схемы являются частью сгенерированной схемы OpenAPI и используются для автоматического документирования <abbr title="Пользовательских интерфейсов (User Interfaces)">UI</abbr>.
## Автоматическое документирование
Схема JSON ваших моделей будет частью сгенерированной схемы OpenAPI и будет отображена в интерактивной документации API:
<img src="/img/tutorial/body/image01.png">
Также она будет указана в документации по API внутри каждой *операции пути*, в которой используются:
<img src="/img/tutorial/body/image02.png">
## Поддержка редактора
В вашем редакторе внутри вашей функции у вас будут подсказки по типам и автодополнение (это не будет работать, если вы получаете словарь вместо модели Pydantic):
<img src="/img/tutorial/body/image03.png">
Также вы будете получать ошибки в случае несоответствия типов:
<img src="/img/tutorial/body/image04.png">
Это не случайно, весь фреймворк построен вокруг такого дизайна.
И это все тщательно протестировано еще на этапе разработки дизайна, до реализации, чтобы это работало со всеми редакторами.
Для поддержки этого даже были внесены некоторые изменения в сам Pydantic.
На всех предыдущих скриншотах используется <a href="https://code.visualstudio.com" class="external-link" target="_blank">Visual Studio Code</a>.
Но у вас будет такая же поддержка и с <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>, и вообще с любым редактором Python:
<img src="/img/tutorial/body/image05.png">
!!! tip "Подсказка"
Если вы используете <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> в качестве редактора, то вам стоит попробовать плагин <a href="https://github.com/koxudaxi/pydantic-pycharm-plugin/" class="external-link" target="_blank">Pydantic PyCharm Plugin</a>.
Он улучшает поддержку редактором моделей Pydantic в части:
* автодополнения,
* проверки типов,
* рефакторинга,
* поиска,
* инспектирования.
## Использование модели
Внутри функции вам доступны все атрибуты объекта модели напрямую:
```Python hl_lines="21"
{!../../../docs_src/body/tutorial002.py!}
```
## Тело запроса + параметры пути
Вы можете одновременно объявлять параметры пути и тело запроса.
**FastAPI** распознает, какие параметры функции соответствуют параметрам пути и должны быть **получены из пути**, а какие параметры функции, объявленные как модели Pydantic, должны быть **получены из тела запроса**.
```Python hl_lines="17-18"
{!../../../docs_src/body/tutorial003.py!}
```
## Тело запроса + параметры пути + параметры запроса
Вы также можете одновременно объявить параметры для **пути**, **запроса** и **тела запроса**.
**FastAPI** распознает каждый из них и возьмет данные из правильного источника.
```Python hl_lines="18"
{!../../../docs_src/body/tutorial004.py!}
```
Параметры функции распознаются следующим образом:
* Если параметр также указан в **пути**, то он будет использоваться как параметр пути.
* Если аннотация типа параметра содержит **примитивный тип** (`int`, `float`, `str`, `bool` и т.п.), он будет интерпретирован как параметр **запроса**.
* Если аннотация типа параметра представляет собой **модель Pydantic**, он будет интерпретирован как параметр **тела запроса**.
!!! note "Заметка"
FastAPI понимает, что значение параметра `q` не является обязательным, потому что имеет значение по умолчанию `= None`.
Аннотация `Optional` в `Optional[str]` не используется FastAPI, но помогает вашему редактору лучше понимать ваш код и обнаруживать ошибки.
## Без Pydantic
Если вы не хотите использовать модели Pydantic, вы все еще можете использовать параметры **тела запроса**. Читайте в документации раздел [Тело - Несколько параметров: Единичные значения в теле](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}.

View File

@@ -0,0 +1,112 @@
# Отладка
Вы можете подключить отладчик в своем редакторе, например, в Visual Studio Code или PyCharm.
## Вызов `uvicorn`
В вашем FastAPI приложении, импортируйте и вызовите `uvicorn` напрямую:
```Python hl_lines="1 15"
{!../../../docs_src/debugging/tutorial001.py!}
```
### Описание `__name__ == "__main__"`
Главная цель использования `__name__ == "__main__"` в том, чтобы код выполнялся при запуске файла с помощью:
<div class="termy">
```console
$ python myapp.py
```
</div>
но не вызывался, когда другой файл импортирует это, например::
```Python
from myapp import app
```
#### Больше деталей
Давайте назовём ваш файл `myapp.py`.
Если вы запустите его с помощью:
<div class="termy">
```console
$ python myapp.py
```
</div>
то встроенная переменная `__name__`, автоматически создаваемая Python в вашем файле, будет иметь значение строкового типа `"__main__"`.
Тогда выполнится условие и эта часть кода:
```Python
uvicorn.run(app, host="0.0.0.0", port=8000)
```
будет запущена.
---
Но этого не произойдет, если вы импортируете этот модуль (файл).
Таким образом, если у вас есть файл `importer.py` с таким импортом:
```Python
from myapp import app
# Some more code
```
то автоматическая создаваемая внутри файла `myapp.py` переменная `__name__` будет иметь значение отличающееся от `"__main__"`.
Следовательно, строка:
```Python
uvicorn.run(app, host="0.0.0.0", port=8000)
```
не будет выполнена.
!!! Информация
Для получения дополнительной информации, ознакомьтесь с <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">официальной документацией Python</a>.
## Запуск вашего кода с помощью отладчика
Так как вы запускаете сервер Uvicorn непосредственно из вашего кода, вы можете вызвать Python программу (ваше FastAPI приложение) напрямую из отладчика.
---
Например, в Visual Studio Code вы можете выполнить следующие шаги:
* Перейдите на панель "Debug".
* Выберите "Add configuration...".
* Выберите "Python"
* Запустите отладчик "`Python: Current File (Integrated Terminal)`".
Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д.
Вот как это может выглядеть:
<img src="/img/tutorial/debugging/image01.png">
---
Если используете Pycharm, вы можете выполнить следующие шаги:
* Открыть "Run" меню.
* Выбрать опцию "Debug...".
* Затем в появившемся контекстном меню.
* Выбрать файл для отладки (в данном случае, `main.py`).
Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д.
Вот как это может выглядеть:
<img src="/img/tutorial/debugging/image02.png">

View File

@@ -0,0 +1,333 @@
# Первые шаги
Самый простой FastAPI файл может выглядеть так:
```Python
{!../../../docs_src/first_steps/tutorial001.py!}
```
Скопируйте в файл `main.py`.
Запустите сервер в режиме реального времени:
<div class="termy">
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
<span style="color: green;">INFO</span>: Started reloader process [28720]
<span style="color: green;">INFO</span>: Started server process [28722]
<span style="color: green;">INFO</span>: Waiting for application startup.
<span style="color: green;">INFO</span>: Application startup complete.
```
</div>
!!! note "Технические детали"
Команда `uvicorn main:app` обращается к:
* `main`: файл `main.py` (модуль Python).
* `app`: объект, созданный внутри файла `main.py` в строке `app = FastAPI()`.
* `--reload`: перезапускает сервер после изменения кода. Используйте только для разработки.
В окне вывода появится следующая строка:
```hl_lines="4"
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
Эта строка показывает URL-адрес, по которому приложение доступно на локальной машине.
### Проверьте
Откройте браузер по адресу: <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
Вы увидите JSON-ответ следующего вида:
```JSON
{"message": "Hello World"}
```
### Интерактивная документация API
Перейдите по адресу: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
Вы увидите автоматически сгенерированную, интерактивную документацию по API (предоставленную <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>):
![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png)
### Альтернативная документация API
Теперь перейдите по адресу <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>.
Вы увидите альтернативную автоматически сгенерированную документацию (предоставленную <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>):
![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png)
### OpenAPI
**FastAPI** генерирует "схему" всего API, используя стандарт **OpenAPI**.
#### "Схема"
"Схема" - это определение или описание чего-либо. Не код, реализующий это, а только абстрактное описание.
#### API "схема"
<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> - это спецификация, которая определяет, как описывать схему API.
Определение схемы содержит пути (paths) API, их параметры и т.п.
#### "Схема" данных
Термин "схема" также может относиться к формату или структуре некоторых данных, например, JSON.
Тогда, подразумеваются атрибуты JSON, их типы данных и т.п.
#### OpenAPI и JSON Schema
OpenAPI описывает схему API. Эта схема содержит определения (или "схемы") данных, отправляемых и получаемых API. Для описания структуры данных в JSON используется стандарт **JSON Schema**.
#### Рассмотрим `openapi.json`
Если Вас интересует, как выглядит исходная схема OpenAPI, то FastAPI автоматически генерирует JSON-схему со всеми описаниями API.
Можете посмотреть здесь: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>.
Вы увидите примерно такой JSON:
```JSON
{
"openapi": "3.0.2",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
...
```
#### Для чего нужен OpenAPI
Схема OpenAPI является основой для обеих систем интерактивной документации.
Существуют десятки альтернативных инструментов, основанных на OpenAPI. Вы можете легко добавить любой из них к **FastAPI** приложению.
Вы также можете использовать OpenAPI для автоматической генерации кода для клиентов, которые взаимодействуют с API. Например, для фронтенд-, мобильных или IoT-приложений.
## Рассмотрим поэтапно
### Шаг 1: импортируйте `FastAPI`
```Python hl_lines="1"
{!../../../docs_src/first_steps/tutorial001.py!}
```
`FastAPI` это класс в Python, который предоставляет всю функциональность для API.
!!! note "Технические детали"
`FastAPI` это класс, который наследуется непосредственно от `Starlette`.
Вы можете использовать всю функциональность <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> в `FastAPI`.
### Шаг 2: создайте экземпляр `FastAPI`
```Python hl_lines="3"
{!../../../docs_src/first_steps/tutorial001.py!}
```
Переменная `app` является экземпляром класса `FastAPI`.
Это единая точка входа для создания и взаимодействия с API.
Именно к этой переменной `app` обращается `uvicorn` в команде:
<div class="termy">
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
Если создать такое приложение:
```Python hl_lines="3"
{!../../../docs_src/first_steps/tutorial002.py!}
```
И поместить его в `main.py`, тогда вызов `uvicorn` будет таким:
<div class="termy">
```console
$ uvicorn main:my_awesome_api --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
### Шаг 3: определите *операцию пути (path operation)*
#### Путь (path)
"Путь" это часть URL, после первого символа `/`, следующего за именем домена.
Для URL:
```
https://example.com/items/foo
```
...путь выглядит так:
```
/items/foo
```
!!! info "Дополнительная иформация"
Термин "path" также часто называется "endpoint" или "route".
При создании API, "путь" является основным способом разделения "задач" и "ресурсов".
#### Операция (operation)
"Операция" это один из "методов" HTTP.
Таких, как:
* `POST`
* `GET`
* `PUT`
* `DELETE`
...и более экзотических:
* `OPTIONS`
* `HEAD`
* `PATCH`
* `TRACE`
По протоколу HTTP можно обращаться к каждому пути, используя один (или несколько) из этих "методов".
---
При создании API принято использовать конкретные HTTP-методы для выполнения определенных действий.
Обычно используют:
* `POST`: создать данные.
* `GET`: прочитать.
* `PUT`: изменить (обновить).
* `DELETE`: удалить.
В OpenAPI каждый HTTP метод называется "**операция**".
Мы также будем придерживаться этого термина.
#### Определите *декоратор операции пути (path operation decorator)*
```Python hl_lines="6"
{!../../../docs_src/first_steps/tutorial001.py!}
```
Декоратор `@app.get("/")` указывает **FastAPI**, что функция, прямо под ним, отвечает за обработку запросов, поступающих по адресу:
* путь `/`
* использующих <abbr title="HTTP GET метод"><code>get</code> операцию</abbr>
!!! info "`@decorator` Дополнительная информация"
Синтаксис `@something` в Python называется "декоратор".
Вы помещаете его над функцией. Как красивую декоративную шляпу (думаю, что оттуда и происходит этот термин).
"Декоратор" принимает функцию ниже и выполняет с ней какое-то действие.
В нашем случае, этот декоратор сообщает **FastAPI**, что функция ниже соответствует **пути** `/` и **операции** `get`.
Это и есть "**декоратор операции пути**".
Можно также использовать операции:
* `@app.post()`
* `@app.put()`
* `@app.delete()`
И более экзотические:
* `@app.options()`
* `@app.head()`
* `@app.patch()`
* `@app.trace()`
!!! tip "Подсказка"
Вы можете использовать каждую операцию (HTTP-метод) по своему усмотрению.
**FastAPI** не навязывает определенного значения для каждого метода.
Информация здесь представлена как рекомендация, а не требование.
Например, при использовании GraphQL обычно все действия выполняются только с помощью POST операций.
### Шаг 4: определите **функцию операции пути**
Вот "**функция операции пути**":
* **путь**: `/`.
* **операция**: `get`.
* **функция**: функция ниже "декоратора" (ниже `@app.get("/")`).
```Python hl_lines="7"
{!../../../docs_src/first_steps/tutorial001.py!}
```
Это обычная Python функция.
**FastAPI** будет вызывать её каждый раз при получении `GET` запроса к URL "`/`".
В данном случае это асинхронная функция.
---
Вы также можете определить ее как обычную функцию вместо `async def`:
```Python hl_lines="7"
{!../../../docs_src/first_steps/tutorial003.py!}
```
!!! note "Технические детали"
Если не знаете в чём разница, посмотрите [Конкурентность: *"Нет времени?"*](../async.md#in-a-hurry){.internal-link target=_blank}.
### Шаг 5: верните результат
```Python hl_lines="8"
{!../../../docs_src/first_steps/tutorial001.py!}
```
Вы можете вернуть `dict`, `list`, отдельные значения `str`, `int` и т.д.
Также можно вернуть модели Pydantic (рассмотрим это позже).
Многие объекты и модели будут автоматически преобразованы в JSON (включая ORM). Пробуйте использовать другие объекты, которые предпочтительней для Вас, вероятно, они уже поддерживаются.
## Резюме
* Импортируем `FastAPI`.
* Создаём экземпляр `app`.
* Пишем **декоратор операции пути** (такой как `@app.get("/")`).
* Пишем **функцию операции пути** (`def root(): ...`).
* Запускаем сервер в режиме разработки (`uvicorn main:app --reload`).

View File

@@ -0,0 +1,80 @@
# Учебник - Руководство пользователя - Введение
В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций.
Каждый раздел постепенно основывается на предыдущих, но он структурирован по отдельным темам, так что вы можете перейти непосредственно к конкретной теме для решения ваших конкретных потребностей в API.
Он также создан для использования в качестве будущего справочника.
Так что вы можете вернуться и посмотреть именно то, что вам нужно.
## Запустите код
Все блоки кода можно копировать и использовать напрямую (на самом деле это проверенные файлы Python).
Чтобы запустить любой из примеров, скопируйте код в файл `main.py` и запустите `uvicorn` с параметрами:
<div class="termy">
```console
$ uvicorn main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
<span style="color: green;">INFO</span>: Started reloader process [28720]
<span style="color: green;">INFO</span>: Started server process [28722]
<span style="color: green;">INFO</span>: Waiting for application startup.
<span style="color: green;">INFO</span>: Application startup complete.
```
</div>
**НАСТОЯТЕЛЬНО рекомендуется**, чтобы вы написали или скопировали код, отредактировали его и запустили локально.
Использование кода в вашем редакторе — это то, что действительно показывает вам преимущества FastAPI, видя, как мало кода вам нужно написать, все проверки типов, автодополнение и т.д.
---
## Установка FastAPI
Первый шаг — установить FastAPI.
Для руководства вы, возможно, захотите установить его со всеми дополнительными зависимостями и функциями:
<div class="termy">
```console
$ pip install "fastapi[all]"
---> 100%
```
</div>
...это также включает `uvicorn`, который вы можете использовать в качестве сервера, который запускает ваш код.
!!! note "Технические детали"
Вы также можете установить его по частям.
Это то, что вы, вероятно, сделаете, когда захотите развернуть свое приложение в рабочей среде:
```
pip install fastapi
```
Также установите `uvicorn` для работы в качестве сервера:
```
pip install "uvicorn[standard]"
```
И то же самое для каждой из необязательных зависимостей, которые вы хотите использовать.
## Продвинутое руководство пользователя
Существует также **Продвинутое руководство пользователя**, которое вы сможете прочитать после руководства **Учебник - Руководство пользователя**.
**Продвинутое руководство пользователя** основано на этом, использует те же концепции и учит вас некоторым дополнительным функциям.
Но вы должны сначала прочитать **Учебник - Руководство пользователя** (то, что вы читаете прямо сейчас).
Он разработан таким образом, что вы можете создать полноценное приложение, используя только **Учебник - Руководство пользователя**, а затем расширить его различными способами, в зависимости от ваших потребностей, используя некоторые дополнительные идеи из **Продвинутого руководства пользователя**.

View File

@@ -0,0 +1,292 @@
# Path-параметры и валидация числовых данных
Так же, как с помощью `Query` вы можете добавлять валидацию и метаданные для query-параметров, так и с помощью `Path` вы можете добавлять такую же валидацию и метаданные для path-параметров.
## Импорт Path
Сначала импортируйте `Path` из `fastapi`, а также импортируйте `Annotated`:
=== "Python 3.10+"
```Python hl_lines="1 3"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="1 3"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="3-4"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!}
```
=== "Python 3.10+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="1"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!}
```
=== "Python 3.6+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="3"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!}
```
!!! info "Информация"
Поддержка `Annotated` была добавлена в FastAPI начиная с версии 0.95.0 (и с этой версии рекомендуется использовать этот подход).
Если вы используете более старую версию, вы столкнётесь с ошибками при попытке использовать `Annotated`.
Убедитесь, что вы [обновили версию FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} как минимум до 0.95.1 перед тем, как использовать `Annotated`.
## Определите метаданные
Вы можете указать все те же параметры, что и для `Query`.
Например, чтобы указать значение метаданных `title` для path-параметра `item_id`, вы можете написать:
=== "Python 3.10+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="11"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!}
```
=== "Python 3.10+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="8"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!}
```
=== "Python 3.6+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!}
```
!!! note "Примечание"
Path-параметр всегда является обязательным, поскольку он составляет часть пути.
Поэтому следует объявить его с помощью `...`, чтобы обозначить, что этот параметр обязательный.
Тем не менее, даже если вы объявите его как `None` или установите для него значение по умолчанию, это ни на что не повлияет и параметр останется обязательным.
## Задайте нужный вам порядок параметров
!!! tip "Подсказка"
Это не имеет большого значения, если вы используете `Annotated`.
Допустим, вы хотите объявить query-параметр `q` как обязательный параметр типа `str`.
И если вам больше ничего не нужно указывать для этого параметра, то нет необходимости использовать `Query`.
Но вам по-прежнему нужно использовать `Path` для path-параметра `item_id`. И если по какой-либо причине вы не хотите использовать `Annotated`, то могут возникнуть небольшие сложности.
Если вы поместите параметр со значением по умолчанию перед другим параметром, у которого нет значения по умолчанию, то Python укажет на ошибку.
Но вы можете изменить порядок параметров, чтобы параметр без значения по умолчанию (query-параметр `q`) шёл первым.
Это не имеет значения для **FastAPI**. Он распознает параметры по их названиям, типам и значениям по умолчанию (`Query`, `Path`, и т.д.), ему не важен их порядок.
Поэтому вы можете определить функцию так:
=== "Python 3.6 без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="7"
{!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!}
```
Но имейте в виду, что если вы используете `Annotated`, вы не столкнётесь с этой проблемой, так как вы не используете `Query()` или `Path()` в качестве значения по умолчанию для параметра функции.
=== "Python 3.9+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!}
```
## Задайте нужный вам порядок параметров, полезные приёмы
!!! tip "Подсказка"
Это не имеет большого значения, если вы используете `Annotated`.
Здесь описан **небольшой приём**, который может оказаться удобным, хотя часто он вам не понадобится.
Если вы хотите:
* объявить query-параметр `q` без `Query` и без значения по умолчанию
* объявить path-параметр `item_id` с помощью `Path`
* указать их в другом порядке
* не использовать `Annotated`
...то вы можете использовать специальную возможность синтаксиса Python.
Передайте `*` в качестве первого параметра функции.
Python не будет ничего делать с `*`, но он будет знать, что все следующие параметры являются именованными аргументами (парами ключ-значение), также известными как <abbr title="From: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>, даже если у них нет значений по умолчанию.
```Python hl_lines="7"
{!../../../docs_src/path_params_numeric_validations/tutorial003.py!}
```
### Лучше с `Annotated`
Имейте в виду, что если вы используете `Annotated`, то, поскольку вы не используете значений по умолчанию для параметров функции, то у вас не возникнет подобной проблемы и вам не придётся использовать `*`.
=== "Python 3.9+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!}
```
## Валидация числовых данных: больше или равно
С помощью `Query` и `Path` (и других классов, которые мы разберём позже) вы можете добавлять ограничения для числовых данных.
В этом примере при указании `ge=1`, параметр `item_id` должен быть больше или равен `1` ("`g`reater than or `e`qual").
=== "Python 3.9+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!}
```
=== "Python 3.6+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="8"
{!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!}
```
## Валидация числовых данных: больше и меньше или равно
То же самое применимо к:
* `gt`: больше (`g`reater `t`han)
* `le`: меньше или равно (`l`ess than or `e`qual)
=== "Python 3.9+"
```Python hl_lines="10"
{!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!}
```
=== "Python 3.6+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="9"
{!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!}
```
## Валидация числовых данных: числа с плавающей точкой, больше и меньше
Валидация также применима к значениям типа `float`.
В этом случае становится важной возможность добавить ограничение <abbr title="greater than"><code>gt</code></abbr>, вместо <abbr title="greater than or equal"><code>ge</code></abbr>, поскольку в таком случае вы можете, например, создать ограничение, чтобы значение было больше `0`, даже если оно меньше `1`.
Таким образом, `0.5` будет корректным значением. А `0.0` или `0` — нет.
То же самое справедливо и для <abbr title="less than"><code>lt</code></abbr>.
=== "Python 3.9+"
```Python hl_lines="13"
{!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="12"
{!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!}
```
=== "Python 3.6+ без Annotated"
!!! tip "Подсказка"
Рекомендуется использовать версию с `Annotated` если возможно.
```Python hl_lines="11"
{!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!}
```
## Резюме
С помощью `Query`, `Path` (и других классов, которые мы пока не затронули) вы можете добавлять метаданные и строковую валидацию тем же способом, как и в главе [Query-параметры и валидация строк](query-params-str-validations.md){.internal-link target=_blank}.
А также вы можете добавить валидацию числовых данных:
* `gt`: больше (`g`reater `t`han)
* `ge`: больше или равно (`g`reater than or `e`qual)
* `lt`: меньше (`l`ess `t`han)
* `le`: меньше или равно (`l`ess than or `e`qual)
!!! info "Информация"
`Query`, `Path` и другие классы, которые мы разберём позже, являются наследниками общего класса `Param`.
Все они используют те же параметры для дополнительной валидации и метаданных, которые вы видели ранее.
!!! note "Технические детали"
`Query`, `Path` и другие "классы", которые вы импортируете из `fastapi`, на самом деле являются функциями, которые при вызове возвращают экземпляры одноимённых классов.
Объект `Query`, который вы импортируете, является функцией. И при вызове она возвращает экземпляр одноимённого класса `Query`.
Использование функций (вместо использования классов напрямую) нужно для того, чтобы ваш редактор не подсвечивал ошибки, связанные с их типами.
Таким образом вы можете использовать привычный вам редактор и инструменты разработки, не добавляя дополнительных конфигураций для игнорирования подобных ошибок.

View File

@@ -0,0 +1,251 @@
# Path-параметры
Вы можете определить "параметры" или "переменные" пути, используя синтаксис форматированных строк Python:
```Python hl_lines="6-7"
{!../../../docs_src/path_params/tutorial001.py!}
```
Значение параметра пути `item_id` будет передано в функцию в качестве аргумента `item_id`.
Если запустите этот пример и перейдёте по адресу: <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то увидите ответ:
```JSON
{"item_id":"foo"}
```
## Параметры пути с типами
Вы можете объявить тип параметра пути в функции, используя стандартные аннотации типов Python.
```Python hl_lines="7"
{!../../../docs_src/path_params/tutorial002.py!}
```
Здесь, `item_id` объявлен типом `int`.
!!! check "Заметка"
Это обеспечит поддержку редактора внутри функции (проверка ошибок, автодополнение и т.п.).
## <abbr title="Или сериализация, парсинг">Преобразование</abbr> данных
Если запустите этот пример и перейдёте по адресу: <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>, то увидите ответ:
```JSON
{"item_id":3}
```
!!! check "Заметка"
Обратите внимание на значение `3`, которое получила (и вернула) функция. Это целочисленный Python `int`, а не строка `"3"`.
Используя определения типов, **FastAPI** выполняет автоматический <abbr title="преобразование строк из HTTP-запроса в типы данных Python">"парсинг"</abbr> запросов.
## <abbr title="Или валидация">Проверка</abbr> данных
Если откроете браузер по адресу <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то увидите интересную HTTP-ошибку:
```JSON
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
```
из-за того, что параметр пути `item_id` имеет значение `"foo"`, которое не является типом `int`.
Та же ошибка возникнет, если вместо `int` передать `float` , например: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a>
!!! check "Заметка"
**FastAPI** обеспечивает проверку типов, используя всё те же определения типов.
Обратите внимание, что в тексте ошибки явно указано место не прошедшее проверку.
Это очень полезно при разработке и отладке кода, который взаимодействует с API.
## Документация
И теперь, когда откроете браузер по адресу: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, то увидите вот такую автоматически сгенерированную документацию API:
<img src="/img/tutorial/path-params/image01.png">
!!! check "Заметка"
Ещё раз, просто используя определения типов, **FastAPI** обеспечивает автоматическую интерактивную документацию (с интеграцией Swagger UI).
Обратите внимание, что параметр пути объявлен целочисленным.
## Преимущества стандартизации, альтернативная документация
Поскольку сгенерированная схема соответствует стандарту <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md" class="external-link" target="_blank">OpenAPI</a>, её можно использовать со множеством совместимых инструментов.
Именно поэтому, FastAPI сам предоставляет альтернативную документацию API (используя ReDoc), которую можно получить по адресу: <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>.
<img src="/img/tutorial/path-params/image02.png">
По той же причине, есть множество совместимых инструментов, включая инструменты генерации кода для многих языков.
## Pydantic
Вся проверка данных выполняется под капотом с помощью <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>. Поэтому вы можете быть уверены в качестве обработки данных.
Вы можете использовать в аннотациях как простые типы данных, вроде `str`, `float`, `bool`, так и более сложные типы.
Некоторые из них рассматриваются в следующих главах данного руководства.
## Порядок имеет значение
При создании *операций пути* можно столкнуться с ситуацией, когда путь является фиксированным.
Например, `/users/me`. Предположим, что это путь для получения данных о текущем пользователе.
У вас также может быть путь `/users/{user_id}`, чтобы получить данные о конкретном пользователе по его ID.
Поскольку *операции пути* выполняются в порядке их объявления, необходимо, чтобы путь для `/users/me` был объявлен раньше, чем путь для `/users/{user_id}`:
```Python hl_lines="6 11"
{!../../../docs_src/path_params/tutorial003.py!}
```
Иначе путь для `/users/{user_id}` также будет соответствовать `/users/me`, "подразумевая", что он получает параметр `user_id` со значением `"me"`.
Аналогично, вы не можете переопределить операцию с путем:
```Python hl_lines="6 11"
{!../../../docs_src/path_params/tutorial003b.py!}
```
Первый будет выполняться всегда, так как путь совпадает первым.
## Предопределенные значения
Что если нам нужно заранее определить допустимые *параметры пути*, которые *операция пути* может принимать? В таком случае можно использовать стандартное перечисление <abbr title="Enumeration">`Enum`</abbr> Python.
### Создание класса `Enum`
Импортируйте `Enum` и создайте подкласс, который наследуется от `str` и `Enum`.
Мы наследуемся от `str`, чтобы документация API могла понять, что значения должны быть типа `string` и отображалась правильно.
Затем создайте атрибуты класса с фиксированными допустимыми значениями:
```Python hl_lines="1 6-9"
{!../../../docs_src/path_params/tutorial005.py!}
```
!!! info "Дополнительная информация"
<a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">Перечисления (enum) доступны в Python</a> начиная с версии 3.4.
!!! tip "Подсказка"
Если интересно, то "AlexNet", "ResNet" и "LeNet" - это названия <abbr title="Технически, это архитектуры моделей глубокого обучения">моделей</abbr> машинного обучения.
### Определение *параметра пути*
Определите *параметр пути*, используя в аннотации типа класс перечисления (`ModelName`), созданный ранее:
```Python hl_lines="16"
{!../../../docs_src/path_params/tutorial005.py!}
```
### Проверьте документацию
Поскольку доступные значения *параметра пути* определены заранее, интерактивная документация может наглядно их отображать:
<img src="/img/tutorial/path-params/image03.png">
### Работа с *перечислениями* в Python
Значение *параметра пути* будет *элементом перечисления*.
#### Сравнение *элементов перечисления*
Вы можете сравнить это значение с *элементом перечисления* класса `ModelName`:
```Python hl_lines="17"
{!../../../docs_src/path_params/tutorial005.py!}
```
#### Получение *значения перечисления*
Можно получить фактическое значение (в данном случае - `str`) с помощью `model_name.value` или в общем случае `your_enum_member.value`:
```Python hl_lines="20"
{!../../../docs_src/path_params/tutorial005.py!}
```
!!! tip "Подсказка"
Значение `"lenet"` также можно получить с помощью `ModelName.lenet.value`.
#### Возврат *элементов перечисления*
Из *операции пути* можно вернуть *элементы перечисления*, даже вложенные в тело JSON (например в `dict`).
Они будут преобразованы в соответствующие значения (в данном случае - строки) перед их возвратом клиенту:
```Python hl_lines="18 21 23"
{!../../../docs_src/path_params/tutorial005.py!}
```
Вы отправите клиенту такой JSON-ответ:
```JSON
{
"model_name": "alexnet",
"message": "Deep Learning FTW!"
}
```
## Path-параметры, содержащие пути
Предположим, что есть *операция пути* с путем `/files/{file_path}`.
Но вам нужно, чтобы `file_path` сам содержал *путь*, например, `home/johndoe/myfile.txt`.
Тогда URL для этого файла будет такой: `/files/home/johndoe/myfile.txt`.
### Поддержка OpenAPI
OpenAPI не поддерживает способов объявления *параметра пути*, содержащего внутри *путь*, так как это может привести к сценариям, которые сложно определять и тестировать.
Тем не менее это можно сделать в **FastAPI**, используя один из внутренних инструментов Starlette.
Документация по-прежнему будет работать, хотя и не добавит никакой информации о том, что параметр должен содержать путь.
### Конвертер пути
Благодаря одной из опций Starlette, можете объявить *параметр пути*, содержащий *путь*, используя URL вроде:
```
/files/{file_path:path}
```
В этом случае `file_path` - это имя параметра, а часть `:path`, указывает, что параметр должен соответствовать любому *пути*.
Можете использовать так:
```Python hl_lines="6"
{!../../../docs_src/path_params/tutorial004.py!}
```
!!! tip "Подсказка"
Возможно, вам понадобится, чтобы параметр содержал `/home/johndoe/myfile.txt` с ведущим слэшем (`/`).
В этом случае URL будет таким: `/files//home/johndoe/myfile.txt`, с двойным слэшем (`//`) между `files` и `home`.
## Резюме
Используя **FastAPI** вместе со стандартными объявлениями типов Python (короткими и интуитивно понятными), вы получаете:
* Поддержку редактора (проверку ошибок, автозаполнение и т.п.)
* "<abbr title="преобразование строк из HTTP-запроса в типы данных Python">Парсинг</abbr>" данных
* Валидацию данных
* Автоматическую документацию API с указанием типов параметров.
И объявлять типы достаточно один раз.
Это, вероятно, является главным заметным преимуществом **FastAPI** по сравнению с альтернативными фреймворками (кроме <abbr title="не считая оптимизаций">сырой</abbr> производительности).

View File

@@ -0,0 +1,225 @@
# Query-параметры
Когда вы объявляете параметры функции, которые не являются параметрами пути, они автоматически интерпретируются как "query"-параметры.
```Python hl_lines="9"
{!../../../docs_src/query_params/tutorial001.py!}
```
Query-параметры представляют из себя набор пар ключ-значение, которые идут после знака `?` в URL-адресе, разделенные символами `&`.
Например, в этом URL-адресе:
```
http://127.0.0.1:8000/items/?skip=0&limit=10
```
...параметры запроса такие:
* `skip`: со значением `0`
* `limit`: со значением `10`
Будучи частью URL-адреса, они "по умолчанию" являются строками.
Но когда вы объявляете их с использованием аннотаций (в примере выше, как `int`), они конвертируются в указанный тип данных и проходят проверку на соответствие ему.
Все те же правила, которые применяются к path-параметрам, также применяются и query-параметрам:
* Поддержка от редактора кода (очевидно)
* <abbr title="преобразование строки, полученной из HTTP запроса в Python данные">"Парсинг"</abbr> данных
* Проверка на соответствие данных (Валидация)
* Автоматическая документация
## Значения по умолчанию
Поскольку query-параметры не являются фиксированной частью пути, они могут быть не обязательными и иметь значения по умолчанию.
В примере выше значения по умолчанию равны `skip=0` и `limit=10`.
Таким образом, результат перехода по URL-адресу:
```
http://127.0.0.1:8000/items/
```
будет таким же, как если перейти используя параметры по умолчанию:
```
http://127.0.0.1:8000/items/?skip=0&limit=10
```
Но если вы введёте, например:
```
http://127.0.0.1:8000/items/?skip=20
```
Значения параметров в вашей функции будут:
* `skip=20`: потому что вы установили это в URL-адресе
* `limit=10`: т.к это было значение по умолчанию
## Необязательные параметры
Аналогично, вы можете объявлять необязательные query-параметры, установив их значение по умолчанию, равное `None`:
=== "Python 3.10+"
```Python hl_lines="7"
{!> ../../../docs_src/query_params/tutorial002_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/query_params/tutorial002.py!}
```
В этом случае, параметр `q` будет не обязательным и будет иметь значение `None` по умолчанию.
!!! Важно
Также обратите внимание, что **FastAPI** достаточно умён чтобы заметить, что параметр `item_id` является path-параметром, а `q` нет, поэтому, это параметр запроса.
## Преобразование типа параметра запроса
Вы также можете объявлять параметры с типом `bool`, которые будут преобразованы соответственно:
=== "Python 3.10+"
```Python hl_lines="7"
{!> ../../../docs_src/query_params/tutorial003_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="9"
{!> ../../../docs_src/query_params/tutorial003.py!}
```
В этом случае, если вы сделаете запрос:
```
http://127.0.0.1:8000/items/foo?short=1
```
или
```
http://127.0.0.1:8000/items/foo?short=True
```
или
```
http://127.0.0.1:8000/items/foo?short=true
```
или
```
http://127.0.0.1:8000/items/foo?short=on
```
или
```
http://127.0.0.1:8000/items/foo?short=yes
```
или в любом другом варианте написания (в верхнем регистре, с заглавной буквой, и т.п), внутри вашей функции параметр `short` будет иметь значение `True` типа данных `bool` . В противном случае - `False`.
## Смешивание query-параметров и path-параметров
Вы можете объявлять несколько query-параметров и path-параметров одновременно,**FastAPI** сам разберётся, что чем является.
И вы не обязаны объявлять их в каком-либо определенном порядке.
Они будут обнаружены по именам:
=== "Python 3.10+"
```Python hl_lines="6 8"
{!> ../../../docs_src/query_params/tutorial004_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="8 10"
{!> ../../../docs_src/query_params/tutorial004.py!}
```
## Обязательные query-параметры
Когда вы объявляете значение по умолчанию для параметра, который не является path-параметром (в этом разделе, мы пока что познакомились только с path-параметрами), то это значение не является обязательным.
Если вы не хотите задавать конкретное значение, но хотите сделать параметр необязательным, вы можете установить значение по умолчанию равным `None`.
Но если вы хотите сделать query-параметр обязательным, вы можете просто не указывать значение по умолчанию:
```Python hl_lines="6-7"
{!../../../docs_src/query_params/tutorial005.py!}
```
Здесь параметр запроса `needy` является обязательным параметром с типом данных `str`.
Если вы откроете в браузере URL-адрес, например:
```
http://127.0.0.1:8000/items/foo-item
```
...без добавления обязательного параметра `needy`, вы увидите подобного рода ошибку:
```JSON
{
"detail": [
{
"loc": [
"query",
"needy"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
```
Поскольку `needy` является обязательным параметром, вам необходимо указать его в URL-адресе:
```
http://127.0.0.1:8000/items/foo-item?needy=sooooneedy
```
...это будет работать:
```JSON
{
"item_id": "foo-item",
"needy": "sooooneedy"
}
```
Конечно, вы можете определить некоторые параметры как обязательные, некоторые - со значением по умполчанию, а некоторые - полностью необязательные:
=== "Python 3.10+"
```Python hl_lines="8"
{!> ../../../docs_src/query_params/tutorial006_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="10"
{!> ../../../docs_src/query_params/tutorial006.py!}
```
В этом примере, у нас есть 3 параметра запроса:
* `needy`, обязательный `str`.
* `skip`, типа `int` и со значением по умолчанию `0`.
* `limit`, необязательный `int`.
!!! подсказка
Вы можете использовать класс `Enum` также, как ранее применяли его с [Path-параметрами](path-params.md#predefined-values){.internal-link target=_blank}.

View File

@@ -0,0 +1,189 @@
# Объявление примера запроса данных
Вы можете объявлять примеры данных, которые ваше приложение может получать.
Вот несколько способов, как это можно сделать.
## Pydantic `schema_extra`
Вы можете объявить ключ `example` для модели Pydantic, используя класс `Config` и переменную `schema_extra`, как описано в <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic документации: Настройка схемы</a>:
=== "Python 3.10+"
```Python hl_lines="13-21"
{!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="15-23"
{!> ../../../docs_src/schema_extra_example/tutorial001.py!}
```
Эта дополнительная информация будет включена в **JSON Schema** выходных данных для этой модели, и она будет использоваться в документации к API.
!!! tip Подсказка
Вы можете использовать тот же метод для расширения JSON-схемы и добавления своей собственной дополнительной информации.
Например, вы можете использовать это для добавления дополнительной информации для пользовательского интерфейса в вашем веб-приложении и т.д.
## Дополнительные аргументы поля `Field`
При использовании `Field()` с моделями Pydantic, вы также можете объявлять дополнительную информацию для **JSON Schema**, передавая любые другие произвольные аргументы в функцию.
Вы можете использовать это, чтобы добавить аргумент `example` для каждого поля:
=== "Python 3.10+"
```Python hl_lines="2 8-11"
{!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!}
```
=== "Python 3.6+"
```Python hl_lines="4 10-13"
{!> ../../../docs_src/schema_extra_example/tutorial002.py!}
```
!!! warning Внимание
Имейте в виду, что эти дополнительные переданные аргументы не добавляют никакой валидации, только дополнительную информацию для документации.
## Использование `example` и `examples` в OpenAPI
При использовании любой из этих функций:
* `Path()`
* `Query()`
* `Header()`
* `Cookie()`
* `Body()`
* `Form()`
* `File()`
вы также можете добавить аргумент, содержащий `example` или группу `examples` с дополнительной информацией, которая будет добавлена в **OpenAPI**.
### Параметр `Body` с аргументом `example`
Здесь мы передаём аргумент `example`, как пример данных ожидаемых в параметре `Body()`:
=== "Python 3.10+"
```Python hl_lines="22-27"
{!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="22-27"
{!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="23-28"
{!> ../../../docs_src/schema_extra_example/tutorial003_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="18-23"
{!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="20-25"
{!> ../../../docs_src/schema_extra_example/tutorial003.py!}
```
### Аргумент "example" в UI документации
С любым из вышеуказанных методов это будет выглядеть так в `/docs`:
<img src="/img/tutorial/body-fields/image01.png">
### `Body` с аргументом `examples`
В качестве альтернативы одному аргументу `example`, вы можете передавать `examples` используя тип данных `dict` с **несколькими примерами**, каждый из которых содержит дополнительную информацию, которая также будет добавлена в **OpenAPI**.
Ключи `dict` указывают на каждый пример, а значения для каждого из них - на еще один тип `dict` с дополнительной информацией.
Каждый конкретный пример типа `dict` в аргументе `examples` может содержать:
* `summary`: Краткое описание для примера.
* `description`: Полное описание, которое может содержать текст в формате Markdown.
* `value`: Это конкретный пример, который отображается, например, в виде типа `dict`.
* `externalValue`: альтернатива параметру `value`, URL-адрес, указывающий на пример. Хотя это может не поддерживаться таким же количеством инструментов разработки и тестирования API, как параметр `value`.
=== "Python 3.10+"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="24-50"
{!> ../../../docs_src/schema_extra_example/tutorial004_an.py!}
```
=== "Python 3.10+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="19-45"
{!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!}
```
=== "Python 3.6+ non-Annotated"
!!! tip Заметка
Рекомендуется использовать версию с `Annotated`, если это возможно.
```Python hl_lines="21-47"
{!> ../../../docs_src/schema_extra_example/tutorial004.py!}
```
### Аргумент "examples" в UI документации
С аргументом `examples`, добавленным в `Body()`, страница документации `/docs` будет выглядеть так:
<img src="/img/tutorial/body-fields/image02.png">
## Технические Детали
!!! warning Внимание
Эти технические детали относятся к стандартам **JSON Schema** и **OpenAPI**.
Если предложенные выше идеи уже работают для вас, возможно этого будет достаточно и эти детали вам не потребуются, можете спокойно их пропустить.
Когда вы добавляете пример внутрь модели Pydantic, используя `schema_extra` или `Field(example="something")`, этот пример добавляется в **JSON Schema** для данной модели Pydantic.
И эта **JSON Schema** модели Pydantic включается в **OpenAPI** вашего API, а затем используется в UI документации.
Поля `example` как такового не существует в стандартах **JSON Schema**. В последних версиях JSON-схемы определено поле <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>, но OpenAPI 3.0.3 основан на более старой версии JSON-схемы, которая не имела поля `examples`.
Таким образом, OpenAPI 3.0.3 определяет своё собственное поле <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> для модифицированной версии **JSON Schema**, которую он использует чтобы достичь той же цели (однако это именно поле `example`, а не `examples`), и именно это используется API в UI документации (с интеграцией Swagger UI).
Итак, хотя поле `example` не является частью JSON-схемы, оно является частью настраиваемой версии JSON-схемы в OpenAPI, и именно это поле будет использоваться в UI документации.
Однако, когда вы используете поле `example` или `examples` с любой другой функцией (`Query()`, `Body()`, и т.д.), эти примеры не добавляются в JSON-схему, которая описывает эти данные (даже в собственную версию JSON-схемы OpenAPI), они добавляются непосредственно в объявление *операции пути* в OpenAPI (вне частей OpenAPI, которые используют JSON-схему).
Для функций `Path()`, `Query()`, `Header()`, и `Cookie()`, аргументы `example` или `examples` добавляются в <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object" class="external-link" target="_blank">определение OpenAPI, к объекту `Parameter Object` (в спецификации)</a>.
И для функций `Body()`, `File()` и `Form()` аргументы `example` или `examples` аналогично добавляются в <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject" class="external-link" target="_blank"> определение OpenAPI, к объекту `Request Body Object`, в поле `content` в объекте `Media Type Object` (в спецификации)</a>.
С другой стороны, существует более новая версия OpenAPI: **3.1.0**, недавно выпущенная. Она основана на последней версии JSON-схемы и большинство модификаций из OpenAPI JSON-схемы удалены в обмен на новые возможности из последней версии JSON-схемы, так что все эти мелкие отличия устранены. Тем не менее, Swagger UI в настоящее время не поддерживает OpenAPI 3.1.0, поэтому пока лучше продолжать использовать вышеупомянутые методы.

View File

@@ -0,0 +1,40 @@
# Статические Файлы
Вы можете предоставлять статические файлы автоматически из директории, используя `StaticFiles`.
## Использование `StaticFiles`
* Импортируйте `StaticFiles`.
* "Примонтируйте" экземпляр `StaticFiles()` с указанием определенной директории.
```Python hl_lines="2 6"
{!../../../docs_src/static_files/tutorial001.py!}
```
!!! заметка "Технические детали"
Вы также можете использовать `from starlette.staticfiles import StaticFiles`.
**FastAPI** предоставляет `starlette.staticfiles` под псевдонимом `fastapi.staticfiles`, просто для вашего удобства, как разработчика. Но на самом деле это берётся напрямую из библиотеки Starlette.
### Что такое "Монтирование"
"Монтирование" означает добавление полноценного "независимого" приложения в определенную директорию, которое затем обрабатывает все подпути.
Это отличается от использования `APIRouter`, так как примонтированное приложение является полностью независимым.
OpenAPI и документация из вашего главного приложения не будет содержать ничего из примонтированного приложения, и т.д.
Вы можете прочитать больше об этом в **Расширенном руководстве пользователя**.
## Детали
Первый параметр `"/static"` относится к подпути, по которому это "подприложение" будет "примонтировано". Таким образом, любой путь начинающийся со `"/static"` будет обработан этим приложением.
Параметр `directory="static"` относится к имени директории, которая содержит ваши статические файлы.
`name="static"` даёт имя маршруту, которое может быть использовано внутри **FastAPI**.
Все эти параметры могут отличаться от "`static`", настройте их в соответствии с вашими нуждами и конкретными деталями вашего собственного приложения.
## Больше информации
Для получения дополнительной информации о деталях и настройках ознакомьтесь с <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">Документацией Starlette о статических файлах</a>.

View File

@@ -67,17 +67,27 @@ nav:
- fastapi-people.md
- python-types.md
- Учебник - руководство пользователя:
- tutorial/index.md
- tutorial/first-steps.md
- tutorial/path-params.md
- tutorial/query-params-str-validations.md
- tutorial/path-params-numeric-validations.md
- tutorial/body-fields.md
- tutorial/background-tasks.md
- tutorial/extra-data-types.md
- tutorial/cookie-params.md
- tutorial/testing.md
- tutorial/response-status-code.md
- tutorial/query-params.md
- tutorial/body-multiple-params.md
- tutorial/static-files.md
- tutorial/debugging.md
- tutorial/schema-extra-example.md
- async.md
- Развёртывание:
- deployment/index.md
- deployment/versions.md
- deployment/concepts.md
- deployment/https.md
- deployment/manually.md
- project-generation.md

View File

@@ -0,0 +1,31 @@
# 响应 - 更改状态码
你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。
但在某些情况下,你需要返回一个不同于默认值的状态码。
## 使用场景
例如假设你想默认返回一个HTTP状态码为“OK”`200`
但如果数据不存在你想创建它并返回一个HTTP状态码为“CREATED”`201`
但你仍然希望能够使用`response_model`过滤和转换你返回的数据。
对于这些情况,你可以使用一个`Response`参数。
## 使用 `Response` 参数
你可以在你的*路径操作函数*中声明一个`Response`类型的参数就像你可以为cookies和头部做的那样
然后你可以在这个*临时*响应对象中设置`status_code`
```Python hl_lines="1 9 12"
{!../../../docs_src/response_change_status_code/tutorial001.py!}
```
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
**FastAPI**将使用这个临时响应来提取状态码也包括cookies和头部并将它们放入包含你返回的值的最终响应中该响应由任何`response_model`过滤。
你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。

View File

@@ -0,0 +1,39 @@
# 响应头
## 使用 `Response` 参数
你可以在你的*路径操作函数*中声明一个`Response`类型的参数就像你可以为cookies做的那样
然后你可以在这个*临时*响应对象中设置头部。
```Python hl_lines="1 7-8"
{!../../../docs_src/response_headers/tutorial002.py!}
```
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
**FastAPI**将使用这个临时响应来提取头部也包括cookies和状态码并将它们放入包含你返回的值的最终响应中该响应由任何`response_model`过滤。
你也可以在依赖项中声明`Response`参数并在其中设置头部和cookies
## 直接返回 `Response`
你也可以在直接返回`Response`时添加头部。
按照[直接返回响应](response-directly.md){.internal-link target=_blank}中所述创建响应,并将头部作为附加参数传递:
```Python hl_lines="10-12"
{!../../../docs_src/response_headers/tutorial001.py!}
```
!!! 注意 "技术细节"
你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。
**FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`只是为了方便开发者。但是大多数可用的响应都直接来自Starlette。
由于`Response`经常用于设置头部和cookies因此**FastAPI**还在`fastapi.Response`中提供了它。
## 自定义头部
请注意,可以使用'X-'前缀添加自定义专有头部。
但是如果你有自定义头部你希望浏览器中的客户端能够看到它们你需要将它们添加到你的CORS配置中在[CORS跨源资源共享](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在<a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette的CORS文档</a>中记录的`expose_headers`参数。

View File

@@ -97,7 +97,7 @@ $ python -m venv env
<div class="termy">
```console
$ pip install -e ."[dev,doc,test]"
$ pip install -r requirements.txt
---> 100%
```

View File

@@ -0,0 +1,39 @@
# 静态文件
您可以使用 `StaticFiles`从目录中自动提供静态文件。
## 使用`StaticFiles`
* 导入`StaticFiles`
* "挂载"(Mount) 一个 `StaticFiles()` 实例到一个指定路径。
```Python hl_lines="2 6"
{!../../../docs_src/static_files/tutorial001.py!}
```
!!! note "技术细节"
你也可以用 `from starlette.staticfiles import StaticFiles`。
**FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles` 只是为了方便你开发者。但它确实来自Starlette。
### 什么是"挂载"(Mounting)
"挂载" 表示在特定路径添加一个完全"独立的"应用,然后负责处理所有子路径。
这与使用`APIRouter`不同因为安装的应用程序是完全独立的。OpenAPI和来自你主应用的文档不会包含已挂载应用的任何东西等等。
你可以在**高级用户指南**中了解更多。
## 细节
这个 "子应用" 会被 "挂载" 到第一个 `"/static"` 指向的子路径。因此,任何以`"/static"`开头的路径都会被它处理。
`directory="static"` 指向包含你的静态文件的目录名字。
`name="static"` 提供了一个能被**FastAPI**内部使用的名字。
所有这些参数可以不同于"`static`",根据你应用的需要和具体细节调整它们。
## 更多信息
更多细节和选择查阅 <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">Starlette's docs about Static Files</a>.

View File

@@ -108,6 +108,7 @@ nav:
- tutorial/sql-databases.md
- tutorial/bigger-applications.md
- tutorial/metadata.md
- tutorial/static-files.md
- tutorial/debugging.md
- 高级用户指南:
- advanced/index.md
@@ -116,6 +117,8 @@ nav:
- advanced/response-directly.md
- advanced/custom-response.md
- advanced/response-cookies.md
- advanced/response-change-status-code.md
- advanced/response-headers.md
- advanced/wsgi.md
- contributing.md
- help-fastapi.md

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.95.2"
__version__ = "0.100.0-beta2"
from starlette import status as status

595
fastapi/_compat.py Normal file
View File

@@ -0,0 +1,595 @@
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
@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,
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = schema_generator.generate_inner(field._type_adapter.core_schema)
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,
) -> Dict[str, Dict[str, Any]]:
inputs = [
(field, "validation", field._type_adapter.core_schema) for field in fields
]
_, definitions = schema_generator.generate_definitions(inputs=inputs) # type: ignore[arg-type]
return 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,
) -> 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,
) -> 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)
)

View File

@@ -15,12 +15,12 @@ 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,
websocket_request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.logger import logger
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
from fastapi.openapi.docs import (
@@ -30,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
@@ -145,6 +145,11 @@ class FastAPI(Starlette):
self.exception_handlers.setdefault(
RequestValidationError, request_validation_exception_handler
)
self.exception_handlers.setdefault(
WebSocketRequestValidationError,
# Starlette still has incorrect type specification for the handlers
websocket_request_validation_exception_handler, # type: ignore
)
self.user_middleware: List[Middleware] = (
[] if middleware is None else list(middleware)
@@ -291,8 +296,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,
@@ -349,8 +354,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,
@@ -395,15 +400,34 @@ class FastAPI(Starlette):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None
self,
path: str,
endpoint: Callable[..., Any],
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[Depends]] = None,
) -> None:
self.router.add_api_websocket_route(path, endpoint, name=name)
self.router.add_api_websocket_route(
path,
endpoint,
name=name,
dependencies=dependencies,
)
def websocket(
self, path: str, name: Optional[str] = None
self,
path: str,
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[Depends]] = None,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_websocket_route(path, func, name=name)
self.add_api_websocket_route(
path,
func,
name=name,
dependencies=dependencies,
)
return func
return decorator
@@ -451,8 +475,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,
@@ -506,8 +530,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,
@@ -561,8 +585,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,
@@ -616,8 +640,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,
@@ -671,8 +695,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,
@@ -726,8 +750,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,
@@ -781,8 +805,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,
@@ -836,8 +860,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,

View File

@@ -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:

View File

@@ -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:

View File

@@ -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):

View File

@@ -1,15 +1,87 @@
import dataclasses
from collections import defaultdict
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):

View File

@@ -1,10 +1,11 @@
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.utils import is_body_allowed_for_status_code
from fastapi.websockets import WebSocket
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
async def http_exception_handler(request: Request, exc: HTTPException) -> Response:
@@ -23,3 +24,11 @@ async def request_validation_exception_handler(
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": jsonable_encoder(exc.errors())},
)
async def websocket_request_validation_exception_handler(
websocket: WebSocket, exc: WebSocketRequestValidationError
) -> None:
await websocket.close(
code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors())
)

View File

@@ -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
@@ -11,7 +10,7 @@ class HTTPException(StarletteHTTPException):
self,
status_code: int,
detail: Any = None,
headers: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(status_code=status_code, detail=detail, headers=headers)
@@ -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

View File

@@ -10,19 +10,16 @@ class AsyncExitStackMiddleware:
self.context_name = context_name
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if AsyncExitStack:
dependency_exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
dependency_exception = e
raise e
if dependency_exception:
# This exception was possibly handled by the dependency but it should
# still bubble up so that the ServerErrorMiddleware can return a 500
# or the ExceptionMiddleware can catch and handle any other exceptions
raise dependency_exception
else:
await self.app(scope, receive, send) # pragma: no cover
dependency_exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
dependency_exception = e
raise e
if dependency_exception:
# This exception was possibly handled by the dependency but it should
# still bubble up so that the ServerErrorMiddleware can return a 500
# or the ExceptionMiddleware can catch and handle any other exceptions
raise dependency_exception

View File

@@ -1,2 +1,3 @@
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
REF_PREFIX = "#/components/schemas/"
REF_TEMPLATE = "#/components/schemas/{model}"

View File

@@ -1,11 +1,20 @@
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, 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 Literal
try:
import email_validator # type: ignore
import email_validator
assert email_validator # make autoflake ignore the unused import
from pydantic import EmailStr
@@ -24,22 +33,52 @@ 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):
name: str
url: Optional[AnyUrl] = None
class Config:
extra = "allow"
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Info(BaseModel):
@@ -50,8 +89,13 @@ 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):
@@ -59,8 +103,13 @@ class ServerVariable(BaseModel):
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):
@@ -68,8 +117,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):
@@ -88,16 +142,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):
@@ -108,14 +172,14 @@ class Schema(BaseModel):
exclusiveMaximum: Optional[float] = None
minimum: Optional[float] = None
exclusiveMinimum: Optional[float] = None
maxLength: Optional[int] = Field(default=None, gte=0)
minLength: Optional[int] = Field(default=None, gte=0)
maxLength: Optional[int] = Field(default=None, ge=0)
minLength: Optional[int] = Field(default=None, ge=0)
pattern: Optional[str] = None
maxItems: Optional[int] = Field(default=None, gte=0)
minItems: Optional[int] = Field(default=None, gte=0)
maxItems: Optional[int] = Field(default=None, ge=0)
minItems: Optional[int] = Field(default=None, ge=0)
uniqueItems: Optional[bool] = None
maxProperties: Optional[int] = Field(default=None, gte=0)
minProperties: Optional[int] = Field(default=None, gte=0)
maxProperties: Optional[int] = Field(default=None, ge=0)
minProperties: Optional[int] = Field(default=None, ge=0)
required: Optional[List[str]] = None
enum: Optional[List[Any]] = None
type: Optional[str] = None
@@ -138,8 +202,13 @@ class Schema(BaseModel):
example: Optional[Any] = None
deprecated: Optional[bool] = None
class Config:
extra: str = "allow"
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Example(BaseModel):
@@ -148,8 +217,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):
@@ -166,8 +240,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):
@@ -176,8 +255,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):
@@ -194,8 +278,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):
@@ -212,8 +301,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):
@@ -224,8 +318,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):
@@ -234,8 +333,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):
@@ -253,8 +357,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):
@@ -272,8 +381,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):
@@ -287,8 +401,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):
@@ -298,18 +417,18 @@ class APIKeyIn(Enum):
class APIKey(SecurityBase):
type_ = Field(SecuritySchemeType.apiKey, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type")
in_: APIKeyIn = Field(alias="in")
name: str
class HTTPBase(SecurityBase):
type_ = Field(SecuritySchemeType.http, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type")
scheme: str
class HTTPBearer(HTTPBase):
scheme = "bearer"
scheme: Literal["bearer"] = "bearer"
bearerFormat: Optional[str] = None
@@ -317,8 +436,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):
@@ -344,17 +468,24 @@ 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):
type_ = Field(SecuritySchemeType.oauth2, alias="type")
type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type")
flows: OAuthFlows
class OpenIdConnect(SecurityBase):
type_ = Field(SecuritySchemeType.openIdConnect, alias="type")
type_: SecuritySchemeType = Field(
default=SecuritySchemeType.openIdConnect, alias="type"
)
openIdConnectUrl: str
@@ -373,8 +504,13 @@ class Components(BaseModel):
# Using Any for Specification Extensions
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
class Config:
extra = "allow"
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Tag(BaseModel):
@@ -382,8 +518,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):
@@ -397,10 +538,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)

View File

@@ -1,32 +1,32 @@
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,
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
@@ -88,7 +88,8 @@ 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,
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
@@ -96,13 +97,16 @@ 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,
)
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
@@ -119,13 +123,16 @@ 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,
) -> 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_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type
@@ -181,7 +188,7 @@ def get_openapi_operation_metadata(
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
if file_name:
message += f" at {file_name}"
warnings.warn(message)
warnings.warn(message, stacklevel=1)
operation_ids.add(operation_id)
operation["operationId"] = operation_id
if route.deprecated:
@@ -190,7 +197,11 @@ 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,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
@@ -218,7 +229,9 @@ 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,
)
parameters.extend(operation_parameters)
if parameters:
@@ -236,7 +249,9 @@ 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,
)
if request_body_oai:
operation["requestBody"] = request_body_oai
@@ -250,8 +265,9 @@ 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,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@@ -277,10 +293,10 @@ 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,
)
else:
response_schema = {}
@@ -309,8 +325,10 @@ 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,
)
media_type = route_response_media_type or "application/json"
additional_schema = (
@@ -332,10 +350,8 @@ def get_openapi_path(
openapi_response["description"] = description
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
if (all_route_params or route.body_field) and not any(
[
status in operation["responses"]
for status in [http422, "4XX", "default"]
]
status in operation["responses"]
for status in [http422, "4XX", "default"]
):
operation["responses"][http422] = {
"description": "Validation Error",
@@ -358,13 +374,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
@@ -379,13 +395,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
@@ -418,15 +433,21 @@ def get_openapi(
components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
flat_models = get_flat_models_from_routes(routes)
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(routes)
model_name_map = get_compat_model_name_map(all_fields)
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
definitions = get_definitions(
fields=all_fields,
schema_generator=schema_generator,
model_name_map=model_name_map,
)
for route in routes:
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,
)
if result:
path, security_schemes, path_definitions = result

View File

@@ -1,7 +1,7 @@
from typing import Any, Callable, Dict, Optional, Sequence
from fastapi import params
from pydantic.fields import Undefined
from fastapi._compat import Undefined
def Path( # noqa: N802
@@ -16,6 +16,7 @@ def Path( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -34,6 +35,7 @@ def Path( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -55,6 +57,7 @@ def Query( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -73,6 +76,7 @@ def Query( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -95,6 +99,7 @@ def Header( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -114,6 +119,7 @@ def Header( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -135,6 +141,7 @@ def Cookie( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -153,6 +160,7 @@ def Cookie( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -176,6 +184,7 @@ def Body( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -194,6 +203,7 @@ def Body( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -214,6 +224,7 @@ def Form( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -231,6 +242,7 @@ def Form( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -251,6 +263,7 @@ def File( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -268,6 +281,7 @@ def File( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,

View File

@@ -1,7 +1,9 @@
from enum import Enum
from typing import Any, Callable, Dict, Optional, Sequence
from typing import Any, Callable, Dict, Optional, Sequence, Type
from pydantic.fields import FieldInfo, Undefined
from pydantic.fields import FieldInfo
from ._compat import PYDANTIC_V2, Undefined
class ParamTypes(Enum):
@@ -18,6 +20,7 @@ class Param(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -27,6 +30,7 @@ class Param(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -36,9 +40,8 @@ class Param(FieldInfo):
):
self.deprecated = deprecated
self.example = example
self.examples = examples
self.include_in_schema = include_in_schema
super().__init__(
kwargs = dict(
default=default,
alias=alias,
title=title,
@@ -49,9 +52,19 @@ class Param(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(**kwargs)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -64,6 +77,7 @@ class Path(Param):
self,
default: Any = ...,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -73,6 +87,7 @@ class Path(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -84,6 +99,7 @@ class Path(Param):
self.in_ = self.in_
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -93,6 +109,7 @@ class Path(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -109,6 +126,7 @@ class Query(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -118,6 +136,7 @@ class Query(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -127,6 +146,7 @@ class Query(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -136,6 +156,7 @@ class Query(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -152,6 +173,7 @@ class Header(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
@@ -162,6 +184,7 @@ class Header(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -172,6 +195,7 @@ class Header(Param):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -181,6 +205,7 @@ class Header(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -197,6 +222,7 @@ class Cookie(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -206,6 +232,7 @@ class Cookie(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -215,6 +242,7 @@ class Cookie(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@@ -224,6 +252,7 @@ class Cookie(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@@ -238,6 +267,7 @@ class Body(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
@@ -249,6 +279,7 @@ class Body(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -257,8 +288,7 @@ class Body(FieldInfo):
self.embed = embed
self.media_type = media_type
self.example = example
self.examples = examples
super().__init__(
kwargs = dict(
default=default,
alias=alias,
title=title,
@@ -269,9 +299,20 @@ class Body(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(
**kwargs,
)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -282,6 +323,7 @@ class Form(Body):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
title: Optional[str] = None,
@@ -292,6 +334,7 @@ class Form(Body):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -299,6 +342,7 @@ class Form(Body):
):
super().__init__(
default=default,
annotation=annotation,
embed=True,
media_type=media_type,
alias=alias,
@@ -310,6 +354,7 @@ class Form(Body):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@@ -322,6 +367,7 @@ class File(Form):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
title: Optional[str] = None,
@@ -332,6 +378,7 @@ class File(Form):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@@ -339,6 +386,7 @@ class File(Form):
):
super().__init__(
default=default,
annotation=annotation,
media_type=media_type,
alias=alias,
title=title,
@@ -349,6 +397,7 @@ class File(Form):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,

View File

@@ -27,8 +27,6 @@ class UJSONResponse(JSONResponse):
class ORJSONResponse(JSONResponse):
media_type = "application/json"
def render(self, content: Any) -> bytes:
assert orjson is not None, "orjson must be installed to use ORJSONResponse"
return orjson.dumps(

View File

@@ -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,9 +37,14 @@ from fastapi.dependencies.utils import (
get_typed_return_annotation,
solve_dependencies,
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.types import DecoratedCallable
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
)
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import (
create_cloned_field,
create_response_field,
@@ -40,23 +53,20 @@ 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
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import BaseRoute, Match
from starlette.routing import Mount as Mount # noqa
from starlette.routing import (
BaseRoute,
Match,
compile_path,
get_name,
request_response,
websocket_session,
)
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.routing import Mount as Mount # noqa
from starlette.types import ASGIApp, Lifespan, Scope
from starlette.websockets import WebSocket
@@ -69,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,
@@ -111,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,
@@ -121,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,
@@ -171,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,
@@ -216,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
@@ -232,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
@@ -283,8 +319,7 @@ def get_websocket_app(
)
values, errors, _, _2, _3 = solved_result
if errors:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
raise WebSocketRequestValidationError(errors)
raise WebSocketRequestValidationError(_normalize_errors(errors))
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
@@ -298,13 +333,21 @@ class APIWebSocketRoute(routing.WebSocketRoute):
endpoint: Callable[..., Any],
*,
name: Optional[str] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
dependency_overrides_provider: Optional[Any] = None,
) -> None:
self.path = path
self.endpoint = endpoint
self.name = get_name(endpoint) if name is None else name
self.dependencies = list(dependencies or [])
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
for depends in self.dependencies[::-1]:
self.dependant.dependencies.insert(
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
self.app = websocket_session(
get_websocket_app(
dependant=self.dependant,
@@ -337,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,
@@ -412,16 +455,14 @@ 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)
else:
self.response_field = None # type: ignore
self.secure_cloned_response_field = None
if dependencies:
self.dependencies = list(dependencies)
else:
self.dependencies = []
self.dependencies = list(dependencies or [])
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed"
@@ -516,7 +557,7 @@ class APIRouter(routing.Router):
), "A path prefix must not end with '/', as the routes will start with '/'"
self.prefix = prefix
self.tags: List[Union[str, Enum]] = tags or []
self.dependencies = list(dependencies or []) or []
self.dependencies = list(dependencies or [])
self.deprecated = deprecated
self.include_in_schema = include_in_schema
self.responses = responses or {}
@@ -561,8 +602,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,
@@ -642,8 +683,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,
@@ -690,21 +731,37 @@ class APIRouter(routing.Router):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None
self,
path: str,
endpoint: Callable[..., Any],
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[params.Depends]] = None,
) -> None:
current_dependencies = self.dependencies.copy()
if dependencies:
current_dependencies.extend(dependencies)
route = APIWebSocketRoute(
self.prefix + path,
endpoint=endpoint,
name=name,
dependencies=current_dependencies,
dependency_overrides_provider=self.dependency_overrides_provider,
)
self.routes.append(route)
def websocket(
self, path: str, name: Optional[str] = None
self,
path: str,
name: Optional[str] = None,
*,
dependencies: Optional[Sequence[params.Depends]] = None,
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_websocket_route(path, func, name=name)
self.add_api_websocket_route(
path, func, name=name, dependencies=dependencies
)
return func
return decorator
@@ -744,7 +801,7 @@ class APIRouter(routing.Router):
path = getattr(r, "path") # noqa: B009
name = getattr(r, "name", "unknown")
if path is not None and not path:
raise Exception(
raise FastAPIError(
f"Prefix and path cannot be both empty (path operation: {name})"
)
if responses is None:
@@ -819,8 +876,16 @@ class APIRouter(routing.Router):
name=route.name,
)
elif isinstance(route, APIWebSocketRoute):
current_dependencies = []
if dependencies:
current_dependencies.extend(dependencies)
if route.dependencies:
current_dependencies.extend(route.dependencies)
self.add_api_websocket_route(
prefix + route.path, route.endpoint, name=route.name
prefix + route.path,
route.endpoint,
dependencies=current_dependencies,
name=route.name,
)
elif isinstance(route, routing.WebSocketRoute):
self.add_websocket_route(
@@ -845,8 +910,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,
@@ -901,8 +966,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,
@@ -957,8 +1022,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,
@@ -1013,8 +1078,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,
@@ -1069,8 +1134,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,
@@ -1125,8 +1190,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,
@@ -1181,8 +1246,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,
@@ -1237,8 +1302,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,

View File

@@ -21,7 +21,9 @@ class APIKeyQuery(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.query}, name=name, description=description
**{"in": APIKeyIn.query}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@@ -48,7 +50,9 @@ class APIKeyHeader(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.header}, name=name, description=description
**{"in": APIKeyIn.header}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@@ -75,7 +79,9 @@ class APIKeyCookie(APIKeyBase):
auto_error: bool = True,
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.cookie}, name=name, description=description
**{"in": APIKeyIn.cookie}, # type: ignore[arg-type]
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error

View File

@@ -73,11 +73,6 @@ class HTTPBasic(HTTPBase):
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
else:
unauthorized_headers = {"WWW-Authenticate": "Basic"}
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
if not authorization or scheme.lower() != "basic":
if self.auto_error:
raise HTTPException(
@@ -87,6 +82,11 @@ class HTTPBasic(HTTPBase):
)
else:
return None
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
try:
data = b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union, cast
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import OAuth2 as OAuth2Model
@@ -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,
@@ -121,7 +125,9 @@ class OAuth2(SecurityBase):
description: Optional[str] = None,
auto_error: bool = True,
):
self.model = OAuth2Model(flows=flows, description=description)
self.model = OAuth2Model(
flows=cast(OAuthFlowsModel, flows), description=description
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@@ -148,7 +154,9 @@ class OAuth2PasswordBearer(OAuth2):
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
flows = OAuthFlowsModel(
password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
)
super().__init__(
flows=flows,
scheme_name=scheme_name,
@@ -185,12 +193,15 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(
authorizationCode={
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
}
authorizationCode=cast(
Any,
{
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
},
)
)
super().__init__(
flows=flows,

View File

@@ -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]]

View File

@@ -1,21 +1,42 @@
import re
import warnings
from dataclasses import is_dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
from typing import (
TYPE_CHECKING,
Any,
Dict,
MutableMapping,
Optional,
Set,
Type,
Union,
cast,
)
from weakref import WeakKeyDictionary
import fastapi
from fastapi._compat import (
PYDANTIC_V2,
BaseConfig,
ModelField,
PydanticSchemaGenerationError,
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
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
# Cache for `create_cloned_field`
_CLONED_TYPES_CACHE: MutableMapping[
Type[BaseModel], Type[BaseModel]
] = WeakKeyDictionary()
def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
if status_code is None:
@@ -34,24 +55,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))
@@ -60,8 +63,8 @@ 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,
@@ -70,20 +73,27 @@ def create_response_field(
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 not PYDANTIC_V2:
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. "
@@ -98,11 +108,15 @@ def create_response_field(
def create_cloned_field(
field: ModelField,
*,
cloned_types: Optional[Dict[Type[BaseModel], Type[BaseModel]]] = None,
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
) -> ModelField:
# _cloned_types has already cloned types, to support recursive models
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:
cloned_types = {}
cloned_types = _CLONED_TYPES_CACHE
original_type = field.type_
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
original_type = original_type.__pydantic_model__
@@ -118,30 +132,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
@@ -202,3 +216,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}")

View File

@@ -42,7 +42,7 @@ classifiers = [
]
dependencies = [
"starlette>=0.27.0,<0.28.0",
"pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,<3.0.0",
]
dynamic = ["version"]
@@ -51,47 +51,6 @@ Homepage = "https://github.com/tiangolo/fastapi"
Documentation = "https://fastapi.tiangolo.com/"
[project.optional-dependencies]
test = [
"pytest >=7.1.3,<8.0.0",
"coverage[toml] >= 6.5.0,< 8.0",
"mypy ==0.982",
"ruff ==0.0.138",
"black == 23.1.0",
"isort >=5.0.6,<6.0.0",
"httpx >=0.23.0,<0.24.0",
"email_validator >=1.1.1,<2.0.0",
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
"sqlalchemy >=1.3.18,<1.4.43",
"peewee >=3.13.3,<4.0.0",
"databases[sqlite] >=0.3.2,<0.7.0",
"orjson >=3.2.1,<4.0.0",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0",
"python-multipart >=0.0.5,<0.0.7",
"flask >=1.1.2,<3.0.0",
"anyio[trio] >=3.2.1,<4.0.0",
"python-jose[cryptography] >=3.3.0,<4.0.0",
"pyyaml >=5.3.1,<7.0.0",
"passlib[bcrypt] >=1.7.2,<2.0.0",
# types
"types-ujson ==5.7.0.1",
"types-orjson ==3.6.2",
]
doc = [
"mkdocs >=1.1.2,<2.0.0",
"mkdocs-material >=8.1.4,<9.0.0",
"mdx-include >=1.4.1,<2.0.0",
"mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0",
"typer-cli >=0.0.13,<0.0.14",
"typer[all] >=0.6.1,<0.8.0",
"pyyaml >=5.3.1,<7.0.0",
]
dev = [
"ruff ==0.0.138",
"uvicorn[standard] >=0.12.0,<0.21.0",
"pre-commit >=2.17.0,<3.0.0",
]
all = [
"httpx >=0.23.0",
"jinja2 >=2.11.2",
@@ -100,17 +59,13 @@ 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",
]
[tool.hatch.version]
path = "fastapi/__init__.py"
[tool.isort]
profile = "black"
known_third_party = ["fastapi", "pydantic", "starlette"]
[tool.mypy]
strict = true
@@ -128,6 +83,7 @@ check_untyped_defs = true
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"
@@ -166,7 +122,7 @@ select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
# "I", # isort
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]

8
requirements-docs.txt Normal file
View File

@@ -0,0 +1,8 @@
-e .
mkdocs >=1.1.2,<2.0.0
mkdocs-material >=8.1.4,<9.0.0
mdx-include >=1.4.1,<2.0.0
mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0
typer-cli >=0.0.13,<0.0.14
typer[all] >=0.6.1,<0.8.0
pyyaml >=5.3.1,<7.0.0

27
requirements-tests.txt Normal file
View File

@@ -0,0 +1,27 @@
-e .
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
dirty-equals >= 0.6.0
mypy ==1.3.0
ruff ==0.0.272
black == 23.3.0
httpx >=0.23.0,<0.24.0
email_validator >=2.0.0,<3.0.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43
peewee >=3.13.3,<4.0.0
databases[sqlite] >=0.3.2,<0.7.0
orjson >=3.2.1,<4.0.0
ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0
python-multipart >=0.0.5,<0.0.7
flask >=1.1.2,<3.0.0
anyio[trio] >=3.2.1,<4.0.0
python-jose[cryptography] >=3.3.0,<4.0.0
pyyaml >=5.3.1,<7.0.0
passlib[bcrypt] >=1.7.2,<2.0.0
# types
types-ujson ==5.7.0.1
types-orjson ==3.6.2

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
-e .[all]
-r requirements-tests.txt
-r requirements-docs.txt
uvicorn[standard] >=0.12.0,<0.21.0
pre-commit >=2.17.0,<3.0.0

View File

@@ -3,4 +3,6 @@
set -e
set -x
# Check README.md is up to date
python ./scripts/docs.py verify-readme
python ./scripts/docs.py build-all

View File

@@ -3,4 +3,3 @@ set -x
ruff fastapi tests docs_src scripts --fix
black fastapi tests docs_src scripts
isort fastapi tests docs_src scripts

View File

@@ -6,4 +6,3 @@ set -x
mypy fastapi
ruff fastapi tests docs_src scripts
black fastapi tests --check
isort fastapi tests docs_src scripts --check-only

View File

@@ -3,7 +3,5 @@
set -e
set -x
# Check README.md is up to date
python ./scripts/docs.py verify-readme
export PYTHONPATH=./docs_src
coverage run -m pytest tests ${@}

View File

@@ -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",
}

View File

@@ -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",
}
)
]
}

View File

@@ -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"}),
}
],
}

93
tests/test_compat.py Normal file
View File

@@ -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]])

View File

@@ -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)

View File

@@ -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")

View File

@@ -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"}

View File

@@ -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():

View File

@@ -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 = {}

View File

@@ -1,5 +1,6 @@
import pytest
from fastapi import APIRouter, FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.testclient import TestClient
app = FastAPI()
@@ -31,5 +32,5 @@ def test_use_empty():
def test_include_empty():
# if both include and router.path are empty - it should raise exception
with pytest.raises(Exception):
with pytest.raises(FastAPIError):
app.include_router(router)

View File

@@ -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": {

View File

View File

@@ -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}

View File

@@ -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() == {

View File

@@ -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.0.2",
"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"},
},
},
}
},
}

View File

@@ -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.0.2",
"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"]

View File

@@ -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")

View File

@@ -1,12 +1,16 @@
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:
@@ -45,22 +49,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"
@@ -69,8 +57,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):
@@ -83,23 +75,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"}}
@@ -153,14 +128,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():
@@ -196,6 +204,7 @@ def test_encode_model_with_default():
}
@needs_pydanticv1
def test_custom_encoders():
class safe_datetime(datetime):
pass
@@ -226,14 +235,67 @@ 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}

View File

@@ -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": {

View File

@@ -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():

View File

@@ -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",
},

View File

@@ -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": {

Some files were not shown because too many files have changed in this diff Show More