ingest
Read an enrichment file into stored records.
Resolution, hashing, and CSV parsing. The row -> fact convention itself lives
in entries.py; this module only locates the file and applies that convention
per row.
Module
Functions
file_content_hash
def file_content_hash(path: Path) ‑> str:The sha256 of a file's contents, read in chunks.
Arguments
path: The file to hash.
Returns The hex digest.
records_from_csv
def records_from_csv( path: Path, source: EnrichmentSource, *, task_hash: str, source_hash: str, processed_at: datetime,) ‑> list[PatientEnrichmentRecord]:Parse one enrichment CSV into stored records.
A file whose columns cannot be determined — no header to read, a parse
failure, or a configured column absent from whatever header was found —
raises EnrichmentSourceUnreadableError rather than degrading to []:
the caller replaces a source's entire stored row set with what this
returns, so conflating "unreadable" with "readable and genuinely empty"
would delete good rows on nothing more than a bad read. A file that
parses cleanly and has a valid header but zero data rows returns []
for real — that is the one case in which replacing stored rows with
nothing is correct.
A configured optional column that is present but whose values do not
resolve stays per-row lenient: that cell's field is left unset, because a
single unreadable eye or date is genuinely "unstated" rather than an
error. What is not silent is the systematic case. Unresolved values are
counted and reported once per source per ingest by _log_unresolved,
warning when no row in the file resolved — a site exporting eye as
1/2 instead of L/R would otherwise rewrite the entire source
unlateralised, delete the previously good rows, and stop every
side: study_eye criterion qualifying, while the step logged nothing but
a row count.
A row whose patient does not resolve is skipped rather than stored, and
counted the same warn-once way by _log_unresolved_identity. A fact
attributed to nobody cannot decide a criterion, and there is no safe
fallback identity to give it. Whether the ID is read from the source's own
column or derived from its name and date-of-birth columns is
EnrichmentSource's decision; this applies it per row.
Arguments
path: The resolved file to read.source: The source's config.task_hash: The step's own cache partition key.source_hash: The file's content hash, stored on every row.processed_at: The ingest timestamp.
Returns
One record per fact per readable row; [] for a readable file with
no data rows.
Raises
EnrichmentSourceUnreadableError: If the file's columns could not be determined — it could not be parsed, a required column (the identity column(s), and every value column) is absent from its header, or a configured optional column (laterality_column/measured_at_column/mrn_column) is absent from its header.
resolve_enrichment_file
def resolve_enrichment_file(file: str) ‑> pathlib.Path:Resolve a configured file name under the enrichment directory.
Arguments
file: The configured file name.
Returns The resolved absolute path.
Raises
ValueError: If the resolved path falls outside the enrichment directory, or does not name a file within it. Config names a file, never a location, so an escaping or directory-valued path is a configuration error rather than a read to attempt.
Classes
EnrichmentSourceUnreadableError
class EnrichmentSourceUnreadableError(*args, **kwargs):A source file could not be interpreted at all.
Distinct from a readable file that happens to have zero data rows
(a valid header with no rows below it): that case is genuinely empty and
records_from_csv returns [] for it, which the caller may correctly
treat as "this source now has no facts" and replace its stored rows with
nothing. This error means the opposite — the file's columns could not be
determined at all (no header to read, a parse failure, or a configured
column absent from whatever header was found) — so nothing is known
about what the source's current rows should be, and replacing the
previously stored rows with [] would silently delete good data rather
than reflect anything real about this file.