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,838 @@
|
|
|
1
|
+
"""Enhanced YAML error formatting.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to format YAML validation errors with:
|
|
4
|
+
1. Line numbers where errors occur
|
|
5
|
+
2. Context - the actual YAML snippet
|
|
6
|
+
3. Helpful suggestions ("Did you mean?")
|
|
7
|
+
4. Graceful handling of common mistakes
|
|
8
|
+
|
|
9
|
+
Refs #94
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import difflib
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from typing import TYPE_CHECKING, Any, get_args
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from dataface.core.errors.structured import StructuredError
|
|
20
|
+
|
|
21
|
+
# Union-branch discriminator patterns: strings in Pydantic loc tuples that
|
|
22
|
+
# identify which branch of a union was being tried, not a real field name.
|
|
23
|
+
# These are NOT real YAML keys — they are Pydantic-internal identifiers.
|
|
24
|
+
_UNION_DISCRIMINATOR_PREFIXES = (
|
|
25
|
+
"function-after[",
|
|
26
|
+
"function-before[",
|
|
27
|
+
"function-wrap[",
|
|
28
|
+
"function-plain[",
|
|
29
|
+
"dict[",
|
|
30
|
+
"list[",
|
|
31
|
+
"tagged-union[",
|
|
32
|
+
"union[",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Branch names that are literal Pydantic type names (not real YAML fields)
|
|
36
|
+
_PRIMITIVE_TYPE_DISCRIMINATORS = {"str", "int", "float", "bool", "bytes", "None"}
|
|
37
|
+
|
|
38
|
+
# Names of our known union-branch model types (not real YAML fields)
|
|
39
|
+
_MODEL_TYPE_DISCRIMINATORS = {
|
|
40
|
+
"ForeachItem",
|
|
41
|
+
"ChartPatch",
|
|
42
|
+
"AuthoredFace",
|
|
43
|
+
"Variable",
|
|
44
|
+
"VariableRef",
|
|
45
|
+
"QueryRef",
|
|
46
|
+
"ChartRef",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_union_discriminator(part: Any) -> bool:
|
|
51
|
+
"""Return True when a loc element is a Pydantic union-branch label, not a YAML key."""
|
|
52
|
+
if not isinstance(part, str):
|
|
53
|
+
return False
|
|
54
|
+
if part in _PRIMITIVE_TYPE_DISCRIMINATORS:
|
|
55
|
+
return True
|
|
56
|
+
if part in _MODEL_TYPE_DISCRIMINATORS:
|
|
57
|
+
return True
|
|
58
|
+
# Functional discriminator tags (authored.py) use '@' prefix to prevent collision
|
|
59
|
+
# with user-chosen YAML keys ('ref', 'inline', etc.).
|
|
60
|
+
if part.startswith("@"):
|
|
61
|
+
return True
|
|
62
|
+
return any(part.startswith(pfx) for pfx in _UNION_DISCRIMINATOR_PREFIXES)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _find_discriminator_index(loc: tuple[Any, ...]) -> int | None:
|
|
66
|
+
"""Return the index of the union-branch discriminator in loc, or None."""
|
|
67
|
+
for i, part in enumerate(loc):
|
|
68
|
+
if _is_union_discriminator(part):
|
|
69
|
+
return i
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _branch_score(err: dict[str, Any]) -> int:
|
|
74
|
+
"""Score a branch error — lower is better (more informative).
|
|
75
|
+
|
|
76
|
+
extra_forbidden → field we actually authored that's forbidden here → 0 (best)
|
|
77
|
+
missing → required field for this branch missing → 1
|
|
78
|
+
other → some other type mismatch → 2 (noisier)
|
|
79
|
+
"""
|
|
80
|
+
t = err.get("type", "")
|
|
81
|
+
if t == "extra_forbidden":
|
|
82
|
+
return 0
|
|
83
|
+
if t == "missing":
|
|
84
|
+
return 1
|
|
85
|
+
return 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _collapse_union_validation_errors(
|
|
89
|
+
errors: list[dict[str, Any]],
|
|
90
|
+
) -> list[dict[str, Any]]:
|
|
91
|
+
"""Collapse Pydantic union-branch noise into the most informative errors.
|
|
92
|
+
|
|
93
|
+
Pydantic tries every branch of a union type annotation (str | AuthoredFace
|
|
94
|
+
| ChartPatch | ForeachItem | dict[str,ChartPatch]) for each
|
|
95
|
+
layout row. A single invalid row can produce 14+ error dicts — one per
|
|
96
|
+
(branch × wrong-field) combination. This function keeps only the errors
|
|
97
|
+
from the best-matching branch per unique loc prefix.
|
|
98
|
+
|
|
99
|
+
Algorithm:
|
|
100
|
+
1. Partition errors into "union branch" (loc contains a discriminator) and
|
|
101
|
+
"non-union" (plain field errors with no discriminator in loc).
|
|
102
|
+
2. For each unique prefix (loc up to the discriminator), collect all branches.
|
|
103
|
+
3. Pick the branch whose errors have the lowest aggregate _branch_score.
|
|
104
|
+
4. Keep the errors from that winning branch, mapped to the canonical loc
|
|
105
|
+
(prefix + remaining path after the discriminator).
|
|
106
|
+
5. Return non-union errors + winning-branch errors, capped at MAX_GROUPS.
|
|
107
|
+
"""
|
|
108
|
+
MAX_GROUPS = 8
|
|
109
|
+
|
|
110
|
+
non_union: list[dict[str, Any]] = []
|
|
111
|
+
# Maps prefix_key → {branch_name: [error_dicts]}
|
|
112
|
+
branch_groups: dict[tuple, dict[str, list[dict[str, Any]]]] = {}
|
|
113
|
+
|
|
114
|
+
for err in errors:
|
|
115
|
+
loc = err.get("loc", ())
|
|
116
|
+
disc_idx = _find_discriminator_index(loc)
|
|
117
|
+
if disc_idx is None:
|
|
118
|
+
non_union.append(err)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
prefix = loc[:disc_idx]
|
|
122
|
+
branch_name = str(loc[disc_idx])
|
|
123
|
+
branch_groups.setdefault(prefix, {}).setdefault(branch_name, []).append(err)
|
|
124
|
+
|
|
125
|
+
# Pick the best branch per prefix
|
|
126
|
+
collapsed: list[dict[str, Any]] = list(non_union)
|
|
127
|
+
|
|
128
|
+
for _prefix, branches in branch_groups.items():
|
|
129
|
+
# Score each branch: sum of individual error scores
|
|
130
|
+
scored = sorted(
|
|
131
|
+
branches.items(),
|
|
132
|
+
key=lambda kv: (
|
|
133
|
+
sum(_branch_score(e) for e in kv[1]),
|
|
134
|
+
len(kv[1]), # tie-break: fewer errors
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
best_branch_name, best_errors = scored[0]
|
|
138
|
+
|
|
139
|
+
# Rewrite locs to strip the discriminator so downstream path logic works
|
|
140
|
+
for err in best_errors:
|
|
141
|
+
loc = err.get("loc", ())
|
|
142
|
+
disc_idx = _find_discriminator_index(loc)
|
|
143
|
+
if disc_idx is not None:
|
|
144
|
+
# Canonical loc: prefix + everything after the discriminator
|
|
145
|
+
canonical_loc = loc[:disc_idx] + loc[disc_idx + 1 :]
|
|
146
|
+
collapsed.append({**err, "loc": canonical_loc})
|
|
147
|
+
else:
|
|
148
|
+
collapsed.append(err)
|
|
149
|
+
|
|
150
|
+
return collapsed[:MAX_GROUPS]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _all_authored_chart_fields() -> list[str]:
|
|
154
|
+
"""Union of all field names declared across every per-family chart patch class."""
|
|
155
|
+
from dataface.core.compile.models.chart.authored import (
|
|
156
|
+
AreaChart,
|
|
157
|
+
BarChart,
|
|
158
|
+
CalloutChart,
|
|
159
|
+
GeoshapeChart,
|
|
160
|
+
HeatmapChart,
|
|
161
|
+
KpiChart,
|
|
162
|
+
LayeredChart,
|
|
163
|
+
LineChart,
|
|
164
|
+
PieChart,
|
|
165
|
+
PointMapChart,
|
|
166
|
+
ScatterChart,
|
|
167
|
+
SparkBarChart,
|
|
168
|
+
TableChart,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
fields: set[str] = set()
|
|
172
|
+
for cls in (
|
|
173
|
+
BarChart,
|
|
174
|
+
LineChart,
|
|
175
|
+
AreaChart,
|
|
176
|
+
ScatterChart,
|
|
177
|
+
HeatmapChart,
|
|
178
|
+
PieChart,
|
|
179
|
+
KpiChart,
|
|
180
|
+
TableChart,
|
|
181
|
+
PointMapChart,
|
|
182
|
+
GeoshapeChart,
|
|
183
|
+
LayeredChart,
|
|
184
|
+
CalloutChart,
|
|
185
|
+
SparkBarChart,
|
|
186
|
+
):
|
|
187
|
+
fields.update(cls.model_fields.keys())
|
|
188
|
+
return sorted(fields)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@lru_cache(maxsize=1)
|
|
192
|
+
def get_valid_chart_types() -> tuple[str, ...]:
|
|
193
|
+
"""Dynamically extract valid chart types from the ChartType enum.
|
|
194
|
+
|
|
195
|
+
This ensures the valid types list stays in sync with the actual
|
|
196
|
+
type definitions in types.py. Uses lru_cache for thread-safe caching.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Tuple of valid chart type strings (tuple for hashability/immutability)
|
|
200
|
+
"""
|
|
201
|
+
from dataface.core.compile.models.chart.authored import (
|
|
202
|
+
ChartType,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return tuple(ct.value for ct in ChartType)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@lru_cache(maxsize=1)
|
|
209
|
+
def get_valid_input_types() -> tuple[str, ...]:
|
|
210
|
+
"""Dynamically extract valid input types from VariableInputType.
|
|
211
|
+
|
|
212
|
+
This ensures the valid types list stays in sync with the actual
|
|
213
|
+
type definitions in types.py. Uses lru_cache for thread-safe caching.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Tuple of valid input type strings (tuple for hashability/immutability)
|
|
217
|
+
"""
|
|
218
|
+
from dataface.core.compile.models.variable.authored import VariableInputType
|
|
219
|
+
|
|
220
|
+
return tuple(get_args(VariableInputType))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@lru_cache(maxsize=1)
|
|
224
|
+
def get_valid_chart_types_with_aliases() -> tuple[str, ...]:
|
|
225
|
+
"""Valid chart-type values including alias names (e.g. 'point', 'choropleth').
|
|
226
|
+
|
|
227
|
+
Tools that surface valid-name lists for users (LSP completion, hover,
|
|
228
|
+
diagnostics) need aliases included. The compiler does not — it normalizes
|
|
229
|
+
aliases to canonical names before validating.
|
|
230
|
+
"""
|
|
231
|
+
from dataface.core.compile.normalize_charts import CHART_TYPE_ALIASES
|
|
232
|
+
|
|
233
|
+
return get_valid_chart_types() + tuple(CHART_TYPE_ALIASES)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def find_yaml_line_number(
|
|
237
|
+
yaml_content: str,
|
|
238
|
+
field_path: list[str],
|
|
239
|
+
) -> int | None:
|
|
240
|
+
"""Find the line number for a field path in YAML content.
|
|
241
|
+
|
|
242
|
+
Uses a simple regex-based approach to find where a field is defined.
|
|
243
|
+
More reliable than YAML parsing for error reporting since we need
|
|
244
|
+
to match the original source positions.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
yaml_content: Raw YAML string
|
|
248
|
+
field_path: List of keys forming the path (e.g., ["charts", "revenue", "type"])
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Line number (1-indexed) or None if not found
|
|
252
|
+
"""
|
|
253
|
+
if not yaml_content or not field_path:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
lines = yaml_content.split("\n")
|
|
257
|
+
|
|
258
|
+
# Track current position in path and indentation level
|
|
259
|
+
current_path_idx = 0
|
|
260
|
+
target_key = field_path[current_path_idx]
|
|
261
|
+
expected_indent = 0
|
|
262
|
+
|
|
263
|
+
for line_num, line in enumerate(lines, start=1):
|
|
264
|
+
# Skip empty lines and comments
|
|
265
|
+
stripped = line.strip()
|
|
266
|
+
if not stripped or stripped.startswith("#"):
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Calculate current indentation
|
|
270
|
+
indent = len(line) - len(line.lstrip())
|
|
271
|
+
|
|
272
|
+
# Check if this line has a key
|
|
273
|
+
if ":" in stripped:
|
|
274
|
+
key_part = stripped.split(":")[0].strip()
|
|
275
|
+
|
|
276
|
+
# If we're looking for a key at this level and found it
|
|
277
|
+
if key_part == target_key and indent >= expected_indent:
|
|
278
|
+
# Move to next part of path
|
|
279
|
+
current_path_idx += 1
|
|
280
|
+
|
|
281
|
+
# If we've found all parts, this is the line
|
|
282
|
+
if current_path_idx >= len(field_path):
|
|
283
|
+
return line_num
|
|
284
|
+
|
|
285
|
+
# Otherwise, update expected indent and target key
|
|
286
|
+
expected_indent = indent + 2 # YAML typically uses 2-space indent
|
|
287
|
+
target_key = field_path[current_path_idx]
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_yaml_context(
|
|
293
|
+
yaml_content: str,
|
|
294
|
+
line_number: int,
|
|
295
|
+
context_lines: int = 2,
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Get YAML context around an error line.
|
|
298
|
+
|
|
299
|
+
Returns the surrounding lines with the error line highlighted.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
yaml_content: Raw YAML string
|
|
303
|
+
line_number: Line number of the error (1-indexed)
|
|
304
|
+
context_lines: Number of lines to show before/after
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Formatted context string with line numbers and error marker
|
|
308
|
+
"""
|
|
309
|
+
lines = yaml_content.split("\n")
|
|
310
|
+
|
|
311
|
+
# Ensure valid line number
|
|
312
|
+
if line_number < 1 or line_number > len(lines):
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
# Calculate range
|
|
316
|
+
start = max(0, line_number - context_lines - 1)
|
|
317
|
+
end = min(len(lines), line_number + context_lines)
|
|
318
|
+
|
|
319
|
+
# Build context with line numbers
|
|
320
|
+
result_lines: list[str] = []
|
|
321
|
+
for idx in range(start, end):
|
|
322
|
+
actual_line_num = idx + 1
|
|
323
|
+
line = lines[idx]
|
|
324
|
+
|
|
325
|
+
# Mark the error line
|
|
326
|
+
if actual_line_num == line_number:
|
|
327
|
+
marker = ">>>"
|
|
328
|
+
suffix = " # <-- Error here"
|
|
329
|
+
else:
|
|
330
|
+
marker = " "
|
|
331
|
+
suffix = ""
|
|
332
|
+
|
|
333
|
+
result_lines.append(f"{marker} {actual_line_num:4d} | {line}{suffix}")
|
|
334
|
+
|
|
335
|
+
return "\n".join(result_lines)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def suggest_similar_value(
|
|
339
|
+
invalid_value: str,
|
|
340
|
+
valid_values: Sequence[str],
|
|
341
|
+
cutoff: float = 0.6,
|
|
342
|
+
) -> str | None:
|
|
343
|
+
"""Suggest a similar valid value for a typo.
|
|
344
|
+
|
|
345
|
+
Uses difflib to find close matches to the invalid value.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
invalid_value: The invalid value that was provided
|
|
349
|
+
valid_values: List of valid values to match against
|
|
350
|
+
cutoff: Minimum similarity ratio (0-1) for a match
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The closest valid value, or None if no good match
|
|
354
|
+
"""
|
|
355
|
+
if not invalid_value or not valid_values:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# Normalize for comparison
|
|
359
|
+
normalized_input = invalid_value.lower().strip()
|
|
360
|
+
|
|
361
|
+
# First check for exact match (case insensitive)
|
|
362
|
+
for valid in valid_values:
|
|
363
|
+
if valid.lower() == normalized_input:
|
|
364
|
+
return valid
|
|
365
|
+
|
|
366
|
+
# Use difflib to find close matches
|
|
367
|
+
matches = difflib.get_close_matches(
|
|
368
|
+
normalized_input,
|
|
369
|
+
[v.lower() for v in valid_values],
|
|
370
|
+
n=1,
|
|
371
|
+
cutoff=cutoff,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if matches:
|
|
375
|
+
# Return the original case version
|
|
376
|
+
for valid in valid_values:
|
|
377
|
+
if valid.lower() == matches[0]:
|
|
378
|
+
return valid
|
|
379
|
+
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def parse_pydantic_error_path(error_message: str) -> tuple[list[str], str | None]:
|
|
384
|
+
"""Parse a Pydantic error message to extract field path and invalid value.
|
|
385
|
+
|
|
386
|
+
Pydantic errors have patterns like:
|
|
387
|
+
- "Field 'charts -> revenue -> type': Input should be ..."
|
|
388
|
+
- "validation error for AuthoredFace\\ncharts -> revenue -> type\\n Input should be..."
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
error_message: The error message from Pydantic
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Tuple of (field_path as list, invalid_value or None)
|
|
395
|
+
"""
|
|
396
|
+
field_path: list[str] = []
|
|
397
|
+
invalid_value: str | None = None
|
|
398
|
+
|
|
399
|
+
# Pattern for "Field 'path -> path -> field'" format
|
|
400
|
+
field_match = re.search(r"Field ['\"]([^'\"]+)['\"]", error_message)
|
|
401
|
+
if field_match:
|
|
402
|
+
path_str = field_match.group(1)
|
|
403
|
+
# Split by " -> " (with spaces)
|
|
404
|
+
field_path = [p.strip() for p in path_str.split("->")]
|
|
405
|
+
|
|
406
|
+
# Alternative pattern for raw Pydantic output
|
|
407
|
+
if not field_path:
|
|
408
|
+
# Look for "charts -> revenue -> type" style paths
|
|
409
|
+
path_match = re.search(r"(\w+(?:\s*->\s*\w+)+)", error_message)
|
|
410
|
+
if path_match:
|
|
411
|
+
path_str = path_match.group(1)
|
|
412
|
+
field_path = [p.strip() for p in path_str.split("->")]
|
|
413
|
+
|
|
414
|
+
# Try to extract the invalid value from "Input should be" messages
|
|
415
|
+
# These often list valid values, with the invalid one being what was provided
|
|
416
|
+
input_match = re.search(
|
|
417
|
+
r"input[_\s]?value['\"]?:?\s*['\"]?(\w+)", error_message, re.I
|
|
418
|
+
)
|
|
419
|
+
if input_match:
|
|
420
|
+
invalid_value = input_match.group(1)
|
|
421
|
+
|
|
422
|
+
return field_path, invalid_value
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def extract_valid_values_from_error(error_message: str) -> list[str]:
|
|
426
|
+
"""Extract list of valid values from an error message.
|
|
427
|
+
|
|
428
|
+
Pydantic validation errors often include messages like:
|
|
429
|
+
"Input should be 'bar', 'line', 'area', ..."
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
error_message: The error message to parse
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
List of valid values extracted from the message
|
|
436
|
+
"""
|
|
437
|
+
valid_values: list[str] = []
|
|
438
|
+
|
|
439
|
+
# Pattern for "should be 'val1', 'val2', ... or 'valN'"
|
|
440
|
+
should_be_match = re.search(
|
|
441
|
+
r"should be\s+['\"]?(\w+)['\"]?(?:,\s*['\"]?(\w+)['\"]?)*(?:\s+or\s+['\"]?(\w+)['\"]?)?",
|
|
442
|
+
error_message,
|
|
443
|
+
re.I,
|
|
444
|
+
)
|
|
445
|
+
if should_be_match:
|
|
446
|
+
# Extract all quoted values
|
|
447
|
+
values = re.findall(r"['\"](\w+)['\"]", error_message)
|
|
448
|
+
valid_values.extend(values)
|
|
449
|
+
|
|
450
|
+
return list(set(valid_values)) # Deduplicate
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def format_validation_error(
|
|
454
|
+
error: Exception,
|
|
455
|
+
yaml_content: str | None = None,
|
|
456
|
+
) -> str:
|
|
457
|
+
"""Format a validation error with enhanced information.
|
|
458
|
+
|
|
459
|
+
Adds line numbers, context, and suggestions to validation errors.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
error: The validation error (Pydantic or other)
|
|
463
|
+
yaml_content: Optional YAML content for context extraction
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Formatted error message with enhanced information
|
|
467
|
+
"""
|
|
468
|
+
error_msg = str(error)
|
|
469
|
+
|
|
470
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
471
|
+
|
|
472
|
+
if isinstance(error, PydanticValidationError):
|
|
473
|
+
return _format_pydantic_validation_error(error, yaml_content)
|
|
474
|
+
|
|
475
|
+
# For non-Pydantic errors, try to enhance based on message pattern
|
|
476
|
+
return _enhance_error_message(error_msg, yaml_content)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def format_validation_errors_structured(
|
|
480
|
+
error: Any,
|
|
481
|
+
yaml_content: str | None = None,
|
|
482
|
+
) -> "list[StructuredError]":
|
|
483
|
+
"""Format a Pydantic ValidationError into collapsed StructuredError objects.
|
|
484
|
+
|
|
485
|
+
Each returned StructuredError corresponds to one collapsed error group (not
|
|
486
|
+
one raw Pydantic error). Union-branch phantom failures are suppressed; the
|
|
487
|
+
best-matching branch per loc prefix is kept. Cap: MAX_GROUPS errors.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
error: A pydantic.ValidationError instance.
|
|
491
|
+
yaml_content: Raw YAML string — used for line-number resolution.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
list of StructuredError (≤ MAX_GROUPS elements).
|
|
495
|
+
"""
|
|
496
|
+
from dataface.core.errors.codes_compile import DF_COMPILE_EXTRA_FIELD
|
|
497
|
+
from dataface.core.errors.codes_unknown import DF_UNKNOWN_INTERNAL
|
|
498
|
+
from dataface.core.errors.structured import StructuredError
|
|
499
|
+
|
|
500
|
+
raw_errors = error.errors()
|
|
501
|
+
if not raw_errors:
|
|
502
|
+
return [
|
|
503
|
+
StructuredError(
|
|
504
|
+
code=DF_UNKNOWN_INTERNAL.code,
|
|
505
|
+
message=str(error),
|
|
506
|
+
domain=DF_UNKNOWN_INTERNAL.domain,
|
|
507
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
508
|
+
)
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
collapsed = _collapse_union_validation_errors(raw_errors)
|
|
512
|
+
|
|
513
|
+
structured: list[StructuredError] = []
|
|
514
|
+
for err in collapsed:
|
|
515
|
+
loc = err.get("loc", ())
|
|
516
|
+
err_type = err.get("type", "")
|
|
517
|
+
err_msg = err.get("msg", "Validation error")
|
|
518
|
+
|
|
519
|
+
field_path_parts = [str(p) for p in loc if not str(p).startswith("function-")]
|
|
520
|
+
field_path_str = " -> ".join(field_path_parts) if field_path_parts else ""
|
|
521
|
+
|
|
522
|
+
# Resolve line number from the field path.
|
|
523
|
+
# Strip numeric indices (list positions) — find_yaml_line_number only
|
|
524
|
+
# understands named keys and cannot navigate by index. Skipping them
|
|
525
|
+
# still yields a useful line for the surrounding block.
|
|
526
|
+
line_num: int | None = None
|
|
527
|
+
if yaml_content and field_path_parts:
|
|
528
|
+
named_parts = [p for p in field_path_parts if not p.isdigit()]
|
|
529
|
+
if named_parts:
|
|
530
|
+
line_num = find_yaml_line_number(yaml_content, named_parts)
|
|
531
|
+
|
|
532
|
+
# Pick the most specific error code based on Pydantic error type
|
|
533
|
+
if err_type == "extra_forbidden":
|
|
534
|
+
ec = DF_COMPILE_EXTRA_FIELD
|
|
535
|
+
else:
|
|
536
|
+
ec = DF_UNKNOWN_INTERNAL
|
|
537
|
+
|
|
538
|
+
# Include input value in message when it's a simple string (not a dict/list)
|
|
539
|
+
# so error consumers can see what value was actually provided.
|
|
540
|
+
input_val = err.get("input")
|
|
541
|
+
if field_path_str:
|
|
542
|
+
if isinstance(input_val, str):
|
|
543
|
+
message = f"Field '{field_path_str}': {err_msg} (got: {input_val!r})"
|
|
544
|
+
else:
|
|
545
|
+
message = f"Field '{field_path_str}': {err_msg}"
|
|
546
|
+
else:
|
|
547
|
+
message = err_msg
|
|
548
|
+
|
|
549
|
+
hint = _get_suggestion_for_error(
|
|
550
|
+
err, field_path_parts, err.get("input"), yaml_content
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
structured.append(
|
|
554
|
+
StructuredError(
|
|
555
|
+
code=ec.code,
|
|
556
|
+
message=message,
|
|
557
|
+
domain=ec.domain,
|
|
558
|
+
field_path=field_path_str or None,
|
|
559
|
+
line=line_num,
|
|
560
|
+
hint=hint,
|
|
561
|
+
doc_url=ec.doc_url,
|
|
562
|
+
docs_topic=ec.docs_topic,
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return structured
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _format_pydantic_validation_error(
|
|
570
|
+
error: Any,
|
|
571
|
+
yaml_content: str | None = None,
|
|
572
|
+
) -> str:
|
|
573
|
+
"""Format a Pydantic ValidationError with enhanced information.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
error: Pydantic ValidationError
|
|
577
|
+
yaml_content: Optional YAML content for context
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Formatted error message
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
errors = error.errors()
|
|
584
|
+
if not errors:
|
|
585
|
+
return str(error)
|
|
586
|
+
|
|
587
|
+
formatted_parts: list[str] = []
|
|
588
|
+
|
|
589
|
+
for err in errors:
|
|
590
|
+
loc = err.get("loc", [])
|
|
591
|
+
err_msg = err.get("msg", "Validation error")
|
|
592
|
+
input_value = err.get("input")
|
|
593
|
+
|
|
594
|
+
# Build human-readable field path, stripping Pydantic discriminated-union
|
|
595
|
+
# internals: "function-after[...](...)" wrappers and "tagged-union[...]" labels
|
|
596
|
+
# that appear in loc when using Annotated[..., Discriminator(...)].
|
|
597
|
+
field_path = [
|
|
598
|
+
str(p)
|
|
599
|
+
for p in loc
|
|
600
|
+
if not str(p).startswith("function-")
|
|
601
|
+
and not str(p).startswith("tagged-union[")
|
|
602
|
+
]
|
|
603
|
+
path_str = " -> ".join(field_path) if field_path else "root"
|
|
604
|
+
|
|
605
|
+
# Determine what kind of error this is
|
|
606
|
+
parts: list[str] = []
|
|
607
|
+
|
|
608
|
+
# Find line number if we have YAML content
|
|
609
|
+
line_num = None
|
|
610
|
+
context = ""
|
|
611
|
+
if yaml_content and field_path:
|
|
612
|
+
line_num = find_yaml_line_number(yaml_content, field_path)
|
|
613
|
+
if line_num:
|
|
614
|
+
context = get_yaml_context(yaml_content, line_num)
|
|
615
|
+
|
|
616
|
+
# Build error header with location
|
|
617
|
+
if line_num:
|
|
618
|
+
parts.append(f"Error at line {line_num}:")
|
|
619
|
+
else:
|
|
620
|
+
parts.append("Validation error:")
|
|
621
|
+
|
|
622
|
+
# Add context if available
|
|
623
|
+
if context:
|
|
624
|
+
parts.append(context)
|
|
625
|
+
parts.append("") # Blank line
|
|
626
|
+
|
|
627
|
+
# Add the actual error message
|
|
628
|
+
parts.append(f" Field '{path_str}': {err_msg}")
|
|
629
|
+
|
|
630
|
+
# Add suggestion for invalid values
|
|
631
|
+
suggestion = _get_suggestion_for_error(
|
|
632
|
+
err, field_path, input_value, yaml_content
|
|
633
|
+
)
|
|
634
|
+
if suggestion:
|
|
635
|
+
parts.append(f"\n {suggestion}")
|
|
636
|
+
|
|
637
|
+
formatted_parts.append("\n".join(parts))
|
|
638
|
+
|
|
639
|
+
return "\n\n".join(formatted_parts)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _get_suggestion_for_error(
|
|
643
|
+
err: dict[str, Any],
|
|
644
|
+
field_path: list[str],
|
|
645
|
+
input_value: Any,
|
|
646
|
+
yaml_content: str | None = None,
|
|
647
|
+
) -> str | None:
|
|
648
|
+
"""Get a helpful suggestion for an error.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
err: Pydantic error dict
|
|
652
|
+
field_path: Path to the field
|
|
653
|
+
input_value: The invalid input value
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Suggestion string or None
|
|
657
|
+
"""
|
|
658
|
+
err_type = err.get("type", "")
|
|
659
|
+
err_msg = err.get("msg", "")
|
|
660
|
+
|
|
661
|
+
# Check if this is a chart type error
|
|
662
|
+
if (
|
|
663
|
+
field_path
|
|
664
|
+
and field_path[-1] == "type"
|
|
665
|
+
and "charts" in field_path
|
|
666
|
+
and isinstance(input_value, str)
|
|
667
|
+
):
|
|
668
|
+
valid_chart_types = get_valid_chart_types()
|
|
669
|
+
suggestion = suggest_similar_value(input_value, valid_chart_types)
|
|
670
|
+
if suggestion:
|
|
671
|
+
return f"💡 Did you mean '{suggestion}'? Valid chart types: {', '.join(valid_chart_types[:10])}..."
|
|
672
|
+
return f"💡 Valid chart types: {', '.join(valid_chart_types[:10])}..."
|
|
673
|
+
|
|
674
|
+
# Check if this is a variable input type error
|
|
675
|
+
if (
|
|
676
|
+
field_path
|
|
677
|
+
and field_path[-1] == "input"
|
|
678
|
+
and "variables" in field_path
|
|
679
|
+
and isinstance(input_value, str)
|
|
680
|
+
):
|
|
681
|
+
valid_input_types = get_valid_input_types()
|
|
682
|
+
suggestion = suggest_similar_value(input_value, valid_input_types)
|
|
683
|
+
if suggestion:
|
|
684
|
+
return f"💡 Did you mean '{suggestion}'? Valid input types: {', '.join(valid_input_types)}"
|
|
685
|
+
|
|
686
|
+
# Missing required field
|
|
687
|
+
if "required" in err_type.lower() or "missing" in err_msg.lower():
|
|
688
|
+
field_name = field_path[-1] if field_path else "field"
|
|
689
|
+
return f"💡 The '{field_name}' field is required."
|
|
690
|
+
|
|
691
|
+
# Unknown fields. When AuthoredChart is nested inside AuthoredFace the Pydantic
|
|
692
|
+
# error loc is ("charts", chart_name, family_name, field_name) — length 4
|
|
693
|
+
# (with discriminated union: the family name like "bar", "kpi" is included).
|
|
694
|
+
# The legacy non-discriminated-union path was length 3.
|
|
695
|
+
# At the face root level the loc is length 1. Both cases use the same
|
|
696
|
+
# suggestion engine; only the path heuristic differs.
|
|
697
|
+
if err_type == "extra_forbidden":
|
|
698
|
+
if field_path[0:1] == ["charts"] and len(field_path) >= 3:
|
|
699
|
+
# Nested chart field: charts → chart_name → [family_name →] unknown_key
|
|
700
|
+
field_name = field_path[-1]
|
|
701
|
+
# Migration hint: height/width must be at chart root, not in style:.
|
|
702
|
+
if "style" in field_path and field_name in ("height", "width"):
|
|
703
|
+
return (
|
|
704
|
+
f"💡 chart.{field_name} must be set at the chart root, "
|
|
705
|
+
f"not under style: — move it up one level."
|
|
706
|
+
)
|
|
707
|
+
valid_fields = _all_authored_chart_fields()
|
|
708
|
+
suggestion = suggest_similar_value(field_name, valid_fields)
|
|
709
|
+
if suggestion:
|
|
710
|
+
return f"💡 Unknown chart field. Did you mean '{suggestion}'?"
|
|
711
|
+
return "💡 Unknown chart field. Check the chart schema for supported keys."
|
|
712
|
+
|
|
713
|
+
if len(field_path) == 1:
|
|
714
|
+
field_name = field_path[0]
|
|
715
|
+
yaml_parent_path = _find_yaml_parent_path_for_key(yaml_content, field_name)
|
|
716
|
+
|
|
717
|
+
if yaml_parent_path == []:
|
|
718
|
+
from dataface.core.compile.models.face.authored import AuthoredFace
|
|
719
|
+
|
|
720
|
+
valid_fields = sorted(AuthoredFace.model_fields)
|
|
721
|
+
suggestion = suggest_similar_value(field_name, valid_fields)
|
|
722
|
+
if suggestion:
|
|
723
|
+
return f"💡 Unknown field. Did you mean '{suggestion}'?"
|
|
724
|
+
return "💡 Unknown field. Check the face schema for supported keys."
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
yaml_parent_path is not None
|
|
728
|
+
and len(yaml_parent_path) == 2
|
|
729
|
+
and yaml_parent_path[0] == "charts"
|
|
730
|
+
):
|
|
731
|
+
valid_fields = _all_authored_chart_fields()
|
|
732
|
+
suggestion = suggest_similar_value(field_name, valid_fields)
|
|
733
|
+
if suggestion:
|
|
734
|
+
return f"💡 Unknown chart field. Did you mean '{suggestion}'?"
|
|
735
|
+
return (
|
|
736
|
+
"💡 Unknown chart field. Check the chart schema for supported keys."
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
return "💡 Unknown field. Check the schema for supported keys."
|
|
740
|
+
|
|
741
|
+
# For literal/enum type errors, extract valid values from message
|
|
742
|
+
if "literal" in err_type.lower() or "should be" in err_msg.lower():
|
|
743
|
+
valid_values = extract_valid_values_from_error(err_msg)
|
|
744
|
+
if valid_values and isinstance(input_value, str):
|
|
745
|
+
suggestion = suggest_similar_value(input_value, valid_values)
|
|
746
|
+
if suggestion:
|
|
747
|
+
return f"💡 Did you mean '{suggestion}'?"
|
|
748
|
+
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _find_yaml_parent_path_for_key(
|
|
753
|
+
yaml_content: str | None,
|
|
754
|
+
field_name: str,
|
|
755
|
+
) -> list[str] | None:
|
|
756
|
+
if not yaml_content:
|
|
757
|
+
return None
|
|
758
|
+
|
|
759
|
+
stack: list[tuple[int, str]] = []
|
|
760
|
+
for line in yaml_content.splitlines():
|
|
761
|
+
stripped = line.strip()
|
|
762
|
+
if not stripped or stripped.startswith("#") or ":" not in stripped:
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
indent = len(line) - len(line.lstrip())
|
|
766
|
+
while stack and indent <= stack[-1][0]:
|
|
767
|
+
stack.pop()
|
|
768
|
+
|
|
769
|
+
key_part, value_part = stripped.split(":", 1)
|
|
770
|
+
if key_part.startswith("- "):
|
|
771
|
+
key_part = key_part[2:].strip()
|
|
772
|
+
key = key_part.strip().strip("'\"")
|
|
773
|
+
parent_path = [name for _, name in stack]
|
|
774
|
+
|
|
775
|
+
if key == field_name:
|
|
776
|
+
return parent_path
|
|
777
|
+
|
|
778
|
+
if value_part.strip() == "":
|
|
779
|
+
stack.append((indent, key))
|
|
780
|
+
|
|
781
|
+
return None
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _enhance_error_message(
|
|
785
|
+
error_msg: str,
|
|
786
|
+
yaml_content: str | None = None,
|
|
787
|
+
) -> str:
|
|
788
|
+
"""Enhance a non-Pydantic error message.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
error_msg: The error message string
|
|
792
|
+
yaml_content: Optional YAML content for context
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Enhanced error message
|
|
796
|
+
"""
|
|
797
|
+
# Try to find field path in the error message
|
|
798
|
+
field_path, invalid_value = parse_pydantic_error_path(error_msg)
|
|
799
|
+
|
|
800
|
+
parts: list[str] = []
|
|
801
|
+
|
|
802
|
+
# Find line number if possible
|
|
803
|
+
line_num = None
|
|
804
|
+
context = ""
|
|
805
|
+
if yaml_content and field_path:
|
|
806
|
+
line_num = find_yaml_line_number(yaml_content, field_path)
|
|
807
|
+
if line_num:
|
|
808
|
+
context = get_yaml_context(yaml_content, line_num)
|
|
809
|
+
|
|
810
|
+
# Add line number header
|
|
811
|
+
if line_num:
|
|
812
|
+
parts.append(f"Error at line {line_num}:")
|
|
813
|
+
else:
|
|
814
|
+
parts.append("Error:")
|
|
815
|
+
|
|
816
|
+
# Add context
|
|
817
|
+
if context:
|
|
818
|
+
parts.append(context)
|
|
819
|
+
parts.append("")
|
|
820
|
+
|
|
821
|
+
# Add the original error
|
|
822
|
+
parts.append(f" {error_msg}")
|
|
823
|
+
|
|
824
|
+
# Try to add suggestions based on content
|
|
825
|
+
if "chart" in error_msg.lower() and "type" in error_msg.lower():
|
|
826
|
+
# Extract potential invalid type from message
|
|
827
|
+
type_match = re.search(r"['\"](\w+)['\"]", error_msg)
|
|
828
|
+
if type_match:
|
|
829
|
+
invalid_type = type_match.group(1)
|
|
830
|
+
valid_chart_types = get_valid_chart_types()
|
|
831
|
+
if invalid_type not in valid_chart_types:
|
|
832
|
+
suggestion = suggest_similar_value(invalid_type, valid_chart_types)
|
|
833
|
+
if suggestion:
|
|
834
|
+
parts.append(
|
|
835
|
+
f"\n 💡 Did you mean '{suggestion}'? Valid types: {', '.join(valid_chart_types[:10])}..."
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
return "\n".join(parts)
|