"""Typed error hierarchy rooted in IngestError. STABILITY CONTRACT: `IngestError.code` is the machine-readable API for distinguishing sub-causes — consumers assert on it, never on message wording. Codes listed in the class docstrings below are stable across releases; message text is NOT stable and may improve freely. """ from __future__ import annotations class IngestError(Exception): """Base class for every error raised by this library. Carries a stable, machine-readable `code` naming the sub-cause (see the subclass docstrings for the registry). Errors constructed without an explicit code carry `"unspecified"`. """ def __init__(self, message: str, *, code: str = "unspecified") -> None: super().__init__(message) self.code = code class ManifestError(IngestError): """The manifest failed fail-fast schema validation (ingest-spec §4). Codes: - `manifest_unreadable` — the manifest file cannot be read - `manifest_invalid_json` — the bytes are not valid UTF-8 JSON - `manifest_version_unsupported` — manifest_version is not the integer 1 - `manifest_schema` — a generic shape violation (wrong type, missing or unknown field, empty string, bad identifier, multi-line title, non-positive max_rows, empty extractions list) - `source_type_unknown` — source.type is not 'file', 'sql', or 'http' - `credential_embedded` — source.base_url embeds userinfo credentials - `extraction_id_duplicate` — two extractions share an id - `okf_type_reserved` — an extraction claims the reserved 'verdict' layer """ class RenderError(IngestError): """A value cannot be rendered under the §5 body rules (never silent coercion). Codes: - `unsupported_cell_type` — a SQL cell is not integer/float/text/NULL """ class SourceError(IngestError): """An extraction failed against its source (ingest-spec §4, §8). Covers fail-closed path-boundary violations, missing/malformed source content, and max_rows cap violations — always typed, never a leaked OSError and never silent truncation. Codes: - `path_escape` — a path resolves outside its root directory, or cannot be resolved at all (e.g. an embedded NUL byte) - `source_root_missing` — the file-source root is not a directory - `source_file_missing` — the query does not resolve to a file - `csv_no_header` — the CSV has no header row - `csv_ragged_row` — a CSV row's cell count differs from the header's - `max_rows_exceeded` — an extraction exceeds max_rows (any source type) - `connection_ref_unset` — the sql connection_ref env var is not set - `database_missing` — the sql connection_ref points at a missing file - `sql_no_columns` — the sql statement produced no result columns - `sql_failed` — the sql statement failed at the database - `credential_ref_unset` — the http credential_ref env var is not set - `http_transport` — the http GET failed at transport or decode - `fence_marker_in_body` — an http body line is a code-fence marker """ class ExtractionError(IngestError): """A dropped file could not be converted to text (Door B, Phase 2). File-type -> text extraction is text-only plumbing; a corrupt file, an unknown type, or a binary type without its optional parser is a typed per-file failure — never a silent skip, never a leaked stdlib error, and never a bundled parser in core. Codes: - `extractor_unknown` — no extractor is registered for the file extension - `extractor_extra_missing` — a `[extract]`-gated binary type (pdf/docx/ xlsx) was given but the optional extra is not installed - `extractor_decode_error` — a text-type file's bytes are not valid UTF-8 - `extractor_empty_csv` — a CSV has no header row """ class MaterializationError(IngestError): """Materialization refused or failed (ingest-spec §5). Codes: - `ingested_at_invalid` — ingested_at is not ISO-8601 UTC with a Z suffix - `collision_unstamped` — the §3 collision gate: a generated name is occupied by a file without the ingest stamp - `inbox_slug_empty` — a dropped file's name reduces to an empty slug under the id grammar (Door B; never an invented fallback name) - `inbox_slug_too_long` — the generated inbox filename would exceed the 255-byte filesystem limit (Door B; never a truncated name, which would be lossy and could collide with another long name sharing its prefix) - `inbox_slug_collision` — two files dropped in the same run reduce to one generated filename (Door B); both are refused rather than letting iteration order decide which one survives - `inbox_title_invalid` — an inbox title is multi-line or contains `[`/`]`, either of which would break frontmatter or an index link - `inbox_source_file_invalid` — an inbox `source_file` is multi-line and would inject frontmatter lines - `okf_type_reserved` — an inbox concept claims the reserved 'verdict' layer (the same reservation ManifestError enforces at Door A) - `import_path_empty` — an external concept path reduces to an empty slug under the id grammar (Door C; never an invented fallback name) - `import_path_too_long` — the generated import filename would exceed the 255-byte filesystem limit (Door C; never a truncated name) - `import_slug_collision` — two concepts in one external bundle reduce to one generated filename (Door C); both are refused rather than letting iteration order decide which one survives - `import_label_invalid` — an external concept path contains `[`/`]`, which would break its index link (the guard's path gate permits them) - `import_provenance_invalid` — an `origin`/`channel` outside the guard's pinned vocabulary (Door C); refused rather than carried, because the guard derives trust from `origin` by enum identity and an unrecognised value would be silently downgraded """ class NetworkGateError(IngestError): """A network source was used without the per-run opt-in flag (spec §8). Local-only default, no silent egress: the flag is a run argument — the manifest cannot grant itself network access. Codes: - `network_opt_in_missing` — an http source without allow_network=True """