Initial commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from app.database import get_session
|
||||
from app.crud import get_sessions, end_sleep_session
|
||||
from app.schemas import (
|
||||
SessionListResponse,
|
||||
SleepSessionUpdate,
|
||||
SleepSessionRead,
|
||||
SleepSessionCreate,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/sessions", tags=["sessions"])
|
||||
|
||||
|
||||
@router.post("", response_model=SleepSessionRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_session(
|
||||
payload: SleepSessionCreate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
from app.crud import get_active_session, create_sleep_session
|
||||
|
||||
active = await get_active_session(session)
|
||||
if active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="An active sleep session is already open.",
|
||||
)
|
||||
|
||||
return await create_sleep_session(session, payload.start_time)
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
async def read_active_session(session: AsyncSession = Depends(get_session)):
|
||||
from app.crud import get_active_session
|
||||
|
||||
active = await get_active_session(session)
|
||||
return {"session": active}
|
||||
|
||||
|
||||
@router.get("", response_model=SessionListResponse)
|
||||
async def list_sessions(
|
||||
from_date: Optional[datetime] = None,
|
||||
to_date: Optional[datetime] = None,
|
||||
limit: int = 200,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
sessions, total = await get_sessions(session, from_date, to_date, limit)
|
||||
return {"sessions": sessions, "total": total}
|
||||
|
||||
|
||||
@router.patch("/{session_id}/end", response_model=None)
|
||||
async def end_session_endpoint(
|
||||
session_id: int,
|
||||
payload: SleepSessionUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
if not payload.end_time:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="end_time is required to end a session."
|
||||
)
|
||||
|
||||
result = await end_sleep_session(session, session_id, payload.end_time)
|
||||
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Session not found.")
|
||||
if result is False:
|
||||
raise HTTPException(status_code=409, detail="Session has already ended.")
|
||||
if result == "invalid":
|
||||
raise HTTPException(
|
||||
status_code=400, detail="end_time cannot be before start_time."
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_session_endpoint(
|
||||
session_id: int, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
from app.crud import delete_session
|
||||
|
||||
success = await delete_session(session, session_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Session not found.")
|
||||
return None
|
||||
|
||||
|
||||
@router.put("/{session_id}", response_model=None)
|
||||
async def update_session_endpoint(
|
||||
session_id: int,
|
||||
payload: SleepSessionUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
from app.crud import update_session
|
||||
|
||||
updated = await update_session(
|
||||
session, session_id, payload.model_dump(exclude_unset=True)
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Session not found.")
|
||||
return updated
|
||||
@@ -0,0 +1,159 @@
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from app.database import get_session
|
||||
from app.crud import get_sessions
|
||||
from app.schemas import DailySummaryResponse, TimetableResponse, WakeupBedtimeResponse
|
||||
from app.utils.time_utils import get_day_night_splits
|
||||
|
||||
router = APIRouter(prefix="/api/stats", tags=["stats"])
|
||||
|
||||
|
||||
@router.get("/daily-summary", response_model=DailySummaryResponse)
|
||||
async def get_daily_summary(
|
||||
from_date: datetime,
|
||||
to_date: datetime,
|
||||
day_start: str = Query("07:00"),
|
||||
day_end: str = Query("20:00"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
day_start_time = time.fromisoformat(day_start)
|
||||
day_end_time = time.fromisoformat(day_end)
|
||||
except ValueError:
|
||||
return {"days": []}
|
||||
|
||||
sessions, _ = await get_sessions(session, from_date=from_date, to_date=to_date)
|
||||
summary_map = {}
|
||||
|
||||
for s in sessions:
|
||||
if not s.end_time:
|
||||
continue
|
||||
segments = get_day_night_splits(
|
||||
s.start_time, s.end_time, day_start_time, day_end_time
|
||||
)
|
||||
for seg in segments:
|
||||
date_str = seg["start"].strftime("%Y-%m-%d")
|
||||
if date_str not in summary_map:
|
||||
summary_map[date_str] = {"day": 0.0, "night": 0.0}
|
||||
duration = (seg["end"] - seg["start"]).total_seconds() / 60.0
|
||||
if seg["type"] == "day":
|
||||
summary_map[date_str]["day"] += duration
|
||||
else:
|
||||
summary_map[date_str]["night"] += duration
|
||||
|
||||
days = []
|
||||
for date_str in sorted(summary_map.keys()):
|
||||
data = summary_map[date_str]
|
||||
days.append(
|
||||
{
|
||||
"date": date_str,
|
||||
"total_minutes": int(round(data["day"] + data["night"])),
|
||||
"day_minutes": int(round(data["day"])),
|
||||
"night_minutes": int(round(data["night"])),
|
||||
}
|
||||
)
|
||||
return {"days": days}
|
||||
|
||||
|
||||
@router.get("/timetable", response_model=TimetableResponse)
|
||||
async def get_timetable(
|
||||
from_date: datetime,
|
||||
to_date: datetime,
|
||||
day_start: str = Query("07:00"),
|
||||
day_end: str = Query("20:00"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
sessions, _ = await get_sessions(session, from_date=from_date, to_date=to_date)
|
||||
slots = []
|
||||
current_date = from_date.date()
|
||||
while current_date <= to_date.date():
|
||||
date_str = current_date.strftime("%Y-%m-%d")
|
||||
for i in range(96):
|
||||
slot_start_time = datetime.combine(current_date, time(0, 0)) + timedelta(
|
||||
minutes=i * 15
|
||||
)
|
||||
slot_end_time = slot_start_time + timedelta(minutes=15)
|
||||
slot_start_str = slot_start_time.strftime("%H:%M")
|
||||
slot_end_str = slot_end_time.strftime("%H:%M")
|
||||
is_sleeping = False
|
||||
for s in sessions:
|
||||
if s.start_time <= slot_end_time and (
|
||||
s.end_time is None or s.end_time >= slot_start_time
|
||||
):
|
||||
is_sleeping = True
|
||||
break
|
||||
slots.append(
|
||||
{
|
||||
"date": date_str,
|
||||
"slot_start": slot_start_str,
|
||||
"slot_end": slot_end_str,
|
||||
"sleeping": is_sleeping,
|
||||
}
|
||||
)
|
||||
current_date += timedelta(days=1)
|
||||
return {"slots": slots}
|
||||
|
||||
|
||||
@router.get("/wake-sleep-times", response_model=WakeupBedtimeResponse)
|
||||
async def get_wake_sleep_times(
|
||||
from_date: datetime,
|
||||
to_date: datetime,
|
||||
wakeup_ref: str = Query("04:00"),
|
||||
bedtime_ref: str = Query("19:00"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
w_ref_time = time.fromisoformat(wakeup_ref)
|
||||
b_ref_time = time.fromisoformat(bedtime_ref)
|
||||
except ValueError:
|
||||
return {"days": []}
|
||||
|
||||
sessions, _ = await get_sessions(session, from_date=from_date, to_date=to_date)
|
||||
days_data = []
|
||||
current_date = from_date.date()
|
||||
while current_date <= to_date.date():
|
||||
date_str = current_date.strftime("%Y-%m-%d")
|
||||
day_sessions = [
|
||||
s
|
||||
for s in sessions
|
||||
if s.start_time.date() == current_date
|
||||
or (s.end_time and s.end_time.date() == current_date)
|
||||
]
|
||||
|
||||
wakeup_min = None
|
||||
bedtime_min = None
|
||||
|
||||
if day_sessions:
|
||||
# Find first wakeup after wakeup_ref
|
||||
wakeups = []
|
||||
for s in day_sessions:
|
||||
if s.end_time and s.end_time.time() >= w_ref_time:
|
||||
diff = (
|
||||
s.end_time - datetime.combine(current_date, w_ref_time)
|
||||
).total_seconds() / 60.0
|
||||
wakeups.append(int(diff))
|
||||
if wakeups:
|
||||
wakeup_min = min(wakeups)
|
||||
|
||||
# Find last bedtime before bedtime_ref
|
||||
bedtimes = []
|
||||
for s in day_sessions:
|
||||
if s.start_time.time() <= b_ref_time:
|
||||
diff = (
|
||||
s.start_time - datetime.combine(current_date, b_ref_time)
|
||||
).total_seconds() / 60.0
|
||||
bedtimes.append(int(diff))
|
||||
if bedtimes:
|
||||
bedtime_min = max(bedtimes)
|
||||
|
||||
days_data.append(
|
||||
{
|
||||
"date": date_str,
|
||||
"wakeup_minutes": wakeup_min,
|
||||
"bedtime_minutes": bedtime_min,
|
||||
}
|
||||
)
|
||||
current_date += timedelta(days=1)
|
||||
return {"days": days_data}
|
||||
Reference in New Issue
Block a user