cadrumo.application.aggregation.observation_window module¶
Shared set-replace algorithm for per-(modelo, filing_year, period) observation stores.
The percepciones (Modelo 190) and retención (Modelo 180/193) per-perceptor
observation repositories each persist a distinct row type keyed by
(modelo, filing_year, period) plus a per-record identity, and each needs
the exact same SET-REPLACE discipline on a re-pull: clear every prior row for
the window, then write the supplied set. Leaving the stale window behind (an
additive upsert instead of a set-replace) inflates the next calculate’s
distinct count with a record the operator no longer declares — a silent
over-count. This module holds that one algorithm so
PercepcionObservationRepository
and RetencionObservationRepository
both call it instead of each carrying their own copy of the loop.
It also holds hashed_tax_id_token(), the one normalise-then-hash step
both repositories’ opaque object-key builders need. The two stores stay
intentionally distinct (different distinct keys, different models — see each
module’s own docstring), but this single identity-hashing primitive is
identical between them and belongs here rather than duplicated per file.
- hashed_tax_id_token(tax_id, *, field_name)[source]¶
Return the sha256 of a tax id’s canonical identity token.
Normalises through the same
tax_id_identity_token()each observation model applies on construction, so an object key and its aggregation grouping key derive from one identity. The normalisation is idempotent, so an observation-sourced tax id passes through unchanged; the blank guard remains as a defence for callers that key without an observation.- Parameters:
tax_id (
str) – The perceptor’s tax id, in any of the formstax_id_identity_token()accepts.field_name (
str) – The field name to report in the blank-token refusal, so each caller’s diagnostic names its own parameter.
- Return type:
This stays package-private on purpose. The live IVA surface composes the same two steps for its own subject ref and deliberately does not call this. The two callers want OPPOSITE blank behaviour: refusing is required here, where a perceptor with no identifier cannot be keyed, and wrong there, where a subjectless row is a legitimate domain value that must still project. A shared function cannot both raise and not raise, so that caller would guard blank before calling and consume only the hash.
The return width is the second reason and the sharper one: this returns the full digest, while the live ref is truncated, and a truncation width IS an identity contract – two refs compare equal only if every producer cuts them identically. A shared function returning full hex leaves that caller cutting locally anyway.
What is left to share is a single expression over
tax_id_identity_token(), which is already the one identity authority both go through.Note for anyone re-opening this: the domain-neutral refusal a relocation would need already exists –
IdentityErrorwitherrors.identity.document_empty, shipped in all four catalogues. So “the error would leak retenciones vocabulary” is NOT a reason to keep these apart, and was wrongly given as one when this note was first written. The reasons are the opposite blank contracts and the width.- Raises:
AggregationValidationError –
tax_idnormalises to a blank token.- Parameters:
- Return type:
- class ObservationWindowRepository(*args, **kwargs)[source]¶
-
Required application capability for the set-replace window algorithm.
- validate_observation_window_modelo(modelo)[source]¶
Validate the logical window owner at the outer persistence boundary.
- extract_identifier(payload)[source]¶
Return the natural identifier for one persisted payload.
- Return type:
str
- Parameters:
payload (PayloadT)
- replace_observation_window(repository, *, modelo, filing_year, period, observations, source_kind, build_payload, captured_at=None, source_metadata=None)[source]¶
Replace the FULL observation set for one (modelo, filing_year, period) window.
SET-REPLACE, not additive upsert: every prior row for the exact key-tuple on
repositoryis cleared and the suppliedobservationsare written in its place. An emptyobservationsclears the window, matching each repository’s own no-silent-under-declaration contract on the caller side.The clear and the write commit as ONE transaction through the repository port’s
replace_recordsoperation. Deleting each stale row and then saving each replacement one at a time made every intermediate state observable: a failure part-way through the write loop left the window holding neither the old declared set nor the new one, and the next calculate read that partial window as the operator’s declared truth — a silent under-count with the evidence for it already destroyed.- Parameters:
repository (
ObservationWindowRepository[TypeVar(PayloadT, bound=BaseModel)]) – The window’s encrypted store.modelo (
str) – Modelo id owning the window.filing_year (
int) – Filing year owning the window.period (
Period) – Period owning the window.observations (
Sequence[TypeVar(ObservationT)]) – The full replacement set (possibly empty).source_kind (
str) – Capture provenance recorded on every written row.build_payload (
Callable[...,TypeVar(PayloadT, bound=BaseModel)]) – Each repository’s own typed envelope-payload builder. Returns the payload rather than persisting it, so every row is constructed and validated before any of them is committed.captured_at (
datetime|None) – Capture instant stamped on every written row; defaults to now, resolved once so the whole set shares one instant.source_metadata (
Mapping[str,str] |None) – Capture metadata recorded on every written row.
- Return type: