Spaces:
Running
Running
| # app/api/schedule.py | |
| """ | |
| User schedule management endpoints. | |
| Allows users to set their daily availability for study time. | |
| Endpoints: | |
| GET /api/schedule β get current schedule | |
| POST /api/schedule β save/update schedule | |
| """ | |
| import logging | |
| from typing import Dict, List | |
| from fastapi import APIRouter, Depends | |
| from pydantic import BaseModel, Field, field_validator | |
| from sqlalchemy.orm import Session | |
| from app.core.database import get_db | |
| from app.core.security import get_current_user | |
| from app.models.user import User | |
| logger = logging.getLogger("hale.api.schedule") | |
| router = APIRouter(prefix="/api", tags=["Schedule"]) | |
| # ββ Schemas ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TimeBlock(BaseModel): | |
| start: str = Field(..., description="Start time in HH:MM format") | |
| end: str = Field(..., description="End time in HH:MM format") | |
| label: str = Field("Study", description="Label for this block (Study, Exercise, etc.)") | |
| def validate_time(cls, v: str) -> str: | |
| import re | |
| if not re.match(r"^\d{2}:\d{2}$", v): | |
| raise ValueError("Time must be in HH:MM format") | |
| h, m = map(int, v.split(":")) | |
| if not (0 <= h <= 23 and 0 <= m <= 59): | |
| raise ValueError("Invalid time value") | |
| return v | |
| class ScheduleRequest(BaseModel): | |
| lifestyle: str = Field( | |
| "student", | |
| description="Lifestyle type: student, job, college, freelancer, custom" | |
| ) | |
| sleep_time: str = Field("23:00", description="Bedtime in HH:MM") | |
| wake_time: str = Field("07:00", description="Wake-up time in HH:MM") | |
| blocks: Dict[str, List[TimeBlock]] = Field( | |
| default_factory=dict, | |
| description="Available blocks per weekday: {'monday': [{start, end, label}], ...}" | |
| ) | |
| def validate_lifestyle(cls, v: str) -> str: | |
| valid = {"student", "job", "college", "freelancer", "custom"} | |
| if v not in valid: | |
| raise ValueError(f"lifestyle must be one of: {', '.join(valid)}") | |
| return v | |
| # ββ Preset templates per lifestyle ββββββββββββββββββββββββββββββββββββββββ | |
| LIFESTYLE_PRESETS: Dict[str, Dict] = { | |
| "student": { | |
| "blocks": { | |
| "monday": [{"start": "18:00", "end": "20:30", "label": "Study"}], | |
| "tuesday": [{"start": "18:00", "end": "20:30", "label": "Study"}], | |
| "wednesday": [{"start": "18:00", "end": "20:30", "label": "Study"}], | |
| "thursday": [{"start": "18:00", "end": "20:30", "label": "Study"}], | |
| "friday": [{"start": "17:00", "end": "19:30", "label": "Study"}], | |
| "saturday": [{"start": "10:00", "end": "13:00", "label": "Study"}], | |
| "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], | |
| } | |
| }, | |
| "job": { | |
| "blocks": { | |
| "monday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], | |
| "tuesday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], | |
| "wednesday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], | |
| "thursday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], | |
| "friday": [{"start": "21:00", "end": "22:30", "label": "Study"}], | |
| "saturday": [{"start": "09:00", "end": "12:00", "label": "Study"}], | |
| "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], | |
| } | |
| }, | |
| "college": { | |
| "blocks": { | |
| "monday": [{"start": "16:00", "end": "19:00", "label": "Study"}], | |
| "tuesday": [{"start": "16:00", "end": "19:00", "label": "Study"}], | |
| "wednesday": [{"start": "16:00", "end": "19:00", "label": "Study"}], | |
| "thursday": [{"start": "16:00", "end": "19:00", "label": "Study"}], | |
| "friday": [{"start": "15:00", "end": "18:00", "label": "Study"}], | |
| "saturday": [{"start": "10:00", "end": "14:00", "label": "Study"}], | |
| "sunday": [{"start": "11:00", "end": "14:00", "label": "Study"}], | |
| } | |
| }, | |
| "freelancer": { | |
| "blocks": { | |
| "monday": [{"start": "09:00", "end": "11:00", "label": "Study"}], | |
| "tuesday": [{"start": "09:00", "end": "11:00", "label": "Study"}], | |
| "wednesday": [{"start": "09:00", "end": "11:00", "label": "Study"}], | |
| "thursday": [{"start": "09:00", "end": "11:00", "label": "Study"}], | |
| "friday": [{"start": "09:00", "end": "11:00", "label": "Study"}], | |
| "saturday": [{"start": "10:00", "end": "13:00", "label": "Study"}], | |
| "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], | |
| } | |
| }, | |
| } | |
| def _compute_daily_minutes(blocks_dict: Dict) -> Dict[str, int]: | |
| """Compute total study minutes per day.""" | |
| result = {} | |
| for day, blocks in blocks_dict.items(): | |
| total = 0 | |
| for b in blocks: | |
| try: | |
| sh, sm = map(int, b["start"].split(":")) | |
| eh, em = map(int, b["end"].split(":")) | |
| total += (eh * 60 + em) - (sh * 60 + sm) | |
| except Exception: | |
| pass | |
| result[day] = max(0, total) | |
| return result | |
| # ββ Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_schedule( | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| schedule = current_user.schedule_json or {} | |
| if not schedule: | |
| # Return default student preset | |
| preset = LIFESTYLE_PRESETS["student"].copy() | |
| preset["lifestyle"] = "student" | |
| preset["sleep_time"] = "23:00" | |
| preset["wake_time"] = "07:00" | |
| preset["daily_minutes"] = _compute_daily_minutes(preset["blocks"]) | |
| return {"schedule": preset, "is_default": True} | |
| daily_mins = _compute_daily_minutes(schedule.get("blocks", {})) | |
| return {"schedule": {**schedule, "daily_minutes": daily_mins}, "is_default": False} | |
| def save_schedule( | |
| payload: ScheduleRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| # Apply preset as base if blocks were not explicitly provided | |
| if "blocks" not in payload.model_fields_set and payload.lifestyle in LIFESTYLE_PRESETS: | |
| blocks = LIFESTYLE_PRESETS[payload.lifestyle]["blocks"] | |
| else: | |
| blocks = {day: [b.model_dump() for b in block_list] for day, block_list in payload.blocks.items()} | |
| schedule_data = { | |
| "lifestyle": payload.lifestyle, | |
| "sleep_time": payload.sleep_time, | |
| "wake_time": payload.wake_time, | |
| "blocks": blocks, | |
| } | |
| current_user.schedule_json = schedule_data | |
| db.commit() | |
| daily_mins = _compute_daily_minutes(blocks) | |
| logger.info("Schedule saved for %s: %s (%d days configured)", current_user.user_id, payload.lifestyle, len(blocks)) | |
| return { | |
| "message": "Schedule saved!", | |
| "schedule": {**schedule_data, "daily_minutes": daily_mins}, | |
| "avg_daily_min": sum(daily_mins.values()) // max(len(daily_mins), 1), | |
| } | |
| def get_presets(): | |
| result = {} | |
| for lifestyle, data in LIFESTYLE_PRESETS.items(): | |
| daily_mins = _compute_daily_minutes(data["blocks"]) | |
| result[lifestyle] = { | |
| "blocks": data["blocks"], | |
| "daily_minutes": daily_mins, | |
| "avg_daily_min": sum(daily_mins.values()) // 7, | |
| } | |
| return {"presets": result} | |