Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
February 19, 2022 09:37 am GMT

The Best Practice of handling FastAPI Schema

The Best Practice of handling FastAPI Schema

FastAPI Has a concept of Schema. DTO is more familiar name If you have developed at Spring or NestJS. Simply, DTO is Data Transfer Object and It is a kind of promise to exchange object information between methods or classes in a specific model.

FastAPI's schema depends on pydantic model.

from pydantic import BaseModelclass Item(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: float

As you can see, schema is made by inheriting the pydantic Base Model. Now let's use this to construct an api endpoint.

from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Item(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: float@app.get("/")async def root(        item: Item):    return {"message": "Hello World"}

If you declare that you're gonna get an item schema at the basic endpoint,

Image description

In this way, an example of receiving the model is automatically generated. You can use this in other ways like

from fastapi import FastAPI, Dependsfrom pydantic import BaseModelapp = FastAPI()class Item(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: float@app.get("/")async def root(        item: Item = Depends()):    return {"message": "Hello World"}

If you register the value "Depends" at schema,

Image description

You can see that the part that you want to receive as a Body has changed to Parameter. Doesn't it feel like You've seen it somewhere? Right. It is a common method for constructing search query. However, as can be seen above, it can be seen that there is a limit of required values. It's a search query, but it doesn't make sense that all members have a required option. So it is necessary to change all of this to optional.

very easily

from pydantic import BaseModelfrom typing import Optionalclass Item(BaseModel):    name: Optional[str]    model: Optional[str]    manufacturer: Optional[str]    price: Optional[float]    tax: Optional[float]

It can be made in this way, but if the default value of the schema members are optional, there is a hassle of removing all the Optional when configuring the PUT method endpoint later. Oh, it is recommended that anyone who has no clear distinction between the PUT method and the PATCH method here refer to this article. In short, PUT is the promised method of changing the total value of an element, and PATCH is the method of changing some.

So I use this way.

from pydantic.main import ModelMetaclassclass AllOptional(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('__'):                annotations[field] = Optional[annotations[field]]        namespaces['__annotations__'] = annotations        return super().__new__(self, name, bases, namespaces, **kwargs)

This class has the function of changing all member variables of the class to Optional when declared as metaclass.

from fastapi import FastAPI, Dependsfrom pydantic import BaseModelfrom pydantic.main import ModelMetaclassfrom typing import Optionalapp = FastAPI()class AllOptional(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('__'):                annotations[field] = Optional[annotations[field]]        namespaces['__annotations__'] = annotations        return super().__new__(self, name, bases, namespaces, **kwargs)class Item(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: floatclass OptionalItem(Item, metaclass=AllOptional):    pass@app.get("/")async def root(        item: OptionalItem = Depends()):    return {"message": "Hello World"}

If you inherit the class made of the pydantic Base Model and declare 'metaclass=AllOptional' there,

Image description

You can see Parameters without required option.

However, when dealing with a schema in practice, there are things that are not needed when putting it in and necessary when bringing it later. It is common for id, created_datetime, updated_datetime, etc. And sometimes you have to remove some members and show them. In spring or nest, you can flexibly cope with this situation by using the setFieldToIgnore function or the Omit class, but as anyone who has dealt with FastAPI can see, there are no such things.

So after a lot of trial and error, i established a way of using FastAPI schema like the DTO we used before. It would be irrelevant to say that this is the best practice now. Why? I looked up numerous examples, but there was no perfect alternative, so I defined them myself and made them.

This is an example using the schema called Item used above.

from pydantic import BaseModelclass BaseItem(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: float

The most basic members are defined as BaseItem. It can be seen as defining the elements used in Upsert. The pydantic model will basically be a schema used in the PUT method because it will be treated as Required if it is not set as Optional or = None.

class Item(BaseItem, metaclass=AllOptional):    id: int    created_datetime: datetime    updated_datetime: datetime

It is an item that inherits BaseItem. Even if the class inherited the Base Model is inherited, the inherited class also becomes a pydantic model, so it can be declared this simply. It is used to throw items that are not used in Upsert, including basic elements. This is a schema mainly used to inquire items, so wouldn't it be okay not to attach Optional...? But if there is a non-Optional member and its value has no Value, an error is occurred.

class FindBase(BaseModel):    count: int    page: int    order: strclass FindItem(FindBase, BaseItem, metaclass=AllOptional):    pass

Parameters used to find items can be declared in this way. It inherits the BaseItem and FindBase declared above and treats them both as options with metaclass=AllOptional to remove the required option of the parameter. And lastly,

class Omit(ModelMetaclass):    def __new__(self, name, bases, namespaces, **kwargs):        omit_fields = getattr(namespaces.get("Config", {}), "omit_fields", {})        fields = namespaces.get('__fields__', {})        annotations = namespaces.get('__annotations__', {})        for base in bases:            fields.update(base.__fields__)            annotations.update(base.__annotations__)        merged_keys = fields.keys() & annotations.keys()        [merged_keys.add(field) for field in fields]        new_fields = {}        new_annotations = {}        for field in merged_keys:            if not field.startswith('__') and field not in omit_fields:                new_annotations[field] = annotations.get(field, fields[field].type_)                new_fields[field] = fields[field]        namespaces['__annotations__'] = new_annotations        namespaces['__fields__'] = new_fields        return super().__new__(self, name, bases, namespaces, **kwargs)

This is a metaclass that supports the Omit function. I needed it, but I couldn't find it, so I made it.

class BaseItem(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: floatclass OmittedTaxPrice(BaseItem, metaclass=Omit):    class Config:        omit_fields = {'tax', 'price'}

If you declare omit_fields in class Config in this way, you can get a new schema with removed fields.

And one thing to note is that metaclass is executed every time a class is created, so only one class involved in an inheritance relationship can be declared. For example

class BaseItem(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: floatclass Item(BaseItem, metaclass=AllOptional):    id: int    created_datetime: datetime    updated_datetime: datetimeclass TestItem(Item):    additional_number: int

If the Item declared metaclass=AllOptional is inherited from TestItem, AllOptional is executed twice. Once when creating an Item class, once again when creating a TestItem that inherits the Item. Therefore, the additional_number, a member variable of TestItem that inherits the Item, is also Optional. That is metaclass. So, if you declare another metaclass like Omit, the program is broken.

# ...class TestItem(Item, metaclass=AllOptional)    pass# ...# `TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases`

So, it is good to declare and use metaclass in the last inheritance class before it is used as schema. This makes easy to recognize its function.

Anyway, if you use the above-mentioned four http methods, GET, POST, PUT, and PATCH, which are often used, it consists of this. It can also be found in my git repo. https://github.com/jujumilk3/fastapi-best-practice/tree/schema

from datetime import datetimefrom fastapi import FastAPI, Dependsfrom pydantic import BaseModelfrom pydantic.main import ModelMetaclassfrom typing import Optional, Listapp = FastAPI()class AllOptional(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('__'):                annotations[field] = Optional[annotations[field]]        namespaces['__annotations__'] = annotations        return super().__new__(self, name, bases, namespaces, **kwargs)class Omit(ModelMetaclass):    def __new__(self, name, bases, namespaces, **kwargs):        omit_fields = getattr(namespaces.get("Config", {}), "omit_fields", {})        fields = namespaces.get('__fields__', {})        annotations = namespaces.get('__annotations__', {})        for base in bases:            fields.update(base.__fields__)            annotations.update(base.__annotations__)        merged_keys = fields.keys() & annotations.keys()        [merged_keys.add(field) for field in fields]        new_fields = {}        new_annotations = {}        for field in merged_keys:            if not field.startswith('__') and field not in omit_fields:                new_annotations[field] = annotations.get(field, fields[field].type_)                new_fields[field] = fields[field]        namespaces['__annotations__'] = new_annotations        namespaces['__fields__'] = new_fields        return super().__new__(self, name, bases, namespaces, **kwargs)class BaseItem(BaseModel):    name: str    model: str    manufacturer: str    price: float    tax: floatclass UpsertItem(BaseItem, metaclass=AllOptional):    passclass Item(BaseItem, metaclass=AllOptional):    id: int    created_datetime: datetime    updated_datetime: datetimeclass OmittedTaxPrice(BaseItem, metaclass=Omit):    class Config:        omit_fields = {'tax', 'price'}class FindBase(BaseModel):    count: int    page: int    order: strclass FindItem(FindBase, BaseItem, metaclass=AllOptional):    pass@app.get("/")async def root():    return {"message": "Hello World"}@app.get("/items/", response_model=List[Item])async def find_items(        find_query: FindItem = Depends()):    return {"hello": "world"}@app.post("/items/", response_model=Item)async def create_item(        schema: UpsertItem):    return {"hello": "world"}@app.patch("/items/{id}", response_model=Item)async def update_item(        id: int,        schema: UpsertItem):    return {"hello": "world"}@app.put("/items/{id}", response_model=Item)async def put_item(        id: int,        schema: BaseItem):    return {"hello": "world"}@app.get("/items/omitted", response_model=OmittedTaxPrice)async def omitted_item(        schema: OmittedTaxPrice):    return {"hello": "world"}

When configured in this way, the following swagger documents are generated.

1. find items endpoint with optional parameters

Image description
Image description

2. create item

Image description

3. Put method

Image description

If you omit the item name according to the usage of PUT METHOD, 422 error is returned to send it with some name. The PUT was formed by declaring that it would receive BaseItem as a body.

Image description
Image description

4. PATCH method

Image description

5. Omitted

Image description

In this way, we learned how to deal with FastAPIschema. If you use something, you may see more necessary functions, but the endpoint and docs configuration that you need this much is progressing without difficulty in my case. If you want to actively use FastAPI's schema to write docs, please refer to it to reduce trial and error.


Original Link: https://dev.to/gyudoza/the-best-practice-of-handling-fastapi-schema-2g3a

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To