Python peewee / fastapi get User without loading Items - python

So I followed this tutorial to kombine fastapi & peewee:
link
And due to this tutorial i got those models (peewee):
class User(peewee.Model):
email = peewee.CharField(unique=True, index=True)
hashed_password = peewee.CharField()
is_active = peewee.BooleanField(default=True)
class Meta:
database = db
class Item(peewee.Model):
title = peewee.CharField(index=True)
description = peewee.CharField(index=True)
owner = peewee.ForeignKeyField(User, backref="items")
class Meta:
database = db
And those basemodels (fastapi):
class PeeweeGetterDict(GetterDict):
def get(self, key: Any, default: Any = None):
res = getattr(self._obj, key, default)
if isinstance(res, peewee.ModelSelect):
return list(res)
return res
class ItemBase(BaseModel):
title: str
description: Optional[str] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
class Config:
orm_mode = True
getter_dict = PeeweeGetterDict
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
class Config:
orm_mode = True
getter_dict = PeeweeGetterDict
I have this to call the api:
database.db.connect()
database.db.create_tables([User, Item])
database.db.close()
app = FastAPI()
def get_db(db_state=Depends(reset_db_state)):
try:
database.db.connect()
yield
finally:
if not database.db.is_closed():
database.db.close()
#app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)])
def read_users():
return list(models.User.select()
This is the base. Now begins my problem / question:
If I call this request (GET "/users/"), I get the following JSON as a result (the data is imaginary its only about the structure)
[
{
"email": "123#test.com"
"id": 1
"is_active": 1
"items": [
{
"title": "item1"
"description": "placeholder"
"id": "1"
"owner_id": "1"
}
]
}
]
This is how it is supposed to be BUT i don't want it exactly like this. I want that I only get the user data, without its items.
So... my question:
How can I get the user data without loading the data of the items?

There is a simple way to do that, response_model_exclude is exactly what you are looking for.
#app.get("/users/", response_model=List[schemas.User], response_model_exclude={"items"}, dependencies=[Depends(get_db)])
def read_users():
return list(models.User.select()
Ref: FastAPI Response Model

Related

Fastapi how to return one field from nested model

I have a user model which is linked to a status model. How do I return only one field from the status model?
what i get now:
"status": {
"name": "figmer",
"id": "4"
},
what I want
"status": "figmer"
My models
class UserBase(BaseModel):
status_id: int = Field(default=1, title="status id")
status: Union[PermissionRetrieve, str] = Field(default=..., title="status")
class Config:
orm_mode = True
class PermissionBase(BaseModel):
name: str
class Config:
orm_mode = True
class PermissionRetrieve(PermissionBase):
id: str
JSON serialiation can be customized via json_encoders:
By default, models are serialised as dictionaries. If you want to serialise them differently, you can add models_as_dict=False when calling json() method and add the classes of the model in json_encoders
from pydantic import BaseModel, Field
class PermissionBase(BaseModel):
name: str
class PermissionRetrieve(PermissionBase):
id: str
class UserBase(BaseModel):
status_id: int = Field(default=1, title="status id")
status: PermissionRetrieve = Field(title="status")
class Config:
json_encoders = {
PermissionRetrieve: lambda v: v.name,
}
user = UserBase(status=PermissionRetrieve(name="figmer", id="4"))
print(user.json(models_as_dict=False)) # prints {"status_id": 1, "status": "figmer"}

Getting nested (joined) tables to display in the OpenAPI interface provided by FastAPI and SQLModel

I'm having trouble understanding how to display the children data in a one-to-many relationship using FastAPI and SQLModel. I'm using Python 3.10.3, FastAPI version 0.78.0 and SQLModel version 0.0.6. Here's a simplified version of the parent/child database models:
from datetime import datetime
from email.policy import default
from sqlalchemy import UniqueConstraint
from sqlmodel import Field, SQLModel, Relationship
class CustomerBase(SQLModel):
__table_args__ = (UniqueConstraint("email"),)
first_name: str
last_name: str
email: str
active: bool | None = True
class Customer(CustomerBase, table=True):
id: int | None =Field(primary_key=True, default=None)
class CustomerCreate(CustomerBase):
pass
class CustomerRead(CustomerBase):
id: int
class CustomerReadWithCalls(CustomerRead):
calls: list["CallRead"] = []
class CallBase(SQLModel):
duration: int
cost_per_minute: int | None = None
customer_id: int | None = Field(default=None, foreign_key="customer.id")
created: datetime = Field(nullable=False, default=datetime.now().date())
class Call(CallBase, table=True):
id: int | None = Field(primary_key=True)
class CallCreate(CallBase):
pass
class CallRead(CallBase):
id: int
class CallReadWithCustomer(CallRead):
customer: CustomerRead | None
Here is the API Route:
from fastapi import APIRouter, HTTPException, Depends, Query
from rbi_app.crud.customer import (
get_customers,
get_customer,
)
from rbi_app.models import (
CustomerRead,
CustomerReadWithCalls,
)
from rbi_app.database import Session, get_session
router = APIRouter()
#router.get("/customers/", status_code=200, response_model=list[CustomerRead])
def read_customers(
email: str = "",
offset: int = 0,
limit: int = Query(default=100, lte=100),
db: Session = Depends(get_session)
):
return get_customers(db, email, offset=offset, limit=limit)
#router.get("/customers/{customer_id}", status_code=200, response_model=CustomerReadWithCalls)
def read_customer(id: int, db: Session = Depends(get_session)):
customer = get_customer(db, id)
if customer is None:
raise HTTPException(status_code=404, detail=f"Customer not found for {id=}")
return customer
And here are the queries to the database the API Route endpoints make:
from sqlmodel import select
from rbi_app.database import Session
from rbi_app.models import (
Customer,
CustomerCreate,
)
# from rbi_app.schemas.customer import CustomerCreate
def get_customer(db: Session, id: int):
return db.get(Customer, id)
def get_customers(db: Session, email: str = "", offset: int = 0, limit: int = 100):
if email:
return db.exec(select(Customer).where(Customer.email == email)).first()
return db.exec(select(Customer).offset(offset).limit(limit).order_by(Customer.id)).all()
When I navigate to a route to get all a customer my query runs and I get a customer, but there is no "calls" list attribute in the customer. The OpenAPI display shows a "calls" attribute, but it's empty.
What am I doing wrong?
The issue here seems to be that you did not define the relationship on the Customer model (or the Call module). Since you query the database with the Customer model and it has no calls attribute, none of that data is present in the object returned by your get_customer function.
Even though your route defines the CustomerReadWithCalls as a response model, upon calling it, the object of that class can only ever be instantiated with the data returned by your route handler function, which is your Customer instance in this case. Since that does not even have the calls attribute (let alone the data), the CustomerReadWithCalls object is essentially created with the default value that you defined for the calls field -- the empty list.
Adding
calls: list["Call"] = Relationship(back_populates="customer")
to your Customer model should be enough.
(But as a side note, for me the route documentation only works properly, when I explicitly update the references on the CustomerReadWithCalls model after the CallRead definition.)
Here is a full working example.
models.py
from datetime import datetime
from sqlalchemy import UniqueConstraint
from sqlmodel import Field, Relationship, SQLModel
class CustomerBase(SQLModel):
__table_args__ = (UniqueConstraint("email"),)
first_name: str
last_name: str
email: str
active: bool | None = True
class Customer(CustomerBase, table=True):
id: int | None = Field(primary_key=True, default=None)
calls: list["Call"] = Relationship(back_populates="customer")
class CustomerCreate(CustomerBase):
pass
class CustomerRead(CustomerBase):
id: int
class CustomerReadWithCalls(CustomerRead):
calls: list["CallRead"] = []
class CallBase(SQLModel):
duration: int
cost_per_minute: int | None = None
customer_id: int | None = Field(default=None, foreign_key="customer.id")
created: datetime = Field(nullable=False, default=datetime.now().date())
class Call(CallBase, table=True):
id: int | None = Field(primary_key=True, default=None)
customer: Customer | None = Relationship(back_populates="calls")
class CallCreate(CallBase):
pass
class CallRead(CallBase):
id: int
# After the definition of `CallRead`, update the forward reference to it:
CustomerReadWithCalls.update_forward_refs()
class CallReadWithCustomer(CallRead):
customer: CustomerRead | None
routes.py
from fastapi import FastAPI, HTTPException, Depends
from sqlmodel import Session, SQLModel, create_engine
from .models import CustomerReadWithCalls, Customer, Call
api = FastAPI()
sqlite_file_name = 'database.db'
sqlite_url = f'sqlite:///{sqlite_file_name}'
engine = create_engine(sqlite_url, echo=True)
#api.on_event('startup')
def initialize_db():
SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
# For testing:
with Session(engine) as session:
customer = Customer(first_name="Foo", last_name="Bar", email="foo#bar.com")
call1 = Call(duration=123)
call2 = Call(duration=456)
customer.calls.extend([call1, call2])
session.add(customer)
session.commit()
def get_session() -> Session:
session = Session(engine)
try:
yield session
finally:
session.close()
def get_customer(db: Session, id: int):
return db.get(Customer, id)
#api.get("/customers/{customer_id}", status_code=200, response_model=CustomerReadWithCalls)
def read_customer(customer_id: int, db: Session = Depends(get_session)):
customer = get_customer(db, customer_id)
if customer is None:
raise HTTPException(status_code=404, detail=f"Customer not found for {customer_id=}")
return customer
Starting the API server and sending GET to http://127.0.0.1:8000/customers/1 gives me
{
"first_name": "Foo",
"last_name": "Bar",
"email": "foo#bar.com",
"active": true,
"id": 1,
"calls": [
{
"duration": 123,
"cost_per_minute": null,
"customer_id": 1,
"created": "2022-08-16T00:00:00",
"id": 1
},
{
"duration": 456,
"cost_per_minute": null,
"customer_id": 1,
"created": "2022-08-16T00:00:00",
"id": 2
}
]
}
Hope this helps.

PynamoDB same model with multipe databases

The way PynamoDB is implemented is that it looks to a specific single DynamoDB table:
class UserModel(Model):
class Meta:
# Specific table.
table_name = 'dynamodb-user'
region = 'us-west-1'
The way my infrastructure works is that it has as many dynamodb tables as I have clients, so a single Lambda function has to deal with any amount of separate tables that are identical in structure e.g. represent "UserModel". I can't specify a concrete one.
How would I make this model definition dynamic?
Thanks!
Possible solution:
def create_user_model(table_name: str, region: str):
return type("UserModel", (Model,), {
"key" : UnicodeAttribute(hash_key=True),
"range_key" : UnicodeAttribute(range_key=True),
# Place for other keys
"Meta": type("Meta", (object,), {
"table_name": table_name,
"region": region,
"host": None,
"billing_mode": 'PAY_PER_REQUEST',
})
})
UserModel_dev = create_user_model("user_model_dev", "us-west-1")
UserModel_prod = create_user_model("user_model_prod", "us-west-1")
Update:
A cleaner version:
class UserModel(Model):
key = UnicodeAttribute(hash_key=True)
range_key = UnicodeAttribute(range_key=True)
#staticmethod
def create(table_name: str, region: str):
return type("UserModelDynamic", (UserModel,), {
"Meta": type("Meta", (object,), {
"table_name": table_name,
"region": region,
"host": None,
"billing_mode": 'PAY_PER_REQUEST',
})
})
Open-sourced a solution that is tested and works.
https://github.com/Biomapas/B.DynamoDbCommon/blob/master/b_dynamodb_common/models/model_type_factory.py
Read README.md for more details.
Code:
from typing import TypeVar, Generic, Type
from pynamodb.models import Model
T = TypeVar('T')
class ModelTypeFactory(Generic[T]):
def __init__(self, model_type: Type[T]):
self.__model_type = model_type
# Ensure that given generic belongs to pynamodb.Model class.
if not issubclass(model_type, Model):
raise TypeError('Given model type must inherit from pynamodb.Model class!')
def create(self, custom_table_name: str, custom_region: str) -> Type[T]:
parent_class = self.__model_type
class InnerModel(parent_class):
class Meta:
table_name = custom_table_name
region = custom_region
return InnerModel

pydantic custom data type for phone number : value_error.missing

mysql schema for user
user_id binary(16)
first_name varchar(32)
last_name varchar(32)
email varchar(255)
phone varchar(32)
role enum('member','admin','operator')
created_on datetime
updated_on datetime
id varchar(32)
my pydantic model is
class UserPatchEntity(BaseModel):
user_id: Optional[UUID]
first_name: Optional[str] = Field(min_length=1, max_length=32)
last_name: Optional[str] = Field(min_length=1, max_length=32)
email: Optional[EmailStr]
phone: Optional[Phone]---------------> HERE
role: Optional[RoleType]
i want to create a custom datatype for phone number...so i can use it at multiple places....i am trying like below
class Phone(BaseModel):
phone: str
#validator('phone')
def phone_validation(cls, v):
phone = v.phone.get("phone")
logger.debug(f"phone in 2 validator:{v}")
regex = r"^(\+)[1-9][0-9\-\(\)\.]{9,15}$"
if v and not re.search(regex, v, re.I):
raise ValueError("Phone Number Invalid.")
return v
class Config:
orm_mode = True
use_enum_values = True
via postman, i am sending
{
"phone": "+917777777777"
}
i see 400 error in postman
[
{
"loc": [
"phone",
"phone"
],
"msg": "field required",
"type": "value_error.missing"
}
]
Any inputs on what i am doing wrong above
This doesn't work because you are passing in:
{
"phone": "+917777777777"
}
while the model has the structure UserPatchEntity.Phone.phone.
In order to make this work, I see two options:
1. Adapt the JSON Body (and fix the validator)
Fixing the validator:
class Phone(BaseModel):
phone: str
#validator("phone")
def phone_validation(cls, v):
# phone = v.phone.get("phone") # <-- This line needs to be removed.
logger.debug(f"phone in 2 validator:{v}")
regex = r"^(\+)[1-9][0-9\-\(\)\.]{9,15}$"
if v and not re.search(regex, v, re.I):
raise ValueError("Phone Number Invalid.")
return v
class Config:
orm_mode = True
use_enum_values = True
Send this instead:
{
"phone": {
"phone": "+917777777777"
}
}
It should work now.
2. Move the validator to the Parent Model
I would assume that the following is more in line what you want:
class UserPatchEntity(BaseModel):
user_id: Optional[UUID]
first_name: Optional[str] = Field(min_length=1, max_length=32)
last_name: Optional[str] = Field(min_length=1, max_length=32)
email: Optional[EmailStr]
phone: Optional[str] # <-- This is a str now, everything else stays the same
role: Optional[RoleType]
#validator("phone")
def phone_validation(cls, v):
logger.debug(f"phone in 2 validator:{v}")
regex = r"^(\+)[1-9][0-9\-\(\)\.]{9,15}$"
if v and not re.search(regex, v, re.I):
raise ValueError("Phone Number Invalid.")
return v
class Config:
orm_mode = True
use_enum_values = True
With that you can pass in phone in the body as you would expect:
{
"phone": "+917777777777"
}
Assuming you're using FastAPI, in both cases, you'd get the following error if the phone number is too short, too long, or contains invalid characters:
{
"detail": [
{
"loc": [
"body",
"phone"
],
"msg": "Phone Number Invalid.",
"type": "value_error"
}
]
}
NOTES/HINTS:
Pydantic has a constr (Constrained String) type, which allows you to define the minimum and maximum length, and a few other things. You could use the following instead of str for the phone field, e.g.:
class UserPatchEntity(BaseModel):
...
phone: Optional[
constr(
strip_whitespace=True,
min_length=9,
max_length=15,
)
]
It also allows you to directly specify a regex string, e.g.:
class UserPatchEntity(BaseModel):
...
phone: Optional[
constr(
strip_whitespace=True,
regex=r"^(\+)[1-9][0-9\-\(\)\.]{9,15}$",
)
]
Which would save you writing the explicit validator.
If you continue using regular expressions "directly", I would suggest to compile() them to make them a bit faster.
Assuming you are using FastAPI, you can navigate to http://localhost:8000/docs, when the app is running, where you'll find a full OpenAPI interface. You can send payloads directly from there and it also shows you the signature for each endpoint. In the first case, if you defined an endpoint like this:
#router.post("/user/")
async def post_user(user_patch_entity: UserPatchEntity):
return user_patch_entity.phone
it would show the documentation like that:

Best way to flatten and remap ORM to Pydantic Model

I am using Pydantic with FastApi to output ORM data into JSON. I would like to flatten and remap the ORM model to eliminate an unnecessary level in the JSON.
Here's a simplified example to illustrate the problem.
original output: {"id": 1, "billing":
[
{"id": 1, "order_id": 1, "first_name": "foo"},
{"id": 2, "order_id": 1, "first_name": "bar"}
]
}
desired output: {"id": 1, "name": ["foo", "bar"]}
How to map values from nested dict to Pydantic Model? provides a solution that works for dictionaries by using the init function in the Pydantic model class. This example shows how that works with dictionaries:
from pydantic import BaseModel
# The following approach works with a dictionary as the input
order_dict = {"id": 1, "billing": {"first_name": "foo"}}
# desired output: {"id": 1, "name": "foo"}
class Order_Model_For_Dict(BaseModel):
id: int
name: str = None
class Config:
orm_mode = True
def __init__(self, **kwargs):
print(
"kwargs for dictionary:", kwargs
) # kwargs for dictionary: {'id': 1, 'billing': {'first_name': 'foo'}}
kwargs["name"] = kwargs["billing"]["first_name"]
super().__init__(**kwargs)
print(Order_Model_For_Dict.parse_obj(order_dict)) # id=1 name='foo'
(This script is complete, it should run "as is")
However, when working with ORM objects, this approach does not work. It appears that the init function is not called. Here's an example which will not provide the desired output.
from pydantic import BaseModel, root_validator
from typing import List
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from pydantic.utils import GetterDict
class BillingOrm(Base):
__tablename__ = "billing"
id = Column(Integer, primary_key=True, nullable=False)
order_id = Column(ForeignKey("orders.id", ondelete="CASCADE"), nullable=False)
first_name = Column(String(20))
class OrderOrm(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, nullable=False)
billing = relationship("BillingOrm")
class Billing(BaseModel):
id: int
order_id: int
first_name: str
class Config:
orm_mode = True
class Order(BaseModel):
id: int
name: List[str] = None
# billing: List[Billing] # uncomment to verify the relationship is working
class Config:
orm_mode = True
def __init__(self, **kwargs):
# This __init__ function does not run when using from_orm to parse ORM object
print("kwargs for orm:", kwargs)
kwargs["name"] = kwargs["billing"]["first_name"]
super().__init__(**kwargs)
billing_orm_1 = BillingOrm(id=1, order_id=1, first_name="foo")
billing_orm_2 = BillingOrm(id=2, order_id=1, first_name="bar")
order_orm = OrderOrm(id=1)
order_orm.billing.append(billing_orm_1)
order_orm.billing.append(billing_orm_2)
order_model = Order.from_orm(order_orm)
# Output returns 'None' for name instead of ['foo','bar']
print(order_model) # id=1 name=None
(This script is complete, it should run "as is")
The output returns name=None instead of the desired list of names.
In the above example, I am using Order.from_orm to create the Pydantic model. This approach seems to be the same that is used by FastApi when specifying a response model. The desired solution should support use in the FastApi response model as shown in this example:
#router.get("/orders", response_model=List[schemas.Order])
async def list_orders(db: Session = Depends(get_db)):
return get_orders(db)
Update:
Regarding MatsLindh comment to try validators, I replaced the init function with a root validator, however, I'm unable to mutate the return values to include a new attribute. I suspect this issue is because it is a ORM object and not a true dictionary. The following code will extract the names and print them in the desired list. However, I can't see how to include this updated result in the model response:
#root_validator(pre=True)
def flatten(cls, values):
if isinstance(values, GetterDict):
names = [
billing_entry.first_name for billing_entry in values.get("billing")
]
print(names)
# values["name"] = names # error: 'GetterDict' object does not support item assignment
return values
I also found a couple other discussions on this problem that led me to try this approach:
https://github.com/samuelcolvin/pydantic/issues/717
https://gitmemory.com/issue/samuelcolvin/pydantic/821/744047672
What if you override the from_orm class method?
class Order(BaseModel):
id: int
name: List[str] = None
billing: List[Billing]
class Config:
orm_mode = True
#classmethod
def from_orm(cls, obj: Any) -> 'Order':
# `obj` is the orm model instance
if hasattr(obj, 'billing'):
obj.name = obj.billing.first_name
return super().from_orm(obj)
I really missed the handy Django REST Framework serializers while working with the FastAPI + Pydantic stack... So I wrangled with GetterDict to allow defining field getter function in the Pydantic model like this:
class User(FromORM):
fullname: str
class Config(FromORM.Config):
getter_dict = FieldGetter.bind(lambda: User)
#staticmethod
def get_fullname(obj: User) -> str:
return f'{obj.firstname} {obj.lastname}'
where the magic part FieldGetter is implemented as
from typing import Any, Callable, Optional, Type
from types import new_class
from pydantic import BaseModel
from pydantic.utils import GetterDict
class FieldGetter(GetterDict):
model_class_forward_ref: Optional[Callable] = None
model_class: Optional[Type[BaseModel]] = None
def __new__(cls, *args, **kwargs):
inst = super().__new__(cls)
if cls.model_class_forward_ref:
inst.model_class = cls.model_class_forward_ref()
return inst
#classmethod
def bind(cls, model_class_forward_ref: Callable):
sub_class = new_class(f'{cls.__name__}FieldGetter', (cls,))
sub_class.model_class_forward_ref = model_class_forward_ref
return sub_class
def get(self, key: str, default):
if hasattr(self._obj, key):
return super().get(key, default)
getter_fun_name = f'get_{key}'
if not (getter := getattr(self.model_class, getter_fun_name, None)):
raise AttributeError(f'no field getter function found for {key}')
return getter(self._obj)
class FromORM(BaseModel):
class Config:
orm_mode = True
getter_dict = FieldGetter

Categories

Resources