Spaces:
Runtime error
Runtime error
| """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}" | |