Tree — App / Analysis / Dashboard / Sheet¶
Top-level tree nodes. App is the construction root; an app emits an
Analysis and (optionally) a Dashboard mirroring it. Each carries
a list of Sheet nodes; sheets carry visuals, filter groups, filter
controls, and parameter controls.
structure ¶
Structural tree types — GridSlot / Sheet / Analysis /
Dashboard / App.
The skeleton the rest of the tree hangs off. Authors construct an
App, attach an Analysis (which holds the sheet tree),
optionally attach a Dashboard, and call app.emit_analysis()
/ app.emit_dashboard() to get the models.py instances ready
for deploy.
LayoutNode ¶
Bases: Protocol
Anything placeable in a sheet's grid layout.
Both typed visual subtypes (KPI / Table / BarChart /
Sankey) and the typed TextBox wrapper satisfy this Protocol.
Each exposes element_id (the layout slot's ElementId) and
element_type ("VISUAL" or "TEXT_BOX") — the slot reads
them off the node at emit time.
The Protocol means Sheet.place(node, ...) accepts both visuals
and text boxes uniformly; QuickSight's two-list split (Visuals vs
TextBoxes in SheetDefinition) stays an emit-time concern that
callers never see.
GridSlot
dataclass
¶
One placement in a sheet's grid layout.
Holds an OBJECT reference to the placed LayoutNode. The element
id and element type are read off the node at emit time — the slot
is agnostic about whether it carries a visual or a text box.
__init__ ¶
__init__(element: LayoutNode, col_span: int, row_span: int, col_index: int, row_index: int | None = None) -> None
Sheet
dataclass
¶
Tree node for one sheet on an Analysis / Dashboard.
Sheet has four concerns:
- Metadata —
sheet_id,name,title,descriptionset at construction. - Layout — visuals + text boxes placed in a grid. Accessed via
sheet.layout:sheet.layout.row(height=...).add_kpi(width=, title=, values=, ...)for sequential rows, orsheet.layout. absolute(col_index=, row_index=, col_span=, row_span=).add_*(...)for explicit positioning. The layout's row tracks the column cursor so call sites don't computecol_indexarithmetic by hand. - Controls — parameter / filter controls live above/aside the
canvas (NOT in the grid). Added directly on Sheet via
sheet.add_parameter_dropdown(...)/add_parameter_slider/add_parameter_datetime_picker/add_filter_dropdown/add_filter_slider/add_filter_datetime_picker/add_filter_cross_sheet— one shortcut per control kind. - Scope wiring —
sheet.scope(filter_group, [v1, v2])scopes a filter group to specific visuals on this sheet.
emit() returns the SheetDefinition ready to drop into
AnalysisDefinition.Sheets.
visuals
class-attribute
instance-attribute
¶
visuals: list[VisualLike] = field(default_factory=list[VisualLike])
parameter_controls
class-attribute
instance-attribute
¶
parameter_controls: list[ParameterControlLike] = field(default_factory=list[ParameterControlLike])
filter_controls
class-attribute
instance-attribute
¶
filter_controls: list[FilterControlLike] = field(default_factory=list[FilterControlLike])
text_boxes
class-attribute
instance-attribute
¶
grid_slots
class-attribute
instance-attribute
¶
grid_slots: list[GridSlot] = field(default_factory=list['GridSlot'])
layout
property
¶
layout: 'SheetLayout'
L.1.21 — Layout namespace for grid placement.
Visuals + text boxes are added through the layout (one of:
sheet.layout.row(height=...).add_kpi(width=..., ...) or
sheet.layout.absolute(col_index=..., row_index=..., col_span=...,
row_span=...).add_kpi(...)). The Sheet itself coordinates four
concerns; layout is one of them, factored out so the call sites
for "place a visual" don't crowd against "register a control" or
"scope a filter group".
Lazily constructed; the same SheetLayout instance returns on subsequent accesses so the row cursor advances across calls.
__init__ ¶
__init__(sheet_id: SheetId, name: str, title: str, description: str, visuals: list[VisualLike] = list[VisualLike](), parameter_controls: list[ParameterControlLike] = list[ParameterControlLike](), filter_controls: list[FilterControlLike] = list[FilterControlLike](), text_boxes: list[TextBox] = list[TextBox](), grid_slots: list[GridSlot] = list['GridSlot']()) -> None
add_parameter_dropdown ¶
add_parameter_dropdown(*, parameter: ParameterDeclLike, title: str, selectable_values: SelectableValues, type: Literal['SINGLE_SELECT', 'MULTI_SELECT'] = 'SINGLE_SELECT', hidden_select_all: bool = False, cascade_source: ParameterDropdown | None = None, cascade_match_column: Column | None = None, control_id: str | AutoResolved = AUTO) -> ParameterDropdown
Construct + register a parameter dropdown control on this sheet.
selectable_values is required: a parameter dropdown without
a source list (StaticValues / LinkedValues) shows only the
QuickSight empty-state "All" placeholder, so the user can't
actually pick a value — the bound parameter stays unset and
any CategoryFilter using it matches nothing. Caught the L1
Daily Statement account-dropdown footgun (v8.3.3 hotfix); the
type makes it unrepresentable going forward.
add_parameter_slider ¶
add_parameter_slider(*, parameter: ParameterDeclLike, title: str, minimum_value: float, maximum_value: float, step_size: float, control_id: str | AutoResolved = AUTO) -> ParameterSlider
Construct + register a parameter slider control on this sheet.
add_parameter_datetime_picker ¶
add_parameter_datetime_picker(*, parameter: ParameterDeclLike, title: str, control_id: str | AutoResolved = AUTO) -> ParameterDateTimePicker
Construct + register a parameter datetime picker control.
add_parameter_text_field ¶
add_parameter_text_field(*, parameter: ParameterDeclLike, title: str, control_id: str | AutoResolved = AUTO) -> ParameterTextField
Construct + register a free-text parameter input control.
Use when the parameter's option universe is unbounded / unknown
at deploy time, or when LinkedValues / StaticValues paths fail
(X.1.b L2FT cascade Value dropdown hit Sample values not
found from QS's lazy sample-values fetch on cold per-CI-run
dashboards). Text input has no equivalent fetch path.
add_filter_dropdown ¶
add_filter_dropdown(*, filter: FilterLike, title: str, type: Literal['SINGLE_SELECT', 'MULTI_SELECT'] = 'MULTI_SELECT', selectable_values: SelectableValues | None = None, control_id: str | AutoResolved = AUTO) -> FilterDropdown
Construct + register a filter dropdown control on this sheet.
add_filter_slider ¶
add_filter_slider(*, filter: FilterLike, title: str, minimum_value: float, maximum_value: float, step_size: float, type: Literal['SINGLE_POINT', 'RANGE'] = 'RANGE', control_id: str | AutoResolved = AUTO) -> FilterSlider
Construct + register a filter slider control on this sheet.
add_filter_datetime_picker ¶
add_filter_datetime_picker(*, filter: FilterLike, title: str, type: Literal['SINGLE_VALUED', 'DATE_RANGE'] = 'DATE_RANGE', control_id: str | AutoResolved = AUTO) -> FilterDateTimePicker
Construct + register a filter datetime picker control.
add_filter_cross_sheet ¶
add_filter_cross_sheet(*, filter: FilterLike, control_id: str | AutoResolved = AUTO) -> FilterCrossSheet
Construct + register a cross-sheet filter control on this sheet.
scope ¶
scope(fg: FilterGroup, visuals: list[VisualLike]) -> FilterGroup
L.1.21 — scope a filter group to specific visuals on this sheet.
Reads more naturally than fg.scope_visuals(sheet, visuals) since
the sheet is the contextual subject. Runtime check stays —
Python's type system can't track which Sheet instance a
visual was registered on without dependent types.
find_visual ¶
find_visual(*, title: str | None = None, title_contains: str | None = None, visual_id: VisualId | str | None = None) -> VisualLike
Look up a single visual on this sheet by title / partial title / visual id.
Designed for e2e + introspection: pass any of the three lookup keys and get the matching node back. Raises if no match or multiple matches — the API forces unambiguity at the call site so tests can rely on the result.
Auto-IDs (L.1.8.5) make this the right way to find a visual from outside the tree — IDs are not stable under tree restructuring, but titles + structural position are.
Row
dataclass
¶
One horizontal band in a Sheet's grid layout.
Tracks the column cursor as visuals + text boxes are added; refuses
widths that overflow the 36-column grid. height becomes every
visual's row_span; the column cursor advances by each visual's
width (col_span). A new row from sheet.layout.row() lands
below the previous row — the SheetLayout tracks the vertical cursor.
add_kpi ¶
add_kpi(*, width: int, title: str, values: list[Measure] | None = None, subtitle: str | None = None, visual_id: VisualId | AutoResolved = AUTO) -> KPI
Construct + register + place a KPI in this row.
add_table ¶
add_table(*, width: int, title: str, group_by: list[Dim] | None = None, values: list[Measure] | None = None, columns: list[Dim] | None = None, subtitle: str | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | list[tuple[FieldRef, Literal['ASC', 'DESC']]] | None = None, actions: list[Action] | None = None, conditional_formatting: list[CellFormat] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> Table
Construct + register + place a Table in this row.
Aggregated mode: pass group_by + values. Unaggregated
mode (raw column display): pass columns. The two modes are
mutually exclusive (Table.post_init enforces this).
add_bar_chart ¶
add_bar_chart(*, width: int, title: str, category: list[Dim] | None = None, values: list[Measure] | None = None, colors: list[Dim] | None = None, subtitle: str | None = None, orientation: Literal['HORIZONTAL', 'VERTICAL'] | None = None, bars_arrangement: Literal['CLUSTERED', 'STACKED', 'STACKED_PERCENT'] | None = None, category_label: str | None = None, value_label: str | None = None, color_label: str | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | None = None, actions: list[Action] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> BarChart
Construct + register + place a BarChart in this row.
add_line_chart ¶
add_line_chart(*, width: int, title: str, category: list[Dim] | None = None, values: list[Measure] | None = None, colors: list[Dim] | None = None, subtitle: str | None = None, chart_type: Literal['LINE', 'AREA', 'STACKED_AREA'] | None = None, category_label: str | None = None, value_label: str | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | None = None, actions: list[Action] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> LineChart
Construct + register + place a LineChart in this row.
add_sankey ¶
add_sankey(*, width: int, title: str, source: Dim, target: Dim, weight: Measure, subtitle: str | None = None, items_limit: int | None = None, actions: list[Action] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> Sankey
Construct + register + place a Sankey in this row.
add_text_box ¶
Register + place a pre-constructed TextBox in this row.
TextBox content is verbose XML (built via common/rich_text),
so the analyst constructs the TextBox separately and passes it
here. Uniqueness is by object identity — placing the same
TextBox in two rows is a programmer error caught by the row-
cursor advance (you'd be calling _consume twice).
AbsoluteSlot
dataclass
¶
One explicit-position slot. Use for layouts that don't fit the
row pattern — overlapping visuals, asymmetric grids, off-grid
positioning. One-shot: construct via sheet.layout.absolute(
col_index=, row_index=, col_span=, row_span=) then chain a single
add_<kind>(...) to fill it. Re-using an AbsoluteSlot for
multiple visuals would emit duplicate slot positions and isn't
supported.
__init__ ¶
__init__(sheet: Sheet, col_span: int, row_span: int, col_index: int, row_index: int | None) -> None
add_kpi ¶
add_kpi(*, title: str, values: list[Measure] | None = None, subtitle: str | None = None, visual_id: VisualId | AutoResolved = AUTO) -> KPI
add_table ¶
add_table(*, title: str, group_by: list[Dim] | None = None, values: list[Measure] | None = None, columns: list[Dim] | None = None, subtitle: str | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | list[tuple[FieldRef, Literal['ASC', 'DESC']]] | None = None, actions: list[Action] | None = None, conditional_formatting: list[CellFormat] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> Table
add_bar_chart ¶
add_bar_chart(*, title: str, category: list[Dim] | None = None, values: list[Measure] | None = None, subtitle: str | None = None, orientation: Literal['HORIZONTAL', 'VERTICAL'] | None = None, bars_arrangement: Literal['CLUSTERED', 'STACKED', 'STACKED_PERCENT'] | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | None = None, actions: list[Action] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> BarChart
add_line_chart ¶
add_line_chart(*, title: str, category: list[Dim] | None = None, values: list[Measure] | None = None, colors: list[Dim] | None = None, subtitle: str | None = None, chart_type: Literal['LINE', 'AREA', 'STACKED_AREA'] | None = None, category_label: str | None = None, value_label: str | None = None, sort_by: tuple[FieldRef, Literal['ASC', 'DESC']] | None = None, actions: list[Action] | None = None, visual_id: VisualId | AutoResolved = AUTO) -> LineChart
SheetLayout
dataclass
¶
Layout namespace on a Sheet — manages rows + absolute placements.
Tracks a vertical row cursor: each row(height=H) opens a new row
at the current cursor and advances it by H for the next row.
Absolute placements don't advance the cursor (they're independent
of row flow).
row ¶
row(*, height: int) -> Row
Open a new row at the current vertical cursor with the given
height. The cursor advances by height so the next row()
call lands below this one.
absolute ¶
absolute(*, col_index: int, row_index: int | None = None, col_span: int, row_span: int) -> AbsoluteSlot
Open an explicit-position slot. Doesn't advance the row cursor — absolute placements are independent of row flow.
Analysis
dataclass
¶
Tree node for the Analysis-level structure.
analysis_id_suffix is the part the App's cfg.prefixed()
will prepend to (e.g. "investigation-analysis" becomes
"qs-gen-investigation-analysis"). Keeping the suffix on the
tree node keeps the per-app naming under the tree's control while
leaving the global resource-prefix in the Config.
emit_definition() returns the models.AnalysisDefinition —
the App combines this with metadata (AwsAccountId,
ThemeArn, Permissions, dataset declarations) to produce
the full models.Analysis ready for deploy.
sheets
class-attribute
instance-attribute
¶
sheets: list[Sheet] = field(default_factory=list['Sheet'])
parameters
class-attribute
instance-attribute
¶
parameters: list[ParameterDeclLike] = field(default_factory=list[ParameterDeclLike])
filter_groups
class-attribute
instance-attribute
¶
filter_groups: list[FilterGroup] = field(default_factory=list[FilterGroup])
calc_fields
class-attribute
instance-attribute
¶
__init__ ¶
__init__(analysis_id_suffix: str, name: str, sheets: list[Sheet] = list['Sheet'](), parameters: list[ParameterDeclLike] = list[ParameterDeclLike](), filter_groups: list[FilterGroup] = list[FilterGroup](), calc_fields: list[CalcField] = list[CalcField]()) -> None
add_parameter ¶
add_parameter(param: T) -> T
Declare a parameter on this analysis.
Construction-time check: parameter names are unique within
the analysis. Catches the silent shadow bug where two declarations
share a Name and only one wins at deploy time. Generic over
the concrete subtype so the returned ref keeps its type
(StringParam / IntegerParam / DateTimeParam) (PEP 695).
add_filter_group ¶
add_filter_group(fg: FilterGroup) -> FilterGroup
Register a filter group on this analysis.
Construction-time check: explicit filter group IDs are unique. (Auto-IDs are unique by construction — assigned from the index in the analysis's filter_groups list — so the check only applies when callers passed an explicit id.)
find_sheet ¶
Look up a single sheet on this analysis by name or sheet id.
Raises on no-match or multi-match. Sheet IDs stay explicit
(URL-facing per the L.1.8.5 mixed scheme) so passing
sheet_id= is the most robust lookup; name= is the
next-best for tests that don't want to hardcode IDs.
find_filter_group ¶
find_filter_group(*, filter_group_id: FilterGroupId | str | None = None) -> FilterGroup
Look up a single filter group by id (auto or explicit).
find_parameter ¶
find_parameter(*, name: str) -> ParameterDeclLike
Look up a single parameter declaration by name.
add_calc_field ¶
Register a calculated field on this analysis.
Construction-time check: calc field names are unique within the analysis. Two calc fields sharing a Name silently let one win at deploy time — same shadow-bug class as parameters / filter groups / datasets.
datasets ¶
datasets() -> set[Dataset]
Walk the analysis tree and return every Dataset referenced by any visual, filter group, or registered calc field. Used by App.dataset_dependencies to derive the precise refresh set.
Visuals using the spike-shape VisualNode factory wrapper
don't expose their dataset refs (the factory hides them).
Typed Visual subtypes (KPI / Table / BarChart /
Sankey) all expose datasets() and contribute. The
spike-shape gap closes once apps port to typed subtypes
(L.2/L.3/L.4).
Registered CalcFields contribute too — their Dataset ref
becomes a dep even if no visual directly references the
underlying columns.
calc_fields_referenced ¶
calc_fields_referenced() -> set[CalcField]
Walk the analysis tree and return every CalcField referenced
by any visual or filter group. Distinct from self.calc_fields
(the registry): this returns only the calc fields actually used.
Catches "calc field declared but never used" (registered but not in this set) and "calc field used but not declared" (in this set but not in the registry — App.validate_calc_field references raises on emit).
Dashboard
dataclass
¶
Tree node for a Dashboard.
Carries an object reference to the Analysis whose definition
this Dashboard publishes. dashboard_id_suffix follows the same
pattern as Analysis.analysis_id_suffix — App's cfg.prefixed()
prepends the project resource prefix.
analysis is the SAME tree node the App owns; the Dashboard
re-emits the same definition the Analysis produces, which matches
the existing build_dashboard(cfg) pattern in the per-app
builders.
App
dataclass
¶
Top-level tree node — coordinates an Analysis + Dashboard plus the deploy-time context (theme, dataset arns, permissions) drawn from the Config.
Authors construct an App, attach the Analysis (which holds the
sheet tree), optionally attach the Dashboard (most apps do — they
publish what they author), and call emit_analysis() /
emit_dashboard() to get the models.py instances ready for
deploy.
Datasets are registered on the App via add_dataset() and
referenced from visuals / filters by object ref. At emit time
the App walks the tree's dataset_dependencies() and includes
only the datasets actually used in the emitted
DataSetIdentifierDeclarations — selective by construction.
Validation: if a visual or filter references a Dataset that
isn't registered on the App, emit_analysis raises with the
offending identifiers.
datasets
class-attribute
instance-attribute
¶
__init__ ¶
__init__(name: str, cfg: Config, analysis: Analysis | None = None, dashboard: Dashboard | None = None, datasets: list[Dataset] = list[Dataset](), allow_bare_strings: bool = False) -> None
create_dashboard ¶
create_dashboard(*, dashboard_id_suffix: str, name: str) -> Dashboard
Construct + register a Dashboard against the App's already-set Analysis.
The App owns the Analysis already; this shortcut prevents the analysis-mismatch bug class by construction — there's no opening to pass a different Analysis.
add_dataset ¶
Register a Dataset on the App.
Construction-time check: dataset identifiers are unique within the app. Catches the silent shadow bug where two registrations share an identifier and only one wins at deploy.
dataset_dependencies ¶
dataset_dependencies() -> set[Dataset]
The set of Datasets referenced anywhere in the App's tree.
Walks the Analysis (sheets → visuals + filter_groups). Each
typed Visual subtype + typed Filter wrapper exposes its own
datasets() set; the App unions them.
Deployment side effect. This set drives: - selective deploy (only re-create / refresh the datasets downstream of an actual change), - matview REFRESH ordering (REFRESH only the matviews backing datasets that the changed deploy surface depends on).
Returns an empty set when the App has no Analysis.
find_sheet ¶
Convenience pass-through to app.analysis.find_sheet(...).