Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: upgrade python packages #554

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -10,7 +10,7 @@
from beanie import init_beanie
from fastapi_pagination.ext.motor import paginate
from motor import motor_asyncio
from kernelci.api.models import Hierarchy, Node, parse_node_obj

Check failure on line 13 in api/db.py

View workflow job for this annotation

GitHub Actions / Lint

Unable to import 'kernelci.api.models'
from .models import User, UserGroup


Expand Down Expand Up @@ -170,7 +170,7 @@
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 All @@ -30,7 +31,7 @@
from pymongo.errors import DuplicateKeyError
from fastapi_users import FastAPIUsers
from beanie import PydanticObjectId
from kernelci.api.models import (

Check failure on line 34 in api/main.py

View workflow job for this annotation

GitHub Actions / Lint

Unable to import 'kernelci.api.models'
Node,
Hierarchy,
PublishEvent,
Expand All @@ -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 @@

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 @@
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 @@
"""
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
83 changes: 65 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,8 +29,7 @@
Document,
PydanticObjectId,
)
from bson import ObjectId
from kernelci.api.models_base import DatabaseModel, ModelId

Check failure on line 32 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Unable to import 'kernelci.api.models_base'


# PubSub model definitions
Expand Down Expand Up @@ -56,6 +57,7 @@
description='Timestamp of connection creation'
)
last_poll: Optional[datetime] = Field(
default=None,
description='Timestamp when connection last polled for data'
)

Expand All @@ -79,12 +81,20 @@
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

Check warning on line 91 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Method could be a function
"""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 +107,66 @@
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):

Check failure on line 120 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Inheriting 'schemas.BaseUser[PydanticObjectId]', which is not a class.
"""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

Check warning on line 126 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Method could be a function
"""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

Check warning on line 149 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Method could be a function
"""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

Check warning on line 164 in api/models.py

View workflow job for this annotation

GitHub Actions / Lint

Method could be a function
"""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 +186,3 @@
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
8 changes: 4 additions & 4 deletions docker/api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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
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
7 changes: 4 additions & 3 deletions tests/e2e_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from motor.motor_asyncio import AsyncIOMotorClient

from api.main import versioned_app
from kernelci.api.models import Node, Regression

Check failure on line 14 in tests/e2e_tests/conftest.py

View workflow job for this annotation

GitHub Actions / Lint

Unable to import 'kernelci.api.models'

Check warning on line 14 in tests/e2e_tests/conftest.py

View workflow job for this annotation

GitHub Actions / Lint

third party import "from kernelci.api.models import Node, Regression" should be placed before "from api.main import versioned_app"

BASE_URL = 'http://api:8000/latest/'
DB_URL = 'mongodb://db:27017'
Expand All @@ -19,8 +19,8 @@

db_client = AsyncIOMotorClient(DB_URL)
db = db_client[DB_NAME]
node_model_fields = set(Node.__fields__.keys())
regression_model_fields = set(Regression.__fields__.keys())
node_model_fields = set(Node.model_fields.keys())
regression_model_fields = set(Regression.model_fields.keys())
paginated_response_keys = {
'items',
'total',
Expand All @@ -42,7 +42,8 @@
"""Database create method"""
delattr(obj, 'id')
col = db[collection]
res = await col.insert_one(obj.dict(by_alias=True))
# 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
1 change: 1 addition & 0 deletions tests/e2e_tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@pytest.mark.dependency(
depends=[
'tests/e2e_tests/test_subscribe_handler.py::test_subscribe_node_channel'],

Check warning on line 18 in tests/e2e_tests/test_pipeline.py

View workflow job for this annotation

GitHub Actions / Lint

line too long (82 > 79 characters)
scope='session')
@pytest.mark.order(4)
@pytest.mark.asyncio
Expand All @@ -38,10 +38,11 @@

# Create Task to listen pubsub event on 'node' channel
task_listen = create_listen_task(test_async_client,
pytest.node_channel_subscription_id) # pylint: disable=no-member

Check warning on line 41 in tests/e2e_tests/test_pipeline.py

View workflow job for this annotation

GitHub Actions / Lint

line too long (102 > 79 characters)

# Create a node
node = {
"kind": "checkout",
"name": "checkout",
"path": ["checkout"],
"data": {
Expand Down
9 changes: 9 additions & 0 deletions tests/unit_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,15 @@ async def mock_beanie_get_user_by_id(mocker):
return async_mock


@pytest.fixture
async def mock_beanie_user_update(mocker):
"""Mocks async call to external method to update user"""
async_mock = AsyncMock()
mocker.patch('fastapi_users_db_beanie.BeanieUserDatabase.update',
side_effect=async_mock)
return async_mock


@pytest.fixture
def mock_auth_current_user(mocker):
"""
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/test_node_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import json

from tests.unit_tests.conftest import BEARER_TOKEN
from kernelci.api.models import Node, Revision

Check failure on line 16 in tests/unit_tests/test_node_handler.py

View workflow job for this annotation

GitHub Actions / Lint

Unable to import 'kernelci.api.models'

Check warning on line 16 in tests/unit_tests/test_node_handler.py

View workflow job for this annotation

GitHub Actions / Lint

third party import "from kernelci.api.models import Node, Revision" should be placed before "from tests.unit_tests.conftest import BEARER_TOKEN"
from api.models import PageModel


Expand All @@ -33,7 +33,7 @@
"describe": "v5.16-rc4-31-g2a987e65025e",
}

revision_obj = Revision.parse_obj(revision_data)
revision_obj = Revision.model_validate(revision_data)
node_obj = Node(
id="61bda8f2eb1a63d2b7152418",
kind="checkout",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/test_root_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ def test_root_endpoint(test_client):
HTTP Response Code 200 OK
JSON with 'message' key
"""
response = test_client.get("/latest")
response = test_client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "KernelCI API"}
5 changes: 4 additions & 1 deletion tests/unit_tests/test_token_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: LGPL-2.1-or-later

Check warning on line 1 in tests/unit_tests/test_token_handler.py

View workflow job for this annotation

GitHub Actions / Lint

Similar lines in 2 files

Check warning on line 1 in tests/unit_tests/test_token_handler.py

View workflow job for this annotation

GitHub Actions / Lint

Similar lines in 2 files
#
# Copyright (C) 2022 Jeny Sadadia
# Author: Jeny Sadadia <[email protected]>
Expand All @@ -15,7 +15,8 @@


@pytest.mark.asyncio
async def test_token_endpoint(test_async_client, mock_user_find):
async def test_token_endpoint(test_async_client, mock_user_find,
mock_beanie_user_update):
"""
Test Case : Test KernelCI API /user/login endpoint
Expected Result :
Expand All @@ -34,6 +35,8 @@
is_verified=True
)
mock_user_find.return_value = user
mock_beanie_user_update.return_value = user

response = await test_async_client.post(
"user/login",
headers={
Expand Down
Loading