Doing cleanup of backend.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-30 11:31:21 -04:00
parent 47262f4799
commit d48d0e7d4f
7 changed files with 69 additions and 37 deletions
+4
View File
@@ -0,0 +1,4 @@
[project]
id = numinar-coding-project-backend
url = /codebases/numinar-coding-project-backend/findings
name = numinar-coding-project-backend
+1 -1
View File
@@ -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:
+2 -3
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+57 -28
View File
@@ -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()
+3 -3
View File
@@ -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,
)