From d48d0e7d4fc03ce8c601777550b13bf82f0a6101 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Tue, 30 Sep 2025 11:31:21 -0400 Subject: [PATCH] Doing cleanup of backend. Signed-off-by: Cliff Hill --- backend/.safety-project.ini | 4 ++ backend/src/backend/models.py | 2 +- backend/src/backend/routers/bookings.py | 5 +- backend/src/backend/routers/rooms.py | 2 +- backend/src/backend/routers/users.py | 2 +- backend/src/backend/services/bookings.py | 85 ++++++++++++++++-------- backend/tests/conftest.py | 6 +- 7 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 backend/.safety-project.ini diff --git a/backend/.safety-project.ini b/backend/.safety-project.ini new file mode 100644 index 00000000..9c66c52f --- /dev/null +++ b/backend/.safety-project.ini @@ -0,0 +1,4 @@ +[project] +id = numinar-coding-project-backend +url = /codebases/numinar-coding-project-backend/findings +name = numinar-coding-project-backend diff --git a/backend/src/backend/models.py b/backend/src/backend/models.py index 61b79fdc..8cf50dbe 100644 --- a/backend/src/backend/models.py +++ b/backend/src/backend/models.py @@ -118,7 +118,7 @@ class Booking(Base): return self._invitee_emails @invitees.setter - def invitees(self, emails: list[str]): + def invitees(self, emails: list[str]) -> None: """Set the list of invitee email addresses for this booking. Args: diff --git a/backend/src/backend/routers/bookings.py b/backend/src/backend/routers/bookings.py index 6907d305..a912e6f2 100644 --- a/backend/src/backend/routers/bookings.py +++ b/backend/src/backend/routers/bookings.py @@ -1,7 +1,6 @@ """Routes for booking-related operations in the backend.""" import logging -from typing import List from fastapi import APIRouter from fastapi import HTTPException @@ -123,7 +122,7 @@ async def create_booking( booking_response = await new_booking_func( session, booking, publish_room_availability_event ) - return booking_response + return BookingResponse.model_validate(booking_response) except ValueError as err: logger.warning(f"Validation error while creating booking: {str(err)}") # Detect overlap/conflict error @@ -286,7 +285,7 @@ async def read_bookings_for_month( session: DBSession, get_bookings_for_month_func: GetBookingsForMonthService, get_room_func: GetRoomService, -) -> List[BookingResponse]: +) -> list[BookingResponse]: """Retrieve all bookings for all rooms for a given month. Args: diff --git a/backend/src/backend/routers/rooms.py b/backend/src/backend/routers/rooms.py index 370b5731..a4fa8e33 100644 --- a/backend/src/backend/routers/rooms.py +++ b/backend/src/backend/routers/rooms.py @@ -182,7 +182,7 @@ async def read_rooms( try: rooms = await get_rooms_func(session) logger.info(f"Successfully retrieved {len(rooms)} rooms") - return rooms + return list(rooms) except NoResultFound as err: logger.warning(f"Room not found: {str(err)}") raise HTTPException(status_code=404, detail="Room not found") from err diff --git a/backend/src/backend/routers/users.py b/backend/src/backend/routers/users.py index a2cb0123..ed12efc8 100644 --- a/backend/src/backend/routers/users.py +++ b/backend/src/backend/routers/users.py @@ -45,7 +45,7 @@ async def read_users( try: users = await get_users_func(session) logger.info(f"Successfully retrieved {len(users)} users") - return users + return list(users) except NoResultFound as err: logger.warning(f"User not found: {str(err)}") raise HTTPException(status_code=404, detail="User not found") from err diff --git a/backend/src/backend/services/bookings.py b/backend/src/backend/services/bookings.py index e2c5d7f6..758d8331 100644 --- a/backend/src/backend/services/bookings.py +++ b/backend/src/backend/services/bookings.py @@ -9,7 +9,6 @@ from typing import Awaitable from typing import Callable from typing import TypedDict from typing import Unpack -from typing import cast import asyncpg from sqlalchemy import and_ @@ -59,7 +58,9 @@ async def get_booking(session: AsyncSession, booking_id: int) -> Booking: stmt = ( select(Booking) .where(Booking.id == booking_id) - .options(selectinload(Booking._invitees)) # type: ignore + .options( + selectinload(Booking._invitees) # pyright: ignore[reportPrivateUsage] + ) ) booking = await session.scalar(stmt) if booking is None: @@ -281,7 +282,7 @@ async def get_bookings_for_month(session: AsyncSession, month: str) -> list[Book result = await session.execute( select(Booking) .where(and_(Booking.start_time >= start_date, Booking.start_time < end_date)) - .options(selectinload(Booking._invitees)) # type: ignore + .options(selectinload(Booking._invitees)) # pyright: ignore[reportPrivateUsage] ) bookings = list(result.scalars().all()) booking_ids = [b.id for b in bookings] @@ -396,6 +397,40 @@ async def validate_no_overlap( ) +def _normalize_invitees(invitees_raw: object) -> list[str]: + if isinstance(invitees_raw, list): + return [str(e) for e in invitees_raw] # pyright: ignore + elif hasattr(invitees_raw, "__iter__") and not isinstance(invitees_raw, str): + return [str(e) for e in invitees_raw] # pyright: ignore + return [] + + +async def _remove_old_invitees( + session: AsyncSession, current: Booking, new_invitees: list[str] +) -> None: + current_invitees = { + invitee.user_email: invitee for invitee in current.get_invitee_objects() + } + new_invitees_set = set(new_invitees) + for email in list(current_invitees.keys()): + if email not in new_invitees_set: + invitee_obj = current_invitees[email] + await session.delete(invitee_obj) + await session.flush() + + +async def _update_booking_record( + session: AsyncSession, booking_id: int, kwargs: dict[str, object] +) -> None: + stmt = ( + update(Booking) + .where(Booking.id == booking_id) + .values(**{k: v for k, v in kwargs.items() if k != "invitees"}) + ) + await session.execute(stmt) + await session.commit() + + async def update_booking( session: AsyncSession, booking_id: int, @@ -414,49 +449,43 @@ async def update_booking( The updated booking object. Raises: - NoResultFound: If no booking with the specified ID is found. asyncpg.InterfaceError: If a database connection error occurs. + TypeError: If room_id is not int or str. """ logger.debug( f"Entering update_booking with booking_id: {booking_id}, params: {kwargs}" ) try: current = await get_booking(session, booking_id) - new_room_id = cast(int, kwargs.get("room_id", current.room_id)) - new_start = cast(datetime, kwargs.get("start_time", current.start_time)) - new_end = cast(datetime, kwargs.get("end_time", current.end_time)) - new_invitees = kwargs.get("invitees", current.invitees) or [] - # Use model property for desired state + room_id_val = kwargs.get("room_id", current.room_id) + if isinstance(room_id_val, int): + new_room_id = room_id_val + elif isinstance(room_id_val, str): + new_room_id = int(room_id_val) + else: + raise TypeError(f"room_id must be int or str, got {type(room_id_val)}") + new_start = kwargs.get("start_time", current.start_time) + if not isinstance(new_start, datetime): + new_start = datetime.fromisoformat(str(new_start)) + new_end = kwargs.get("end_time", current.end_time) + if not isinstance(new_end, datetime): + new_end = datetime.fromisoformat(str(new_end)) + new_invitees_raw = kwargs.get("invitees", current.invitees) or [] + new_invitees = _normalize_invitees(new_invitees_raw) current.invitees = new_invitees - # Remove invitees not in new list - current_invitees = { - invitee.user_email: invitee for invitee in current.get_invitee_objects() - } - new_invitees_set = set(new_invitees) - for email in list(current_invitees.keys()): - if email not in new_invitees_set: - invitee_obj = current_invitees[email] - await session.delete(invitee_obj) - await session.flush() - # Update booking itself + await _remove_old_invitees(session, current, new_invitees) await validate_room_exists(session, new_room_id, get_room) now = datetime.now(timezone.utc) max_months = config("BOOKING_MAX_MONTHS", default=12, cast=int) validate_time_constraints(new_start, new_end, now, max_months) await validate_no_overlap(session, booking_id, new_room_id, new_start, new_end) - stmt = ( - update(Booking) - .where(Booking.id == booking_id) - .values(**{k: v for k, v in kwargs.items() if k != "invitees"}) - ) - await session.execute(stmt) - await session.commit() + await _update_booking_record(session, booking_id, dict(kwargs)) # Add invitees new to list updated = await get_booking(session, booking_id) updated_emails = { invitee.user_email for invitee in updated.get_invitee_objects() } - for email in new_invitees_set - updated_emails: + for email in set(new_invitees) - updated_emails: new_invitee = Invitee(booking_id=booking_id, user_email=email) session.add(new_invitee) await session.commit() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index ef5551bc..7d53538a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -405,12 +405,12 @@ def updated_booking( """ updated = Booking( room_id=2, # Match the update data in the test - title=booking_update_data["title"], # type: ignore + title=booking_update_data.get("title", sample_booking.title), start_time=datetime.fromisoformat( - booking_update_data["start_time"] # type: ignore + booking_update_data.get("start_time", sample_booking.start_time.isoformat()) ), end_time=datetime.fromisoformat( - booking_update_data["end_time"] # type: ignore + booking_update_data.get("end_time", sample_booking.end_time.isoformat()) ), invitees=sample_booking.invitees, )