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,710 @@
|
|
|
1
|
+
"""Unified query interface types — compiled representation.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE (Output) / EXECUTE (Input)
|
|
4
|
+
Purpose: Define type-safe query classes with a unified interface.
|
|
5
|
+
|
|
6
|
+
This module implements the unified query interface pattern where:
|
|
7
|
+
- All queries inherit from a common Query base class
|
|
8
|
+
- Each query type has its own class with type-specific fields
|
|
9
|
+
- Type guards enable type-safe narrowing in adapter code
|
|
10
|
+
- The interface makes adapter routing clear and type-safe
|
|
11
|
+
|
|
12
|
+
Benefits:
|
|
13
|
+
- Type safety: Compile-time verification of query fields
|
|
14
|
+
- Self-documenting: Each query class shows exactly which fields apply
|
|
15
|
+
- Extensibility: Adding new query types is isolated
|
|
16
|
+
- IDE support: Autocomplete and refactoring work correctly
|
|
17
|
+
|
|
18
|
+
Dependencies:
|
|
19
|
+
- pydantic (BaseModel)
|
|
20
|
+
- abc (ABC, abstractmethod)
|
|
21
|
+
|
|
22
|
+
See also:
|
|
23
|
+
- execute/adapters/base.py: Adapters consume these types
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
from abc import ABC, abstractmethod
|
|
29
|
+
from typing import Any, TypeGuard
|
|
30
|
+
|
|
31
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
32
|
+
|
|
33
|
+
from dataface.core.compile.models.query.authored import Pivot, RestMethod, TimeGrain
|
|
34
|
+
|
|
35
|
+
# Note: source field can be a string reference or inline Dict config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# BASE QUERY CLASS
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Query(BaseModel, ABC):
|
|
44
|
+
"""Base query interface - all queries return tabular data.
|
|
45
|
+
|
|
46
|
+
This abstract base class defines the common interface for all query types.
|
|
47
|
+
Subclasses must implement query_type and source_description properties.
|
|
48
|
+
|
|
49
|
+
Common Fields (available on all queries):
|
|
50
|
+
filters: Optional filter conditions to apply to results
|
|
51
|
+
limit: Optional maximum number of rows to return
|
|
52
|
+
|
|
53
|
+
Abstract Properties:
|
|
54
|
+
query_type: Type identifier for adapter routing (e.g., "sql", "csv")
|
|
55
|
+
source_description: Human-readable description of data source
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> class CustomQuery(Query):
|
|
59
|
+
... custom_field: str
|
|
60
|
+
...
|
|
61
|
+
... @property
|
|
62
|
+
... def query_type(self) -> str:
|
|
63
|
+
... return "custom"
|
|
64
|
+
...
|
|
65
|
+
... @property
|
|
66
|
+
... def source_description(self) -> str:
|
|
67
|
+
... return f"Custom: {self.custom_field}"
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Common fields all queries can have
|
|
71
|
+
description: str | None = Field(
|
|
72
|
+
default=None, description="Human-readable description of this query."
|
|
73
|
+
)
|
|
74
|
+
filters: dict[str, Any] | None = Field(
|
|
75
|
+
default=None, description="Filter conditions applied to query results."
|
|
76
|
+
)
|
|
77
|
+
limit: int | None = Field(
|
|
78
|
+
default=None, description="Maximum number of rows to return."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Query-level pivot rendering hint.
|
|
82
|
+
# Note: pivot is a table-renderer hint (cross-tab layout), not a SQL transformation.
|
|
83
|
+
# Chart consumers receive long-form rows; only render_table_svg consumes this field.
|
|
84
|
+
# This lives on the compiled Query base (not just types.Query) so it survives
|
|
85
|
+
# normalize_query's model_dump → SqlQuery(**dict) round-trip.
|
|
86
|
+
pivot: Pivot | None = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description="Cross-tab pivot hint: column values become headers, value fills cells.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Diagnostic suppression — list of diagnostic codes to suppress
|
|
92
|
+
ignore: list[str] | None = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description="Diagnostic codes to suppress (e.g., ['fanout_risk', 'reaggregation']).",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Variable dependencies - computed during normalization
|
|
98
|
+
variable_dependencies: set[str] = Field(
|
|
99
|
+
default_factory=set,
|
|
100
|
+
description="Variable names this query references in SQL/filters (computed during normalization).",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(extra="forbid")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def query_type(self) -> str:
|
|
108
|
+
"""Type identifier for adapter routing.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
String identifying the query type (e.g., "sql", "csv", "http")
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def source_description(self) -> str:
|
|
118
|
+
"""Human-readable description of data source.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
String describing the data source for debugging/logging
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def apply_limit(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
126
|
+
"""Apply limit to result data.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
data: Query results as list of dicts
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Possibly truncated list of dicts
|
|
133
|
+
"""
|
|
134
|
+
if self.limit and self.limit > 0:
|
|
135
|
+
return data[: self.limit]
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ============================================================================
|
|
140
|
+
# TYPE-SPECIFIC QUERY CLASSES
|
|
141
|
+
# ============================================================================
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SqlQuery(Query):
|
|
145
|
+
"""SQL query against a database.
|
|
146
|
+
|
|
147
|
+
Executes raw SQL against the configured database connection.
|
|
148
|
+
Supports Jinja templating for variable substitution.
|
|
149
|
+
|
|
150
|
+
Required Fields:
|
|
151
|
+
sql: SQL query string (may contain Jinja templates)
|
|
152
|
+
|
|
153
|
+
Connection Fields (one required):
|
|
154
|
+
source: Source name or inline config
|
|
155
|
+
|
|
156
|
+
Optional Fields:
|
|
157
|
+
target: dbt target name (defaults to 'dev' if not specified)
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> query = SqlQuery(
|
|
161
|
+
... sql="SELECT * FROM users WHERE id = {{ user_id }}",
|
|
162
|
+
... source="my_postgres"
|
|
163
|
+
... )
|
|
164
|
+
>>> query.query_type # "sql"
|
|
165
|
+
>>> query.source_description # "SQL: SELECT * FROM users WHERE id = {{ user_..."
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
sql: str = Field(
|
|
169
|
+
description="SQL query string. May contain Jinja templates (e.g., {{ variable_name }})."
|
|
170
|
+
)
|
|
171
|
+
setup_sql: str | None = Field(
|
|
172
|
+
default=None,
|
|
173
|
+
description="Non-nestable SQL preamble (e.g., CREATE TEMP FUNCTION). Runs before the main query.",
|
|
174
|
+
)
|
|
175
|
+
# Connection configuration
|
|
176
|
+
source: str | dict[str, Any] | None = Field(
|
|
177
|
+
default=None,
|
|
178
|
+
description="Source name or inline source config dict for the database connection.",
|
|
179
|
+
)
|
|
180
|
+
target: str | None = Field(
|
|
181
|
+
default=None,
|
|
182
|
+
description="dbt target name (defaults to 'dev' if not specified).",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def query_type(self) -> str:
|
|
187
|
+
"""Return 'sql' as query type."""
|
|
188
|
+
return "sql"
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def source_description(self) -> str:
|
|
192
|
+
"""Return truncated SQL for description."""
|
|
193
|
+
sql_preview = self.sql[:50] if len(self.sql) > 50 else self.sql
|
|
194
|
+
return f"SQL: {sql_preview}..."
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class CsvQuery(Query):
|
|
198
|
+
"""Query data from a CSV file.
|
|
199
|
+
|
|
200
|
+
Loads and queries data from CSV files. Supports column selection,
|
|
201
|
+
filtering, and limiting results.
|
|
202
|
+
|
|
203
|
+
Fields (one required):
|
|
204
|
+
file: Path to CSV file (relative to project root or absolute)
|
|
205
|
+
source: Source reference or inline CSV source config
|
|
206
|
+
|
|
207
|
+
Optional Fields:
|
|
208
|
+
columns: List of columns to select (default: all)
|
|
209
|
+
filter: Dict of column->value for exact match filtering
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> query = CsvQuery(file="data/sales.csv", columns=["date", "amount"])
|
|
213
|
+
>>> query.query_type # "csv"
|
|
214
|
+
>>> query.source_description # "CSV: data/sales.csv"
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
file: str | None = Field(
|
|
218
|
+
default=None,
|
|
219
|
+
description="Path to CSV file (relative to project root or absolute). Use 'source' for named sources.",
|
|
220
|
+
)
|
|
221
|
+
source: str | dict[str, Any] | None = Field(
|
|
222
|
+
default=None, description="Source name or inline CSV source config dict."
|
|
223
|
+
)
|
|
224
|
+
columns: list[str] | None = Field(
|
|
225
|
+
default=None, description="List of columns to select. Default: all columns."
|
|
226
|
+
)
|
|
227
|
+
filter: dict[str, Any] | None = Field(
|
|
228
|
+
default=None, description="Dict of column->value for exact match row filtering."
|
|
229
|
+
)
|
|
230
|
+
# Additional CSV options
|
|
231
|
+
delimiter: str | None = Field(
|
|
232
|
+
default=None, description="Column delimiter character. Default: comma (',')."
|
|
233
|
+
)
|
|
234
|
+
encoding: str | None = Field(
|
|
235
|
+
default=None, description="File encoding. Default: UTF-8."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def query_type(self) -> str:
|
|
240
|
+
"""Return 'csv' as query type."""
|
|
241
|
+
return "csv"
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def source_description(self) -> str:
|
|
245
|
+
"""Return file path for description."""
|
|
246
|
+
if self.file:
|
|
247
|
+
return f"CSV: {self.file}"
|
|
248
|
+
elif isinstance(self.source, str):
|
|
249
|
+
return f"CSV source: {self.source}"
|
|
250
|
+
elif isinstance(self.source, dict) and "file" in self.source:
|
|
251
|
+
return f"CSV: {self.source['file']}"
|
|
252
|
+
return "CSV query"
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def effective_file(self) -> str | None:
|
|
256
|
+
"""Get effective file path from file or source config."""
|
|
257
|
+
if self.file:
|
|
258
|
+
return self.file
|
|
259
|
+
if isinstance(self.source, dict) and "file" in self.source:
|
|
260
|
+
return self.source["file"]
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class MetricFlowQuery(Query):
|
|
265
|
+
"""Query the dbt Semantic Layer.
|
|
266
|
+
|
|
267
|
+
Executes queries against dbt's Semantic Layer using MetricFlow.
|
|
268
|
+
Supports metrics, dimensions, filters, and time grains.
|
|
269
|
+
|
|
270
|
+
Required Fields:
|
|
271
|
+
metrics: List of metric names to query
|
|
272
|
+
|
|
273
|
+
Optional Fields:
|
|
274
|
+
dimensions: List of dimension names to group by
|
|
275
|
+
time_grain: Time grain for time-based dimensions (day, week, month, etc.)
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> query = MetricFlowQuery(
|
|
279
|
+
... metrics=["revenue", "orders"],
|
|
280
|
+
... dimensions=["date_day", "region"]
|
|
281
|
+
... )
|
|
282
|
+
>>> query.query_type # "metricflow"
|
|
283
|
+
>>> query.source_description # "MetricFlow: revenue, orders"
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
metrics: list[str] = Field(
|
|
287
|
+
description="Metric names to query from the dbt Semantic Layer."
|
|
288
|
+
)
|
|
289
|
+
dimensions: list[str] | None = Field(
|
|
290
|
+
default=None,
|
|
291
|
+
description="Dimension names to group by (e.g., 'date_day', 'region').",
|
|
292
|
+
)
|
|
293
|
+
time_grain: TimeGrain | None = Field(
|
|
294
|
+
default=None,
|
|
295
|
+
description="Time grain for time-based dimensions (day, week, month, quarter, year).",
|
|
296
|
+
)
|
|
297
|
+
source: str | dict[str, Any] | None = Field(
|
|
298
|
+
default=None, description="Source name or inline MetricFlow source config."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def query_type(self) -> str:
|
|
303
|
+
"""Return 'metricflow' as query type."""
|
|
304
|
+
return "metricflow"
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def source_description(self) -> str:
|
|
308
|
+
"""Return metrics list for description."""
|
|
309
|
+
return f"MetricFlow: {', '.join(self.metrics)}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class HttpQuery(Query):
|
|
313
|
+
"""Query data from an HTTP API.
|
|
314
|
+
|
|
315
|
+
Fetches data from REST API endpoints. Supports various HTTP methods,
|
|
316
|
+
headers, query parameters, and request bodies.
|
|
317
|
+
|
|
318
|
+
Required Fields:
|
|
319
|
+
url: Full URL of the API endpoint
|
|
320
|
+
|
|
321
|
+
Optional Fields:
|
|
322
|
+
method: HTTP method (default: GET)
|
|
323
|
+
headers: Request headers
|
|
324
|
+
params: Query parameters
|
|
325
|
+
body: Request body (for POST/PUT/PATCH)
|
|
326
|
+
json_path: JSONPath expression to extract data from response
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
>>> query = HttpQuery(
|
|
330
|
+
... url="https://api.example.com/predict",
|
|
331
|
+
... method="POST",
|
|
332
|
+
... body={"input": "data"}
|
|
333
|
+
... )
|
|
334
|
+
>>> query.query_type # "http"
|
|
335
|
+
>>> query.source_description # "HTTP: POST https://api.example.com/predict"
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
url: str = Field(
|
|
339
|
+
description="Full URL of the HTTP endpoint.",
|
|
340
|
+
)
|
|
341
|
+
method: RestMethod = Field(
|
|
342
|
+
default="GET",
|
|
343
|
+
description="HTTP method (GET, POST, PUT, DELETE, PATCH). Default: GET.",
|
|
344
|
+
)
|
|
345
|
+
headers: dict[str, str] | None = Field(
|
|
346
|
+
default=None, description="Request headers (merged with source-level headers)."
|
|
347
|
+
)
|
|
348
|
+
params: dict[str, Any] | None = Field(
|
|
349
|
+
default=None, description="URL query parameters."
|
|
350
|
+
)
|
|
351
|
+
body: dict[str, Any] | str | None = Field(
|
|
352
|
+
default=None,
|
|
353
|
+
description="Request body for POST/PUT/PATCH (dict or JSON string).",
|
|
354
|
+
)
|
|
355
|
+
json_path: str | None = Field(
|
|
356
|
+
default=None,
|
|
357
|
+
description="JSONPath expression to extract tabular data from the response.",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def query_type(self) -> str:
|
|
362
|
+
"""Return 'http' as query type."""
|
|
363
|
+
return "http"
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def source_description(self) -> str:
|
|
367
|
+
"""Return method and URL for description."""
|
|
368
|
+
return f"HTTP: {self.method} {self.url}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class ValuesQuery(Query):
|
|
372
|
+
"""Inline data query with rows embedded directly in the YAML.
|
|
373
|
+
|
|
374
|
+
Provides two syntaxes:
|
|
375
|
+
1. Dict rows (verbose): rows: [{month: "Jan", revenue: 100}, ...]
|
|
376
|
+
2. Columns + values (compact, SQL-style): columns: [month, revenue], values: [["Jan", 100], ...]
|
|
377
|
+
|
|
378
|
+
When using columns + values, rows are computed automatically via model_post_init.
|
|
379
|
+
|
|
380
|
+
Example (dict rows):
|
|
381
|
+
>>> query = ValuesQuery(rows=[{"month": "Jan", "revenue": 100}])
|
|
382
|
+
|
|
383
|
+
Example (columns + values):
|
|
384
|
+
>>> query = ValuesQuery(
|
|
385
|
+
... columns=["month", "revenue"],
|
|
386
|
+
... values=[["Jan", 100], ["Feb", 140]],
|
|
387
|
+
... )
|
|
388
|
+
>>> query.rows # [{"month": "Jan", "revenue": 100}, {"month": "Feb", "revenue": 140}]
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
rows: list[dict[str, Any]] = Field(
|
|
392
|
+
default_factory=list,
|
|
393
|
+
description="Inline data rows as a list of dicts (e.g., [{month: 'Jan', revenue: 100}]).",
|
|
394
|
+
)
|
|
395
|
+
columns: list[str] | None = Field(
|
|
396
|
+
default=None, description="Column names for compact 'columns + values' syntax."
|
|
397
|
+
)
|
|
398
|
+
values: list[list[Any]] | None = Field(
|
|
399
|
+
default=None,
|
|
400
|
+
description="Row values for compact syntax, parallel to 'columns' (e.g., [['Jan', 100], ['Feb', 140]]).",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def model_post_init(self, _context: Any) -> None:
|
|
404
|
+
"""Build rows from columns + values if provided."""
|
|
405
|
+
if self.columns and self.values is not None and not self.rows:
|
|
406
|
+
ncols = len(self.columns)
|
|
407
|
+
for i, row in enumerate(self.values):
|
|
408
|
+
if len(row) != ncols:
|
|
409
|
+
raise ValueError(
|
|
410
|
+
f"Values row {i} has {len(row)} items, "
|
|
411
|
+
f"expected {ncols} (columns: {self.columns})"
|
|
412
|
+
)
|
|
413
|
+
self.rows = [
|
|
414
|
+
dict(zip(self.columns, row, strict=True)) for row in self.values
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def query_type(self) -> str:
|
|
419
|
+
"""Return 'values' as query type."""
|
|
420
|
+
return "values"
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def source_description(self) -> str:
|
|
424
|
+
"""Return content-based description for caching.
|
|
425
|
+
|
|
426
|
+
Includes a hash of actual row data so that distinct ValuesQuery
|
|
427
|
+
instances with the same row count produce different cache keys.
|
|
428
|
+
Cached after first computation since rows are set once in model_post_init.
|
|
429
|
+
"""
|
|
430
|
+
if not hasattr(self, "_cached_source_description"):
|
|
431
|
+
n = len(self.rows)
|
|
432
|
+
content_hash = hashlib.sha256(
|
|
433
|
+
json.dumps(self.rows, sort_keys=True, default=str).encode()
|
|
434
|
+
).hexdigest()[:12]
|
|
435
|
+
object.__setattr__(
|
|
436
|
+
self,
|
|
437
|
+
"_cached_source_description",
|
|
438
|
+
f"Values: {n} {'row' if n == 1 else 'rows'} [{content_hash}]",
|
|
439
|
+
)
|
|
440
|
+
return self._cached_source_description # type: ignore[attr-defined]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class DbtModelQuery(Query):
|
|
444
|
+
"""Query against a dbt model.
|
|
445
|
+
|
|
446
|
+
Executes queries against a dbt model, using the model's compiled SQL
|
|
447
|
+
or the model's relation name for direct querying.
|
|
448
|
+
|
|
449
|
+
Required Fields:
|
|
450
|
+
model: Name of the dbt model
|
|
451
|
+
|
|
452
|
+
Optional Fields:
|
|
453
|
+
columns: List of columns to select (default: all)
|
|
454
|
+
|
|
455
|
+
Example:
|
|
456
|
+
>>> query = DbtModelQuery(model="stg_customers", columns=["id", "name"])
|
|
457
|
+
>>> query.query_type # "dbt_model"
|
|
458
|
+
>>> query.source_description # "dbt Model: stg_customers"
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
model: str = Field(
|
|
462
|
+
description="Name of the dbt model to query (e.g., 'stg_customers', 'fct_orders')."
|
|
463
|
+
)
|
|
464
|
+
columns: list[str] | None = Field(
|
|
465
|
+
default=None,
|
|
466
|
+
description="Columns to select from the model. Default: all columns.",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def query_type(self) -> str:
|
|
471
|
+
"""Return 'dbt_model' as query type."""
|
|
472
|
+
return "dbt_model"
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def source_description(self) -> str:
|
|
476
|
+
"""Return model name for description."""
|
|
477
|
+
return f"dbt Model: {self.model}"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class SchemaResolverQuery(Query):
|
|
481
|
+
"""Query the LayeredSchemaResolver in-process.
|
|
482
|
+
|
|
483
|
+
Dispatches to list_schemas / list_tables / profile_table / profile_column
|
|
484
|
+
based on which optional fields are populated:
|
|
485
|
+
- source only → list_schemas(source)
|
|
486
|
+
- source + schema → list_tables(source, schema)
|
|
487
|
+
- source + schema + table → profile_table(source, schema, table) → column rows
|
|
488
|
+
- source + schema + table + column → profile_column(source, schema, table, column)
|
|
489
|
+
|
|
490
|
+
Note: The YAML field is ``schema`` but the Python attribute is ``schema_name``
|
|
491
|
+
to avoid shadowing ``BaseModel.schema`` (a Pydantic v2 legacy classmethod).
|
|
492
|
+
|
|
493
|
+
Fields must be literal strings — Jinja templates (``{{``, ``{%``, ``{#``)
|
|
494
|
+
are rejected at model construction time. Field prerequisites are also
|
|
495
|
+
validated at construction time: ``table`` requires ``schema``; ``column``
|
|
496
|
+
requires both ``schema`` and ``table``.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
source: str = Field(description="dbt source name to query.")
|
|
500
|
+
# alias="schema" so YAML authors write `schema: analytics`; Python code uses
|
|
501
|
+
# query.schema_name to avoid the BaseModel.schema classmethod conflict.
|
|
502
|
+
schema_name: str | None = Field(
|
|
503
|
+
default=None, alias="schema", description="Schema name (omit to list schemas)."
|
|
504
|
+
)
|
|
505
|
+
table: str | None = Field(
|
|
506
|
+
default=None, description="Table name (omit to list tables)."
|
|
507
|
+
)
|
|
508
|
+
column: str | None = Field(
|
|
509
|
+
default=None, description="Column name (omit for full table profile)."
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# populate_by_name=True: 'schema' is a Pydantic BaseModel class method name
|
|
513
|
+
# (BaseModel.schema() in v1 / model_json_schema() in v2), so using it as a
|
|
514
|
+
# field name directly would shadow the classmethod. The Python field is
|
|
515
|
+
# `schema_name`; YAML authors write `schema:` and the alias bridges the two.
|
|
516
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
517
|
+
|
|
518
|
+
@field_validator("source", "schema_name", "table", "column", mode="before")
|
|
519
|
+
@classmethod
|
|
520
|
+
def _no_jinja(cls, v: Any) -> Any:
|
|
521
|
+
if isinstance(v, str) and any(tok in v for tok in ("{{", "{%", "{#")):
|
|
522
|
+
raise ValueError(
|
|
523
|
+
"schema_resolver fields must be literal strings; "
|
|
524
|
+
"Jinja templates ({{ }}, {% %}, {# #}) are not supported."
|
|
525
|
+
)
|
|
526
|
+
return v
|
|
527
|
+
|
|
528
|
+
@model_validator(mode="after")
|
|
529
|
+
def _field_prerequisites(self) -> "SchemaResolverQuery":
|
|
530
|
+
if self.table is not None and self.schema_name is None:
|
|
531
|
+
raise ValueError("schema_resolver: 'table' requires 'schema' to be set")
|
|
532
|
+
if self.column is not None and self.table is None:
|
|
533
|
+
raise ValueError(
|
|
534
|
+
"schema_resolver: 'column' requires 'table' (and 'schema') to be set"
|
|
535
|
+
)
|
|
536
|
+
return self
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def query_type(self) -> str:
|
|
540
|
+
return "schema_resolver"
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def source_description(self) -> str:
|
|
544
|
+
parts = [self.source]
|
|
545
|
+
if self.schema_name:
|
|
546
|
+
parts.append(self.schema_name)
|
|
547
|
+
if self.table:
|
|
548
|
+
parts.append(self.table)
|
|
549
|
+
if self.column:
|
|
550
|
+
parts.append(self.column)
|
|
551
|
+
return f"schema_resolver: {'.'.join(parts)}"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# ============================================================================
|
|
555
|
+
# TYPE ALIASES
|
|
556
|
+
# ============================================================================
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# Type alias for any query - union of all query types
|
|
560
|
+
# Use this for type annotations when you want to accept any query type
|
|
561
|
+
AnyQuery = (
|
|
562
|
+
SqlQuery
|
|
563
|
+
| CsvQuery
|
|
564
|
+
| MetricFlowQuery
|
|
565
|
+
| HttpQuery
|
|
566
|
+
| DbtModelQuery
|
|
567
|
+
| ValuesQuery
|
|
568
|
+
| SchemaResolverQuery
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# Set of valid query type strings
|
|
573
|
+
VALID_QUERY_TYPES: set[str] = {
|
|
574
|
+
"sql",
|
|
575
|
+
"csv",
|
|
576
|
+
"metricflow",
|
|
577
|
+
"http",
|
|
578
|
+
"dbt_model",
|
|
579
|
+
"values",
|
|
580
|
+
"schema_resolver",
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# ============================================================================
|
|
585
|
+
# TYPE GUARDS
|
|
586
|
+
# ============================================================================
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def is_sql_query(query: AnyQuery) -> TypeGuard[SqlQuery]:
|
|
590
|
+
"""Type guard for SQL queries.
|
|
591
|
+
|
|
592
|
+
Use this for type-safe narrowing in conditional blocks.
|
|
593
|
+
After this check, the type checker knows the query is SqlQuery.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
query: Any compiled query
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
True if query is a SqlQuery
|
|
600
|
+
|
|
601
|
+
Example:
|
|
602
|
+
>>> if is_sql_query(query):
|
|
603
|
+
... # Type checker knows query.sql exists
|
|
604
|
+
... print(query.sql)
|
|
605
|
+
"""
|
|
606
|
+
return query.query_type == "sql"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def is_csv_query(query: AnyQuery) -> TypeGuard[CsvQuery]:
|
|
610
|
+
"""Type guard for CSV queries.
|
|
611
|
+
|
|
612
|
+
Use this for type-safe narrowing in conditional blocks.
|
|
613
|
+
After this check, the type checker knows the query is CsvQuery.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
query: Any compiled query
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
True if query is a CsvQuery
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
>>> if is_csv_query(query):
|
|
623
|
+
... # Type checker knows query.file exists
|
|
624
|
+
... print(query.file)
|
|
625
|
+
"""
|
|
626
|
+
return query.query_type == "csv"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def is_metricflow_query(query: AnyQuery) -> TypeGuard[MetricFlowQuery]:
|
|
630
|
+
"""Type guard for MetricFlow queries.
|
|
631
|
+
|
|
632
|
+
Use this for type-safe narrowing in conditional blocks.
|
|
633
|
+
After this check, the type checker knows the query is MetricFlowQuery.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
query: Any compiled query
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
True if query is a MetricFlowQuery
|
|
640
|
+
|
|
641
|
+
Example:
|
|
642
|
+
>>> if is_metricflow_query(query):
|
|
643
|
+
... # Type checker knows query.metrics exists
|
|
644
|
+
... print(query.metrics)
|
|
645
|
+
"""
|
|
646
|
+
return query.query_type == "metricflow"
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def is_http_query(query: AnyQuery) -> TypeGuard[HttpQuery]:
|
|
650
|
+
"""Type guard for HTTP queries.
|
|
651
|
+
|
|
652
|
+
Use this for type-safe narrowing in conditional blocks.
|
|
653
|
+
After this check, the type checker knows the query is HttpQuery.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
query: Any compiled query
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
True if query is a HttpQuery
|
|
660
|
+
|
|
661
|
+
Example:
|
|
662
|
+
>>> if is_http_query(query):
|
|
663
|
+
... # Type checker knows query.url exists
|
|
664
|
+
... print(query.url)
|
|
665
|
+
"""
|
|
666
|
+
return query.query_type == "http"
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def is_dbt_model_query(query: AnyQuery) -> TypeGuard[DbtModelQuery]:
|
|
670
|
+
"""Type guard for dbt model queries.
|
|
671
|
+
|
|
672
|
+
Use this for type-safe narrowing in conditional blocks.
|
|
673
|
+
After this check, the type checker knows the query is DbtModelQuery.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
query: Any compiled query
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
True if query is a DbtModelQuery
|
|
680
|
+
|
|
681
|
+
Example:
|
|
682
|
+
>>> if is_dbt_model_query(query):
|
|
683
|
+
... # Type checker knows query.model exists
|
|
684
|
+
... print(query.model)
|
|
685
|
+
"""
|
|
686
|
+
return query.query_type == "dbt_model"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def is_values_query(query: AnyQuery) -> TypeGuard[ValuesQuery]:
|
|
690
|
+
"""Type guard for values queries.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
query: Any compiled query
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
True if query is a ValuesQuery
|
|
697
|
+
"""
|
|
698
|
+
return query.query_type == "values"
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def is_schema_resolver_query(query: AnyQuery) -> TypeGuard[SchemaResolverQuery]:
|
|
702
|
+
"""Type guard for schema resolver queries.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
query: Any compiled query
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
True if query is a SchemaResolverQuery
|
|
709
|
+
"""
|
|
710
|
+
return query.query_type == "schema_resolver"
|