dataface 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- mdsvg/utils.py +86 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
"""Layout resolution and unified layout construction."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
8
|
+
|
|
9
|
+
from dataface.core.compile.errors import CompilationError, ReferenceError
|
|
10
|
+
from dataface.core.compile.models.chart.authored import (
|
|
11
|
+
_SharedChartFields,
|
|
12
|
+
)
|
|
13
|
+
from dataface.core.compile.models.chart.compiled import (
|
|
14
|
+
Chart,
|
|
15
|
+
)
|
|
16
|
+
from dataface.core.compile.models.face.authored import (
|
|
17
|
+
AuthoredFace,
|
|
18
|
+
ForeachConfig,
|
|
19
|
+
ForeachItem,
|
|
20
|
+
ForeachQuery,
|
|
21
|
+
GridLayout,
|
|
22
|
+
LayoutType,
|
|
23
|
+
TabLayout,
|
|
24
|
+
)
|
|
25
|
+
from dataface.core.compile.models.face.compiled import Face, Layout, LayoutItem
|
|
26
|
+
from dataface.core.compile.models.query.compiled import AnyQuery
|
|
27
|
+
from dataface.core.compile.models.style.authored import StylePatch
|
|
28
|
+
from dataface.core.compile.models.style.merged import MergedStyle
|
|
29
|
+
from dataface.core.compile.normalize_charts import (
|
|
30
|
+
_generate_inline_chart_id,
|
|
31
|
+
_normalize_chart,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _validate_style_patch(style_dict: dict[str, Any]) -> StylePatch:
|
|
36
|
+
"""Validate a raw style dict and return a StylePatch.
|
|
37
|
+
|
|
38
|
+
Re-raises PydanticValidationError with ``style.`` prepended to each loc
|
|
39
|
+
so errors from nested-face style blocks say ``style.bad_key`` rather than
|
|
40
|
+
the bare ``bad_key`` that a standalone StylePatch.model_validate raises.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
return StylePatch.model_validate(style_dict)
|
|
44
|
+
except PydanticValidationError as exc:
|
|
45
|
+
raise PydanticValidationError.from_exception_data(
|
|
46
|
+
title=exc.title,
|
|
47
|
+
input_type="python",
|
|
48
|
+
line_errors=[
|
|
49
|
+
{
|
|
50
|
+
"type": err["type"],
|
|
51
|
+
"loc": ("style",) + err["loc"],
|
|
52
|
+
"input": err["input"],
|
|
53
|
+
"ctx": err.get("ctx", {}),
|
|
54
|
+
}
|
|
55
|
+
for err in exc.errors()
|
|
56
|
+
],
|
|
57
|
+
) from exc
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_dimension(value: Any, name: str) -> None:
|
|
61
|
+
"""Reject zero/negative dimension values at compile time.
|
|
62
|
+
|
|
63
|
+
Uses ``parse_dimension`` from the sizing module so the validator and
|
|
64
|
+
the sizing engine agree on what constitutes a valid dimension.
|
|
65
|
+
``total=1.0`` makes percentages yield their numeric fraction (e.g.
|
|
66
|
+
``"50%"`` → 0.5) so the sign check works uniformly.
|
|
67
|
+
"""
|
|
68
|
+
if value is None:
|
|
69
|
+
return
|
|
70
|
+
from dataface.core.compile.sizing import parse_dimension
|
|
71
|
+
|
|
72
|
+
parsed = parse_dimension(str(value), 1.0)
|
|
73
|
+
if parsed is None:
|
|
74
|
+
raise CompilationError(
|
|
75
|
+
f"'{name}: {value}' is not a valid dimension"
|
|
76
|
+
" (use e.g. '200', '200px', or '50%')"
|
|
77
|
+
)
|
|
78
|
+
if parsed <= 0:
|
|
79
|
+
raise CompilationError(f"'{name}: {value}' must be positive (got {parsed})")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_unified_layout(
|
|
83
|
+
face: AuthoredFace,
|
|
84
|
+
charts: dict[str, Chart],
|
|
85
|
+
query_registry: dict[str, AnyQuery],
|
|
86
|
+
face_id: str,
|
|
87
|
+
depth: int,
|
|
88
|
+
base_path: Path | None = None,
|
|
89
|
+
default_source: str | None = None,
|
|
90
|
+
chart_registry: dict[str, Any] | None = None,
|
|
91
|
+
theme: str | None = None,
|
|
92
|
+
parent_variables: dict[str, Any] | None = None,
|
|
93
|
+
*,
|
|
94
|
+
_resolve_jinja: bool = False,
|
|
95
|
+
resolved_style: MergedStyle,
|
|
96
|
+
parent_level: int = 0,
|
|
97
|
+
) -> Layout:
|
|
98
|
+
"""Build a unified Layout from face layout fields.
|
|
99
|
+
|
|
100
|
+
Converts rows/cols/grid/tabs to unified Layout structure.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
face: AuthoredFace with layout fields
|
|
104
|
+
charts: Available charts for resolution
|
|
105
|
+
query_registry: Query registry for inline charts
|
|
106
|
+
face_id: Parent face ID
|
|
107
|
+
depth: Nesting depth
|
|
108
|
+
base_path: Base path for resolving external file references
|
|
109
|
+
default_source: Default source for inline queries
|
|
110
|
+
theme: Vega-Lite theme to inherit to nested faces
|
|
111
|
+
parent_variables: Variables from parent scope for template resolution
|
|
112
|
+
parent_level: Semantic heading level of the enclosing face. Passed
|
|
113
|
+
through to nested normalize_face calls so children compute their
|
|
114
|
+
own level as parent_level + (1 if they have a title else 0).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Unified Layout structure
|
|
118
|
+
"""
|
|
119
|
+
from dataface.core.compile.normalizer import slugify
|
|
120
|
+
|
|
121
|
+
# Track used chart IDs across all layout items to avoid duplicates
|
|
122
|
+
used_chart_ids = set(charts.keys())
|
|
123
|
+
|
|
124
|
+
if face.rows:
|
|
125
|
+
items = _resolve_layout_items(
|
|
126
|
+
face.rows,
|
|
127
|
+
charts,
|
|
128
|
+
query_registry,
|
|
129
|
+
face_id,
|
|
130
|
+
depth,
|
|
131
|
+
"row",
|
|
132
|
+
base_path,
|
|
133
|
+
default_source,
|
|
134
|
+
chart_registry,
|
|
135
|
+
theme,
|
|
136
|
+
used_chart_ids,
|
|
137
|
+
parent_variables,
|
|
138
|
+
_resolve_jinja=_resolve_jinja,
|
|
139
|
+
parent_level=parent_level,
|
|
140
|
+
)
|
|
141
|
+
return Layout(type=LayoutType.ROWS, items=items)
|
|
142
|
+
|
|
143
|
+
elif face.cols:
|
|
144
|
+
items = _resolve_layout_items(
|
|
145
|
+
face.cols,
|
|
146
|
+
charts,
|
|
147
|
+
query_registry,
|
|
148
|
+
face_id,
|
|
149
|
+
depth,
|
|
150
|
+
"col",
|
|
151
|
+
base_path,
|
|
152
|
+
default_source,
|
|
153
|
+
chart_registry,
|
|
154
|
+
theme,
|
|
155
|
+
used_chart_ids,
|
|
156
|
+
parent_variables,
|
|
157
|
+
_resolve_jinja=_resolve_jinja,
|
|
158
|
+
parent_level=parent_level,
|
|
159
|
+
)
|
|
160
|
+
return Layout(type=LayoutType.COLS, items=items)
|
|
161
|
+
|
|
162
|
+
elif face.grid:
|
|
163
|
+
grid_obj = face.grid
|
|
164
|
+
items = _resolve_grid_items(
|
|
165
|
+
grid_obj,
|
|
166
|
+
charts,
|
|
167
|
+
query_registry,
|
|
168
|
+
face_id,
|
|
169
|
+
depth,
|
|
170
|
+
base_path,
|
|
171
|
+
default_source,
|
|
172
|
+
chart_registry,
|
|
173
|
+
theme,
|
|
174
|
+
used_chart_ids,
|
|
175
|
+
parent_variables,
|
|
176
|
+
_resolve_jinja=_resolve_jinja,
|
|
177
|
+
parent_level=parent_level,
|
|
178
|
+
)
|
|
179
|
+
return Layout(
|
|
180
|
+
type=LayoutType.GRID,
|
|
181
|
+
items=items,
|
|
182
|
+
columns=grid_obj.columns,
|
|
183
|
+
gap=grid_obj.gap,
|
|
184
|
+
row_height=grid_obj.row_height,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
elif face.tabs:
|
|
188
|
+
tabs_obj = face.tabs
|
|
189
|
+
items, tab_titles = _resolve_tab_items(
|
|
190
|
+
tabs_obj,
|
|
191
|
+
query_registry,
|
|
192
|
+
face_id,
|
|
193
|
+
depth,
|
|
194
|
+
base_path,
|
|
195
|
+
default_source,
|
|
196
|
+
chart_registry,
|
|
197
|
+
theme,
|
|
198
|
+
_resolve_jinja=_resolve_jinja,
|
|
199
|
+
resolved_style=resolved_style,
|
|
200
|
+
parent_level=parent_level,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Compute tab slugs and variable name
|
|
204
|
+
tab_slugs = [slugify(title) for title in tab_titles]
|
|
205
|
+
# Validate slugs
|
|
206
|
+
for i, slug in enumerate(tab_slugs):
|
|
207
|
+
if not slug:
|
|
208
|
+
raise CompilationError(
|
|
209
|
+
f"Tab title '{tab_titles[i]}' produces an empty slug. "
|
|
210
|
+
"Tab titles must contain at least one alphanumeric character."
|
|
211
|
+
)
|
|
212
|
+
# Check for slug collisions
|
|
213
|
+
if len(set(tab_slugs)) != len(tab_slugs):
|
|
214
|
+
seen: set[str] = set()
|
|
215
|
+
for slug in tab_slugs:
|
|
216
|
+
if slug in seen:
|
|
217
|
+
raise CompilationError(
|
|
218
|
+
f"Duplicate tab slug '{slug}'. Tab titles must produce "
|
|
219
|
+
"unique slugs (after lowercasing and replacing spaces with _)."
|
|
220
|
+
)
|
|
221
|
+
seen.add(slug)
|
|
222
|
+
|
|
223
|
+
# Use explicit id, or generate a unique name from face_id
|
|
224
|
+
# (avoids collisions when multiple tabs layouts exist in nested faces)
|
|
225
|
+
tab_variable = tabs_obj.id or f"_tab_{face_id}"
|
|
226
|
+
|
|
227
|
+
# Resolve default tab from slug
|
|
228
|
+
default_slug = slugify(tabs_obj.default) if tabs_obj.default else tab_slugs[0]
|
|
229
|
+
default_tab = tab_slugs.index(default_slug) if default_slug in tab_slugs else 0
|
|
230
|
+
|
|
231
|
+
return Layout(
|
|
232
|
+
type=LayoutType.TABS,
|
|
233
|
+
items=items,
|
|
234
|
+
tab_titles=tab_titles,
|
|
235
|
+
tab_slugs=tab_slugs,
|
|
236
|
+
tab_variable=tab_variable,
|
|
237
|
+
default_tab=default_tab,
|
|
238
|
+
tab_position=tabs_obj.position,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Content-only face. Orphan-chart detection lives post-compile so
|
|
242
|
+
# cross-face references work — see _collect_orphan_charts in compiler.py.
|
|
243
|
+
return Layout(type=LayoutType.ROWS, items=[])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _resolve_layout_items(
|
|
247
|
+
items: list[Any],
|
|
248
|
+
charts: dict[str, Chart],
|
|
249
|
+
query_registry: dict[str, AnyQuery],
|
|
250
|
+
face_id: str,
|
|
251
|
+
depth: int,
|
|
252
|
+
prefix: str,
|
|
253
|
+
base_path: Path | None = None,
|
|
254
|
+
default_source: str | None = None,
|
|
255
|
+
chart_registry: dict[str, Any] | None = None,
|
|
256
|
+
theme: str | None = None,
|
|
257
|
+
used_chart_ids: set | None = None,
|
|
258
|
+
parent_variables: dict[str, Any] | None = None,
|
|
259
|
+
_resolve_jinja: bool = False,
|
|
260
|
+
parent_level: int = 0,
|
|
261
|
+
) -> list[LayoutItem]:
|
|
262
|
+
"""Resolve layout items to LayoutItem objects.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
items: List of layout items (strings, dicts, charts)
|
|
266
|
+
charts: Available charts
|
|
267
|
+
query_registry: Query registry
|
|
268
|
+
face_id: Parent face ID
|
|
269
|
+
depth: Nesting depth
|
|
270
|
+
prefix: Item prefix for ID generation
|
|
271
|
+
base_path: Base path for resolving external file references
|
|
272
|
+
default_source: Default source for inline queries
|
|
273
|
+
theme: Vega-Lite theme to inherit to nested faces
|
|
274
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
275
|
+
used_chart_ids: Set of already-used chart IDs for uniqueness
|
|
276
|
+
parent_variables: Variables from parent scope for template resolution
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of resolved LayoutItem objects
|
|
280
|
+
"""
|
|
281
|
+
resolved: list[LayoutItem] = []
|
|
282
|
+
|
|
283
|
+
# Initialize used_chart_ids with existing chart names if not provided
|
|
284
|
+
if used_chart_ids is None:
|
|
285
|
+
used_chart_ids = set(charts.keys())
|
|
286
|
+
|
|
287
|
+
for idx, item in enumerate(items):
|
|
288
|
+
# Handle foreach construct
|
|
289
|
+
if isinstance(item, ForeachItem):
|
|
290
|
+
foreach_items = _resolve_foreach_item(
|
|
291
|
+
item.foreach,
|
|
292
|
+
charts,
|
|
293
|
+
query_registry,
|
|
294
|
+
face_id,
|
|
295
|
+
depth,
|
|
296
|
+
f"{prefix}{idx}",
|
|
297
|
+
base_path,
|
|
298
|
+
default_source,
|
|
299
|
+
chart_registry,
|
|
300
|
+
theme,
|
|
301
|
+
used_chart_ids,
|
|
302
|
+
parent_variables,
|
|
303
|
+
parent_level=parent_level,
|
|
304
|
+
)
|
|
305
|
+
resolved.extend(foreach_items)
|
|
306
|
+
else:
|
|
307
|
+
layout_item = _resolve_single_item(
|
|
308
|
+
item,
|
|
309
|
+
charts,
|
|
310
|
+
query_registry,
|
|
311
|
+
face_id,
|
|
312
|
+
depth,
|
|
313
|
+
f"{prefix}{idx}",
|
|
314
|
+
base_path,
|
|
315
|
+
default_source,
|
|
316
|
+
chart_registry,
|
|
317
|
+
theme,
|
|
318
|
+
used_chart_ids,
|
|
319
|
+
parent_variables,
|
|
320
|
+
_resolve_jinja=_resolve_jinja,
|
|
321
|
+
parent_level=parent_level,
|
|
322
|
+
)
|
|
323
|
+
resolved.append(layout_item)
|
|
324
|
+
|
|
325
|
+
return resolved
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _resolve_single_item(
|
|
329
|
+
item: Any,
|
|
330
|
+
charts: dict[str, Chart],
|
|
331
|
+
query_registry: dict[str, AnyQuery],
|
|
332
|
+
face_id: str,
|
|
333
|
+
depth: int,
|
|
334
|
+
item_id: str,
|
|
335
|
+
base_path: Path | None = None,
|
|
336
|
+
default_source: str | None = None,
|
|
337
|
+
chart_registry: dict[str, Any] | None = None,
|
|
338
|
+
theme: str | None = None,
|
|
339
|
+
used_chart_ids: set | None = None,
|
|
340
|
+
parent_variables: dict[str, Any] | None = None,
|
|
341
|
+
_resolve_jinja: bool = False,
|
|
342
|
+
parent_level: int = 0,
|
|
343
|
+
) -> LayoutItem:
|
|
344
|
+
"""Resolve a single layout item.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
item: Layout item to resolve
|
|
348
|
+
charts: Available charts
|
|
349
|
+
query_registry: Query registry
|
|
350
|
+
theme: Vega-Lite theme to inherit to nested faces
|
|
351
|
+
face_id: Parent face ID
|
|
352
|
+
depth: Nesting depth
|
|
353
|
+
item_id: ID for this item (fallback for inline charts)
|
|
354
|
+
base_path: Base path for resolving external file references
|
|
355
|
+
default_source: Default source for inline queries
|
|
356
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
357
|
+
used_chart_ids: Set of already-used chart IDs for uniqueness
|
|
358
|
+
parent_variables: Variables from parent scope for template resolution
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Resolved LayoutItem
|
|
362
|
+
"""
|
|
363
|
+
# String reference to chart OR board file import
|
|
364
|
+
if isinstance(item, str):
|
|
365
|
+
# Check for board file import (ends with .yml or .yaml)
|
|
366
|
+
if item.endswith(".yml") or item.endswith(".yaml"):
|
|
367
|
+
return _resolve_board_file_import(
|
|
368
|
+
item,
|
|
369
|
+
query_registry,
|
|
370
|
+
face_id,
|
|
371
|
+
depth,
|
|
372
|
+
item_id,
|
|
373
|
+
base_path,
|
|
374
|
+
default_source,
|
|
375
|
+
chart_registry,
|
|
376
|
+
theme,
|
|
377
|
+
parent_variables,
|
|
378
|
+
_resolve_jinja=_resolve_jinja,
|
|
379
|
+
parent_level=parent_level,
|
|
380
|
+
)
|
|
381
|
+
elif item in charts:
|
|
382
|
+
return LayoutItem(type="chart", chart=charts[item])
|
|
383
|
+
else:
|
|
384
|
+
raise ReferenceError(item, "layout item")
|
|
385
|
+
|
|
386
|
+
# Already a Chart
|
|
387
|
+
if isinstance(item, Chart):
|
|
388
|
+
return LayoutItem(type="chart", chart=item)
|
|
389
|
+
|
|
390
|
+
# dict[str, AuthoredChart]: {"my_chart": AuthoredChart(...)}
|
|
391
|
+
if isinstance(item, dict):
|
|
392
|
+
if len(item) != 1:
|
|
393
|
+
raise CompilationError(
|
|
394
|
+
f"Named chart dict must have exactly one key, got: {list(item.keys())}"
|
|
395
|
+
)
|
|
396
|
+
_chart_name, chart_def = next(iter(item.items()))
|
|
397
|
+
chart_dict = chart_def.model_dump(exclude_none=True)
|
|
398
|
+
if used_chart_ids is None:
|
|
399
|
+
used_chart_ids = set(charts.keys())
|
|
400
|
+
inline_id = _generate_inline_chart_id(chart_dict, item_id, used_chart_ids)
|
|
401
|
+
source_path = f"{item_id.replace('row', 'rows[').replace('col', 'cols[')}]"
|
|
402
|
+
chart = _normalize_chart(
|
|
403
|
+
inline_id,
|
|
404
|
+
chart_dict,
|
|
405
|
+
query_registry,
|
|
406
|
+
base_path,
|
|
407
|
+
default_source,
|
|
408
|
+
source_path=source_path,
|
|
409
|
+
)
|
|
410
|
+
return LayoutItem(type="chart", chart=chart)
|
|
411
|
+
|
|
412
|
+
# Pre-validated AuthoredFace (typed inline nested face from rows/cols)
|
|
413
|
+
if isinstance(item, AuthoredFace):
|
|
414
|
+
from dataface.core.compile.normalizer import normalize_face
|
|
415
|
+
|
|
416
|
+
# In foreach context, resolve Jinja templates baked into the typed face.
|
|
417
|
+
# Pydantic parsed templates as literal strings; dump to dict, resolve, re-validate.
|
|
418
|
+
if _resolve_jinja and parent_variables:
|
|
419
|
+
face_dict = item.model_dump(exclude_none=True, by_alias=False)
|
|
420
|
+
face_dict = _resolve_dict_templates(face_dict, parent_variables)
|
|
421
|
+
item = AuthoredFace.model_validate(face_dict)
|
|
422
|
+
|
|
423
|
+
details = item.details
|
|
424
|
+
user_width = str(item.width) if item.width is not None else None
|
|
425
|
+
layout_height = str(item.height) if item.height is not None else None
|
|
426
|
+
_validate_dimension(user_width, "width")
|
|
427
|
+
_validate_dimension(layout_height, "height")
|
|
428
|
+
|
|
429
|
+
compiled_nested = normalize_face(
|
|
430
|
+
item,
|
|
431
|
+
face_id=f"{face_id}_{item_id}",
|
|
432
|
+
parent_context={
|
|
433
|
+
"parent_id": face_id,
|
|
434
|
+
"base_path": base_path,
|
|
435
|
+
"default_source": default_source,
|
|
436
|
+
"theme": theme,
|
|
437
|
+
"variables": parent_variables,
|
|
438
|
+
"_resolve_jinja": _resolve_jinja,
|
|
439
|
+
},
|
|
440
|
+
query_registry=query_registry,
|
|
441
|
+
chart_registry=chart_registry,
|
|
442
|
+
depth=depth + 1,
|
|
443
|
+
base_path=base_path,
|
|
444
|
+
parent_level=parent_level,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
details_kwargs: dict[str, Any] = {}
|
|
448
|
+
if details:
|
|
449
|
+
details_id = item.id or f"_details_{item_id}"
|
|
450
|
+
details_kwargs = {
|
|
451
|
+
"details_variable": details_id,
|
|
452
|
+
"details_summary": details.summary,
|
|
453
|
+
"details_expanded_summary": details.expanded_title or details.summary,
|
|
454
|
+
}
|
|
455
|
+
compiled_nested.meta["details_expanded_default"] = details.expanded
|
|
456
|
+
|
|
457
|
+
return LayoutItem(
|
|
458
|
+
type="face",
|
|
459
|
+
face=compiled_nested,
|
|
460
|
+
user_width=user_width,
|
|
461
|
+
layout_height=layout_height,
|
|
462
|
+
description=item.description,
|
|
463
|
+
visible=item.visible,
|
|
464
|
+
**details_kwargs,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Pre-validated chart patch (typed inline chart from rows/cols)
|
|
468
|
+
if isinstance(item, _SharedChartFields):
|
|
469
|
+
if used_chart_ids is None:
|
|
470
|
+
used_chart_ids = set(charts.keys())
|
|
471
|
+
# exclude_none=True: None-defaulted fields must be absent so _normalize_chart's
|
|
472
|
+
# auto-derive guards ("title" not in chart_dict) fire correctly.
|
|
473
|
+
# visible is excluded=True on AuthoredChart so it won't appear in chart_dict.
|
|
474
|
+
chart_dict = item.model_dump(exclude_none=True)
|
|
475
|
+
inline_id = _generate_inline_chart_id(chart_dict, item_id, used_chart_ids)
|
|
476
|
+
source_path = f"{item_id.replace('row', 'rows[').replace('col', 'cols[')}]"
|
|
477
|
+
chart = _normalize_chart(
|
|
478
|
+
inline_id,
|
|
479
|
+
chart_dict,
|
|
480
|
+
query_registry,
|
|
481
|
+
base_path,
|
|
482
|
+
default_source,
|
|
483
|
+
source_path=source_path,
|
|
484
|
+
)
|
|
485
|
+
return LayoutItem(type="chart", chart=chart, visible=item.visible)
|
|
486
|
+
|
|
487
|
+
raise CompilationError(f"Invalid layout item type: {type(item)}")
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# Layout keys that should not have templates resolved (processed by layout resolution)
|
|
491
|
+
_LAYOUT_KEYS = frozenset({"rows", "cols", "grid", "tabs"})
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _resolve_dict_templates(
|
|
495
|
+
data: dict[str, Any],
|
|
496
|
+
variables: dict[str, Any],
|
|
497
|
+
) -> dict[str, Any]:
|
|
498
|
+
"""Resolve Jinja templates recursively in dict values.
|
|
499
|
+
|
|
500
|
+
Used to resolve loop variables and inherited variables in nested face
|
|
501
|
+
definitions created by foreach (compile-time static expansion).
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
data: Dict with potentially templated string values
|
|
505
|
+
variables: Variables for Jinja resolution
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Dict with resolved string values
|
|
509
|
+
"""
|
|
510
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
511
|
+
|
|
512
|
+
result: dict[str, Any] = {}
|
|
513
|
+
for key, value in data.items():
|
|
514
|
+
if isinstance(value, str):
|
|
515
|
+
# Resolve Jinja templates in strings
|
|
516
|
+
result[key] = resolve_jinja_template(value, variables, strict=False)
|
|
517
|
+
elif isinstance(value, dict):
|
|
518
|
+
# Recurse into nested dicts
|
|
519
|
+
result[key] = _resolve_dict_templates(value, variables)
|
|
520
|
+
elif isinstance(value, list):
|
|
521
|
+
# Don't recurse into layout items - those are processed by _resolve_layout_items
|
|
522
|
+
if key in _LAYOUT_KEYS:
|
|
523
|
+
result[key] = value
|
|
524
|
+
else:
|
|
525
|
+
result[key] = [
|
|
526
|
+
(
|
|
527
|
+
_resolve_dict_templates(v, variables)
|
|
528
|
+
if isinstance(v, dict)
|
|
529
|
+
else (
|
|
530
|
+
resolve_jinja_template(v, variables, strict=False)
|
|
531
|
+
if isinstance(v, str)
|
|
532
|
+
else v
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
for v in value
|
|
536
|
+
]
|
|
537
|
+
else:
|
|
538
|
+
result[key] = value
|
|
539
|
+
return result
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _resolve_board_file_import(
|
|
543
|
+
file_path: str,
|
|
544
|
+
query_registry: dict[str, AnyQuery],
|
|
545
|
+
face_id: str,
|
|
546
|
+
depth: int,
|
|
547
|
+
item_id: str,
|
|
548
|
+
base_path: Path | None = None,
|
|
549
|
+
default_source: str | None = None,
|
|
550
|
+
chart_registry: dict[str, Any] | None = None,
|
|
551
|
+
theme: str | None = None,
|
|
552
|
+
parent_variables: dict[str, Any] | None = None,
|
|
553
|
+
_resolve_jinja: bool = False,
|
|
554
|
+
parent_level: int = 0,
|
|
555
|
+
) -> LayoutItem:
|
|
556
|
+
"""Load and resolve a board file import.
|
|
557
|
+
|
|
558
|
+
Board file imports allow including external YAML face files in layouts.
|
|
559
|
+
The imported face inherits variables from the parent context.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
file_path: Path to the YAML file (relative to base_path or absolute)
|
|
563
|
+
charts: Available charts
|
|
564
|
+
query_registry: Query registry
|
|
565
|
+
face_id: Parent face ID
|
|
566
|
+
depth: Nesting depth
|
|
567
|
+
item_id: ID for this item
|
|
568
|
+
base_path: Base path for resolving relative file paths
|
|
569
|
+
default_source: Default source for inline queries
|
|
570
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
571
|
+
theme: Vega-Lite theme to inherit
|
|
572
|
+
used_chart_ids: Set of already-used chart IDs for uniqueness
|
|
573
|
+
parent_variables: Variables from parent scope for template resolution
|
|
574
|
+
_resolve_jinja: If True, resolve Jinja templates in the YAML content
|
|
575
|
+
(foreach creates static faces; all templates baked at compile time).
|
|
576
|
+
If False, skip YAML content resolution so interactive variables
|
|
577
|
+
remain as templates and are resolved at render time.
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
LayoutItem containing the compiled nested face
|
|
581
|
+
|
|
582
|
+
Raises:
|
|
583
|
+
CompilationError: If file not found or invalid
|
|
584
|
+
"""
|
|
585
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
586
|
+
from dataface.core.compile.normalizer import normalize_face
|
|
587
|
+
|
|
588
|
+
# Resolve the file path (might contain Jinja templates for parameterised imports
|
|
589
|
+
# like `partials/{{ chart_type }}.yml`). This always uses parent_variables so
|
|
590
|
+
# file paths with interactive variable defaults still resolve at compile time.
|
|
591
|
+
parent_variables = parent_variables or {}
|
|
592
|
+
resolved_path = resolve_jinja_template(file_path, parent_variables, strict=False)
|
|
593
|
+
|
|
594
|
+
# Resolve relative to base_path
|
|
595
|
+
full_path = base_path / resolved_path if base_path else Path(resolved_path)
|
|
596
|
+
|
|
597
|
+
if not full_path.exists():
|
|
598
|
+
raise CompilationError(
|
|
599
|
+
f"Board file not found: '{resolved_path}' "
|
|
600
|
+
f"(resolved from '{file_path}', base_path={base_path})"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Load and parse the YAML file
|
|
604
|
+
try:
|
|
605
|
+
yaml_content = full_path.read_text()
|
|
606
|
+
# Only resolve Jinja templates in the YAML content when inside a foreach.
|
|
607
|
+
# Foreach creates static compile-time faces, so all templates (loop vars +
|
|
608
|
+
# interactive defaults) must be baked in. For regular imports, leave
|
|
609
|
+
# templates intact so they are resolved dynamically at render time.
|
|
610
|
+
if _resolve_jinja:
|
|
611
|
+
yaml_content = resolve_jinja_template(
|
|
612
|
+
yaml_content, parent_variables, strict=False
|
|
613
|
+
)
|
|
614
|
+
face_data = yaml.safe_load(yaml_content)
|
|
615
|
+
except yaml.YAMLError as e:
|
|
616
|
+
raise CompilationError(
|
|
617
|
+
f"Failed to parse board file '{resolved_path}': {e}"
|
|
618
|
+
) from e
|
|
619
|
+
except OSError as e:
|
|
620
|
+
raise CompilationError(
|
|
621
|
+
f"Failed to read board file '{resolved_path}': {e}"
|
|
622
|
+
) from e
|
|
623
|
+
|
|
624
|
+
if not isinstance(face_data, dict):
|
|
625
|
+
raise CompilationError(
|
|
626
|
+
f"Board file '{resolved_path}' must contain a YAML dictionary"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Convert raw YAML dict to AuthoredFace; model_validate handles all nested models.
|
|
630
|
+
nested_face = AuthoredFace.model_validate(face_data)
|
|
631
|
+
|
|
632
|
+
# Build parent context with variables from parent scope
|
|
633
|
+
parent_context = {
|
|
634
|
+
"parent_id": face_id,
|
|
635
|
+
"base_path": full_path.parent, # Use imported file's directory as base
|
|
636
|
+
"default_source": default_source,
|
|
637
|
+
"theme": theme,
|
|
638
|
+
"variables": parent_variables, # Pass variables to child
|
|
639
|
+
"_resolve_jinja": _resolve_jinja, # Propagate foreach context to children
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# Compile the nested face
|
|
643
|
+
compiled_nested = normalize_face(
|
|
644
|
+
nested_face,
|
|
645
|
+
face_id=f"{face_id}_{item_id}",
|
|
646
|
+
parent_context=parent_context,
|
|
647
|
+
query_registry=query_registry,
|
|
648
|
+
chart_registry=chart_registry,
|
|
649
|
+
depth=depth + 1,
|
|
650
|
+
base_path=full_path.parent,
|
|
651
|
+
parent_level=parent_level,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return LayoutItem(type="face", face=compiled_nested)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _resolve_foreach_item(
|
|
658
|
+
foreach_config: ForeachConfig,
|
|
659
|
+
charts: dict[str, Chart],
|
|
660
|
+
query_registry: dict[str, AnyQuery],
|
|
661
|
+
face_id: str,
|
|
662
|
+
depth: int,
|
|
663
|
+
item_id: str,
|
|
664
|
+
base_path: Path | None = None,
|
|
665
|
+
default_source: str | None = None,
|
|
666
|
+
chart_registry: dict[str, Any] | None = None,
|
|
667
|
+
theme: str | None = None,
|
|
668
|
+
used_chart_ids: set | None = None,
|
|
669
|
+
parent_variables: dict[str, Any] | None = None,
|
|
670
|
+
parent_level: int = 0,
|
|
671
|
+
) -> list[LayoutItem]:
|
|
672
|
+
"""Resolve a foreach construct that iterates over inline data.
|
|
673
|
+
|
|
674
|
+
foreach allows dynamically generating layout items at compile time.
|
|
675
|
+
Each row from the inline data becomes available as a loop variable,
|
|
676
|
+
accessible via dot notation (e.g., {{ col.name }}).
|
|
677
|
+
|
|
678
|
+
Example YAML:
|
|
679
|
+
rows:
|
|
680
|
+
- foreach:
|
|
681
|
+
query:
|
|
682
|
+
data:
|
|
683
|
+
- name: "revenue"
|
|
684
|
+
type: "numeric"
|
|
685
|
+
- name: "date"
|
|
686
|
+
type: "date"
|
|
687
|
+
as: col
|
|
688
|
+
items:
|
|
689
|
+
- 'partials/{{ col.type }}.yml'
|
|
690
|
+
|
|
691
|
+
Note: At compile time, foreach only works with inline data (query.data).
|
|
692
|
+
Query references are not executed during compilation.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
foreach_config: Validated ForeachConfig with query, as_, and items.
|
|
696
|
+
charts: Available charts
|
|
697
|
+
query_registry: Query registry
|
|
698
|
+
face_id: Parent face ID
|
|
699
|
+
depth: Nesting depth
|
|
700
|
+
item_id: ID prefix for generated items
|
|
701
|
+
base_path: Base path for resolving external file references
|
|
702
|
+
default_source: Default source for inline queries
|
|
703
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
704
|
+
theme: Vega-Lite theme to inherit
|
|
705
|
+
used_chart_ids: Set of already-used chart IDs for uniqueness
|
|
706
|
+
parent_variables: Variables from parent scope
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
List of LayoutItems generated from the foreach iteration
|
|
710
|
+
"""
|
|
711
|
+
query_config = foreach_config.query
|
|
712
|
+
loop_var = foreach_config.as_
|
|
713
|
+
item_templates = foreach_config.items
|
|
714
|
+
parent_variables = parent_variables or {}
|
|
715
|
+
|
|
716
|
+
# Extract inline data from query config
|
|
717
|
+
# At compile time, foreach only works with inline data
|
|
718
|
+
static_data = _get_foreach_data(query_config)
|
|
719
|
+
if not static_data:
|
|
720
|
+
return []
|
|
721
|
+
|
|
722
|
+
# Generate layout items for each row
|
|
723
|
+
resolved_items: list[LayoutItem] = []
|
|
724
|
+
for idx, row in enumerate(static_data):
|
|
725
|
+
loop_context = _build_loop_context(row, loop_var, parent_variables)
|
|
726
|
+
|
|
727
|
+
for template_idx, template_item in enumerate(item_templates):
|
|
728
|
+
layout_item = _resolve_single_item(
|
|
729
|
+
template_item,
|
|
730
|
+
charts,
|
|
731
|
+
query_registry,
|
|
732
|
+
face_id,
|
|
733
|
+
depth,
|
|
734
|
+
f"{item_id}_foreach{idx}_{template_idx}",
|
|
735
|
+
base_path,
|
|
736
|
+
default_source,
|
|
737
|
+
chart_registry,
|
|
738
|
+
theme,
|
|
739
|
+
used_chart_ids,
|
|
740
|
+
loop_context,
|
|
741
|
+
_resolve_jinja=True,
|
|
742
|
+
parent_level=parent_level,
|
|
743
|
+
)
|
|
744
|
+
resolved_items.append(layout_item)
|
|
745
|
+
|
|
746
|
+
return resolved_items
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _get_foreach_data(query_config: ForeachQuery) -> list[dict[str, Any]] | None:
|
|
750
|
+
"""Extract data for foreach iteration from a ForeachQuery.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
query_config: Validated ForeachQuery with optional data or static_data.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
List of row dicts for iteration, or None if no data.
|
|
757
|
+
"""
|
|
758
|
+
data = query_config.data or query_config.static_data
|
|
759
|
+
if isinstance(data, list):
|
|
760
|
+
return data
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _build_loop_context(
|
|
765
|
+
row: Any,
|
|
766
|
+
loop_var: str,
|
|
767
|
+
parent_variables: dict[str, Any],
|
|
768
|
+
) -> dict[str, Any]:
|
|
769
|
+
"""Build the variable context for a foreach iteration.
|
|
770
|
+
|
|
771
|
+
The loop variable is accessible via dot notation: {{ col.field }}
|
|
772
|
+
This keeps variable scoping explicit and avoids shadowing parent variables.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
row: Current row data (dict or scalar)
|
|
776
|
+
loop_var: Name of the loop variable
|
|
777
|
+
parent_variables: Variables inherited from parent scope
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
Combined context dict for Jinja resolution
|
|
781
|
+
"""
|
|
782
|
+
loop_context = dict(parent_variables)
|
|
783
|
+
|
|
784
|
+
if isinstance(row, dict):
|
|
785
|
+
# Row is a dict - accessible as {{ loop_var.field }}
|
|
786
|
+
loop_context[loop_var] = row
|
|
787
|
+
else:
|
|
788
|
+
# Scalar value - wrap in dict with 'value' key
|
|
789
|
+
loop_context[loop_var] = {"value": row}
|
|
790
|
+
|
|
791
|
+
return loop_context
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _resolve_grid_items(
|
|
795
|
+
grid: GridLayout,
|
|
796
|
+
charts: dict[str, Chart],
|
|
797
|
+
query_registry: dict[str, AnyQuery],
|
|
798
|
+
face_id: str,
|
|
799
|
+
depth: int,
|
|
800
|
+
base_path: Path | None = None,
|
|
801
|
+
default_source: str | None = None,
|
|
802
|
+
chart_registry: dict[str, Any] | None = None,
|
|
803
|
+
theme: str | None = None,
|
|
804
|
+
used_chart_ids: set | None = None,
|
|
805
|
+
parent_variables: dict[str, Any] | None = None,
|
|
806
|
+
_resolve_jinja: bool = False,
|
|
807
|
+
parent_level: int = 0,
|
|
808
|
+
) -> list[LayoutItem]:
|
|
809
|
+
"""Resolve grid layout items.
|
|
810
|
+
|
|
811
|
+
Auto-calculates col/row positions for items without explicit positioning.
|
|
812
|
+
Items flow left-to-right, wrapping to the next row when they exceed
|
|
813
|
+
the column count.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
grid: GridLayout definition
|
|
817
|
+
charts: Available charts
|
|
818
|
+
query_registry: Query registry
|
|
819
|
+
face_id: Parent face ID
|
|
820
|
+
depth: Nesting depth
|
|
821
|
+
base_path: Base path for resolving external file references
|
|
822
|
+
default_source: Default source for inline queries
|
|
823
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
824
|
+
theme: Vega-Lite theme to inherit to nested faces
|
|
825
|
+
parent_variables: Variables from parent scope for template resolution
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
List of LayoutItem with grid positioning
|
|
829
|
+
"""
|
|
830
|
+
resolved: list[LayoutItem] = []
|
|
831
|
+
columns = grid.columns
|
|
832
|
+
|
|
833
|
+
# Track grid occupancy for auto-positioning
|
|
834
|
+
# For items without explicit position, place left-to-right, top-to-bottom
|
|
835
|
+
current_col = 0
|
|
836
|
+
current_row = 0
|
|
837
|
+
|
|
838
|
+
for idx, grid_item in enumerate(grid.items):
|
|
839
|
+
item = grid_item.item
|
|
840
|
+
layout_item = _resolve_single_item(
|
|
841
|
+
item,
|
|
842
|
+
charts,
|
|
843
|
+
query_registry,
|
|
844
|
+
face_id,
|
|
845
|
+
depth,
|
|
846
|
+
f"grid{idx}",
|
|
847
|
+
base_path,
|
|
848
|
+
default_source,
|
|
849
|
+
chart_registry,
|
|
850
|
+
theme,
|
|
851
|
+
used_chart_ids,
|
|
852
|
+
parent_variables,
|
|
853
|
+
_resolve_jinja=_resolve_jinja,
|
|
854
|
+
parent_level=parent_level,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# Get span values (default to 1)
|
|
858
|
+
# Support 'width' as alias for 'col_span' and 'height' as alias for 'row_span'
|
|
859
|
+
# This is more intuitive for users (width: 6 means span 6 columns)
|
|
860
|
+
col_span = grid_item.col_span or grid_item.width or 1
|
|
861
|
+
row_span = grid_item.row_span or grid_item.height or 1
|
|
862
|
+
|
|
863
|
+
# Use explicit position if provided, otherwise auto-calculate
|
|
864
|
+
if grid_item.col is not None:
|
|
865
|
+
item_col = grid_item.col
|
|
866
|
+
else:
|
|
867
|
+
# Auto-position: check if item fits in current row
|
|
868
|
+
if current_col + col_span > columns:
|
|
869
|
+
# Wrap to next row
|
|
870
|
+
current_col = 0
|
|
871
|
+
current_row += 1
|
|
872
|
+
item_col = current_col
|
|
873
|
+
# Advance position for next item
|
|
874
|
+
current_col += col_span
|
|
875
|
+
|
|
876
|
+
item_row = grid_item.row if grid_item.row is not None else current_row
|
|
877
|
+
|
|
878
|
+
# Set grid positioning
|
|
879
|
+
layout_item.col = item_col
|
|
880
|
+
layout_item.row = item_row
|
|
881
|
+
layout_item.col_span = col_span
|
|
882
|
+
layout_item.row_span = row_span
|
|
883
|
+
if grid_item.description and not layout_item.description:
|
|
884
|
+
layout_item.description = grid_item.description
|
|
885
|
+
resolved.append(layout_item)
|
|
886
|
+
|
|
887
|
+
return resolved
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _resolve_tab_items(
|
|
891
|
+
tabs: TabLayout,
|
|
892
|
+
query_registry: dict[str, AnyQuery],
|
|
893
|
+
face_id: str,
|
|
894
|
+
depth: int,
|
|
895
|
+
base_path: Path | None = None,
|
|
896
|
+
default_source: str | None = None,
|
|
897
|
+
chart_registry: dict[str, Any] | None = None,
|
|
898
|
+
theme: str | None = None,
|
|
899
|
+
*,
|
|
900
|
+
_resolve_jinja: bool = False,
|
|
901
|
+
resolved_style: MergedStyle,
|
|
902
|
+
parent_level: int = 0,
|
|
903
|
+
) -> tuple[list[LayoutItem], list[str]]:
|
|
904
|
+
"""Resolve tab layout items.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
tabs: TabLayout definition
|
|
908
|
+
charts: Available charts
|
|
909
|
+
query_registry: Query registry
|
|
910
|
+
face_id: Parent face ID
|
|
911
|
+
theme: Vega-Lite theme to inherit to nested faces
|
|
912
|
+
depth: Nesting depth
|
|
913
|
+
base_path: Base path for resolving external file references
|
|
914
|
+
default_source: Default source for inline queries
|
|
915
|
+
parent_variables: Variables from parent scope for template resolution
|
|
916
|
+
chart_registry: Raw chart definitions for nested face normalization
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
Tuple of (items, tab_titles)
|
|
920
|
+
"""
|
|
921
|
+
from dataface.core.compile.normalizer import normalize_face
|
|
922
|
+
|
|
923
|
+
resolved: list[LayoutItem] = []
|
|
924
|
+
titles: list[str] = []
|
|
925
|
+
|
|
926
|
+
for idx, tab_item in enumerate(tabs.items):
|
|
927
|
+
titles.append(tab_item.title)
|
|
928
|
+
|
|
929
|
+
# Tab items can have nested layouts
|
|
930
|
+
tab_dict = tab_item.model_dump(exclude_none=True)
|
|
931
|
+
|
|
932
|
+
if any(k in tab_dict for k in ["rows", "cols", "grid", "tabs"]):
|
|
933
|
+
# Tab is a nested face. Pop TabItem-only fields not on AuthoredFace,
|
|
934
|
+
# then model_validate so style errors include the "style." loc prefix.
|
|
935
|
+
tab_dict.pop("icon", None)
|
|
936
|
+
nested_face = AuthoredFace.model_validate(tab_dict)
|
|
937
|
+
compiled_nested = normalize_face(
|
|
938
|
+
nested_face,
|
|
939
|
+
face_id=f"{face_id}_tab{idx}",
|
|
940
|
+
parent_context={
|
|
941
|
+
"parent_id": face_id,
|
|
942
|
+
"base_path": base_path,
|
|
943
|
+
"default_source": default_source,
|
|
944
|
+
"theme": theme,
|
|
945
|
+
"_resolve_jinja": _resolve_jinja,
|
|
946
|
+
},
|
|
947
|
+
query_registry=query_registry,
|
|
948
|
+
chart_registry=chart_registry,
|
|
949
|
+
depth=depth + 1,
|
|
950
|
+
base_path=base_path,
|
|
951
|
+
parent_level=parent_level,
|
|
952
|
+
)
|
|
953
|
+
resolved.append(
|
|
954
|
+
LayoutItem(
|
|
955
|
+
type="face",
|
|
956
|
+
face=compiled_nested,
|
|
957
|
+
description=tab_item.description,
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
elif tab_item.text:
|
|
962
|
+
# Content-only tab
|
|
963
|
+
content_face = Face(
|
|
964
|
+
id=f"{face_id}_tab{idx}",
|
|
965
|
+
title=tab_item.title,
|
|
966
|
+
description=tab_item.description or "",
|
|
967
|
+
text=tab_item.text,
|
|
968
|
+
layout=Layout(type=LayoutType.ROWS, items=[]),
|
|
969
|
+
theme=theme,
|
|
970
|
+
resolved_style=resolved_style,
|
|
971
|
+
level=parent_level + 1,
|
|
972
|
+
)
|
|
973
|
+
resolved.append(
|
|
974
|
+
LayoutItem(
|
|
975
|
+
type="face",
|
|
976
|
+
face=content_face,
|
|
977
|
+
description=tab_item.description,
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
else:
|
|
982
|
+
# Empty tab
|
|
983
|
+
empty_face = Face(
|
|
984
|
+
id=f"{face_id}_tab{idx}",
|
|
985
|
+
title=tab_item.title,
|
|
986
|
+
description=tab_item.description or "",
|
|
987
|
+
layout=Layout(type=LayoutType.ROWS, items=[]),
|
|
988
|
+
theme=theme,
|
|
989
|
+
resolved_style=resolved_style,
|
|
990
|
+
level=parent_level + 1,
|
|
991
|
+
)
|
|
992
|
+
resolved.append(
|
|
993
|
+
LayoutItem(
|
|
994
|
+
type="face",
|
|
995
|
+
face=empty_face,
|
|
996
|
+
description=tab_item.description,
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
return resolved, titles
|