More restructuring of files to adhere more to how FastAPI does things.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-22 22:35:26 -04:00
parent 85ef1e09ac
commit 428b9835e1
9 changed files with 445 additions and 386 deletions

View File

@@ -1,10 +1,5 @@
"""DB connection and session management for SQLModel."""
"""Database configuration and engine setup."""
from collections.abc import AsyncGenerator
from typing import TypeVar
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.ext.asyncio import create_async_engine
from backend import config
@@ -23,15 +18,3 @@ echo = config("ENVIRONMENT", default="development") in {"development"}
# Create the async engine
engine = create_async_engine(db_url, echo=echo)
# Used in the sessionize decorator
T = TypeVar("T")
# Create an asynchronous session maker for SQLModel, used in the sessionize decorator
async_maker = async_sessionmaker(engine)
async def get_session() -> AsyncGenerator[AsyncSession, None]: # pragma: no cover
"""Inject a new DB session."""
async with async_maker() as session:
yield session

View File

@@ -1,11 +0,0 @@
"""Dependency definitions for FastAPI routes."""
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.db import get_session
type SessionDep = Annotated[AsyncSession, Depends(get_session)]

View File

@@ -0,0 +1,23 @@
"""Dependency definitions for FastAPI routes."""
from typing import Annotated
from typing import AsyncGenerator
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import async_sessionmaker
from backend import db
# Create an asynchronous session maker for SQLModel, used in the sessionize decorator
async_maker = async_sessionmaker(db.engine)
async def get_session() -> AsyncGenerator[AsyncSession, None]: # pragma: no cover
"""Inject a new DB session."""
async with async_maker() as session:
yield session
type SessionDep = Annotated[AsyncSession, Depends(get_session)]

View File

@@ -0,0 +1 @@
"""Services package for the Numinar coding project backend."""

View File

@@ -0,0 +1,130 @@
"""SQL Database services for managing bookings."""
import logging
from datetime import datetime
from typing import TypedDict
from typing import Unpack
from typing import cast
from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import Room
from backend.models import User
logger = logging.getLogger(__name__)
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]
async def get_bookings_for_room(session: AsyncSession, room_id: int) -> BookingList:
"""Retrieve all bookings for a specific room."""
logger.debug(f"Entering get_bookings_for_room with room_id: {room_id}")
try:
stmt = select(Booking).where(Booking.room_id == room_id)
result = await session.scalars(stmt)
bookings = cast(BookingList, result.all())
logger.info(
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
)
return bookings
except Exception as e:
logger.error(f"Failed to retrieve bookings for room_id {room_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_bookings_for_room")
async def get_booking(session: AsyncSession, booking_id: int) -> Booking:
"""Retrieve a booking by its ID."""
logger.debug(f"Entering get_booking with booking_id: {booking_id}")
try:
stmt = select(Booking).where(Booking.id == booking_id)
booking = await session.scalar(stmt)
logger.info(f"Successfully retrieved booking with id: {booking_id}")
return cast(Booking, booking)
except NoResultFound:
logger.error(f"Booking with id {booking_id} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve booking with id {booking_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_booking")
async def new_booking(session: AsyncSession, booking: Booking) -> Booking:
"""Create a new booking in the database."""
logger.debug("Entering new_booking")
try:
session.add(booking)
await session.commit()
logger.info(f"Successfully created new booking with id: {booking.id}")
return booking
except Exception as e:
logger.error(f"Failed to create new booking: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting new_booking")
class BookingParams(TypedDict):
"""Parameters for updating a booking."""
room_id: int
start_time: datetime
end_time: datetime
async def update_booking(
session: AsyncSession,
booking_id: int,
**kwargs: Unpack[BookingParams],
) -> Booking:
"""Update an existing booking."""
logger.debug(
f"Entering update_booking with booking_id: {booking_id}, params: {kwargs}"
)
try:
stmt = update(Booking).where(Booking.id == booking_id).values(**kwargs)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No booking found with id {booking_id} for update")
raise ValueError(f"No booking found with id {booking_id}")
await session.commit()
booking = await get_booking(session, booking_id)
logger.info(f"Successfully updated booking with id: {booking_id}")
return booking
except Exception as e:
logger.error(f"Failed to update booking with id {booking_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting update_booking")
async def delete_booking(session: AsyncSession, booking_id: int) -> None:
"""Delete a booking from the database."""
logger.debug(f"Entering delete_booking with booking_id: {booking_id}")
try:
stmt = delete(Booking).where(Booking.id == booking_id)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No booking found with id {booking_id} for deletion")
raise ValueError(f"No booking found with id {booking_id}")
await session.commit()
logger.info(f"Successfully deleted booking with id: {booking_id}")
except Exception as e:
logger.error(f"Failed to delete booking with id {booking_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting delete_booking")

