"""Service layer for workout session management. Handles creation, retrieval, and updates for workout sessions. A session represents a single workout on a specific date for a user. """ from datetime import date from typing import Optional import structlog from sqlmodel import Session, select from app.models.workout_session import WorkoutSession logger = structlog.get_logger(__name__) class WorkoutSessionService: """Handles CRUD operations for WorkoutSession records. Args: session: An active SQLModel Session. """ def __init__(self, session: Session) -> None: self._session = session def get_or_create_session( self, user_id: int, workout_day_id: int, session_date: date, ) -> WorkoutSession: """Get an existing session or create a new one. If a session already exists for this user + day + date combo, return it. Otherwise, create a new one. This allows logging to start automatically without an explicit "start session" step. Args: user_id: The user's ID. workout_day_id: The workout day's ID. session_date: The date of the workout. Returns: The existing or newly created WorkoutSession. """ statement = select(WorkoutSession).where( WorkoutSession.user_id == user_id, WorkoutSession.workout_day_id == workout_day_id, WorkoutSession.date == session_date, ) existing = self._session.exec(statement).first() if existing: return existing ws = WorkoutSession( user_id=user_id, workout_day_id=workout_day_id, date=session_date, ) self._session.add(ws) self._session.commit() self._session.refresh(ws) logger.info( "workout_session_created", user_id=user_id, day_id=workout_day_id, date=str(session_date), ) return ws def list_sessions( self, user_id: int, limit: int = 50, ) -> list[WorkoutSession]: """List workout sessions for a user, most recent first. Args: user_id: The user's ID. limit: Maximum number of sessions to return. Returns: List of WorkoutSession records, ordered by date descending. """ statement = ( select(WorkoutSession) .where(WorkoutSession.user_id == user_id) .order_by(WorkoutSession.date.desc()) .limit(limit) ) return list(self._session.exec(statement).all()) def get_session_by_id(self, session_id: int) -> Optional[WorkoutSession]: """Retrieve a workout session by primary key. Args: session_id: The session ID. Returns: The WorkoutSession record, or None if not found. """ return self._session.get(WorkoutSession, session_id) def update_session(self, session_id: int, **kwargs) -> WorkoutSession: """Update fields on an existing workout session. Args: session_id: The session ID. **kwargs: Field names and new values. Returns: The updated WorkoutSession record. Raises: ValueError: If the session is not found. """ ws = self.get_session_by_id(session_id) if ws is None: raise ValueError(f"WorkoutSession with id {session_id} not found") for key, value in kwargs.items(): if hasattr(ws, key): setattr(ws, key, value) self._session.add(ws) self._session.commit() self._session.refresh(ws) return ws