How to query a nested attribute in SQLModel? - python

I have these two tables named User and UserRole.
class UserRoleType(str, enum.Enum):
admin = 'admin'
client = 'client'
class UserRole(SQLModel, table=True):
__tablename__ = 'user_role'
id: int | None = Field(default=None, primary_key=True)
type: UserRoleType = Field(
default=UserRoleType.client,
sa_column=Column(Enum(UserRoleType)),
)
write_access: bool = Field(default=False)
read_access: bool = Field(default=False)
users: List['User'] = Relationship(back_populates='user_role')
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(..., index=True)
user_role_id: int = Field(..., foreign_key='user_role.id')
user_role: 'UserRole' = Relationship(back_populates='users')
I can easily insert them into the DB with:
async with get_session() as session:
role = UserRole(description=UserRoleType.client)
session.add(role)
await session.commit()
user = User( username='test', user_role_id=role.id)
session.add(user)
await session.commit()
await session.refresh(user)
And access the committed data with:
results = await session.execute(select(User).where(User.id == 1)).one()
Output:
(User(user_role_id=1, username='test', id=1),)
Notice that there's an user_role_id, but where's the user_role object?
In fact, if I try to access it, it raises:
*** AttributeError: Could not locate column in row for column 'user_role'
I also tried to pass the role instead of the user_role_id at the insertion of the User:
user = User( username='test', user_role=role)
But I got:
sqlalchemy.exc.InterfaceError: (sqlite3.InterfaceError) Error binding parameter 2 - probably unsupported type.

A few things first.
You did not include your import statements, so I will have to guess a few things.
You probably want the User.user_role_id and User.user_role fields to be "pydantically" optional. This allows you to create user instances without passing the role to the constructor, giving you the option to do so after initialization or for example by appending User objects to the UserRole.users list instead. To enforce that a user must have a role on the database level, you simply define nullable=False on the User.user_role_id field. That way, if you try to commit to the DB without having defined a user role for a user in any of the possible ways, you will get an error.
In your database insertion code you write role = UserRole(description=UserRoleType.client). I assume the description is from older code and you meant to write role = UserRole(type=UserRoleType.client).
You probably want your UserRole.type to be not nullable on the database side. You can do so by passing nullable=False to the Column constructor (not the Field constructor).
I will simplify a little by using blocking code (non-async) and a SQLite database.
This should work:
from enum import Enum as EnumPy
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Enum as EnumSQL
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class UserRoleType(str, EnumPy):
admin = 'admin'
client = 'client'
class UserRole(SQLModel, table=True):
__tablename__ = 'user_role'
id: int | None = Field(default=None, primary_key=True)
type: UserRoleType = Field(default=UserRoleType.client, sa_column=Column(EnumSQL(UserRoleType), nullable=False))
write_access: bool = Field(default=False)
read_access: bool = Field(default=False)
users: list['User'] = Relationship(back_populates='user_role')
class User(SQLModel, table=True):
__tablename__ = 'user'
id: int | None = Field(default=None, primary_key=True)
username: str = Field(..., index=True)
user_role_id: int | None = Field(foreign_key='user_role.id', default=None, nullable=False)
user_role: UserRole | None = Relationship(back_populates='users')
def test() -> None:
# Initialize database & session:
sqlite_file_name = 'user_role.db'
sqlite_url = f'sqlite:///{sqlite_file_name}'
engine = create_engine(sqlite_url)
SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
session = Session(engine)
# Create the test objects:
role = UserRole(type=UserRoleType.client)
user = User(username='test', user_role=role)
session.add(user)
session.commit()
session.refresh(user)
# Do some checks:
assert isinstance(user.user_role.type, EnumPy)
assert user.user_role_id == role.id and isinstance(role.id, int)
assert role.users == [user]
if __name__ == '__main__':
test()
PS: I know the question was posted a while ago, but maybe this still helps or helps someone else.

Related

Playhouse's (peewee extenstion) signals does not work correctly

