aeat.adapters.outbound.google._calc_sheets_pull module

Read operator-edited Sheets cells back into structured records.

Pairs with _calc_sheets_apply. The export side materialises a SheetExportPlan as a real Google Sheets workbook; this module reads the operator’s edits back out, validates the workbook is still bound to the RegistrySnapshot the engine compiled it from, and returns typed records the caller can inspect, compute from, or assemble into ledger / filing inputs.

Two safety gates fire before any value is read:

  1. Drive ownership marker — the spreadsheet must carry the appProperties.aeat_vault_app=aeat marker. Reading values from a spreadsheet that lacks the marker would mix operator content with foreign Drive files and break the aeat-vault/ isolation contract.

  2. Registry-SHA metadata match — the spreadsheet’s developer metadata must declare aeat_registry_sha = <snapshot.registry_sha> and aeat_modelo_id / aeat_revision_id / aeat_filing_year / aeat_period matching the caller’s snapshot. A mismatch means the workbook was compiled against a different registry slice — casilla identity/layout, formula chains, and bracket tables may have shifted. The pull is refused with a typed error.

The pull adapter does NOT mutate any local state; it returns a PullResult and leaves applying the edits to the caller.

See also

pull_operator_edits() reads the workbook, compute_from_pull() maps a matching pull into RegistryCalculationResult, and verify_pull_coverage() compares a pull against its source SheetExportPlan when the caller still has that plan.

class OperatorEdit(**data)[source]

Bases: BaseModel

One operator-edited cell value.

display_number and label are display-only fields added by the pull adapter from the workbook’s column metadata. They are not part of the canonical OperatorInput contract; use to_operator_input() to project this shape onto the canonical one.

value mirrors the cell’s raw shape from Google Sheets. The union is intentionally ambiguous between Decimal and numeric-shaped str because the wire JSON representation cannot statically distinguish them (pydantic serialises Decimal as a JSON string). The runtime path in compute_from_pull() is what disambiguates via _coerce_edit_value_to_decimal() for numeric input casillas and _enum_binding_text() for enum bindings.

Parameters:
casilla_id: CasillaId
display_number: str
label: str
value: Decimal | str | bool | None
to_operator_input()[source]

Project onto the canonical OperatorInput shape.

Drops display-only fields.

Return type:

OperatorInput

class BindingEdit(**data)[source]

Bases: BaseModel

One operator-edited binding cell value (numeric or enum).

Same union-ambiguity reasoning as value — the wire JSON cannot statically distinguish a CCAA-shape "04" from a numeric Decimal("4"). The runtime dispatch in compute_from_pull() is what routes the value: enum bindings go through _enum_binding_text() and numeric bindings go through _coerce_edit_value_to_decimal().

Parameters:
  • binding (BindingId)

  • value (Decimal | str | None)

binding: BindingId
value: Decimal | str | None
class RelationEdit(**data)[source]

Bases: BaseModel

One pre-resolved cross-revision relation value mirrored in Tarifas.

The provenance / source_modelo / source_filing_year / source_periods / source_casilla_ids / legal_refs / source_refs / resolved_at fields are recovered from the workbook’s developer metadata (aeat_relation:<relation> keys written by the apply adapter). They are absent for relations that were edited manually in the workbook without an apply round-trip; in that case the relation is treated as provenance="operator_manual" by convention.

Parameters:
  • relation (RelationId)

  • value (Decimal | None)

  • provenance (Literal['local_filing', 'aeat_live', 'operator_manual'] | None)

  • source_modelo (ModeloId | None)

  • source_filing_year (int | None)

  • source_periods (tuple[str, ...])

  • source_casilla_ids (tuple[CasillaId, ...])

  • legal_refs (tuple[LegalRefId, ...])

  • source_refs (tuple[SourceRefId, ...])

  • resolved_at (datetime | None)

relation: RelationId
value: Decimal | None
provenance: Literal['local_filing', 'aeat_live', 'operator_manual'] | None
source_modelo: ModeloId | None
source_filing_year: int | None
source_periods: tuple[str, ...]
source_casilla_ids: tuple[CasillaId, ...]
legal_refs: tuple[LegalRefId, ...]
source_refs: tuple[SourceRefId, ...]
resolved_at: datetime | None
class RowSetCellEdit(**data)[source]

Bases: BaseModel

One operator-edited cell from a Detalle tab row-set.

Parameters:
  • binding (BindingId)

  • row_index (int)

  • value (Decimal | str | None)

binding: BindingId
row_index: int
value: Decimal | str | None
class RowSetEdit(**data)[source]

Bases: BaseModel

All operator-supplied detail rows for one row-set grouping.

Parameters:
grouping: str
cells: tuple[RowSetCellEdit, ...]
class PullMetadata(**data)[source]

Bases: BaseModel

Workbook identity metadata recovered from developer metadata.

This is a loose parsing shape: exported_at is str | None because the developer-metadata round-trip may yield a raw ISO string or nothing. Use to_sheet_export_metadata() to project onto the strict canonical SheetExportMetadata shape when the workbook is known to carry a valid export stamp.

Parameters:
  • modelo_id (str)

  • revision_id (RevisionId)

  • filing_year (int)

  • period (str)

  • engine_version (str)

  • registry_sha (str)

  • exported_at (str | None)

modelo_id: str
revision_id: RevisionId
filing_year: int
period: str
engine_version: str
registry_sha: str
exported_at: str | None
to_sheet_export_metadata()[source]

