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,904 @@
|
|
|
1
|
+
"""AuthoredFace compiler module.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE
|
|
4
|
+
Purpose: Compile YAML face definitions into Face objects
|
|
5
|
+
ready for execution and rendering.
|
|
6
|
+
|
|
7
|
+
Entry Points:
|
|
8
|
+
- compile(yaml_content: str) -> CompileResult
|
|
9
|
+
- compile_file(file_path: Path) -> CompileResult
|
|
10
|
+
|
|
11
|
+
This is the main orchestrator for compilation. It:
|
|
12
|
+
1. Parses YAML to AuthoredFace (parser.py)
|
|
13
|
+
2. Validates the face structure (validator.py)
|
|
14
|
+
3. Normalizes references and adds metadata (normalizer.py)
|
|
15
|
+
|
|
16
|
+
Layout dimensions are calculated later in the render pipeline (after query
|
|
17
|
+
execution) so table heights can use actual row counts.
|
|
18
|
+
|
|
19
|
+
Dependencies:
|
|
20
|
+
- .parser (parse_yaml)
|
|
21
|
+
- .validator (validate_face)
|
|
22
|
+
- .normalizer (normalize_face)
|
|
23
|
+
- .jinja (detect_query_dependencies)
|
|
24
|
+
- .errors (CompilationError, etc.)
|
|
25
|
+
|
|
26
|
+
See also:
|
|
27
|
+
- docs/docs/contributing/architecture.md for the pipeline overview
|
|
28
|
+
- execute/executor.py for the next stage
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import TYPE_CHECKING, Any
|
|
37
|
+
|
|
38
|
+
import yaml
|
|
39
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
40
|
+
|
|
41
|
+
from dataface.core.compile.models.face.compiled import Face
|
|
42
|
+
from dataface.core.compile.models.query.compiled import AnyQuery
|
|
43
|
+
from dataface.core.errors.structured import StructuredError
|
|
44
|
+
from dataface.core.render.warnings import unreferenced_chart
|
|
45
|
+
from dataface.core.render.warnings.base import RenderWarning
|
|
46
|
+
from dataface.core.render.warnings.from_query_diagnostic import from_query_diagnostic
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
from dataface.core.compile.errors import (
|
|
50
|
+
CompilationError,
|
|
51
|
+
JinjaError,
|
|
52
|
+
ParseError,
|
|
53
|
+
ReferenceError,
|
|
54
|
+
)
|
|
55
|
+
from dataface.core.compile.jinja import detect_query_dependencies
|
|
56
|
+
from dataface.core.compile.meta import get_meta_for_face, merge_meta_with_face
|
|
57
|
+
from dataface.core.compile.models.face.authored import AuthoredFace
|
|
58
|
+
from dataface.core.compile.models.refs import ChartRef, QueryRef, VariableRef
|
|
59
|
+
from dataface.core.compile.models.variable.authored import Variable
|
|
60
|
+
from dataface.core.compile.normalizer import normalize_face, normalize_query
|
|
61
|
+
from dataface.core.compile.parser import parse_yaml
|
|
62
|
+
from dataface.core.compile.validator import validate_face
|
|
63
|
+
from dataface.core.compile.yaml_error_formatter import (
|
|
64
|
+
format_validation_errors_structured,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if TYPE_CHECKING:
|
|
68
|
+
from dataface.core.inspect.query_validator import (
|
|
69
|
+
QueryDiagnostic,
|
|
70
|
+
RelationshipContext,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _to_plain_dict(obj: Any) -> Any:
|
|
75
|
+
"""Recursively convert DotDict (or any dict subclass) to plain dict."""
|
|
76
|
+
if isinstance(obj, dict):
|
|
77
|
+
return {k: _to_plain_dict(v) for k, v in obj.items()}
|
|
78
|
+
if isinstance(obj, list):
|
|
79
|
+
return [_to_plain_dict(v) for v in obj]
|
|
80
|
+
return obj
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_project_source_paths(
|
|
84
|
+
sources: dict[str, dict[str, Any]],
|
|
85
|
+
project_dir: Path,
|
|
86
|
+
) -> dict[str, dict[str, Any]]:
|
|
87
|
+
"""Resolve relative file/database paths in project-level source configs."""
|
|
88
|
+
resolved_sources: dict[str, dict[str, Any]] = {}
|
|
89
|
+
|
|
90
|
+
for name, config in sources.items():
|
|
91
|
+
resolved_config = dict(config)
|
|
92
|
+
source_type = resolved_config.get("type")
|
|
93
|
+
|
|
94
|
+
if source_type == "duckdb":
|
|
95
|
+
path_value = resolved_config.get("path")
|
|
96
|
+
if (
|
|
97
|
+
isinstance(path_value, str)
|
|
98
|
+
and path_value != ":memory:"
|
|
99
|
+
and not Path(path_value).is_absolute()
|
|
100
|
+
):
|
|
101
|
+
resolved_config["path"] = str((project_dir / path_value).resolve())
|
|
102
|
+
elif source_type in {"csv", "parquet", "json"}:
|
|
103
|
+
file_value = resolved_config.get("file")
|
|
104
|
+
if isinstance(file_value, str) and not Path(file_value).is_absolute():
|
|
105
|
+
resolved_config["file"] = str((project_dir / file_value).resolve())
|
|
106
|
+
|
|
107
|
+
resolved_sources[name] = resolved_config
|
|
108
|
+
|
|
109
|
+
return resolved_sources
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class CompileResult:
|
|
114
|
+
"""Result of compilation.
|
|
115
|
+
|
|
116
|
+
Contains the compiled face (if successful), any errors encountered,
|
|
117
|
+
and warnings that don't prevent compilation.
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
face: Compiled face (None if errors occurred)
|
|
121
|
+
errors: Structured compile errors (list[StructuredError] — union-aware collapse applied)
|
|
122
|
+
warnings: Non-fatal warnings as ``RenderWarning`` objects with stable
|
|
123
|
+
SCREAMING_SNAKE_CASE codes. Each emitter (orphan-chart check,
|
|
124
|
+
validate_compiled_queries) owns its code on a module under
|
|
125
|
+
``dataface.core.render.warnings``.
|
|
126
|
+
suppressed_warnings: ``RenderWarning`` entries that matched a
|
|
127
|
+
suppression layer (query-level ``ignore:`` or meta.yaml lint
|
|
128
|
+
config). Kept separate so consumers can surface "would have
|
|
129
|
+
warned" without re-running validation.
|
|
130
|
+
diagnostics: Structured query diagnostics from validate_compiled_queries.
|
|
131
|
+
Includes all severity levels; ``warnings`` only includes warning+error.
|
|
132
|
+
Infrastructure for structured consumers (UI, AI agents).
|
|
133
|
+
query_registry: All normalized queries (for executor)
|
|
134
|
+
sources: Source configurations (for resolving named source references)
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> result = compile(yaml_content)
|
|
138
|
+
>>> if result.success:
|
|
139
|
+
... print(f"Compiled: {result.face.title}")
|
|
140
|
+
... else:
|
|
141
|
+
... for error in result.errors:
|
|
142
|
+
... print(f"Error: {error}")
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
face: Face | None = None
|
|
146
|
+
errors: list[StructuredError] = field(default_factory=list)
|
|
147
|
+
warnings: list[RenderWarning] = field(default_factory=list)
|
|
148
|
+
suppressed_warnings: list[RenderWarning] = field(default_factory=list)
|
|
149
|
+
diagnostics: list[QueryDiagnostic] = field(default_factory=list)
|
|
150
|
+
query_registry: dict[str, AnyQuery] = field(default_factory=dict)
|
|
151
|
+
meta_config: Any = None
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def success(self) -> bool:
|
|
155
|
+
"""Check if compilation was successful."""
|
|
156
|
+
return self.face is not None and len(self.errors) == 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def compile(
|
|
160
|
+
yaml_content: str,
|
|
161
|
+
options: dict[str, Any] | None = None,
|
|
162
|
+
base_dir: Path | None = None,
|
|
163
|
+
project_dir: Path | None = None,
|
|
164
|
+
) -> CompileResult:
|
|
165
|
+
"""Compile YAML content to a Face.
|
|
166
|
+
|
|
167
|
+
Stage: COMPILE (Full Pipeline)
|
|
168
|
+
|
|
169
|
+
This is the main entry point for compilation. It orchestrates:
|
|
170
|
+
1. PARSE: YAML string → AuthoredFace object
|
|
171
|
+
2. VALIDATE: Check structure and references
|
|
172
|
+
3. NORMALIZE: Resolve references, add metadata
|
|
173
|
+
|
|
174
|
+
Layout sizing happens later in the render pipeline (data-aware).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
yaml_content: YAML string to compile
|
|
178
|
+
options: Optional compilation options
|
|
179
|
+
base_dir: Base directory for resolving file references
|
|
180
|
+
project_dir: Project directory for resolving project-level sources
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
CompileResult with compiled face or errors
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> yaml_content = '''
|
|
187
|
+
... title: My Dataface
|
|
188
|
+
... queries:
|
|
189
|
+
... users: SELECT * FROM users
|
|
190
|
+
... charts:
|
|
191
|
+
... user_count:
|
|
192
|
+
... query: users
|
|
193
|
+
... type: kpi
|
|
194
|
+
... value: count
|
|
195
|
+
... rows:
|
|
196
|
+
... - user_count
|
|
197
|
+
... '''
|
|
198
|
+
>>> result = compile(yaml_content)
|
|
199
|
+
>>> if result.success:
|
|
200
|
+
... face = result.face
|
|
201
|
+
... print(face.title) # "My Dataface"
|
|
202
|
+
"""
|
|
203
|
+
options = options or {}
|
|
204
|
+
errors: list[StructuredError] = []
|
|
205
|
+
warnings: list[RenderWarning] = []
|
|
206
|
+
|
|
207
|
+
# ════════════════════════════════════════════════════════════════════
|
|
208
|
+
# STEP 1: Parse YAML
|
|
209
|
+
# ════════════════════════════════════════════════════════════════════
|
|
210
|
+
# Convert YAML string to AuthoredFace object. Handles syntax errors.
|
|
211
|
+
try:
|
|
212
|
+
face = parse_yaml(yaml_content)
|
|
213
|
+
except ParseError as e:
|
|
214
|
+
# When the parse error was caused by a PydanticValidationError, use the
|
|
215
|
+
# union-aware collapse to produce structured errors from the raw Pydantic
|
|
216
|
+
# error list. Other parse errors (bad YAML syntax, empty doc) produce one
|
|
217
|
+
# structured error from the CompilationError shape.
|
|
218
|
+
if isinstance(e.__cause__, PydanticValidationError):
|
|
219
|
+
return CompileResult(
|
|
220
|
+
errors=format_validation_errors_structured(e.__cause__, yaml_content)
|
|
221
|
+
)
|
|
222
|
+
return CompileResult(errors=[e.to_structured()])
|
|
223
|
+
|
|
224
|
+
# ════════════════════════════════════════════════════════════════════
|
|
225
|
+
# STEP 2: Validate Structure
|
|
226
|
+
# ════════════════════════════════════════════════════════════════════
|
|
227
|
+
# Check references and semantic constraints.
|
|
228
|
+
validation_errors = validate_face(face)
|
|
229
|
+
if validation_errors:
|
|
230
|
+
return CompileResult(errors=[e.to_structured() for e in validation_errors])
|
|
231
|
+
|
|
232
|
+
# ════════════════════════════════════════════════════════════════════
|
|
233
|
+
# STEP 3: Get Default Source and Extract Named Sources
|
|
234
|
+
# ════════════════════════════════════════════════════════════════════
|
|
235
|
+
# Get project-level sources when available so named source references and
|
|
236
|
+
# project defaults behave consistently for file-based compiles.
|
|
237
|
+
project_sources = None
|
|
238
|
+
if project_dir is not None:
|
|
239
|
+
from dataface.core.compile.config import get_project_sources
|
|
240
|
+
|
|
241
|
+
project_sources = get_project_sources(project_dir)
|
|
242
|
+
|
|
243
|
+
# Get the face's default source to pass to query normalization.
|
|
244
|
+
default_source = face.get_default_source()
|
|
245
|
+
if default_source is None and project_sources is not None:
|
|
246
|
+
default_source = project_sources.default
|
|
247
|
+
|
|
248
|
+
# Extract named source configurations from the sources section
|
|
249
|
+
# These are used to resolve source references in queries (e.g., source: profiles)
|
|
250
|
+
sources_registry = {}
|
|
251
|
+
if project_sources is not None:
|
|
252
|
+
assert project_dir is not None
|
|
253
|
+
sources_registry.update(
|
|
254
|
+
resolve_project_source_paths(project_sources.sources, project_dir)
|
|
255
|
+
)
|
|
256
|
+
sources_registry.update(_extract_sources(face))
|
|
257
|
+
|
|
258
|
+
# ════════════════════════════════════════════════════════════════════
|
|
259
|
+
# STEP 4: Build Query Registry
|
|
260
|
+
# ════════════════════════════════════════════════════════════════════
|
|
261
|
+
# Collect and normalize all queries before normalization.
|
|
262
|
+
# This allows charts to reference queries from any part of the face.
|
|
263
|
+
try:
|
|
264
|
+
query_registry = build_query_registry(
|
|
265
|
+
face, base_dir, default_source=default_source
|
|
266
|
+
)
|
|
267
|
+
except CompilationError as e:
|
|
268
|
+
return CompileResult(errors=[e.to_structured()])
|
|
269
|
+
|
|
270
|
+
# ════════════════════════════════════════════════════════════════════
|
|
271
|
+
# STEP 5: Detect Circular Dependencies
|
|
272
|
+
# ════════════════════════════════════════════════════════════════════
|
|
273
|
+
# Check for {{ queries.* }} cycles in SQL.
|
|
274
|
+
if query_registry:
|
|
275
|
+
try:
|
|
276
|
+
detect_query_dependencies(query_registry)
|
|
277
|
+
except JinjaError as e:
|
|
278
|
+
return CompileResult(errors=[e.to_structured()])
|
|
279
|
+
|
|
280
|
+
# ════════════════════════════════════════════════════════════════════
|
|
281
|
+
# STEP 5b: Build Chart Registry
|
|
282
|
+
# ════════════════════════════════════════════════════════════════════
|
|
283
|
+
# Collect all charts from the entire face tree (global namespace).
|
|
284
|
+
# Charts are global like queries - any layout can reference any chart.
|
|
285
|
+
try:
|
|
286
|
+
chart_registry = build_chart_registry(face, base_dir=base_dir)
|
|
287
|
+
except CompilationError as e:
|
|
288
|
+
return CompileResult(errors=[e.to_structured()])
|
|
289
|
+
|
|
290
|
+
# ════════════════════════════════════════════════════════════════════
|
|
291
|
+
# STEP 6: Normalize (Transform to Face)
|
|
292
|
+
# ════════════════════════════════════════════════════════════════════
|
|
293
|
+
# Resolve references, add metadata, create unified layout.
|
|
294
|
+
# Note: Variable registry is built by renderer at render time (not needed here)
|
|
295
|
+
try:
|
|
296
|
+
compiled = normalize_face(
|
|
297
|
+
face,
|
|
298
|
+
query_registry=query_registry,
|
|
299
|
+
chart_registry=chart_registry,
|
|
300
|
+
base_path=base_dir,
|
|
301
|
+
)
|
|
302
|
+
except ReferenceError as e:
|
|
303
|
+
return CompileResult(errors=[e.to_structured()])
|
|
304
|
+
except CompilationError as e:
|
|
305
|
+
return CompileResult(errors=[e.to_structured()])
|
|
306
|
+
except PydanticValidationError as e:
|
|
307
|
+
return CompileResult(
|
|
308
|
+
errors=format_validation_errors_structured(e, yaml_content)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
compiled.sources = _to_plain_dict(sources_registry)
|
|
312
|
+
|
|
313
|
+
# Orphan charts: defined but never referenced from any layout in the tree.
|
|
314
|
+
# Warn rather than error — a content-only face still renders, the warning
|
|
315
|
+
# surfaces lazy authoring. Render-side has the harder check for the
|
|
316
|
+
# specific case of "face has charts but layout is empty" (Case A) which
|
|
317
|
+
# would produce a silently empty dashboard.
|
|
318
|
+
for chart_id in _collect_orphan_charts(compiled):
|
|
319
|
+
warnings.append(
|
|
320
|
+
RenderWarning(
|
|
321
|
+
code=unreferenced_chart.CODE,
|
|
322
|
+
chart=chart_id,
|
|
323
|
+
message=(
|
|
324
|
+
f"Chart '{chart_id}' is defined but not referenced in any layout."
|
|
325
|
+
),
|
|
326
|
+
fix=(
|
|
327
|
+
"Add it to `rows:`/`cols:`/`grid:`/`tabs:` "
|
|
328
|
+
"or delete the definition."
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Layout dimensions are calculated later in the render pipeline
|
|
334
|
+
# (after query execution) so table heights can use actual row counts.
|
|
335
|
+
|
|
336
|
+
return CompileResult(
|
|
337
|
+
face=compiled,
|
|
338
|
+
errors=errors,
|
|
339
|
+
warnings=warnings,
|
|
340
|
+
query_registry=query_registry,
|
|
341
|
+
meta_config=options.get("meta_config"),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _collect_orphan_charts(face: Face) -> list[str]:
|
|
346
|
+
"""Return chart IDs defined somewhere in the face tree but never placed
|
|
347
|
+
in any layout. Walks nested faces; chart references resolve globally
|
|
348
|
+
(a parent-defined chart referenced from a nested face is not an orphan).
|
|
349
|
+
"""
|
|
350
|
+
defined: set[str] = set()
|
|
351
|
+
referenced: set[str] = set()
|
|
352
|
+
|
|
353
|
+
def walk(f: Face) -> None:
|
|
354
|
+
if f.charts:
|
|
355
|
+
defined.update(f.charts.keys())
|
|
356
|
+
for item in f.layout.items:
|
|
357
|
+
if item.type == "chart" and item.chart and item.chart.id:
|
|
358
|
+
referenced.add(item.chart.id)
|
|
359
|
+
elif item.type == "face" and item.face is not None:
|
|
360
|
+
walk(item.face)
|
|
361
|
+
|
|
362
|
+
walk(face)
|
|
363
|
+
return sorted(defined - referenced)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _collect_suppressed_codes(
|
|
367
|
+
result: CompileResult,
|
|
368
|
+
meta_config: Any = None,
|
|
369
|
+
) -> dict[str, set[str]]:
|
|
370
|
+
"""Build per-query suppression sets from query ignore + meta.yaml lint config.
|
|
371
|
+
|
|
372
|
+
Returns a dict mapping query_name → set of suppressed diagnostic codes.
|
|
373
|
+
"""
|
|
374
|
+
per_query: dict[str, set[str]] = {}
|
|
375
|
+
# Layer 2: per-query ignore field
|
|
376
|
+
for name, query in result.query_registry.items():
|
|
377
|
+
codes: set[str] = set()
|
|
378
|
+
if query.ignore:
|
|
379
|
+
codes.update(query.ignore)
|
|
380
|
+
per_query[name] = codes
|
|
381
|
+
|
|
382
|
+
# Layer 3: meta.yaml lint config
|
|
383
|
+
if meta_config is not None:
|
|
384
|
+
global_ignore = set(getattr(meta_config, "lint_ignore", []))
|
|
385
|
+
per_query_meta = getattr(meta_config, "lint_ignore_queries", {})
|
|
386
|
+
for name in per_query:
|
|
387
|
+
per_query[name] |= global_ignore
|
|
388
|
+
if name in per_query_meta:
|
|
389
|
+
per_query[name].update(per_query_meta[name])
|
|
390
|
+
|
|
391
|
+
return per_query
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def validate_compiled_queries(
|
|
395
|
+
result: CompileResult,
|
|
396
|
+
relationship_context: RelationshipContext | None = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Run query validation on all SQL queries in a compile result (mutates in place).
|
|
399
|
+
|
|
400
|
+
Calls ``validate_query`` for each SQL query, appends structured
|
|
401
|
+
``QueryDiagnostic`` objects to ``result.diagnostics``, and emits
|
|
402
|
+
``RenderWarning`` entries onto ``result.warnings`` for findings at
|
|
403
|
+
warning severity or above. Each warning carries a stable code owned by
|
|
404
|
+
a module under ``dataface.core.render.warnings`` (see
|
|
405
|
+
``from_query_diagnostic`` for the mapping).
|
|
406
|
+
|
|
407
|
+
Honors diagnostic suppressions from:
|
|
408
|
+
- SQL-inline ``-- dft:ignore`` comments (handled by validate_query)
|
|
409
|
+
- Per-query ``ignore`` field
|
|
410
|
+
- meta.yaml ``lint`` config
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
result: CompileResult to enrich.
|
|
414
|
+
relationship_context: Optional RelationshipContext for severity calibration.
|
|
415
|
+
"""
|
|
416
|
+
if not result.success or not result.query_registry:
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
from dataface.core.compile.models.query.compiled import is_sql_query
|
|
420
|
+
from dataface.core.inspect.query_validator import validate_query
|
|
421
|
+
|
|
422
|
+
suppressed = _collect_suppressed_codes(result, result.meta_config)
|
|
423
|
+
|
|
424
|
+
for name, query in result.query_registry.items():
|
|
425
|
+
if not is_sql_query(query):
|
|
426
|
+
continue
|
|
427
|
+
query_suppress = suppressed.get(name, set())
|
|
428
|
+
diags, suppressed_diags = validate_query(
|
|
429
|
+
query.sql,
|
|
430
|
+
relationship_context=relationship_context,
|
|
431
|
+
suppress=query_suppress,
|
|
432
|
+
return_suppressed=True,
|
|
433
|
+
)
|
|
434
|
+
for d in diags:
|
|
435
|
+
result.diagnostics.append(d)
|
|
436
|
+
if d.severity in ("error", "warning"):
|
|
437
|
+
result.warnings.append(from_query_diagnostic(name, d))
|
|
438
|
+
for d in suppressed_diags:
|
|
439
|
+
result.suppressed_warnings.append(from_query_diagnostic(name, d))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def compile_file(
|
|
443
|
+
file_path: Path,
|
|
444
|
+
options: dict[str, Any] | None = None,
|
|
445
|
+
apply_meta: bool = True,
|
|
446
|
+
root_path: Path | None = None,
|
|
447
|
+
) -> CompileResult:
|
|
448
|
+
"""Compile a YAML file to a Face.
|
|
449
|
+
|
|
450
|
+
Convenience function that reads a file and compiles it. Optionally
|
|
451
|
+
applies meta.yaml cascading configuration from parent directories.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
file_path: Path to YAML file
|
|
455
|
+
options: Optional compilation options
|
|
456
|
+
apply_meta: If True, resolve and apply meta.yaml chain (default: True)
|
|
457
|
+
root_path: Project root for meta resolution (default: auto-detect)
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
CompileResult with compiled face or errors
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
FileNotFoundError: If file doesn't exist
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> result = compile_file(Path("face.yml"))
|
|
467
|
+
>>> if result.success:
|
|
468
|
+
... print(result.face.title)
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
if isinstance(file_path, str):
|
|
472
|
+
file_path = Path(file_path)
|
|
473
|
+
|
|
474
|
+
if not file_path.exists():
|
|
475
|
+
return CompileResult(
|
|
476
|
+
errors=[CompilationError(f"File not found: {file_path}").to_structured()]
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Markdown report files: translate to YAML before compiling
|
|
480
|
+
if file_path.suffix.lower() == ".md":
|
|
481
|
+
from dataface.core.compile.markdown import is_markdown_face, markdown_to_yaml
|
|
482
|
+
|
|
483
|
+
if not is_markdown_face(file_path):
|
|
484
|
+
return CompileResult(
|
|
485
|
+
errors=[
|
|
486
|
+
CompilationError(
|
|
487
|
+
"Not a Dataface face file: .md files require YAML frontmatter "
|
|
488
|
+
"with at least one of: queries, charts, variables, source, sources"
|
|
489
|
+
).to_structured()
|
|
490
|
+
]
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
raw_text = file_path.read_text()
|
|
495
|
+
yaml_content = markdown_to_yaml(raw_text)
|
|
496
|
+
except (OSError, ValueError) as e:
|
|
497
|
+
return CompileResult(
|
|
498
|
+
errors=[CompilationError(f"Markdown parse error: {e}").to_structured()]
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
try:
|
|
502
|
+
yaml_content = file_path.read_text()
|
|
503
|
+
except OSError as e:
|
|
504
|
+
return CompileResult(
|
|
505
|
+
errors=[CompilationError(f"Failed to read file: {e}").to_structured()]
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Apply meta.yaml chain if enabled
|
|
509
|
+
meta_config = None
|
|
510
|
+
if apply_meta:
|
|
511
|
+
try:
|
|
512
|
+
# Parse YAML to dict first
|
|
513
|
+
face_data = yaml.safe_load(yaml_content) or {}
|
|
514
|
+
|
|
515
|
+
if not isinstance(face_data, dict):
|
|
516
|
+
return CompileResult(
|
|
517
|
+
errors=[
|
|
518
|
+
CompilationError(
|
|
519
|
+
f"Face YAML must be a mapping at the top level, got {type(face_data).__name__}"
|
|
520
|
+
).to_structured()
|
|
521
|
+
]
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Resolve meta chain and merge with face
|
|
525
|
+
meta_config = get_meta_for_face(file_path, root_path=root_path)
|
|
526
|
+
if not meta_config.is_empty():
|
|
527
|
+
merged_data = merge_meta_with_face(meta_config, face_data)
|
|
528
|
+
# Re-serialize to YAML for the compile function
|
|
529
|
+
yaml_content = yaml.dump(
|
|
530
|
+
merged_data, default_flow_style=False, sort_keys=False
|
|
531
|
+
)
|
|
532
|
+
except CompilationError as e:
|
|
533
|
+
return CompileResult(errors=[e.to_structured()])
|
|
534
|
+
except (yaml.YAMLError, OSError) as e:
|
|
535
|
+
# Meta resolution errors are warnings, not failures
|
|
536
|
+
# Continue with original content, but log the issue for debugging
|
|
537
|
+
logger.warning(
|
|
538
|
+
"Meta resolution failed for %s, continuing with original content: %s",
|
|
539
|
+
file_path,
|
|
540
|
+
e,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
project_dir = root_path
|
|
544
|
+
if project_dir is None:
|
|
545
|
+
from dataface.core.project_roots import discover_render_context
|
|
546
|
+
|
|
547
|
+
compile_boundary = Path(file_path.anchor) if file_path.anchor else None
|
|
548
|
+
project_dir, _ = discover_render_context(file_path.parent, compile_boundary)
|
|
549
|
+
|
|
550
|
+
# Thread meta lint config through to compile for query diagnostics
|
|
551
|
+
compile_options = dict(options) if options else {}
|
|
552
|
+
if meta_config is not None:
|
|
553
|
+
compile_options["meta_config"] = meta_config
|
|
554
|
+
|
|
555
|
+
result = compile(
|
|
556
|
+
yaml_content,
|
|
557
|
+
options=compile_options,
|
|
558
|
+
base_dir=file_path.parent,
|
|
559
|
+
project_dir=project_dir,
|
|
560
|
+
)
|
|
561
|
+
# Stamp file path and add validate next_command on each compile error so
|
|
562
|
+
# UI surfaces and CLI can surface "dft validate <file>" without re-deriving the path.
|
|
563
|
+
if result.errors:
|
|
564
|
+
file_str = str(file_path)
|
|
565
|
+
result.errors = [_stamp_error_file(e, file_str) for e in result.errors]
|
|
566
|
+
return result
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _stamp_error_file(err: StructuredError, file_str: str) -> StructuredError:
|
|
570
|
+
"""Return a copy of err with file set and a Validate next_command added."""
|
|
571
|
+
from dataface.core.errors.structured import NextCommand
|
|
572
|
+
|
|
573
|
+
next_commands = list(err.next_commands)
|
|
574
|
+
if not any(nc.label == "Validate" for nc in next_commands):
|
|
575
|
+
next_commands.append(
|
|
576
|
+
NextCommand(label="Validate", command=f"dft validate {file_str}")
|
|
577
|
+
)
|
|
578
|
+
return err.model_copy(update={"file": file_str, "next_commands": next_commands})
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
|
|
582
|
+
"""Extract all nested faces from a face's layout.
|
|
583
|
+
|
|
584
|
+
Recursively traverses rows, cols, grid, and tabs to find all nested faces.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
face: AuthoredFace to extract nested faces from
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
List of nested AuthoredFace objects found in the layout
|
|
591
|
+
"""
|
|
592
|
+
from dataface.core.compile.models.face.authored import (
|
|
593
|
+
ForeachItem,
|
|
594
|
+
TabItem,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
nested_faces: list[AuthoredFace] = []
|
|
598
|
+
items: list[Any] = []
|
|
599
|
+
|
|
600
|
+
# Collect items from rows and cols
|
|
601
|
+
if face.rows:
|
|
602
|
+
items.extend(face.rows)
|
|
603
|
+
if face.cols:
|
|
604
|
+
items.extend(face.cols)
|
|
605
|
+
|
|
606
|
+
# Collect items from grid (always GridLayout now — dict form removed)
|
|
607
|
+
if face.grid:
|
|
608
|
+
items.extend([gi.item for gi in face.grid.items])
|
|
609
|
+
|
|
610
|
+
# Collect items from tabs (always TabLayout now — dict form removed)
|
|
611
|
+
if face.tabs:
|
|
612
|
+
items.extend(face.tabs.items)
|
|
613
|
+
|
|
614
|
+
# Process all items to find nested faces
|
|
615
|
+
for item in items:
|
|
616
|
+
if isinstance(item, AuthoredFace):
|
|
617
|
+
nested_faces.append(item)
|
|
618
|
+
elif isinstance(item, TabItem):
|
|
619
|
+
# Tab items contain their own rows/cols — treat as an inline face
|
|
620
|
+
tab_face = AuthoredFace.model_construct(
|
|
621
|
+
rows=item.rows,
|
|
622
|
+
cols=item.cols,
|
|
623
|
+
grid=item.grid,
|
|
624
|
+
tabs=item.tabs,
|
|
625
|
+
)
|
|
626
|
+
nested_faces.append(tab_face)
|
|
627
|
+
elif isinstance(item, ForeachItem):
|
|
628
|
+
# Recurse into foreach items to find any nested faces
|
|
629
|
+
for foreach_subitem in item.foreach.items:
|
|
630
|
+
if isinstance(foreach_subitem, AuthoredFace):
|
|
631
|
+
nested_faces.append(foreach_subitem)
|
|
632
|
+
|
|
633
|
+
return nested_faces
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _build_registry(
|
|
637
|
+
face: AuthoredFace,
|
|
638
|
+
registry_type: str,
|
|
639
|
+
registry: dict[str, Any] | None = None,
|
|
640
|
+
**kwargs: Any,
|
|
641
|
+
) -> dict[str, Any]:
|
|
642
|
+
"""Build a registry for queries or charts recursively.
|
|
643
|
+
|
|
644
|
+
This unified function traverses the entire face tree to collect definitions
|
|
645
|
+
of the specified type. Each face processes its own definitions, then recursively
|
|
646
|
+
processes all nested faces and merges their results.
|
|
647
|
+
|
|
648
|
+
Note: Variables are built at render time, not compile time, so they don't use
|
|
649
|
+
this registry building function.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
face: AuthoredFace to process
|
|
653
|
+
registry_type: Type of registry to build ("queries" or "charts")
|
|
654
|
+
registry: Existing registry to add to (will be created if None)
|
|
655
|
+
**kwargs: Additional arguments specific to registry type:
|
|
656
|
+
- For "queries": base_dir, default_source
|
|
657
|
+
- For "charts": base_dir
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Complete registry dictionary
|
|
661
|
+
|
|
662
|
+
Raises:
|
|
663
|
+
CompilationError: If duplicate names found or invalid registry_type
|
|
664
|
+
"""
|
|
665
|
+
if registry is None:
|
|
666
|
+
registry = {}
|
|
667
|
+
|
|
668
|
+
# Process definitions at this face level
|
|
669
|
+
if registry_type == "queries":
|
|
670
|
+
_process_queries_for_registry(face, registry, **kwargs)
|
|
671
|
+
elif registry_type == "charts":
|
|
672
|
+
_process_charts_for_registry(face, registry, **kwargs)
|
|
673
|
+
else:
|
|
674
|
+
raise CompilationError(f"Invalid registry type: {registry_type}")
|
|
675
|
+
|
|
676
|
+
# Recursively process nested faces
|
|
677
|
+
nested_faces = _extract_nested_faces(face)
|
|
678
|
+
for nested_face in nested_faces:
|
|
679
|
+
_build_registry(nested_face, registry_type, registry, **kwargs)
|
|
680
|
+
|
|
681
|
+
return registry
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _process_queries_for_registry(
|
|
685
|
+
face: AuthoredFace,
|
|
686
|
+
registry: dict[str, AnyQuery],
|
|
687
|
+
base_dir: Path | None = None,
|
|
688
|
+
default_source: str | None = None,
|
|
689
|
+
) -> None:
|
|
690
|
+
"""Process queries from a face and add to registry.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
face: AuthoredFace to process queries from
|
|
694
|
+
registry: Registry to add queries to
|
|
695
|
+
base_dir: Base directory for cross-file references
|
|
696
|
+
default_source: Default source to apply to queries without explicit source
|
|
697
|
+
|
|
698
|
+
Raises:
|
|
699
|
+
CompilationError: If duplicate query names found
|
|
700
|
+
"""
|
|
701
|
+
# Get default source from this face level (may override parent)
|
|
702
|
+
face_default_source = face.get_default_source()
|
|
703
|
+
effective_default_source = face_default_source or default_source
|
|
704
|
+
|
|
705
|
+
# Process queries at this level
|
|
706
|
+
for name, query_def in (face.queries or {}).items():
|
|
707
|
+
if name in registry:
|
|
708
|
+
raise CompilationError(
|
|
709
|
+
f"Duplicate query name '{name}'. Query names must be unique "
|
|
710
|
+
"within a file (and those that are imported)."
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Handle cross-file references
|
|
714
|
+
if isinstance(query_def, QueryRef):
|
|
715
|
+
registry[name] = load_from_reference(query_def, base_dir=base_dir)
|
|
716
|
+
else:
|
|
717
|
+
registry[name] = normalize_query(
|
|
718
|
+
name, query_def, default_source=effective_default_source
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _process_charts_for_registry(
|
|
723
|
+
face: AuthoredFace,
|
|
724
|
+
registry: dict[str, Any],
|
|
725
|
+
base_dir: Path | None = None,
|
|
726
|
+
**kwargs: Any,
|
|
727
|
+
) -> None:
|
|
728
|
+
"""Process charts from a face and add to registry.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
face: AuthoredFace to process charts from
|
|
732
|
+
registry: Registry to add charts to
|
|
733
|
+
base_dir: Base directory for cross-file references
|
|
734
|
+
**kwargs: Additional arguments (e.g., query_registry for future use)
|
|
735
|
+
|
|
736
|
+
Raises:
|
|
737
|
+
CompilationError: If duplicate chart names found
|
|
738
|
+
"""
|
|
739
|
+
# Process charts at this level
|
|
740
|
+
for name, chart_def in (face.charts or {}).items():
|
|
741
|
+
if name in registry:
|
|
742
|
+
raise CompilationError(
|
|
743
|
+
f"Duplicate chart name '{name}'. Chart names must be unique "
|
|
744
|
+
"within a file (and those that are imported)."
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Handle cross-file references
|
|
748
|
+
if isinstance(chart_def, ChartRef):
|
|
749
|
+
registry[name] = load_from_reference(chart_def, base_dir=base_dir)
|
|
750
|
+
else:
|
|
751
|
+
registry[name] = chart_def
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def build_query_registry(
|
|
755
|
+
face: AuthoredFace,
|
|
756
|
+
base_dir: Path | None = None,
|
|
757
|
+
registry: dict[str, AnyQuery] | None = None,
|
|
758
|
+
default_source: str | None = None,
|
|
759
|
+
) -> dict[str, AnyQuery]:
|
|
760
|
+
"""Build complete query registry from face and nested faces.
|
|
761
|
+
|
|
762
|
+
Traverses the entire face structure to collect all query definitions,
|
|
763
|
+
including from nested faces.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
face: AuthoredFace to process
|
|
767
|
+
base_dir: Base directory for cross-file references
|
|
768
|
+
registry: Existing registry to add to
|
|
769
|
+
default_source: Default source to apply to queries without explicit source
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
Complete query registry
|
|
773
|
+
|
|
774
|
+
Raises:
|
|
775
|
+
CompilationError: If duplicate query names found
|
|
776
|
+
"""
|
|
777
|
+
return _build_registry(
|
|
778
|
+
face,
|
|
779
|
+
"queries",
|
|
780
|
+
registry=registry,
|
|
781
|
+
base_dir=base_dir,
|
|
782
|
+
default_source=default_source,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def build_chart_registry(
|
|
787
|
+
face: AuthoredFace,
|
|
788
|
+
base_dir: Path | None = None,
|
|
789
|
+
registry: dict[str, Any] | None = None,
|
|
790
|
+
) -> dict[str, Any]:
|
|
791
|
+
"""Build complete chart registry from face and nested faces.
|
|
792
|
+
|
|
793
|
+
Traverses the entire face structure to collect all chart definitions,
|
|
794
|
+
including from nested faces. Charts are global - any layout can reference any chart.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
face: AuthoredFace to process
|
|
798
|
+
base_dir: Base directory for cross-file references
|
|
799
|
+
registry: Existing registry to add to
|
|
800
|
+
|
|
801
|
+
Returns:
|
|
802
|
+
Complete chart registry (raw chart definitions, not normalized)
|
|
803
|
+
|
|
804
|
+
Raises:
|
|
805
|
+
CompilationError: If duplicate chart names found
|
|
806
|
+
"""
|
|
807
|
+
return _build_registry(face, "charts", registry=registry, base_dir=base_dir)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def load_from_reference(
|
|
811
|
+
reference: VariableRef | QueryRef | ChartRef,
|
|
812
|
+
base_dir: Path | None = None,
|
|
813
|
+
) -> Any:
|
|
814
|
+
"""Load an item from a typed cross-file reference.
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
reference: A typed ref model (VariableRef, QueryRef, or ChartRef).
|
|
818
|
+
The grammar has already been validated by the Pydantic model.
|
|
819
|
+
base_dir: Base directory for resolving paths
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
- QueryRef → AnyQuery
|
|
823
|
+
- ChartRef → chart dict
|
|
824
|
+
- VariableRef → Variable
|
|
825
|
+
|
|
826
|
+
Raises:
|
|
827
|
+
CompilationError: If reference cannot be resolved
|
|
828
|
+
"""
|
|
829
|
+
if isinstance(reference, VariableRef):
|
|
830
|
+
section_name = "variables"
|
|
831
|
+
elif isinstance(reference, QueryRef):
|
|
832
|
+
section_name = "queries"
|
|
833
|
+
elif isinstance(reference, ChartRef):
|
|
834
|
+
section_name = "charts"
|
|
835
|
+
else:
|
|
836
|
+
raise CompilationError(f"Unexpected reference type: {type(reference)!r}")
|
|
837
|
+
|
|
838
|
+
# Grammar already validated by the typed model; split is deterministic.
|
|
839
|
+
file_path_str, item_name = reference.ref.rsplit(f".{section_name}.", 1)
|
|
840
|
+
|
|
841
|
+
# Add extension if needed
|
|
842
|
+
if not (file_path_str.endswith(".yml") or file_path_str.endswith(".yaml")):
|
|
843
|
+
file_path_str += ".yml"
|
|
844
|
+
|
|
845
|
+
full_path = base_dir / file_path_str if base_dir else Path(file_path_str)
|
|
846
|
+
|
|
847
|
+
if not full_path.exists():
|
|
848
|
+
raise CompilationError(f"Referenced file not found: {file_path_str}")
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
with open(full_path) as f:
|
|
852
|
+
content = yaml.safe_load(f)
|
|
853
|
+
except (OSError, yaml.YAMLError) as e:
|
|
854
|
+
raise CompilationError(f"Failed to load {file_path_str}: {e}") from e
|
|
855
|
+
|
|
856
|
+
if not content or section_name not in content:
|
|
857
|
+
raise CompilationError(f"No {section_name} found in {file_path_str}")
|
|
858
|
+
|
|
859
|
+
section = content[section_name]
|
|
860
|
+
singular = {"queries": "query", "charts": "chart", "variables": "variable"}.get(
|
|
861
|
+
section_name, section_name[:-1]
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
if not isinstance(section, dict) or item_name not in section:
|
|
865
|
+
raise CompilationError(
|
|
866
|
+
f"{singular.capitalize()} '{item_name}' not found in {file_path_str}"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
item_def = section[item_name]
|
|
870
|
+
|
|
871
|
+
if section_name == "queries":
|
|
872
|
+
return normalize_query(item_name, item_def)
|
|
873
|
+
if section_name == "charts":
|
|
874
|
+
return item_def
|
|
875
|
+
# section_name == "variables"
|
|
876
|
+
if isinstance(item_def, Variable):
|
|
877
|
+
return item_def
|
|
878
|
+
if isinstance(item_def, dict):
|
|
879
|
+
return Variable(**item_def)
|
|
880
|
+
raise CompilationError(
|
|
881
|
+
f"Invalid variable definition for '{item_name}' in {file_path_str}"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _extract_sources(face: AuthoredFace) -> dict[str, dict]:
|
|
886
|
+
"""Extract named source configurations from AuthoredFace.
|
|
887
|
+
|
|
888
|
+
Extracts named source definitions (excluding 'default') into a dict.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
face: AuthoredFace with optional sources section
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
Dict mapping source name to source config dict
|
|
895
|
+
"""
|
|
896
|
+
if not face.sources:
|
|
897
|
+
return {}
|
|
898
|
+
|
|
899
|
+
# Extract named sources (everything except 'default')
|
|
900
|
+
return {
|
|
901
|
+
k: v
|
|
902
|
+
for k, v in face.sources.model_dump(exclude_none=True).items()
|
|
903
|
+
if k != "default"
|
|
904
|
+
}
|