Source code for recon_gen.common.models

"""Dataclass models mapping to AWS QuickSight API JSON structures.

Each top-level model (Theme, DataSet, Analysis) has a `to_aws_json()` method
that returns the exact dict shape expected by the corresponding AWS CLI command
(create-theme, create-data-set, create-analysis).
"""

from __future__ import annotations

import json
from collections.abc import Sequence
from dataclasses import dataclass, asdict
from enum import Enum
from typing import Any, ClassVar, Literal


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _strip_nones(obj: Any) -> Any:
    """Recursively remove keys with None values from dicts.

    The shape is necessarily ``Any`` — this walks arbitrary
    QuickSight-API JSON structures (asdict() output for any model).
    Pyright would otherwise demand a recursive ``JsonValue`` alias
    that doesn't buy us anything for an internal helper.
    """
    if isinstance(obj, dict):
        return {  # pyright: ignore[reportUnknownVariableType]: walks arbitrary QS-API JSON, recursive Any by design
            k: _strip_nones(v)  # pyright: ignore[reportUnknownArgumentType]: walks arbitrary QS-API JSON, recursive Any by design
            for k, v in obj.items()  # pyright: ignore[reportUnknownVariableType]: walks arbitrary QS-API JSON, recursive Any by design
            if v is not None
        }
    if isinstance(obj, list):
        return [
            _strip_nones(v)  # pyright: ignore[reportUnknownArgumentType]: walks arbitrary QS-API JSON, recursive Any by design
            for v in obj  # pyright: ignore[reportUnknownVariableType]: walks arbitrary QS-API JSON, recursive Any by design
        ]
    if isinstance(obj, Enum):
        return obj.value
    return obj


# ---------------------------------------------------------------------------
# Common / shared types
# ---------------------------------------------------------------------------

