Is it possible to modify Pydantic BaseModel attributes just after creating it? - python

I am starting to learn FastAPI and Pydantic and have a doubt. I have the following subclass of BaseModel
class Product(BaseModel):
image: str
name: str
After saving this model, I want image to store the value /static/ + image so as to create nice hyperlinked REST endpoint. This is possible using __post_init_post_parse__ hook of pydantic dataclass but since FastAPI currently doesn't support it, I was wondering what can be a workaround this.

You could use a custom validator:
>>> from pydantic import BaseModel, validator
>>> class Product(BaseModel):
image: str
name: str
#validator('image')
def static_mage(cls, image):
return '/static/{}'.format(image)
>>> p = Product(image='pic.png', name='product_1')
>>> p
Product(image='/static/pic.png', name='product_1')

Related

Return uppercase UUID in FastApi

When using FastApi with a pydantic model at the reponse model, I found that the uuid are always returned lowercase by the http response. Is there any standard way to return them upper cased?
from fastapi import FastAPI
from pydantic import BaseModel
from uuid import UUID
app = FastAPI()
class Test(BaseModel):
ID: UUID
#app.get("/test", response_model=Test)
async def test():
id_ = uuid.uuid4()
return Test(ID=id_)
When making the request the returned uuid will be in lowercased.
from requestr
a = requests.get("http://localhost:800/test").text # you ir
# a -> '{"ID":"fffc0b5b-8e8d-4d06-b910-2ae8d616166c"}' # it is lowercased
The only somewhat hacky way I found to return them uppercased is overwriting the uuid class __str__ method or sub-classing uuid:
What I tried (and works):
# use in main.py when importing for first time
def newstr(self):
hex = '%032x' % self.int
return ('%s-%s-%s-%s-%s' % (hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:])).upper()
uuid.UUID.__str__ = newstr
But I was wondering if there is a standard way of doing this without modifying the original class, maybe a post process in pydantic or a setting in FastApi.
You can define custom json_encoders:
class Test(BaseModel):
ID: UUID
class Config:
json_encoders = {
UUID: lambda val: str(val).upper()
}

SqlModel : Fastapi AttributeError: type object 'AddressBaseCore' has no attribute '__config__'

