Spaces:
Sleeping
Sleeping
| """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 | |