I have two models: User and UserSettings because I decided to divide user related properties and privacy settings.
I am using PeeweeORM for building models and creating tables. Here is a short part of my code:
class User(BaseModel):
id = UUIDField(primary_key=True, default=uuid4)
tg_id = BigIntegerField(unique=True, null=True)
class UserSettings(BaseModel):
user: User = ForeignKeyField(User, backref='settings', unique=True)
show_location: bool = BooleanField(default=True)
As far as I know peewee itself has no built-in OneToOne relation support and I have decided to use playhouse's django-like signals described in peewee docs so that there is one record in UserSettings table for each one User. As it was defined in docs, I have inherited my BaseModel class from playhouse.signal Model class to make signals working. Here is a signal itself:
#post_save(sender=User)
def on_user_created(model_class: User, instance: User, created: bool):
print("works1") # Signal is working correctly. I see this output in console
if created: # DOES NOT WORK HERE! I AM GETTING False value on created
print('works2')
us = UserSettings()
us.user = instance
us.save(force_insert=True)
So this is the way I am creating new users:
def create_or_update_user_tg(tg_id: int, name: str, age: int, city: str,
gender: Gender, search_gender: SearchGender,
profile_description: str = None, location: Location = None,
medias: typing.List[tuple[str]] = None) -> \
typing.Union[User, None]:
u, is_creating = User.get_or_none(tg_id=tg_id), False
if not u:
u, is_creating = User(), True
u.tg_id = tg_id
u.name = name
u.age = age
u.city = city
u.gender = gender.value
u.search_gender = search_gender.value
u.profile_description = profile_description
if location:
u.longitude = location.longitude
u.latitude = location.latitude
u.save(force_insert=is_creating)
upload_user_medias(u.tg_id, medias, delete_existing=True)
return u
Thanks for responses guys! Waiting for your advices.
It seems to be working fine for me. Here's a stripped-down, simplified example:
from peewee import *
from playhouse.signals import Model, post_save
from uuid import uuid4
db = SqliteDatabase(':memory:')
class User(Model):
id = UUIDField(default=uuid4)
username = TextField()
class Meta:
database = db
db.create_tables([User])
#post_save(sender=User)
def on_save(model_class, instance, created=None):
print(instance.username, created)
u = User()
u.username='foo'
u.save(force_insert=True)
u.save()
The output, as expected:
foo True
foo False

SQLModel error: "AttributeError: 'Team' object has no attribute 'heroes'"

I'm trying to get my hands on Tiangolo's SQLModel and I tried the example code in this doc here : https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/read-relationships/
The code is the following :
from typing import List, Optional
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team")
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
engine = create_engine(sqlite_url, echo=True)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def create_heroes():
with Session(engine) as session:
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar")
hero_deadpond = Hero(
name="Deadpond", secret_name="Dive Wilson", team=team_z_force
)
hero_rusty_man = Hero(
name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers
)
hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_deadpond)
session.add(hero_rusty_man)
session.add(hero_spider_boy)
session.commit()
session.refresh(hero_deadpond)
session.refresh(hero_rusty_man)
session.refresh(hero_spider_boy)
print("Created hero:", hero_deadpond)
print("Created hero:", hero_rusty_man)
print("Created hero:", hero_spider_boy)
hero_spider_boy.team = team_preventers
session.add(hero_spider_boy)
session.commit()
session.refresh(hero_spider_boy)
print("Updated hero:", hero_spider_boy)
hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35)
hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E")
team_wakaland = Team(
name="Wakaland",
headquarters="Wakaland Capital City",
heroes=[hero_black_lion, hero_sure_e],
)
session.add(team_wakaland)
session.commit()
session.refresh(team_wakaland)
print("Team Wakaland:", team_wakaland)
hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32)
hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36)
hero_cap = Hero(
name="Captain North America", secret_name="Esteban Rogelios", age=93
)
team_preventers.heroes.append(hero_tarantula)
team_preventers.heroes.append(hero_dr_weird)
team_preventers.heroes.append(hero_cap)
session.add(team_preventers)
session.commit()
session.refresh(hero_tarantula)
session.refresh(hero_dr_weird)
session.refresh(hero_cap)
print("Preventers new hero:", hero_tarantula)
print("Preventers new hero:", hero_dr_weird)
print("Preventers new hero:", hero_cap)
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Spider-Boy")
result = session.exec(statement)
hero_spider_boy = result.one()
statement = select(Team).where(Team.id == hero_spider_boy.id)
result = session.exec(statement)
team = result.first()
print("Spider-Boy's team:", team)
print("Spider-Boy's team again:", hero_spider_boy.team)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
However, when I try to run it, I've got the following error :
File "example.py", line 83, in create_heroes
team_preventers.heroes.append(hero_tarantula)
AttributeError: 'Team' object has no attribute 'heroes'
I can't understand why this error occurs since the heroes attribute seems to be well defined in the Team class. And I'm pretty sure an example code in a tutorial couldn't be wrong. Am I missing something? Thanks!
Note : Using last 0.0.6 sqlmodel version on Python 3.9
SQLModel has an outstanding issue with SQLAlchemy 1.4.36+
https://github.com/tiangolo/sqlmodel/issues/315
For now,
pip install sqlalchemy==1.4.35

