Initial commit

This commit is contained in:
2026-04-24 08:02:13 +02:00
commit 867c06295c
76 changed files with 10573 additions and 0 deletions
+103
View File
@@ -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
+159
View File
@@ -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}