Skip to content

Commit acbfc79

Browse files
Jeny Sadadianuclearcat
Jeny Sadadia
authored andcommitted
api: upgrade python packages
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]>
1 parent 9bddc8f commit acbfc79

File tree

7 files changed

+90
-36
lines changed

7 files changed

+90
-36
lines changed

api/config.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
"""Module settings"""
77

8-
from pydantic import BaseSettings, EmailStr
8+
from pydantic import EmailStr
9+
from pydantic_settings import BaseSettings
910

1011

1112
# pylint: disable=too-few-public-methods

api/db.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ async def create(self, obj):
170170
raise ValueError(f"Object cannot be created with id: {obj.id}")
171171
delattr(obj, 'id')
172172
col = self._get_collection(obj.__class__)
173-
res = await col.insert_one(obj.dict(by_alias=True))
173+
res = await col.insert_one(obj.model_dump(by_alias=True))
174174
obj.id = res.inserted_id
175175
return obj
176176

api/main.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import re
1313
from typing import List, Union, Optional
1414
import threading
15+
from contextlib import asynccontextmanager
1516
from fastapi import (
1617
Depends,
1718
FastAPI,
@@ -53,6 +54,14 @@
5354
)
5455

5556

57+
@asynccontextmanager
58+
async def lifespan(app: FastAPI): # pylint: disable=redefined-outer-name
59+
"""Lifespan functions for startup and shutdown events"""
60+
await pubsub_startup()
61+
await create_indexes()
62+
await initialize_beanie()
63+
yield
64+
5665
# List of all the supported API versions. This is a placeholder until the API
5766
# actually supports multiple versions with different sets of endpoints and
5867
# models etc.
@@ -105,8 +114,7 @@ def all(self):
105114

106115
metrics = Metrics()
107116

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

121129

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

128135

129-
@app.on_event('startup')
130136
async def create_indexes():
131137
"""Startup event handler to create database indexes"""
132138
await db.create_indexes()
133139

134140

135-
@app.on_event('startup')
136141
async def initialize_beanie():
137142
"""Startup event handler to initialize Beanie"""
138143
await db.initialize_beanie()
@@ -535,7 +540,7 @@ def serialize_paginated_data(model, data: list):
535540
"""
536541
serialized_data = []
537542
for obj in data:
538-
serialized_data.append(model(**obj).dict())
543+
serialized_data.append(model(**obj).model_dump(mode='json'))
539544
return serialized_data
540545

541546

api/models.py

+65-18
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
"""Server-side model definitions"""
1313

1414
from datetime import datetime
15-
from typing import Optional, TypeVar
15+
from typing import Optional, TypeVar, Dict, Any, List
1616
from pydantic import (
1717
BaseModel,
18-
conlist,
1918
Field,
19+
model_serializer,
20+
field_validator,
2021
)
22+
from typing_extensions import Annotated
2123
from fastapi import Query
2224
from fastapi_pagination import LimitOffsetPage, LimitOffsetParams
2325
from fastapi_users.db import BeanieBaseUser
@@ -27,7 +29,6 @@
2729
Document,
2830
PydanticObjectId,
2931
)
30-
from bson import ObjectId
3132
from kernelci.api.models_base import DatabaseModel, ModelId
3233

3334

@@ -56,6 +57,7 @@ class SubscriptionStats(Subscription):
5657
description='Timestamp of connection creation'
5758
)
5859
last_poll: Optional[datetime] = Field(
60+
default=None,
5961
description='Timestamp when connection last polled for data'
6062
)
6163

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

90+
@field_validator('groups')
91+
def validate_groups(cls, groups): # pylint: disable=no-self-argument
92+
"""Unique group constraint"""
93+
unique_names = {group.name for group in groups}
94+
if len(unique_names) != len(groups):
95+
raise ValueError("Groups must have unique names.")
96+
return groups
97+
8898
class Settings(BeanieBaseUser.Settings):
8999
"""Configurations"""
90100
# MongoDB collection name for model
@@ -97,23 +107,66 @@ def get_indexes(cls):
97107
cls.Index('email', {'unique': True}),
98108
]
99109

110+
@model_serializer(when_used='json')
111+
def serialize_model(self) -> Dict[str, Any]:
112+
"""Serialize model by converting PyObjectId to string"""
113+
values = self.__dict__.copy()
114+
for field_name, value in values.items():
115+
if isinstance(value, PydanticObjectId):
116+
values[field_name] = str(value)
117+
return values
118+
100119

101120
class UserRead(schemas.BaseUser[PydanticObjectId], ModelId):
102121
"""Schema for reading a user"""
103-
username: Indexed(str, unique=True)
104-
groups: conlist(UserGroup, unique_items=True)
122+
username: Annotated[str, Indexed(unique=True)]
123+
groups: List[UserGroup] = Field(default=[])
124+
125+
@field_validator('groups')
126+
def validate_groups(cls, groups): # pylint: disable=no-self-argument
127+
"""Unique group constraint"""
128+
unique_names = {group.name for group in groups}
129+
if len(unique_names) != len(groups):
130+
raise ValueError("Groups must have unique names.")
131+
return groups
132+
133+
@model_serializer(when_used='json')
134+
def serialize_model(self) -> Dict[str, Any]:
135+
"""Serialize model by converting PyObjectId to string"""
136+
values = self.__dict__.copy()
137+
for field_name, value in values.items():
138+
if isinstance(value, PydanticObjectId):
139+
values[field_name] = str(value)
140+
return values
105141

106142

107143
class UserCreate(schemas.BaseUserCreate):
108144
"""Schema for creating a user"""
109-
username: Indexed(str, unique=True)
110-
groups: Optional[conlist(str, unique_items=True)]
145+
username: Annotated[str, Indexed(unique=True)]
146+
groups: List[str] = Field(default=[])
147+
148+
@field_validator('groups')
149+
def validate_groups(cls, groups): # pylint: disable=no-self-argument
150+
"""Unique group constraint"""
151+
unique_names = set(groups)
152+
if len(unique_names) != len(groups):
153+
raise ValueError("Groups must have unique names.")
154+
return groups
111155

112156

113157
class UserUpdate(schemas.BaseUserUpdate):
114158
"""Schema for updating a user"""
115-
username: Optional[Indexed(str, unique=True)]
116-
groups: Optional[conlist(str, unique_items=True)]
159+
username: Annotated[Optional[str], Indexed(unique=True),
160+
Field(default=None)]
161+
groups: List[str] = Field(default=[])
162+
163+
@field_validator('groups')
164+
def validate_groups(cls, groups): # pylint: disable=no-self-argument
165+
"""Unique group constraint"""
166+
unique_names = set(groups)
167+
if len(unique_names) != len(groups):
168+
raise ValueError("Groups must have unique names.")
169+
return groups
117170

118171

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

135188
__params_type__ = CustomLimitOffsetParams
136-
137-
class Config:
138-
"""Configuration attributes for PageNode"""
139-
json_encoders = {
140-
ObjectId: str,
141-
}

api/user_manager.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""User Manager"""
77

