Skip to content

Commit

Permalink
api: upgrade python packages
Browse files Browse the repository at this point in the history
Enable support for `pydantic v2` along
with the latest `fastapi-pagination` package.
To enable the upgrade `fastapi` and `fastapi-users`
packages are also required to be upgraded.
Use `lifespan` functions for startup events as `on_event`
is deprecated in the latest `fastapi` version.

Signed-off-by: Jeny Sadadia <[email protected]>
  • Loading branch information
Jeny Sadadia committed Oct 24, 2024
1 parent 9bddc8f commit 0bd1ba6
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 36 deletions.
3 changes: 2 additions & 1 deletion api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

"""Module settings"""

from pydantic import BaseSettings, EmailStr
from pydantic import EmailStr
from pydantic_settings import BaseSettings


# pylint: disable=too-few-public-methods
Expand Down
2 changes: 1 addition & 1 deletion api/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ async def create(self, obj):
raise ValueError(f"Object cannot be created with id: {obj.id}")
delattr(obj, 'id')
col = self._get_collection(obj.__class__)
res = await col.insert_one(obj.dict(by_alias=True))
res = await col.insert_one(obj.model_dump(by_alias=True))
obj.id = res.inserted_id
return obj

Expand Down
17 changes: 11 additions & 6 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import re
from typing import List, Union, Optional
import threading
from contextlib import asynccontextmanager
from fastapi import (
Depends,
FastAPI,
Expand Down Expand Up @@ -53,6 +54,14 @@
)


@asynccontextmanager
async def lifespan(app: FastAPI): # pylint: disable=redefined-outer-name
"""Lifespan functions for startup and shutdown events"""
await pubsub_startup()
await create_indexes()
await initialize_beanie()
yield

# List of all the supported API versions. This is a placeholder until the API
# actually supports multiple versions with different sets of endpoints and
# models etc.
Expand Down Expand Up @@ -105,8 +114,7 @@ def all(self):

metrics = Metrics()


app = FastAPI()
app = FastAPI(lifespan=lifespan)
db = Database(service=(os.getenv('MONGO_SERVICE') or 'mongodb://db:27017'))
auth = Authentication(token_url="user/login")
pubsub = None # pylint: disable=invalid-name
Expand All @@ -119,20 +127,17 @@ def all(self):
user_manager = create_user_manager()


@app.on_event('startup')
async def pubsub_startup():
"""Startup event handler to create Pub/Sub object"""
global pubsub # pylint: disable=invalid-name
pubsub = await PubSub.create()


@app.on_event('startup')
async def create_indexes():
"""Startup event handler to create database indexes"""
await db.create_indexes()