Project onto a SheetExportMetadata.

Parses exported_at from an ISO string. Returns None when the stamp is absent or unparseable rather than raising, so callers can treat it as metadata_match="missing".

Return type:

SheetExportMetadata | None

class MetadataMatchState(*values)[source]

Bases: StrEnum

Registry-SHA + stamp alignment result for a pulled Sheets workbook.

MATCHES
STALE
MISSING
class PullResult(**data)[source]

Bases: BaseModel

Outcome of one Google Sheets pull cycle.

Carries the typed edit families read from the workbook, the PullMetadata stamp recovered from developer metadata, the MetadataMatchState verdict against the caller’s RegistrySnapshot, and the count of non-blank cells read. metadata_match may be STALE or MISSING; callers must treat that as a refusal boundary before applying edits to local state.

Parameters:
spreadsheet_id: str
operator_edits: tuple[OperatorEdit, ...]
binding_edits: tuple[BindingEdit, ...]
relation_edits: tuple[RelationEdit, ...]
row_set_edits: tuple[RowSetEdit, ...]
metadata: PullMetadata
metadata_match: MetadataMatchState
cells_read: int
pull_operator_edits(snapshot, *, spreadsheet_id, credentials)[source]

Read operator-edited cells back from a workbook into typed records.

This is the readback entrypoint behind aeat config google sync calc pull. It verifies the Drive ownership marker, reads developer metadata, classifies metadata against snapshot, reads operator/binding/relation cells plus Detalle row-set blocks, and returns a PullResult.

Parameters:
  • snapshot (RegistrySnapshot) – The RegistrySnapshot the workbook was compiled against. Used to derive the layout (cell addresses for every casilla / binding / relation) and to validate the workbook’s developer-metadata stamps.

  • spreadsheet_id (str) – The Drive file id of the workbook to read. Must already exist and carry the appProperties.aeat_vault_app=aeat ownership marker.

  • credentials (object) – A google.oauth2.credentials.Credentials-shaped object carrying a refresh + access token with at least the drive.file + spreadsheets scopes.

Return type:

PullResult

Returns:

A PullResult carrying the operator edits, binding edits, relation edits, and the metadata-match verdict. A metadata_match="stale" result still includes the edits but signals to the caller that the workbook’s identity does not match the supplied snapshot — applying these edits to the local store may corrupt data.

Raises:
class PullCoverageDiscrepancy(**data)[source]

Bases: BaseModel

One coverage delta between a plan and pulled workbook records.

The apply adapter writes a richly-shaped workbook (tariffs, constraints, protected ranges, row-sets); the pull adapter materialises a slimmer PullResult of operator-editable surfaces. A corrupted or hand-edited workbook could have structural cells stripped or row-set columns removed without surfacing as a load error. verify_pull_coverage() enumerates every coverage mismatch as one of these typed records so callers can choose to refuse the merge, log a warning, or surface a diagnostic to the operator.

The check is intentionally caller-opt-in: not every consumer of compute_from_pull() carries the original SheetExportPlan (e.g. a fresh pull from a workbook the operator authored without a prior apply). Callers that DO have the plan should run the check before consuming the pull.

Parameters:
  • kind (Literal['metadata_mismatch', 'row_set_missing', 'row_set_extra', 'binding_count_mismatch', 'relation_count_mismatch'])

  • detail (str)

  • expected (str)

  • observed (str)

kind: Literal['metadata_mismatch', 'row_set_missing', 'row_set_extra', 'binding_count_mismatch', 'relation_count_mismatch']
detail: str
expected: str
observed: str
verify_pull_coverage(plan, pull)[source]

Return every coverage discrepancy between plan and pull.

Returns an empty tuple of PullCoverageDiscrepancy when the two sides agree on the surfaces the pull captures. Non-empty tuples enumerate structural deltas: missing row-set groupings, unexpected groupings, binding count mismatch, relation count mismatch, or registry-metadata drift.

Tariffs, cell constraints, and protected ranges are NOT re-validated against the workbook itself (the pull adapter never reads them back); a future extension can compare developer- metadata digests for those surfaces when the apply side stamps them.

Return type:

tuple[PullCoverageDiscrepancy, ...]

Parameters:
compute_from_pull(snapshot, pull)[source]

Run the local Decimal runtime against a PullResult.

Maps each edit family back to the runtime contract:

  • value flows into runtime inputs, with Decimal("0") substituted for None so the runtime’s “every non-computed casilla has a value” precondition holds.

  • value is routed by the binding’s typed_enum declaration: numeric bindings flow into binding_values as Decimals; enum bindings flow into enum_binding_values as plain strings.

  • value flows into relation_values as Decimals, with Decimal("0") substituted for None.

Refuses to compute when the workbook’s metadata stamps do not match the supplied snapshot (pull.metadata_match != "matches"). The caller is responsible for handling stale workbooks before invoking this helper.

Parameters:
  • snapshot (RegistrySnapshot) – The RegistrySnapshot the workbook was compiled against. Used to derive input casilla identifiers, active relation periods, and the metadata-match gate.

  • pull (PullResult) – The PullResult carrying the operator-edited cells to compute from.

Return type:

RegistryCalculationResult

Returns:

A RegistryCalculationResult produced by calculate_registry_snapshot().

Raises:

OutboundStorageConflictError – When pull does not bind to snapshot by metadata verdict and registry-SHA stamp.