lint / ruff (push) Failing after 15s
tests / desktop (push) Successful in 21s
tests / web (push) Successful in 46s
docker / image (push) Failing after 1m31s
tests / android-core (push) Successful in 1m5s
tests / backend (push) Successful in 2m33s
android / apk (push) Successful in 4m34s
163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""The timer endpoints. Thin: every decision lives in `services.pomodoro`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from app.api.deps import DbSession, Settings
|
|
from app.api.schemas import (
|
|
AbandonRequest,
|
|
AttachTaskRequest,
|
|
CompleteTimerRequest,
|
|
ExtendRequest,
|
|
StartTimerRequest,
|
|
)
|
|
from app.services import pomodoro, report, stats
|
|
from app.services.notify import count_sent, notify, record_sent
|
|
from app.timeutil import local_date
|
|
|
|
router = APIRouter(prefix="/api/timer", tags=["timer"])
|
|
|
|
|
|
@router.get("/state")
|
|
def get_state(session: DbSession, settings: Settings) -> dict:
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/start")
|
|
def start(body: StartTimerRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.start(
|
|
session,
|
|
settings,
|
|
kind=body.kind,
|
|
minutes=body.minutes,
|
|
task_id=body.task_id,
|
|
note=body.note,
|
|
)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/pause")
|
|
def pause(session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.pause(session, settings)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/resume")
|
|
def resume(session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.resume(session)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/complete")
|
|
def complete(body: CompleteTimerRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
finished, follow_on = pomodoro.complete(
|
|
session,
|
|
settings,
|
|
auto_next=body.auto_next,
|
|
expect_id=body.session_id,
|
|
)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
# `finished` is None when the named interval was no longer the running one:
|
|
# nothing happened, so there is nothing to celebrate and nothing to announce.
|
|
celebration = _maybe_celebrate(session, settings) if finished and finished.is_work else None
|
|
state = pomodoro.state(session, settings)
|
|
state["finished"] = pomodoro.session_dict(finished)
|
|
state["auto_started"] = pomodoro.session_dict(follow_on)
|
|
state["celebration"] = celebration
|
|
return state
|
|
|
|
|
|
@router.post("/abandon")
|
|
def abandon(body: AbandonRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.abandon(session, settings, reason=body.reason)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/interrupt")
|
|
def interrupt(session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.add_interruption(session)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/extend")
|
|
def extend(body: ExtendRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.extend(session, body.minutes)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.post("/task")
|
|
def attach_task(body: AttachTaskRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
pomodoro.attach_task(session, body.task_id)
|
|
except pomodoro.TimerError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return pomodoro.state(session, settings)
|
|
|
|
|
|
@router.get("/sessions")
|
|
def sessions(
|
|
session: DbSession,
|
|
settings: Settings,
|
|
day: date | None = Query(default=None, description="Local date; defaults to today."),
|
|
) -> dict:
|
|
target = day or local_date(settings.timezone)
|
|
rows = pomodoro.sessions_for_day(session, settings, target)
|
|
return {
|
|
"date": target.isoformat(),
|
|
"sessions": [pomodoro.session_dict(row) for row in rows],
|
|
"metrics": stats.day_metrics(session, settings, target),
|
|
}
|
|
|
|
|
|
def _maybe_celebrate(session, settings) -> str | None:
|
|
"""Cheer in Telegram every N pomodoros — the cheapest dopamine there is."""
|
|
every = settings.notifications.telegram.celebrate_every_n_pomodoros
|
|
if not (settings.notifications.telegram.enabled and every):
|
|
return None
|
|
|
|
done = pomodoro.completed_work_today(session, settings)
|
|
if done == 0 or done % every != 0:
|
|
return None
|
|
|
|
today = local_date(settings.timezone)
|
|
kind = f"celebrate:{done}"
|
|
if count_sent(session, kind, today):
|
|
return None
|
|
|
|
ru = settings.notifications.report.language == "ru"
|
|
goal = settings.pomodoro.daily_goal_pomodoros
|
|
bar = report.progress_bar(done, goal)
|
|
text = (
|
|
f"🎉 <b>{done}</b> помидорок готово!\n{bar} {done}/{goal}\nДержи темп."
|
|
if ru
|
|
else f"🎉 <b>{done}</b> pomodoros done!\n{bar} {done}/{goal}\nKeep the rhythm."
|
|
)
|
|
deliveries = notify(settings, text)
|
|
if any(delivery.ok for delivery in deliveries):
|
|
record_sent(session, kind, today, {"pomodoros": done})
|
|
return text
|