View File

@@ -0,0 +1,113 @@
"""SQL Database services for managing bookings."""
import logging
from typing import cast
from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import Invitee
from backend.models import Room
from backend.models import User
from backend.services.users import get_user_by_email
logger = logging.getLogger(__name__)
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]
async def get_invitees_for_booking(session: AsyncSession, booking_id: int) -> UserList:
"""Retrieve all invitees for a specific booking."""
logger.debug(f"Entering get_invitees_for_booking with booking_id: {booking_id}")
try:
stmt = select(Invitee.user).where(Invitee.booking_id == booking_id)
result = await session.scalars(stmt)
invitees = cast(UserList, result.all())
logger.info(
f"Successfully retrieved {len(invitees)} invitees for booking_id: {booking_id}"
)
return invitees
except Exception as e:
logger.error(
f"Failed to retrieve invitees for booking_id {booking_id}: {str(e)}"
)
raise
finally:
logger.debug("Exiting get_invitees_for_booking")
async def add_invitee_to_booking(
session: AsyncSession, booking_id: int, email: str
) -> Invitee:
"""Add an invitee to a booking."""
logger.debug(
f"Entering add_invitee_to_booking with booking_id: {booking_id}, email: {email}"
)
try:
user = await get_user_by_email(session, email)
invitee = Invitee(booking_id=booking_id, user_id=user.id)
session.add(invitee)
await session.commit()
logger.info(
f"Successfully added invitee with email {email} to booking_id: {booking_id}"
)
return invitee
except NoResultFound as e:
logger.error(
f"User with email {email} does not exist for booking_id {booking_id}"
)
raise ValueError(f"User with email {email} does not exist.") from e
except Exception as e:
logger.error(
f"Failed to add invitee with email {email} to booking_id {booking_id}: {str(e)}"
)
await session.rollback()
raise
finally:
logger.debug("Exiting add_invitee_to_booking")
async def remove_invitee_from_booking(
session: AsyncSession, booking_id: int, email: str
) -> None:
"""Remove an invitee from a booking."""
logger.debug(
f"Entering remove_invitee_from_booking with booking_id: {booking_id}, email: {email}"
)
try:
user = await get_user_by_email(session, email)
stmt = delete(Invitee).where(
Invitee.booking_id == booking_id, Invitee.user_id == user.id
)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(
f"No invitee with email {email} found for booking_id {booking_id}"
)
raise ValueError(
f"No invitee with email {email} found for booking_id {booking_id}"
)
await session.commit()
logger.info(
f"Successfully removed invitee with email {email} from booking_id: {booking_id}"
)
except NoResultFound as e:
logger.error(
f"User with email {email} does not exist for booking_id {booking_id}"
)
raise ValueError(f"User with email {email} does not exist.") from e
except Exception as e:
logger.error(
f"Failed to remove invitee with email {email}"
f" from booking_id {booking_id}: {str(e)}"
)
await session.rollback()
raise
finally:
logger.debug("Exiting remove_invitee_from_booking")

View File

