safe_resolve let an untyped ValueError escape when a target carried an
embedded NUL: os.path.realpath rejects the string before the boundary
check runs. That broke SourceError's documented contract ("always typed,
never a leaked OSError") and the v0.3.0 promise of stable machine-readable
codes on every failure.
A target that cannot be resolved at all now fails closed under the
existing `path_escape` code, with its own message. No new code is added —
`path_escape` already means "the boundary refused this target"; its
docstring is widened to say so. Consumers asserting on the code are
unaffected.
Found by consumer review: portfolio-optimiser's navigate_bundle leaks the
same ValueError from the same class of input, which makes an entire bundle
unreadable instead of skipping one link. That defect is theirs to fix; this
commit closes the library-side half.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdVgowYC4LARgvNdNMiuvz
93 lines
3.7 KiB
Python
93 lines
3.7 KiB
Python
"""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 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
|
|
"""
|
|
|
|
|
|
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
|
|
"""
|