recon_gen.common.dataset_contract

Dataset column contracts and shared dataset-building helpers.

A DatasetContract declares the column interface a dataset produces. The SQL is one implementation of that contract (against the demo schema); customers swap in their own SQL. Everything downstream (visuals, filters, drill-downs) binds to contract columns, not SQL specifics.

Functions

build_dataset(cfg, dataset_id, name, ...[, ...])

Build an AWS-shape DataSet.

dataset_permissions(cfg)

get_contract(visual_identifier)

Look up the contract registered under visual_identifier.

get_dataset_params(visual_identifier)

Look up the dataset-parameter list registered under visual_identifier.

get_sql(visual_identifier)

Look up the SQL registered under visual_identifier.

isolated_dataset_registries()

Snapshot the three dataset registries on enter; restore on exit.

register_contract(visual_identifier, contract)

Register a visual_identifier -> contract mapping for shape lookup.

register_dataset_params(visual_identifier, ...)

Register a visual_identifier → dataset-parameter list mapping.

register_sql(visual_identifier, sql)

Register a visual_identifier → dataset SQL mapping.

Classes

ColumnShape(*values)

Application-level value shape of a drill-eligible column.

ColumnSpec(name, type[, shape, ...])

Declared column on a dataset's contract.

DatasetContract(columns)

Storage(*values)

BH.24 (2026-05-25) — per-column storage shape.

class recon_gen.common.dataset_contract.ColumnShape(*values)[source]

Bases: Enum

Application-level value shape of a drill-eligible column.

Layered above the AWS coarse type (STRING/DATETIME/…) so that two columns sharing a wire type but representing different semantic values cannot be cross-wired to the same drill parameter. K.2 spike found a silent zero-row bug where exception_date (DATETIME) was bound to a SINGLE_VALUED string parameter; QuickSight coerced it to the full timestamp text "2026-04-07 00:00:00.000" which never matched the destination’s posted_date column (also STRING but YYYY-MM-DD formatted via TO_CHAR). The shape captures both the encoding and the semantic, so the typed drill helper can refuse the wiring at code-gen time instead of silently producing zero rows.

Tag a column with a shape only if it’s an actual drill source or destination — every other column stays shape=None and is rejected by DrillSourceField resolution.

ACCOUNT_DISPLAY = 'account_display'
ACCOUNT_ID = 'account_id'
DATETIME_DAY = 'datetime_day'
DATE_YYYY_MM_DD_TEXT = 'date_yyyy_mm_dd_text'
EXTERNAL_TXN_ID = 'external_txn_id'
L2_DECLARED_NAME = 'l2_declared_name'
LEDGER_ACCOUNT_ID = 'ledger_account_id'
PAYMENT_ID = 'payment_id'
RAIL_NAME = 'rail_name'
SETTLEMENT_ID = 'settlement_id'
SUBLEDGER_ACCOUNT_ID = 'subledger_account_id'
TRANSFER_ID = 'transfer_id'
can_assign_to(other)[source]

True iff a value of self is acceptable into a other param.

Identical shapes are always assignable. SUBLEDGER_ACCOUNT_ID and LEDGER_ACCOUNT_ID widen to ACCOUNT_ID (the destination daily_balances.account_id column holds both ledger and sub-ledger ids). Date encodings do NOT widen — DATETIME and YYYY-MM-DD text are different wire types and cross-wiring them is the K.2 bug class.

Return type:

bool

Parameters:

other (ColumnShape)

class recon_gen.common.dataset_contract.ColumnSpec(name, type, shape=None, display_name=None, currency=False, storage=Storage.DOLLARS)[source]

Bases: object

Declared column on a dataset’s contract.

display_name (v8.5.0): plain-English header label QuickSight table visuals use as the column header. When omitted, defaults to a title-cased rewrite of the snake_case name — e.g. account_id → “Account ID” (with smart-uppercasing of common initialisms via _smart_title). Override when the auto-derived form is awkward — e.g. amount_money defaults to “Amount Money”, but Investigation tables read better with the explicit override “Amount” since the surrounding context already implies money.