[docs] @dataclass class ColumnIdentifier: """References a column on a specific dataset by identifier. Reused across many AWS QuickSight shapes — every typed field-well leaf (CategoricalDimensionField, DateDimensionField, etc.) and cascading control source carries one. """ DataSetIdentifier: str ColumnName: str
[docs] @dataclass class CategoricalDimensionField: FieldId: str Column: ColumnIdentifier HierarchyId: str | None = None
[docs] @dataclass class DateDimensionField: FieldId: str Column: ColumnIdentifier DateGranularity: str | None = None # YEAR|QUARTER|MONTH|WEEK|DAY|HOUR|... HierarchyId: str | None = None
[docs] @dataclass class NumericalDimensionField: FieldId: str Column: ColumnIdentifier HierarchyId: str | None = None FormatConfiguration: NumberFormatConfiguration | None = None
[docs] @dataclass class DimensionField: """Union type — set exactly one.""" CategoricalDimensionField: CategoricalDimensionField | None = None DateDimensionField: DateDimensionField | None = None NumericalDimensionField: NumericalDimensionField | None = None
[docs] @dataclass class NumericalAggregationFunction: SimpleNumericalAggregation: str | None = None # SUM|COUNT|AVG|MIN|MAX
[docs] @dataclass class DecimalPlacesConfiguration: """Wire shape for the per-field decimal-places setting.""" DecimalPlaces: int
[docs] @dataclass class ThousandSeparatorOptions: Symbol: str # COMMA | DOT | SPACE Visibility: str # VISIBLE | HIDDEN
[docs] @dataclass class SeparatorConfiguration: DecimalSeparator: str | None = None # COMMA | DOT | SPACE ThousandsSeparator: ThousandSeparatorOptions | None = None
[docs] @dataclass class CurrencyDisplayFormatConfiguration: """Currency-format wire shape. ``Symbol`` is an ISO 4217 code (``"USD"``); QuickSight renders the appropriate symbol prefix (``$`` for USD, etc.) automatically.""" Symbol: str | None = None DecimalPlacesConfiguration: DecimalPlacesConfiguration | None = None SeparatorConfiguration: SeparatorConfiguration | None = None
[docs] @dataclass class NumericFormatConfiguration: """Discriminated union — exactly one of the three sub-format configurations is set.""" NumberDisplayFormatConfiguration: dict[str, Any] | None = None CurrencyDisplayFormatConfiguration: CurrencyDisplayFormatConfiguration | None = None PercentageDisplayFormatConfiguration: dict[str, Any] | None = None
[docs] @dataclass class NumberFormatConfiguration: """Wrapper carrying the actual format inside its ``FormatConfiguration`` slot — this two-level nesting matches the AWS QuickSight wire schema.""" FormatConfiguration: NumericFormatConfiguration | None = None
[docs] @dataclass class NumericalMeasureField: FieldId: str Column: ColumnIdentifier AggregationFunction: NumericalAggregationFunction | None = None FormatConfiguration: NumberFormatConfiguration | None = None
[docs] @dataclass class CategoricalMeasureField: FieldId: str Column: ColumnIdentifier AggregationFunction: str | None = None # COUNT|DISTINCT_COUNT
[docs] @dataclass class DateMeasureField: FieldId: str Column: ColumnIdentifier AggregationFunction: str | None = None # COUNT|DISTINCT_COUNT|MIN|MAX
[docs] @dataclass class MeasureField: """Union type — set exactly one.""" NumericalMeasureField: NumericalMeasureField | None = None CategoricalMeasureField: CategoricalMeasureField | None = None DateMeasureField: DateMeasureField | None = None
[docs] @dataclass class VisualTitleLabelOptions: Visibility: str = "VISIBLE" # VISIBLE|HIDDEN FormatText: dict[str, str] | None = None # {"PlainText": "..."}
[docs] @dataclass class VisualSubtitleLabelOptions: Visibility: str = "VISIBLE" FormatText: dict[str, str] | None = None
# --------------------------------------------------------------------------- # Theme models # ---------------------------------------------------------------------------
[docs] @dataclass class DataColorPalette: Colors: list[str] | None = None EmptyFillColor: str | None = None MinMaxGradient: list[str] | None = None
[docs] @dataclass class UIColorPalette: PrimaryBackground: str | None = None PrimaryForeground: str | None = None SecondaryBackground: str | None = None SecondaryForeground: str | None = None Accent: str | None = None AccentForeground: str | None = None Danger: str | None = None DangerForeground: str | None = None Warning: str | None = None WarningForeground: str | None = None Success: str | None = None SuccessForeground: str | None = None Dimension: str | None = None DimensionForeground: str | None = None Measure: str | None = None MeasureForeground: str | None = None
[docs] @dataclass class TileBorder: Show: bool | None = None
[docs] @dataclass class Tile: Border: TileBorder | None = None
[docs] @dataclass class Gutter: Show: bool | None = None
[docs] @dataclass class Margin: Show: bool | None = None
[docs] @dataclass class TileLayout: Gutter: Gutter | None = None Margin: Margin | None = None
[docs] @dataclass class SheetStyle: Tile: Tile | None = None TileLayout: TileLayout | None = None
[docs] @dataclass class FontFamily: FontFamily: str
[docs] @dataclass class Typography: FontFamilies: list[FontFamily] | None = None
[docs] @dataclass class ThemeConfiguration: DataColorPalette: DataColorPalette | None = None UIColorPalette: UIColorPalette | None = None Sheet: SheetStyle | None = None Typography: Typography | None = None
[docs] @dataclass class Tag: Key: str Value: str
[docs] @dataclass class ResourcePermission: Principal: str Actions: list[str]
[docs] @dataclass class Theme: AwsAccountId: str ThemeId: str Name: str BaseThemeId: str Configuration: ThemeConfiguration Permissions: list[ResourcePermission] | None = None Tags: list[Tag] | None = None VersionDescription: str | None = None
[docs] def to_aws_json(self) -> dict[str, Any]: return _strip_nones(asdict(self))
[docs] def to_json_string(self, indent: int = 2) -> str: return json.dumps(self.to_aws_json(), indent=indent)
# --------------------------------------------------------------------------- # DataSource models # ---------------------------------------------------------------------------
[docs] @dataclass class PostgreSqlParameters: Host: str Port: int Database: str
[docs] @dataclass class OracleParameters: """QuickSight OracleParameters shape (boto3 quicksight create-data-source). Per the AWS QuickSight API: - ``Host`` — RDS Oracle endpoint hostname. - ``Port`` — listener port (defaults 1521). - ``Database`` — SID or service name (e.g. ``ORCL``). - ``UseServiceName`` — when True, ``Database`` is treated as a service name; when False (AWS default), as a SID. ``UseServiceName`` defaults True on this dataclass because RDS Oracle endpoints (and Aurora Oracle) connect via service name — the SID-style interpretation only matches older self-managed Oracle installs. Override at construction if needed. """ Host: str Port: int Database: str UseServiceName: bool = True
[docs] @dataclass class DataSourceParameters: PostgreSqlParameters: PostgreSqlParameters | None = None OracleParameters: OracleParameters | None = None
[docs] @dataclass class CredentialPair: Username: str Password: str
[docs] @dataclass class DataSourceCredentials: CredentialPair: CredentialPair | None = None
[docs] @dataclass class SslProperties: DisableSsl: bool = False
[docs] @dataclass class DataSource: AwsAccountId: str DataSourceId: str Name: str Type: str # POSTGRESQL, MYSQL, etc. DataSourceParameters: DataSourceParameters Credentials: DataSourceCredentials | None = None SslProperties: SslProperties | None = None Permissions: list[ResourcePermission] | None = None Tags: list[Tag] | None = None
[docs] def to_aws_json(self) -> dict[str, Any]: return _strip_nones(asdict(self))
[docs] def to_json_string(self, indent: int = 2) -> str: return json.dumps(self.to_aws_json(), indent=indent)
# --------------------------------------------------------------------------- # DataSet models # ---------------------------------------------------------------------------
[docs] @dataclass class InputColumn: Name: str Type: str # STRING|INTEGER|DECIMAL|DATETIME|BIT SubType: str | None = None
[docs] @dataclass class CustomSql: Name: str DataSourceArn: str SqlQuery: str Columns: list[InputColumn]
[docs] @dataclass class PhysicalTable: """Union type — set exactly one.""" CustomSql: CustomSql | None = None
[docs] @dataclass class LogicalTableSource: PhysicalTableId: str | None = None DataSetArn: str | None = None
[docs] @dataclass class LogicalTable: Alias: str Source: LogicalTableSource DataTransforms: list[dict[str, Any]] | None = None
[docs] @dataclass class DataSetUsageConfiguration: DisableUseAsDirectQuerySource: bool = False DisableUseAsImportedSource: bool = False
# AWS QuickSight `create-data-set` rejects a dataset parameter whose # `DefaultValues.StaticValues` list has > 32 elements ("member must have # length less than or equal to 32" — the array length, not per-string # length). For a dropdown whose value universe is unbounded (rail / chain # / template / transfer_type / role names — an institution may declare # >32 of any), the default must be a short sentinel + a match-all SQL # guard (`apps/l1_dashboard`'s `_data_value_clause`, `apps/l2_flow_tracing`'s # `_match_all_in_clause`), NOT the value list (X.2.t.2). This is checked # at construction so it fails at the buggy emit line, not 10 min into a # deploy. _DATASET_PARAM_STATIC_VALUES_CAP = 32 def _check_static_values_cap( values: Sequence[object] | None, kind: str, ) -> None: if values is not None and len(values) > _DATASET_PARAM_STATIC_VALUES_CAP: raise ValueError( f"{kind}.DefaultValues.StaticValues has {len(values)} elements; " f"AWS QuickSight caps it at {_DATASET_PARAM_STATIC_VALUES_CAP}. " f"Use a 1-element sentinel default + a match-all SQL guard for an " f"unbounded value universe (see X.2.t.2 in PLAN.md)." )
[docs] @dataclass class StringDatasetParameterDefaultValues: StaticValues: list[str] | None = None def __post_init__(self) -> None: _check_static_values_cap(self.StaticValues, "StringDatasetParameter")
[docs] @dataclass class IntegerDatasetParameterDefaultValues: StaticValues: list[int] | None = None def __post_init__(self) -> None: _check_static_values_cap(self.StaticValues, "IntegerDatasetParameter")
[docs] @dataclass class DecimalDatasetParameterDefaultValues: StaticValues: list[float] | None = None def __post_init__(self) -> None: _check_static_values_cap(self.StaticValues, "DecimalDatasetParameter")
[docs] @dataclass class DateTimeDatasetParameterDefaultValues: StaticValues: list[str] | None = None # ISO8601 datetime strings def __post_init__(self) -> None: _check_static_values_cap(self.StaticValues, "DateTimeDatasetParameter")
[docs] @dataclass(kw_only=True) class StringDatasetParameter: # AK.1 — build_dataset assigns a deterministic, dataset-scoped UUID # (``auto_id(f"{dataset_id}:dsparam:{Name}")``). Construction sites do # NOT set this; "" is the unset marker the remap fills. Keeping it # app-un-settable makes a colliding hand-picked Id unrepresentable. Id: str = "" Name: str ValueType: str # SINGLE_VALUED|MULTI_VALUED DefaultValues: StringDatasetParameterDefaultValues | None = None
[docs] @dataclass(kw_only=True) class IntegerDatasetParameter: Id: str = "" # AK.1 — see StringDatasetParameter.Id Name: str ValueType: str # SINGLE_VALUED|MULTI_VALUED DefaultValues: IntegerDatasetParameterDefaultValues | None = None
[docs] @dataclass(kw_only=True) class DecimalDatasetParameter: Id: str = "" # AK.1 — see StringDatasetParameter.Id Name: str ValueType: str # SINGLE_VALUED|MULTI_VALUED DefaultValues: DecimalDatasetParameterDefaultValues | None = None
[docs] @dataclass(kw_only=True) class DateTimeDatasetParameter: Id: str = "" # AK.1 — see StringDatasetParameter.Id Name: str ValueType: str # SINGLE_VALUED|MULTI_VALUED TimeGranularity: str | None = None DefaultValues: DateTimeDatasetParameterDefaultValues | None = None
[docs] @dataclass class DatasetParameter: """Discriminated union — set exactly one variant.""" StringDatasetParameter: StringDatasetParameter | None = None IntegerDatasetParameter: IntegerDatasetParameter | None = None DecimalDatasetParameter: DecimalDatasetParameter | None = None DateTimeDatasetParameter: DateTimeDatasetParameter | None = None
[docs] @dataclass class DataSet: AwsAccountId: str DataSetId: str Name: str PhysicalTableMap: dict[str, PhysicalTable] ImportMode: str = "DIRECT_QUERY" # DIRECT_QUERY|SPICE LogicalTableMap: dict[str, LogicalTable] | None = None DataSetUsageConfiguration: DataSetUsageConfiguration | None = None Permissions: list[ResourcePermission] | None = None Tags: list[Tag] | None = None # Dataset-level parameters substituted into CustomSql via the # ``<<$paramName>>`` syntax. Bridge analysis params via # ``MappedDataSetParameters`` on each ParameterDeclaration variant. DatasetParameters: list[DatasetParameter] | None = None
[docs] def to_aws_json(self) -> dict[str, Any]: return _strip_nones(asdict(self))
[docs] def to_json_string(self, indent: int = 2) -> str: return json.dumps(self.to_aws_json(), indent=indent)
# --------------------------------------------------------------------------- # Analysis models — Visuals # --------------------------------------------------------------------------- # -- Axis labels (shared by bar, pie, etc.) --
[docs] @dataclass class AxisLabelReferenceOptions: """Bind an ``AxisLabelOptions.CustomLabel`` to a specific field-well leaf. AWS QuickSight requires the ``ApplyTo`` reference for the custom label to render on the axis — without it, ``CustomLabel`` is silently ignored (chart axis renders the raw column name). See `quicksight-quirks.md` entry on chart axis labels. """ FieldId: str Column: ColumnIdentifier
[docs] @dataclass class AxisLabelOptions: """Label override for a single axis field. ``ApplyTo`` is the binding to a specific ``FieldId`` + dataset column. AWS QuickSight requires it for ``CustomLabel`` to render — a label without ``ApplyTo`` is parsed cleanly but produces a chart that still shows the raw column name on the axis (silent no-op). v8.6.1 made the field required at the type level after v8.5.5's auto-derive-but-no-render bug. """ CustomLabel: str | None = None ApplyTo: AxisLabelReferenceOptions | None = None
[docs] @dataclass class ChartAxisLabelOptions: """Axis label options — list of per-field overrides plus visibility.""" Visibility: str = "VISIBLE" # VISIBLE|HIDDEN AxisLabelOptions: list[AxisLabelOptions] | None = None
# -- Axis display (BQ.5 — log-scale Y for one-bar-dominance charts) -- # # AWS QS API path (BarChart): BarChartConfiguration.ValueAxis is an # AxisDisplayOptions. The "Logarithmic vs Linear scale" knob lives FIVE # levels deep: # # ValueAxis: AxisDisplayOptions # └── DataOptions: AxisDataOptions # └── NumericAxisOptions: NumericAxisOptions # └── Scale: AxisScale # └── Logarithmic: AxisLogarithmicScale (Base float) # OR Linear: AxisLinearScale (Step / StepCount) # # We only need the Logarithmic shape (default base 10) — Linear is QS's # default so we never set it explicitly. The dataclasses still model both # variants so the discriminated union shape stays representable for any # future Linear-step override.
[docs] @dataclass class AxisLogarithmicScale: Base: float | None = None # default 10 per QS schema
[docs] @dataclass class AxisLinearScale: StepCount: float | None = None StepSize: float | None = None
[docs] @dataclass class AxisScale: """Discriminated union — set exactly one variant. QS rejects the payload if both Linear AND Logarithmic are non-null.""" Linear: AxisLinearScale | None = None Logarithmic: AxisLogarithmicScale | None = None
[docs] @dataclass class NumericAxisOptions: Scale: AxisScale | None = None
[docs] @dataclass class AxisDataOptions: NumericAxisOptions: NumericAxisOptions | None = None
[docs] @dataclass class AxisDisplayOptions: """The QS AxisDisplayOptions shape — we only populate ``DataOptions`` for the log-scale path. The other AxisDisplayOptions sub-fields (``TickLabelOptions`` / ``AxisLineVisibility`` / ``GridLineVisibility`` / ``ScrollbarOptions`` / ``AxisOffset``) stay None so QS defaults take over (we want default tick + grid styling on every axis we touch).""" DataOptions: AxisDataOptions | None = None
# -- Bar chart --
[docs] @dataclass class BarChartAggregatedFieldWells: Category: list[DimensionField] | None = None Values: list[MeasureField] | None = None Colors: list[DimensionField] | None = None
[docs] @dataclass class BarChartFieldWells: BarChartAggregatedFieldWells: BarChartAggregatedFieldWells | None = None
[docs] @dataclass class BarChartSortConfiguration: CategorySort: list[dict[str, Any]] | None = None
[docs] @dataclass class BarChartConfiguration: FieldWells: BarChartFieldWells | None = None Orientation: str | None = None # HORIZONTAL|VERTICAL BarsArrangement: str | None = None # CLUSTERED|STACKED|STACKED_PERCENT SortConfiguration: BarChartSortConfiguration | None = None CategoryLabelOptions: ChartAxisLabelOptions | None = None ValueLabelOptions: ChartAxisLabelOptions | None = None ColorLabelOptions: ChartAxisLabelOptions | None = None ValueAxis: AxisDisplayOptions | None = None # BQ.5 log-scale wire
[docs] @dataclass class BarChartVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: BarChartConfiguration | None = None Actions: list[VisualCustomAction] | None = None
# -- Line chart --
[docs] @dataclass class LineChartAggregatedFieldWells: Category: list[DimensionField] | None = None # x-axis Values: list[MeasureField] | None = None # y-axis Colors: list[DimensionField] | None = None # one line per color value
[docs] @dataclass class LineChartFieldWells: LineChartAggregatedFieldWells: LineChartAggregatedFieldWells | None = None
[docs] @dataclass class LineChartSortConfiguration: CategorySort: list[dict[str, Any]] | None = None
[docs] @dataclass class LineChartConfiguration: FieldWells: LineChartFieldWells | None = None Type: str | None = None # LINE | AREA | STACKED_AREA — default LINE SortConfiguration: LineChartSortConfiguration | None = None XAxisLabelOptions: ChartAxisLabelOptions | None = None PrimaryYAxisLabelOptions: ChartAxisLabelOptions | None = None
[docs] @dataclass class LineChartVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: LineChartConfiguration | None = None Actions: list[VisualCustomAction] | None = None
# -- Pie chart --
[docs] @dataclass class PieChartAggregatedFieldWells: Category: list[DimensionField] | None = None Values: list[MeasureField] | None = None
[docs] @dataclass class PieChartFieldWells: PieChartAggregatedFieldWells: PieChartAggregatedFieldWells | None = None
[docs] @dataclass class DonutOptions: ArcOptions: dict[str, str] | None = None # {"ArcThickness": "MEDIUM"}
[docs] @dataclass class PieChartConfiguration: FieldWells: PieChartFieldWells | None = None DonutOptions: DonutOptions | None = None CategoryLabelOptions: ChartAxisLabelOptions | None = None ValueLabelOptions: ChartAxisLabelOptions | None = None
[docs] @dataclass class PieChartVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: PieChartConfiguration | None = None Actions: list[VisualCustomAction] | None = None
# -- KPI --
[docs] @dataclass class KPIFieldWells: Values: list[MeasureField] | None = None TargetValues: list[MeasureField] | None = None TrendGroups: list[DimensionField] | None = None
[docs] @dataclass class KPIOptions: """KPI display options. QS rejects partial KPIOptions when the field-well has no TargetValue/TrendGroup (verified against hand-built control KPI on 2026-04-29 — `KPIOptions(PrimaryValueDisplayType="ACTUAL")` alone got rejected with "Only PrimaryValueFontSize display property can be defined..."). The hand-built shape — Comparison + PrimaryValueDisplayType + SecondaryValueFontConfiguration + Sparkline + VisualLayoutOptions — is the smallest set QS UI produces and accepts. Tree's KPI.emit() defaults to that shape. """ Comparison: dict[str, Any] | None = None PrimaryValueDisplayType: str | None = None # HIDDEN|COMPARISON|ACTUAL SecondaryValueFontConfiguration: dict[str, Any] | None = None Sparkline: dict[str, Any] | None = None VisualLayoutOptions: dict[str, Any] | None = None
[docs] @dataclass class KPIConfiguration: FieldWells: KPIFieldWells | None = None KPIOptions: KPIOptions | None = None
[docs] @dataclass class KPIVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: KPIConfiguration | None = None # BK.2 — KPIConditionalFormatting free-form dict (QS's KPI # conditional-formatting shape is a typed nested-dict of options; # the tree's KPIValueZeroIndicator emits the canonical pair-of- # CHECKMARK/X options). Same pass-through pattern Table's # ConditionalFormatting follows. ConditionalFormatting: dict[str, Any] | None = None
# -- Table --
[docs] @dataclass class TableAggregatedFieldWells: GroupBy: list[DimensionField] | None = None Values: list[MeasureField] | None = None
[docs] @dataclass class TableUnaggregatedFieldWells: Values: list[dict[str, Any]] | None = None # UnaggregatedField list
[docs] @dataclass class TableFieldWells: TableAggregatedFieldWells: TableAggregatedFieldWells | None = None TableUnaggregatedFieldWells: TableUnaggregatedFieldWells | None = None
[docs] @dataclass class TableOptions: HeaderStyle: dict[str, Any] | None = None CellStyle: dict[str, Any] | None = None
[docs] @dataclass class TableFieldOption: """Per-column header override for a Table visual (v8.5.0). ``FieldId`` references one of the Table's field-well leaves (Dim or Measure). ``CustomLabel`` overrides the header text QuickSight would otherwise auto-derive from the column name. ``Visibility`` defaults to ``"VISIBLE"``. """ FieldId: str CustomLabel: str | None = None Visibility: str = "VISIBLE" # VISIBLE | HIDDEN
[docs] @dataclass class TableFieldOptions: """Container for per-column table header overrides (v8.5.0).""" SelectedFieldOptions: list[TableFieldOption] | None = None
[docs] @dataclass class TableConfiguration: FieldWells: TableFieldWells | None = None SortConfiguration: dict[str, Any] | None = None TableOptions: TableOptions | None = None FieldOptions: TableFieldOptions | None = None
[docs] @dataclass class TableVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: TableConfiguration | None = None Actions: list[VisualCustomAction] | None = None ConditionalFormatting: dict[str, Any] | None = None
# -- Sankey diagram --
[docs] @dataclass class SankeyDiagramAggregatedFieldWells: Source: list[DimensionField] | None = None Destination: list[DimensionField] | None = None Weight: list[MeasureField] | None = None
[docs] @dataclass class SankeyDiagramFieldWells: SankeyDiagramAggregatedFieldWells: SankeyDiagramAggregatedFieldWells | None = None
[docs] @dataclass class SankeyDiagramSortConfiguration: # ItemsLimitConfiguration shape: {"ItemsLimit": int, "OtherCategories": "INCLUDE"|"EXCLUDE"}. # Caps how many distinct source / destination nodes the diagram # renders; over-cap entries roll up into "Other" or get dropped. WeightSort: list[dict[str, Any]] | None = None SourceItemsLimit: dict[str, Any] | None = None DestinationItemsLimit: dict[str, Any] | None = None
[docs] @dataclass class SankeyDiagramChartConfiguration: FieldWells: SankeyDiagramFieldWells | None = None SortConfiguration: SankeyDiagramSortConfiguration | None = None DataLabels: dict[str, Any] | None = None
[docs] @dataclass class SankeyDiagramVisual: VisualId: str Title: VisualTitleLabelOptions | None = None Subtitle: VisualSubtitleLabelOptions | None = None ChartConfiguration: SankeyDiagramChartConfiguration | None = None Actions: list[VisualCustomAction] | None = None
# -- Custom actions (drill-down navigation, filtering) --
[docs] @dataclass class LocalNavigationConfiguration: TargetSheetId: str
[docs] @dataclass class CustomActionNavigationOperation: LocalNavigationConfiguration: LocalNavigationConfiguration
[docs] @dataclass class CustomActionSetParametersOperation: ParameterValueConfigurations: list[dict[str, Any]]
[docs] @dataclass class SameSheetTargetVisualConfiguration: TargetVisualOptions: str | None = None # ALL_VISUALS TargetVisuals: list[str] | None = None
[docs] @dataclass class FilterOperationTargetVisualsConfiguration: SameSheetTargetVisualConfiguration: SameSheetTargetVisualConfiguration | None = None
[docs] @dataclass class FilterOperationSelectedFieldsConfiguration: SelectedFieldOptions: str | None = None # ALL_FIELDS SelectedFields: list[str] | None = None SelectedColumns: list[ColumnIdentifier] | None = None
[docs] @dataclass class CustomActionFilterOperation: SelectedFieldsConfiguration: FilterOperationSelectedFieldsConfiguration TargetVisualsConfiguration: FilterOperationTargetVisualsConfiguration
[docs] @dataclass class VisualCustomActionOperation: """Union type — set exactly one.""" NavigationOperation: CustomActionNavigationOperation | None = None SetParametersOperation: CustomActionSetParametersOperation | None = None FilterOperation: CustomActionFilterOperation | None = None
[docs] @dataclass class VisualCustomAction: # Trigger constants — prefer VisualCustomAction.DATA_POINT_CLICK # over the bare string literal at call sites. DATA_POINT_CLICK: ClassVar[Literal["DATA_POINT_CLICK"]] = "DATA_POINT_CLICK" DATA_POINT_MENU: ClassVar[Literal["DATA_POINT_MENU"]] = "DATA_POINT_MENU" # Status constants. ENABLED: ClassVar[Literal["ENABLED"]] = "ENABLED" DISABLED: ClassVar[Literal["DISABLED"]] = "DISABLED" CustomActionId: str Name: str Trigger: Literal["DATA_POINT_CLICK", "DATA_POINT_MENU"] ActionOperations: list[VisualCustomActionOperation] Status: Literal["ENABLED", "DISABLED"] = "ENABLED"
# -- Visual union --
[docs] @dataclass class Visual: """Union type — set exactly one.""" BarChartVisual: BarChartVisual | None = None LineChartVisual: LineChartVisual | None = None PieChartVisual: PieChartVisual | None = None KPIVisual: KPIVisual | None = None TableVisual: TableVisual | None = None SankeyDiagramVisual: SankeyDiagramVisual | None = None
# --------------------------------------------------------------------------- # Analysis models — Filters # ---------------------------------------------------------------------------
[docs] @dataclass class DefaultDateTimePickerControlOptions: Type: str = "DATE_RANGE" # SINGLE_VALUED|DATE_RANGE CommitMode: str | None = None # AUTO|MANUAL
[docs] @dataclass class DefaultDropdownControlOptions: Type: str = "MULTI_SELECT" # MULTI_SELECT|SINGLE_SELECT CommitMode: str | None = None # AUTO|MANUAL
[docs] @dataclass class DefaultSliderControlOptions: MaximumValue: float MinimumValue: float StepSize: float Type: str = "SINGLE_POINT" # SINGLE_POINT|RANGE
[docs] @dataclass class DefaultFilterControlOptions: """Union type — set exactly one.""" DefaultDateTimePickerOptions: DefaultDateTimePickerControlOptions | None = None DefaultDropdownOptions: DefaultDropdownControlOptions | None = None DefaultSliderOptions: DefaultSliderControlOptions | None = None
[docs] @dataclass class DefaultFilterControlConfiguration: Title: str ControlOptions: DefaultFilterControlOptions
[docs] @dataclass class CategoryFilterConfiguration: FilterListConfiguration: dict[str, Any] | None = None CustomFilterListConfiguration: dict[str, Any] | None = None CustomFilterConfiguration: dict[str, Any] | None = None
[docs] @dataclass class CategoryFilter: FilterId: str Column: ColumnIdentifier Configuration: CategoryFilterConfiguration DefaultFilterControlConfiguration: DefaultFilterControlConfiguration | None = None
[docs] @dataclass class TimeRangeFilter: FilterId: str Column: ColumnIdentifier NullOption: str = "NON_NULLS_ONLY" # ALL_VALUES|NULLS_ONLY|NON_NULLS_ONLY TimeGranularity: str | None = None RangeMinimumValue: dict[str, Any] | None = None RangeMaximumValue: dict[str, Any] | None = None IncludeMinimum: bool | None = None IncludeMaximum: bool | None = None DefaultFilterControlConfiguration: DefaultFilterControlConfiguration | None = None
[docs] @dataclass class TimeEqualityFilter: FilterId: str Column: ColumnIdentifier Value: str | None = None # ISO datetime; pair with TimeGranularity ParameterName: str | None = None TimeGranularity: str | None = None RollingDate: dict[str, Any] | None = None DefaultFilterControlConfiguration: DefaultFilterControlConfiguration | None = None
[docs] @dataclass class NumericRangeFilterValue: """Set exactly one — a literal bound or a parameter binding.""" StaticValue: float | None = None Parameter: str | None = None # name of an IntegerParameter / DecimalParameter
[docs] @dataclass class NumericRangeFilter: FilterId: str Column: ColumnIdentifier NullOption: str = "NON_NULLS_ONLY" RangeMinimum: NumericRangeFilterValue | None = None RangeMaximum: NumericRangeFilterValue | None = None IncludeMinimum: bool | None = None IncludeMaximum: bool | None = None DefaultFilterControlConfiguration: DefaultFilterControlConfiguration | None = None
[docs] @dataclass class Filter: """Union type — set exactly one.""" CategoryFilter: CategoryFilter | None = None TimeRangeFilter: TimeRangeFilter | None = None TimeEqualityFilter: TimeEqualityFilter | None = None NumericRangeFilter: NumericRangeFilter | None = None
[docs] @dataclass class SheetVisualScopingConfiguration: # Scope constants — prefer SheetVisualScopingConfiguration.ALL_VISUALS # over the bare string literal at call sites. ALL_VISUALS: ClassVar[Literal["ALL_VISUALS"]] = "ALL_VISUALS" SELECTED_VISUALS: ClassVar[Literal["SELECTED_VISUALS"]] = "SELECTED_VISUALS" SheetId: str Scope: Literal["ALL_VISUALS", "SELECTED_VISUALS"] VisualIds: list[str] | None = None
[docs] @dataclass class SelectedSheetsFilterScopeConfiguration: SheetVisualScopingConfigurations: list[SheetVisualScopingConfiguration] | None = None
[docs] @dataclass class AllSheetsFilterScopeConfiguration: pass # empty object — presence alone means "all sheets"
[docs] @dataclass class FilterScopeConfiguration: """Union type — set exactly one.""" AllSheets: AllSheetsFilterScopeConfiguration | None = None SelectedSheets: SelectedSheetsFilterScopeConfiguration | None = None
[docs] @dataclass class FilterGroup: # CrossDataset constants — prefer FilterGroup.SINGLE_DATASET over the # bare string literal at call sites. SINGLE_DATASET: ClassVar[Literal["SINGLE_DATASET"]] = "SINGLE_DATASET" ALL_DATASETS: ClassVar[Literal["ALL_DATASETS"]] = "ALL_DATASETS" # Status constants. ENABLED: ClassVar[Literal["ENABLED"]] = "ENABLED" DISABLED: ClassVar[Literal["DISABLED"]] = "DISABLED" FilterGroupId: str Filters: list[Filter] ScopeConfiguration: FilterScopeConfiguration CrossDataset: Literal["SINGLE_DATASET", "ALL_DATASETS"] = "SINGLE_DATASET" Status: Literal["ENABLED", "DISABLED"] | None = None
# --------------------------------------------------------------------------- # Analysis models — Filter controls # ---------------------------------------------------------------------------
[docs] @dataclass class FilterDropDownControl: FilterControlId: str Title: str SourceFilterId: str Type: str | None = None # MULTI_SELECT|SINGLE_SELECT # FilterSelectableValues shape: {"Values": [str, ...]}. Restricts the # dropdown menu to a fixed list of options instead of auto-populating # from the column. Useful for toggle-like controls where only one # option (e.g. "Unsettled") should be pickable. SelectableValues: dict[str, Any] | None = None
[docs] @dataclass class FilterDateTimePickerControl: FilterControlId: str Title: str SourceFilterId: str Type: str | None = None # SINGLE_VALUED|DATE_RANGE
[docs] @dataclass class FilterSliderControl: FilterControlId: str Title: str SourceFilterId: str MaximumValue: float MinimumValue: float StepSize: float Type: str | None = None # SINGLE_POINT|RANGE
[docs] @dataclass class FilterCrossSheetControl: FilterControlId: str SourceFilterId: str
[docs] @dataclass class FilterControl: """Union type — set exactly one.""" Dropdown: FilterDropDownControl | None = None DateTimePicker: FilterDateTimePickerControl | None = None Slider: FilterSliderControl | None = None CrossSheet: FilterCrossSheetControl | None = None
# --------------------------------------------------------------------------- # Analysis models — Parameter controls # # QuickSight disables a regular FilterControl whose backing filter is # parameter-bound (CustomFilterConfiguration with ParameterName) — the UI # shows "this control was disabled because the filter is using # parameters". The right widget for that case is a ParameterControl # bound directly to the parameter; the parameter-bound filter then # responds to the parameter value the control writes. # ---------------------------------------------------------------------------
[docs] @dataclass class ParameterDropDownControl: ParameterControlId: str Title: str SourceParameterName: str Type: str | None = None # SINGLE_SELECT|MULTI_SELECT # ParameterSelectableValues shape: either {"Values": [str, ...]} # for a static list or {"LinkToDataSetColumn": {"DataSetIdentifier", # "ColumnName"}} for an auto-populated list. The link query bypasses # the sheet's parameter-bound filter so users see every available # option, not the filtered slice. SelectableValues: dict[str, Any] | None = None # DropDownControlDisplayOptions shape: e.g. # {"SelectAllOptions": {"Visibility": "HIDDEN"}} # to suppress the "Select all" entry. Useful for SINGLE_SELECT # dropdowns where empty/All semantics don't apply (e.g., a Sankey # anchor that needs exactly one value to render). DisplayOptions: dict[str, Any] | None = None # UI-level cascade wiring: when any control listed in # SourceControls changes, refresh THIS control's options. Without # this, dataset-parameter-bridged cascades (MappedDataSetParameters) # don't trigger dropdown refresh — the dataset re-queries on # parameter change but the control's cached snapshot of options # stays stale. CascadingControlConfiguration: CascadingControlConfiguration | None = None
[docs] @dataclass class ParameterDateTimePickerControl: ParameterControlId: str Title: str SourceParameterName: str
[docs] @dataclass class ParameterSliderControl: ParameterControlId: str Title: str SourceParameterName: str MinimumValue: float MaximumValue: float StepSize: float
[docs] @dataclass class ParameterTextFieldControl: """Free-text input bound to a string parameter. The analyst types a value; QS sets the bound parameter to that value (no enumeration). Right shape for parameters whose option universe is unbounded / unknown at deploy time, or where the LinkedValues / StaticValues sample-fetch path is misbehaving (the X.1.b L2FT cascade Value dropdown ran into ``Sample values not found`` from QS's lazy sample-values fetch on cold per-CI-run dashboards; text input has no equivalent fetch path). """ ParameterControlId: str Title: str SourceParameterName: str
[docs] @dataclass class ParameterControl: """Union type — set exactly one.""" Dropdown: ParameterDropDownControl | None = None DateTimePicker: ParameterDateTimePickerControl | None = None Slider: ParameterSliderControl | None = None TextField: ParameterTextFieldControl | None = None
# --------------------------------------------------------------------------- # Analysis models — Sheet & Layout # ---------------------------------------------------------------------------
[docs] @dataclass class FreeFormLayoutElement: # ElementType constants — prefer FreeFormLayoutElement.VISUAL over # the bare string literal at call sites. VISUAL: ClassVar[Literal["VISUAL"]] = "VISUAL" FILTER_CONTROL: ClassVar[Literal["FILTER_CONTROL"]] = "FILTER_CONTROL" PARAMETER_CONTROL: ClassVar[Literal["PARAMETER_CONTROL"]] = "PARAMETER_CONTROL" TEXT_BOX: ClassVar[Literal["TEXT_BOX"]] = "TEXT_BOX" IMAGE: ClassVar[Literal["IMAGE"]] = "IMAGE" ElementId: str ElementType: Literal[ "VISUAL", "FILTER_CONTROL", "PARAMETER_CONTROL", "TEXT_BOX", "IMAGE" ] XAxisLocation: str # pixels as string YAxisLocation: str Width: str Height: str Visibility: str = "VISIBLE"
[docs] @dataclass class FreeFormLayoutConfiguration: Elements: list[FreeFormLayoutElement]
[docs] @dataclass class GridLayoutElement: # ElementType constants — prefer GridLayoutElement.VISUAL over the # bare string literal at call sites. VISUAL: ClassVar[Literal["VISUAL"]] = "VISUAL" FILTER_CONTROL: ClassVar[Literal["FILTER_CONTROL"]] = "FILTER_CONTROL" PARAMETER_CONTROL: ClassVar[Literal["PARAMETER_CONTROL"]] = "PARAMETER_CONTROL" TEXT_BOX: ClassVar[Literal["TEXT_BOX"]] = "TEXT_BOX" IMAGE: ClassVar[Literal["IMAGE"]] = "IMAGE" ElementId: str ElementType: Literal[ "VISUAL", "FILTER_CONTROL", "PARAMETER_CONTROL", "TEXT_BOX", "IMAGE" ] ColumnSpan: int RowSpan: int ColumnIndex: int | None = None RowIndex: int | None = None # v8.6.9 — QS UI calls this "Card layout padding"; CSS-shaped string # like ``"12px"``. Applies to any grid element type (VISUAL / TEXT_BOX # / etc.) and gives the rendered card interior breathing room. None # falls back to QS's bare default (no padding). Padding: str | None = None
[docs] @dataclass class GridLayoutConfiguration: Elements: list[GridLayoutElement] # M.4.4.10ab — QS UI emits CanvasSizeOptions inside every GridLayout. # Without it the editor fails when adding visuals (verified against # hand-built control on 2026-04-29). dict[str, Any] for now — # the shape is `{ScreenCanvasSizeOptions: {ResizeOption: FIXED, # OptimizedViewPortWidth: "1600px"}}`. Promote to typed dataclass # if a third call site needs it. CanvasSizeOptions: dict[str, Any] | None = None
[docs] @dataclass class LayoutConfiguration: """Union type — set exactly one.""" GridLayout: GridLayoutConfiguration | None = None FreeFormLayout: FreeFormLayoutConfiguration | None = None
[docs] @dataclass class Layout: Configuration: LayoutConfiguration
[docs] @dataclass class SheetTextBox: SheetTextBoxId: str Content: str # rich-text HTML
[docs] @dataclass class SheetDefinition: SheetId: str Name: str | None = None Title: str | None = None Description: str | None = None ContentType: str = "INTERACTIVE" # INTERACTIVE|PAGINATED Visuals: list[Visual] | None = None FilterControls: list[FilterControl] | None = None ParameterControls: list[ParameterControl] | None = None Layouts: list[Layout] | None = None TextBoxes: list[SheetTextBox] | None = None
# --------------------------------------------------------------------------- # Analysis models — Top-level # ---------------------------------------------------------------------------
[docs] @dataclass class DataSetIdentifierDeclaration: Identifier: str DataSetArn: str
[docs] @dataclass class MappedDataSetParameter: """Bridges an analysis-level parameter to a dataset-level parameter. When the analysis parameter changes, QuickSight pushes the value into the named dataset parameter — which then substitutes into the dataset's CustomSql via ``<<$paramName>>`` at query time. The mapping list lives on the analysis ParameterDeclaration variant (StringParameterDeclaration, etc.). """ DataSetIdentifier: str DataSetParameterName: str
[docs] @dataclass class CascadingControlSource: """One source control in a CascadingControlConfiguration. ``SourceSheetControlId`` is the upstream control's ID; QS refreshes THIS control whenever that source control's value changes. ``ColumnToMatch`` is the documented value-match hint (used by column-on-column cascades); for parameter-bridged cascades it can be the dependent control's own source column — it doubles as a "this column is the one that varies with the source" marker. """ SourceSheetControlId: str | None = None ColumnToMatch: ColumnIdentifier | None = None
[docs] @dataclass class CascadingControlConfiguration: """UI-level cascade wiring: tells QS to refresh THIS control's options when any of the listed source controls change. Required for the M.3.10c metadata cascade — without this, even with `MappedDataSetParameters` correctly bridging analysis params to dataset params, QS won't refresh the dependent dropdown's options when the source dropdown changes (the dataset query substitution stays "pending" until something else triggers a refresh, like a sheet reload). """ SourceControls: list[CascadingControlSource] | None = None
[docs] @dataclass class StringParameterDeclaration: ParameterValueType: str # SINGLE_VALUED|MULTI_VALUED Name: str DefaultValues: dict[str, Any] MappedDataSetParameters: list[MappedDataSetParameter] | None = None # BR.x — QS analysis-editor "Set reserved value" advanced setting. # Shape: {"ValueWhenUnsetOption": "RECOMMENDED_VALUE"|"NULL", "CustomValue": "<str>"}. # Required when a parameter feeds a CascadingControlConfiguration — # cascade's internal match expression substitutes this value when # the parameter is "unset"; the QS UI default of NULL makes the # cascade expression invalid ("calculated field has invalid syntax" # on the target control). See docs/reference/quicksight-quirks.md # — unmapped DatasetParameter entry, cascade tooltip section. ValueWhenUnset: dict[str, Any] | None = None
[docs] @dataclass class IntegerParameterDeclaration: ParameterValueType: str # SINGLE_VALUED|MULTI_VALUED Name: str DefaultValues: dict[str, Any] # {"StaticValues": [int]} MappedDataSetParameters: list[MappedDataSetParameter] | None = None ValueWhenUnset: dict[str, Any] | None = None
[docs] @dataclass class DateTimeDefaultValues: StaticValues: list[str] | None = None DynamicValue: dict[str, Any] | None = None RollingDate: dict[str, Any] | None = None
[docs] @dataclass class DateTimeParameterDeclaration: Name: str TimeGranularity: str | None = None DefaultValues: DateTimeDefaultValues | None = None ValueWhenUnset: dict[str, Any] | None = None MappedDataSetParameters: list[MappedDataSetParameter] | None = None
[docs] @dataclass class ParameterDeclaration: """Union type — set exactly one.""" StringParameterDeclaration: StringParameterDeclaration | None = None IntegerParameterDeclaration: IntegerParameterDeclaration | None = None DateTimeParameterDeclaration: DateTimeParameterDeclaration | None = None
[docs] @dataclass class AnalysisDefinition: DataSetIdentifierDeclarations: list[DataSetIdentifierDeclaration] Sheets: list[SheetDefinition] | None = None FilterGroups: list[FilterGroup] | None = None ParameterDeclarations: list[ParameterDeclaration] | None = None CalculatedFields: list[dict[str, Any]] | None = None # M.4.4.10ab — three top-level fields QS UI emits + appears to # require for editor compat. Without them the analysis loads but # adding visuals/sheets through the editor fails (verified against # hand-built control on 2026-04-29). dict[str, Any] until a typed # API materializes; defaults match QS UI's outputs exactly. Options: dict[str, Any] | None = None AnalysisDefaults: dict[str, Any] | None = None QueryExecutionOptions: dict[str, Any] | None = None
[docs] @dataclass class Analysis: AwsAccountId: str AnalysisId: str Name: str Definition: AnalysisDefinition ThemeArn: str | None = None Permissions: list[ResourcePermission] | None = None Tags: list[Tag] | None = None
[docs] def to_aws_json(self) -> dict[str, Any]: return _strip_nones(asdict(self))
[docs] def to_json_string(self, indent: int = 2) -> str: return json.dumps(self.to_aws_json(), indent=indent)
# --------------------------------------------------------------------------- # Dashboard models # ---------------------------------------------------------------------------
[docs] @dataclass class DashboardPublishOptions: AdHocFilteringOption: dict[str, str] | None = None ExportToCSVOption: dict[str, str] | None = None SheetControlsOption: dict[str, str] | None = None
[docs] @dataclass class LinkSharingConfiguration: Permissions: list[ResourcePermission] | None = None
[docs] @dataclass class Dashboard: AwsAccountId: str DashboardId: str Name: str Definition: AnalysisDefinition ThemeArn: str | None = None Permissions: list[ResourcePermission] | None = None Tags: list[Tag] | None = None VersionDescription: str | None = None DashboardPublishOptions: DashboardPublishOptions | None = None LinkSharingConfiguration: LinkSharingConfiguration | None = None
[docs] def to_aws_json(self) -> dict[str, Any]: return _strip_nones(asdict(self))
[docs] def to_json_string(self, indent: int = 2) -> str: return json.dumps(self.to_aws_json(), indent=indent)