init
This commit is contained in:
121 files changed
+17641
No files matched your search
@@ -0,0 +1,20 @@
|
||||
"""HTTP layer. Routers are thin; the thinking happens in `app.services`."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import (
|
||||
routes_blocker,
|
||||
routes_reports,
|
||||
routes_settings,
|
||||
routes_tasks,
|
||||
routes_timer,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(routes_timer.router)
|
||||
api_router.include_router(routes_tasks.router)
|
||||
api_router.include_router(routes_blocker.router)
|
||||
api_router.include_router(routes_reports.router)
|
||||
api_router.include_router(routes_settings.router)
|
||||
|
||||
__all__ = ["api_router"]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Shared dependencies: the database session and the optional shared secret."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import env_settings
|
||||
from app.db import get_session
|
||||
from app.services.settings_service import load_settings
|
||||
from app.settings_schema import AppSettings
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_session)]
|
||||
|
||||
|
||||
def require_token(x_api_token: Annotated[str | None, Header()] = None) -> None:
|
||||
"""Guard every API call when `API_TOKEN` is set.
|
||||
|
||||
Off by default: on a laptop the backend is bound to localhost and a token is
|
||||
friction. Set it the moment the container is reachable from anywhere else.
|
||||
"""
|
||||
expected = env_settings().api_token
|
||||
if not expected:
|
||||
return
|
||||
if x_api_token != expected:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Bad or missing X-API-Token."
|
||||
)
|
||||
|
||||
|
||||
def get_settings(session: DbSession) -> AppSettings:
|
||||
return load_settings(session)
|
||||
|
||||
|
||||
Settings = Annotated[AppSettings, Depends(get_settings)]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Blocker endpoints.
|
||||
|
||||
`GET /api/blocker/policy` is the one the desktop app polls every few seconds;
|
||||
everything else is for the UI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.api.deps import DbSession, Settings
|
||||
from app.api.schemas import BlockEventRequest
|
||||
from app.services import blocker_service
|
||||
from app.services.notify import notify
|
||||
|
||||
router = APIRouter(prefix="/api/blocker", tags=["blocker"])
|
||||
|
||||
|
||||
@router.get("/policy")
|
||||
def policy(session: DbSession, settings: Settings) -> dict:
|
||||
return blocker_service.policy(session, settings)
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
def events(
|
||||
session: DbSession,
|
||||
settings: Settings,
|
||||
day: date | None = Query(default=None),
|
||||
) -> dict:
|
||||
return blocker_service.events_for_day(session, settings, day)
|
||||
|
||||
|
||||
@router.post("/events")
|
||||
def record(body: BlockEventRequest, session: DbSession, settings: Settings) -> dict:
|
||||
"""The desktop app reports every attempt it turned away.
|
||||
|
||||
These are what make the daily report fun ("you reached for youtube 12
|
||||
times"), so they are recorded even when the blocker only warned.
|
||||
"""
|
||||
event = blocker_service.record_event(
|
||||
session,
|
||||
settings,
|
||||
kind=body.kind,
|
||||
target=body.target,
|
||||
action=body.action,
|
||||
details=body.details,
|
||||
)
|
||||
if settings.blocker.notify_on_block and body.action == "killed":
|
||||
notify(settings, f"🛑 Closed <b>{body.target}</b> — you are in a work interval.")
|
||||
return {"id": event.id, "recorded": True}
|
||||
|
||||
|
||||
@router.get("/defaults")
|
||||
def defaults() -> dict:
|
||||
"""Starter lists, so the blocker page is not an empty text box."""
|
||||
return {"domains": blocker_service.DEFAULT_DOMAINS, "apps": blocker_service.DEFAULT_APPS}
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Report and statistics endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.api.deps import DbSession, Settings
|
||||
from app.api.schemas import SendReportRequest, TestNotificationRequest
|
||||
from app.services import notify as notify_service
|
||||
from app.services import report, scheduler, stats
|
||||
from app.timeutil import ensure_utc, local_date
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["reports"])
|
||||
|
||||
|
||||
@router.get("/reports/daily")
|
||||
def daily(
|
||||
session: DbSession,
|
||||
settings: Settings,
|
||||
day: date | None = Query(default=None),
|
||||
rebuild: bool = Query(default=True),
|
||||
) -> dict:
|
||||
target = day or local_date(settings.timezone)
|
||||
row, payload = report.get_or_build_daily(session, settings, target, rebuild=rebuild)
|
||||
return {
|
||||
"report": payload,
|
||||
"sent_at": ensure_utc(row.sent_at).isoformat() if row.sent_at else None,
|
||||
"telegram_preview": report.render_daily_telegram(payload, settings),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/reports/daily/send")
|
||||
def send_daily(body: SendReportRequest, session: DbSession, settings: Settings) -> dict:
|
||||
target = body.report_date or local_date(settings.timezone)
|
||||
return report.send_daily(session, settings, target, force=body.force)
|
||||
|
||||
|
||||
@router.get("/reports/weekly")
|
||||
def weekly(
|
||||
session: DbSession,
|
||||
settings: Settings,
|
||||
end: date | None = Query(default=None),
|
||||
) -> dict:
|
||||
target = end or local_date(settings.timezone)
|
||||
payload = report.build_weekly(session, settings, target)
|
||||
return {"report": payload, "telegram_preview": report.render_weekly_telegram(payload, settings)}
|
||||
|
||||
|
||||
@router.post("/reports/weekly/send")
|
||||
def send_weekly(session: DbSession, settings: Settings) -> dict:
|
||||
payload = report.build_weekly(session, settings, local_date(settings.timezone))
|
||||
text = report.render_weekly_telegram(payload, settings)
|
||||
deliveries = notify_service.notify(settings, text, urgent=True)
|
||||
return {"sent": any(d.ok for d in deliveries), "deliveries": [d.as_dict() for d in deliveries]}
|
||||
|
||||
|
||||
@router.post("/reports/briefing/send")
|
||||
def send_briefing(session: DbSession, settings: Settings) -> dict:
|
||||
text = report.render_morning_briefing(session, settings)
|
||||
deliveries = notify_service.notify(settings, text, urgent=True)
|
||||
return {
|
||||
"sent": any(d.ok for d in deliveries),
|
||||
"deliveries": [d.as_dict() for d in deliveries],
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats/summary")
|
||||
def summary(session: DbSession, settings: Settings, days: int = Query(default=30, ge=1, le=365)) -> dict:
|
||||
today = local_date(settings.timezone)
|
||||
return {
|
||||
"today": stats.day_metrics(session, settings, today),
|
||||
"streak": stats.streak(session, settings),
|
||||
"lifetime": stats.lifetime(session, settings),
|
||||
"average_7d": stats.averages(session, settings, 7, until=today),
|
||||
"range": stats.range_metrics(session, settings, today - timedelta(days=days - 1), today),
|
||||
"heatmap": stats.heatmap(session, settings, weeks=12),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats/day")
|
||||
def day_stats(session: DbSession, settings: Settings, day: date | None = Query(default=None)) -> dict:
|
||||
return stats.day_metrics(session, settings, day or local_date(settings.timezone))
|
||||
|
||||
|
||||
@router.post("/notifications/test")
|
||||
def test_notification(body: TestNotificationRequest, settings: Settings) -> dict:
|
||||
text = body.text or (
|
||||
"🍅 <b>Kadence</b>\nNotifications are wired up correctly."
|
||||
)
|
||||
deliveries = notify_service.notify(settings, text, urgent=True)
|
||||
return {"deliveries": [d.as_dict() for d in deliveries]}
|
||||
|
||||
|
||||
@router.get("/scheduler/jobs")
|
||||
def scheduler_jobs() -> dict:
|
||||
"""What the background jobs are, and when they next fire."""
|
||||
return {"jobs": scheduler.describe()}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Settings endpoints.
|
||||
|
||||
The browser never sees a real token: `GET` masks every secret, and a `PUT` that
|
||||
sends the mask back untouched keeps the stored value.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.deps import DbSession, Settings
|
||||
from app.services import blocker_service, settings_service
|
||||
from app.settings_schema import AppSettings
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_settings(settings: Settings) -> dict:
|
||||
return {
|
||||
"settings": settings_service.mask_secrets(settings.model_dump(mode="json")),
|
||||
"defaults": {
|
||||
"blocked_domains": blocker_service.DEFAULT_DOMAINS,
|
||||
"blocked_apps": blocker_service.DEFAULT_APPS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.put("")
|
||||
def put_settings(session: DbSession, patch: dict[str, Any] = Body(...)) -> dict:
|
||||
try:
|
||||
updated = settings_service.apply_patch(session, patch)
|
||||
except ValidationError as exc:
|
||||
# `exc.errors()` can carry exception objects in `ctx`, which JSON cannot
|
||||
# take; pydantic's own `json()` renders them as text.
|
||||
raise HTTPException(status_code=422, detail=json.loads(exc.json())) from exc
|
||||
return {"settings": settings_service.mask_secrets(updated.model_dump(mode="json"))}
|
||||
|
||||
|
||||
@router.post("/reset")
|
||||
def reset_settings(session: DbSession) -> dict:
|
||||
"""Back to defaults, re-seeded from the environment."""
|
||||
updated = settings_service.save_settings(session, settings_service.settings_from_env())
|
||||
return {"settings": settings_service.mask_secrets(updated.model_dump(mode="json"))}
|
||||
|
||||
|
||||
@router.get("/schema")
|
||||
def settings_schema() -> dict:
|
||||
"""The JSON Schema, so a client can render the settings form generically."""
|
||||
return AppSettings.model_json_schema()
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Task endpoints: the aggregated list, refreshing it, and acting on it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from app.api.deps import DbSession, Settings
|
||||
from app.api.schemas import TaskCreateRequest, TaskDoneRequest, TaskUpdateRequest
|
||||
from app.models import TaskRecord
|
||||
from app.services import tasks_service
|
||||
from app.sources.registry import SOURCE_LABELS, check_source
|
||||
from app.timeutil import ensure_utc, local_date
|
||||
|
||||
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
|
||||
|
||||
|
||||
def task_dict(record: TaskRecord, today) -> dict:
|
||||
return {
|
||||
"id": record.id,
|
||||
"source": record.source,
|
||||
"source_label": SOURCE_LABELS.get(record.source, record.source),
|
||||
"external_id": record.external_id,
|
||||
"title": record.title,
|
||||
"description": record.description,
|
||||
"url": record.url,
|
||||
"project": record.project,
|
||||
"status": record.status,
|
||||
"priority": record.priority,
|
||||
"due_date": record.due_date.isoformat() if record.due_date else None,
|
||||
"start_date": record.start_date.isoformat() if record.start_date else None,
|
||||
"labels": record.labels or [],
|
||||
"is_done": record.is_done,
|
||||
"completed_at": ensure_utc(record.completed_at).isoformat() if record.completed_at else None,
|
||||
"pinned": record.pinned,
|
||||
"stale": record.stale,
|
||||
"estimated_pomodoros": record.estimated_pomodoros,
|
||||
"days_overdue": (today - record.due_date).days if record.due_date and record.due_date < today else 0,
|
||||
"bucket": _bucket_of(record, today),
|
||||
}
|
||||
|
||||
|
||||
def _bucket_of(record: TaskRecord, today) -> str:
|
||||
if record.due_date is None:
|
||||
return "someday"
|
||||
if record.due_date < today:
|
||||
return "overdue"
|
||||
if record.due_date == today:
|
||||
return "today"
|
||||
return "upcoming"
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_tasks(
|
||||
session: DbSession,
|
||||
settings: Settings,
|
||||
include_done: bool = Query(default=False),
|
||||
include_stale: bool = Query(default=False),
|
||||
source: list[str] | None = Query(default=None),
|
||||
) -> dict:
|
||||
today = local_date(settings.timezone)
|
||||
records = tasks_service.list_tasks(
|
||||
session, include_done=include_done, include_stale=include_stale, sources=source
|
||||
)
|
||||
buckets = tasks_service.bucket_tasks(records, today)
|
||||
return {
|
||||
"date": today.isoformat(),
|
||||
"tasks": [task_dict(record, today) for record in records],
|
||||
"counts": {name: len(items) for name, items in buckets.items()},
|
||||
"sources": sorted({record.source for record in records}),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
def refresh(session: DbSession, settings: Settings) -> dict:
|
||||
"""Pull every enabled source now. Failures come back per source, not as a 500."""
|
||||
return tasks_service.refresh_tasks(session, settings)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create(body: TaskCreateRequest, session: DbSession, settings: Settings) -> dict:
|
||||
record = tasks_service.create_local_task(
|
||||
session,
|
||||
title=body.title,
|
||||
description=body.description,
|
||||
due_date=body.due_date,
|
||||
priority=body.priority,
|
||||
project=body.project,
|
||||
estimated_pomodoros=body.estimated_pomodoros,
|
||||
)
|
||||
return task_dict(record, local_date(settings.timezone))
|
||||
|
||||
|
||||
@router.patch("/{task_id}")
|
||||
def update(task_id: int, body: TaskUpdateRequest, session: DbSession, settings: Settings) -> dict:
|
||||
try:
|
||||
record = tasks_service.update_task(session, task_id, **body.model_dump(exclude_unset=True))
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return task_dict(record, local_date(settings.timezone))
|
||||
|
||||
|
||||
@router.post("/{task_id}/done")
|
||||
def mark_done(task_id: int, body: TaskDoneRequest, session: DbSession, settings: Settings) -> dict:
|
||||
try:
|
||||
record, warning = tasks_service.complete_task(session, settings, task_id, done=body.done)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return {"task": task_dict(record, local_date(settings.timezone)), "warning": warning}
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
def delete(task_id: int, session: DbSession) -> dict:
|
||||
tasks_service.delete_task(session, task_id)
|
||||
return {"deleted": task_id}
|
||||
|
||||
|
||||
@router.post("/sources/{name}/test")
|
||||
def test_source(name: str, settings: Settings) -> dict:
|
||||
"""What the settings page's "Test connection" button calls."""
|
||||
ok, detail = check_source(settings, name)
|
||||
return {"source": name, "ok": ok, "detail": detail}
|
||||
@@ -0,0 +1,155 @@
|
||||
"""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)
|
||||
except pomodoro.TimerError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
celebration = _maybe_celebrate(session, settings) if 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
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Request bodies. Responses are plain dicts built by the services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class StartTimerRequest(BaseModel):
|
||||
kind: Literal["work", "short_break", "long_break"] | None = None
|
||||
minutes: int | None = Field(default=None, ge=1, le=240)
|
||||
task_id: int | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class AbandonRequest(BaseModel):
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class ExtendRequest(BaseModel):
|
||||
minutes: int = Field(default=5, ge=1, le=120)
|
||||
|
||||
|
||||
class AttachTaskRequest(BaseModel):
|
||||
task_id: int | None = None
|
||||
|
||||
|
||||
class CompleteTimerRequest(BaseModel):
|
||||
auto_next: bool = True
|
||||
|
||||
|
||||
class TaskCreateRequest(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=500)
|
||||
description: str | None = None
|
||||
due_date: date | None = None
|
||||
priority: str | None = None
|
||||
project: str | None = None
|
||||
estimated_pomodoros: int | None = Field(default=None, ge=1, le=50)
|
||||
|
||||
|
||||
class TaskUpdateRequest(BaseModel):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
due_date: date | None = None
|
||||
priority: str | None = None
|
||||
project: str | None = None
|
||||
estimated_pomodoros: int | None = None
|
||||
pinned: bool | None = None
|
||||
|
||||
|
||||
class TaskDoneRequest(BaseModel):
|
||||
done: bool = True
|
||||
|
||||
|
||||
class BlockEventRequest(BaseModel):
|
||||
kind: Literal["domain", "app"] = "domain"
|
||||
target: str = Field(min_length=1, max_length=255)
|
||||
action: Literal["blocked", "killed", "warned"] = "blocked"
|
||||
details: str | None = None
|
||||
|
||||
|
||||
class SendReportRequest(BaseModel):
|
||||
report_date: date | None = None
|
||||
force: bool = True
|
||||
|
||||
|
||||
class TestNotificationRequest(BaseModel):
|
||||
text: str | None = None
|
||||
|
||||
|
||||
class SettingsPatch(BaseModel):
|
||||
"""A partial settings document; anything omitted keeps its stored value."""
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return self.model_dump(exclude_unset=True)
|
||||
Reference in new issue
Block a user