diff --git a/calendar_backend/routes/base.py b/calendar_backend/routes/base.py index 780de7ab..4797a889 100644 --- a/calendar_backend/routes/base.py +++ b/calendar_backend/routes/base.py @@ -103,7 +103,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - app.add_middleware( DBSessionMiddleware, - db_url=settings.DB_DSN, + db_url=str(settings.DB_DSN), engine_args={"pool_pre_ping": True, "isolation_level": "AUTOCOMMIT"}, ) app.add_middleware( diff --git a/calendar_backend/routes/event/comment.py b/calendar_backend/routes/event/comment.py index eace2e34..38cc954b 100644 --- a/calendar_backend/routes/event/comment.py +++ b/calendar_backend/routes/event/comment.py @@ -17,10 +17,10 @@ async def comment_event(event_id: int, comment: EventCommentPost) -> CommentEventGet: approve_status = ApproveStatuses.APPROVED if not settings.REQUIRE_REVIEW_EVENT_COMMENT else ApproveStatuses.PENDING comment_event = DbCommentEvent.create( - event_id=event_id, session=db.session, **comment.dict(), approve_status=approve_status + event_id=event_id, session=db.session, **comment.model_dump(), approve_status=approve_status ) db.session.commit() - return CommentEventGet.from_orm(comment_event) + return CommentEventGet.model_validate(comment_event) @router.patch("/{id}", response_model=CommentEventGet) @@ -30,9 +30,9 @@ async def update_comment(id: int, event_id: int, comment_inp: EventCommentPatch) raise ObjectNotFound(DbCommentEvent, id) if comment.approve_status is not ApproveStatuses.PENDING: raise ForbiddenAction(DbCommentEvent, id) - comment_event = DbCommentEvent.update(id, session=db.session, **comment_inp.dict(exclude_unset=True)) + comment_event = DbCommentEvent.update(id, session=db.session, **comment_inp.model_dump(exclude_unset=True)) db.session.commit() - return CommentEventGet.from_orm(comment_event) + return CommentEventGet.model_validate(comment_event) @router.get("/{id}", response_model=CommentEventGet) @@ -40,7 +40,7 @@ async def get_comment(id: int, event_id: int) -> CommentEventGet: comment = DbCommentEvent.get(id, session=db.session) if not comment.event_id == event_id or comment.approve_status != ApproveStatuses.APPROVED: raise ObjectNotFound(DbCommentEvent, id) - return CommentEventGet.from_orm(comment) + return CommentEventGet.model_validate(comment) @router.delete("/{id}", response_model=None) diff --git a/calendar_backend/routes/event/comment_review.py b/calendar_backend/routes/event/comment_review.py index d3813231..5dd81536 100644 --- a/calendar_backend/routes/event/comment_review.py +++ b/calendar_backend/routes/event/comment_review.py @@ -3,7 +3,7 @@ from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends from fastapi_sqlalchemy import db -from pydantic import parse_obj_as +from pydantic import TypeAdapter from calendar_backend.exceptions import ObjectNotFound from calendar_backend.models import ApproveStatuses @@ -25,7 +25,8 @@ async def get_unreviewed_comments( .filter(DbCommentEvent.event_id == event_id, DbCommentEvent.approve_status == ApproveStatuses.PENDING) .all() ) - return parse_obj_as(list[CommentEventGet], comments) + adapter = TypeAdapter(list[CommentEventGet]) + return adapter.validate_python(comments) @router.post("/{id}/review/", response_model=CommentEventGet) @@ -42,4 +43,4 @@ async def review_comment( if action == ApproveStatuses.DECLINED: DbCommentEvent.delete(comment.id, session=db.session) db.session.commit() - return CommentEventGet.from_orm(comment) + return CommentEventGet.model_validate(comment) diff --git a/calendar_backend/routes/event/event.py b/calendar_backend/routes/event/event.py index cfb080dd..c33d0ed2 100644 --- a/calendar_backend/routes/event/event.py +++ b/calendar_backend/routes/event/event.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, Query from fastapi.responses import FileResponse from fastapi_sqlalchemy import db -from pydantic import parse_obj_as +from pydantic import TypeAdapter from calendar_backend.exceptions import NotEnoughCriteria from calendar_backend.methods import list_calendar @@ -23,7 +23,7 @@ @router.get("/{id}", response_model=EventGet) async def get_event_by_id(id: int) -> EventGet: - return EventGet.from_orm(Event.get(id, session=db.session)) + return EventGet.model_validate(Event.get(id, session=db.session)) async def _get_timetable(start: date, end: date, group_id, lecturer_id, room_id, detail, limit, offset): @@ -60,7 +60,7 @@ async def _get_timetable(start: date, end: date, group_id, lecturer_id, room_id, } ] - return GetListEvent(items=events, limit=limit, offset=offset, total=cnt).dict(exclude=fmt) + return GetListEvent(items=events, limit=limit, offset=offset, total=cnt).model_dump(exclude=fmt) @router.get("/", response_model=GetListEvent | None) @@ -86,7 +86,7 @@ async def get_events( @router.post("/", response_model=EventGet) async def create_event(event: EventPost, _=Depends(UnionAuth(scopes=["timetable.event.create"]))) -> EventGet: - event_dict = event.dict() + event_dict = event.model_dump() rooms = [Room.get(room_id, session=db.session) for room_id in event_dict.pop("room_id", [])] lecturers = [Lecturer.get(lecturer_id, session=db.session) for lecturer_id in event_dict.pop("lecturer_id", [])] groups = [Group.get(group_id, session=db.session) for group_id in event_dict.pop("group_id", [])] @@ -98,7 +98,7 @@ async def create_event(event: EventPost, _=Depends(UnionAuth(scopes=["timetable. session=db.session, ) db.session.commit() - return EventGet.from_orm(event_get) + return EventGet.model_validate(event_get) @router.post("/bulk", response_model=list[EventGet]) @@ -107,7 +107,7 @@ async def create_events( ) -> list[EventGet]: result = [] for event in events: - event_dict = event.dict() + event_dict = event.model_dump() rooms = [Room.get(room_id, session=db.session) for room_id in event_dict.pop("room_id", [])] lecturers = [Lecturer.get(lecturer_id, session=db.session) for lecturer_id in event_dict.pop("lecturer_id", [])] groups = [Group.get(group_id, session=db.session) for group_id in event_dict.pop("group_id", [])] @@ -121,16 +121,17 @@ async def create_events( ) ) db.session.commit() - return parse_obj_as(list[EventGet], result) + adapter = TypeAdapter(list[EventGet]) + return adapter.validate_python(result) @router.patch("/{id}", response_model=EventGet) async def patch_event( id: int, event_inp: EventPatch, _=Depends(UnionAuth(scopes=["timetable.event.update"])) ) -> EventGet: - patched = Event.update(id, session=db.session, **event_inp.dict(exclude_unset=True)) + patched = Event.update(id, session=db.session, **event_inp.model_dump(exclude_unset=True)) db.session.commit() - return EventGet.from_orm(patched) + return EventGet.model_validate(patched) @router.delete("/bulk", response_model=None) diff --git a/calendar_backend/routes/group/group.py b/calendar_backend/routes/group/group.py index 2797b606..7125c2a6 100644 --- a/calendar_backend/routes/group/group.py +++ b/calendar_backend/routes/group/group.py @@ -16,7 +16,7 @@ @router.get("/{id}", response_model=GroupGet) async def get_group_by_id(id: int) -> GroupGet: - return GroupGet.from_orm(Group.get(id, session=db.session)) + return GroupGet.model_validate(Group.get(id, session=db.session)) @router.get("/", response_model=GetListGroup) @@ -40,9 +40,9 @@ async def get_groups(query: str = "", limit: int = 10, offset: int = 0) -> GetLi async def create_group(group: GroupPost, _=Depends(UnionAuth(scopes=["timetable.group.create"]))) -> GroupGet: if db.session.query(Group).filter(Group.number == group.number).one_or_none(): raise HTTPException(status_code=423, detail="Already exists") - group = Group.create(**group.dict(), session=db.session) + group = Group.create(**group.model_dump(), session=db.session) db.session.commit() - return GroupGet.from_orm(group) + return GroupGet.model_validate(group) @router.patch("/{id}", response_model=GroupGet) @@ -56,9 +56,9 @@ async def patch_group( and query.id != id ): raise HTTPException(status_code=423, detail="Already exists") - patched = Group.update(id, **group_inp.dict(exclude_unset=True), session=db.session) + patched = Group.update(id, **group_inp.model_dump(exclude_unset=True), session=db.session) db.session.commit() - return GroupGet.from_orm(patched) + return GroupGet.model_validate(patched) @router.delete("/{id}", response_model=None) diff --git a/calendar_backend/routes/lecturer/comment.py b/calendar_backend/routes/lecturer/comment.py index 1c3689f7..e4c200d9 100644 --- a/calendar_backend/routes/lecturer/comment.py +++ b/calendar_backend/routes/lecturer/comment.py @@ -21,11 +21,11 @@ async def comment_lecturer(lecturer_id: int, comment: LecturerCommentPost) -> Co db_comment_lecturer = DbCommentLecturer.create( lecturer_id=lecturer_id, session=db.session, - **comment.dict(), + **comment.model_dump(), approve_status=approve_status, ) db.session.commit() - return CommentLecturer.from_orm(db_comment_lecturer) + return CommentLecturer.model_validate(db_comment_lecturer) @router.patch("/comment/{id}", response_model=CommentLecturer) @@ -35,9 +35,9 @@ async def update_comment_lecturer(id: int, lecturer_id: int, comment_inp: Lectur raise ObjectNotFound(DbCommentLecturer, id) if comment.approve_status is not ApproveStatuses.PENDING: raise ForbiddenAction(DbCommentLecturer, id) - patched = DbCommentLecturer.update(id, session=db.session, **comment_inp.dict(exclude_unset=True)) + patched = DbCommentLecturer.update(id, session=db.session, **comment_inp.model_dump(exclude_unset=True)) db.session.commit() - return CommentLecturer.from_orm(patched) + return CommentLecturer.model_validate(patched) @router.delete("/comment/{id}", response_model=None) @@ -58,7 +58,7 @@ async def get_comment(id: int, lecturer_id: int) -> CommentLecturer: raise ObjectNotFound(DbCommentLecturer, id) if comment.approve_status is not None: raise ForbiddenAction(DbCommentLecturer, id) - return CommentLecturer.from_orm(comment) + return CommentLecturer.model_validate(comment) @router.get("/comment/", response_model=LecturerComments) diff --git a/calendar_backend/routes/lecturer/comment_review.py b/calendar_backend/routes/lecturer/comment_review.py index 94433728..ddb296e2 100644 --- a/calendar_backend/routes/lecturer/comment_review.py +++ b/calendar_backend/routes/lecturer/comment_review.py @@ -3,7 +3,7 @@ from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends from fastapi_sqlalchemy import db -from pydantic import parse_obj_as +from pydantic import TypeAdapter from calendar_backend.exceptions import ObjectNotFound from calendar_backend.models.db import ApproveStatuses @@ -25,7 +25,8 @@ async def get_unreviewed_comments( ) .all() ) - return parse_obj_as(list[CommentLecturer], comments) + adapter = TypeAdapter(list[CommentLecturer]) + return adapter.validate_python(comments) @router.post("/{id}/review/", response_model=CommentLecturer) @@ -42,4 +43,4 @@ async def review_comment( if action == ApproveStatuses.DECLINED: DbCommentLecturer.delete(comment.id, session=db.session) db.session.commit() - return CommentLecturer.from_orm(comment) + return CommentLecturer.model_validate(comment) diff --git a/calendar_backend/routes/lecturer/lecturer.py b/calendar_backend/routes/lecturer/lecturer.py index 07d5ad35..9320aab0 100644 --- a/calendar_backend/routes/lecturer/lecturer.py +++ b/calendar_backend/routes/lecturer/lecturer.py @@ -22,7 +22,7 @@ @router.get("/{id}", response_model=LecturerGet) async def get_lecturer_by_id(id: int) -> LecturerGet: lecturer = Lecturer.get(id, session=db.session) - result = LecturerGet.from_orm(Lecturer.get(id, session=db.session)) + result = LecturerGet.model_validate(Lecturer.get(id, session=db.session)) if lecturer.avatar_id: result.avatar_link = get_photo_webpath(lecturer.avatar.link) return result @@ -49,7 +49,7 @@ async def get_lecturers( result = [] for row in query: - row_get = LecturerGet.from_orm(row) + row_get = LecturerGet.model_validate(row) if row.avatar: row_get.avatar_link = get_photo_webpath(row.avatar.link) result.append(row_get) @@ -65,9 +65,9 @@ async def get_lecturers( async def create_lecturer( lecturer: LecturerPost, _=Depends(UnionAuth(scopes=["timetable.lecturer.create"])) ) -> LecturerGet: - dblecturer = Lecturer.create(session=db.session, **lecturer.dict()) + dblecturer = Lecturer.create(session=db.session, **lecturer.model_dump()) db.session.commit() - return LecturerGet.from_orm(dblecturer) + return LecturerGet.model_validate(dblecturer) @router.patch("/{id}", response_model=LecturerGet) @@ -79,12 +79,15 @@ async def patch_lecturer( if photo.lecturer_id != id or photo.approve_status != ApproveStatuses.APPROVED: raise ObjectNotFound(DbPhoto, lecturer_inp.avatar_id) lecturer_upd = Lecturer.update( - id, session=db.session, **lecturer_inp.dict(exclude_unset=True), avatar_link=get_photo_webpath(photo.link) + id, + session=db.session, + **lecturer_inp.model_dump(exclude_unset=True), + avatar_link=get_photo_webpath(photo.link), ) else: - lecturer_upd = Lecturer.update(id, session=db.session, **lecturer_inp.dict(exclude_unset=True)) + lecturer_upd = Lecturer.update(id, session=db.session, **lecturer_inp.model_dump(exclude_unset=True)) db.session.commit() - return LecturerGet.from_orm(lecturer_upd) + return LecturerGet.model_validate(lecturer_upd) @router.delete("/{id}", response_model=None) diff --git a/calendar_backend/routes/lecturer/photo.py b/calendar_backend/routes/lecturer/photo.py index db2b2147..87e329ff 100644 --- a/calendar_backend/routes/lecturer/photo.py +++ b/calendar_backend/routes/lecturer/photo.py @@ -29,7 +29,7 @@ async def upload_photo(lecturer_id: int, photo: UploadFile = File(...)) -> Photo """ photo = await upload_lecturer_photo(lecturer_id, db.session, file=photo) db.session.commit() - return Photo.from_orm(photo) + return Photo.model_validate(photo) @router.get("/photo", response_model=LecturerPhotos) @@ -68,4 +68,4 @@ async def get_photo(id: int, lecturer_id: int) -> Photo: photo = DbPhoto.get(id, session=db.session) if photo.lecturer_id != lecturer_id or photo.approve_status != ApproveStatuses.APPROVED: raise ObjectNotFound(DbPhoto, id) - return Photo.from_orm(photo) + return Photo.model_validate(photo) diff --git a/calendar_backend/routes/lecturer/photo_review.py b/calendar_backend/routes/lecturer/photo_review.py index 6c96c8ab..a856876e 100644 --- a/calendar_backend/routes/lecturer/photo_review.py +++ b/calendar_backend/routes/lecturer/photo_review.py @@ -51,7 +51,7 @@ async def get_unreviewed_photos( result = [] for row in query: - get_row = Photo.from_orm(row) + get_row = Photo.model_validate(row) get_row.link = get_photo_webpath(row.link) result.append(get_row) @@ -78,4 +78,4 @@ async def review_photo( if not photo.lecturer.avatar: photo.lecturer.avatar_id = photo.id db.session.flush() - return Photo.from_orm(photo) + return Photo.model_validate(photo) diff --git a/calendar_backend/routes/models/base.py b/calendar_backend/routes/models/base.py index 97809112..42271191 100644 --- a/calendar_backend/routes/models/base.py +++ b/calendar_backend/routes/models/base.py @@ -1,11 +1,10 @@ import datetime -from pydantic import BaseModel, validator +from pydantic import BaseModel, ConfigDict, field_validator class Base(BaseModel): - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class CommentLecturer(Base): @@ -28,11 +27,11 @@ class CommentEventGet(Base): class GroupGet(Base): id: int - name: str | None + name: str | None = None number: str @classmethod - @validator("number") + @field_validator("number") def number_validate(cls, v: str): if len(v) not in [3, 4]: raise ValueError("Group number must contain 3 or 4 characters") @@ -49,9 +48,9 @@ class LecturerGet(Base): first_name: str middle_name: str last_name: str - avatar_id: int | None - avatar_link: str | None - description: str | None + avatar_id: int | None = None + avatar_link: str | None = None + description: str | None = None def __repr__(self): return f"Lecturer(id={self.id}, first_name={self.first_name}, middle_name={self.middle_name}, last_name={self.last_name})" @@ -60,9 +59,9 @@ def __repr__(self): class RoomGet(Base): id: int name: str - building: str | None - building_url: str | None - direction: str | None + building: str | None = None + building_url: str | None = None + direction: str | None = None def __repr__(self): return f"Room(id={self.id}, name={self.name}, direction={self.direction}, building={self.building})" diff --git a/calendar_backend/routes/models/event.py b/calendar_backend/routes/models/event.py index 89c87266..24bee14d 100644 --- a/calendar_backend/routes/models/event.py +++ b/calendar_backend/routes/models/event.py @@ -4,12 +4,12 @@ class EventPatch(Base): - name: str | None - room_id: list[int] | None - group_id: list[int] | None - lecturer_id: list[int] | None - start_ts: datetime.datetime | None - end_ts: datetime.datetime | None + name: str | None = None + room_id: list[int] | None = None + group_id: list[int] | None = None + lecturer_id: list[int] | None = None + start_ts: datetime.datetime | None = None + end_ts: datetime.datetime | None = None def __repr__(self): return ( @@ -58,8 +58,8 @@ class EventCommentPost(Base): class EventCommentPatch(Base): - text: str | None - author_name: str | None + text: str | None = None + author_name: str | None = None class EventComments(Base): diff --git a/calendar_backend/routes/models/group.py b/calendar_backend/routes/models/group.py index c77853e5..f24cc63b 100644 --- a/calendar_backend/routes/models/group.py +++ b/calendar_backend/routes/models/group.py @@ -1,14 +1,14 @@ -from pydantic import validator +from pydantic import field_validator from .base import Base, EventGet, GroupGet class GroupPatch(Base): - name: str | None - number: str | None + name: str | None = None + number: str | None = None @classmethod - @validator("number") + @field_validator("number") def number_validate(cls, v: str): if v is None: return v @@ -23,11 +23,11 @@ def __repr__(self): class GroupPost(Base): - name: str | None + name: str | None = None number: str @classmethod - @validator("number") + @field_validator("number") def number_validate(cls, v: str): if v is None: return v diff --git a/calendar_backend/routes/models/lecturer.py b/calendar_backend/routes/models/lecturer.py index 776095ba..52098d51 100644 --- a/calendar_backend/routes/models/lecturer.py +++ b/calendar_backend/routes/models/lecturer.py @@ -11,11 +11,11 @@ class LecturerPhotos(Base): class LecturerPatch(Base): - first_name: str | None - middle_name: str | None - last_name: str | None - avatar_id: int | None - description: str | None + first_name: str | None = None + middle_name: str | None = None + last_name: str | None = None + avatar_id: int | None = None + description: str | None = None def __repr__(self): return f"Lecturer(first_name={self.first_name}, middle_name={self.middle_name}, last_name={self.last_name})" @@ -25,7 +25,7 @@ class LecturerPost(Base): first_name: str middle_name: str last_name: str - description: str | None + description: str | None = None def __repr__(self): return f"Lecturer(first_name={self.first_name}, middle_name={self.middle_name}, last_name={self.last_name})" @@ -54,8 +54,8 @@ class LecturerCommentPost(Base): class LecturerCommentPatch(Base): - author_name: str | None - text: str | None + author_name: str | None = None + text: str | None = None class LecturerComments(Base): diff --git a/calendar_backend/routes/models/room.py b/calendar_backend/routes/models/room.py index 6ab0e83d..98ae78c5 100644 --- a/calendar_backend/routes/models/room.py +++ b/calendar_backend/routes/models/room.py @@ -4,10 +4,10 @@ class RoomPatch(Base): - name: str | None - building: str | None - building_url: str | None - direction: Direction | None + name: str | None = None + building: str | None = None + building_url: str | None = None + direction: Direction | None = None def __repr__(self): return f"Room(name={self.name}, direction={self.direction})" @@ -15,9 +15,9 @@ def __repr__(self): class RoomPost(Base): name: str - building: str | None - building_url: str | None - direction: Direction | None + building: str | None = None + building_url: str | None = None + direction: Direction | None = None class RoomEvents(RoomGet): diff --git a/calendar_backend/settings.py b/calendar_backend/settings.py index 9d7a9195..41e68817 100644 --- a/calendar_backend/settings.py +++ b/calendar_backend/settings.py @@ -2,7 +2,8 @@ from functools import lru_cache from auth_lib.fastapi import UnionAuthSettings -from pydantic import AnyHttpUrl, BaseSettings, DirectoryPath, Json, PostgresDsn +from pydantic import AnyHttpUrl, ConfigDict, DirectoryPath, Json, PostgresDsn +from pydantic_settings import BaseSettings class Settings(UnionAuthSettings, BaseSettings): @@ -21,18 +22,14 @@ class Settings(UnionAuthSettings, BaseSettings): REQUIRE_REVIEW_PHOTOS: bool = True REQUIRE_REVIEW_LECTURER_COMMENT: bool = True REQUIRE_REVIEW_EVENT_COMMENT: bool = True - GOOGLE_CLIENT_SECRET: Json | None + GOOGLE_CLIENT_SECRET: Json | None = None CORS_ALLOW_ORIGINS: list[str] = ['*'] CORS_ALLOW_CREDENTIALS: bool = True CORS_ALLOW_METHODS: list[str] = ['*'] CORS_ALLOW_HEADERS: list[str] = ['*'] - SUPPORTED_FILE_EXTENSIONS: list[str] = ['png', 'svg', 'jpg', 'jpeg', 'webp'] + SUPPORTED_FILE_EXTENSIONS: list[str] = ["png", "svg", "jpg", "jpeg", "webp"] - class Config: - """Pydantic BaseSettings config""" - - case_sensitive = True - env_file = ".env" + model_config = ConfigDict(case_sensitive=True, env_file='.env', extra='ignore') @lru_cache diff --git a/migrations/env.py b/migrations/env.py index 54d9554c..522e2171 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -61,7 +61,7 @@ def run_migrations_online(): """ configuration = config.get_section(config.config_ini_section) - configuration['sqlalchemy.url'] = settings.DB_DSN + configuration['sqlalchemy.url'] = str(settings.DB_DSN) connectable = engine_from_config( configuration, prefix="sqlalchemy.", diff --git a/requirements.txt b/requirements.txt index 4164cd9e..8c99bdc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ httpx Pillow logging-profcomff auth-lib-profcomff[fastapi] +pydantic-settings \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 3eac84b6..096339a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ def client_auth(mocker: MockerFixture): @pytest.fixture() def dbsession(): settings = get_settings() - engine = create_engine(settings.DB_DSN, isolation_level='AUTOCOMMIT') + engine = create_engine(str(settings.DB_DSN), isolation_level='AUTOCOMMIT') TestingSessionLocal = sessionmaker(bind=engine) DeclarativeBase.metadata.create_all(bind=engine) return TestingSessionLocal()