Get all required fields of a nested Python Pydantic model - python

My pydantic nested model is defined as below:
from pydantic import BaseModel
from typing import Optional
class Location(BaseModel):
city: Optional[str]
state: str
country: str
class User(BaseModel):
id: int
name: Optional[str] = "Gandalf"
age: Optional[int]
location: Location
I would like to get all required fields for the User model.
For the above example, the expected output is ["id", "name", "state", "country"].
Any help greatly appreciated.

Here is a solution with a generator function:
from collections.abc import Iterator
def required_fields(model: type[BaseModel], recursive: bool = False) -> Iterator[str]:
for name, field in model.__fields__.items():
t = field.type_
if not field.required:
continue
if recursive and isinstance(t, type) and issubclass(t, BaseModel):
yield from required_fields(t, recursive=True)
else:
yield name
Using the models you defined in your example, we can demonstrate it like this:
print(list(required_fields(User, recursive=True)))
Output:
['id', 'state', 'country']

Related

Get List Of Class's Attributes Including Attributes Of Sub Objects - Python

I want to get a list of all the attributes of the class including the attributes used in sub_objects of this class.
Example:
#dataclass
class Phones:
mobile: Optional[str] = None
work_phone: Optional[str] = None
#dataclass
class People:
id: str
name: str
phones: Phones
I have People class and one of its attributes is of type Phones.
I want to return this list:
['id', 'name', 'mobile', 'work_phone']
I tried __dict__, __annotations__, dir() and more staff but I can't find a way to do it generic and dynamic. My solution is to do a convertor and return this list hardcoded which seems as a bad idea for maintenance.
I want all the attributes with primitive type. (For example I don't want to include phones.)
Recursion?
You dont need the sbNative thing, its just my module for clean logging.
from dataclasses import dataclass, is_dataclass
from typing import Optional
from sbNative.debugtools import log
#dataclass
class Phones:
mobile: Optional[str] = None
work_phone: Optional[str] = None
#dataclass
class People:
id: str
name: str
phones: Phones
def find_dataclasses(cls):
classes = []
for obj in cls.__annotations__.values():
if is_dataclass(obj):
classes += find_dataclasses(obj)
classes.append(obj)
return classes
if __name__ == "__main__":
log(*find_dataclasses(People))
Thanks to https://stackoverflow.com/users/13526701/noblockhit
I managed to achieve what I wanted with the next code:
def list_attributes(entity: object) -> List[str]:
"""
#returns: List of all the primitive attributes
"""
attributes: List[str] = []
entity_attributes = entity.__annotations__.items()
for attribute_name, attribute_type in entity_attributes:
if is_dataclass(attribute_type):
attributes += list_attributes(attribute_type)
else:
attributes.append(attribute_name)
return attributes
This is good, but unfortunately it won't work if you have a more complex annotation, such as a container type like List[Phones]. For example:
#dataclass
class Phones:
mobile: Optional[str] = None
work_phone: Optional[str] = None
#dataclass
class People:
id: str
name: str
phones: List[Phones]
The current output will be: ['id', 'name', 'phones']. But, note that we want to exclude the field phones, and include all the fields of the Phone class, mobile and work_phone.
To handle such types, you can use typing.get_args() and iterate over each of the subscripted types, checking if each type is a dataclass.
from dataclasses import dataclass, is_dataclass
from typing import Optional, List, get_args
#dataclass
class Phones:
mobile: Optional[str] = None
work_phone: Optional[str] = None
#dataclass
class People:
id: str
name: str
phones: List[Phones]
def list_attributes(entity: object) -> List[str]:
"""
#returns: List of all the primitive attributes
"""
attributes: List[str] = []
entity_attributes = entity.__annotations__.items()
for attribute_name, attribute_type in entity_attributes:
args = get_args(attribute_type)
if args:
found_class = False
for arg in args:
if is_dataclass(arg):
found_class = True
attributes += list_attributes(arg)
if not found_class:
attributes.append(attribute_name)
elif is_dataclass(attribute_type):
attributes += list_attributes(attribute_type)
else:
attributes.append(attribute_name)
return attributes
print(list_attributes(People))
The above modified version correctly outputs the desired result:
['id', 'name', 'mobile', 'work_phone']

Pydantic Recursive Models

I am refencing the answer on this other stackoverflow post on using the Typing library Literal to specify a unique array to strings to validate the data with Pydantic but I am running into a problem of calling another class recursively.
This is what my code looks like:
from pydantic import BaseModel, PydanticValueError, ValidationError, validator
from typing import Literal,Optional
ACTION_TYPE_MAPPING = Literal["read", "write", "release"]
OBJECT_TYPE_MAPPING = Literal["multiStateValue", "multiStateInput", "multiStateOutput",
"analogValue", "analogInput", "analogOutput",
"binaryValue", "binaryInput", "binaryOutput"]
BOOLEAN_ACTION_MAPPING = Literal["active", "inactive"]
# MAIN MODEL
class BacnetRequestModel(BaseModel):
action_type: ACTION_TYPE_MAPPING
object_type: OBJECT_TYPE_MAPPING
object_instance: int
value: Optional[ValueModel(object_type)] <---- MESSED UP HERE, how to call ValueModel?
class ValueModel(BaseModel):
multiStateValue: Optional[int]
multiStateInput: Optional[int]
multiStateOutput: Optional[int]
analogValue: Optional[int]
analogInput: Optional[int]
analogOutput: Optional[int]
binaryValue: Optional[BOOLEAN_ACTION_MAPPING]
binaryInput: Optional[BOOLEAN_ACTION_MAPPING]
binaryOutput: Optional[BOOLEAN_ACTION_MAPPING]
test = BacnetRequestModel(action_type="write",
object_type="binaryOutput",
object_instance="3",
value = "active"
)
How do I call the class ValueModel based on the objectType that was inputted to the function where in this case it was binaryOutput that should only accept a value of BOOLEAN_ACTION_MAPPING. Any tips help not a lot of wisdom here...
Traceback is:
value = Optional[ValueModel(object_type)]
NameError: name 'ValueModel' is not defined

How to pass the path parameter to the Pydantic model?

Is there some way to do the following?
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: str
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
#app.post("/items/{item_id}")
async def create_item(item: Item):
return item
I want to have the item_id path parameter value inside the Item model.
Is it possible?
Pydantic in FastAPI is used to define data model. You can do as follows:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: Optional[str] = None
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
#app.post("/items/{item_id}")
async def create_item(item_id, item: Item):
item.id = item_id
return item
Please, note that id is declared as optional to avoid validation problems with body parameters in the request. Indeed, you are not going to pass the id in the body but in the path.
If necessary, you can also decouple the request- from the response-model (see here). It depends on what you need.
Replace your id: str with id: Optional [str] = None
Replace your create_item (item: Item) with create_item (item_id, item: Item):
Add item.id = item_id under your async. Obviously then you leave the ruo return item

How to extend a Pydantic object and change some fields' type?

There are two similar pydantic object like that. The only difference is some fields are optionally.
How can I just define the fields in one object and extend into another one?
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
class ProjectPatchObject(ProjectCreateObject):
project_id: str
project_name: Optional[str]
project_type: Optional[ProjectTypeEnum]
depot: Optional[str]
system: Optional[str]
...
I find a good and easy way by __init__subclass__.
The docs also can be generated successfully.
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
def __init_subclass__(cls, optional_fields=(), **kwargs):
"""
allow some fields of subclass turn into optional
"""
super().__init_subclass__(**kwargs)
for field in optional_fields:
cls.__fields__[field].outer_type_ = Optional
cls.__fields__[field].required = False
_patch_fields = ProjectCreateObject.__fields__.keys() - {'project_id'}
class ProjectPatchObject(ProjectCreateObject, optional_fields=_patch_fields):
pass
You've pretty much answered it yourself. Unless there's something more to the question.
from typing import Optional
from pydantic import BaseModel
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: str
depot: str
system: str
class ProjectPatchObject(ProjectCreateObject):
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
if __name__ == "__main__":
p = ProjectCreateObject(
project_id="id",
project_name="name",
project_type="type",
depot="depot",
system="system",
)
print(p)
c = ProjectPatchObject(project_id="id", depot="newdepot")
print(c)
Running this gives:
project_id='id' project_name='name' project_type='type' depot='depot' system='system'
project_id='id' project_name=None project_type=None depot='newdepot' system=None
Another way to look at it is to define the base as optional and then create a validator to check when all required:
from pydantic import BaseModel, root_validator, MissingError
class ProjectPatchObject(BaseModel):
project_id: str
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
class ProjectCreateObject(ProjectPatchObject):
#root_validator
def check(cls, values):
for k, v in values.items():
if v is None:
raise MissingError()
return values
Or use metaclass like in this thread: Make every fields as optional with Pydantic
class AllOptional(pydantic.main.ModelMetaclass):
def __new__(self, name, bases, namespaces, **kwargs):
annotations = namespaces.get('__annotations__', {})
for base in bases:
annotations.update(base.__annotations__)
for field in annotations:
if not field.startswith('__') and field != 'project_id':
annotations[field] = Optional[annotations[field]]
namespaces['__annotations__'] = annotations
return super().__new__(self, name, bases, namespaces, **kwargs)
And in your example...
class ProjectPatchObject(ProjectCreateObject, metaclass=AllOptional):
...

Partial update in FastAPI

I want to implement a put or patch request in FastAPI that supports partial update. The official documentation is really confusing and I can't figure out how to do the request. (I don't know that items is in the documentation since my data will be passed with request's body, not a hard-coded dict).
class QuestionSchema(BaseModel):
title: str = Field(..., min_length=3, max_length=50)
answer_true: str = Field(..., min_length=3, max_length=50)
answer_false: List[str] = Field(..., min_length=3, max_length=50)
category_id: int
class QuestionDB(QuestionSchema):
id: int
async def put(id: int, payload: QuestionSchema):
query = (
questions
.update()
.where(id == questions.c.id)
.values(**payload)
.returning(questions.c.id)
)
return await database.execute(query=query)
#router.put("/{id}/", response_model=QuestionDB)
async def update_question(payload: QuestionSchema, id: int = Path(..., gt=0),):
question = await crud.get(id)
if not question:
raise HTTPException(status_code=404, detail="question not found")
## what should be the stored_item_data, as documentation?
stored_item_model = QuestionSchema(**stored_item_data)
update_data = payload.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
response_object = {
"id": question_id,
"title": payload.title,
"answer_true": payload.answer_true,
"answer_false": payload.answer_false,
"category_id": payload.category_id,
}
return response_object
How can I complete my code to get a successful partial update here?
Posting this here for googlers who are looking for an intuitive solution for creating Optional Versions of their pydantic Models without code duplication.
Let's say we have a User model, and we would like to allow for PATCH requests to update the User. But we need to create a schema that tells FastApi what to expect in the content body, and specifically that all the fields are Optional (Since that's the nature of PATCH requests). We can do so without redefining all the fields
from pydantic import BaseModel
from typing import Optional
# Creating our Base User Model
class UserBase(BaseModel):
username: str
email: str
# And a Model that will be used to create an User
class UserCreate(UserBase):
password: str
Code Duplication ❌
class UserOptional(UserCreate):
username: Optional[str]
email: Optional[str]
password: Optional[str]
One Liner ✅
# Now we can make a UserOptional class that will tell FastApi that all the fields are optional.
# Doing it this way cuts down on the duplication of fields
class UserOptional(UserCreate):
__annotations__ = {k: Optional[v] for k, v in UserCreate.__annotations__.items()}
NOTE: Even if one of the fields on the Model is already Optional, it won't make a difference due to the nature of Optional being typing.Union[type passed to Optional, None] in the background.
i.e typing.Union[str, None] == typing.Optional[str]
You can even make it into a function if your going to be using it more than once:
def convert_to_optional(schema):
return {k: Optional[v] for k, v in schema.__annotations__.items()}
class UserOptional(UserCreate):
__annotations__ = convert_to_optional(UserCreate)
I got this answer on the FastAPI's Github issues.
You could make the fields Optional on the base class and create a new QuestionCreate model that extends the QuestionSchema. As an example:
from typing import Optional
class Question(BaseModel):
title: Optional[str] = None # title is optional on the base schema
...
class QuestionCreate(Question):
title: str # Now title is required
The cookiecutter template here provides some good insight too.
I created a library (pydantic-partial) just for that, converting all the fields in the normal DTO model to being optional. See https://medium.com/#david.danier/how-to-handle-patch-requests-with-fastapi-c9a47ac51f04 for a code example and more detailed explanation.
https://github.com/team23/pydantic-partial/
Based on the answer of #cdraper, I made a partial model factory:
from typing import Mapping, Any, List, Type
from pydantic import BaseModel
def model_annotations_with_parents(model: BaseModel) -> Mapping[str, Any]:
parent_models: List[Type] = [
parent_model for parent_model in model.__bases__
if (
issubclass(parent_model, BaseModel)
and hasattr(parent_model, '__annotations__')
)
]
annotations: Mapping[str, Any] = {}
for parent_model in reversed(parent_models):
annotations.update(model_annotations_with_parents(parent_model))
annotations.update(model.__annotations__)
return annotations
def partial_model_factory(model: BaseModel, prefix: str = "Partial", name: str = None) -> BaseModel:
if not name:
name = f"{prefix}{model.__name__}"
return type(
name, (model,),
dict(
__module__=model.__module__,
__annotations__={
k: Optional[v]
for k, v in model_annotations_with_parents(model).items()
}
)
)
def partial_model(cls: BaseModel) -> BaseModel:
return partial_model_factory(cls, name=cls.__name__)
Can be used with the function partial_model_factory:
PartialQuestionSchema = partial_model_factory(QuestionSchema)
Or with decorator partial_model:
#partial_model
class PartialQuestionSchema(QuestionSchema):
pass

Categories

Resources