How to return the average value using fastapi & pydantic
Question:
I am new to fasts-api and python as I am a mobile developer.
At the moment I managed to get this response from my API (please look at averageLevel
which is an array at the moment):
[
{
"user_id": 139,
"event_date": "2023-03-20T12:18:17",
"public": 1,
"waitlist": 1,
"maxParticipant": 9,
"event_type": "1 vs 1",
"event_field": "Acrylic",
"event_id": 173,
"created": "2023-03-20T13:18:05",
"organizer": {
"user_id": 139,
"email": "[email protected]",
"name": "Daniele",
"lastName": "Proietti",
"city": "Rome",
"state": "Lazio",
"zipCode": " 32323",
"country": "Italy",
"averageLevel": [
{
"level": 70
}
]
}
}
]
The table that holds the player’s level values is the following:
my models.py:
class Event(Base):
__tablename__ = "event"
event_id = Column(_sql.Integer, primary_key=True, index=True, nullable=False)
user_id = Column(_sql.Integer, ForeignKey("users.user_id", ondelete="CASCADE"), primary_key=False, index=True, nullable=False)
sport_id = Column(_sql.Integer, ForeignKey("sportSupported.sport_id"), primary_key=False, index=True, nullable=False)
place_id = Column(_sql.Integer, ForeignKey("places.place_id"), primary_key=False, index=True, nullable=False)
created = Column(_sql.DateTime(), default=datetime.now(), nullable=True)
event_date = Column(_sql.DateTime(), nullable=False)
public = Column(_sql.Integer, default = 0)
waitlist = Column(_sql.Integer, default = 0)
event_type = Column(String(100), nullable=False)
event_field = Column(String(100), nullable=False)
description = Column(String(255), nullable=True)
maxParticipant = Column(_sql.Integer, nullable=False)
player_level = Column(_sql.Float, default= 0.0, nullable=False)
organizer = relationship("User")
participants = relationship("User", secondary=association_table)
venue = relationship("Place", back_populates="event")
class User(Base):
__tablename__ = "users"
user_id = Column(Integer, primary_key=True, index=True, nullable=False)
email = Column(String(100), nullable=False)
password = Column(String(255), nullable=False)
salt = Column(String(255),nullable=False)
name = Column(String(50),nullable=False)
lastName = Column(String(100), nullable=False)
dob = Column(DateTime(), nullable=True)
city = Column(String(50), nullable=True)
state = Column(String(40),nullable=True)
zipCode = Column(String(10), nullable=True)
country = Column(String(90), nullable=True)
telephoneNumber = Column(String(60))
joined = Column(DateTime(), default=datetime.now(), nullable=False)
facebook_id = Column(Integer)
verified = Column(Integer, default = 0, nullable=False)
testAccount = Column(Integer, default = 0, nullable=False)
averageLevel = relationship("PlayersLevel")
def __repr__(self):
return f"{self.user_id}"
class PlayersLevel(Base):
__tablename__ = "playersLevel"
prog_id = Column(Integer, primary_key=True, index=True, nullable=False)
user_id = Column(Integer,ForeignKey("users.user_id", ondelete="CASCADE"),primary_key=False, index=True, nullable=False)
event_id = Column(Integer, nullable=False)
date = Column(DateTime(), default=datetime.now(), nullable=False)
level = Column(Integer)
sport_id = Column(Integer)
def __repr__(self):
return f"{self.progr_id}"
and schema.py:
class User(BaseClass):
user_id: int
email: str
name: str
lastName: str
city: str
state: str
zipCode: str
country: str
averageLevel: list[PlayersLevel] = []
class PlayersLevel(BaseClass):
proper_id: int
user_id: int
event_id: int
date: datetime
level: int
sport_id: int
class EventRead(Event):
event_id: int
created: datetime
organizer: User
participants: list[User] = []
venue: Place
I query the database like this (all events):
def get_events(db: Session):
return db.query(models.Event).all()
However, what I would like to achieve is the average of all level rows for a specific user_id
(not an array of levels as it is now): something like this:
[
{
"user_id": 139,
"event_date": "2023-03-20T12:18:17",
"public": 1,
"waitlist": 1,
"maxParticipant": 9,
"event_type": "1 vs 1",
"event_field": "Acrylic",
"event_id": 173,
"created": "2023-03-20T13:18:05",
"organizer": {
"user_id": 139,
"email": "[email protected]",
"name": "Daniele",
"lastName": "Proietti",
"city": "Rome",
"state": "Lazio",
"zipCode": " 32323",
"country": "Italy",
"averageLevel": // not an array here
{
"level": 74 // if there are 3 rows for the user (70, 72, 80) I would like to get the average IE 74
}
}
}
]
how can I achieve this?
Answers:
You can define a custom root validator with pre=True
on the user model to check, if a list of levels was passed and calculate the average from those.
Here is a simplified example:
from collections.abc import Mapping
from typing import Any
from pydantic import BaseModel as PydanticBaseModel, root_validator
class BaseModel(PydanticBaseModel):
class Config:
orm_mode = True
class UserModel(BaseModel):
id: int
name: str
avg_level: float
@root_validator(pre=True)
def calc_from_levels(cls, values: Mapping[str, Any]) -> dict[str, Any]:
values = dict(values)
lvl_objects = values.get("levels")
if values.get("avg_level") is not None or not lvl_objects:
return values
values["avg_level"] = sum(
obj.level for obj in lvl_objects
) / len(lvl_objects)
return values
class EventModel(BaseModel):
id: int
organizer: UserModel
Note that this implementation assumes the relevant key in the provided data is named levels
. If you want to stick with your name averageLevel
(which I would not recommend because it is misleading) or name it something else on your ORM class, you need to adjust the lvl_objects = values.get("levels")
line in the validator method accordingly. That is where the list is extracted.
Also notice that this implementation still allows avg_level
to be set explicitly. If that key is present, the validator will not even attempt to look for the levels list. Likewise, if no levels list was provided (or it is empty), nothing else happens in this validator, which means that will likely cause a validation error further down the line because I defined avg_level
as required (no default).
Just some dumb data calasses to test:
from dataclasses import dataclass
@dataclass
class PlayerLevel:
level: int
@dataclass
class User:
id: int
name: str
levels: list[PlayerLevel]
@dataclass
class Event:
id: int
organizer: User
mock_db_event = Event(
id=123,
organizer=User(
id=1,
name="foo",
levels=[
PlayerLevel(70),
PlayerLevel(72),
PlayerLevel(80),
]
),
)
event = EventModel.from_orm(mock_db_event)
print(event.json(indent=4))
Output:
{
"id": 123,
"organizer": {
"id": 1,
"name": "foo",
"avg_level": 74.0
}
}
I am new to fasts-api and python as I am a mobile developer.
At the moment I managed to get this response from my API (please look at averageLevel
which is an array at the moment):
[
{
"user_id": 139,
"event_date": "2023-03-20T12:18:17",
"public": 1,
"waitlist": 1,
"maxParticipant": 9,
"event_type": "1 vs 1",
"event_field": "Acrylic",
"event_id": 173,
"created": "2023-03-20T13:18:05",
"organizer": {
"user_id": 139,
"email": "[email protected]",
"name": "Daniele",
"lastName": "Proietti",
"city": "Rome",
"state": "Lazio",
"zipCode": " 32323",
"country": "Italy",
"averageLevel": [
{
"level": 70
}
]
}
}
]
The table that holds the player’s level values is the following:
my models.py:
class Event(Base):
__tablename__ = "event"
event_id = Column(_sql.Integer, primary_key=True, index=True, nullable=False)
user_id = Column(_sql.Integer, ForeignKey("users.user_id", ondelete="CASCADE"), primary_key=False, index=True, nullable=False)
sport_id = Column(_sql.Integer, ForeignKey("sportSupported.sport_id"), primary_key=False, index=True, nullable=False)
place_id = Column(_sql.Integer, ForeignKey("places.place_id"), primary_key=False, index=True, nullable=False)
created = Column(_sql.DateTime(), default=datetime.now(), nullable=True)
event_date = Column(_sql.DateTime(), nullable=False)
public = Column(_sql.Integer, default = 0)
waitlist = Column(_sql.Integer, default = 0)
event_type = Column(String(100), nullable=False)
event_field = Column(String(100), nullable=False)
description = Column(String(255), nullable=True)
maxParticipant = Column(_sql.Integer, nullable=False)
player_level = Column(_sql.Float, default= 0.0, nullable=False)
organizer = relationship("User")
participants = relationship("User", secondary=association_table)
venue = relationship("Place", back_populates="event")
class User(Base):
__tablename__ = "users"
user_id = Column(Integer, primary_key=True, index=True, nullable=False)
email = Column(String(100), nullable=False)
password = Column(String(255), nullable=False)
salt = Column(String(255),nullable=False)
name = Column(String(50),nullable=False)
lastName = Column(String(100), nullable=False)
dob = Column(DateTime(), nullable=True)
city = Column(String(50), nullable=True)
state = Column(String(40),nullable=True)
zipCode = Column(String(10), nullable=True)
country = Column(String(90), nullable=True)
telephoneNumber = Column(String(60))
joined = Column(DateTime(), default=datetime.now(), nullable=False)
facebook_id = Column(Integer)
verified = Column(Integer, default = 0, nullable=False)
testAccount = Column(Integer, default = 0, nullable=False)
averageLevel = relationship("PlayersLevel")
def __repr__(self):
return f"{self.user_id}"
class PlayersLevel(Base):
__tablename__ = "playersLevel"
prog_id = Column(Integer, primary_key=True, index=True, nullable=False)
user_id = Column(Integer,ForeignKey("users.user_id", ondelete="CASCADE"),primary_key=False, index=True, nullable=False)
event_id = Column(Integer, nullable=False)
date = Column(DateTime(), default=datetime.now(), nullable=False)
level = Column(Integer)
sport_id = Column(Integer)
def __repr__(self):
return f"{self.progr_id}"
and schema.py:
class User(BaseClass):
user_id: int
email: str
name: str
lastName: str
city: str
state: str
zipCode: str
country: str
averageLevel: list[PlayersLevel] = []
class PlayersLevel(BaseClass):
proper_id: int
user_id: int
event_id: int
date: datetime
level: int
sport_id: int
class EventRead(Event):
event_id: int
created: datetime
organizer: User
participants: list[User] = []
venue: Place
I query the database like this (all events):
def get_events(db: Session):
return db.query(models.Event).all()
However, what I would like to achieve is the average of all level rows for a specific user_id
(not an array of levels as it is now): something like this:
[
{
"user_id": 139,
"event_date": "2023-03-20T12:18:17",
"public": 1,
"waitlist": 1,
"maxParticipant": 9,
"event_type": "1 vs 1",
"event_field": "Acrylic",
"event_id": 173,
"created": "2023-03-20T13:18:05",
"organizer": {
"user_id": 139,
"email": "[email protected]",
"name": "Daniele",
"lastName": "Proietti",
"city": "Rome",
"state": "Lazio",
"zipCode": " 32323",
"country": "Italy",
"averageLevel": // not an array here
{
"level": 74 // if there are 3 rows for the user (70, 72, 80) I would like to get the average IE 74
}
}
}
]
how can I achieve this?
You can define a custom root validator with pre=True
on the user model to check, if a list of levels was passed and calculate the average from those.
Here is a simplified example:
from collections.abc import Mapping
from typing import Any
from pydantic import BaseModel as PydanticBaseModel, root_validator
class BaseModel(PydanticBaseModel):
class Config:
orm_mode = True
class UserModel(BaseModel):
id: int
name: str
avg_level: float
@root_validator(pre=True)
def calc_from_levels(cls, values: Mapping[str, Any]) -> dict[str, Any]:
values = dict(values)
lvl_objects = values.get("levels")
if values.get("avg_level") is not None or not lvl_objects:
return values
values["avg_level"] = sum(
obj.level for obj in lvl_objects
) / len(lvl_objects)
return values
class EventModel(BaseModel):
id: int
organizer: UserModel
Note that this implementation assumes the relevant key in the provided data is named levels
. If you want to stick with your name averageLevel
(which I would not recommend because it is misleading) or name it something else on your ORM class, you need to adjust the lvl_objects = values.get("levels")
line in the validator method accordingly. That is where the list is extracted.
Also notice that this implementation still allows avg_level
to be set explicitly. If that key is present, the validator will not even attempt to look for the levels list. Likewise, if no levels list was provided (or it is empty), nothing else happens in this validator, which means that will likely cause a validation error further down the line because I defined avg_level
as required (no default).
Just some dumb data calasses to test:
from dataclasses import dataclass
@dataclass
class PlayerLevel:
level: int
@dataclass
class User:
id: int
name: str
levels: list[PlayerLevel]
@dataclass
class Event:
id: int
organizer: User
mock_db_event = Event(
id=123,
organizer=User(
id=1,
name="foo",
levels=[
PlayerLevel(70),
PlayerLevel(72),
PlayerLevel(80),
]
),
)
event = EventModel.from_orm(mock_db_event)
print(event.json(indent=4))
Output:
{
"id": 123,
"organizer": {
"id": 1,
"name": "foo",
"avg_level": 74.0
}
}