RICS / backend /utils /tenant_store.py
StormShadow308's picture
Add demo documentation and Docker setup for v2 report generation system
aad7814
Raw
History Blame Contribute Delete
4.76 kB
"""Per-tenant on-disk layout for schema + FAISS artifacts.
Layout (under ``settings.data_dir``)::
{data_dir}/tenants/{tenant_id}/
schema.json # discovered template schema
schema_prev.json # rotated previous schema (admin override)
faiss/master/ # MASTER tier index
faiss/reference/ # REFERENCE tier index
Path segments must use stable IDs (tenant_id, section_id, draft_id, document_id).
Human section *labels* (e.g. ``Gas/Oil``, ``service and terms of engagement``) belong
only inside JSON payloads — never as directory or file names.
"""
from __future__ import annotations
import re
from pathlib import Path
from backend.config import settings
_SECTION_ID_RE = re.compile(r"^[A-Z]\d{0,2}$", re.IGNORECASE)
_FORBIDDEN_PATH_CHARS_RE = re.compile(r'[/\\:*?"<>|]')
def path_safe_label(label: str) -> str:
"""Sanitize a human label when it must appear in a path segment (prefer IDs instead)."""
s = (label or "").replace("/", "_").replace("\\", "_").replace(":", "")
s = _FORBIDDEN_PATH_CHARS_RE.sub("_", s)
s = re.sub(r"\s+", "_", s.strip())
return s[:120] or "unknown"
def path_safe_section_id(section_id: str) -> str:
"""Return a filesystem-safe RICS section code (``F2``, ``M``, ``D1``, …).
Rejects descriptive labels such as ``Gas/Oil`` that would create invalid paths on
Windows when used as directory names.
"""
sid = (section_id or "").strip()
if not sid:
raise ValueError("Empty section_id")
if "/" in sid or "\\" in sid:
raise ValueError(
f"section_id {section_id!r} looks like a label, not an ID — use e.g. F2, M, D1"
)
normalized = sid.upper().replace(" ", "")
if normalized == "UNASSIGNED":
return normalized
if _SECTION_ID_RE.fullmatch(normalized):
return normalized
raise ValueError(f"Invalid section_id for filesystem path: {section_id!r}")
def path_safe_segment(segment: str, *, fallback: str = "unknown") -> str:
"""Sanitize a generic tenant/draft/document path component."""
s = (segment or "").strip()
if not s:
return fallback
if "/" in s or "\\" in s:
s = path_safe_label(s)
else:
s = _FORBIDDEN_PATH_CHARS_RE.sub("_", s)
s = re.sub(r"\s+", "_", s)
return s[:120] or fallback
def normalize_tenant_id(tenant_id: str) -> str:
"""Stable tenant directory key under ``tenants/{id}/``."""
return path_safe_segment(tenant_id, fallback="default")
def ensure_tenant_schema(tenant_id: str):
"""Ensure ``schema.json`` exists with the canonical 14-parent RICS L3 matrix."""
from backend.core import template_discoverer
return template_discoverer.ensure_canonical_schema(normalize_tenant_id(tenant_id))
def tenant_root(tenant_id: str) -> Path:
safe_id = normalize_tenant_id(tenant_id)
root = settings.data_dir_path / "tenants" / safe_id
root.mkdir(parents=True, exist_ok=True)
return root
def schema_path(tenant_id: str) -> Path:
return tenant_root(tenant_id) / "schema.json"
def schema_prev_path(tenant_id: str) -> Path:
return tenant_root(tenant_id) / "schema_prev.json"
def faiss_dir(tenant_id: str, tier: str) -> Path:
safe_tier = path_safe_segment(tier, fallback="unknown")
d = tenant_root(tenant_id) / "faiss" / safe_tier
d.mkdir(parents=True, exist_ok=True)
return d
def scrub_audit_path(tenant_id: str) -> Path:
"""Append-only JSONL log of reference-tier PII scrubbing at ingest."""
return tenant_root(tenant_id) / "scrub_audit.jsonl"
def photo_draft_root(tenant_id: str, draft_id: str) -> Path:
"""Root folder for a report draft's section photos."""
safe_draft = path_safe_segment(draft_id, fallback="draft")
d = tenant_root(tenant_id) / "photo_drafts" / safe_draft
d.mkdir(parents=True, exist_ok=True)
return d
def photo_section_dir(tenant_id: str, draft_id: str, section_id: str) -> Path:
safe_section = path_safe_section_id(section_id)
d = photo_draft_root(tenant_id, draft_id) / safe_section
d.mkdir(parents=True, exist_ok=True)
return d
def reference_uploads_dir(tenant_id: str) -> Path:
"""Persisted copies of tenant reference uploads (for re-ingest / delete)."""
d = tenant_root(tenant_id) / "reference_uploads"
d.mkdir(parents=True, exist_ok=True)
return d
def reference_upload_path(tenant_id: str, document_id: str, suffix: str) -> Path:
"""Path for a stored reference upload — ``document_id`` only, never the filename label."""
safe_doc = path_safe_segment(document_id, fallback="document")
safe_suffix = suffix if suffix.startswith(".") else f".{suffix}"
return reference_uploads_dir(tenant_id) / f"{safe_doc}{safe_suffix}"