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,658 @@
|
|
|
1
|
+
"""Typed schema verb — hierarchical data drill-down: source → schema → table → column.
|
|
2
|
+
|
|
3
|
+
Every response is a single ``SchemaResponse`` envelope wrapped in
|
|
4
|
+
``sources``, regardless of drill level. Leaves are bare scalars;
|
|
5
|
+
response-level provenance lives in the ``_meta`` footer.
|
|
6
|
+
|
|
7
|
+
Wildcard targeting at every level: ``*`` for everything, ``"a,b"`` for a
|
|
8
|
+
finite list, ``"stg_*"`` for fnmatch globs. The resolver expands
|
|
9
|
+
wildcards once at entry; this verb is just the wire-shape adapter.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
from dataface.core.compile.errors import DatafaceError
|
|
22
|
+
from dataface.core.errors import (
|
|
23
|
+
DF_EXECUTE_COLUMN_NOT_FOUND,
|
|
24
|
+
DF_EXECUTE_SCHEMA_NOT_FOUND,
|
|
25
|
+
DF_EXECUTE_TABLE_NOT_FOUND,
|
|
26
|
+
)
|
|
27
|
+
from dataface.core.errors.structured import StructuredError
|
|
28
|
+
from dataface.core.execute.adapters import AdapterRegistry
|
|
29
|
+
from dataface.core.inspect.cache_factory import (
|
|
30
|
+
build_resolver,
|
|
31
|
+
project_root_for_registry,
|
|
32
|
+
)
|
|
33
|
+
from dataface.core.inspect.partition_types import TablePartitions
|
|
34
|
+
from dataface.core.inspect.resolver import LayeredSchemaResolver, is_exact_target
|
|
35
|
+
from dataface.core.schema.guidance import attach_empty_table_list_hints
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
# SuperSchemaSource lives in the private dataface-super-schema package.
|
|
39
|
+
# Accepted by schema() as an injection point for callers with the private
|
|
40
|
+
# package installed. At runtime the type is Any; mypy sees the class via
|
|
41
|
+
# the guard.
|
|
42
|
+
from dataface_super_schema.inspect.sources.super_schema import SuperSchemaSource
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SchemaArgs(BaseModel):
|
|
46
|
+
"""Browse the data hierarchy: source → schema → table → column.
|
|
47
|
+
|
|
48
|
+
The deepest non-None arg determines the return tier. No args: lists
|
|
49
|
+
configured data sources (level 1). source=X: lists schema/namespace
|
|
50
|
+
names in that source (level 2). source=X, schema=Y: lists tables with
|
|
51
|
+
lean summaries (level 3 — name, kind, row_count, description, tags
|
|
52
|
+
when known; full column profiles require drilling). source=X,
|
|
53
|
+
schema=Y, table=T: deep-profiles that table (level 4). source=X,
|
|
54
|
+
schema=Y, table=T, column=C: column profile (level 5). Each level
|
|
55
|
+
accepts ``*``, comma-lists (``"a,b,c"``), and fnmatch globs
|
|
56
|
+
(``"stg_*"``) for multi-target queries. Always wrapped in ``sources``;
|
|
57
|
+
response-level provenance lives in the ``_meta`` footer. If
|
|
58
|
+
it returns zero tables, do not assume the project has no data — some
|
|
59
|
+
projects query files directly (for example via read_csv(...)) instead
|
|
60
|
+
of persistent database tables.
|
|
61
|
+
|
|
62
|
+
Power-user note: ``--json | jq`` is the escape hatch for ad-hoc
|
|
63
|
+
cross-cutting queries the curated verbs don't anticipate — e.g. find
|
|
64
|
+
every column ending in ``_at``, or list tables above a row-count
|
|
65
|
+
threshold. The JSON shape is a stable named-dict tree; pipe it into
|
|
66
|
+
``jq`` and use ``.sources.<src>.schemas.<schema>.tables`` as the root.
|
|
67
|
+
For keyword and predicate search across descriptions, tags, and roles
|
|
68
|
+
use ``dft schema -s <keyword>`` instead.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
72
|
+
|
|
73
|
+
source: str | None = Field(
|
|
74
|
+
None,
|
|
75
|
+
description="Data source name. Omit to list all configured sources.",
|
|
76
|
+
)
|
|
77
|
+
schema_name: str | None = Field(
|
|
78
|
+
None,
|
|
79
|
+
alias="schema",
|
|
80
|
+
description=(
|
|
81
|
+
"Schema/dataset/namespace name. Requires source. Omit to list "
|
|
82
|
+
"schemas in the source. Accepts wildcards: '*', comma-lists, "
|
|
83
|
+
"fnmatch globs ('stg_*')."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
table: str | None = Field(
|
|
87
|
+
None,
|
|
88
|
+
description=(
|
|
89
|
+
"Table name. Requires source and schema. Accepts wildcards: "
|
|
90
|
+
"'*', comma-lists, fnmatch globs ('stg_*')."
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
column: str | None = Field(
|
|
94
|
+
None,
|
|
95
|
+
description=(
|
|
96
|
+
"Column name. Requires source, schema, and table. Accepts "
|
|
97
|
+
"wildcards: '*', comma-lists, fnmatch globs ('*_id')."
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
force_refresh: bool = Field(
|
|
101
|
+
False,
|
|
102
|
+
description=(
|
|
103
|
+
"Re-profile a specific table even if cached data exists; valid only "
|
|
104
|
+
"with source, schema, and table, and without column."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
table_search: str | None = Field(
|
|
108
|
+
None,
|
|
109
|
+
description=(
|
|
110
|
+
"Regexp filter (re.search, re.IGNORECASE) applied at level 3 "
|
|
111
|
+
"(source + schema → tables). A table is included when the pattern matches "
|
|
112
|
+
"its name or any cached column name — the match is unanchored, so 'id' "
|
|
113
|
+
"matches 'user_id'. Un-profiled tables filter on name only; no extra DB "
|
|
114
|
+
"queries are issued. Example: 'revenue|arr|mrr'."
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ResponseMeta(BaseModel):
|
|
120
|
+
"""Response-level provenance footer.
|
|
121
|
+
|
|
122
|
+
Present on successful resolver responses. Validation-error envelopes may
|
|
123
|
+
omit it. ``sources_consulted`` reports which layers contributed:
|
|
124
|
+
``["super_schema"]`` on cache hit, ``["dbt_adapter", "dbt_manifest"]``
|
|
125
|
+
on cache miss with manifest, ``["dbt_adapter"]`` without manifest, ``[]``
|
|
126
|
+
for level 1 (registry-only).
|
|
127
|
+
|
|
128
|
+
``retrieved_at`` and ``cache_built_at`` are ISO-8601 strings, not
|
|
129
|
+
``datetime`` objects — the MCP transport calls plain ``json.dumps``
|
|
130
|
+
on the model_dump output and would choke on a datetime instance.
|
|
131
|
+
The resolver stamps them via ``datetime.isoformat()`` upstream.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
135
|
+
|
|
136
|
+
retrieved_at: str
|
|
137
|
+
sources_consulted: list[Literal["super_schema", "dbt_adapter", "dbt_manifest"]] = []
|
|
138
|
+
cache_built_at: str | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TableProfile(BaseModel):
|
|
142
|
+
"""Typed level-4 table profile fields added by the schema verb.
|
|
143
|
+
|
|
144
|
+
The named fields (``partitions``, ``last_modified``) enumerate the
|
|
145
|
+
agent-api typed contract; additional fields emitted by the
|
|
146
|
+
``LayeredSchemaResolver`` (rows, description, columns, …) flow through
|
|
147
|
+
via ``extra='allow'`` because the resolver owns the level-4 wire shape
|
|
148
|
+
and this model is the bridge — not the spec.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
152
|
+
|
|
153
|
+
partitions: TablePartitions | None = None
|
|
154
|
+
last_modified: datetime | None = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SchemaResponse(BaseModel):
|
|
158
|
+
"""Single envelope for every drill level.
|
|
159
|
+
|
|
160
|
+
``sources`` is the named-dict tree (``dict[str, ...]`` at every level).
|
|
161
|
+
``_meta`` carries response-level provenance. ``success`` and ``errors``
|
|
162
|
+
preserve the MCP envelope guarantee. ``structured_errors`` carries
|
|
163
|
+
fully-typed error objects for MCP consumers that can act on code/doc_url.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
167
|
+
|
|
168
|
+
success: bool = True
|
|
169
|
+
sources: dict[str, dict[str, Any]] = Field(
|
|
170
|
+
default_factory=dict,
|
|
171
|
+
description="Schema data keyed by source name (tables, columns, etc.).",
|
|
172
|
+
)
|
|
173
|
+
supported_query_types: list[str] | None = None
|
|
174
|
+
meta: ResponseMeta | None = Field(
|
|
175
|
+
default=None, alias="_meta", description="Pagination and timing metadata."
|
|
176
|
+
)
|
|
177
|
+
errors: list[str] = []
|
|
178
|
+
structured_errors: list[StructuredError] = []
|
|
179
|
+
# Empty-tables UX: when level 3 returns nothing the verb attaches
|
|
180
|
+
# discoverability hints. Optional; absent on success-with-results.
|
|
181
|
+
message: str | None = None
|
|
182
|
+
warnings: list[str] | None = None
|
|
183
|
+
hints: list[str] | None = None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_LINEAGE_DEPTH_MIN = 1
|
|
187
|
+
_LINEAGE_DEPTH_MAX = 10
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def schema(
|
|
191
|
+
source: str | None = None,
|
|
192
|
+
schema: str | None = None,
|
|
193
|
+
table: str | None = None,
|
|
194
|
+
column: str | None = None,
|
|
195
|
+
*,
|
|
196
|
+
cache_path: Path | None = None,
|
|
197
|
+
cache_source: SuperSchemaSource | None = None,
|
|
198
|
+
force_refresh: bool = False,
|
|
199
|
+
lineage_depth: int = 1,
|
|
200
|
+
table_search: str | None = None,
|
|
201
|
+
surface: Literal["cli", "mcp"] = "mcp",
|
|
202
|
+
adapter_registry: AdapterRegistry,
|
|
203
|
+
) -> SchemaResponse:
|
|
204
|
+
"""Browse the data hierarchy: source → schema → table → column.
|
|
205
|
+
|
|
206
|
+
``table_search`` is a regexp (``re.IGNORECASE``) applied at level 3 (schema
|
|
207
|
+
→ tables). A table passes if the pattern matches its name OR any cached
|
|
208
|
+
column name. For un-profiled tables (no cached column list), name-only
|
|
209
|
+
matching is applied — no extra DB queries are issued.
|
|
210
|
+
|
|
211
|
+
``cache_path``: Explicit path to ``super_schema.json``. When ``None``
|
|
212
|
+
(default), the path is auto-derived from the registry's project root.
|
|
213
|
+
Pass an explicit path only when the cache lives outside the project root.
|
|
214
|
+
|
|
215
|
+
``cache_source``: Inject a pre-constructed ``SuperSchemaSource`` directly.
|
|
216
|
+
Takes priority over ``cache_path`` when both are provided. Use when
|
|
217
|
+
the caller has already constructed the cache source.
|
|
218
|
+
|
|
219
|
+
``surface``: Controls vocabulary in empty-cache hints. ``"cli"`` emits
|
|
220
|
+
``dft inspect`` / ``dft search`` / ``dft query`` verbs; ``"mcp"`` (default)
|
|
221
|
+
emits MCP tool names (``search_dashboards``, ``execute_query``).
|
|
222
|
+
"""
|
|
223
|
+
if not (_LINEAGE_DEPTH_MIN <= lineage_depth <= _LINEAGE_DEPTH_MAX):
|
|
224
|
+
return SchemaResponse(
|
|
225
|
+
success=False,
|
|
226
|
+
errors=[
|
|
227
|
+
f"lineage_depth must be between {_LINEAGE_DEPTH_MIN} and "
|
|
228
|
+
f"{_LINEAGE_DEPTH_MAX}, got {lineage_depth}"
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
invalid = _validate_schema_request(
|
|
233
|
+
source=source,
|
|
234
|
+
schema=schema,
|
|
235
|
+
table=table,
|
|
236
|
+
column=column,
|
|
237
|
+
force_refresh=force_refresh,
|
|
238
|
+
table_search=table_search,
|
|
239
|
+
)
|
|
240
|
+
if invalid is not None:
|
|
241
|
+
return invalid
|
|
242
|
+
|
|
243
|
+
compiled_search: re.Pattern[str] | None = None
|
|
244
|
+
if table_search is not None:
|
|
245
|
+
try:
|
|
246
|
+
compiled_search = re.compile(table_search, re.IGNORECASE)
|
|
247
|
+
except re.error as exc:
|
|
248
|
+
return SchemaResponse(
|
|
249
|
+
success=False,
|
|
250
|
+
errors=[f"table_search is not a valid regexp: {exc}"],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if source is None:
|
|
254
|
+
response = _list_sources_response(adapter_registry)
|
|
255
|
+
response.supported_query_types = sorted(adapter_registry.supported_types)
|
|
256
|
+
return response
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
resolver = build_resolver(
|
|
260
|
+
adapter_registry, cache_path, cache_source=cache_source
|
|
261
|
+
)
|
|
262
|
+
raw = _dispatch_schema_request(
|
|
263
|
+
resolver=resolver,
|
|
264
|
+
source=source,
|
|
265
|
+
schema=schema,
|
|
266
|
+
table=table,
|
|
267
|
+
column=column,
|
|
268
|
+
force_refresh=force_refresh,
|
|
269
|
+
lineage_depth=lineage_depth,
|
|
270
|
+
)
|
|
271
|
+
response = SchemaResponse.model_validate(raw)
|
|
272
|
+
_validate_table_profiles(
|
|
273
|
+
response,
|
|
274
|
+
source=source,
|
|
275
|
+
schema=schema,
|
|
276
|
+
table=table,
|
|
277
|
+
column=column,
|
|
278
|
+
)
|
|
279
|
+
not_found = _validate_exact_targets_found(
|
|
280
|
+
response,
|
|
281
|
+
resolver=resolver,
|
|
282
|
+
source=source,
|
|
283
|
+
schema=schema,
|
|
284
|
+
table=table,
|
|
285
|
+
column=column,
|
|
286
|
+
)
|
|
287
|
+
if not_found is not None:
|
|
288
|
+
return not_found
|
|
289
|
+
except DatafaceError as exc:
|
|
290
|
+
return SchemaResponse(
|
|
291
|
+
success=False,
|
|
292
|
+
errors=[str(exc)],
|
|
293
|
+
structured_errors=[exc.to_structured()],
|
|
294
|
+
)
|
|
295
|
+
except Exception as exc: # noqa: BLE001 — verb returns errors via response envelope
|
|
296
|
+
return SchemaResponse(success=False, errors=[str(exc)])
|
|
297
|
+
|
|
298
|
+
if schema is not None and table is None:
|
|
299
|
+
assert source is not None
|
|
300
|
+
if compiled_search is not None:
|
|
301
|
+
_filter_tables_by_search(
|
|
302
|
+
response, source=source, rx=compiled_search, resolver=resolver
|
|
303
|
+
)
|
|
304
|
+
# Skip the "no tables found" cache-guidance hints — the empty result is
|
|
305
|
+
# from the filter, not a missing cache. Attach a filter-specific message.
|
|
306
|
+
# compiled_search was built from table_search, so table_search is str here.
|
|
307
|
+
assert isinstance(table_search, str)
|
|
308
|
+
_attach_filtered_empty_message(
|
|
309
|
+
response, source=source, pattern=table_search
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
project_root = project_root_for_registry(adapter_registry)
|
|
313
|
+
attach_empty_table_list_hints(
|
|
314
|
+
response,
|
|
315
|
+
adapter_registry,
|
|
316
|
+
source,
|
|
317
|
+
uses_direct_file_queries=lambda: _project_uses_direct_file_queries(
|
|
318
|
+
project_root
|
|
319
|
+
),
|
|
320
|
+
surface=surface,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return response
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _validate_exact_targets_found(
|
|
327
|
+
response: SchemaResponse,
|
|
328
|
+
resolver: LayeredSchemaResolver,
|
|
329
|
+
source: str,
|
|
330
|
+
schema: str | None,
|
|
331
|
+
table: str | None,
|
|
332
|
+
column: str | None,
|
|
333
|
+
) -> SchemaResponse | None:
|
|
334
|
+
"""Check whether every exact drill-down target exists in the source universe.
|
|
335
|
+
|
|
336
|
+
Only validates tiers where the spec is exact AND all parent tiers are also
|
|
337
|
+
exact. Returns a ``SchemaResponse(success=False, ...)`` envelope on the
|
|
338
|
+
first missing tier, or ``None`` when all exact targets were found (or a
|
|
339
|
+
wildcard appears at some tier).
|
|
340
|
+
|
|
341
|
+
Universe queries (``list_schemas``, ``list_tables``) are deferred to the
|
|
342
|
+
error path — only called when the response tree is missing the target.
|
|
343
|
+
This avoids extra adapter round-trips on successful requests.
|
|
344
|
+
"""
|
|
345
|
+
if not is_exact_target(schema):
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
# Check schema tier using the response tree (fast path).
|
|
349
|
+
# The resolver omits a schema entry when none of its tables matched the
|
|
350
|
+
# table spec, so absence can mean either "schema not found" or "schema
|
|
351
|
+
# exists but no tables matched". We resolve the ambiguity lazily by
|
|
352
|
+
# querying the universe only when the schema is absent from the response.
|
|
353
|
+
source_data = response.sources.get(source, {})
|
|
354
|
+
schema_in_response = schema in source_data.get("schemas", {})
|
|
355
|
+
|
|
356
|
+
if not schema_in_response:
|
|
357
|
+
# Query the real schema universe to distinguish "schema not found"
|
|
358
|
+
# from "schema found but no tables matched".
|
|
359
|
+
all_schemas = sorted(
|
|
360
|
+
resolver.list_schemas(source=source)
|
|
361
|
+
.get("sources", {})
|
|
362
|
+
.get(source, {})
|
|
363
|
+
.get("schemas", {})
|
|
364
|
+
.keys()
|
|
365
|
+
)
|
|
366
|
+
if schema not in all_schemas:
|
|
367
|
+
exc = DatafaceError.from_code(
|
|
368
|
+
DF_EXECUTE_SCHEMA_NOT_FOUND,
|
|
369
|
+
schema=schema,
|
|
370
|
+
source=source,
|
|
371
|
+
available=", ".join(all_schemas) if all_schemas else "(none)",
|
|
372
|
+
)
|
|
373
|
+
return SchemaResponse(
|
|
374
|
+
success=False,
|
|
375
|
+
errors=[str(exc)],
|
|
376
|
+
structured_errors=[exc.to_structured()],
|
|
377
|
+
)
|
|
378
|
+
# Schema exists but was omitted because no tables matched.
|
|
379
|
+
# Fall through to the table-tier check.
|
|
380
|
+
|
|
381
|
+
if not is_exact_target(table):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# is_exact_target(schema) returned True above, so schema is not None.
|
|
385
|
+
assert schema is not None
|
|
386
|
+
|
|
387
|
+
# Check table tier using the response tree.
|
|
388
|
+
schema_data = source_data.get("schemas", {}).get(schema, {})
|
|
389
|
+
table_in_response = table in schema_data.get("tables", {})
|
|
390
|
+
|
|
391
|
+
if not table_in_response:
|
|
392
|
+
all_tables = sorted(
|
|
393
|
+
resolver.list_tables(source=source, schema=schema)
|
|
394
|
+
.get("sources", {})
|
|
395
|
+
.get(source, {})
|
|
396
|
+
.get("schemas", {})
|
|
397
|
+
.get(schema, {})
|
|
398
|
+
.get("tables", {})
|
|
399
|
+
.keys()
|
|
400
|
+
)
|
|
401
|
+
if table not in all_tables:
|
|
402
|
+
exc = DatafaceError.from_code(
|
|
403
|
+
DF_EXECUTE_TABLE_NOT_FOUND,
|
|
404
|
+
table=table,
|
|
405
|
+
schema=schema,
|
|
406
|
+
source=source,
|
|
407
|
+
available=", ".join(all_tables) if all_tables else "(none)",
|
|
408
|
+
)
|
|
409
|
+
return SchemaResponse(
|
|
410
|
+
success=False,
|
|
411
|
+
errors=[str(exc)],
|
|
412
|
+
structured_errors=[exc.to_structured()],
|
|
413
|
+
)
|
|
414
|
+
# Table exists but is absent from the response because the column
|
|
415
|
+
# filter already dropped it. Fall through to the column-tier check.
|
|
416
|
+
|
|
417
|
+
if not is_exact_target(column):
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
# Check column tier.
|
|
421
|
+
# When the table is in the response, the column list is reliable (exact
|
|
422
|
+
# table profile was dispatched). When the table was dropped by the column
|
|
423
|
+
# filter (table_in_response is False), the column is definitively absent —
|
|
424
|
+
# we confirmed the table exists above, so the column is not found.
|
|
425
|
+
if table_in_response:
|
|
426
|
+
table_data = schema_data.get("tables", {}).get(table, {})
|
|
427
|
+
columns_present = set((table_data.get("columns") or {}).keys())
|
|
428
|
+
else:
|
|
429
|
+
# Table was dropped by the column filter — fetch the column universe
|
|
430
|
+
# from the live resolver to build the "available" hint.
|
|
431
|
+
# is_exact_target(table) returned True above, so table is not None.
|
|
432
|
+
assert table is not None
|
|
433
|
+
full_profile = resolver.profile_table(source=source, schema=schema, table=table)
|
|
434
|
+
raw_cols = (
|
|
435
|
+
full_profile.get("sources", {})
|
|
436
|
+
.get(source, {})
|
|
437
|
+
.get("schemas", {})
|
|
438
|
+
.get(schema, {})
|
|
439
|
+
.get("tables", {})
|
|
440
|
+
.get(table, {})
|
|
441
|
+
.get("columns", {})
|
|
442
|
+
)
|
|
443
|
+
columns_present = set(raw_cols.keys())
|
|
444
|
+
|
|
445
|
+
if column not in columns_present:
|
|
446
|
+
exc = DatafaceError.from_code(
|
|
447
|
+
DF_EXECUTE_COLUMN_NOT_FOUND,
|
|
448
|
+
col_name=column,
|
|
449
|
+
table=table,
|
|
450
|
+
schema=schema,
|
|
451
|
+
source=source,
|
|
452
|
+
available=(
|
|
453
|
+
", ".join(sorted(columns_present)) if columns_present else "(none)"
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
return SchemaResponse(
|
|
457
|
+
success=False,
|
|
458
|
+
errors=[str(exc)],
|
|
459
|
+
structured_errors=[exc.to_structured()],
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _validate_table_profiles(
|
|
466
|
+
response: SchemaResponse,
|
|
467
|
+
source: str,
|
|
468
|
+
schema: str | None,
|
|
469
|
+
table: str | None,
|
|
470
|
+
column: str | None,
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Validate level-4 profile-table dicts against the agent API shape."""
|
|
473
|
+
if schema is None or table is None or column is not None:
|
|
474
|
+
return
|
|
475
|
+
schemas = response.sources.get(source, {}).get("schemas", {})
|
|
476
|
+
for schema_entry in schemas.values():
|
|
477
|
+
tables = schema_entry.get("tables", {})
|
|
478
|
+
for raw_profile in tables.values():
|
|
479
|
+
if isinstance(raw_profile, dict):
|
|
480
|
+
TableProfile.model_validate(raw_profile)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _list_sources_response(adapter_registry: AdapterRegistry) -> SchemaResponse:
|
|
484
|
+
sources: dict[str, dict[str, Any]] = {}
|
|
485
|
+
for entry in adapter_registry.list_sql_sources():
|
|
486
|
+
source_entry = dict(entry)
|
|
487
|
+
name = str(source_entry.pop("name"))
|
|
488
|
+
sources[name] = source_entry
|
|
489
|
+
return SchemaResponse(
|
|
490
|
+
sources=sources,
|
|
491
|
+
_meta=ResponseMeta(
|
|
492
|
+
retrieved_at=datetime.now(timezone.utc).isoformat(),
|
|
493
|
+
sources_consulted=[],
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _filter_tables_by_search(
|
|
499
|
+
response: SchemaResponse,
|
|
500
|
+
source: str,
|
|
501
|
+
rx: re.Pattern[str],
|
|
502
|
+
resolver: LayeredSchemaResolver,
|
|
503
|
+
) -> None:
|
|
504
|
+
"""Remove tables from a level-3 response that don't match ``rx``.
|
|
505
|
+
|
|
506
|
+
A table passes when the compiled pattern matches its name, or when the
|
|
507
|
+
cache has a profile for the table and any cached column name matches.
|
|
508
|
+
Un-profiled tables (no cache entry) are filtered on name only —
|
|
509
|
+
no extra DB queries are issued.
|
|
510
|
+
|
|
511
|
+
Reads the cache file at most once per schema (not once per table).
|
|
512
|
+
|
|
513
|
+
Mutates ``response`` in place.
|
|
514
|
+
"""
|
|
515
|
+
schemas = response.sources.get(source, {}).get("schemas", {})
|
|
516
|
+
for schema_name, schema_entry in schemas.items():
|
|
517
|
+
tables = schema_entry.get("tables")
|
|
518
|
+
if not tables:
|
|
519
|
+
continue
|
|
520
|
+
# Single read for the whole schema — avoids N file-parses in the loop.
|
|
521
|
+
# When no cache source is configured (OSS dbt-only mode), there are no
|
|
522
|
+
# profiled column names; filtering falls back to name-only matching.
|
|
523
|
+
col_names_by_table: dict[str, set[str]] = (
|
|
524
|
+
resolver.cache.column_names_for_schema(schema_name)
|
|
525
|
+
if resolver.cache is not None
|
|
526
|
+
else {}
|
|
527
|
+
)
|
|
528
|
+
to_drop = [
|
|
529
|
+
name
|
|
530
|
+
for name in tables
|
|
531
|
+
if not _table_matches(rx, name, col_names_by_table.get(name))
|
|
532
|
+
]
|
|
533
|
+
for name in to_drop:
|
|
534
|
+
del tables[name]
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _attach_filtered_empty_message(
|
|
538
|
+
response: SchemaResponse,
|
|
539
|
+
source: str,
|
|
540
|
+
pattern: str,
|
|
541
|
+
) -> None:
|
|
542
|
+
"""Set a filter-specific message when table_search empties the table list.
|
|
543
|
+
|
|
544
|
+
Prevents the "no cache / run dft inspect" guidance from being shown when
|
|
545
|
+
the result is empty solely because the regexp matched nothing.
|
|
546
|
+
"""
|
|
547
|
+
schemas = response.sources.get(source, {}).get("schemas", {})
|
|
548
|
+
total_tables = sum(len(se.get("tables") or {}) for se in schemas.values())
|
|
549
|
+
if total_tables == 0:
|
|
550
|
+
response.message = f"No tables matched table_search={pattern!r}."
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _table_matches(
|
|
554
|
+
rx: re.Pattern[str],
|
|
555
|
+
table_name: str,
|
|
556
|
+
col_names: set[str] | None,
|
|
557
|
+
) -> bool:
|
|
558
|
+
"""Return True if ``rx`` matches the table name or any column name.
|
|
559
|
+
|
|
560
|
+
``col_names`` is ``None`` for un-profiled tables — name-only match applies.
|
|
561
|
+
No DB queries are issued.
|
|
562
|
+
"""
|
|
563
|
+
if rx.search(table_name):
|
|
564
|
+
return True
|
|
565
|
+
return col_names is not None and any(rx.search(c) for c in col_names)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _validate_schema_request(
|
|
569
|
+
source: str | None,
|
|
570
|
+
schema: str | None,
|
|
571
|
+
table: str | None,
|
|
572
|
+
column: str | None,
|
|
573
|
+
force_refresh: bool,
|
|
574
|
+
table_search: str | None,
|
|
575
|
+
) -> SchemaResponse | None:
|
|
576
|
+
if table_search is not None and (
|
|
577
|
+
source is None or schema is None or table is not None or column is not None
|
|
578
|
+
):
|
|
579
|
+
return SchemaResponse(
|
|
580
|
+
success=False,
|
|
581
|
+
errors=[
|
|
582
|
+
"table_search is only valid at the schema level "
|
|
583
|
+
"(source and schema provided, without table or column)"
|
|
584
|
+
],
|
|
585
|
+
)
|
|
586
|
+
if force_refresh and (
|
|
587
|
+
source is None or schema is None or table is None or column is not None
|
|
588
|
+
):
|
|
589
|
+
return SchemaResponse(
|
|
590
|
+
success=False,
|
|
591
|
+
errors=[
|
|
592
|
+
"force_refresh is only valid when querying a specific table "
|
|
593
|
+
"(source, schema, and table, without column)"
|
|
594
|
+
],
|
|
595
|
+
)
|
|
596
|
+
if column is not None and (table is None or source is None or schema is None):
|
|
597
|
+
return SchemaResponse(
|
|
598
|
+
success=False,
|
|
599
|
+
errors=["Must provide source, schema, and table when querying a column"],
|
|
600
|
+
)
|
|
601
|
+
if table is not None and (source is None or schema is None):
|
|
602
|
+
return SchemaResponse(
|
|
603
|
+
success=False,
|
|
604
|
+
errors=["Must provide source and schema when querying a table"],
|
|
605
|
+
)
|
|
606
|
+
if schema is not None and source is None:
|
|
607
|
+
return SchemaResponse(
|
|
608
|
+
success=False,
|
|
609
|
+
errors=["Must provide source when querying a schema"],
|
|
610
|
+
)
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _dispatch_schema_request(
|
|
615
|
+
resolver: LayeredSchemaResolver,
|
|
616
|
+
source: str,
|
|
617
|
+
schema: str | None,
|
|
618
|
+
table: str | None,
|
|
619
|
+
column: str | None,
|
|
620
|
+
force_refresh: bool,
|
|
621
|
+
lineage_depth: int,
|
|
622
|
+
) -> dict[str, Any]:
|
|
623
|
+
if column is not None:
|
|
624
|
+
# mypy: guaranteed by _validate_schema_request before dispatch.
|
|
625
|
+
assert schema is not None and table is not None
|
|
626
|
+
return resolver.profile_column(
|
|
627
|
+
source=source, schema=schema, table=table, column=column
|
|
628
|
+
)
|
|
629
|
+
if table is not None:
|
|
630
|
+
# mypy: guaranteed by _validate_schema_request before dispatch.
|
|
631
|
+
assert schema is not None
|
|
632
|
+
return resolver.profile_table(
|
|
633
|
+
source=source,
|
|
634
|
+
schema=schema,
|
|
635
|
+
table=table,
|
|
636
|
+
fresh=force_refresh,
|
|
637
|
+
lineage_depth=lineage_depth,
|
|
638
|
+
)
|
|
639
|
+
if schema is not None:
|
|
640
|
+
return resolver.list_tables(source=source, schema=schema)
|
|
641
|
+
return resolver.list_schemas(source=source)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _project_uses_direct_file_queries(project_root: Path) -> bool:
|
|
645
|
+
from dataface.agent_api.dashboards import list_dashboards
|
|
646
|
+
|
|
647
|
+
for dashboard in list_dashboards(directory=project_root, recursive=True).dashboards:
|
|
648
|
+
try:
|
|
649
|
+
content = dashboard.absolute_path.read_text()
|
|
650
|
+
except OSError:
|
|
651
|
+
continue
|
|
652
|
+
if (
|
|
653
|
+
"read_csv(" in content
|
|
654
|
+
or "read_parquet(" in content
|
|
655
|
+
or "read_json(" in content
|
|
656
|
+
):
|
|
657
|
+
return True
|
|
658
|
+
return False
|