@@ -0,0 +1,124 @@
"""SQL Database services for managing rooms."""
import logging
from typing import TypedDict
from typing import Unpack
from typing import cast
from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import Room
from backend.models import User
logger = logging.getLogger(__name__)
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]
async def get_rooms(session: AsyncSession) -> RoomList:
"""Retrieve all rooms from the database."""
logger.debug("Entering get_rooms")
try:
stmt = select(Room)
result = await session.scalars(stmt)
rooms = cast(RoomList, result.all())
logger.info(f"Successfully retrieved {len(rooms)} rooms")
return rooms
except Exception as e:
logger.error(f"Failed to retrieve rooms: {str(e)}")
raise
finally:
logger.debug("Exiting get_rooms")
async def get_room(session: AsyncSession, room_id: int) -> Room:
"""Retrieve a room by its ID."""
logger.debug(f"Entering get_room with room_id: {room_id}")
try:
stmt = select(Room).where(Room.id == room_id)
room = await session.scalar(stmt)
logger.info(f"Successfully retrieved room with id: {room_id}")
return cast(Room, room)
except NoResultFound:
logger.error(f"Room with id {room_id} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve room with id {room_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_room")
async def new_room(session: AsyncSession, room: Room) -> Room:
"""Create a new room in the database."""
logger.debug("Entering new_room")
try:
session.add(room)
await session.commit()
logger.info(f"Successfully created new room with id: {room.id}")
return room
except Exception as e:
logger.error(f"Failed to create new room: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting new_room")
class RoomParams(TypedDict):
"""Parameters for updating a room."""
name: str
location: str
equipment: str
capacity: int
async def update_room(
session: AsyncSession, room_id: int, **kwargs: Unpack[RoomParams]
) -> Room:
"""Update an existing room."""
logger.debug(f"Entering update_room with room_id: {room_id}, params: {kwargs}")
try:
stmt = update(Room).where(Room.id == room_id).values(**kwargs)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No room found with id {room_id} for update")
raise ValueError(f"No room found with id {room_id}")
await session.commit()
room = await get_room(session, room_id)
logger.info(f"Successfully updated room with id: {room_id}")
return room
except Exception as e:
logger.error(f"Failed to update room with id {room_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting update_room")
async def delete_room(session: AsyncSession, room_id: int) -> None:
"""Delete a room from the database."""
logger.debug(f"Entering delete_room with room_id: {room_id}")
try:
stmt = delete(Room).where(Room.id == room_id)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No room found with id {room_id} for deletion")
raise ValueError(f"No room found with id {room_id}")
await session.commit()
logger.info(f"Successfully deleted room with id: {room_id}")
except Exception as e:
logger.error(f"Failed to delete room with id {room_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting delete_room")

View File

@@ -0,0 +1,53 @@
"""SQL Database for managing users."""
import logging
from typing import cast
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import Room
from backend.models import User
logger = logging.getLogger(__name__)
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]
async def get_users(session: AsyncSession) -> UserList:
"""Retrieve all users from the database."""
logger.debug("Entering get_users")
try:
stmt = select(User)
result = await session.scalars(stmt)
users = cast(UserList, result.all())
logger.info(f"Successfully retrieved {len(users)} users")
return users
except Exception as e:
logger.error(f"Failed to retrieve users: {str(e)}")
raise
finally:
logger.debug("Exiting get_users")
async def get_user_by_email(session: AsyncSession, email: str) -> User:
"""Retrieve a user by their email address."""
logger.debug(f"Entering get_user_by_email with email: {email}")
try:
stmt = select(User).where(User.email == email)
user = await session.scalar(stmt)
logger.info(f"Successfully retrieved user with email: {email}")
return cast(User, user)
except NoResultFound:
logger.error(f"User with email {email} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve user with email {email}: {str(e)}")
raise
finally:
logger.debug("Exiting get_user_by_email")

View File

