codebook / potato /memos /service.py
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
3.67 kB
"""
Memo service (universal) — visibility + permission rules over the store.
Visibility model (decided 2026-05-18):
- ``private`` (default): visible to the author and to admins/adjudicators.
- ``shared``: visible to the author, admins/adjudicators, AND peer
annotators on the same project.
- Admins/adjudicators can ALWAYS read every memo regardless of setting.
Permissions:
- Read: per the visibility rule above.
- Edit (body/visibility): author only.
- Delete: author OR an admin/adjudicator (moderation).
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from . import store
VALID_VISIBILITY = ("private", "shared")
class MemoError(Exception):
"""Base error for memo operations (maps to 4xx at the API layer)."""
class MemoNotFound(MemoError):
pass
class MemoPermissionError(MemoError):
pass
def _can_read(memo: Dict[str, Any], requester: str, is_privileged: bool) -> bool:
return (
is_privileged
or memo["created_by"] == requester
or memo["visibility"] == "shared"
)
def create_memo(
task_dir: str,
*,
project: str,
instance_id: str,
body: str,
created_by: str,
anchor: Optional[Dict[str, Any]] = None,
visibility: str = "private",
) -> Dict[str, Any]:
body = (body or "").strip()
if not body:
raise MemoError("Memo body must not be empty")
if visibility not in VALID_VISIBILITY:
raise MemoError(
f"visibility must be one of {VALID_VISIBILITY} (got {visibility!r})"
)
if anchor is not None:
if not isinstance(anchor, dict) or "start" not in anchor or "end" not in anchor:
raise MemoError("anchor must be {start, end[, field]} or null")
return store.create(
task_dir, project=project, instance_id=instance_id, body=body,
created_by=created_by, anchor=anchor, visibility=visibility,
)
def list_visible(
task_dir: str,
*,
project: str,
instance_id: str,
requester: str,
is_privileged: bool = False,
) -> List[Dict[str, Any]]:
"""Memos on an instance the requester is allowed to see."""
return [
m for m in store.list_for_instance(task_dir, project, instance_id)
if _can_read(m, requester, is_privileged)
]
def _load_or_404(task_dir: str, memo_id: str) -> Dict[str, Any]:
memo = store.get(task_dir, memo_id)
if memo is None:
raise MemoNotFound(f"Memo {memo_id} not found")
return memo
def update_memo(
task_dir: str,
memo_id: str,
*,
requester: str,
is_privileged: bool = False,
body: Optional[str] = None,
visibility: Optional[str] = None,
) -> Dict[str, Any]:
memo = _load_or_404(task_dir, memo_id)
if memo["created_by"] != requester:
raise MemoPermissionError("Only the memo author may edit it")
if body is not None and not body.strip():
raise MemoError("Memo body must not be empty")
if visibility is not None and visibility not in VALID_VISIBILITY:
raise MemoError(
f"visibility must be one of {VALID_VISIBILITY} (got {visibility!r})"
)
return store.update(
task_dir, memo_id,
body=body.strip() if body is not None else None,
visibility=visibility,
)
def delete_memo(
task_dir: str,
memo_id: str,
*,
requester: str,
is_privileged: bool = False,
) -> None:
memo = _load_or_404(task_dir, memo_id)
if memo["created_by"] != requester and not is_privileged:
raise MemoPermissionError(
"Only the author or an admin/adjudicator may delete this memo"
)
store.delete(task_dir, memo_id)