Source code for recon_gen.common.tree.calc_fields

"""Typed analysis-level calculated fields (L.1.8).

A ``CalcField`` is the typed wrapper around the existing per-app
``CalculatedField`` dict (``{Name, DataSetIdentifier, Expression}``).
Visuals and filters reference calc fields the same way they reference
real dataset columns — by passing the column to ``Dim`` / ``Measure``
/ ``CategoryFilter`` / ``NumericRangeFilter``. The column slot accepts
either a bare ``str`` (a real column or a calc-field name) OR a
``CalcField`` object reference; the typed ref carries the validated
calc-field identity through the type checker.

Validation (L.1.8):

- ``Analysis.add_calc_field`` rejects duplicate calc-field names
  within an analysis.
- ``App._validate_calc_field_references`` (added in L.1.8) raises if
  any tree-referenced ``CalcField`` isn't registered on the Analysis.
  Catches "filter references calc field that doesn't exist" and
  "calc field declared but never used".

Dependency graph (L.1.7 + L.1.8):

- Each ``CalcField`` carries a ``Dataset`` ref. The CalcField's
  dataset participates in ``App.dataset_dependencies()`` so
  declaring a calc field on dataset D establishes D as a dep even
  when no visual directly references D's columns.

Auto-name (L.2.6 follow-up): ``name`` is Optional. When omitted, the
App walker assigns ``calc-{idx}`` at emit time based on the calc
field's index in ``analysis.calc_fields``. Pass an explicit ``name=``
when the calc field's column header text matters to analysts (the name
becomes the underlying ColumnName in the data model — analyst-facing
unless a visual's label options override it).
"""

from __future__ import annotations

from dataclasses import dataclass

from recon_gen.common.dataset_contract import ColumnShape
from recon_gen.common.tree._helpers import AUTO, AutoResolved, _AutoSentinel
from recon_gen.common.tree.datasets import Column, Dataset


[docs] @dataclass(eq=False) class CalcField: """Tree node for one analysis-level calculated field. ``name`` is the column-style identifier visuals/filters reference (e.g. ``"is_anchor_edge"``). Optional — auto-derived as ``calc-{idx}`` at emit time when not specified. ``dataset`` is the ``Dataset`` object ref the expression evaluates against. ``expression`` is the QuickSight calc expression (e.g. ``"ifelse({source} = ${pAnchor}, 'yes', 'no')"``). ``shape`` is Optional and only matters for drill sources: when a drill action reads this calc field's value (via a ``Dim`` / ``Measure`` object ref in the drill's ``writes``), the tree needs a ``ColumnShape`` to type-check the drill parameter binding. Tag here once rather than re-passing the shape at every drill site. Identity-keyed (``eq=False``) so the auto-name resolver can mutate the ``name`` field at emit time. CalcFields stay hashable via the default object identity hash, which is what the dependency-graph set membership needs anyway. Emits a plain dict that drops straight into ``AnalysisDefinition.CalculatedFields`` — same shape the existing builders write today. """ dataset: Dataset expression: str name: str | AutoResolved = AUTO shape: ColumnShape | None = None
[docs] def emit(self) -> dict[str, str]: assert not isinstance(self.name, _AutoSentinel), ( "name wasn't resolved — App.resolve_auto_ids() must run " "before CalcField.emit()." ) return { "Name": self.name, "DataSetIdentifier": self.dataset.identifier, "Expression": self.expression, }
# Type alias used everywhere a tree node accepts a column reference. # Three forms (the resolver below pulls the column name out at emit # time): # # - ``str`` — bare column name. Escape hatch — no contract validation. # Use sparingly (e.g., test fixtures or datasets without a contract). # - ``Column`` — typed ref produced by ``ds["column_name"]``. Validated # against the dataset's contract at construction. Preferred form. # - ``CalcField`` — analysis-level calc field. Carries its own dataset # ref + name; the dependency-graph walk picks up the calc field's # dataset. ColumnRef = str | CalcField | Column
[docs] def resolve_column(column: ColumnRef) -> str: """Read the column-name string off a ``ColumnRef``. For a ``CalcField``, the name is set by ``App.resolve_auto_ids()``; callers asserting the resolver ran can rely on this returning ``str``. """ if isinstance(column, CalcField): assert not isinstance(column.name, _AutoSentinel), ( "CalcField.name wasn't resolved — App.resolve_auto_ids() " "must run before resolve_column() on a CalcField ref." ) return column.name if isinstance(column, Column): return column.name return column
[docs] def calc_field_in(column: ColumnRef) -> CalcField | None: """Return the CalcField if ``column`` is one, else ``None``. Used by the dependency-graph walk to harvest CalcField refs from Dim / Measure / Filter column slots. ``Column`` refs return None (they reference a real dataset column, not a calc field). """ if isinstance(column, CalcField): return column return None