pydantic.error_wrappers.ValidationError: 11 validation errors for For Trip type=value_error.missing

Im getting this error with my pydantic schema, but oddly it is generating the object correctly, and sending it to the SQLAlchemy models, then it suddenly throws error for all elements in the model.
response -> id
field required (type=value_error.missing)
response -> date
field required (type=value_error.missing)
response -> time
field required (type=value_error.missing)
response -> price
field required (type=value_error.missing)
response -> distance
field required (type=value_error.missing)
response -> origin_id
field required (type=value_error.missing)
response -> destination_id
field required (type=value_error.missing)
response -> driver_id
field required (type=value_error.missing)
response -> passenger_id
field required (type=value_error.missing)
response -> vehicle_id
field required (type=value_error.missing)
response -> status
field required (type=value_error.missing)
i must say that all the fields should have values. And the error trace do not references any part of my code so i dont even know where to debug. Im a noob in SQLAlchemy/pydantic
here are some parts of the code
class Trip(BaseModel):
id: int
date: str
time: str
price: float
distance: float
origin_id: int
destination_id: int
driver_id: int
passenger_id: int
vehicle_id: int
status: Status
class Config:
orm_mode = True
class TripDB(Base):
__tablename__ = 'trip'
__table_args__ = {'extend_existing': True}
id = Column(Integer, primary_key=True, index=True)
date = Column(DateTime, nullable=False)
time = Column(String(64), nullable=False)
price = Column(Float, nullable=False)
distance = Column(Float, nullable=False)
status = Column(String(64), nullable=False)
origin_id = Column(
Integer, ForeignKey('places.id'), nullable=False)
destination_id = Column(
Integer, ForeignKey('places.id'), nullable=False)
origin = relationship("PlaceDB", foreign_keys=[origin_id])
destination = relationship("PlaceDB", foreign_keys=[destination_id])
driver_id = Column(
Integer, ForeignKey('driver.id'), nullable=False)
vehicle_id = Column(
Integer, ForeignKey('vehicle.id'), nullable=False)
passenger_id = Column(
Integer, ForeignKey('passenger.id'), nullable=False)
def create_trip(trip: Trip, db: Session):
origin = db.query(models.PlaceDB).filter(models.PlaceDB.id == trip.origin_id).first()
destination = db.query(models.PlaceDB).filter(models.PlaceDB.id == trip.destination_id).first()
db_trip = TripDB(
id=(trip.id or None),
date=trip.date or None, time=trip.time or None, price=trip.price or None,
distance=trip.distance or None,
origin_id=trip.origin_id or None, destination_id=(trip.destination_id or None), status=trip.status or None,
driver_id=trip.driver_id or None, passenger_id=trip.passenger_id or None, vehicle_id=trip.vehicle_id or None, origin=origin, destination=destination)
try:
db.add(db_trip)
db.commit()
db.refresh(db_trip)
return db_trip
except:
return "Somethig went wrong"
It seems like a bug on the pydantic model, it happened to me as well, and i was not able to fix it, but indeed if you just skip the type check in the route it works fine
It seems like there is a conflict in your schema and create_trip function. Have you checked whether you are passing the correct param to your schema? You have defined most of the fields as not nullable, and you are passing None as an alternative value in db.add() command.
I had a similar problem with my code, and I figured that naming and type convention between schema and server.py. After matching the field names and type in both file, I resolved the error of the field required (type=value_error.missing).
# project/schema.py
from pydantic import BaseModel
# here I had made mistake by using target_URL in server.py
# variable name and type should be same in both schema and server
class URLBase(BaseModel):
target_url: str
class URL(URLBase):
is_active: bool
clicks: int
class Config:
orm_mode = True
class URLInfo(URL):
url: str
admin_url: str
# project/server.py
#app.post('/url', response_model=schema.URLInfo)
def create_short_url(url: schema.URLBase, db: Session = Depends(get_db)):
if not validators.url(url.target_url):
raise bad_request_message(message="Your provided URL is not valid!")
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
key = "".join(secrets.choice(chars) for _ in range(5))
secret_key = "".join(secrets.choice(chars) for _ in range(8))
db_url = models.URL(target_url=url.target_url, key=key, secret_key=secret_key)
db.add(db_url)
db.commit()
db.refresh(db_url)
db_url.url = key
db_url.admin_url = secret_key
return db_url
Please check the return statement, i had similar issue and got the issue reolved by correcting my return statement. I see return statement missing or wrong in create_trip function.

