| """ |
| store.py — tiny JSON-backed persistence with a swappable interface. |
| |
| Why JSON for now: the user asked to "store what's needed when there's a need." |
| This keeps zero external dependencies. NOTE: a Hugging Face Space's disk is |
| EPHEMERAL — it resets on rebuild/restart — so this is fine for development and |
| demos, but for durable data we'll swap this layer for a real DB (Supabase, |
| Postgres, an HF Dataset, etc.) without touching the rest of the app. |
| |
| Shape: |
| { |
| "domains": [ |
| {"id", "name", "description", "system_prompt", |
| "projects": [ |
| {"id", "name", "description", |
| "milestones": [ {milestone row dicts} ]} |
| ]} |
| ], |
| "hr": {"documents": [{"id", "name", "text", "chunks": [str, ...]}]} |
| } |
| """ |
| import json |
| import os |
| import threading |
| import uuid |
|
|
| DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "..", "data")) |
| DATA_FILE = os.path.join(DATA_DIR, "store.json") |
|
|
| _lock = threading.Lock() |
| _state = None |
|
|
|
|
| def _new_id() -> str: |
| return uuid.uuid4().hex[:12] |
|
|
|
|
| def _empty_state() -> dict: |
| return {"domains": [], "hr": {"documents": []}} |
|
|
|
|
| def _load() -> dict: |
| global _state |
| if _state is not None: |
| return _state |
| try: |
| with open(DATA_FILE, "r", encoding="utf-8") as f: |
| _state = json.load(f) |
| except (FileNotFoundError, json.JSONDecodeError): |
| _state = _empty_state() |
| |
| _state.setdefault("domains", []) |
| _state.setdefault("hr", {"documents": []}) |
| return _state |
|
|
|
|
| def _save() -> None: |
| os.makedirs(DATA_DIR, exist_ok=True) |
| tmp = DATA_FILE + ".tmp" |
| with open(tmp, "w", encoding="utf-8") as f: |
| json.dump(_state, f, ensure_ascii=False, indent=2) |
| os.replace(tmp, DATA_FILE) |
|
|
|
|
| |
|
|
| def list_domains() -> list: |
| return _load()["domains"] |
|
|
|
|
| def get_domain(domain_id: str): |
| return next((d for d in _load()["domains"] if d["id"] == domain_id), None) |
|
|
|
|
| def add_domain(name: str, description: str = "", system_prompt: str = "") -> dict: |
| with _lock: |
| d = { |
| "id": _new_id(), |
| "name": name.strip(), |
| "description": description.strip(), |
| "system_prompt": system_prompt.strip(), |
| "projects": [], |
| } |
| _load()["domains"].append(d) |
| _save() |
| return d |
|
|
|
|
| def delete_domain(domain_id: str) -> bool: |
| with _lock: |
| st = _load() |
| before = len(st["domains"]) |
| st["domains"] = [d for d in st["domains"] if d["id"] != domain_id] |
| changed = len(st["domains"]) != before |
| if changed: |
| _save() |
| return changed |
|
|
|
|
| |
|
|
| def get_project(domain_id: str, project_id: str): |
| d = get_domain(domain_id) |
| if not d: |
| return None |
| return next((p for p in d["projects"] if p["id"] == project_id), None) |
|
|
|
|
| def add_project(domain_id: str, name: str, description: str = "") -> dict: |
| with _lock: |
| d = get_domain(domain_id) |
| if not d: |
| raise KeyError("domain not found") |
| p = { |
| "id": _new_id(), |
| "name": name.strip(), |
| "description": description.strip(), |
| "sheet_url": "", |
| "last_synced": None, |
| "milestones": [], |
| } |
| d["projects"].append(p) |
| _save() |
| return p |
|
|
|
|
| def delete_project(domain_id: str, project_id: str) -> bool: |
| with _lock: |
| d = get_domain(domain_id) |
| if not d: |
| return False |
| before = len(d["projects"]) |
| d["projects"] = [p for p in d["projects"] if p["id"] != project_id] |
| changed = len(d["projects"]) != before |
| if changed: |
| _save() |
| return changed |
|
|
|
|
| def set_project_milestones(domain_id: str, project_id: str, milestones: list) -> dict: |
| with _lock: |
| p = get_project(domain_id, project_id) |
| if not p: |
| raise KeyError("project not found") |
| p["milestones"] = milestones |
| _save() |
| return p |
|
|
|
|
| def update_project(domain_id: str, project_id: str, **fields) -> dict: |
| with _lock: |
| p = get_project(domain_id, project_id) |
| if not p: |
| raise KeyError("project not found") |
| p.update(fields) |
| _save() |
| return p |
|
|
|
|
| |
|
|
| def list_hr_documents() -> list: |
| return _load()["hr"]["documents"] |
|
|
|
|
| def add_hr_document(name: str, text: str, chunks: list) -> dict: |
| with _lock: |
| doc = {"id": _new_id(), "name": name, "text": text, "chunks": chunks} |
| _load()["hr"]["documents"].append(doc) |
| _save() |
| return doc |
|
|
|
|
| def delete_hr_document(doc_id: str) -> bool: |
| with _lock: |
| docs = _load()["hr"]["documents"] |
| before = len(docs) |
| _load()["hr"]["documents"] = [d for d in docs if d["id"] != doc_id] |
| changed = len(_load()["hr"]["documents"]) != before |
| if changed: |
| _save() |
| return changed |
|
|