@@ -1,357 +0,0 @@
"""SQL Database access functions for the Numinar coding project backend."""
import logging
from datetime import datetime
from typing import TypedDict
from typing import Unpack
from typing import cast
from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import Invitee
from backend.models import Room
from backend.models import User
logger = logging.getLogger(__name__)
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]
async def get_users(session: AsyncSession) -> UserList:
"""Retrieve all users from the database."""
logger.debug("Entering get_users")
try:
stmt = select(User)
result = await session.scalars(stmt)
users = cast(UserList, result.all())
logger.info(f"Successfully retrieved {len(users)} users")
return users
except Exception as e:
logger.error(f"Failed to retrieve users: {str(e)}")
raise
finally:
logger.debug("Exiting get_users")
async def get_user_by_email(session: AsyncSession, email: str) -> User:
"""Retrieve a user by their email address."""
logger.debug(f"Entering get_user_by_email with email: {email}")
try:
stmt = select(User).where(User.email == email)
user = await session.scalar(stmt)
logger.info(f"Successfully retrieved user with email: {email}")
return cast(User, user)
except NoResultFound:
logger.error(f"User with email {email} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve user with email {email}: {str(e)}")
raise
finally:
logger.debug("Exiting get_user_by_email")
async def get_rooms(session: AsyncSession) -> RoomList:
"""Retrieve all rooms from the database."""
logger.debug("Entering get_rooms")
try:
stmt = select(Room)
result = await session.scalars(stmt)
rooms = cast(RoomList, result.all())
logger.info(f"Successfully retrieved {len(rooms)} rooms")
return rooms
except Exception as e:
logger.error(f"Failed to retrieve rooms: {str(e)}")
raise
finally:
logger.debug("Exiting get_rooms")
async def get_room(session: AsyncSession, room_id: int) -> Room:
"""Retrieve a room by its ID."""
logger.debug(f"Entering get_room with room_id: {room_id}")
try:
stmt = select(Room).where(Room.id == room_id)
room = await session.scalar(stmt)
logger.info(f"Successfully retrieved room with id: {room_id}")
return cast(Room, room)
except NoResultFound:
logger.error(f"Room with id {room_id} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve room with id {room_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_room")
async def new_room(session: AsyncSession, room: Room) -> Room:
"""Create a new room in the database."""
logger.debug("Entering new_room")
try:
session.add(room)
await session.commit()
logger.info(f"Successfully created new room with id: {room.id}")
return room
except Exception as e:
logger.error(f"Failed to create new room: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting new_room")
class RoomParams(TypedDict):
"""Parameters for updating a room."""
name: str
location: str
equipment: str
capacity: int
async def update_room(
session: AsyncSession, room_id: int, **kwargs: Unpack[RoomParams]
) -> Room:
"""Update an existing room."""
logger.debug(f"Entering update_room with room_id: {room_id}, params: {kwargs}")
try:
stmt = update(Room).where(Room.id == room_id).values(**kwargs)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No room found with id {room_id} for update")
raise ValueError(f"No room found with id {room_id}")
await session.commit()
room = await get_room(session, room_id)
logger.info(f"Successfully updated room with id: {room_id}")
return room
except Exception as e:
logger.error(f"Failed to update room with id {room_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting update_room")
async def delete_room(session: AsyncSession, room_id: int) -> None:
"""Delete a room from the database."""
logger.debug(f"Entering delete_room with room_id: {room_id}")
try:
stmt = delete(Room).where(Room.id == room_id)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No room found with id {room_id} for deletion")
raise ValueError(f"No room found with id {room_id}")
await session.commit()
logger.info(f"Successfully deleted room with id: {room_id}")
except Exception as e:
logger.error(f"Failed to delete room with id {room_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting delete_room")
async def get_bookings_for_room(session: AsyncSession, room_id: int) -> BookingList:
"""Retrieve all bookings for a specific room."""
logger.debug(f"Entering get_bookings_for_room with room_id: {room_id}")
try:
stmt = select(Booking).where(Booking.room_id == room_id)
result = await session.scalars(stmt)
bookings = cast(BookingList, result.all())
logger.info(
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
)
return bookings
except Exception as e:
logger.error(f"Failed to retrieve bookings for room_id {room_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_bookings_for_room")
async def get_booking(session: AsyncSession, booking_id: int) -> Booking:
"""Retrieve a booking by its ID."""
logger.debug(f"Entering get_booking with booking_id: {booking_id}")
try:
stmt = select(Booking).where(Booking.id == booking_id)
booking = await session.scalar(stmt)
logger.info(f"Successfully retrieved booking with id: {booking_id}")
return cast(Booking, booking)
except NoResultFound:
logger.error(f"Booking with id {booking_id} not found")
raise
except Exception as e:
logger.error(f"Failed to retrieve booking with id {booking_id}: {str(e)}")
raise
finally:
logger.debug("Exiting get_booking")
async def new_booking(session: AsyncSession, booking: Booking) -> Booking:
"""Create a new booking in the database."""
logger.debug("Entering new_booking")
try:
session.add(booking)
await session.commit()
logger.info(f"Successfully created new booking with id: {booking.id}")
return booking
except Exception as e:
logger.error(f"Failed to create new booking: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting new_booking")
class BookingParams(TypedDict):
"""Parameters for updating a booking."""
room_id: int
start_time: datetime
end_time: datetime
async def update_booking(
session: AsyncSession,
booking_id: int,
**kwargs: Unpack[BookingParams],
) -> Booking:
"""Update an existing booking."""
logger.debug(
f"Entering update_booking with booking_id: {booking_id}, params: {kwargs}"
)
try:
stmt = update(Booking).where(Booking.id == booking_id).values(**kwargs)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No booking found with id {booking_id} for update")
raise ValueError(f"No booking found with id {booking_id}")
await session.commit()
booking = await get_booking(session, booking_id)
logger.info(f"Successfully updated booking with id: {booking_id}")
return booking
except Exception as e:
logger.error(f"Failed to update booking with id {booking_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting update_booking")
async def delete_booking(session: AsyncSession, booking_id: int) -> None:
"""Delete a booking from the database."""
logger.debug(f"Entering delete_booking with booking_id: {booking_id}")
try:
stmt = delete(Booking).where(Booking.id == booking_id)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(f"No booking found with id {booking_id} for deletion")
raise ValueError(f"No booking found with id {booking_id}")
await session.commit()
logger.info(f"Successfully deleted booking with id: {booking_id}")
except Exception as e:
logger.error(f"Failed to delete booking with id {booking_id}: {str(e)}")
await session.rollback()
raise
finally:
logger.debug("Exiting delete_booking")
async def get_invitees_for_booking(session: AsyncSession, booking_id: int) -> UserList:
"""Retrieve all invitees for a specific booking."""
logger.debug(f"Entering get_invitees_for_booking with booking_id: {booking_id}")
try:
stmt = select(Invitee.user).where(Invitee.booking_id == booking_id)
result = await session.scalars(stmt)
invitees = cast(UserList, result.all())
logger.info(
f"Successfully retrieved {len(invitees)} invitees for booking_id: {booking_id}"
)
return invitees
except Exception as e:
logger.error(
f"Failed to retrieve invitees for booking_id {booking_id}: {str(e)}"
)
raise
finally:
logger.debug("Exiting get_invitees_for_booking")
async def add_invitee_to_booking(
session: AsyncSession, booking_id: int, email: str
) -> Invitee:
"""Add an invitee to a booking."""
logger.debug(
f"Entering add_invitee_to_booking with booking_id: {booking_id}, email: {email}"
)
try:
user = await get_user_by_email(session, email)
invitee = Invitee(booking_id=booking_id, user_id=user.id)
session.add(invitee)
await session.commit()
logger.info(
f"Successfully added invitee with email {email} to booking_id: {booking_id}"
)
return invitee
except NoResultFound as e:
logger.error(
f"User with email {email} does not exist for booking_id {booking_id}"
)
raise ValueError(f"User with email {email} does not exist.") from e
except Exception as e:
logger.error(
f"Failed to add invitee with email {email} to booking_id {booking_id}: {str(e)}"
)
await session.rollback()
raise
finally:
logger.debug("Exiting add_invitee_to_booking")
async def remove_invitee_from_booking(
session: AsyncSession, booking_id: int, email: str
) -> None:
"""Remove an invitee from a booking."""
logger.debug(
f"Entering remove_invitee_from_booking with booking_id: {booking_id}, email: {email}"
)
try:
user = await get_user_by_email(session, email)
stmt = delete(Invitee).where(
Invitee.booking_id == booking_id, Invitee.user_id == user.id
)
result = await session.execute(stmt)
if result.rowcount == 0:
logger.error(
f"No invitee with email {email} found for booking_id {booking_id}"
)
raise ValueError(
f"No invitee with email {email} found for booking_id {booking_id}"
)
await session.commit()
logger.info(
f"Successfully removed invitee with email {email} from booking_id: {booking_id}"
)
except NoResultFound as e:
logger.error(
f"User with email {email} does not exist for booking_id {booking_id}"
)
raise ValueError(f"User with email {email} does not exist.") from e
except Exception as e:
logger.error(
f"Failed to remove invitee with email {email}"
f" from booking_id {booking_id}: {str(e)}"
)
await session.rollback()
raise
finally:
logger.debug("Exiting remove_invitee_from_booking")