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,387 @@
|
|
|
1
|
+
"""SQL statement-type guard for dft-core.
|
|
2
|
+
|
|
3
|
+
Two public validators:
|
|
4
|
+
validate_select_only(sql, dialect=None) — read-only queries only
|
|
5
|
+
validate_setup_sql(sql, dialect=None) — TEMP CREATE + DuckDB MACRO only
|
|
6
|
+
|
|
7
|
+
Both raise MutatingSqlError on policy violation, UnparseableSqlError when
|
|
8
|
+
the SQL skeleton cannot be statically determined (Jinja shapes we don't model,
|
|
9
|
+
or sqlglot parse failure). Callers decide what to do with each error type.
|
|
10
|
+
|
|
11
|
+
The approach:
|
|
12
|
+
1. Build a SQL skeleton from the Jinja AST: TemplateData verbatim, {{ expr }}
|
|
13
|
+
as placeholder identifiers, {% if %}/{% for %}/{% set %} walked structurally
|
|
14
|
+
so both branches of if are included (catching malicious-branch injection).
|
|
15
|
+
Unknown node types raise UnparseableSqlError (default-deny).
|
|
16
|
+
2. Strip a leading EXPLAIN / EXPLAIN ANALYZE keyword — sqlglot opaquely wraps
|
|
17
|
+
`EXPLAIN <anything>` as a Command node, so the inner statement is invisible
|
|
18
|
+
to AST allowlist checks. Strip-and-revalidate forces the inner body through
|
|
19
|
+
the same gate, so `EXPLAIN DROP TABLE x` fails like the bare DROP would.
|
|
20
|
+
3. sqlglot.parse(skeleton, read=dialect) to get top-level statements.
|
|
21
|
+
4. Walk statements against the function's allowlist, then scan descendants for
|
|
22
|
+
mutating-statement types so CTE-laundered DML (`WITH x AS (DELETE …) SELECT
|
|
23
|
+
* FROM x`) is rejected.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
|
|
30
|
+
import jinja2
|
|
31
|
+
import jinja2.nodes
|
|
32
|
+
import sqlglot
|
|
33
|
+
import sqlglot.errors
|
|
34
|
+
import sqlglot.expressions as exp
|
|
35
|
+
|
|
36
|
+
from dataface.core.execute.errors import MutatingSqlError, UnparseableSqlError
|
|
37
|
+
|
|
38
|
+
# Dataface dialect names → sqlglot dialect names. sqlglot calls SQL Server
|
|
39
|
+
# "tsql"; we present it as "sqlserver"/"mssql". "mariadb" is an alias for
|
|
40
|
+
# "mysql" in both. This is the canonical mapping for the guard; the wider
|
|
41
|
+
# dataface.core.execute.dialects registry does not currently expose one.
|
|
42
|
+
_DIALECT_MAP: dict[str, str] = {
|
|
43
|
+
"sqlserver": "tsql",
|
|
44
|
+
"mssql": "tsql",
|
|
45
|
+
"mariadb": "mysql",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sqlglot_dialect(dialect: str | None) -> str | None:
|
|
50
|
+
"""Translate a Dataface dialect name to the sqlglot equivalent.
|
|
51
|
+
|
|
52
|
+
sqlglot knows 'tsql' (not 'sqlserver'/'mssql') and 'mysql' (not 'mariadb').
|
|
53
|
+
Returns None unchanged (sqlglot default dialect).
|
|
54
|
+
"""
|
|
55
|
+
if dialect is None:
|
|
56
|
+
return None
|
|
57
|
+
return _DIALECT_MAP.get(dialect, dialect)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Top-level statements allowed by validate_select_only. exp.Select is further
|
|
61
|
+
# narrowed by an INTO-anywhere descendant scan below.
|
|
62
|
+
_ALLOWED_QUERY_NODES: frozenset[type[exp.Expression]] = frozenset(
|
|
63
|
+
{
|
|
64
|
+
exp.Select,
|
|
65
|
+
exp.Subquery,
|
|
66
|
+
exp.Union,
|
|
67
|
+
exp.Intersect,
|
|
68
|
+
exp.Except,
|
|
69
|
+
exp.Describe,
|
|
70
|
+
exp.Show,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Mutating statement types that must not appear anywhere in a SELECT-family
|
|
75
|
+
# tree — including inside CTEs, subqueries, and UNION arms. The CTE-laundered
|
|
76
|
+
# bypass shape `WITH x AS (DELETE … RETURNING *) SELECT * FROM x` parses as a
|
|
77
|
+
# top-level Select with the DML buried as a descendant; descendant-scan closes
|
|
78
|
+
# the gap. Top-level type-allowlist would otherwise admit the outer Select.
|
|
79
|
+
_DISALLOWED_DESCENDANT_NODES: tuple[type[exp.Expression], ...] = (
|
|
80
|
+
exp.Insert,
|
|
81
|
+
exp.Update,
|
|
82
|
+
exp.Delete,
|
|
83
|
+
exp.Merge,
|
|
84
|
+
exp.Drop,
|
|
85
|
+
exp.Alter,
|
|
86
|
+
exp.TruncateTable,
|
|
87
|
+
exp.Create,
|
|
88
|
+
exp.Pragma,
|
|
89
|
+
exp.Grant,
|
|
90
|
+
exp.Attach,
|
|
91
|
+
exp.Detach,
|
|
92
|
+
# sqlglot has no exp.Revoke as of 26.x; REVOKE parses as a Command at the
|
|
93
|
+
# top level and is caught there. If a future sqlglot adds Revoke as a
|
|
94
|
+
# dedicated node, add it here so a buried REVOKE inside a CTE is caught.
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_ALLOWED_SETUP_NODES: frozenset[type[exp.Expression]] = frozenset({exp.Create})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# sqlglot models `EXPLAIN <stmt>` as an opaque Command — the inner statement
|
|
101
|
+
# is invisible to the AST. Strip the EXPLAIN keyword (and common modifiers)
|
|
102
|
+
# from the skeleton so the body re-parses through the normal allowlist and
|
|
103
|
+
# `EXPLAIN DROP TABLE x` fails like the bare DROP would. Only the leading
|
|
104
|
+
# EXPLAIN is stripped — a mid-stream `…; EXPLAIN …` parses as Command and
|
|
105
|
+
# is rejected by the allowlist (acceptable fail-closed for an uncommon shape).
|
|
106
|
+
_EXPLAIN_PREFIX_RE = re.compile(
|
|
107
|
+
r"^\s*EXPLAIN"
|
|
108
|
+
r"(?:\s+(?:ANALYZE|VERBOSE|QUERY\s+PLAN))*"
|
|
109
|
+
r"(?:\s*\([^)]*\))?"
|
|
110
|
+
r"\s+",
|
|
111
|
+
re.IGNORECASE,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _strip_explain_prefix(skeleton: str) -> str:
|
|
116
|
+
return _EXPLAIN_PREFIX_RE.sub("", skeleton, count=1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Databricks Delta Lake adds DETAIL / HISTORY variants to DESCRIBE that
|
|
120
|
+
# sqlglot does not model — `DESCRIBE DETAIL my_table` fails to parse. Both
|
|
121
|
+
# variants are read-only metadata reads (mirrors of Delta-log inspection).
|
|
122
|
+
# Strip the variant keyword so the body re-parses as a standard exp.Describe.
|
|
123
|
+
_DESCRIBE_VARIANT_PREFIX_RE = re.compile(
|
|
124
|
+
r"^\s*DESCRIBE\s+(?:DETAIL|HISTORY)\s+",
|
|
125
|
+
re.IGNORECASE,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _strip_describe_variant_prefix(skeleton: str) -> str:
|
|
130
|
+
return _DESCRIBE_VARIANT_PREFIX_RE.sub("DESCRIBE ", skeleton, count=1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Read-only commands sqlglot falls back to `Command` for on dialects that
|
|
134
|
+
# lack a dedicated node (postgres / bigquery / redshift / databricks / default
|
|
135
|
+
# all parse `SHOW TABLES` as `Command(name="SHOW")`). Terminal commands —
|
|
136
|
+
# no inner statement to revalidate.
|
|
137
|
+
_ALLOWED_TOP_LEVEL_COMMAND_KEYWORDS: frozenset[str] = frozenset({"SHOW"})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _is_allowed_top_level_command(stmt: exp.Expression) -> bool:
|
|
141
|
+
return (
|
|
142
|
+
isinstance(stmt, exp.Command)
|
|
143
|
+
and (stmt.name or "").upper() in _ALLOWED_TOP_LEVEL_COMMAND_KEYWORDS
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _walk_jinja(node: jinja2.nodes.Node, out: list[str], counter: list[int]) -> None:
|
|
148
|
+
"""Walk a jinja2 AST, appending SQL fragments to *out*.
|
|
149
|
+
|
|
150
|
+
counter is a one-element list used as a mutable int for placeholder naming.
|
|
151
|
+
Raises UnparseableSqlError for any node type outside the explicit table —
|
|
152
|
+
default-deny so AssignBlock / Macro / Include / extension nodes never
|
|
153
|
+
silently swallow embedded SQL.
|
|
154
|
+
"""
|
|
155
|
+
if isinstance(node, jinja2.nodes.Template):
|
|
156
|
+
for child in node.body:
|
|
157
|
+
_walk_jinja(child, out, counter)
|
|
158
|
+
|
|
159
|
+
elif isinstance(node, jinja2.nodes.TemplateData):
|
|
160
|
+
out.append(node.data)
|
|
161
|
+
|
|
162
|
+
elif isinstance(node, jinja2.nodes.Output):
|
|
163
|
+
# Output interleaves TemplateData (literal SQL) and expression nodes
|
|
164
|
+
# ({{ ref('orders') }}, {{ var }}, …). Literals pass through; each
|
|
165
|
+
# expression becomes a placeholder identifier.
|
|
166
|
+
for child in node.nodes:
|
|
167
|
+
if isinstance(child, jinja2.nodes.TemplateData):
|
|
168
|
+
out.append(child.data)
|
|
169
|
+
else:
|
|
170
|
+
out.append(f"__dft_j{counter[0]}__")
|
|
171
|
+
counter[0] += 1
|
|
172
|
+
|
|
173
|
+
elif isinstance(node, jinja2.nodes.If):
|
|
174
|
+
# jinja2 represents elif_ as a flat list of If nodes (each with empty
|
|
175
|
+
# elif_), not a linked-list chain. Iterate every element so DROPs
|
|
176
|
+
# buried in the 2nd / 3rd elif slot are walked, not just the first.
|
|
177
|
+
for child in node.body:
|
|
178
|
+
_walk_jinja(child, out, counter)
|
|
179
|
+
for elif_node in node.elif_:
|
|
180
|
+
out.append(" ; ")
|
|
181
|
+
for child in elif_node.body:
|
|
182
|
+
_walk_jinja(child, out, counter)
|
|
183
|
+
if node.else_:
|
|
184
|
+
out.append(" ; ")
|
|
185
|
+
for child in node.else_:
|
|
186
|
+
_walk_jinja(child, out, counter)
|
|
187
|
+
|
|
188
|
+
elif isinstance(node, jinja2.nodes.For):
|
|
189
|
+
# Walk body once and the `else_` clause if present — jinja2 runs the
|
|
190
|
+
# else clause when the iterable is empty. Same branch-coverage posture
|
|
191
|
+
# as If: SQL hiding in the else branch must reach the skeleton.
|
|
192
|
+
for child in node.body:
|
|
193
|
+
_walk_jinja(child, out, counter)
|
|
194
|
+
if node.else_:
|
|
195
|
+
out.append(" ; ")
|
|
196
|
+
for child in node.else_:
|
|
197
|
+
_walk_jinja(child, out, counter)
|
|
198
|
+
|
|
199
|
+
elif isinstance(node, jinja2.nodes.Assign):
|
|
200
|
+
# {% set x = ... %} — emits nothing.
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
# AssignBlock ({% set sql %}...{% endset %}), Macro, Include, Import,
|
|
205
|
+
# FromImport, Extends, Block, FilterBlock, CallBlock, future jinja2
|
|
206
|
+
# additions — all default-deny. If AssignBlock fell through, the body's
|
|
207
|
+
# SQL would never reach the parser.
|
|
208
|
+
raise UnparseableSqlError(f"unsupported_jinja_node:{type(node).__name__}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _build_skeleton(template_str: str) -> str:
|
|
212
|
+
"""Return a SQL skeleton by walking the jinja2 AST.
|
|
213
|
+
|
|
214
|
+
TemplateData becomes literal SQL; `{{ expr }}` becomes a placeholder
|
|
215
|
+
identifier; `{% if %}` recurses all branches. Unknown jinja nodes raise
|
|
216
|
+
UnparseableSqlError so no SQL can hide inside them.
|
|
217
|
+
"""
|
|
218
|
+
if "{{" not in template_str and "{%" not in template_str:
|
|
219
|
+
return template_str
|
|
220
|
+
|
|
221
|
+
env = jinja2.Environment()
|
|
222
|
+
try:
|
|
223
|
+
ast = env.parse(template_str)
|
|
224
|
+
except jinja2.TemplateSyntaxError as exc:
|
|
225
|
+
raise UnparseableSqlError(exc) from exc
|
|
226
|
+
|
|
227
|
+
out: list[str] = []
|
|
228
|
+
counter = [0]
|
|
229
|
+
_walk_jinja(ast, out, counter)
|
|
230
|
+
return "".join(out)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse(skeleton: str, dialect: str | None) -> list[exp.Expression | None]:
|
|
234
|
+
try:
|
|
235
|
+
return sqlglot.parse(skeleton, read=sqlglot_dialect(dialect))
|
|
236
|
+
except sqlglot.errors.ParseError as exc:
|
|
237
|
+
raise UnparseableSqlError(exc) from exc
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _reject_mutating_descendants(
|
|
241
|
+
stmt: exp.Expression, allowlist_label: str = "query"
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Scan stmt for mutating-statement descendants and raise on first hit.
|
|
244
|
+
|
|
245
|
+
Catches CTE-laundered DML (`WITH x AS (DELETE …) SELECT *`), DML inside
|
|
246
|
+
subqueries, DML inside UNION arms, and `SELECT … INTO` at any depth —
|
|
247
|
+
all of which would otherwise pass a top-level type-only check. Used by
|
|
248
|
+
both validate_select_only and validate_setup_sql; the latter passes
|
|
249
|
+
allowlist_label="setup" so the error message names the right policy.
|
|
250
|
+
|
|
251
|
+
sqlglot's find_all includes the calling node, so the outer Create on the
|
|
252
|
+
setup path would re-match itself — skip self explicitly. Nested CREATEs
|
|
253
|
+
inside `CREATE TEMP TABLE t AS (CREATE …)` still get rejected.
|
|
254
|
+
"""
|
|
255
|
+
for desc in stmt.find_all(*_DISALLOWED_DESCENDANT_NODES):
|
|
256
|
+
if desc is stmt:
|
|
257
|
+
continue
|
|
258
|
+
raise MutatingSqlError(
|
|
259
|
+
rejected_node_kind=type(desc).__name__,
|
|
260
|
+
fragment_preview=desc.sql()[:60],
|
|
261
|
+
allowlist_label=allowlist_label,
|
|
262
|
+
)
|
|
263
|
+
for select in stmt.find_all(exp.Select):
|
|
264
|
+
if select.args.get("into") is not None:
|
|
265
|
+
raise MutatingSqlError(
|
|
266
|
+
rejected_node_kind="Select(into)",
|
|
267
|
+
fragment_preview=stmt.sql()[:60],
|
|
268
|
+
allowlist_label=allowlist_label,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _is_bq_temp_function_create(stmt: exp.Expression) -> bool:
|
|
273
|
+
"""Return True iff stmt is a BigQuery-style CREATE TEMP FUNCTION.
|
|
274
|
+
|
|
275
|
+
Used by validate_select_only to permit the BigQuery scripting pattern
|
|
276
|
+
`CREATE TEMP FUNCTION ... ; SELECT ...`. Only FUNCTION kind is accepted;
|
|
277
|
+
CREATE TEMP TABLE and CREATE TEMP VIEW belong in validate_setup_sql, not
|
|
278
|
+
inline query scripts.
|
|
279
|
+
"""
|
|
280
|
+
if not isinstance(stmt, exp.Create):
|
|
281
|
+
return False
|
|
282
|
+
kind = (stmt.args.get("kind") or "").upper()
|
|
283
|
+
if kind != "FUNCTION":
|
|
284
|
+
return False
|
|
285
|
+
props = stmt.args.get("properties")
|
|
286
|
+
return props is not None and any(
|
|
287
|
+
isinstance(p, exp.TemporaryProperty) for p in props.expressions
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def validate_select_only(sql: str, dialect: str | None = None) -> None:
|
|
292
|
+
"""Raise MutatingSqlError on non-read-only SQL; UnparseableSqlError when undetermined.
|
|
293
|
+
|
|
294
|
+
Accepts: SELECT (no INTO), WITH, UNION, INTERSECT, EXCEPT, DESCRIBE, SHOW,
|
|
295
|
+
EXPLAIN of an allowed inner statement.
|
|
296
|
+
On BigQuery only: also accepts a script of one or more CREATE TEMP FUNCTION
|
|
297
|
+
statements followed by a final SELECT-family statement (inline scalar UDF
|
|
298
|
+
pattern from Looker-migrated dashboards).
|
|
299
|
+
Rejects: DROP, DELETE, INSERT, UPDATE, ALTER, GRANT, REVOKE, TRUNCATE,
|
|
300
|
+
ATTACH, LOAD, CALL, PRAGMA, CREATE, and any other mutating
|
|
301
|
+
statement — including DML hidden inside a CTE or subquery.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
sql: Raw SQL string, possibly containing Jinja expressions.
|
|
305
|
+
dialect: sqlglot dialect name (e.g. "duckdb", "postgres"). None = default.
|
|
306
|
+
"""
|
|
307
|
+
skeleton = _strip_describe_variant_prefix(
|
|
308
|
+
_strip_explain_prefix(_build_skeleton(sql))
|
|
309
|
+
)
|
|
310
|
+
statements = [
|
|
311
|
+
s
|
|
312
|
+
for s in _parse(skeleton, dialect)
|
|
313
|
+
if s is not None and not isinstance(s, exp.Semicolon)
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
# BigQuery inline-UDF scripting pattern: one or more CREATE TEMP FUNCTION
|
|
317
|
+
# statements followed by a SELECT-family statement. This is a read-only
|
|
318
|
+
# pattern — the TEMP FUNCTIONs are ephemeral UDFs, not persistent DDL.
|
|
319
|
+
# Only allowed on the bigquery dialect; other dialects keep the full ban.
|
|
320
|
+
if dialect == "bigquery" and statements:
|
|
321
|
+
leading_temp_fns = [
|
|
322
|
+
s for s in statements[:-1] if _is_bq_temp_function_create(s)
|
|
323
|
+
]
|
|
324
|
+
if leading_temp_fns and len(leading_temp_fns) == len(statements) - 1:
|
|
325
|
+
final = statements[-1]
|
|
326
|
+
# Validate the TEMP FUNCTION creates for buried DML.
|
|
327
|
+
for fn_stmt in leading_temp_fns:
|
|
328
|
+
_reject_mutating_descendants(fn_stmt)
|
|
329
|
+
# Validate the trailing statement as a normal query.
|
|
330
|
+
if type(final) in _ALLOWED_QUERY_NODES:
|
|
331
|
+
_reject_mutating_descendants(final)
|
|
332
|
+
return
|
|
333
|
+
if _is_allowed_top_level_command(final):
|
|
334
|
+
return
|
|
335
|
+
# Trailing statement is not a SELECT-family — fall through to
|
|
336
|
+
# per-statement validation which will reject it.
|
|
337
|
+
|
|
338
|
+
for stmt in statements:
|
|
339
|
+
if type(stmt) in _ALLOWED_QUERY_NODES:
|
|
340
|
+
_reject_mutating_descendants(stmt)
|
|
341
|
+
continue
|
|
342
|
+
if _is_allowed_top_level_command(stmt):
|
|
343
|
+
continue
|
|
344
|
+
raise MutatingSqlError(
|
|
345
|
+
rejected_node_kind=type(stmt).__name__,
|
|
346
|
+
fragment_preview=stmt.sql()[:60],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def validate_setup_sql(sql: str, dialect: str | None = None) -> None:
|
|
351
|
+
"""Raise MutatingSqlError on non-setup SQL; UnparseableSqlError when undetermined.
|
|
352
|
+
|
|
353
|
+
Accepts:
|
|
354
|
+
- CREATE TEMP FUNCTION/TABLE/VIEW (any dialect)
|
|
355
|
+
- CREATE [OR REPLACE] MACRO (DuckDB; no TEMP form exists)
|
|
356
|
+
|
|
357
|
+
Rejects everything else, including non-TEMP CREATE, DROP, INSERT, etc.
|
|
358
|
+
"""
|
|
359
|
+
skeleton = _build_skeleton(sql)
|
|
360
|
+
for stmt in _parse(skeleton, dialect):
|
|
361
|
+
if stmt is None or isinstance(stmt, exp.Semicolon):
|
|
362
|
+
continue
|
|
363
|
+
if type(stmt) not in _ALLOWED_SETUP_NODES:
|
|
364
|
+
raise MutatingSqlError(
|
|
365
|
+
rejected_node_kind=type(stmt).__name__,
|
|
366
|
+
fragment_preview=stmt.sql()[:60],
|
|
367
|
+
allowlist_label="setup",
|
|
368
|
+
)
|
|
369
|
+
kind = (stmt.args.get("kind") or "").upper()
|
|
370
|
+
# sqlglot represents TEMP via TemporaryProperty in the properties list,
|
|
371
|
+
# not as args["temporary"] = True (that field is always None in practice).
|
|
372
|
+
props = stmt.args.get("properties")
|
|
373
|
+
is_temp = props is not None and any(
|
|
374
|
+
isinstance(p, exp.TemporaryProperty) for p in props.expressions
|
|
375
|
+
)
|
|
376
|
+
is_macro = kind == "MACRO"
|
|
377
|
+
is_allowed_temp_kind = kind in {"FUNCTION", "TABLE", "VIEW"}
|
|
378
|
+
|
|
379
|
+
if not (is_macro or (is_temp and is_allowed_temp_kind)):
|
|
380
|
+
raise MutatingSqlError(
|
|
381
|
+
rejected_node_kind=f"Create(kind={kind}, temporary={is_temp})",
|
|
382
|
+
fragment_preview=stmt.sql()[:60],
|
|
383
|
+
allowlist_label="setup",
|
|
384
|
+
)
|
|
385
|
+
# CTAS bodies can smuggle DML through CTEs or SELECT … INTO. Scan
|
|
386
|
+
# descendants the same way validate_select_only does.
|
|
387
|
+
_reject_mutating_descendants(stmt, allowlist_label="setup")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""SQL literal inlining for dbt-adapter execution paths.
|
|
2
|
+
|
|
3
|
+
dbt adapter.execute() accepts plain SQL only — no parameterized queries.
|
|
4
|
+
This module provides functions for inlining positional $N params or named
|
|
5
|
+
dialect-specific params as properly escaped SQL literals. Used by SqlAdapter
|
|
6
|
+
and InspectConnection.
|
|
7
|
+
|
|
8
|
+
Values passed here must come from validated sources (compiled dashboard YAML
|
|
9
|
+
variables, or schema/table names from the inspector). Do not pass raw HTTP
|
|
10
|
+
input through this function.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from datetime import date, datetime, time
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_DOLLAR_RE = re.compile(r"\$(\d+)")
|
|
21
|
+
_PERCENT_S_RE = re.compile(r"%s")
|
|
22
|
+
_QMARK_RE = re.compile(r"\?")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _to_sql_literal(value: Any) -> str:
|
|
26
|
+
"""Convert a Python value to a SQL literal string."""
|
|
27
|
+
if value is None:
|
|
28
|
+
return "NULL"
|
|
29
|
+
if isinstance(value, bool):
|
|
30
|
+
return "TRUE" if value else "FALSE"
|
|
31
|
+
if isinstance(value, int):
|
|
32
|
+
return str(value)
|
|
33
|
+
if isinstance(value, float):
|
|
34
|
+
if math.isnan(value):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"NaN cannot be inlined as a SQL literal — different warehouses use "
|
|
37
|
+
"different syntax (FLOAT64 / DOUBLE / DOUBLE PRECISION / FLOAT). "
|
|
38
|
+
"Filter the value out before passing to inline_params, or pass a "
|
|
39
|
+
"string like 'NaN' if your warehouse supports it."
|
|
40
|
+
)
|
|
41
|
+
if math.isinf(value):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Infinity cannot be inlined as a SQL literal — different warehouses "
|
|
44
|
+
"use different syntax. Filter the value out before passing to "
|
|
45
|
+
"inline_params, or pass a string."
|
|
46
|
+
)
|
|
47
|
+
return repr(value)
|
|
48
|
+
if isinstance(value, Decimal):
|
|
49
|
+
return str(value)
|
|
50
|
+
if isinstance(value, datetime):
|
|
51
|
+
return f"TIMESTAMP '{value.isoformat()}'"
|
|
52
|
+
if isinstance(value, date):
|
|
53
|
+
return f"DATE '{value.isoformat()}'"
|
|
54
|
+
if isinstance(value, time):
|
|
55
|
+
return f"TIME '{value.isoformat()}'"
|
|
56
|
+
if isinstance(value, bytes):
|
|
57
|
+
return f"X'{value.hex()}'"
|
|
58
|
+
# String — escape single quotes to prevent injection
|
|
59
|
+
escaped = str(value).replace("'", "''")
|
|
60
|
+
return f"'{escaped}'"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def inline_params(sql: str, params: list[Any]) -> str:
|
|
64
|
+
"""Inline $N-style positional params into a SQL string as SQL literals.
|
|
65
|
+
|
|
66
|
+
Replaces $1, $2, ... placeholders with properly escaped SQL literals.
|
|
67
|
+
Uses re.sub with a callback so each match position is visited exactly once —
|
|
68
|
+
substituted text is never re-scanned, preventing re-substitution attacks.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
sql: SQL string with $1, $2, ... positional placeholders.
|
|
72
|
+
params: Ordered list of parameter values to inline.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
SQL string with all $N placeholders replaced by SQL literals.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
IndexError: A $N placeholder references an index outside [1, len(params)].
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def repl(m: re.Match[str]) -> str:
|
|
82
|
+
idx = int(m.group(1)) - 1
|
|
83
|
+
if not 0 <= idx < len(params):
|
|
84
|
+
raise IndexError(
|
|
85
|
+
f"Parameter index ${m.group(1)} out of range "
|
|
86
|
+
f"for {len(params)} param(s)"
|
|
87
|
+
)
|
|
88
|
+
return _to_sql_literal(params[idx])
|
|
89
|
+
|
|
90
|
+
return _DOLLAR_RE.sub(repl, sql)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def inline_dialect_params(
|
|
94
|
+
sql: str, params: list[Any], param_fn: Callable[[int], str]
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Inline dialect-specific named params into SQL as SQL literals.
|
|
97
|
+
|
|
98
|
+
For dialects whose placeholder format is not `$N` (e.g., BigQuery's `@paramN`,
|
|
99
|
+
Databricks' `:param1`, SQL Server's `@p1`), replaces each placeholder with a
|
|
100
|
+
properly escaped SQL literal.
|
|
101
|
+
|
|
102
|
+
Uses a single-pass regex so substituted text is never re-scanned — a param
|
|
103
|
+
value that happens to contain another placeholder string is not re-substituted.
|
|
104
|
+
|
|
105
|
+
The dbt adapter.execute() accepts only plain SQL, so this function is used
|
|
106
|
+
when executing against named-param dialects via dbt adapters.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
sql: SQL string with dialect-specific parameter placeholders.
|
|
110
|
+
params: Ordered list of parameter values (1-indexed by placeholder number).
|
|
111
|
+
param_fn: Callable(index: int) -> str — returns the placeholder string for
|
|
112
|
+
the given 1-based index. Pass the dialect's ``param`` method.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
SQL string with all dialect placeholders replaced by SQL literals.
|
|
116
|
+
"""
|
|
117
|
+
if not params:
|
|
118
|
+
return sql
|
|
119
|
+
|
|
120
|
+
# Build a pattern that alternates all placeholders, sorted longest-first so
|
|
121
|
+
# @param10 is matched before @param1 when they share a common prefix.
|
|
122
|
+
placeholders = sorted(
|
|
123
|
+
(re.escape(param_fn(i)), i) for i in range(1, len(params) + 1)
|
|
124
|
+
)
|
|
125
|
+
# Sort by length descending to avoid partial-match of @param1 inside @param10
|
|
126
|
+
placeholders.sort(key=lambda t: len(t[0]), reverse=True)
|
|
127
|
+
idx_by_placeholder = dict(placeholders)
|
|
128
|
+
pattern = re.compile("|".join(ph for ph, _ in placeholders))
|
|
129
|
+
|
|
130
|
+
def repl(m: re.Match[str]) -> str:
|
|
131
|
+
ph = re.escape(m.group(0))
|
|
132
|
+
return _to_sql_literal(params[idx_by_placeholder[ph] - 1])
|
|
133
|
+
|
|
134
|
+
return pattern.sub(repl, sql)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def inline_qmark_params(sql: str, params: list[Any]) -> str:
|
|
138
|
+
"""Inline ?-style positional params into a SQL string as SQL literals.
|
|
139
|
+
|
|
140
|
+
Snowflake uses ? for positional parameters. Each ? is replaced in order
|
|
141
|
+
with the corresponding parameter value as a SQL literal.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
sql: SQL string with ? positional placeholders.
|
|
145
|
+
params: Ordered list of parameter values to inline.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
SQL string with all ? placeholders replaced by SQL literals.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: Param count does not match placeholder count.
|
|
152
|
+
"""
|
|
153
|
+
n_placeholders = len(_QMARK_RE.findall(sql))
|
|
154
|
+
if len(params) != n_placeholders:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"inline_qmark_params: {n_placeholders} ? placeholder(s) in SQL "
|
|
157
|
+
f"but {len(params)} param(s) provided"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
it = iter(params)
|
|
161
|
+
|
|
162
|
+
def repl(m: re.Match[str]) -> str:
|
|
163
|
+
return _to_sql_literal(next(it))
|
|
164
|
+
|
|
165
|
+
return _QMARK_RE.sub(repl, sql)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def inline_percent_params(sql: str, params: list[Any]) -> str:
|
|
169
|
+
"""Inline %s-style positional params into a SQL string as SQL literals.
|
|
170
|
+
|
|
171
|
+
Used by InspectConnection where legacy code used %s placeholders for
|
|
172
|
+
schema/table name parameters that now need literal inlining via dbt adapters.
|
|
173
|
+
Uses re.sub with a callback so each match is visited exactly once —
|
|
174
|
+
substituted text is never re-scanned, preventing re-substitution attacks.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
sql: SQL string with %s positional placeholders.
|
|
178
|
+
params: Ordered list of parameter values to inline. Count must match
|
|
179
|
+
exactly the number of %s placeholders — too few or too many raises.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
SQL string with all %s placeholders replaced by SQL literals.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: Param count does not match placeholder count.
|
|
186
|
+
"""
|
|
187
|
+
n_placeholders = len(_PERCENT_S_RE.findall(sql))
|
|
188
|
+
if len(params) != n_placeholders:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"inline_percent_params: {n_placeholders} %s placeholder(s) in SQL "
|
|
191
|
+
f"but {len(params)} param(s) provided"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
it = iter(params)
|
|
195
|
+
|
|
196
|
+
def repl(m: re.Match[str]) -> str:
|
|
197
|
+
return _to_sql_literal(next(it))
|
|
198
|
+
|
|
199
|
+
return _PERCENT_S_RE.sub(repl, sql)
|
dataface/core/fonts.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Shared font path accessors for compile and render layers."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_fonts_dir() -> Path:
|
|
7
|
+
"""Return the directory holding every vendored font asset."""
|
|
8
|
+
return Path(__file__).parent / "render" / "fonts"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_inter_font_path() -> Path:
|
|
12
|
+
"""Return the vendored Inter variable font file."""
|
|
13
|
+
return get_fonts_dir() / "InterVariable.ttf"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_dft_sans_tabular_font_path() -> Path:
|
|
17
|
+
"""Return the vendored DFT Sans Tabular font file."""
|
|
18
|
+
return get_fonts_dir() / "DFTSansTabular-Regular.ttf"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_dft_serif_oldstyle_tabular_font_path() -> Path:
|
|
22
|
+
"""Return the vendored DFT Serif Oldstyle Tabular font file."""
|
|
23
|
+
return get_fonts_dir() / "DFTSerifOldstyleTabular-Regular.ttf"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_dft_serif_oldstyle_proportional_font_path() -> Path:
|
|
27
|
+
"""Return the vendored DFT Serif Oldstyle Proportional font file."""
|
|
28
|
+
return get_fonts_dir() / "DFTSerifOldstyleProportional-Regular.ttf"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_source_serif_4_ttf_path() -> Path:
|
|
32
|
+
"""Return the vendored Source Serif 4 TTF for ReportLab."""
|
|
33
|
+
return get_fonts_dir() / "SourceSerif4-Regular.ttf"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NOTO_EMOJI_FONT_FAMILY = "Noto Emoji"
|
|
37
|
+
NOTO_COLOR_EMOJI_FONT_FAMILY = "Noto Color Emoji"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_noto_emoji_font_path() -> Path:
|
|
41
|
+
"""Return the vendored Noto Emoji TTF for ReportLab and vl-convert."""
|
|
42
|
+
return get_fonts_dir() / "NotoEmoji-Regular.ttf"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_noto_color_emoji_font_path() -> Path:
|
|
46
|
+
"""Return the vendored Noto Color Emoji TTF for ReportLab and vl-convert."""
|
|
47
|
+
return get_fonts_dir() / "NotoColorEmoji-Regular.ttf"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_mono_font_path() -> Path:
|
|
51
|
+
"""Return the vendored monospace font used for strict code measurement."""
|
|
52
|
+
return get_fonts_dir() / "SourceCodePro-Regular.ttf"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""OSS inspect module — schema sources, resolver, search, and HTML templates.
|
|
2
|
+
|
|
3
|
+
The profiler engine (TableInspector, detectors, cache I/O) lives in the
|
|
4
|
+
private ``dataface-super-schema`` package. This module only contains the
|
|
5
|
+
open-source components: DbtSchemaSource, LayeredSchemaResolver, schema
|
|
6
|
+
search, and the HTML inspect templates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataface.core.inspect.renderer import (
|
|
10
|
+
InspectProfileCompileError,
|
|
11
|
+
render_inspect_dashboard,
|
|
12
|
+
validate_inspect_variables,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Static list of available inspect templates.
|
|
16
|
+
# Template files live in dataface/core/inspect/templates/{name}.yml
|
|
17
|
+
INSPECT_TEMPLATES: list[str] = [
|
|
18
|
+
"categorical_column",
|
|
19
|
+
"charts",
|
|
20
|
+
"date_column",
|
|
21
|
+
"model",
|
|
22
|
+
"numeric_column",
|
|
23
|
+
"quality",
|
|
24
|
+
"string_column",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"render_inspect_dashboard",
|
|
29
|
+
"InspectProfileCompileError",
|
|
30
|
+
"validate_inspect_variables",
|
|
31
|
+
"INSPECT_TEMPLATES",
|
|
32
|
+
]
|