currency + storage (BH.24, 2026-05-25): explicit currency- column declaration. currency=True tells renderers to apply USD formatting ($ prefix, 2 decimals). storage declares how the value comes back from the DB driver — DOLLARS (default, matches today’s “dataset SQL pre-converts via cents_to_dollars_sql” production pattern) or CENTS (raw BIGINT cents, renderer must divide). The two are independent: a column can be currency=True + storage=DOLLARS (the production case), currency=True + storage= CENTS (raw-matview Studio access), currency=False + storage=DOLLARS (a non-money decimal), etc. Renderers consult storage to decide whether to apply the /100 divide; currency only drives format.

Parameters:
  • name (str)

  • type (str)

  • shape (ColumnShape | None)

  • display_name (str | None)

  • currency (bool)

  • storage (Storage)

currency: bool = False
display_name: str | None = None
property human_name: str

Plain-English header label for this column.

display_name if set, else snake_case → “Title Case” with common initialisms preserved (id → ID, eod → EOD, etc.).

name: str
shape: ColumnShape | None = None
storage: Storage = 'dollars'
to_input_column()[source]
Return type:

InputColumn

type: str
class recon_gen.common.dataset_contract.DatasetContract(columns)[source]

Bases: object

Parameters:

columns (list[ColumnSpec])

column(name)[source]
Return type:

ColumnSpec

Parameters:

name (str)

property column_names: list[str]
columns: list[ColumnSpec]
to_input_columns()[source]
Return type:

list[InputColumn]

class recon_gen.common.dataset_contract.Storage(*values)[source]

Bases: Enum

BH.24 (2026-05-25) — per-column storage shape.

Distinguishes columns whose values come back from the DB driver as raw BIGINT cents (the AO.1.impl Studio slice’s matview storage contract) from columns whose values come back as already-converted float / Decimal dollars (the legacy pre-AO.1 pattern where the dataset SELECT wraps with cents_to_dollars_sql).

Why this matters: App2’s _measure_sql and _apply_cents_to_dollars both divide currency values by 100 at the renderer layer per AO.1’s “matview is cents, renderer converts” convention. If the dataset SQL ALSO divides (the production L1 / Inv / Exec / L2FT datasets all do today via cents_to_dollars_sql), the result is 100× off (BG.7 surfaced this on the Daily Statement KPIs: rendered -$11,993.10 vs matview -$1,199,309.68).

Default DOLLARS — matches today’s production behavior (every dataset pre-converts in SQL). Storage.CENTS is explicitly opt-in for columns that legitimately project raw cents (typically Studio bare-matview reads). currency=True becomes a pure FORMAT flag (USD symbol + 2-decimal QS format / “$” prefix on App2); the cents→dollars divide is gated on Storage.CENTS, not on currency=True.

CENTS = 'cents'
DOLLARS = 'dollars'
recon_gen.common.dataset_contract.build_dataset(cfg, dataset_id, name, table_key, sql, contract, visual_identifier, dataset_parameters=None)[source]

Build an AWS-shape DataSet.

dataset_parameters: optional list of dataset-level parameters that get substituted into sql via the <<$paramName>> syntax at QuickSight query time. Bridge to analysis params via MappedDataSetParameters on the analysis ParameterDeclaration.

Phase BM dissolved the pre-BM app2_date_column= kwarg + the {date_filter} template slot in favor of unified <<$pXxxDateStart>> / <<$pXxxDateEnd>> pushdown via the same dataset_parameters mechanism (see common/sql/app2_filters.py::universal_date_range_clause). One SQL form across QS + App2; the day-edge quirk dissolves.

Return type:

DataSet

Parameters:
recon_gen.common.dataset_contract.dataset_permissions(cfg)[source]
Return type:

list[ResourcePermission] | None

Parameters:

cfg (Config)

recon_gen.common.dataset_contract.get_contract(visual_identifier)[source]

Look up the contract registered under visual_identifier.

Raises KeyError if not registered — usually means the dataset hasn’t been built yet in the current process. Tests / generators should call build_dataset() before reaching code that resolves drill source fields.

Return type:

DatasetContract

Parameters:

visual_identifier (str)

recon_gen.common.dataset_contract.get_dataset_params(visual_identifier)[source]

Look up the dataset-parameter list registered under visual_identifier. Returns [] if nothing was registered — unlike get_sql this is not an error (most datasets have no parameters; the App2 executor handles the empty case gracefully).

Return type:

list[DatasetParameter]

Parameters:

visual_identifier (str)

recon_gen.common.dataset_contract.get_sql(visual_identifier)[source]

Look up the SQL registered under visual_identifier.

Raises KeyError if not registered — usually means the dataset hasn’t been built yet in the current process (call build_all_datasets(cfg) from the relevant app before reaching fetcher-construction code).

Return type:

str

Parameters:

visual_identifier (str)

recon_gen.common.dataset_contract.isolated_dataset_registries()[source]

Snapshot the three dataset registries on enter; restore on exit.

Production has exactly one cfg per process — the module-level _SQL_REGISTRY / _DSP_REGISTRY / _CONTRACT_REGISTRY are fine in that mode. Tests that build a second app tree with a non-canonical cfg.db_table_prefix in the same process (e.g. tests/e2e/test_inv_dashboard_agreement.py::isolated_inv_app, which builds an iagree-prefixed clone for the 4-way agreement test) overwrite the canonical entries on a shared key — downstream tests then read the polluted SQL and 500 against the dropped isolation schema.

Wrap the second-cfg build in this manager so its writes are discarded on teardown:

@pytest.fixture(scope="module")
def isolated_inv_app(isolated_inv_cfg):
    with isolated_dataset_registries():
        app = build_investigation_app(isolated_inv_cfg, l2_instance=_INSTANCE)
        app.emit_analysis()
        yield app

See BL.0 in PLAN.md + docs/audits/bl_0_shared_state_keying_smell.md for the architectural reasoning (rejected: lift state out of globals / extend the key with prefix; accepted: scope the band-aid at the test boundary since production is unaffected).

Return type:

Generator[None, None, None]

recon_gen.common.dataset_contract.register_contract(visual_identifier, contract)[source]

Register a visual_identifier -> contract mapping for shape lookup.

The key is the visual identifier (e.g. "ar-ledger-balance-drift-ds"), the same string the visuals use as DataSetIdentifier= and that the analysis maps to a real DataSet ARN via DataSetIdentifierDeclaration.

Idempotent for the same (visual_identifier, contract) pair; raises if a different contract is already registered under the same identifier (catches accidental identifier collisions).

Return type:

None

Parameters:
recon_gen.common.dataset_contract.register_dataset_params(visual_identifier, params)[source]

Register a visual_identifier → dataset-parameter list mapping.

Same overwrite-on-repeat semantics as register_sql. Pass [] (or omit, via build_dataset(dataset_parameters=None)) for a dataset with no parameters — get_dataset_params then returns [] and the App2 executor leaves any stray placeholder for the bind-variable fallback.

Return type:

None

Parameters:
recon_gen.common.dataset_contract.register_sql(visual_identifier, sql)[source]

Register a visual_identifier → dataset SQL mapping.

Idempotent for the same (visual_identifier, sql) pair; the second call with the same identifier overwrites with the new SQL (matches the typical “rebuild for a different dialect” workflow — same identifier, dialect-specific SQL). Tests that need to assert “build was idempotent” should snapshot the registry before / after.

Return type:

None

Parameters:
  • visual_identifier (str)

  • sql (str)