Schema mirrors the webapp tasks module's ScheduledEvent + EventCompletion shape: - tasks.tasks (NIP-52 kind 31922 cache): (pubkey, d_tag) is the parameterized-replaceable key. JSON-encoded participants / categories / recurrence columns are decoded on read via _parse_task_row so each model can keep clean field validators. - tasks.completions (kind 31925 cache): unique on (task_address, pubkey, occurrence). occurrence is NULL for one-shot tasks; create_completion deletes any prior claim for the same triple so the latest event wins. - tasks.settings: singleton row with public_listing toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
4.4 KiB
Python
156 lines
4.4 KiB
Python
import json
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, validator
|
|
|
|
|
|
TaskStatus = Literal["claimed", "in-progress", "completed", "blocked", "cancelled"]
|
|
|
|
|
|
class Participant(BaseModel):
|
|
pubkey: str
|
|
type: str | None = None # "required" | "optional" | "organizer"
|
|
|
|
|
|
class Recurrence(BaseModel):
|
|
frequency: Literal["daily", "weekly"]
|
|
day_of_week: str | None = None # for weekly: monday..sunday
|
|
end_date: str | None = None # YYYY-MM-DD, optional cutoff
|
|
|
|
|
|
class CreateTask(BaseModel):
|
|
wallet: str | None = None # filled from caller's wallet if absent
|
|
title: str
|
|
start_date: str # YYYY-MM-DD or ISO datetime
|
|
end_date: str | None = None
|
|
description: str = ""
|
|
location: str | None = None
|
|
status: str = "pending"
|
|
event_type: str = "task" # 'task' | 'announcement'
|
|
participants: list[Participant] = Field(default_factory=list)
|
|
categories: list[str] = Field(default_factory=list)
|
|
recurrence: Recurrence | None = None
|
|
|
|
@validator("participants", pre=True)
|
|
def parse_participants(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("categories", pre=True)
|
|
def parse_categories(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("recurrence", pre=True)
|
|
def parse_recurrence(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else None
|
|
return v
|
|
|
|
|
|
class Task(BaseModel):
|
|
id: str
|
|
wallet: str
|
|
pubkey: str
|
|
d_tag: str
|
|
title: str
|
|
start_date: str
|
|
end_date: str | None = None
|
|
description: str = ""
|
|
location: str | None = None
|
|
status: str = "pending"
|
|
event_type: str = "task"
|
|
participants: list[Participant] = Field(default_factory=list)
|
|
categories: list[str] = Field(default_factory=list)
|
|
recurrence: Recurrence | None = None
|
|
nostr_event_id: str | None = None
|
|
nostr_event_created_at: int | None = None
|
|
time: datetime
|
|
|
|
@validator("participants", pre=True)
|
|
def parse_participants(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("categories", pre=True)
|
|
def parse_categories(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("recurrence", pre=True)
|
|
def parse_recurrence(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else None
|
|
return v
|
|
|
|
@property
|
|
def address(self) -> str:
|
|
"""NIP-52 replaceable-event address (31922:pubkey:d-tag)."""
|
|
return f"31922:{self.pubkey}:{self.d_tag}"
|
|
|
|
|
|
class PublicTask(BaseModel):
|
|
"""Trimmed task payload for the public/anonymous endpoint."""
|
|
|
|
id: str
|
|
pubkey: str
|
|
d_tag: str
|
|
title: str
|
|
start_date: str
|
|
end_date: str | None = None
|
|
description: str = ""
|
|
location: str | None = None
|
|
status: str
|
|
event_type: str
|
|
participants: list[Participant] = Field(default_factory=list)
|
|
categories: list[str] = Field(default_factory=list)
|
|
recurrence: Recurrence | None = None
|
|
|
|
@validator("participants", pre=True)
|
|
def parse_participants(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("categories", pre=True)
|
|
def parse_categories(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else []
|
|
return v or []
|
|
|
|
@validator("recurrence", pre=True)
|
|
def parse_recurrence(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v) if v else None
|
|
return v
|
|
|
|
|
|
class CreateTaskCompletion(BaseModel):
|
|
task_address: str # "31922:pubkey:d_tag"
|
|
task_status: TaskStatus = "claimed"
|
|
occurrence: str | None = None # YYYY-MM-DD for recurring tasks
|
|
completed_at: int | None = None # unix ts; set when task_status == 'completed'
|
|
notes: str = ""
|
|
|
|
|
|
class TaskCompletion(BaseModel):
|
|
id: str # Nostr event id (or local hash for not-yet-published)
|
|
task_address: str
|
|
pubkey: str # claimer
|
|
occurrence: str | None = None
|
|
task_status: TaskStatus = "claimed"
|
|
completed_at: int | None = None
|
|
notes: str = ""
|
|
nostr_created_at: int
|
|
time: datetime
|
|
|
|
|
|
class TasksSettings(BaseModel):
|
|
"""Extension-level settings singleton."""
|
|
|
|
public_listing: bool = False # expose /api/v1/tasks/public if enabled
|