@app.on_event('startup')
async def initialize_beanie():
"""Startup event handler to initialize Beanie"""
await db.initialize_beanie()
Expand Down Expand Up @@ -535,7 +540,7 @@ def serialize_paginated_data(model, data: list):
"""
serialized_data = []
for obj in data:
serialized_data.append(model(**obj).dict())
serialized_data.append(model(**obj).model_dump(mode='json'))
return serialized_data


Expand Down
84 changes: 66 additions & 18 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"""Server-side model definitions"""

from datetime import datetime
from typing import Optional, TypeVar
from typing import Optional, TypeVar, Dict, Any, List
from pydantic import (
BaseModel,
conlist,
Field,
model_serializer,
field_validator,
)
from typing_extensions import Annotated
from fastapi import Query
from fastapi_pagination import LimitOffsetPage, LimitOffsetParams
from fastapi_users.db import BeanieBaseUser
Expand All @@ -27,7 +29,7 @@
Document,
PydanticObjectId,
)
from bson import ObjectId
# from bson import ObjectId
from kernelci.api.models_base import DatabaseModel, ModelId


Expand Down Expand Up @@ -56,6 +58,7 @@ class SubscriptionStats(Subscription):
description='Timestamp of connection creation'
)
last_poll: Optional[datetime] = Field(
default=None,
description='Timestamp when connection last polled for data'
)

Expand All @@ -79,12 +82,20 @@ def get_indexes(cls):
class User(BeanieBaseUser, Document, # pylint: disable=too-many-ancestors
DatabaseModel):
"""API User model"""
username: Indexed(str, unique=True)
groups: conlist(UserGroup, unique_items=True) = Field(
username: Annotated[str, Indexed(unique=True)]
groups: List[UserGroup] = Field(
default=[],
description="A list of groups that user belongs to"
description="A list of groups that the user belongs to"
)

@field_validator('groups')
def validate_groups(cls, groups): # pylint: disable=no-self-argument
"""Unique group constraint"""
unique_names = {group.name for group in groups}
if len(unique_names) != len(groups):
raise ValueError("Groups must have unique names.")
return groups

class Settings(BeanieBaseUser.Settings):
"""Configurations"""
# MongoDB collection name for model
Expand All @@ -97,23 +108,66 @@ def get_indexes(cls):
cls.Index('email', {'unique': True}),
]

@model_serializer(when_used='json')
def serialize_model(self) -> Dict[str, Any]:
"""Serialize model by converting PyObjectId to string"""
values = self.__dict__.copy()
for field_name, value in values.items():
if isinstance(value, PydanticObjectId):
values[field_name] = str(value)
return values


class UserRead(schemas.BaseUser[PydanticObjectId], ModelId):
"""Schema for reading a user"""
username: Indexed(str, unique=True)
groups: conlist(UserGroup, unique_items=True)
username: Annotated[str, Indexed(unique=True)]
groups: List[UserGroup] = Field(default=[])

@field_validator('groups')
def validate_groups(cls, groups): # pylint: disable=no-self-argument
"""Unique group constraint"""
unique_names = {group.name for group in groups}
if len(unique_names) != len(groups):
raise ValueError("Groups must have unique names.")
return groups

@model_serializer(when_used='json')
def serialize_model(self) -> Dict[str, Any]:
"""Serialize model by converting PyObjectId to string"""
values = self.__dict__.copy()
for field_name, value in values.items():
if isinstance(value, PydanticObjectId):
values[field_name] = str(value)
return values


class UserCreate(schemas.BaseUserCreate):
"""Schema for creating a user"""
username: Indexed(str, unique=True)
groups: Optional[conlist(str, unique_items=True)]
username: Annotated[str, Indexed(unique=True)]
groups: List[str] = Field(default=[])

@field_validator('groups')
def validate_groups(cls, groups): # pylint: disable=no-self-argument
"""Unique group constraint"""
unique_names = set(groups)
if len(unique_names) != len(groups):
raise ValueError("Groups must have unique names.")
return groups


class UserUpdate(schemas.BaseUserUpdate):
"""Schema for updating a user"""
username: Optional[Indexed(str, unique=True)]
groups: Optional[conlist(str, unique_items=True)]
username: Annotated[Optional[str], Indexed(unique=True),
Field(default=None)]
groups: List[str] = Field(default=[])

@field_validator('groups')
def validate_groups(cls, groups): # pylint: disable=no-self-argument
"""Unique group constraint"""
unique_names = set(groups)
if len(unique_names) != len(groups):
raise ValueError("Groups must have unique names.")
return groups


# Pagination models
Expand All @@ -133,9 +187,3 @@ class PageModel(LimitOffsetPage[TypeVar("T")]):
This model is required to serialize paginated model data response"""

__params_type__ = CustomLimitOffsetParams

class Config:
"""Configuration attributes for PageNode"""
json_encoders = {
ObjectId: str,
}
5 changes: 3 additions & 2 deletions api/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""User Manager"""

from typing import Optional, Any, Dict
from fastapi import Depends, Request
from fastapi import Depends, Request, Response
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_users import BaseUserManager
from fastapi_users.db import (
Expand Down Expand Up @@ -68,7 +68,8 @@ async def on_after_verify(self, user: User,
self.email_sender.create_and_send_email(subject, content, user.email)

async def on_after_login(self, user: User,
request: Optional[Request] = None):
request: Optional[Request] = None,
response: Optional[Response] = None):
"""Handler to execute after successful user login"""
print(f"User {user.id} {user.username} logged in.")

Expand Down
12 changes: 8 additions & 4 deletions docker/api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
cloudevents==1.9.0
fastapi[all]==0.99.1
fastapi-pagination==0.9.3
fastapi-users[beanie, oauth]==10.4.0
# fastapi[all]==0.99.1
fastapi[all]==0.115.0
# fastapi-pagination==0.9.3
fastapi-pagination==0.12.30
# fastapi-users[beanie, oauth]==10.4.0
fastapi-users[beanie, oauth]==13.0.0
fastapi-versioning==0.10.0
MarkupSafe==2.0.1
motor==3.6.0
pymongo==4.9.0
passlib==1.7.4
pydantic==1.10.13
# pydantic==1.10.13
pydantic==2.9.2
pymongo-migrate==0.11.0
python-jose[cryptography]==3.3.0
redis==5.0.1
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ requires-python = ">=3.10"
license = {text = "LGPL-2.1-or-later"}
dependencies = [
"cloudevents == 1.9.0",
"fastapi[all] == 0.99.1",
"fastapi-pagination == 0.9.3",
"fastapi-users[beanie, oauth] == 10.4.0",
"fastapi[all] == 0.115.0",
"fastapi-pagination == 0.12.30",
"fastapi-users[beanie, oauth] == 13.0.0",
"fastapi-versioning == 0.10.0",
"MarkupSafe == 2.0.1",
"motor == 3.6.0",
"pymongo == 4.9.0",
"passlib == 1.7.4",
"pydantic == 1.10.13",
"pydantic == 2.9.2",
"pymongo-migrate == 0.11.0",
"python-jose[cryptography] == 3.3.0",
"redis == 5.0.1",
Expand Down

0 comments on commit 0bd1ba6

Please sign in to comment.