* ✨ Pydantic v2 migration, initial implementation (#9500) * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 👷 Add CI for both Pydantic v1 and v2 (#9688) * 👷 Test and install Pydantic v1 and v2 in CI * 💚 Tweak CI config for Pydantic v1 and v2 * 💚 Fix Pydantic v2 specification in CI * 🐛 Fix type annotations for compatibility with Python 3.7 * 💚 Install Pydantic v2 for lints * 🐛 Fix type annotations for Pydantic v2 * 💚 Re-use test cache for lint * ♻️ Refactor internals for test coverage and performance (#9691) * ♻️ Tweak import of Annotated from typing_extensions, they are installed anyway * ♻️ Refactor _compat to define functions for Pydantic v1 or v2 once instead of checking inside * ✅ Add test for UploadFile for Pydantic v2 * ♻️ Refactor types and remove logic for impossible cases * ✅ Add missing tests from test refactor for path params * ✅ Add tests for new decimal encoder * 💡 Add TODO comment for decimals in encoders * 🔥 Remove unneeded dummy function * 🔥 Remove section of code in field_annotation_is_scalar covered by sub-call to field_annotation_is_complex * ♻️ Refactor and tweak variables and types in _compat * ✅ Add tests for corner cases and compat with Pydantic v1 and v2 * ♻️ Refactor type annotations * 🔖 Release version 0.100.0-beta1 * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them (#9707) * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them * ♻️ Update JSON Schema for email field without email-validator installed * 🐛 Fix support for Pydantic v2.0, small changes in their final release (#9771) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> * 🔖 Release version 0.100.0-beta2 * ✨ OpenAPI 3.1.0 with Pydantic v2, merge `master` (#9773) * ➕ Add dirty-equals as a testing dependency (#9778) ➕ Add dirty-equals as a testing dependency, it seems it got lsot at some point * 🔀 Merge master, fix valid JSON Schema accepting bools (#9782) * ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on the Pydantic side (#9787) ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on Pydantic side * ♻️ Deprecate parameter `regex`, use `pattern` instead (#9786) * 📝 Update docs to deprecate regex, recommend pattern * ♻️ Update examples to use new pattern instead of regex * 📝 Add new example with deprecated regex * ♻️ Add deprecation notes and warnings for regex * ✅ Add tests for regex deprecation * ✅ Update tests for compatibility with Pydantic v1 * ✨ Update docs to use Pydantic v2 settings and add note and example about v1 (#9788) * ➕ Add pydantic-settings to all extras * 📝 Update docs for Pydantic settings * 📝 Update Settings source examples to use Pydantic v2, and add a Pydantic v1 version * ✅ Add tests for settings with Pydantic v1 and v2 * 🔥 Remove solved TODO comment * ♻️ Update conditional OpenAPI to use new Pydantic v2 settings * ✅ Update tests to import Annotated from typing_extensions for Python < 3.9 (#9795) * ➕ Add pydantic-extra-types to fastapi[extra] * ➕ temp: Install Pydantic from source to test JSON Schema metadata fixes (#9777) * ➕ Install Pydantic from source, from branch for JSON Schema with metadata * ➕ Update dependencies, install Pydantic main * ➕ Fix dependency URL for Pydantic from source * ➕ Add pydantic-settings for test requirements * 💡 Add TODO comments to re-enable Pydantic main (not from source) (#9796) * ✨ Add new Pydantic Field param options to Query, Cookie, Body, etc. (#9797) * 📝 Add docs for Pydantic v2 for `docs/en/docs/advanced/path-operation-advanced-configuration.md` (#9798) * 📝 Update docs in examples for settings with Pydantic v2 (#9799) * 📝 Update JSON Schema `examples` docs with Pydantic v2 (#9800) * ♻️ Use new Pydantic v2 JSON Schema generator (#9813) Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> * ♻️ Tweak type annotations and Pydantic version range (#9801) * 📌 Re-enable GA Pydantic, for v2, require minimum 2.0.2 (#9814) * 🔖 Release version 0.100.0-beta3 * 🔥 Remove duplicate type declaration from merge conflicts (#9832) * 👷♂️ Run tests with Pydantic v2 GA (#9830) 👷 Run tests for Pydantic v2 GA * 📝 Add notes to docs expecting Pydantic v2 and future updates (#9833) * 📝 Update index with new extras * 📝 Update release notes --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pastukhov Nikita <diementros@yandex.ru>
23 KiB
SQL (Relational) Databases with Peewee
!!! warning If you are just starting, the tutorial SQL (Relational) Databases{.internal-link target=_blank} that uses SQLAlchemy should be enough.
Feel free to skip this.
Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives.
!!! info These docs assume Pydantic v1.
Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes.
If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM (SQL (Relational) Databases{.internal-link target=_blank}), or any other async ORM.
If you already have a code base that uses Peewee ORM, you can check here how to use it with FastAPI.
!!! warning "Python 3.7+ required" You will need Python 3.7 or above to safely use Peewee with FastAPI.
Peewee for async
Peewee was not designed for async frameworks, or with them in mind.
Peewee has some heavy assumptions about its defaults and about how it should be used.
If you are developing an application with an older non-async framework, and can work with all its defaults, it can be a great tool.
But if you need to change some of the defaults, support more than one predefined database, work with an async framework (like FastAPI), etc, you will need to add quite some complex extra code to override those defaults.
Nevertheless, it's possible to do it, and here you'll see exactly what code you have to add to be able to use Peewee with FastAPI.
!!! note "Technical Details" You can read more about Peewee's stand about async in Python in the docs, an issue, a PR.
The same app
We are going to create the same application as in the SQLAlchemy tutorial (SQL (Relational) Databases{.internal-link target=_blank}).
Most of the code is actually the same.
So, we are going to focus only on the differences.
File structure
Let's say you have a directory named my_super_project that contains a sub-directory called sql_app with a structure like this:
.
└── sql_app
├── __init__.py
├── crud.py
├── database.py
├── main.py
└── schemas.py
This is almost the same structure as we had for the SQLAlchemy tutorial.
Now let's see what each file/module does.
Create the Peewee parts
Let's refer to the file sql_app/database.py.
The standard Peewee code
Let's first check all the normal Peewee code, create a Peewee database:
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
!!! tip Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class.
Note
The argument:
check_same_thread=False
is equivalent to the one in the SQLAlchemy tutorial:
connect_args={"check_same_thread": False}
...it is needed only for SQLite.
!!! info "Technical Details"
Exactly the same technical details as in [SQL (Relational) Databases](../tutorial/sql-databases.md#note){.internal-link target=_blank} apply.
Make Peewee async-compatible PeeweeConnectionState
The main issue with Peewee and FastAPI is that Peewee relies heavily on Python's threading.local, and it doesn't have a direct way to override it or let you handle connections/sessions directly (as is done in the SQLAlchemy tutorial).
And threading.local is not compatible with the new async features of modern Python.
!!! note "Technical Details"
threading.local is used to have a "magic" variable that has a different value for each thread.
This was useful in older frameworks designed to have one single thread per request, no more, no less.
Using this, each request would have its own database connection/session, which is the actual final goal.
But FastAPI, using the new async features, could handle more than one request on the same thread. And at the same time, for a single request, it could run multiple things in different threads (in a threadpool), depending on if you use `async def` or normal `def`. This is what gives all the performance improvements to FastAPI.
But Python 3.7 and above provide a more advanced alternative to threading.local, that can also be used in the places where threading.local would be used, but is compatible with the new async features.
We are going to use that. It's called contextvars.
We are going to override the internal parts of Peewee that use threading.local and replace them with contextvars, with the corresponding updates.
This might seem a bit complex (and it actually is), you don't really need to completely understand how it works to use it.
We will create a PeeweeConnectionState:
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
This class inherits from a special internal class used by Peewee.
It has all the logic to make Peewee use contextvars instead of threading.local.
contextvars works a bit differently than threading.local. But the rest of Peewee's internal code assumes that this class works with threading.local.
So, we need to do some extra tricks to make it work as if it was just using threading.local. The __init__, __setattr__, and __getattr__ implement all the required tricks for this to be used by Peewee without knowing that it is now compatible with FastAPI.
!!! tip This will just make Peewee behave correctly when used with FastAPI. Not randomly opening or closing connections that are being used, creating errors, etc.
But it doesn't give Peewee async super-powers. You should still use normal `def` functions and not `async def`.
Use the custom PeeweeConnectionState class
Now, overwrite the ._state internal attribute in the Peewee database db object using the new PeeweeConnectionState:
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
!!! tip
Make sure you overwrite db._state after creating db.
!!! tip
You would do the same for any other Peewee database, including PostgresqlDatabase, MySQLDatabase, etc.
Create the database models
Let's now see the file sql_app/models.py.
Create Peewee models for our data
Now create the Peewee models (classes) for User and Item.
This is the same you would do if you followed the Peewee tutorial and updated the models to have the same data as in the SQLAlchemy tutorial.
!!! tip Peewee also uses the term "model" to refer to these classes and instances that interact with the database.
But Pydantic also uses the term "**model**" to refer to something different, the data validation, conversion, and documentation classes and instances.
Import db from database (the file database.py from above) and use it here.
{!../../../docs_src/sql_databases_peewee/sql_app/models.py!}
!!! tip Peewee creates several magic attributes.
It will automatically add an `id` attribute as an integer to be the primary key.
It will chose the name of the tables based on the class names.
For the `Item`, it will create an attribute `owner_id` with the integer ID of the `User`. But we don't declare it anywhere.
Create the Pydantic models
Now let's check the file sql_app/schemas.py.
!!! tip
To avoid confusion between the Peewee models and the Pydantic models, we will have the file models.py with the Peewee models, and the file schemas.py with the Pydantic models.
These Pydantic models define more or less a "schema" (a valid data shape).
So this will help us avoiding confusion while using both.
Create the Pydantic models / schemas
Create all the same Pydantic models as in the SQLAlchemy tutorial:
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
!!! tip
Here we are creating the models with an id.
We didn't explicitly specify an `id` attribute in the Peewee models, but Peewee adds one automatically.
We are also adding the magic `owner_id` attribute to `Item`.
Create a PeeweeGetterDict for the Pydantic models / schemas
When you access a relationship in a Peewee object, like in some_user.items, Peewee doesn't provide a list of Item.
It provides a special custom object of class ModelSelect.
It's possible to create a list of its items with list(some_user.items).
But the object itself is not a list. And it's also not an actual Python generator. Because of this, Pydantic doesn't know by default how to convert it to a list of Pydantic models / schemas.
But recent versions of Pydantic allow providing a custom class that inherits from pydantic.utils.GetterDict, to provide the functionality used when using the orm_mode = True to retrieve the values for ORM model attributes.
We are going to create a custom PeeweeGetterDict class and use it in all the same Pydantic models / schemas that use orm_mode:
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
Here we are checking if the attribute that is being accessed (e.g. .items in some_user.items) is an instance of peewee.ModelSelect.
And if that's the case, just return a list with it.
And then we use it in the Pydantic models / schemas that use orm_mode = True, with the configuration variable getter_dict = PeeweeGetterDict.
!!! tip
We only need to create one PeeweeGetterDict class, and we can use it in all the Pydantic models / schemas.
CRUD utils
Now let's see the file sql_app/crud.py.
Create all the CRUD utils
Create all the same CRUD utils as in the SQLAlchemy tutorial, all the code is very similar:
{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!}
There are some differences with the code for the SQLAlchemy tutorial.
We don't pass a db attribute around. Instead we use the models directly. This is because the db object is a global object, that includes all the connection logic. That's why we had to do all the contextvars updates above.
Aso, when returning several objects, like in get_users, we directly call list, like in:
list(models.User.select())
This is for the same reason that we had to create a custom PeeweeGetterDict. But by returning something that is already a list instead of the peewee.ModelSelect the response_model in the path operation with List[models.User] (that we'll see later) will work correctly.
Main FastAPI app
And now in the file sql_app/main.py let's integrate and use all the other parts we created before.
Create the database tables
In a very simplistic way create the database tables:
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
Create a dependency
Create a dependency that will connect the database right at the beginning of a request and disconnect it at the end:
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
Here we have an empty yield because we are actually not using the database object directly.
It is connecting to the database and storing the connection data in an internal variable that is independent for each request (using the contextvars tricks from above).
Because the database connection is potentially I/O blocking, this dependency is created with a normal def function.
And then, in each path operation function that needs to access the database we add it as a dependency.
But we are not using the value given by this dependency (it actually doesn't give any value, as it has an empty yield). So, we don't add it to the path operation function but to the path operation decorator in the dependencies parameter:
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
Context variable sub-dependency
For all the contextvars parts to work, we need to make sure we have an independent value in the ContextVar for each request that uses the database, and that value will be used as the database state (connection, transactions, etc) for the whole request.
For that, we need to create another async dependency reset_db_state() that is used as a sub-dependency in get_db(). It will set the value for the context variable (with just a default dict) that will be used as the database state for the whole request. And then the dependency get_db() will store in it the database state (connection, transactions, etc).
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
For the next request, as we will reset that context variable again in the async dependency reset_db_state() and then create a new connection in the get_db() dependency, that new request will have its own database state (connection, transactions, etc).
!!! tip As FastAPI is an async framework, one request could start being processed, and before finishing, another request could be received and start processing as well, and it all could be processed in the same thread.
But context variables are aware of these async features, so, a Peewee database state set in the `async` dependency `reset_db_state()` will keep its own data throughout the entire request.
And at the same time, the other concurrent request will have its own database state that will be independent for the whole request.
Peewee Proxy
If you are using a Peewee Proxy, the actual database is at db.obj.
So, you would reset it with:
async def reset_db_state():
database.db.obj._state._state.set(db_state_default.copy())
database.db.obj._state.reset()
Create your FastAPI path operations
Now, finally, here's the standard FastAPI path operations code.
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
About def vs async def
The same as with SQLAlchemy, we are not doing something like:
user = await models.User.select().first()
...but instead we are using:
user = models.User.select().first()
So, again, we should declare the path operation functions and the dependency without async def, just with a normal def, as:
# Something goes here
def read_users(skip: int = 0, limit: int = 100):
# Something goes here
Testing Peewee with async
This example includes an extra path operation that simulates a long processing request with time.sleep(sleep_time).
It will have the database connection open at the beginning and will just wait some seconds before replying back. And each new request will wait one second less.
This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads.
If you want to check how Peewee would break your app if used without modification, go the the sql_app/database.py file and comment the line:
# db._state = PeeweeConnectionState()
And in the file sql_app/main.py file, comment the body of the async dependency reset_db_state() and replace it with a pass:
async def reset_db_state():
# database.db._state._state.set(db_state_default.copy())
# database.db._state.reset()
pass
Then run your app with Uvicorn:
$ uvicorn sql_app.main:app --reload
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Open your browser at http://127.0.0.1:8000/docs and create a couple of users.
Then open 10 tabs at http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get at the same time.
Go to the path operation "Get /slowusers/" in all of the tabs. Use the "Try it out" button and execute the request in each tab, one right after the other.
The tabs will wait for a bit and then some of them will show Internal Server Error.
What happens
The first tab will make your app create a connection to the database and wait for some seconds before replying back and closing the database connection.
Then, for the request in the next tab, your app will wait for one second less, and so on.
This means that it will end up finishing some of the last tabs' requests earlier than some of the previous ones.
Then one the last requests that wait less seconds will try to open a database connection, but as one of those previous requests for the other tabs will probably be handled in the same thread as the first one, it will have the same database connection that is already open, and Peewee will throw an error and you will see it in the terminal, and the response will have an Internal Server Error.
This will probably happen for more than one of those tabs.
If you had multiple clients talking to your app exactly at the same time, this is what could happen.
And as your app starts to handle more and more clients at the same time, the waiting time in a single request needs to be shorter and shorter to trigger the error.
Fix Peewee with FastAPI
Now go back to the file sql_app/database.py, and uncomment the line:
db._state = PeeweeConnectionState()
And in the file sql_app/main.py file, uncomment the body of the async dependency reset_db_state():
async def reset_db_state():
database.db._state._state.set(db_state_default.copy())
database.db._state.reset()
Terminate your running app and start it again.
Repeat the same process with the 10 tabs. This time all of them will wait and you will get all the results without errors.
...You fixed it!
Review all the files
Remember you should have a directory named my_super_project (or however you want) that contains a sub-directory called sql_app.
sql_app should have the following files:
-
sql_app/__init__.py: is an empty file. -
sql_app/database.py:
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
sql_app/models.py:
{!../../../docs_src/sql_databases_peewee/sql_app/models.py!}
sql_app/schemas.py:
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
sql_app/crud.py:
{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!}
sql_app/main.py:
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
Technical Details
!!! warning These are very technical details that you probably don't need.
The problem
Peewee uses threading.local by default to store it's database "state" data (connection, transactions, etc).
threading.local creates a value exclusive to the current thread, but an async framework would run all the code (e.g. for each request) in the same thread, and possibly not in order.
On top of that, an async framework could run some sync code in a threadpool (using asyncio.run_in_executor), but belonging to the same request.
This means that, with Peewee's current implementation, multiple tasks could be using the same threading.local variable and end up sharing the same connection and data (that they shouldn't), and at the same time, if they execute sync I/O-blocking code in a threadpool (as with normal def functions in FastAPI, in path operations and dependencies), that code won't have access to the database state variables, even while it's part of the same request and it should be able to get access to the same database state.
Context variables
Python 3.7 has contextvars that can create a local variable very similar to threading.local, but also supporting these async features.
There are several things to have in mind.
The ContextVar has to be created at the top of the module, like:
some_var = ContextVar("some_var", default="default value")
To set a value used in the current "context" (e.g. for the current request) use:
some_var.set("new value")
To get a value anywhere inside of the context (e.g. in any part handling the current request) use:
some_var.get()
Set context variables in the async dependency reset_db_state()
If some part of the async code sets the value with some_var.set("updated in function") (e.g. like the async dependency), the rest of the code in it and the code that goes after (including code inside of async functions called with await) will see that new value.
So, in our case, if we set the Peewee state variable (with a default dict) in the async dependency, all the rest of the internal code in our app will see this value and will be able to reuse it for the whole request.
And the context variable would be set again for the next request, even if they are concurrent.
Set database state in the dependency get_db()
As get_db() is a normal def function, FastAPI will make it run in a threadpool, with a copy of the "context", holding the same value for the context variable (the dict with the reset database state). Then it can add database state to that dict, like the connection, etc.
But if the value of the context variable (the default dict) was set in that normal def function, it would create a new value that would stay only in that thread of the threadpool, and the rest of the code (like the path operation functions) wouldn't have access to it. In get_db() we can only set values in the dict, but not the entire dict itself.
So, we need to have the async dependency reset_db_state() to set the dict in the context variable. That way, all the code has access to the same dict for the database state for a single request.
Connect and disconnect in the dependency get_db()
Then the next question would be, why not just connect and disconnect the database in the async dependency itself, instead of in get_db()?
The async dependency has to be async for the context variable to be preserved for the rest of the request, but creating and closing the database connection is potentially blocking, so it could degrade performance if it was there.
So we also need the normal def dependency get_db().