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,162 @@
|
|
|
1
|
+
"""Query execution helpers for the two-phase render pipeline.
|
|
2
|
+
|
|
3
|
+
Phase 1: Execute chart-direct queries that do *not* use ``{{ results.X }}``
|
|
4
|
+
concurrently with ``ThreadPoolExecutor``.
|
|
5
|
+
|
|
6
|
+
Phase 2: Execute chart-direct ``{{ results.X }}`` queries sequentially after
|
|
7
|
+
phase 1, before the render walk begins.
|
|
8
|
+
|
|
9
|
+
Only queries directly referenced by charts are pre-executed. Transitive
|
|
10
|
+
``{{ queries.X }}`` dependencies are *not* pre-executed separately because
|
|
11
|
+
the executor inlines them as subqueries — executing the dependency query on
|
|
12
|
+
its own would be wasted work.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from dataface.core.compile.models.face.compiled import Face, VariableValues
|
|
23
|
+
from dataface.core.compile.models.query.compiled import (
|
|
24
|
+
AnyQuery,
|
|
25
|
+
is_sql_query,
|
|
26
|
+
)
|
|
27
|
+
from dataface.core.execute.batch import DependencyGraph, topological_sort
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from dataface.core.execute.executor import Executor
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_RESULTS_REF_PATTERN = re.compile(r"\{\{\s*results\.(\w+)\s*\}\}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _has_results_refs(query: AnyQuery) -> bool:
|
|
38
|
+
"""Check if a SQL query references ``{{ results.X }}``."""
|
|
39
|
+
if not is_sql_query(query):
|
|
40
|
+
return False
|
|
41
|
+
return "{{ results." in query.sql or "{{results." in query.sql
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _extract_results_refs(query: AnyQuery) -> set[str]:
|
|
45
|
+
"""Extract ``{{ results.X }}`` references from a SQL query."""
|
|
46
|
+
if not is_sql_query(query):
|
|
47
|
+
return set()
|
|
48
|
+
return set(_RESULTS_REF_PATTERN.findall(query.sql))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plan_query_execution_phases(
|
|
52
|
+
face: Face,
|
|
53
|
+
query_names: set[str],
|
|
54
|
+
) -> tuple[set[str], list[str]]:
|
|
55
|
+
"""Split chart-direct queries into parallel and sequential execution phases.
|
|
56
|
+
|
|
57
|
+
Queries that reference ``{{ results.X }}`` must run after the parallel-safe
|
|
58
|
+
phase because they depend on previously executed query results in DuckDB.
|
|
59
|
+
These queries still run before rendering; they are not deferred as a render
|
|
60
|
+
time fallback.
|
|
61
|
+
"""
|
|
62
|
+
parallel_names: set[str] = set()
|
|
63
|
+
sequential_queries: dict[str, AnyQuery] = {}
|
|
64
|
+
|
|
65
|
+
for name in query_names:
|
|
66
|
+
query = face.queries.get(name)
|
|
67
|
+
if query and _has_results_refs(query):
|
|
68
|
+
sequential_queries[name] = query
|
|
69
|
+
else:
|
|
70
|
+
parallel_names.add(name)
|
|
71
|
+
|
|
72
|
+
if not sequential_queries:
|
|
73
|
+
return parallel_names, []
|
|
74
|
+
|
|
75
|
+
graph = DependencyGraph()
|
|
76
|
+
sequential_names = set(sequential_queries)
|
|
77
|
+
for name, query in sequential_queries.items():
|
|
78
|
+
graph.add_node(name)
|
|
79
|
+
for ref in _extract_results_refs(query) & sequential_names:
|
|
80
|
+
graph.add_edge(ref, name)
|
|
81
|
+
|
|
82
|
+
return parallel_names, topological_sort(graph)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def execute_queries_parallel(
|
|
86
|
+
executor: Executor,
|
|
87
|
+
face: Face,
|
|
88
|
+
query_names: set[str],
|
|
89
|
+
variables: VariableValues | None = None,
|
|
90
|
+
max_workers: int = 8,
|
|
91
|
+
) -> dict[str, Exception | None]:
|
|
92
|
+
"""Execute *query_names* concurrently via ``ThreadPoolExecutor``.
|
|
93
|
+
|
|
94
|
+
All queries are submitted immediately — ``{{ queries.X }}`` references are
|
|
95
|
+
Jinja-inlined by the executor so no ordering is needed.
|
|
96
|
+
|
|
97
|
+
``{{ results.X }}`` queries are intentionally *not* accepted here. They
|
|
98
|
+
must be routed through the explicit sequential phase instead of falling
|
|
99
|
+
through to a render-time execution path.
|
|
100
|
+
|
|
101
|
+
Returns ``{query_name: None}`` on success or ``{query_name: exc}``
|
|
102
|
+
on failure. Errors in one query don't block others.
|
|
103
|
+
"""
|
|
104
|
+
if not query_names:
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
unsafe_names = sorted(
|
|
108
|
+
name
|
|
109
|
+
for name in query_names
|
|
110
|
+
if (query := face.queries.get(name)) and _has_results_refs(query)
|
|
111
|
+
)
|
|
112
|
+
if unsafe_names:
|
|
113
|
+
joined = ", ".join(unsafe_names)
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"execute_queries_parallel received queries with {{ results.X }} "
|
|
116
|
+
f"references: {joined}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
results: dict[str, Exception | None] = {}
|
|
120
|
+
|
|
121
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
122
|
+
futures: dict[Future, str] = {
|
|
123
|
+
pool.submit(executor.execute_query, name, variables): name
|
|
124
|
+
for name in query_names
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for fut in as_completed(futures):
|
|
128
|
+
name = futures[fut]
|
|
129
|
+
try:
|
|
130
|
+
fut.result()
|
|
131
|
+
results[name] = None
|
|
132
|
+
except Exception as exc: # noqa: BLE001
|
|
133
|
+
logger.warning("Parallel execution of query '%s' failed: %s", name, exc)
|
|
134
|
+
results[name] = exc
|
|
135
|
+
executor._query_errors[name] = exc
|
|
136
|
+
|
|
137
|
+
return results
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def execute_queries_sequential(
|
|
141
|
+
executor: Executor,
|
|
142
|
+
query_names: list[str],
|
|
143
|
+
variables: VariableValues | None = None,
|
|
144
|
+
) -> dict[str, Exception | None]:
|
|
145
|
+
"""Execute *query_names* sequentially before the render walk.
|
|
146
|
+
|
|
147
|
+
This phase is for direct chart queries that use ``{{ results.X }}``.
|
|
148
|
+
They have runtime dependencies on previously executed query results, so
|
|
149
|
+
they run after the parallel-safe phase but still before rendering.
|
|
150
|
+
"""
|
|
151
|
+
results: dict[str, Exception | None] = {}
|
|
152
|
+
|
|
153
|
+
for name in query_names:
|
|
154
|
+
try:
|
|
155
|
+
executor.execute_query(name, variables)
|
|
156
|
+
results[name] = None
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
logger.warning("Sequential execution of query '%s' failed: %s", name, exc)
|
|
159
|
+
results[name] = exc
|
|
160
|
+
executor._query_errors[name] = exc
|
|
161
|
+
|
|
162
|
+
return results
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Collect and order setup_sql preambles across query dependencies.
|
|
2
|
+
|
|
3
|
+
When query B depends on query A via {{ queries.A }}, and A declares
|
|
4
|
+
setup_sql, that preamble must execute before B's main body runs.
|
|
5
|
+
This module walks the dependency graph, collects setup_sql blocks
|
|
6
|
+
in topological order, and deduplicates identical blocks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from dataface.core.compile.models.query.compiled import (
|
|
12
|
+
AnyQuery,
|
|
13
|
+
is_sql_query,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_QUERY_REF_PATTERN = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def collect_setup_sql(
|
|
20
|
+
query_name: str,
|
|
21
|
+
queries: dict[str, AnyQuery],
|
|
22
|
+
) -> list[str]:
|
|
23
|
+
"""Collect setup_sql statements for a query and all its dependencies.
|
|
24
|
+
|
|
25
|
+
Walks the dependency graph depth-first, returns setup_sql blocks in
|
|
26
|
+
dependency order (dependencies before dependents). Identical blocks
|
|
27
|
+
are deduplicated while preserving the earliest occurrence.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
query_name: Name of the query to collect setup_sql for.
|
|
31
|
+
queries: Full query registry.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Ordered list of unique setup_sql strings to execute before the main query.
|
|
35
|
+
"""
|
|
36
|
+
seen_names: set[str] = set()
|
|
37
|
+
ordered: list[str] = []
|
|
38
|
+
seen_sql: set[str] = set()
|
|
39
|
+
|
|
40
|
+
def _walk(name: str) -> None:
|
|
41
|
+
if name in seen_names or name not in queries:
|
|
42
|
+
return
|
|
43
|
+
seen_names.add(name)
|
|
44
|
+
|
|
45
|
+
query = queries[name]
|
|
46
|
+
# Walk dependencies first (depth-first, dependencies before self)
|
|
47
|
+
if is_sql_query(query):
|
|
48
|
+
for ref in _QUERY_REF_PATTERN.findall(query.sql):
|
|
49
|
+
_walk(ref)
|
|
50
|
+
|
|
51
|
+
# Then collect this query's setup_sql (after deps)
|
|
52
|
+
if is_sql_query(query) and query.setup_sql:
|
|
53
|
+
if query.setup_sql not in seen_sql:
|
|
54
|
+
seen_sql.add(query.setup_sql)
|
|
55
|
+
ordered.append(query.setup_sql)
|
|
56
|
+
|
|
57
|
+
_walk(query_name)
|
|
58
|
+
return ordered
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Project-source state for the execute boundary.
|
|
2
|
+
|
|
3
|
+
Owns the project-source allowlist that the resolver consults: ``_sources.yaml``
|
|
4
|
+
/ ``dataface.yml`` (already cached by ``get_project_sources``) plus any
|
|
5
|
+
synthetic sources registered at runtime (the ``dft inspect`` HTML server is
|
|
6
|
+
the canonical registrar — it synthesizes a ``warehouse`` source when no
|
|
7
|
+
``_sources.yaml`` exists).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from dataface.core.compile.compiler import resolve_project_source_paths
|
|
17
|
+
from dataface.core.compile.config import ProjectSourcesConfig, get_project_sources
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SourceRegistry:
|
|
23
|
+
"""Merged view of project-level sources plus synthetic registrations.
|
|
24
|
+
|
|
25
|
+
A project source by a given name always wins over a synthetic registration
|
|
26
|
+
of the same name (``register`` uses set-if-absent semantics on the synthetic
|
|
27
|
+
map; the project map is layered on top at read time).
|
|
28
|
+
|
|
29
|
+
``project_root=None`` is supported because ``AdapterRegistry`` accepts the
|
|
30
|
+
same — ad-hoc constructions (tests, agent_api one-offs) yield an empty
|
|
31
|
+
source map.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
35
|
+
self._project_root = project_root
|
|
36
|
+
self._synthetic: dict[str, dict[str, Any]] = {}
|
|
37
|
+
|
|
38
|
+
def get(self, name: str) -> dict[str, Any] | None:
|
|
39
|
+
"""Return the resolved config for ``name``, or ``None`` if unknown."""
|
|
40
|
+
return self.all().get(name)
|
|
41
|
+
|
|
42
|
+
def all(self) -> dict[str, dict[str, Any]]:
|
|
43
|
+
"""Return the merged source map (project sources win over synthetics)."""
|
|
44
|
+
return {**self._synthetic, **self._load_project()}
|
|
45
|
+
|
|
46
|
+
def register(self, name: str, config: dict[str, Any]) -> None:
|
|
47
|
+
"""Register a synthetic source. Idempotent: first registration wins."""
|
|
48
|
+
self._synthetic.setdefault(name, dict(config))
|
|
49
|
+
|
|
50
|
+
def project_sources_config(self) -> ProjectSourcesConfig:
|
|
51
|
+
"""Snapshot for the resolver — merged sources plus project default."""
|
|
52
|
+
return ProjectSourcesConfig(default=self._project_default(), sources=self.all())
|
|
53
|
+
|
|
54
|
+
def _load_project(self) -> dict[str, dict[str, Any]]:
|
|
55
|
+
if self._project_root is None:
|
|
56
|
+
return {}
|
|
57
|
+
try:
|
|
58
|
+
project_sources = get_project_sources(self._project_root)
|
|
59
|
+
return resolve_project_source_paths(
|
|
60
|
+
project_sources.sources, self._project_root
|
|
61
|
+
)
|
|
62
|
+
except (OSError, TypeError, ValueError) as e:
|
|
63
|
+
logger.warning("Failed to load project sources: %s", e)
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
def _project_default(self) -> str | None:
|
|
67
|
+
if self._project_root is None:
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
return get_project_sources(self._project_root).default
|
|
71
|
+
except (OSError, TypeError, ValueError):
|
|
72
|
+
return None
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Source resolver: execute-boundary resolution of authored `source:` values.
|
|
2
|
+
|
|
3
|
+
The `SourceResolver` Protocol runs in `AdapterRegistry.execute()` before the
|
|
4
|
+
adapter receives the query. When the authored source resolves to a configured
|
|
5
|
+
profile (face-level, project-level, or an inline dict), the adapter receives a
|
|
6
|
+
typed `SourceConfig`. For source-less queries with no project default
|
|
7
|
+
configured, the resolver returns `None` and the adapter falls through to its
|
|
8
|
+
own default connection (DuckDB ``:memory:`` or the dbt-aware lookup).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Protocol
|
|
15
|
+
|
|
16
|
+
from dataface.core.compile.config import ProjectSourcesConfig
|
|
17
|
+
from dataface.core.compile.errors import DatafaceError
|
|
18
|
+
from dataface.core.compile.models.source import (
|
|
19
|
+
VALID_SOURCE_TYPES,
|
|
20
|
+
SourceConfig,
|
|
21
|
+
parse_source_config,
|
|
22
|
+
)
|
|
23
|
+
from dataface.core.errors.codes_execute import (
|
|
24
|
+
DF_EXECUTE_SOURCE_CROSS_FILE_FORBIDDEN,
|
|
25
|
+
DF_EXECUTE_SOURCE_INLINE_FORBIDDEN,
|
|
26
|
+
DF_EXECUTE_SOURCE_INVALID_TYPE,
|
|
27
|
+
DF_EXECUTE_SOURCE_MISSING_TYPE,
|
|
28
|
+
DF_EXECUTE_SOURCE_NOT_FOUND,
|
|
29
|
+
DF_EXECUTE_SOURCE_NOT_FOUND_EMPTY,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DbtContext:
|
|
35
|
+
"""Opaque marker: a dbt project is in scope for this execute call.
|
|
36
|
+
|
|
37
|
+
When present, unknown string source names fall through to the dbt adapter
|
|
38
|
+
(which resolves profile names via the dbt manifest).
|
|
39
|
+
When absent, unknown names raise DF-EXECUTE-SOURCE-NOT-FOUND.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SourceResolver(Protocol):
|
|
44
|
+
"""Single execute-boundary chokepoint for source resolution.
|
|
45
|
+
|
|
46
|
+
Implementations map an authored `source:` value (str | dict | None) to a
|
|
47
|
+
typed `SourceConfig | None` before any database driver is loaded.
|
|
48
|
+
|
|
49
|
+
Callers (AdapterRegistry) invoke resolve() once per query, then forward the
|
|
50
|
+
result to the matched adapter via a `source_config` kwarg.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def resolve(
|
|
54
|
+
self,
|
|
55
|
+
authored: str | dict[str, Any] | None,
|
|
56
|
+
face_sources: dict[str, dict[str, Any]],
|
|
57
|
+
project_sources: ProjectSourcesConfig,
|
|
58
|
+
dbt_context: DbtContext | None,
|
|
59
|
+
) -> SourceConfig | None:
|
|
60
|
+
"""Resolve an authored source value to a typed SourceConfig.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
authored: The raw `source:` value from the compiled query.
|
|
64
|
+
face_sources: Named sources declared in the face's `sources:` block.
|
|
65
|
+
project_sources: Project-level sources from `_sources.yaml` / dataface.yml.
|
|
66
|
+
dbt_context: Present when a dbt project is in scope; allows unknown
|
|
67
|
+
string names to fall through to the dbt adapter.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Typed SourceConfig, or None when no source applies (adapter uses
|
|
71
|
+
its own default connection) or when dbt_context covers the name.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
DatafaceError: With a DF-EXECUTE-SOURCE-* code on policy violations.
|
|
75
|
+
"""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DefaultSourceResolver:
|
|
80
|
+
"""Behavior-preserving default resolver for CLI, local playground, and inspect surfaces.
|
|
81
|
+
|
|
82
|
+
Resolves authored source values using face-level sources first, then project-level
|
|
83
|
+
sources. Does not apply policy gates — inline dicts and unknown names are accepted
|
|
84
|
+
as-is. Unknown string names with a dbt context fall through to the dbt adapter.
|
|
85
|
+
|
|
86
|
+
Resolution order for a string authored value:
|
|
87
|
+
1. face_sources lookup (face's `sources:` block)
|
|
88
|
+
2. project_sources lookup (`_sources.yaml` / dataface.yml)
|
|
89
|
+
3. dbt fallback: return None when dbt_context is set
|
|
90
|
+
4. raise DF-EXECUTE-SOURCE-NOT-FOUND otherwise
|
|
91
|
+
|
|
92
|
+
For None authored: apply project default when configured, else return None.
|
|
93
|
+
For dict authored: validate and parse directly.
|
|
94
|
+
|
|
95
|
+
Subclasses (e.g. an allowlist-enforcing resolver) override resolve(), apply
|
|
96
|
+
their own policy gates, and delegate to super().resolve() for the shared
|
|
97
|
+
lookup algorithm.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def resolve(
|
|
101
|
+
self,
|
|
102
|
+
authored: str | dict[str, Any] | None,
|
|
103
|
+
face_sources: dict[str, dict[str, Any]],
|
|
104
|
+
project_sources: ProjectSourcesConfig,
|
|
105
|
+
dbt_context: DbtContext | None,
|
|
106
|
+
) -> SourceConfig | None:
|
|
107
|
+
if authored is None:
|
|
108
|
+
default_name = project_sources.default
|
|
109
|
+
if not default_name:
|
|
110
|
+
return None
|
|
111
|
+
source_dict = project_sources.sources.get(default_name)
|
|
112
|
+
if source_dict is None:
|
|
113
|
+
return None
|
|
114
|
+
return self._parse(source_dict)
|
|
115
|
+
|
|
116
|
+
if isinstance(authored, dict):
|
|
117
|
+
return self._parse(authored)
|
|
118
|
+
|
|
119
|
+
source_dict = face_sources.get(authored) or project_sources.sources.get(
|
|
120
|
+
authored
|
|
121
|
+
)
|
|
122
|
+
if source_dict is not None:
|
|
123
|
+
return self._parse(source_dict)
|
|
124
|
+
|
|
125
|
+
if dbt_context is not None:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
available = sorted(project_sources.sources.keys())
|
|
129
|
+
if available:
|
|
130
|
+
raise DatafaceError.from_code(
|
|
131
|
+
DF_EXECUTE_SOURCE_NOT_FOUND,
|
|
132
|
+
source=authored,
|
|
133
|
+
available=available,
|
|
134
|
+
)
|
|
135
|
+
raise DatafaceError.from_code(
|
|
136
|
+
DF_EXECUTE_SOURCE_NOT_FOUND_EMPTY,
|
|
137
|
+
source=authored,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _parse(self, source_dict: dict[str, Any]) -> SourceConfig:
|
|
141
|
+
"""Validate and parse a source dict to a typed SourceConfig."""
|
|
142
|
+
if "type" not in source_dict:
|
|
143
|
+
raise DatafaceError.from_code(
|
|
144
|
+
DF_EXECUTE_SOURCE_MISSING_TYPE,
|
|
145
|
+
offending_value=repr(source_dict),
|
|
146
|
+
)
|
|
147
|
+
if source_dict["type"] not in VALID_SOURCE_TYPES:
|
|
148
|
+
raise DatafaceError.from_code(
|
|
149
|
+
DF_EXECUTE_SOURCE_INVALID_TYPE,
|
|
150
|
+
offending_value=source_dict["type"],
|
|
151
|
+
available=sorted(VALID_SOURCE_TYPES),
|
|
152
|
+
)
|
|
153
|
+
return parse_source_config(source_dict)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _summarize_inline_source(source_dict: dict[str, Any]) -> str:
|
|
157
|
+
"""Summarize a rejected inline source without echoing secret values.
|
|
158
|
+
|
|
159
|
+
The rendered error message surfaces via `QueryResult.error` and structured
|
|
160
|
+
logs — echoing the raw dict (which may carry `password`, `secret`, `token`,
|
|
161
|
+
…) compounds the original mistake. Emit `type` plus the sorted key list so
|
|
162
|
+
authors get actionable feedback ("you authored an inline postgres source")
|
|
163
|
+
without the values.
|
|
164
|
+
"""
|
|
165
|
+
type_value = source_dict.get("type", "<missing type>")
|
|
166
|
+
keys = sorted(source_dict.keys())
|
|
167
|
+
return f"type={type_value!r}, keys={keys}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AllowlistedSourceResolver(DefaultSourceResolver):
|
|
171
|
+
"""Strict source resolver: only names in `project_sources.sources` resolve.
|
|
172
|
+
|
|
173
|
+
Three rejection rules fire before any resolution:
|
|
174
|
+
|
|
175
|
+
1. Inline source dicts — both per-query `source: {type: ..., host: ...}`
|
|
176
|
+
and any face-level `face.sources` entry are refused with
|
|
177
|
+
`DF-EXECUTE-SOURCE-INLINE-FORBIDDEN` (face.sources values are inline
|
|
178
|
+
source definitions by construction, not name references).
|
|
179
|
+
2. Cross-file `#` references — any authored string containing `#` is
|
|
180
|
+
refused with `DF-EXECUTE-SOURCE-CROSS-FILE-FORBIDDEN`.
|
|
181
|
+
3. Unknown names — a string not in `project_sources.sources` is refused
|
|
182
|
+
with `DF-EXECUTE-SOURCE-NOT-FOUND`; the payload's `available` field
|
|
183
|
+
carries the sorted allowlist.
|
|
184
|
+
|
|
185
|
+
Accepted inputs delegate to `DefaultSourceResolver.resolve` for the shared
|
|
186
|
+
lookup. The dbt-context fallback is intentionally disabled: an unknown
|
|
187
|
+
name raises regardless of `dbt_context`, because dbt-profile names are not
|
|
188
|
+
part of the configured allowlist by definition.
|
|
189
|
+
|
|
190
|
+
This is the closed-allowlist resolver — pick it when the caller does not
|
|
191
|
+
trust authored YAML to declare arbitrary connections (e.g. hosted /
|
|
192
|
+
multi-tenant deployments). Loosening any rule changes the trust boundary;
|
|
193
|
+
treat changes here as security decisions, not stylistic ones.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def resolve(
|
|
197
|
+
self,
|
|
198
|
+
authored: str | dict[str, Any] | None,
|
|
199
|
+
face_sources: dict[str, dict[str, Any]],
|
|
200
|
+
project_sources: ProjectSourcesConfig,
|
|
201
|
+
dbt_context: DbtContext | None,
|
|
202
|
+
yaml_path: str = "",
|
|
203
|
+
) -> SourceConfig | None:
|
|
204
|
+
if isinstance(authored, dict):
|
|
205
|
+
raise DatafaceError.from_code(
|
|
206
|
+
DF_EXECUTE_SOURCE_INLINE_FORBIDDEN,
|
|
207
|
+
yaml_path=yaml_path,
|
|
208
|
+
offending_value=_summarize_inline_source(authored),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if isinstance(authored, str) and "#" in authored:
|
|
212
|
+
raise DatafaceError.from_code(
|
|
213
|
+
DF_EXECUTE_SOURCE_CROSS_FILE_FORBIDDEN,
|
|
214
|
+
yaml_path=yaml_path,
|
|
215
|
+
offending_value=authored,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
isinstance(authored, str)
|
|
220
|
+
and authored in face_sources
|
|
221
|
+
and authored not in project_sources.sources
|
|
222
|
+
):
|
|
223
|
+
raise DatafaceError.from_code(
|
|
224
|
+
DF_EXECUTE_SOURCE_INLINE_FORBIDDEN,
|
|
225
|
+
yaml_path=yaml_path,
|
|
226
|
+
offending_value=_summarize_inline_source(face_sources[authored]),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if isinstance(authored, str):
|
|
230
|
+
if authored not in project_sources.sources:
|
|
231
|
+
self._raise_not_found(authored, project_sources, yaml_path)
|
|
232
|
+
elif authored is None:
|
|
233
|
+
default_name = project_sources.default
|
|
234
|
+
if default_name is not None and default_name not in project_sources.sources:
|
|
235
|
+
self._raise_not_found(default_name, project_sources, yaml_path)
|
|
236
|
+
|
|
237
|
+
return super().resolve(
|
|
238
|
+
authored=authored,
|
|
239
|
+
face_sources={},
|
|
240
|
+
project_sources=project_sources,
|
|
241
|
+
dbt_context=None,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _raise_not_found(
|
|
246
|
+
offending: str,
|
|
247
|
+
project_sources: ProjectSourcesConfig,
|
|
248
|
+
yaml_path: str,
|
|
249
|
+
) -> None:
|
|
250
|
+
raise DatafaceError.from_code(
|
|
251
|
+
DF_EXECUTE_SOURCE_NOT_FOUND,
|
|
252
|
+
source=offending,
|
|
253
|
+
available=sorted(project_sources.sources.keys()),
|
|
254
|
+
yaml_path=yaml_path,
|
|
255
|
+
)
|