first commit
This commit is contained in:
197
src/gads_v2/reminders.py
Normal file
197
src/gads_v2/reminders.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from .config import ROOT, client_dir
|
||||
from .history import now_local
|
||||
from .table import print_table
|
||||
|
||||
|
||||
GLOBAL_REMINDERS_PATH = ROOT / "reminders.jsonl"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Reminder:
|
||||
id: str
|
||||
created_at: str
|
||||
due_date: str
|
||||
text: str
|
||||
client: str
|
||||
status: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Reminder":
|
||||
return cls(
|
||||
id=str(data.get("id", "")),
|
||||
created_at=str(data.get("created_at", "")),
|
||||
due_date=str(data.get("due_date", "")),
|
||||
text=str(data.get("text", "")),
|
||||
client=str(data.get("client", "")),
|
||||
status=str(data.get("status", "active")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"created_at": self.created_at,
|
||||
"due_date": self.due_date,
|
||||
"text": self.text,
|
||||
"client": self.client,
|
||||
"status": self.status,
|
||||
}
|
||||
|
||||
|
||||
def normalize_text(value: str) -> str:
|
||||
normalized = unicodedata.normalize("NFKD", value)
|
||||
without_marks = "".join(ch for ch in normalized if not unicodedata.combining(ch))
|
||||
return without_marks.lower()
|
||||
|
||||
|
||||
def reminder_path(domain: str | None = None) -> Path:
|
||||
if domain:
|
||||
base = client_dir(domain)
|
||||
return base / "reminders.jsonl"
|
||||
return GLOBAL_REMINDERS_PATH
|
||||
|
||||
|
||||
def load_reminders(domain: str | None = None, include_global: bool = False) -> list[Reminder]:
|
||||
paths = []
|
||||
if include_global:
|
||||
paths.append(GLOBAL_REMINDERS_PATH)
|
||||
paths.append(reminder_path(domain))
|
||||
reminders: list[Reminder] = []
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
reminders.append(Reminder.from_dict(json.loads(line)))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
reminders.sort(key=lambda item: (item.due_date, item.created_at, item.id))
|
||||
return reminders
|
||||
|
||||
|
||||
def parse_reminder_text(raw_text: str) -> tuple[date, str]:
|
||||
text = " ".join(raw_text.split()).strip()
|
||||
if not text:
|
||||
raise ValueError("Brak tresci przypomnienia.")
|
||||
|
||||
today = now_local().date()
|
||||
normalized = normalize_text(text)
|
||||
due = today
|
||||
matched_prefix = ""
|
||||
|
||||
patterns = [
|
||||
(r"^za\s+(\d+)\s+dni(?:\s+|$)", "days"),
|
||||
(r"^za\s+(\d+)\s+dzien(?:\s+|$)", "days"),
|
||||
(r"^za\s+(\d+)\s+tygodnie(?:\s+|$)", "weeks"),
|
||||
(r"^za\s+(\d+)\s+tygodni(?:\s+|$)", "weeks"),
|
||||
(r"^za\s+(\d+)\s+tydzien(?:\s+|$)", "weeks"),
|
||||
(r"^za\s+(\d+)\s+miesiace(?:\s+|$)", "months"),
|
||||
(r"^za\s+(\d+)\s+miesiecy(?:\s+|$)", "months"),
|
||||
(r"^za\s+(\d+)\s+miesiac(?:\s+|$)", "months"),
|
||||
]
|
||||
for pattern, unit in patterns:
|
||||
match = re.search(pattern, normalized)
|
||||
if not match:
|
||||
continue
|
||||
amount = int(match.group(1))
|
||||
if unit == "days":
|
||||
due = today + timedelta(days=amount)
|
||||
elif unit == "weeks":
|
||||
due = today + timedelta(weeks=amount)
|
||||
else:
|
||||
due = today + timedelta(days=amount * 30)
|
||||
matched_prefix = text[: match.end()].strip()
|
||||
break
|
||||
|
||||
if not matched_prefix:
|
||||
if normalized.startswith("jutro"):
|
||||
due = today + timedelta(days=1)
|
||||
matched_prefix = text[:5].strip()
|
||||
elif normalized.startswith("dzisiaj"):
|
||||
due = today
|
||||
matched_prefix = text[:7].strip()
|
||||
|
||||
note = text[len(matched_prefix) :].strip() if matched_prefix else text
|
||||
note_normalized = normalize_text(note)
|
||||
for prefix in [
|
||||
"przypomnij mi o ",
|
||||
"przypomnij o ",
|
||||
"przypomnienie o ",
|
||||
"o ",
|
||||
]:
|
||||
if note_normalized.startswith(prefix):
|
||||
note = note[len(prefix) :].strip()
|
||||
break
|
||||
if not note:
|
||||
note = text
|
||||
return due, note
|
||||
|
||||
|
||||
def add_reminder(raw_text: str, domain: str | None = None) -> Reminder:
|
||||
due, note = parse_reminder_text(raw_text)
|
||||
ts = now_local()
|
||||
reminder = Reminder(
|
||||
id=f"rem_{ts.strftime('%Y%m%d%H%M%S')}",
|
||||
created_at=ts.isoformat(timespec="seconds"),
|
||||
due_date=due.isoformat(),
|
||||
text=note,
|
||||
client=domain or "",
|
||||
status="active",
|
||||
)
|
||||
path = reminder_path(domain)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(reminder.to_dict(), ensure_ascii=False) + "\n")
|
||||
return reminder
|
||||
|
||||
|
||||
def active_reminders_for_client(domain: str) -> list[Reminder]:
|
||||
return [item for item in load_reminders(domain, include_global=True) if item.status == "active"]
|
||||
|
||||
|
||||
def reminder_status_label(reminder: Reminder) -> str:
|
||||
today = now_local().date()
|
||||
try:
|
||||
due = date.fromisoformat(reminder.due_date)
|
||||
except ValueError:
|
||||
return "brak daty"
|
||||
days = (due - today).days
|
||||
if days < 0:
|
||||
return f"po terminie {abs(days)} dni"
|
||||
if days == 0:
|
||||
return "dzisiaj"
|
||||
if days == 1:
|
||||
return "jutro"
|
||||
return f"za {days} dni"
|
||||
|
||||
|
||||
def print_client_reminders(domain: str, limit: int = 12) -> None:
|
||||
reminders = active_reminders_for_client(domain)
|
||||
if not reminders:
|
||||
return
|
||||
print("\nPrzypomnienia")
|
||||
shown = reminders[:limit]
|
||||
print_table(
|
||||
["Termin", "Status", "Zakres", "Notatka"],
|
||||
[
|
||||
[
|
||||
reminder.due_date,
|
||||
reminder_status_label(reminder),
|
||||
reminder.client or "globalne",
|
||||
reminder.text,
|
||||
]
|
||||
for reminder in shown
|
||||
],
|
||||
)
|
||||
if len(reminders) > len(shown):
|
||||
print(f"... oraz {len(reminders) - len(shown)} kolejnych przypomnien")
|
||||
Reference in New Issue
Block a user