I am new to fastapi and SQLModel, i was trying to implement some basic code from my existing lib, I have an Address Class
like
#dataclass
class Address(DataClassJsonMixin):
addr1: str
city: str
province: str
I simply want to create a class in SQLModel that connects to DB. I have only added a new column ID here. i am getting below error where i am not sure why is it asking for a config attribute.
class AddressMaster(SQLModel, Address):
id: int = Field(default=None, primary_key=True)
AttributeError: type object 'Address' has no attribute '__config__'
It's failing on config = getattr(base, "__config__") that has some information which I am not able to comprehand.
# Only one of the base classes (or the current one) should be a table model
# this allows FastAPI cloning a SQLModel for the response_model without
# trying to create a new SQLAlchemy, for a new table, with the same name, that
# triggers an error
try 1:
from sqlmodel import SQLModel, Field
from ...core import Address
from dataclasses import dataclass
#dataclass
class AddressDB(Address, SQLModel):
pass
# END AddressDB
class AddressMaster(AddressDB, table=True):
"""
Address Master Table
"""
id: int = Field(default=None, primary_key=True)
# END AddressMaster
Object Creation
objAd = AddressMaster.from_dict({"addr1": "Kashmir", "city": "Srinagar", "province": "Kashmir"})
There is an error in the semantics of your AdressMaster class.
If it is meant to be a class related to your DB. Then you have to specify in the first parameter either the class inheriting from a SQL model or from SQLmodel (And in this case, you should rewrite each attribute of your model within this class) directly. And it is necessary to pass it the argument table=True
class AddressMaster(Address, table=True):
id: int = Field(default=None, primary_key=True)
# Here the attributes will be inherited from your Adress class
# (provided that this one in its parentage is an inheritance link with a modelSQL)
Or
class AddressMaster(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
addr1: str
city: str
province: str
# Here, the class is independent from the other pydantic
# validation models since it inherits directly from SQLModel
In Try 1:
You are trying to pass two parameters to your AddressDB class, one of which is an SQLModel. However, SQLModel allows to override SQLAlchemy, and accepts as parameter only models from SQLAlchemy or Pydantic. During the initialization, it goes through the arguments passed in parameter and tries to call the method or attribute Config which exists in the pydantic and SQLAlchemy models. This is the source of your error since you pass in parameter a DataClassJsonMixin which has no Config method or attribute. This is the origin of your error.
How to solve it. You just have to not call DataClassJsonMixin which seems to me to encode / decode JSON data. However, this is a basic behavior of Pydantic (which is used behind SQLModel).
So if you use the first method shown above (i.e. inherited from a SQLModel), you just have to put your validation fields inside AddressDB and make this class inherit only from SQLModel
class AddressDB(SQLModel):
addr1: str
city: str
province: str

How can I unpack a Pydantic BaseModel into kwargs?

I am trying to make a function that takes a pydantic BaseModel as an input to run another function. I need to unpack the BaseModel into kwargs. I tried doing this:
def run_routing_from_job(job):
return run_routing(
job.inputs.input_file,
**job.inputs.config.dict()
)
where job is of the format
class Job(BaseModel):
client_info: ClientInfo # Another BaseModel
inputs: RoutingJobInputs # Another BaseModel
uid: UUID = Field(default_factory=uuid4)
status: str = "job_queued"
result: int = None
However, doing .dict() parses all of the items recursively into a dictionary format. I want to keep the client_info and inputs as a BaseModel class, not convert it into a dictionary.
I could make a way to do it, but I can't find a clean way to do it.
I worked it out, just replace .dict() with __dict__
def run_routing_from_job(job):
return run_routing(
job.inputs.input_file,
**job.inputs.config.__dict__
)

Changing pydantic model Field() arguments with class variables for Fastapi

I'm a little new to tinkering with class inheritance in python, particularly when it comes down to using class attributes. In this case I am using a class attribute to change an argument in pydantic's Field() function. This wouldn't be too hard to do if my class contained it's own constructor, however, my class User1 is inheriting this from pydantic's BaseModel.
The idea is that I would like to be able to change the class attribute prior to creating the instance.
Please see some example code below:
from pydantic import Basemodel, Field
class User1(BaseModel):
_set_ge = None # create class attribute
item: float = Field(..., ge=_set_ge)
# avoid overriding BaseModel's __init__
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
User1._set_ge = 0 # setting the class attribute to a new value
instance = User1(item=-1)
print(instance) # item=-1.0
When creating the instance using instance = User1(item=-1) I would expect a validation error to be thrown, but it instead passes validation and simply returns the item value.
If I had my own constructor there would be little issue in changing the _set_ge, but as User1 inheriting this constructor from BaseModel, things are a little more complicated.
The eventual aim is to add this class to a fastapi endpoint as follows:
from fastapi import Fastapi
from schemas import User1
class NewUser1(User1):
pass
NewUser1._set_ge = 0
#app.post("/")
def endpoint(request: NewUser1):
return User1.item
To reduce code duplication, I aimed to use this method to easily change Field() arguments. If there is a better way, I'd be glad to consider that too.
This question is quite closely related to this unanswered one.
In the end, the #validator proposal by #hernán-alarcón is probably the best way to do this. For example:
from pydantic import Basemodel, Field, NumberNotGeError
from typing import ClassVar
class User(BaseModel):
_set_ge = ClassVar[float] # added the ClassVar typing to make clearer, but the underscore should be sufficient
item: float = Field(...)
#validator('item')
def limits(cls, v):
limit_number = cls._set_ge
if v >= limit_number:
return v
else:
raise NumberNotGeError(limit_value=limit_number)
class User1(User)
_set_ge = 0 # setting the class attribute to a new value
instance = User1(item=-1) # raises the error

Assigning Pydantic Fields not by alias

How can I create a pydantic object, without useing alias names?
from pydantic import BaseModel, Field
class Params(BaseModel):
var_name: int = Field(alias='var_alias')
Params(var_alias=5) # works
Params(var_name=5) # does not work
You need to use allow_population_by_field_name model config option, which is False by default.
from pydantic import BaseModel, Field
class Params(BaseModel):
var_name: int = Field(alias='var_alias')
class Config:
allow_population_by_field_name = True
Params(var_alias=5) # works
Params(var_name=5) # works

Categories

Resources