hale-api / app /api /schedule.py
Raunak211006's picture
Deploy HALE API v3.1
cc38116 verified
Raw
History Blame Contribute Delete
8.42 kB
# 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.)")
@field_validator("start", "end")
@classmethod
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}], ...}"
)
@field_validator("lifestyle")
@classmethod
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 ──────────────────────────────────────────────────────────────
@router.get("/schedule", summary="Get user's current schedule")
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}
@router.post("/schedule", summary="Save user's study schedule")
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),
}
@router.get("/schedule/presets", summary="Get lifestyle schedule presets")
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}