88
from typing import Optional, Any, Dict
9-
from fastapi import Depends, Request
9+
from fastapi import Depends, Request, Response
1010
from fastapi.security import OAuth2PasswordRequestForm
1111
from fastapi_users import BaseUserManager
1212
from fastapi_users.db import (
@@ -68,7 +68,8 @@ async def on_after_verify(self, user: User,
6868
self.email_sender.create_and_send_email(subject, content, user.email)
6969

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

docker/api/requirements.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
cloudevents==1.9.0
2-
fastapi[all]==0.99.1
3-
fastapi-pagination==0.9.3
4-
fastapi-users[beanie, oauth]==10.4.0
2+
fastapi[all]==0.115.0
3+
fastapi-pagination==0.12.30
4+
fastapi-users[beanie, oauth]==13.0.0
55
fastapi-versioning==0.10.0
66
MarkupSafe==2.0.1
77
motor==3.6.0
88
pymongo==4.9.0
99
passlib==1.7.4
10-
pydantic==1.10.13
10+
pydantic==2.9.2
1111
pymongo-migrate==0.11.0
1212
python-jose[cryptography]==3.3.0
1313
redis==5.0.1

pyproject.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ requires-python = ">=3.10"
1313
license = {text = "LGPL-2.1-or-later"}
1414
dependencies = [
1515
"cloudevents == 1.9.0",
16-
"fastapi[all] == 0.99.1",
17-
"fastapi-pagination == 0.9.3",
18-
"fastapi-users[beanie, oauth] == 10.4.0",
16+
"fastapi[all] == 0.115.0",
17+
"fastapi-pagination == 0.12.30",
18+
"fastapi-users[beanie, oauth] == 13.0.0",
1919
"fastapi-versioning == 0.10.0",
2020
"MarkupSafe == 2.0.1",
2121
"motor == 3.6.0",
2222
"pymongo == 4.9.0",
2323
"passlib == 1.7.4",
24-
"pydantic == 1.10.13",
24+
"pydantic == 2.9.2",
2525
"pymongo-migrate == 0.11.0",
2626
"python-jose[cryptography] == 3.3.0",
2727
"redis == 5.0.1",

0 commit comments

Comments
 (0)