Add a custom filed to fastapi responce model (serializers)

I'm following this guide from the Fastapi documentation and I have a question what if want to add a custom field when I return an object from DB. In Django I can use serializers.
My case:
I want to save an image name into DB, but before that I need save an actual file in a static folder. When I call GET /items/1 I want to return not just an image name from DB, but full URL, so I need to execute some logic on every request in order to build the URL. The question is how can I achieve that? The only thing I can think of is to add an additional DTO layer that coverts input data to Pydantic classes, so it's gonna be:
DTO class -> Pydantic -> DB
Is there more fancy way of doing that?
Code example:
schemas.py
from typing import List, Literal, Optional
from enum import Enum, IntEnum
from pydantic import BaseModel, constr, validator
class Ingredient(BaseModel):
quantity: int
quantityUnit: QuantityUnitEnum
name: constr(max_length=50)
class RecipeBase(BaseModel):
id: int = None
title: constr(max_length=50)
# image_name: str
#validator('ingredients')
def ingredients_must_have_unique_name(cls, values):
names = []
for item in values:
names.append(item.name)
if len(names) > len(set(names)):
raise ValueError('must contain unique names')
return values
class RecipeCreate(RecipeBase):
pass
class Recipe(RecipeBase):
id: int
class Config:
orm_mode = True
model.py
class Recipe(Base):
__tablename__ = "recipes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(50), index=True, nullable=False)
image_name = Column(String(50), index=True, nullable=False)
main.py
#app.post("/recipes", response_model=schemas.Recipe)
def create_recipe(recipe: schemas.RecipeCreate, db: Session = Depends(get_db)):
return repository.create_recipe(db=db, recipe=recipe)
#app.get("/recipes/{recipe_id}", response_model=schemas.Recipe)
def get_recipe(recipe_id, db: Session = Depends(get_db)):
return repository.get_recipe(db, recipe_id=recipe_id)
repository.py
def get_recipe(db: Session, recipe_id: int):
return db.query(models.Recipe).get(recipe_id)

FastApi get request shows validation error

I'm getting this error when I try to get some data from my postgre db and using fastapi.
I don't know why it happens...but here is my code, thank you for your help.
Route
#router.get("/fuentes", response_model=FuenteSerializer.MFuente) # <--- WHEN I REMOVE RESPONSE_MODEL WORKS AND RETURNS A JSON DATA DIRECTLY FROM MODEL I GUESS
async def read_fuentes(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
fuentes = FuenteSerializer.get_fuente(db, skip=skip, limit=limit)
return fuentes
sqlalchemy model
class MFuente(Base):
__tablename__ = 'M_fuentes'
idfuentes = Column(Integer, primary_key=True)
idproductos = Column(ForeignKey('M_productos.idproductos', ondelete='RESTRICT', onupdate='RESTRICT'), index=True)
autoapp = Column(CHAR(2))
rutFabricante = Column(String(12))
elemento = Column(String(100))
estado = Column(Integer)
stype = Column(Integer)
aql = Column(String(5))
equiv = Column(String(5))
division = Column(String(100))
nu = Column(Integer)
filexcel = Column(String(100))
M_producto = relationship('MProducto')
Serializer / schema
class MFuente(BaseModel):
idfuentes: int
autoapp: str
fecregistro: datetime.date
rutFabricante: str
elemento: str
estado: int
stype: int
aql: str
equiv: str
division: str
fileexel: str
productos: List[MProducto]
class Config:
orm_mode = True
def get_fuente(db: Session, skip: int = 0, limit: int = 100):
return db.query(Fuente).offset(skip).limit(limit).all()
For a little debugging i created the same little prototype, and i found some possible answers.
First of all here is the app that i created:
class MFuente(BaseModel):
name: str
value: int
#app.get("/items/{name}", response_model=MFuente)
async def get_item(name: str):
query = fuente_db.select().where(fuente_db.c.name == name)
return await database.fetch_all(query)
So with this schema i get the same error
response -> name
field required (type=value_error.missing)
response -> value
field required (type=value_error.missing)
So i debugged a little bit more i found it's all about response_model, so i came up with this:
from typing import List
...
#app.get("/items/{name}", response_model=List[MFuente])
Everything started working:
INFO: 127.0.0.1:52872 - "GET /items/masteryoda HTTP/1.1" 200 OK
So in your case fix will be:
#router.get("/fuentes", response_model=List[FuenteSerializer.MFuente])
^^^^

Categories

Resources