Automatically generate two versions of your pydantic models: one with Extra.forbid and one with Extra.ignore
Project description
pydantic-duality
Automatically and lazily generate three versions of your pydantic models: one with Extra.forbid, one with Extra.ignore, and one with all fields optional
Installation
pip install pydantic-duality
Quickstart
Given the following models:
from pydantic_duality import ConfigMixin
class User(ConfigMixin):
id: UUID
name: str
class Auth(ConfigMixin):
some_field: str
user: User
Using pydantic-duality is roughly equivalent to making all of the following models by hand:
from pydantic import BaseModel
# Equivalent to User and User.__request__
class UserRequest(BaseModel, extra=Extra.forbid):
id: UUID
name: str
# Equivalent to Auth and Auth.__request__
class AuthRequest(BaseModel, extra=Extra.forbid):
some_field: str
user: UserRequest
# Equivalent to User.__response__
class UserResponse(BaseModel, extra=Extra.ignore):
id: UUID
name: str
# Equivalent to Auth.__response__
class AuthResponse(BaseModel, extra=Extra.ignore):
some_field: str
user: UserResponse
# Equivalent to User.__patch_request__
class UserPatchRequest(BaseModel, extra=Extra.forbid):
id: UUID | None
name: str | None
# Equivalent to Auth.__patch_request__
class AuthPatchRequest(BaseModel, extra=Extra.forbid):
some_field: str | None
user: UserPatchRequest | None
So it takes you up to 4 times less code to write the same thing. Note also that pydantic-duality does everything lazily so you will not notice any significant performance or memory usage difference when using it instead of pydantic-duality. Think of it as using all the customized models as cached properties.
Use case
Problem
In API design, it is a good pattern to forbid any extra data from being sent to your endpoints. By default, pydantic just ignores extra data in FastAPI requests. You can fix that by passing extra = Extra.forbid
to your model's config. However, we needed to use Extra.ignore in our response models because we might send a lot more data than required with our responses. But then we get into the following conundrum:
class User(BaseModel):
id: UUID
name: str
class AuthResponse(BaseModel):
some_field: str
user: User
class AuthRequest(SomeResponse, extra=Extra.forbid):
pass
Now you have a problem: even though SomeRequest
is Extra.forbid
, User
is not. It means that your clients can still pass the following payload without any issues:
{
"some_field": "value",
"user": {"id": "e65014c9-4990-4b8d-8ce7-ab5a34ab41bc", "name": "Ovsyanka", "hello": "world"}
}
The easiest way to solve this is to have UserRequest
and UserResponse
, and duplicate this field in your models:
class UserResponse(BaseModel):
id: UUID
name: str
class UserRequest(UserResponse, extra=Extra.forbid):
pass
class AuthResponse(BaseModel):
some_field: str
user: UserResponse
class AuthRequest(SomeResponse, extra=Extra.forbid):
user: UserRequest
Now imagine that users also have the field named "address" that points to some Address
model. Essentially nearly all of your models will need to be duplicated in a similar manner, leading to almost twice as much code.
When we faced this conundrum, we already had an enormous code base so the duplication solution would be a tad too expensive.
Solution
pydantic-duality does this code duplication for you in an intuitive manner automatically. Here's how the models above would look if we used it:
from pydantic_duality import ConfigMixin
class User(ConfigMixin):
id: UUID
name: str
class Auth(ConfigMixin):
some_field: str
user: User
You would use the models above as follows:
Auth.__request__.parse_object(
{
"some_field": "value",
"user": {"id": "e65014c9-4990-4b8d-8ce7-ab5a34ab41bc", "name": "Ovsyanka"}
}
)
Auth.__response__.parse_object(
{
"some_field": "value",
"user": {"id": "e65014c9-4990-4b8d-8ce7-ab5a34ab41bc", "name": "Ovsyanka", "hello": "world"}
}
)
Patch requests
We applied the same principles to solve the problem of schemas for patching objects. Usually these schemas are one-to-one equivalent to regular request schemas except that all fields are nullable. If you wish to do the same thing automatically, you can use __patch_request__
attribute similar to how you would use __request__
and __response__
.
Usage
Creation
Models are created in the exact same manner as pydantic models but you use our ConfigMixin
as base instead of BaseModel
.
from pydantic_duality import ConfigMixin
class User(ConfigMixin):
id: UUID
name: str
class Auth(ConfigMixin):
some_field: str
user: User
If you wish to provide your own base config for all of your models, you can do:
from pydantic_duality import generate_config_mixin
# Any configuration options you like
class MyConfig:
orm_mode = True
...
ConfigMixin = generate_config_mixin(MyConfig)
Parsing
Default
Whenever you do not want to use pydantic-duality's features, you can use your models as if they were regular pydantic models. For example:
class User(ConfigMixin):
id: UUID
name: str
user = User(id="e65014c9-4990-4b8d-8ce7-ab5a34ab41bc", name="Ovsyanka")
print(user.dict())
This is possible because User
is nearly equivalent to User.__request__
. It has all the same fields, operations, and hash value. issubclass and isinstance checks will also show that instances of User.__request__
are also instances of User
. It is, however, important to realize that User is not User.__request__
, it just tries to be as similar as possible.
Advanced
If you need to use __response__
version or both versions of your model, you can do so through __request__
and __response__
attributes. They will give you an identical model with only the difference that __request__
has Extra.forbid and __response__
has Extra.ignore.
class User(ConfigMixin):
id: str
name: str
User.__request__(id="e65014c9", name="John", hello="world") # ValidationError
User.__response__(id="e65014c9", name="John", hello="world") # UserResponse(id="e65014c9", name="John")
User.__patch_request__(id="e65014c9") # UserResponse(id="e65014c9", name=None)
FastAPI integration
pydantic-duality works with FastAPI out of the box. Note, however, that if you want to use Extra.ignore schemas for responses, you have to specify it explicitly with response_model=MyModel.__response__
. Otherwise the Extra.forbid schema will be used.
Configuration override
If you specify extra=Extra.forbid or extra=Extra.ignore on your model explicitly, then pydantic-duality will not change its or its children's extra configuration. Nested models will still be affected as you might expect.
Editor support
This package is fully type hinted. mypy, pyright, and pycharm will detect that __response__
and __request__
attributes are equivalent to your model so you have full full editor support for them.
__patch_request__
is not well supported: pyright and mypy will still think that the model's attributes are non-nullable.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file pydantic_duality-0.5.1.tar.gz
.
File metadata
- Download URL: pydantic_duality-0.5.1.tar.gz
- Upload date:
- Size: 8.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.10.9 Linux/6.0.19-4-MANJARO
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9ea332cfbf3c56714447df8a713127eef2eb2b8f20797eb692a40100621d8e74 |
|
MD5 | 88632787c3f4e2dd03cb952a26d1d7b8 |
|
BLAKE2b-256 | 915d61231ad153456f38dcecba864c61779bf8f274159d6c82f192229d523488 |
File details
Details for the file pydantic_duality-0.5.1-py3-none-any.whl
.
File metadata
- Download URL: pydantic_duality-0.5.1-py3-none-any.whl
- Upload date:
- Size: 6.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.10.9 Linux/6.0.19-4-MANJARO
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 7737d2314732f1a229b03b61930262d7526cc21d2b76e7b9e316e93c7a9a7af6 |
|
MD5 | 79c689bbef25058a9bd59f128c54c3e8 |
|
BLAKE2b-256 | 781b94fc724bf242d8e3fb042d6378c12278b357e1c39fd9d56a6f4100084e1d |