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,283 @@
|
|
|
1
|
+
"""dbt adapter integration for SQL query execution.
|
|
2
|
+
|
|
3
|
+
Stage: EXECUTE (inside RENDER stage)
|
|
4
|
+
Purpose: Execute SQL queries via dbt's adapter system.
|
|
5
|
+
|
|
6
|
+
This adapter leverages dbt's adapter API to execute SQL queries,
|
|
7
|
+
supporting dbt-specific features like ref() and source() resolution.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from dataface.core.compile.models.source import SourceConfig
|
|
19
|
+
|
|
20
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
21
|
+
from dataface.core.compile.models.face.compiled import VariableValues
|
|
22
|
+
from dataface.core.compile.models.query.compiled import (
|
|
23
|
+
AnyQuery,
|
|
24
|
+
is_sql_query,
|
|
25
|
+
)
|
|
26
|
+
from dataface.core.execute.adapters.base import (
|
|
27
|
+
BaseAdapter,
|
|
28
|
+
QueryParams,
|
|
29
|
+
QueryResult,
|
|
30
|
+
handle_adapter_error,
|
|
31
|
+
)
|
|
32
|
+
from dataface.core.execute.dbt_jinja import has_dbt_jinja
|
|
33
|
+
from dataface.core.execute.sql_guard import validate_select_only
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_profiles_yml(dbt_project_path: Path) -> dict[str, Any]:
|
|
37
|
+
"""Read and return the profiles.yml nearest to the project.
|
|
38
|
+
|
|
39
|
+
Checks project-local profiles.yml first, then ~/.dbt/profiles.yml.
|
|
40
|
+
Raises FileNotFoundError if neither exists.
|
|
41
|
+
"""
|
|
42
|
+
for candidate in (
|
|
43
|
+
dbt_project_path / "profiles.yml",
|
|
44
|
+
Path.home() / ".dbt" / "profiles.yml",
|
|
45
|
+
):
|
|
46
|
+
if candidate.exists():
|
|
47
|
+
with open(candidate) as f:
|
|
48
|
+
data = yaml.safe_load(f)
|
|
49
|
+
if not isinstance(data, dict):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"profiles.yml at {candidate} did not parse to a mapping. "
|
|
52
|
+
f"Got: {type(data).__name__}"
|
|
53
|
+
)
|
|
54
|
+
return data
|
|
55
|
+
raise FileNotFoundError(f"No profiles.yml found in {dbt_project_path} or ~/.dbt/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_target_dict(
|
|
59
|
+
dbt_project_path: Path, profile_name: str, target_name: str
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Return the target dict for the given profile+target from profiles.yml.
|
|
62
|
+
|
|
63
|
+
Relative `path:` entries (DuckDB) are resolved against `dbt_project_path`
|
|
64
|
+
so the adapter opens the file dbt would have opened, regardless of CWD.
|
|
65
|
+
"""
|
|
66
|
+
profiles = _read_profiles_yml(dbt_project_path)
|
|
67
|
+
profile = profiles.get(profile_name)
|
|
68
|
+
if profile is None:
|
|
69
|
+
available = [k for k in profiles if k != "config"]
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Profile '{profile_name}' not found in profiles.yml "
|
|
72
|
+
f"(checked {dbt_project_path}). "
|
|
73
|
+
f"Available profiles: {available}"
|
|
74
|
+
)
|
|
75
|
+
outputs = profile.get("outputs", {})
|
|
76
|
+
target = outputs.get(target_name)
|
|
77
|
+
if target is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Target '{target_name}' not found in profile '{profile_name}'. "
|
|
80
|
+
f"Available targets: {list(outputs.keys())}"
|
|
81
|
+
)
|
|
82
|
+
target = dict(target)
|
|
83
|
+
raw_path = target.get("path")
|
|
84
|
+
if isinstance(raw_path, str) and raw_path and raw_path != ":memory:":
|
|
85
|
+
path_obj = Path(raw_path)
|
|
86
|
+
if not path_obj.is_absolute():
|
|
87
|
+
target["path"] = str((dbt_project_path / path_obj).resolve())
|
|
88
|
+
return target
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DbtAdapter(BaseAdapter):
|
|
92
|
+
"""Adapter for executing SQL queries via dbt's adapter system.
|
|
93
|
+
|
|
94
|
+
Supported query types: sql
|
|
95
|
+
|
|
96
|
+
Leverages dbt's adapter API to execute SQL queries with support for
|
|
97
|
+
dbt-specific features like ref(), source(), etc.
|
|
98
|
+
|
|
99
|
+
This adapter requires dbt-adapters and a warehouse-specific dbt package
|
|
100
|
+
(e.g. dbt-duckdb, dbt-postgres) — not dbt-core. A valid dbt project
|
|
101
|
+
with profiles.yml configuration is also required.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> adapter = DbtAdapter(dbt_project_path=Path("./my_dbt_project"))
|
|
105
|
+
>>> query = SqlQuery(sql="SELECT * FROM {{ ref('customers') }}")
|
|
106
|
+
>>> result = adapter.execute(query)
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
dbt_project_path: Path | None = None,
|
|
112
|
+
profile_name: str | None = None,
|
|
113
|
+
target_name: str | None = None,
|
|
114
|
+
):
|
|
115
|
+
"""Initialize dbt adapter.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
dbt_project_path: Path to dbt project (default: current directory)
|
|
119
|
+
profile_name: dbt profile name (default: from dbt_project.yml)
|
|
120
|
+
target_name: dbt target name (default: DBT_TARGET env var, then 'dev')
|
|
121
|
+
"""
|
|
122
|
+
import os
|
|
123
|
+
|
|
124
|
+
self.dbt_project_path = (
|
|
125
|
+
Path(dbt_project_path).resolve()
|
|
126
|
+
if dbt_project_path
|
|
127
|
+
else Path.cwd().resolve()
|
|
128
|
+
)
|
|
129
|
+
self.profile_name = profile_name
|
|
130
|
+
self.target_name = target_name or os.environ.get("DBT_TARGET") or "dev"
|
|
131
|
+
self._adapter: Any = None
|
|
132
|
+
# _dialect is populated by _get_dbt_adapter() — which always runs before
|
|
133
|
+
# any validate_select_only() call site — and `build_adapter` raises if
|
|
134
|
+
# the target dict lacks 'type', so the empty-string default is never
|
|
135
|
+
# observed by the guard in practice.
|
|
136
|
+
self._dialect: str = ""
|
|
137
|
+
self._manifest: dict[str, Any] | None = None
|
|
138
|
+
self._manifest_loaded: bool = False
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def supported_types(self) -> set[str]:
|
|
142
|
+
"""Return supported query types."""
|
|
143
|
+
return {"sql"}
|
|
144
|
+
|
|
145
|
+
def _can_execute(self, query: AnyQuery) -> bool:
|
|
146
|
+
"""Return True if this adapter is certain it can handle the query.
|
|
147
|
+
|
|
148
|
+
Pure predicate — never constructs the adapter, never reads profiles.yml.
|
|
149
|
+
Misconfiguration errors surface in _execute() as QueryResult.error,
|
|
150
|
+
not here in routing.
|
|
151
|
+
|
|
152
|
+
Routing contract:
|
|
153
|
+
- Explicit source (dict or named string) → SqlAdapter owns it
|
|
154
|
+
(it has the inline-source resolver and the named-source registry).
|
|
155
|
+
- No source AND SQL has `{{ ref() }}` / `{{ source() }}` jinja →
|
|
156
|
+
DbtAdapter (needs manifest resolution).
|
|
157
|
+
- No source, no jinja → SqlAdapter (default DuckDB).
|
|
158
|
+
"""
|
|
159
|
+
if not is_sql_query(query):
|
|
160
|
+
return False
|
|
161
|
+
if query.source is not None:
|
|
162
|
+
return False
|
|
163
|
+
return has_dbt_jinja(query.sql)
|
|
164
|
+
|
|
165
|
+
def _execute(
|
|
166
|
+
self,
|
|
167
|
+
query: AnyQuery,
|
|
168
|
+
variables: VariableValues | None = None,
|
|
169
|
+
params: QueryParams = None,
|
|
170
|
+
source_config: SourceConfig | None = None,
|
|
171
|
+
) -> QueryResult:
|
|
172
|
+
"""Execute a SQL query via dbt adapter.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
query: AnyQuery object (SqlQuery expected)
|
|
176
|
+
variables: Variable values for Jinja resolution
|
|
177
|
+
params: Accepted for interface compatibility; not used by dbt adapter.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
QueryResult with data or error
|
|
181
|
+
"""
|
|
182
|
+
if not is_sql_query(query):
|
|
183
|
+
return QueryResult(
|
|
184
|
+
data=[],
|
|
185
|
+
error=f"Expected SQL query, got {query.query_type}",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
if self._adapter is None:
|
|
190
|
+
self._get_dbt_adapter()
|
|
191
|
+
|
|
192
|
+
resolved_sql = self._resolve_dbt_sql(query.sql, variables)
|
|
193
|
+
validate_select_only(resolved_sql, dialect=self._dialect)
|
|
194
|
+
|
|
195
|
+
with self._adapter.connection_named("dataface_query"):
|
|
196
|
+
_, table = self._adapter.execute(
|
|
197
|
+
resolved_sql, auto_begin=True, fetch=True
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
columns = list(table.column_names)
|
|
201
|
+
rows = list(table.rows)
|
|
202
|
+
if query.limit and query.limit > 0:
|
|
203
|
+
rows = rows[: query.limit]
|
|
204
|
+
data = [dict(zip(columns, row, strict=False)) for row in rows]
|
|
205
|
+
return QueryResult(data=data, columns=columns)
|
|
206
|
+
|
|
207
|
+
except Exception as e: # noqa: BLE001
|
|
208
|
+
return handle_adapter_error("Query execution", e)
|
|
209
|
+
|
|
210
|
+
def _get_dbt_adapter(self) -> Any:
|
|
211
|
+
"""Get dbt adapter instance (lazy-loaded).
|
|
212
|
+
|
|
213
|
+
Always loads the manifest first (doesn't require dbt).
|
|
214
|
+
Then delegates adapter construction to build_adapter() in the factory —
|
|
215
|
+
single point of truth for source_config → dbt adapter.
|
|
216
|
+
"""
|
|
217
|
+
if self._adapter is not None:
|
|
218
|
+
return self._adapter
|
|
219
|
+
|
|
220
|
+
# Always try to load manifest first — this doesn't require dbt
|
|
221
|
+
self._load_manifest()
|
|
222
|
+
|
|
223
|
+
if not self.profile_name:
|
|
224
|
+
project_config_path = self.dbt_project_path / "dbt_project.yml"
|
|
225
|
+
if project_config_path.exists():
|
|
226
|
+
with open(project_config_path) as f:
|
|
227
|
+
project_dict = yaml.safe_load(f)
|
|
228
|
+
profile_from_project = project_dict.get("profile")
|
|
229
|
+
if not profile_from_project:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"dbt_project.yml at {project_config_path} has no 'profile:' key"
|
|
232
|
+
)
|
|
233
|
+
self.profile_name = profile_from_project
|
|
234
|
+
else:
|
|
235
|
+
raise FileNotFoundError(
|
|
236
|
+
f"No dbt_project.yml found at {project_config_path} and no "
|
|
237
|
+
f"profile_name was provided to DbtAdapter"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
target_dict = _read_target_dict(
|
|
241
|
+
self.dbt_project_path, self.profile_name, self.target_name
|
|
242
|
+
)
|
|
243
|
+
from dataface.core.execute.adapters.dbt_adapter_factory import build_adapter
|
|
244
|
+
|
|
245
|
+
# build_adapter raises ValueError if target_dict has no 'type'; reordering
|
|
246
|
+
# so the build happens first means `target_dict["type"]` is guaranteed
|
|
247
|
+
# populated when we read it.
|
|
248
|
+
self._adapter = build_adapter(target_dict)
|
|
249
|
+
self._dialect = target_dict["type"]
|
|
250
|
+
return self._adapter
|
|
251
|
+
|
|
252
|
+
def _load_manifest(self) -> None:
|
|
253
|
+
"""Load dbt manifest from target/ or snapshot file.
|
|
254
|
+
|
|
255
|
+
Tries target/manifest.json first (for development), then falls back
|
|
256
|
+
to manifest.snapshot.json (for production deployments).
|
|
257
|
+
"""
|
|
258
|
+
if self._manifest_loaded:
|
|
259
|
+
return # Already attempted (result may be None if no manifest exists)
|
|
260
|
+
|
|
261
|
+
from dataface.core.execute.adapters.dbt_utils import load_dbt_manifest
|
|
262
|
+
|
|
263
|
+
self._manifest = load_dbt_manifest(self.dbt_project_path)
|
|
264
|
+
self._manifest_loaded = True
|
|
265
|
+
|
|
266
|
+
def _resolve_dbt_sql(
|
|
267
|
+
self, sql: str, variables: dict[str, Any] | None = None
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Resolve SQL with dbt-specific Jinja functions."""
|
|
270
|
+
from dataface.core.execute.adapters.dbt_utils import resolve_dbt_refs
|
|
271
|
+
|
|
272
|
+
if has_dbt_jinja(sql) and not self._manifest:
|
|
273
|
+
kind = "ref()" if "ref(" in sql else "source()"
|
|
274
|
+
raise ValueError(
|
|
275
|
+
f"SQL contains {kind} but no dbt manifest found. "
|
|
276
|
+
f"Checked paths: {self.dbt_project_path / 'target' / 'manifest.json'}, "
|
|
277
|
+
f"{self.dbt_project_path / 'manifest.snapshot.json'}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if self._manifest:
|
|
281
|
+
sql = resolve_dbt_refs(sql, self._manifest)
|
|
282
|
+
|
|
283
|
+
return resolve_jinja_template(sql, variables)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Single point of truth for source_config → dbt adapter instantiation.
|
|
2
|
+
|
|
3
|
+
Every consumer that needs a live dbt adapter calls build_adapter().
|
|
4
|
+
The adapter lives as long as the caller holds a reference; GC reclaims
|
|
5
|
+
it automatically via weakref.finalize, which deletes the temp target dir.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import logging
|
|
10
|
+
import multiprocessing
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
import weakref
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
from dataface.core.compile.models.source import infer_bq_method
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Mapping: source_config type → (module_path, AdapterClass, CredentialsClass)
|
|
22
|
+
# Add entries here when new warehouse adapters are needed.
|
|
23
|
+
# This is the single source of truth — dbt_adapter.py imports from here.
|
|
24
|
+
_ADAPTER_TYPE_MAP: dict[str, tuple[str, str, str]] = {
|
|
25
|
+
"duckdb": ("dbt.adapters.duckdb", "DuckDBAdapter", "DuckDBCredentials"),
|
|
26
|
+
"postgres": ("dbt.adapters.postgres", "PostgresAdapter", "PostgresCredentials"),
|
|
27
|
+
"postgresql": ("dbt.adapters.postgres", "PostgresAdapter", "PostgresCredentials"),
|
|
28
|
+
"redshift": ("dbt.adapters.redshift", "RedshiftAdapter", "RedshiftCredentials"),
|
|
29
|
+
"snowflake": ("dbt.adapters.snowflake", "SnowflakeAdapter", "SnowflakeCredentials"),
|
|
30
|
+
"bigquery": ("dbt.adapters.bigquery", "BigQueryAdapter", "BigQueryCredentials"),
|
|
31
|
+
"spark": ("dbt.adapters.spark", "SparkAdapter", "SparkCredentials"),
|
|
32
|
+
"databricks": (
|
|
33
|
+
"dbt.adapters.databricks",
|
|
34
|
+
"DatabricksAdapter",
|
|
35
|
+
"DatabricksCredentials",
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_DUCKDB_INITIALIZE_DB_PATCHED = False
|
|
41
|
+
_ORIGINAL_DUCKDB_INITIALIZE_DB: Any = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _patched_duckdb_initialize_db(
|
|
45
|
+
cls: type[Any],
|
|
46
|
+
creds: Any,
|
|
47
|
+
plugins: dict[str, Any] | None = None,
|
|
48
|
+
) -> Any:
|
|
49
|
+
if getattr(creds, "_dft_read_only", False) and creds.path != ":memory:":
|
|
50
|
+
import duckdb
|
|
51
|
+
|
|
52
|
+
config = dict(creds.config_options or {})
|
|
53
|
+
config.setdefault("enable_external_access", False)
|
|
54
|
+
return duckdb.connect(creds.path, read_only=True, config=config)
|
|
55
|
+
return _ORIGINAL_DUCKDB_INITIALIZE_DB(cls, creds, plugins)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ensure_duckdb_readonly_initialize_db_patch() -> None:
|
|
59
|
+
"""Patch dbt-duckdb's hardcoded read_only=False for schema/introspection paths."""
|
|
60
|
+
global _DUCKDB_INITIALIZE_DB_PATCHED, _ORIGINAL_DUCKDB_INITIALIZE_DB
|
|
61
|
+
if _DUCKDB_INITIALIZE_DB_PATCHED:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
from dbt.adapters.duckdb import environments as duckdb_envs
|
|
65
|
+
|
|
66
|
+
_orig_cm = cast(Any, duckdb_envs.Environment.initialize_db)
|
|
67
|
+
_ORIGINAL_DUCKDB_INITIALIZE_DB = _orig_cm.__func__
|
|
68
|
+
duckdb_envs.Environment.initialize_db = classmethod( # type: ignore[method-assign,assignment]
|
|
69
|
+
_patched_duckdb_initialize_db
|
|
70
|
+
)
|
|
71
|
+
_DUCKDB_INITIALIZE_DB_PATCHED = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_adapter(source_config: dict[str, Any], *, read_only: bool = False) -> Any:
|
|
75
|
+
"""Construct a fresh dbt adapter instance.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
source_config: Dict with at minimum a 'type' key matching a known
|
|
79
|
+
warehouse adapter. Other keys are translated via the credentials
|
|
80
|
+
class's alias map (e.g. BigQuery project→database) and deserialized
|
|
81
|
+
through from_dict, so profile-level extras like threads are dropped.
|
|
82
|
+
read_only: When True, open DuckDB file sources read-only so pure-metadata
|
|
83
|
+
verbs coexist with other readers (e.g. a running ``dft serve``).
|
|
84
|
+
In-memory DuckDB stays read-write. Other warehouses ignore this
|
|
85
|
+
flag until a driver-specific read-only hook exists.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A dbt adapter instance. Callers use adapter.connection_named("name")
|
|
89
|
+
as a context manager before calling adapter.execute(...). The temp
|
|
90
|
+
target directory is deleted automatically when the adapter is GC'd.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: 'type' key is missing or names an unsupported adapter.
|
|
94
|
+
ImportError: The dbt adapter package for this warehouse is not installed.
|
|
95
|
+
"""
|
|
96
|
+
adapter_type = source_config.get("type")
|
|
97
|
+
if not adapter_type:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"source_config must have a 'type' field. "
|
|
100
|
+
f"Got: {list(source_config.keys())}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
adapter_type_lower = str(adapter_type).lower()
|
|
104
|
+
entry = _ADAPTER_TYPE_MAP.get(adapter_type_lower)
|
|
105
|
+
if entry is None:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Unsupported adapter type '{adapter_type}'. "
|
|
108
|
+
f"Supported: {', '.join(sorted(_ADAPTER_TYPE_MAP))}. "
|
|
109
|
+
f"For other warehouses, install the relevant dbt-<warehouse> package "
|
|
110
|
+
f"and add an entry to _ADAPTER_TYPE_MAP in dbt_adapter_factory.py."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
module_path, adapter_name, creds_name = entry
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
mod = importlib.import_module(module_path)
|
|
117
|
+
except ImportError as e:
|
|
118
|
+
raise ImportError(
|
|
119
|
+
f"dbt-{adapter_type_lower} is not installed. "
|
|
120
|
+
f"Install it with: pip install dbt-{adapter_type_lower}"
|
|
121
|
+
) from e
|
|
122
|
+
|
|
123
|
+
adapter_cls = getattr(mod, adapter_name)
|
|
124
|
+
creds_cls = getattr(mod, creds_name)
|
|
125
|
+
|
|
126
|
+
# Strip 'type', translate profile-style aliases (e.g. BigQuery project→database),
|
|
127
|
+
# then use from_dict so extras like threads are silently dropped.
|
|
128
|
+
creds_kwargs = {k: v for k, v in source_config.items() if k != "type"}
|
|
129
|
+
if adapter_type_lower == "bigquery" and "method" not in creds_kwargs:
|
|
130
|
+
creds_kwargs["method"] = infer_bq_method(
|
|
131
|
+
creds_kwargs.get("keyfile"), creds_kwargs.get("keyfile_json")
|
|
132
|
+
)
|
|
133
|
+
creds_kwargs = creds_cls.translate_aliases(creds_kwargs)
|
|
134
|
+
creds = creds_cls.from_dict(creds_kwargs)
|
|
135
|
+
if read_only and adapter_type_lower == "duckdb":
|
|
136
|
+
_ensure_duckdb_readonly_initialize_db_patch()
|
|
137
|
+
creds._dft_read_only = True
|
|
138
|
+
|
|
139
|
+
# Build a minimal duck-typed config satisfying AdapterRequiredConfig.
|
|
140
|
+
# No dbt_project.yml, no manifest — but enough for dbt's macro context to
|
|
141
|
+
# construct itself, since list_schemas / list_relations / get_columns route
|
|
142
|
+
# through execute_macro -> generate_runtime_macro_context, which reads
|
|
143
|
+
# quoting / dependencies / args / vars / load_dependencies / project_root /
|
|
144
|
+
# get_macro_search_order off the config.
|
|
145
|
+
profile_name = f"dft_{adapter_type_lower}"
|
|
146
|
+
target_path = tempfile.mkdtemp(prefix="dft-target-")
|
|
147
|
+
cfg = SimpleNamespace(
|
|
148
|
+
credentials=creds,
|
|
149
|
+
profile_name=profile_name,
|
|
150
|
+
target_name="dev",
|
|
151
|
+
threads=1,
|
|
152
|
+
project_name=profile_name,
|
|
153
|
+
query_comment=SimpleNamespace(comment="", append=False, job_label=False),
|
|
154
|
+
cli_vars={},
|
|
155
|
+
target_path=target_path,
|
|
156
|
+
project_root=target_path,
|
|
157
|
+
log_cache_events=False,
|
|
158
|
+
quoting={"database": True, "schema": True, "identifier": True},
|
|
159
|
+
dependencies={},
|
|
160
|
+
args=SimpleNamespace(vars={}),
|
|
161
|
+
vars=_NoVars(),
|
|
162
|
+
flags={},
|
|
163
|
+
load_dependencies=dict,
|
|
164
|
+
get_macro_search_order=lambda namespace: None,
|
|
165
|
+
to_target_dict=lambda c=source_config: dict(c),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
adapter = adapter_cls(cfg, multiprocessing.get_context("spawn"))
|
|
169
|
+
# Register cleanup to fire when the adapter is GC'd.
|
|
170
|
+
# shutil.rmtree is called with ignore_errors=True so a missing dir is skipped.
|
|
171
|
+
weakref.finalize(adapter, shutil.rmtree, target_path, True)
|
|
172
|
+
_bootstrap_macros(adapter, adapter_type_lower)
|
|
173
|
+
return adapter
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _bootstrap_macros(adapter: Any, adapter_type: str) -> None:
|
|
177
|
+
"""Wire dbt-bundled macros onto the adapter so introspection works.
|
|
178
|
+
|
|
179
|
+
Without this, every BaseAdapter introspection method (list_schemas,
|
|
180
|
+
list_relations, get_columns_in_relation, calculate_freshness_from_metadata,
|
|
181
|
+
get_partitions_metadata) raises "Macro resolver was None" because
|
|
182
|
+
execute_macro() needs a populated _macro_resolver.
|
|
183
|
+
|
|
184
|
+
Also registers a weakref.proxy of the adapter in dbt's FACTORY so the
|
|
185
|
+
macro context can resolve `adapter` via get_adapter(config) — the macro
|
|
186
|
+
context is constructed in dbt-core's MacroContext.__init__, which calls
|
|
187
|
+
get_adapter(self.config). A weakref keeps GC working: the FACTORY entry
|
|
188
|
+
doesn't pin the adapter, so weakref.finalize cleanup still fires when the
|
|
189
|
+
caller drops their reference. Last build_adapter() call wins per type.
|
|
190
|
+
"""
|
|
191
|
+
from dbt.adapters.factory import FACTORY
|
|
192
|
+
from dbt.context.providers import generate_runtime_macro_context
|
|
193
|
+
|
|
194
|
+
from dataface.core.execute.adapters.dbt_macro_loader import load_macro_manifest
|
|
195
|
+
|
|
196
|
+
with FACTORY.lock:
|
|
197
|
+
# weakref.proxy: FACTORY.adapters[type] doesn't pin the adapter, so
|
|
198
|
+
# weakref.finalize cleanup still fires when the caller drops their ref.
|
|
199
|
+
FACTORY.adapters[adapter_type] = weakref.proxy(adapter) # type: ignore[assignment]
|
|
200
|
+
adapter.set_macro_resolver(load_macro_manifest(adapter_type))
|
|
201
|
+
adapter.set_macro_context_generator(generate_runtime_macro_context)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _NoVars:
|
|
205
|
+
"""Stub for `RuntimeConfig.vars` accessed during macro context build.
|
|
206
|
+
|
|
207
|
+
Macros we care about (list_schemas, list_relations, get_columns_in_relation)
|
|
208
|
+
don't reference any project vars, so an empty `vars_for` is the right shape.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def vars_for(self, lookup: Any, adapter_type: str) -> dict:
|
|
212
|
+
return {}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Bootstrap a dbt MacroManifest from pip-installed dbt packages.
|
|
2
|
+
|
|
3
|
+
dbt-core's BaseAdapter introspection API (`list_schemas`, `list_relations`,
|
|
4
|
+
`get_columns_in_relation`, ...) routes through `execute_macro()`, which needs
|
|
5
|
+
a populated `_macro_resolver`. Without it, every adapter built by
|
|
6
|
+
`build_adapter()` raises "no macro registered" the moment we try to introspect.
|
|
7
|
+
|
|
8
|
+
This module loads the bundled macros — dbt-core's global project plus the
|
|
9
|
+
active warehouse adapter's package — into a fresh `MacroManifest` that can be
|
|
10
|
+
attached to an adapter via `set_macro_resolver()`. No user files required,
|
|
11
|
+
no `dbt_project.yml`, no `target/manifest.json`. Just pip-installed packages.
|
|
12
|
+
|
|
13
|
+
Coupled to dbt-internal API (`MacroParser`, `MacroManifest`,
|
|
14
|
+
`load_source_file`, `FACTORY.packages`); the smoke tests are the canary for
|
|
15
|
+
upgrades.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from functools import cache
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from types import SimpleNamespace
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from dbt.contracts.graph.manifest import MacroManifest
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DbtMacroLoaderError(RuntimeError):
|
|
33
|
+
"""Raised when bootstrapping the dbt MacroManifest fails."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_macro_packages(adapter_type: str) -> list[tuple[str, Path]]:
|
|
37
|
+
"""Return [(package_name, package_root), ...] for warehouse + global macros.
|
|
38
|
+
|
|
39
|
+
Uses dbt's adapter FACTORY to resolve include paths the same way dbt-core
|
|
40
|
+
does — by loading the plugin and reading the registered include directory.
|
|
41
|
+
Warehouse package comes first so its macros parse before the global ones,
|
|
42
|
+
matching dbt-core's lookup precedence (warehouse-specific overrides win).
|
|
43
|
+
"""
|
|
44
|
+
from dbt.adapters.factory import FACTORY
|
|
45
|
+
|
|
46
|
+
FACTORY.load_plugin(adapter_type)
|
|
47
|
+
plugin = FACTORY.get_plugin_by_name(adapter_type)
|
|
48
|
+
return [
|
|
49
|
+
(plugin.project_name, FACTORY.packages[plugin.project_name]),
|
|
50
|
+
("dbt", FACTORY.packages["dbt"]),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cache
|
|
55
|
+
def load_macro_manifest(adapter_type: str) -> MacroManifest:
|
|
56
|
+
"""Build a MacroManifest containing dbt-core + warehouse macros.
|
|
57
|
+
|
|
58
|
+
Cached per adapter_type for the process lifetime — the bundled macros
|
|
59
|
+
don't change between calls within a single session.
|
|
60
|
+
"""
|
|
61
|
+
from dbt.contracts.files import ParseFileType
|
|
62
|
+
from dbt.contracts.graph.manifest import MacroManifest, Manifest
|
|
63
|
+
from dbt.parser.macros import MacroParser
|
|
64
|
+
from dbt.parser.read_files import load_source_file
|
|
65
|
+
from dbt.parser.search import FileBlock
|
|
66
|
+
|
|
67
|
+
packages = _resolve_macro_packages(adapter_type)
|
|
68
|
+
manifest = Manifest()
|
|
69
|
+
for package_name, package_root in packages:
|
|
70
|
+
macros_dir = Path(package_root) / "macros"
|
|
71
|
+
if not macros_dir.is_dir():
|
|
72
|
+
raise DbtMacroLoaderError(
|
|
73
|
+
f"dbt {package_name} macros not found at {macros_dir}. "
|
|
74
|
+
f"This usually means the dbt-core or dbt-{adapter_type} "
|
|
75
|
+
f"package layout changed; run the smoke test in "
|
|
76
|
+
f"test_dbt_macro_loader.py to see what dbt-core version "
|
|
77
|
+
f"this code was last validated against."
|
|
78
|
+
)
|
|
79
|
+
# MacroParser only reads project_root / project_name / macro_paths off
|
|
80
|
+
# the project arg, so a SimpleNamespace duck-type is sufficient.
|
|
81
|
+
fake_project = SimpleNamespace(
|
|
82
|
+
project_root=str(package_root),
|
|
83
|
+
project_name=package_name,
|
|
84
|
+
macro_paths=["macros"],
|
|
85
|
+
)
|
|
86
|
+
parser = MacroParser(fake_project, manifest) # type: ignore[arg-type]
|
|
87
|
+
for path in parser.get_paths():
|
|
88
|
+
source_file = load_source_file(path, ParseFileType.Macro, package_name, {})
|
|
89
|
+
assert source_file is not None
|
|
90
|
+
parser.parse_file(FileBlock(source_file))
|
|
91
|
+
|
|
92
|
+
logger.debug(
|
|
93
|
+
"Loaded %d dbt macros for adapter_type=%s", len(manifest.macros), adapter_type
|
|
94
|
+
)
|
|
95
|
+
return MacroManifest(manifest.macros)
|