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,98 @@
|
|
|
1
|
+
"""Cache-source factory and schema resolver builder.
|
|
2
|
+
|
|
3
|
+
Builds a LayeredSchemaResolver from an adapter registry. When the private
|
|
4
|
+
``dataface-super-schema`` package is installed, the resolver uses a warm
|
|
5
|
+
SuperSchemaSource cache; otherwise it operates in dbt-only mode.
|
|
6
|
+
|
|
7
|
+
This module provides factory helpers for building resolvers with optional
|
|
8
|
+
super-schema caches. Other modules that only need a presence check use their
|
|
9
|
+
own ``importlib.util.find_spec("dataface_super_schema")`` probe — that is
|
|
10
|
+
intentional and avoids import cycles.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from dataface.core.execute.adapters import AdapterRegistry
|
|
20
|
+
from dataface.core.inspect.resolver import LayeredSchemaResolver
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from dataface_super_schema.inspect.sources.super_schema import SuperSchemaSource
|
|
24
|
+
|
|
25
|
+
# Probe at module load time — single find_spec call per process.
|
|
26
|
+
_SUPER_SCHEMA_AVAILABLE: bool = (
|
|
27
|
+
importlib.util.find_spec("dataface_super_schema") is not None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def project_root_for_registry(registry: AdapterRegistry) -> Path:
|
|
32
|
+
"""Resolve the registry's project root without consulting cwd."""
|
|
33
|
+
raw = registry.project_root
|
|
34
|
+
if raw is not None:
|
|
35
|
+
return Path(raw).resolve()
|
|
36
|
+
raise ValueError("adapter_registry.project_root is required for schema requests")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_cache_source(cache_path: Path) -> Any:
|
|
40
|
+
"""Build a SuperSchemaSource when the private package is available.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
cache_path: Absolute path to a ``super_schema.json`` cache file.
|
|
44
|
+
|
|
45
|
+
Returns ``None`` when ``dataface-super-schema`` is not installed.
|
|
46
|
+
The resolver degrades to dbt-only mode when ``None`` is returned.
|
|
47
|
+
"""
|
|
48
|
+
if not _SUPER_SCHEMA_AVAILABLE:
|
|
49
|
+
return None
|
|
50
|
+
from dataface_super_schema.inspect.sources.super_schema import ( # noqa: PLC0415
|
|
51
|
+
SuperSchemaSource,
|
|
52
|
+
)
|
|
53
|
+
from dataface_super_schema.inspect.storage import InspectionStorage # noqa: PLC0415
|
|
54
|
+
|
|
55
|
+
return SuperSchemaSource(InspectionStorage(output_path=cache_path))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_cache_source_for_project(project_root: Path) -> Any:
|
|
59
|
+
"""Build a SuperSchemaSource from the project's default cache path.
|
|
60
|
+
|
|
61
|
+
Equivalent to make_cache_source(project_root / InspectionStorage.DEFAULT_PATH)
|
|
62
|
+
without importing InspectionStorage in the caller.
|
|
63
|
+
"""
|
|
64
|
+
if not _SUPER_SCHEMA_AVAILABLE:
|
|
65
|
+
return None
|
|
66
|
+
from dataface_super_schema.inspect.storage import InspectionStorage # noqa: PLC0415
|
|
67
|
+
|
|
68
|
+
return make_cache_source(project_root / InspectionStorage.DEFAULT_PATH)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_resolver(
|
|
72
|
+
adapter_registry: AdapterRegistry,
|
|
73
|
+
cache_path: Path | None = None,
|
|
74
|
+
cache_source: SuperSchemaSource | None = None,
|
|
75
|
+
) -> LayeredSchemaResolver:
|
|
76
|
+
"""Build a LayeredSchemaResolver from an adapter registry and optional cache.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
adapter_registry: Registry providing source configs and project root.
|
|
80
|
+
cache_path: Explicit path to a ``super_schema.json`` file. When
|
|
81
|
+
``None``, the cache is auto-derived from the registry's
|
|
82
|
+
project_root via ``make_cache_source_for_project``.
|
|
83
|
+
cache_source: Explicit warm-cache source. Takes priority over
|
|
84
|
+
``cache_path`` when provided.
|
|
85
|
+
"""
|
|
86
|
+
project_root = project_root_for_registry(adapter_registry)
|
|
87
|
+
resolved_cache: Any = cache_source
|
|
88
|
+
if resolved_cache is None:
|
|
89
|
+
resolved_cache = (
|
|
90
|
+
make_cache_source(cache_path)
|
|
91
|
+
if cache_path is not None
|
|
92
|
+
else make_cache_source_for_project(project_root)
|
|
93
|
+
)
|
|
94
|
+
return LayeredSchemaResolver(
|
|
95
|
+
cache=resolved_cache,
|
|
96
|
+
adapter_registry=adapter_registry,
|
|
97
|
+
project_root=project_root,
|
|
98
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Shared SQL type classification for the inspect module.
|
|
2
|
+
|
|
3
|
+
Provides a single source of truth for categorising database column types
|
|
4
|
+
as numeric, string, temporal, or complex. Used by query_builder,
|
|
5
|
+
quality_detector, semantic_detector, and inspector.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Base-type normaliser
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# Postgres-specific aliases → standard SQL type names
|
|
17
|
+
_POSTGRES_TYPE_MAP: dict[str, str] = {
|
|
18
|
+
"INT2": "SMALLINT",
|
|
19
|
+
"INT4": "INTEGER",
|
|
20
|
+
"INT8": "BIGINT",
|
|
21
|
+
"FLOAT4": "FLOAT",
|
|
22
|
+
"FLOAT8": "DOUBLE",
|
|
23
|
+
"BOOL": "BOOLEAN",
|
|
24
|
+
"BPCHAR": "CHAR",
|
|
25
|
+
"TIMESTAMPTZ": "TIMESTAMP",
|
|
26
|
+
"TIMETZ": "TIME",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_base_type(db_type: str, *, normalize_aliases: bool = False) -> str:
|
|
31
|
+
"""Extract base type name without precision, scale, or modifiers.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
db_type: Raw database type string, e.g. ``"DECIMAL(18,2) NOT NULL"``.
|
|
35
|
+
normalize_aliases: When True, map Postgres aliases to standard
|
|
36
|
+
names (e.g. ``INT8`` → ``BIGINT``).
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> extract_base_type("DECIMAL(18,2)")
|
|
40
|
+
'DECIMAL'
|
|
41
|
+
>>> extract_base_type("int8", normalize_aliases=True)
|
|
42
|
+
'BIGINT'
|
|
43
|
+
"""
|
|
44
|
+
base = re.sub(r"\([^)]*\)", "", db_type.upper()).strip()
|
|
45
|
+
for modifier in ("NOT NULL", "NULL", "PRIMARY KEY", "UNIQUE", "DEFAULT", "ARRAY"):
|
|
46
|
+
base = base.replace(modifier, "").strip()
|
|
47
|
+
base = base.split()[0] if base else base
|
|
48
|
+
if normalize_aliases:
|
|
49
|
+
base = _POSTGRES_TYPE_MAP.get(base, base)
|
|
50
|
+
return base
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Canonical type sets (superset across all supported databases)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
NUMERIC_TYPES: frozenset[str] = frozenset(
|
|
58
|
+
{
|
|
59
|
+
"INTEGER",
|
|
60
|
+
"INT",
|
|
61
|
+
"BIGINT",
|
|
62
|
+
"SMALLINT",
|
|
63
|
+
"TINYINT",
|
|
64
|
+
"DECIMAL",
|
|
65
|
+
"NUMERIC",
|
|
66
|
+
"FLOAT",
|
|
67
|
+
"DOUBLE",
|
|
68
|
+
"REAL",
|
|
69
|
+
"NUMBER", # Oracle / Snowflake
|
|
70
|
+
"INT64",
|
|
71
|
+
"FLOAT64", # BigQuery
|
|
72
|
+
"INT2",
|
|
73
|
+
"INT4",
|
|
74
|
+
"INT8",
|
|
75
|
+
"FLOAT4",
|
|
76
|
+
"FLOAT8", # Postgres aliases
|
|
77
|
+
"SERIAL",
|
|
78
|
+
"BIGSERIAL",
|
|
79
|
+
"SMALLSERIAL", # Postgres auto-increment
|
|
80
|
+
"HUGEINT",
|
|
81
|
+
"UBIGINT",
|
|
82
|
+
"UINTEGER", # DuckDB unsigned
|
|
83
|
+
"USMALLINT",
|
|
84
|
+
"UTINYINT",
|
|
85
|
+
"MONEY", # Postgres money
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
STRING_TYPES: frozenset[str] = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"VARCHAR",
|
|
92
|
+
"CHAR",
|
|
93
|
+
"TEXT",
|
|
94
|
+
"STRING",
|
|
95
|
+
"CLOB",
|
|
96
|
+
"NVARCHAR",
|
|
97
|
+
"NCHAR",
|
|
98
|
+
"NTEXT",
|
|
99
|
+
"BPCHAR", # Postgres blank-padded char
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
TEMPORAL_TYPES: frozenset[str] = frozenset(
|
|
104
|
+
{
|
|
105
|
+
"DATE",
|
|
106
|
+
"TIMESTAMP",
|
|
107
|
+
"DATETIME",
|
|
108
|
+
"TIME",
|
|
109
|
+
"TIMESTAMPTZ",
|
|
110
|
+
"TIMETZ", # Postgres with timezone
|
|
111
|
+
"TIMESTAMP_NTZ",
|
|
112
|
+
"TIMESTAMP_LTZ",
|
|
113
|
+
"TIMESTAMP_TZ", # Snowflake variants
|
|
114
|
+
"INTERVAL",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
COMPLEX_TYPES: frozenset[str] = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"ARRAY",
|
|
121
|
+
"STRUCT",
|
|
122
|
+
"RECORD",
|
|
123
|
+
"GEOGRAPHY",
|
|
124
|
+
"GEOMETRY",
|
|
125
|
+
"BYTES",
|
|
126
|
+
"JSON", # BigQuery
|
|
127
|
+
"VARIANT",
|
|
128
|
+
"OBJECT", # Snowflake
|
|
129
|
+
"MAP", # Databricks
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Prefixes that indicate a complex parameterised type (e.g. ARRAY<INT64>)
|
|
134
|
+
_COMPLEX_PREFIXES = ("ARRAY<", "STRUCT<", "RECORD<", "MAP<")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Convenience helpers
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_numeric(db_type: str) -> bool:
|
|
143
|
+
"""Return True if *db_type* is a numeric column type."""
|
|
144
|
+
return extract_base_type(db_type) in NUMERIC_TYPES
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_string(db_type: str) -> bool:
|
|
148
|
+
"""Return True if *db_type* is a string/text column type."""
|
|
149
|
+
return extract_base_type(db_type) in STRING_TYPES
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_temporal(db_type: str) -> bool:
|
|
153
|
+
"""Return True if *db_type* is a date, time, or timestamp type."""
|
|
154
|
+
return extract_base_type(db_type) in TEMPORAL_TYPES
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_complex(db_type: str) -> bool:
|
|
158
|
+
"""Return True if *db_type* is a complex type unsuitable for standard aggregates."""
|
|
159
|
+
upper = db_type.upper()
|
|
160
|
+
if upper.startswith(_COMPLEX_PREFIXES):
|
|
161
|
+
return True
|
|
162
|
+
return extract_base_type(db_type) in COMPLEX_TYPES
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Parse dbt schema.yml files and map descriptions to inspected tables/columns.
|
|
2
|
+
|
|
3
|
+
Scans ``models/**/schema.yml`` (and ``.yaml``) for model-level and
|
|
4
|
+
column-level ``description`` fields, then maps them onto inspected
|
|
5
|
+
table/column names using case-insensitive matching.
|
|
6
|
+
|
|
7
|
+
Descriptions are stored as explicit source entries with
|
|
8
|
+
``source=dbt_schema_yml`` — they never overwrite inferred metadata.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DbtDescription:
|
|
21
|
+
"""A single description extracted from a dbt schema.yml file."""
|
|
22
|
+
|
|
23
|
+
model_name: str
|
|
24
|
+
column_name: str | None # None = table-level description
|
|
25
|
+
description: str
|
|
26
|
+
source: str = "dbt_schema_yml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DbtSchemaParser:
|
|
30
|
+
"""Discover and parse dbt schema.yml files under a project root."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, project_root: Path) -> None:
|
|
33
|
+
self.project_root = project_root
|
|
34
|
+
self.warnings: list[str] = []
|
|
35
|
+
|
|
36
|
+
def parse(self) -> list[DbtDescription]:
|
|
37
|
+
"""Scan ``models/**/schema.{yml,yaml}`` and extract descriptions."""
|
|
38
|
+
models_dir = self.project_root / "models"
|
|
39
|
+
if not models_dir.is_dir():
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
descriptions: list[DbtDescription] = []
|
|
43
|
+
for pattern in ("**/*.yml", "**/*.yaml"):
|
|
44
|
+
for path in sorted(models_dir.glob(pattern)):
|
|
45
|
+
descriptions.extend(self._parse_file(path))
|
|
46
|
+
return descriptions
|
|
47
|
+
|
|
48
|
+
def _parse_file(self, path: Path) -> list[DbtDescription]:
|
|
49
|
+
"""Parse a single schema file, returning descriptions found."""
|
|
50
|
+
try:
|
|
51
|
+
content = yaml.safe_load(path.read_text())
|
|
52
|
+
except yaml.YAMLError as e:
|
|
53
|
+
self.warnings.append(f"Failed to parse {path}: {e}")
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
if not isinstance(content, dict):
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
models = content.get("models")
|
|
60
|
+
if not isinstance(models, list):
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
descriptions: list[DbtDescription] = []
|
|
64
|
+
for model in models:
|
|
65
|
+
if not isinstance(model, dict):
|
|
66
|
+
continue
|
|
67
|
+
model_name = model.get("name")
|
|
68
|
+
if not model_name:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
model_desc = model.get("description")
|
|
72
|
+
if model_desc:
|
|
73
|
+
descriptions.append(
|
|
74
|
+
DbtDescription(
|
|
75
|
+
model_name=model_name, column_name=None, description=model_desc
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
columns = model.get("columns")
|
|
80
|
+
if not isinstance(columns, list):
|
|
81
|
+
continue
|
|
82
|
+
for col in columns:
|
|
83
|
+
if not isinstance(col, dict):
|
|
84
|
+
continue
|
|
85
|
+
col_name = col.get("name")
|
|
86
|
+
col_desc = col.get("description")
|
|
87
|
+
if col_name and col_desc:
|
|
88
|
+
descriptions.append(
|
|
89
|
+
DbtDescription(
|
|
90
|
+
model_name=model_name,
|
|
91
|
+
column_name=col_name,
|
|
92
|
+
description=col_desc,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return descriptions
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
inspector:
|
|
2
|
+
sample_size: 100
|
|
3
|
+
collect_top_values: true
|
|
4
|
+
top_values_limit: 10
|
|
5
|
+
top_values_max_cardinality: 1000
|
|
6
|
+
top_values_skip_types:
|
|
7
|
+
- INTEGER
|
|
8
|
+
- INT
|
|
9
|
+
- BIGINT
|
|
10
|
+
- SMALLINT
|
|
11
|
+
- TINYINT
|
|
12
|
+
- FLOAT
|
|
13
|
+
- DOUBLE
|
|
14
|
+
- REAL
|
|
15
|
+
- DECIMAL
|
|
16
|
+
- NUMERIC
|
|
17
|
+
- NUMBER
|
|
18
|
+
- DATE
|
|
19
|
+
- TIME
|
|
20
|
+
- TIMESTAMP
|
|
21
|
+
- DATETIME
|
|
22
|
+
- INTERVAL
|
|
23
|
+
- BOOLEAN
|
|
24
|
+
- BOOL
|
|
25
|
+
- UUID
|
|
26
|
+
- BLOB
|
|
27
|
+
- BINARY
|
|
28
|
+
- BYTEA
|
|
29
|
+
- VARBINARY
|
|
30
|
+
- BIT
|
|
31
|
+
deep_profile: false
|
|
32
|
+
collect_histogram_bins: false
|
|
33
|
+
collect_date_distribution: false
|
|
34
|
+
collect_enum_values: false
|
|
35
|
+
enum_cardinality_threshold: 20
|
|
36
|
+
bigquery_exact_threshold_bytes: 1073741824
|
|
37
|
+
fk_range_slack_percent: 2
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Fanout risk scoring.
|
|
2
|
+
|
|
3
|
+
Scores the risk of aggregate inflation when joining table pairs.
|
|
4
|
+
Pure functions — no DB queries.
|
|
5
|
+
|
|
6
|
+
Risk levels:
|
|
7
|
+
- ``none`` — safe join, no action needed
|
|
8
|
+
- ``low`` — join changes grain but may be intentional
|
|
9
|
+
- ``medium`` — aggregation after this join may be incorrect
|
|
10
|
+
- ``high`` — strong likelihood of aggregate inflation
|
|
11
|
+
- ``critical`` — N:M join with aggregation, almost certainly wrong
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
# Fanout thresholds — public, imported by query_validator.py.
|
|
19
|
+
HIGH_FANOUT_THRESHOLD = 10.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Data model
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FanoutRisk:
|
|
29
|
+
"""Fanout risk assessment for a join relationship."""
|
|
30
|
+
|
|
31
|
+
level: str # "none" | "low" | "medium" | "high" | "critical"
|
|
32
|
+
reason: str
|
|
33
|
+
recommendation: str
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, str]:
|
|
36
|
+
"""Dict for serialization.
|
|
37
|
+
|
|
38
|
+
All keys are single words (no underscores) so snake_case and
|
|
39
|
+
camelCase are identical — one method serves both formats.
|
|
40
|
+
"""
|
|
41
|
+
return {
|
|
42
|
+
"level": self.level,
|
|
43
|
+
"reason": self.reason,
|
|
44
|
+
"recommendation": self.recommendation,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Scoring
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def score_fanout_risk(
|
|
54
|
+
multiplicity: str,
|
|
55
|
+
fanout_factor: float,
|
|
56
|
+
has_aggregation: bool = False,
|
|
57
|
+
) -> FanoutRisk:
|
|
58
|
+
"""Score fanout risk for a join pattern.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
multiplicity: Join cardinality ("one-to-one", "one-to-many",
|
|
62
|
+
"many-to-one", "many-to-many").
|
|
63
|
+
fanout_factor: Average row multiplication factor.
|
|
64
|
+
has_aggregation: Whether the query aggregates after the join.
|
|
65
|
+
"""
|
|
66
|
+
# 1:1 — always safe
|
|
67
|
+
if multiplicity == "one-to-one":
|
|
68
|
+
return FanoutRisk("none", "1:1 join — safe", "No action needed")
|
|
69
|
+
|
|
70
|
+
# N:1 — dimension lookup, always safe
|
|
71
|
+
if multiplicity == "many-to-one":
|
|
72
|
+
return FanoutRisk("none", "N:1 dimension lookup — safe", "No action needed")
|
|
73
|
+
|
|
74
|
+
# N:M — always dangerous
|
|
75
|
+
if multiplicity == "many-to-many":
|
|
76
|
+
if has_aggregation:
|
|
77
|
+
return FanoutRisk(
|
|
78
|
+
"critical",
|
|
79
|
+
"N:M join with aggregation — almost certainly inflates results",
|
|
80
|
+
"Use a bridge table or pre-aggregate to the correct grain before joining",
|
|
81
|
+
)
|
|
82
|
+
return FanoutRisk(
|
|
83
|
+
"high",
|
|
84
|
+
"N:M join — row multiplication likely unintended",
|
|
85
|
+
"Consider using a bridge table or pre-aggregating before joining",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 1:N — risk depends on aggregation context and fanout factor
|
|
89
|
+
if multiplicity == "one-to-many":
|
|
90
|
+
if not has_aggregation:
|
|
91
|
+
return FanoutRisk(
|
|
92
|
+
"low",
|
|
93
|
+
"1:N join without aggregation — row expansion expected",
|
|
94
|
+
"Verify this is a detail query, not an aggregate",
|
|
95
|
+
)
|
|
96
|
+
if fanout_factor > HIGH_FANOUT_THRESHOLD:
|
|
97
|
+
return FanoutRisk(
|
|
98
|
+
"high",
|
|
99
|
+
f"1:N join with aggregation and high fanout ({fanout_factor:.1f}x)",
|
|
100
|
+
"Pre-aggregate the many-side to the join key grain before joining",
|
|
101
|
+
)
|
|
102
|
+
return FanoutRisk(
|
|
103
|
+
"medium",
|
|
104
|
+
f"1:N join with aggregation ({fanout_factor:.1f}x fanout)",
|
|
105
|
+
"Pre-aggregate the many-side to the join key grain before joining",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Unknown multiplicity — safe default
|
|
109
|
+
return FanoutRisk("none", "Unknown multiplicity", "Review join pattern")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Manifest I/O and template-comparison helpers for the inspect verb.
|
|
2
|
+
|
|
3
|
+
Pure functions — no agent-api or Pydantic coupling. Used by
|
|
4
|
+
``dataface.agent_api.inspect`` to persist ejection state and compare
|
|
5
|
+
local templates against built-in versions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from hashlib import sha256
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
INSPECT_TEMPLATE_MANIFEST = ".inspect-template-manifest.json"
|
|
17
|
+
MANIFEST_SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_manifest(target_dir: Path, *, dataface_version: str) -> dict:
|
|
21
|
+
"""Load the inspect-template manifest from target_dir, or return a fresh one."""
|
|
22
|
+
manifest_path = target_dir / INSPECT_TEMPLATE_MANIFEST
|
|
23
|
+
if not manifest_path.exists():
|
|
24
|
+
return {
|
|
25
|
+
"schema_version": MANIFEST_SCHEMA_VERSION,
|
|
26
|
+
"dataface_version": dataface_version,
|
|
27
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
28
|
+
"templates": {},
|
|
29
|
+
}
|
|
30
|
+
with open(manifest_path) as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_manifest(target_dir: Path, manifest: dict) -> None:
|
|
35
|
+
"""Write the inspect-template manifest to target_dir."""
|
|
36
|
+
manifest_path = target_dir / INSPECT_TEMPLATE_MANIFEST
|
|
37
|
+
with open(manifest_path, "w") as f:
|
|
38
|
+
json.dump(manifest, f, indent=2, sort_keys=True)
|
|
39
|
+
f.write("\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compare_templates(
|
|
43
|
+
resolved: Path,
|
|
44
|
+
manifest_templates: dict[str, dict],
|
|
45
|
+
templates_pkg: Any, # importlib.resources Traversable; Any avoids 3.10/3.11 compat shim
|
|
46
|
+
) -> tuple[list[str], list[str], list[str], list[str]]:
|
|
47
|
+
"""Compare ejected templates against built-in versions.
|
|
48
|
+
|
|
49
|
+
Returns ``(missing, upstream_changed, custom_safe, unchanged)`` name lists.
|
|
50
|
+
``templates_pkg`` is an ``importlib.resources`` traversable (the package
|
|
51
|
+
containing the canonical ``.yml`` template files).
|
|
52
|
+
"""
|
|
53
|
+
missing: list[str] = []
|
|
54
|
+
upstream_changed: list[str] = []
|
|
55
|
+
custom_safe: list[str] = []
|
|
56
|
+
unchanged: list[str] = []
|
|
57
|
+
|
|
58
|
+
for name, meta in sorted(manifest_templates.items()):
|
|
59
|
+
filename = meta.get("filename") or f"{name}.yml"
|
|
60
|
+
local_path = resolved / filename
|
|
61
|
+
if not local_path.exists():
|
|
62
|
+
missing.append(filename)
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
built_in = templates_pkg.joinpath(filename).read_text()
|
|
66
|
+
built_in_hash = sha256(built_in.encode("utf-8")).hexdigest()
|
|
67
|
+
baseline_hash = meta.get("source_sha256")
|
|
68
|
+
local_hash = sha256(local_path.read_text().encode("utf-8")).hexdigest()
|
|
69
|
+
|
|
70
|
+
if local_hash == built_in_hash:
|
|
71
|
+
unchanged.append(name)
|
|
72
|
+
elif baseline_hash != built_in_hash:
|
|
73
|
+
upstream_changed.append(name)
|
|
74
|
+
else:
|
|
75
|
+
custom_safe.append(name)
|
|
76
|
+
|
|
77
|
+
return missing, upstream_changed, custom_safe, unchanged
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Categorical Column Partial - Bar chart visualization
|
|
2
|
+
#
|
|
3
|
+
# A reusable partial for visualizing categorical column distributions.
|
|
4
|
+
# Designed for use with foreach construct.
|
|
5
|
+
#
|
|
6
|
+
# Expected loop variable (col):
|
|
7
|
+
# - col.column: Column name to visualize
|
|
8
|
+
#
|
|
9
|
+
# Expected parent variables:
|
|
10
|
+
# - model: Table/model name
|
|
11
|
+
# - connection: DuckDB file path or :memory:
|
|
12
|
+
|
|
13
|
+
title: "{{ col.column }}"
|
|
14
|
+
|
|
15
|
+
queries:
|
|
16
|
+
category_counts:
|
|
17
|
+
source:
|
|
18
|
+
type: duckdb
|
|
19
|
+
path: "{{ connection }}"
|
|
20
|
+
sql: |
|
|
21
|
+
SELECT
|
|
22
|
+
COALESCE(CAST({{ col.column }} AS VARCHAR), '(null)') as category,
|
|
23
|
+
COUNT(*) as count
|
|
24
|
+
FROM {{ model }}
|
|
25
|
+
GROUP BY {{ col.column }}
|
|
26
|
+
ORDER BY COUNT(*) DESC
|
|
27
|
+
LIMIT 20
|
|
28
|
+
|
|
29
|
+
charts:
|
|
30
|
+
bar_chart:
|
|
31
|
+
title: "{{ col.column }} Distribution"
|
|
32
|
+
type: bar
|
|
33
|
+
query: category_counts
|
|
34
|
+
x: category
|
|
35
|
+
y: count
|
|
36
|
+
style:
|
|
37
|
+
orientation: horizontal
|
|
38
|
+
|
|
39
|
+
rows:
|
|
40
|
+
- bar_chart
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Date Column Partial - Time series visualization
|
|
2
|
+
#
|
|
3
|
+
# A reusable partial for visualizing date column distributions.
|
|
4
|
+
# Designed for use with foreach construct.
|
|
5
|
+
#
|
|
6
|
+
# Expected loop variable (col):
|
|
7
|
+
# - col.column: Column name to visualize
|
|
8
|
+
#
|
|
9
|
+
# Expected parent variables:
|
|
10
|
+
# - model: Table/model name
|
|
11
|
+
# - connection: DuckDB file path or :memory:
|
|
12
|
+
|
|
13
|
+
title: "{{ col.column }}"
|
|
14
|
+
|
|
15
|
+
queries:
|
|
16
|
+
daily_counts:
|
|
17
|
+
source:
|
|
18
|
+
type: duckdb
|
|
19
|
+
path: "{{ connection }}"
|
|
20
|
+
sql: |
|
|
21
|
+
SELECT
|
|
22
|
+
{{ col.column }}::DATE as date,
|
|
23
|
+
COUNT(*) as count
|
|
24
|
+
FROM {{ model }}
|
|
25
|
+
WHERE {{ col.column }} IS NOT NULL
|
|
26
|
+
GROUP BY {{ col.column }}::DATE
|
|
27
|
+
ORDER BY date
|
|
28
|
+
|
|
29
|
+
charts:
|
|
30
|
+
time_series:
|
|
31
|
+
title: "{{ col.column }} Over Time"
|
|
32
|
+
type: line
|
|
33
|
+
query: daily_counts
|
|
34
|
+
x: date
|
|
35
|
+
y: count
|
|
36
|
+
x_label: Date
|
|
37
|
+
y_label: Count
|
|
38
|
+
|
|
39
|
+
rows:
|
|
40
|
+
- time_series
|