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:

enter image description here

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?

Asked By: Mat

||

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
    }
}
Answered By: Daniil Fajnberg