Fastwhisper / app /store_entries.py
Mbonea's picture
Close P1 arch-code gaps: loop summary, template-echo Rules, Move now, proof kinds.
57ed4c2
Raw
History Blame Contribute Delete
5.18 kB
"""Persist log entries as append-only JSONL with locked full rewrites.
Creation appends one line; updates and deletes rewrite the file atomically.
"""
from datetime import date, datetime, timezone
from uuid import uuid4
from app.fsutil import append_jsonl, read_jsonl, rewrite_jsonl
from app.models import CoachMeta, Entry, EntryCreate, EntryUpdate, Result
from app.paths import Paths
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
class EntryStore:
"""Read and mutate the durable entries JSONL file."""
def __init__(self, paths: Paths) -> None:
self.paths = paths
def create(self, data: EntryCreate) -> Entry:
"""Append a new entry and return the stored record."""
now = _utc_now()
entry = Entry(
id=str(uuid4()),
ts=data.ts or now,
created_at=now,
updated_at=now,
activity=data.activity,
happened=data.happened,
emotions=data.emotions,
intensity=data.intensity,
remedy=data.remedy,
result=data.result,
tags=data.tags,
notes=data.notes,
fse_spike=data.fse_spike,
avoidance_types=data.avoidance_types,
proof_brick=data.proof_brick,
walk_type=data.walk_type,
coach=CoachMeta(),
)
append_jsonl(self.paths.entries, entry.model_dump(mode="json"))
return entry
def _load(self) -> list[Entry]:
return [Entry.model_validate(record) for record in read_jsonl(self.paths.entries)]
def get(self, entry_id: str) -> Entry | None:
"""Return a single entry by id, or None when absent."""
for entry in self._load():
if entry.id == entry_id:
return entry
return None
def list(
self,
*,
start: date | None = None,
end: date | None = None,
tag: str | None = None,
result: Result | None = None,
emotion: str | None = None,
q: str | None = None,
limit: int = 50,
offset: int = 0,
) -> tuple[list[Entry], int]:
"""Return newest-first entries matching filters plus the total count."""
needle = q.lower() if q else None
matches: list[Entry] = []
for entry in self._load():
if start and entry.ts.date() < start:
continue
if end and entry.ts.date() > end:
continue
if tag and tag not in entry.tags:
continue
if result and entry.result != result:
continue
if emotion and emotion not in entry.emotions:
continue
if needle:
haystack = " ".join(
[entry.activity, entry.happened, entry.remedy, entry.notes]
).lower()
if needle not in haystack:
continue
matches.append(entry)
matches.sort(key=lambda item: item.ts, reverse=True)
total = len(matches)
page = matches[offset : offset + limit]
return page, total
def update(self, entry_id: str, patch: EntryUpdate) -> Entry | None:
"""Apply a partial update and rewrite the file, or return None."""
changes = patch.model_dump(exclude_unset=True, mode="json")
records = read_jsonl(self.paths.entries)
updated: Entry | None = None
out: list[dict] = []
for record in records:
if updated is None and record.get("id") == entry_id:
merged = {**record, **changes, "updated_at": _utc_now().isoformat()}
updated = Entry.model_validate(merged)
out.append(updated.model_dump(mode="json"))
else:
out.append(record)
if updated is None:
return None
rewrite_jsonl(self.paths.entries, out)
return updated
def delete(self, entry_id: str) -> bool:
"""Hard-delete an entry, returning whether it existed."""
records = read_jsonl(self.paths.entries)
out = [record for record in records if record.get("id") != entry_id]
if len(out) == len(records):
return False
rewrite_jsonl(self.paths.entries, out)
return True
def set_coach(self, entry_id: str, coach: CoachMeta) -> Entry | None:
"""Persist coach metadata on an entry without changing other fields."""
records = read_jsonl(self.paths.entries)
updated: Entry | None = None
out: list[dict] = []
for record in records:
if updated is None and record.get("id") == entry_id:
merged = {
**record,
"coach": coach.model_dump(mode="json"),
"updated_at": _utc_now().isoformat(),
}
updated = Entry.model_validate(merged)
out.append(updated.model_dump(mode="json"))
else:
out.append(record)
if updated is None:
return None
rewrite_jsonl(self.paths.entries, out)
return updated