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,299 @@
|
|
|
1
|
+
"""DFT typographic system: title level, width offsets, sizes, weights, and family.
|
|
2
|
+
|
|
3
|
+
A title's rendered size is determined by two orthogonal axes:
|
|
4
|
+
|
|
5
|
+
1. **Heading level** (``face.level``): the H-tag the title corresponds to.
|
|
6
|
+
Root face = 1 (H1), nested face = 2 (H2), chart title = parent face level + 1.
|
|
7
|
+
|
|
8
|
+
2. **Width offset**: an additive shift on the heading level based on card pixel
|
|
9
|
+
width. Narrow cards "demote" a title one step down the size ramp; tiny cards
|
|
10
|
+
demote it further. Offsets live in theme YAML under
|
|
11
|
+
``style.title.width_offsets.*``. Width breakpoints are engine constants here.
|
|
12
|
+
|
|
13
|
+
Together they compute:
|
|
14
|
+
effective_level = clamp(level + width_offset(width), 1, len(sizes))
|
|
15
|
+
font_size = sizes[effective_level - 1]
|
|
16
|
+
|
|
17
|
+
Width tiers and their default offsets:
|
|
18
|
+
|
|
19
|
+
tiny (< 360px): offset +3 / weight 600 (legibility floor)
|
|
20
|
+
narrow (3–11 cols, 360–559px): offset +1 / weight from config
|
|
21
|
+
medium (12–23 cols, 560–1099px): offset 0 / weight from config
|
|
22
|
+
wide (24 cols, ≥ 1100px): offset 0 / weight from config
|
|
23
|
+
|
|
24
|
+
Font family rules (theme-driven; values below are per-theme):
|
|
25
|
+
- Tiny + narrow chart titles always resolve to ``style.font.family`` (the body
|
|
26
|
+
family). On every shipped theme that's a sans family.
|
|
27
|
+
- Medium + wide chart titles resolve to ``style.title.font.family`` (the
|
|
28
|
+
title-slot family). On ``stark`` this is also sans, so titles are sans at
|
|
29
|
+
every width. On the shipped ``default`` (and ``cream``) the title-slot
|
|
30
|
+
family is serif (Source Serif 4), so medium/wide chart titles render serif
|
|
31
|
+
while smaller cards stay sans for legibility.
|
|
32
|
+
- Prose content and their titles: serif at any width, regardless of theme.
|
|
33
|
+
Prose is detected via ``is_prose(text)`` — true when word count ≥ 100.
|
|
34
|
+
|
|
35
|
+
Page and section titles (face/board headers) always resolve to
|
|
36
|
+
``style.font.family`` (sans on every shipped theme) at every width unless the
|
|
37
|
+
face contains prose content. Face titles clamp the width used for offset
|
|
38
|
+
computation to the narrow boundary (≥ 360px) — a face header should never
|
|
39
|
+
shrink below the narrow tier even on a narrow nested container.
|
|
40
|
+
|
|
41
|
+
Chart labels (axis, legend, tick) are separately themed — not controlled here.
|
|
42
|
+
|
|
43
|
+
Usage::
|
|
44
|
+
|
|
45
|
+
from dataface.core.compile.typography import chart_title_spec, face_title_spec
|
|
46
|
+
|
|
47
|
+
font_size, weight, family = chart_title_spec(
|
|
48
|
+
width, level=face_level + 1, resolved_chart_style=resolved_style.charts
|
|
49
|
+
)
|
|
50
|
+
font_size, weight, family = face_title_spec(width, level=face_level)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from typing import TYPE_CHECKING
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from dataface.core.compile.models.style.merged import MergedChartsStyle
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Pixel-width thresholds that separate the four width tiers.
|
|
62
|
+
#
|
|
63
|
+
# narrow/medium/wide approximate 11-col and 23-col boundaries for the default
|
|
64
|
+
# 24-column, 1152px-content board (1200px total − 2 × 24px padding).
|
|
65
|
+
# tiny catches sub-narrow charts (small-multiples, 5+ columns) where the flat
|
|
66
|
+
# narrow size feels too large relative to chart width.
|
|
67
|
+
# They are intentionally constant — column-grid geometry, not user preference.
|
|
68
|
+
# Level offsets for each tier live in theme YAML under style.title.width_offsets.*.
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
_TINY_MAX: float = 360.0 # < 360px → tiny
|
|
72
|
+
_NARROW_MAX: float = 560.0 # 360–559px → narrow (3–11 cols)
|
|
73
|
+
_WIDE_MIN: float = 1100.0 # ≥ 1100px → wide (24 cols)
|
|
74
|
+
# 560–1099px → medium (12–23 cols)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def width_tier(width: float) -> str:
|
|
78
|
+
"""Return the tier name for a card's pixel width.
|
|
79
|
+
|
|
80
|
+
Single source of truth for the tier breakpoints — render-side
|
|
81
|
+
callers (e.g. donut-attached-table trigger) call this rather than
|
|
82
|
+
importing the module-private ``_TINY_MAX`` / ``_NARROW_MAX`` /
|
|
83
|
+
``_WIDE_MIN`` constants.
|
|
84
|
+
"""
|
|
85
|
+
if width < _TINY_MAX:
|
|
86
|
+
return "tiny"
|
|
87
|
+
if width < _NARROW_MAX:
|
|
88
|
+
return "narrow"
|
|
89
|
+
if width < _WIDE_MIN:
|
|
90
|
+
return "medium"
|
|
91
|
+
return "wide"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Tiny-tier weight floor. At small widths the theme's default weight (500)
|
|
95
|
+
# reads as anemic; 600 is the legibility floor. Other tiers use
|
|
96
|
+
# theme-configured weight.
|
|
97
|
+
_TINY_WEIGHT: int = 600
|
|
98
|
+
|
|
99
|
+
_PROSE_WORD_THRESHOLD = 100
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _coerce_weight(raw: str | float | int) -> int | str:
|
|
103
|
+
"""Return a CSS-valid font-weight value from FontStyle.weight.
|
|
104
|
+
|
|
105
|
+
FontStyle.weight is ``str | float | None``. SVG and CSS both accept
|
|
106
|
+
numeric weights (int) and keyword weights (``"bold"``, ``"normal"``).
|
|
107
|
+
Cast numeric values to int so SVG emits ``font-weight="500"`` not
|
|
108
|
+
``font-weight="500.0"``; leave string keywords unchanged.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(raw, str):
|
|
111
|
+
return raw
|
|
112
|
+
return int(raw)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _width_offset(width: float, resolved_chart_style: MergedChartsStyle) -> int:
|
|
116
|
+
"""Return the additive level offset for a card at the given pixel width.
|
|
117
|
+
|
|
118
|
+
Reads ``title.width_offsets`` from the active resolved style so face-local
|
|
119
|
+
theme overrides flow through. Width breakpoints are engine constants
|
|
120
|
+
(``_TINY_MAX``, ``_NARROW_MAX``, ``_WIDE_MIN``).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
width: Pixel width of the card/chart.
|
|
124
|
+
resolved_chart_style: Active ``MergedChartsStyle`` for the face.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Integer offset to add to the base heading level before indexing ``sizes``.
|
|
128
|
+
"""
|
|
129
|
+
wo = resolved_chart_style.title.width_offsets
|
|
130
|
+
if width >= _WIDE_MIN:
|
|
131
|
+
return wo.wide
|
|
132
|
+
if width >= _NARROW_MAX:
|
|
133
|
+
return wo.medium
|
|
134
|
+
if width >= _TINY_MAX:
|
|
135
|
+
return wo.narrow
|
|
136
|
+
return wo.tiny
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_prose(text: str) -> bool:
|
|
140
|
+
"""Return True if *text* qualifies as prose.
|
|
141
|
+
|
|
142
|
+
Prose is defined as body text with at least ``_PROSE_WORD_THRESHOLD`` words.
|
|
143
|
+
Prose content and its surrounding title render in serif at any card width.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
text: Raw text content (Markdown or plain).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
``True`` when the word count is ≥ 100.
|
|
150
|
+
"""
|
|
151
|
+
return len(text.split()) >= _PROSE_WORD_THRESHOLD
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def chart_title_spec(
|
|
155
|
+
width: float,
|
|
156
|
+
*,
|
|
157
|
+
level: int,
|
|
158
|
+
resolved_chart_style: MergedChartsStyle,
|
|
159
|
+
use_title_family: bool | None = None,
|
|
160
|
+
) -> tuple[int, int | str, str]:
|
|
161
|
+
"""Return ``(font_size, font_weight, font_family)`` for a chart title.
|
|
162
|
+
|
|
163
|
+
Computes ``effective_level = clamp(level + width_offset(width), 1, len(sizes))``
|
|
164
|
+
then returns ``sizes[effective_level - 1]``.
|
|
165
|
+
|
|
166
|
+
Reads from the *active* resolved style passed in — not from
|
|
167
|
+
``get_config()`` (the global default). That distinction is the whole point:
|
|
168
|
+
pre-fix the function was theme-blind and always returned the default
|
|
169
|
+
theme's families regardless of which theme the face actually used.
|
|
170
|
+
|
|
171
|
+
The resolved style has emoji families already baked into every
|
|
172
|
+
``FontStyle.family`` by ``resolve_style``'s ``_append_emoji_family`` pass,
|
|
173
|
+
so no further emoji handling is needed here.
|
|
174
|
+
|
|
175
|
+
Level offsets per tier come from
|
|
176
|
+
``resolved_chart_style.title.width_offsets.{tiny|narrow|medium|wide}``.
|
|
177
|
+
Font sizes come from ``resolved_chart_style.title.sizes`` (the H1–H6 ramp).
|
|
178
|
+
|
|
179
|
+
Family resolution by width tier:
|
|
180
|
+
- tiny + narrow → ``resolved_chart_style.font_family`` (body family — sans on
|
|
181
|
+
every shipped theme)
|
|
182
|
+
- medium + wide → ``resolved_chart_style.title.font.family`` (title-slot
|
|
183
|
+
family — sans on ``stark``, serif on the shipped ``default``/``cream``),
|
|
184
|
+
falling back to the body family when the title slot is unset.
|
|
185
|
+
|
|
186
|
+
Font weight comes from ``resolved_chart_style.title.font.weight`` for all
|
|
187
|
+
widths except tiny, which floors at 600 for legibility at small sizes.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
width: Pixel width of the chart card.
|
|
191
|
+
level: Base heading level for this title (typically ``face.level + 1``
|
|
192
|
+
for chart titles).
|
|
193
|
+
resolved_chart_style: Active ``MergedChartsStyle`` for the face. Produced
|
|
194
|
+
upstream by ``resolve_style(face_style).charts`` or
|
|
195
|
+
``build_resolved_style(...).charts`` so face-local and
|
|
196
|
+
chart-local style patches are already merged in.
|
|
197
|
+
use_title_family: ``True`` forces the title-slot family even on
|
|
198
|
+
narrow/tiny cards (so prose-tagged titles use serif at every
|
|
199
|
+
width on themes that define a serif title slot). ``False``
|
|
200
|
+
forces the body family at every width. ``None`` uses the
|
|
201
|
+
width default (title-slot family at medium/wide, body family
|
|
202
|
+
at narrow/tiny).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
``(font_size_px, css_font_weight, css_font_family_string)``
|
|
206
|
+
"""
|
|
207
|
+
sizes = resolved_chart_style.title.sizes
|
|
208
|
+
offset = _width_offset(width, resolved_chart_style)
|
|
209
|
+
effective_level = max(1, min(level + offset, len(sizes)))
|
|
210
|
+
font_size = int(sizes[effective_level - 1])
|
|
211
|
+
|
|
212
|
+
weight: int | str = (
|
|
213
|
+
_TINY_WEIGHT
|
|
214
|
+
if width < _TINY_MAX
|
|
215
|
+
else _coerce_weight(resolved_chart_style.title.font.weight or 500)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
pick_title = (
|
|
219
|
+
(width >= _NARROW_MAX) if use_title_family is None else use_title_family
|
|
220
|
+
)
|
|
221
|
+
body = resolved_chart_style.font_family
|
|
222
|
+
assert body is not None, "resolved_chart_style.font_family must be populated"
|
|
223
|
+
title_family = resolved_chart_style.title.font.family
|
|
224
|
+
family = (title_family or body) if pick_title else body
|
|
225
|
+
return font_size, weight, family
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def face_title_spec(
|
|
229
|
+
width: float,
|
|
230
|
+
*,
|
|
231
|
+
level: int,
|
|
232
|
+
) -> tuple[int, int | str, str]:
|
|
233
|
+
"""Return ``(font_size, font_weight, font_family)`` for a face/page title.
|
|
234
|
+
|
|
235
|
+
Same logic as ``chart_title_spec`` but:
|
|
236
|
+
- The width used for offset computation is clamped to ``_TINY_MAX`` — a page
|
|
237
|
+
header should not shrink below the narrow tier even on a narrow container.
|
|
238
|
+
- Always returns sans (Inter) regardless of width.
|
|
239
|
+
|
|
240
|
+
Note: callers that render prose faces override the returned family to serif
|
|
241
|
+
by passing ``font_family=config.style.title.font.family`` to
|
|
242
|
+
``get_compact_style``. The function itself is family-agnostic; the prose
|
|
243
|
+
exception lives in the render layer (see ``render_title`` in
|
|
244
|
+
``render/svg_utils.py``).
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
width: Pixel width of the face/board.
|
|
248
|
+
level: Heading level for this face title (``face.level``).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
``(font_size_px, css_font_weight, css_font_family_string)``
|
|
252
|
+
"""
|
|
253
|
+
from dataface.core.compile.config import get_config
|
|
254
|
+
from dataface.core.compile.models.style.merged import apply_emoji_to_family
|
|
255
|
+
|
|
256
|
+
cfg = get_config()
|
|
257
|
+
sizes = cfg.style.title.sizes
|
|
258
|
+
wo = cfg.style.title.width_offsets
|
|
259
|
+
# Clamp tiny → narrow: face headers don't shrink below the narrow tier.
|
|
260
|
+
effective_width = max(width, _TINY_MAX)
|
|
261
|
+
if effective_width >= _WIDE_MIN:
|
|
262
|
+
offset = wo.wide
|
|
263
|
+
elif effective_width >= _NARROW_MAX:
|
|
264
|
+
offset = wo.medium
|
|
265
|
+
else:
|
|
266
|
+
offset = wo.narrow
|
|
267
|
+
effective_level = max(1, min(level + offset, len(sizes)))
|
|
268
|
+
font_size = int(sizes[effective_level - 1])
|
|
269
|
+
|
|
270
|
+
weight = _coerce_weight(cfg.style.title.font.weight or 500)
|
|
271
|
+
_root = cfg.style.font.family
|
|
272
|
+
assert _root is not None, "style.font.family must be configured"
|
|
273
|
+
_family = apply_emoji_to_family(_root, cfg.style.font.emoji)
|
|
274
|
+
return font_size, weight, _family
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def face_title_markdown(
|
|
278
|
+
title: str, width: float, *, level: int = 1
|
|
279
|
+
) -> tuple[str, float, int | str]:
|
|
280
|
+
"""Return ``(markdown, h1_size, font_weight)`` for a face title.
|
|
281
|
+
|
|
282
|
+
Formats the title as an h1 heading (``# title``). Callers pass ``h1_size``
|
|
283
|
+
to ``get_compact_style(h1_size=…)`` so the h1 renders at the exact pixel
|
|
284
|
+
size, and ``font_weight`` to ``heading_font_weight`` so the DFT weight tier
|
|
285
|
+
is applied. The pixel size comes from ``style.title.sizes`` indexed by the
|
|
286
|
+
effective level.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
title: Title text (Jinja already resolved by the caller).
|
|
290
|
+
width: Pixel width of the face/board (drives width-offset selection).
|
|
291
|
+
level: Heading level for this face's title (``face.level``; default 1
|
|
292
|
+
for root faces).
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
``(markdown_string, h1_size, font_weight)`` — pass to
|
|
296
|
+
``get_compact_style(h1_size=…, heading_font_weight=…)``.
|
|
297
|
+
"""
|
|
298
|
+
font_size, weight, _family = face_title_spec(width, level=level)
|
|
299
|
+
return f"# {title}", float(font_size), weight
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Schema validation module.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE (Step 2 of 4)
|
|
4
|
+
Purpose: Validate AuthoredFace structure and cross-references.
|
|
5
|
+
|
|
6
|
+
Entry Points:
|
|
7
|
+
- validate_face(face: AuthoredFace) -> List[ValidationError]
|
|
8
|
+
|
|
9
|
+
Inputs:
|
|
10
|
+
- AuthoredFace (from parser)
|
|
11
|
+
|
|
12
|
+
Outputs:
|
|
13
|
+
- List[ValidationError] (empty if valid)
|
|
14
|
+
|
|
15
|
+
This validator focuses on semantic validation that Pydantic cannot handle:
|
|
16
|
+
- Cross-reference validation (chart → query)
|
|
17
|
+
- Layout item references (layout → chart)
|
|
18
|
+
- Consistency checks between related fields
|
|
19
|
+
|
|
20
|
+
Pydantic already handles:
|
|
21
|
+
- Type validation
|
|
22
|
+
- Required fields
|
|
23
|
+
- Field constraints
|
|
24
|
+
- Nested validation
|
|
25
|
+
|
|
26
|
+
Dependencies:
|
|
27
|
+
- .types (AuthoredFace, Chart)
|
|
28
|
+
- .errors (ValidationError)
|
|
29
|
+
|
|
30
|
+
See also:
|
|
31
|
+
- compile/parser.py: Previous step
|
|
32
|
+
- compile/normalizer.py: Next step
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from dataface.core.compile.errors import ValidationError
|
|
39
|
+
from dataface.core.compile.models.chart.authored import (
|
|
40
|
+
CalloutChart,
|
|
41
|
+
_SharedChartFields,
|
|
42
|
+
)
|
|
43
|
+
from dataface.core.compile.models.face.authored import AuthoredFace
|
|
44
|
+
from dataface.core.compile.models.refs import ChartRef
|
|
45
|
+
from dataface.core.compile.parser import looks_like_sql
|
|
46
|
+
from dataface.core.errors.codes_compile import DF_COMPILE_UNKNOWN_QUERY
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_face(face: AuthoredFace) -> list[ValidationError]:
|
|
50
|
+
"""Validate a AuthoredFace structure and cross-references.
|
|
51
|
+
|
|
52
|
+
Stage: COMPILE (Step 2 of 4: Validation)
|
|
53
|
+
|
|
54
|
+
Performs semantic validation beyond what Pydantic provides:
|
|
55
|
+
- Chart → Query references exist
|
|
56
|
+
- Layout items reference existing charts
|
|
57
|
+
|
|
58
|
+
Note: Pydantic already validates types, required fields, and nested
|
|
59
|
+
structures during parsing. This function adds cross-reference validation.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
face: Parsed AuthoredFace to validate
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of ValidationError objects (empty if valid)
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> face = parse_yaml(yaml_content)
|
|
69
|
+
>>> errors = validate_face(face)
|
|
70
|
+
>>> if errors:
|
|
71
|
+
... for e in errors:
|
|
72
|
+
... print(f"Error: {e}")
|
|
73
|
+
"""
|
|
74
|
+
errors: list[ValidationError] = []
|
|
75
|
+
|
|
76
|
+
# Collect available names
|
|
77
|
+
query_names = set((face.queries or {}).keys())
|
|
78
|
+
chart_names = set((face.charts or {}).keys())
|
|
79
|
+
|
|
80
|
+
# Validate chart → query references
|
|
81
|
+
errors.extend(_validate_chart_query_references(face.charts or {}, query_names))
|
|
82
|
+
|
|
83
|
+
# Validate layout → chart references
|
|
84
|
+
errors.extend(_validate_layout_references(face, chart_names, query_names))
|
|
85
|
+
|
|
86
|
+
return errors
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_chart_query_references(
|
|
90
|
+
charts: Mapping[str, _SharedChartFields | CalloutChart | ChartRef],
|
|
91
|
+
query_names: set[str],
|
|
92
|
+
) -> list[ValidationError]:
|
|
93
|
+
"""Validate that charts reference existing queries.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
charts: Chart definitions (chart patch instance or ChartRef cross-file references)
|
|
97
|
+
query_names: Available query names
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of validation errors
|
|
101
|
+
"""
|
|
102
|
+
errors: list[ValidationError] = []
|
|
103
|
+
|
|
104
|
+
for chart_name, chart in charts.items():
|
|
105
|
+
# Skip cross-file chart references — resolved during normalization
|
|
106
|
+
if isinstance(chart, ChartRef):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# CalloutChart is minimal — no query field by design.
|
|
110
|
+
if isinstance(chart, CalloutChart):
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
query_ref = chart.query
|
|
114
|
+
|
|
115
|
+
# Skip validation for blank charts (no query)
|
|
116
|
+
if query_ref is None:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Skip validation for inline query definitions (dicts)
|
|
120
|
+
# These are handled during normalization
|
|
121
|
+
if isinstance(query_ref, dict):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Strip "queries." prefix if present
|
|
125
|
+
if query_ref.startswith("queries."):
|
|
126
|
+
query_ref = query_ref[8:]
|
|
127
|
+
|
|
128
|
+
# Cross-file references (e.g., "other_file.queries.name") are resolved later
|
|
129
|
+
if "." in query_ref:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if query_ref not in query_names:
|
|
133
|
+
if looks_like_sql(query_ref):
|
|
134
|
+
# Bare SQL string — normalizer will promote it to an inline query; skip here.
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
errors.append(
|
|
138
|
+
ValidationError.from_code(
|
|
139
|
+
DF_COMPILE_UNKNOWN_QUERY,
|
|
140
|
+
chart_name=chart_name,
|
|
141
|
+
query_name=query_ref,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return errors
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_layout_references(
|
|
149
|
+
face: AuthoredFace,
|
|
150
|
+
chart_names: set[str],
|
|
151
|
+
query_names: set[str],
|
|
152
|
+
) -> list[ValidationError]:
|
|
153
|
+
"""Validate that layout items reference existing charts.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
face: AuthoredFace to validate
|
|
157
|
+
chart_names: Available chart names
|
|
158
|
+
query_names: Available query names
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of validation errors
|
|
162
|
+
"""
|
|
163
|
+
errors: list[ValidationError] = []
|
|
164
|
+
|
|
165
|
+
# Collect all layout items
|
|
166
|
+
layout_items = _get_layout_items(face)
|
|
167
|
+
|
|
168
|
+
for location, item in layout_items:
|
|
169
|
+
if isinstance(item, str):
|
|
170
|
+
# String reference - must be a chart name (or special reference)
|
|
171
|
+
if _is_special_reference(item):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if item not in chart_names:
|
|
175
|
+
errors.append(
|
|
176
|
+
ValidationError(
|
|
177
|
+
f"Layout references unknown chart '{item}'", location=location
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
elif isinstance(item, (dict, _SharedChartFields)):
|
|
182
|
+
# Inline chart - validate its query reference
|
|
183
|
+
query_ref = item.get("query") if isinstance(item, dict) else item.query
|
|
184
|
+
|
|
185
|
+
# Skip validation for blank charts (no query)
|
|
186
|
+
if query_ref is None:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Skip validation for inline query definitions (dicts)
|
|
190
|
+
# These are handled during normalization
|
|
191
|
+
if isinstance(query_ref, dict):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
if query_ref.startswith("queries."):
|
|
195
|
+
query_ref = query_ref[8:]
|
|
196
|
+
|
|
197
|
+
# Skip cross-file references
|
|
198
|
+
if "." not in query_ref and query_ref not in query_names:
|
|
199
|
+
if looks_like_sql(query_ref):
|
|
200
|
+
continue
|
|
201
|
+
errors.append(
|
|
202
|
+
ValidationError.from_code(
|
|
203
|
+
DF_COMPILE_UNKNOWN_QUERY,
|
|
204
|
+
chart_name=location,
|
|
205
|
+
query_name=query_ref,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return errors
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_layout_items(face: AuthoredFace) -> list[tuple[str, Any]]:
|
|
213
|
+
"""Extract all layout items with their locations.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
face: AuthoredFace to extract items from
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of (location, item) tuples
|
|
220
|
+
"""
|
|
221
|
+
items: list[tuple[str, Any]] = []
|
|
222
|
+
|
|
223
|
+
if face.rows:
|
|
224
|
+
for idx, item in enumerate(face.rows):
|
|
225
|
+
items.append((f"rows[{idx}]", item))
|
|
226
|
+
items.extend(_get_nested_items(item, f"rows[{idx}]"))
|
|
227
|
+
|
|
228
|
+
if face.cols:
|
|
229
|
+
for idx, item in enumerate(face.cols):
|
|
230
|
+
items.append((f"cols[{idx}]", item))
|
|
231
|
+
items.extend(_get_nested_items(item, f"cols[{idx}]"))
|
|
232
|
+
|
|
233
|
+
if face.grid:
|
|
234
|
+
for idx, grid_item in enumerate(face.grid.items):
|
|
235
|
+
items.append((f"grid.items[{idx}]", grid_item.item))
|
|
236
|
+
|
|
237
|
+
if face.tabs:
|
|
238
|
+
for idx, tab_item in enumerate(face.tabs.items):
|
|
239
|
+
if tab_item.rows:
|
|
240
|
+
for i, item in enumerate(tab_item.rows):
|
|
241
|
+
items.append((f"tabs.items[{idx}].rows[{i}]", item))
|
|
242
|
+
if tab_item.cols:
|
|
243
|
+
for i, item in enumerate(tab_item.cols):
|
|
244
|
+
items.append((f"tabs.items[{idx}].cols[{i}]", item))
|
|
245
|
+
|
|
246
|
+
return items
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_nested_items(item: Any, parent_location: str) -> list[tuple[str, Any]]:
|
|
250
|
+
"""Extract items from nested face structures.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
item: Item that might be a nested face
|
|
254
|
+
parent_location: Parent location string
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of (location, item) tuples from nested structures
|
|
258
|
+
"""
|
|
259
|
+
items: list[tuple[str, Any]] = []
|
|
260
|
+
|
|
261
|
+
if isinstance(item, dict):
|
|
262
|
+
# If nested face has its own charts, skip validation here -
|
|
263
|
+
# the nested face's layout items will reference its own charts,
|
|
264
|
+
# which will be validated during normalization
|
|
265
|
+
if "charts" in item:
|
|
266
|
+
return items
|
|
267
|
+
|
|
268
|
+
# Nested face without its own charts - extract its layout items
|
|
269
|
+
if "rows" in item:
|
|
270
|
+
for idx, nested in enumerate(item["rows"]):
|
|
271
|
+
items.append((f"{parent_location}.rows[{idx}]", nested))
|
|
272
|
+
items.extend(
|
|
273
|
+
_get_nested_items(nested, f"{parent_location}.rows[{idx}]")
|
|
274
|
+
)
|
|
275
|
+
if "cols" in item:
|
|
276
|
+
for idx, nested in enumerate(item["cols"]):
|
|
277
|
+
items.append((f"{parent_location}.cols[{idx}]", nested))
|
|
278
|
+
items.extend(
|
|
279
|
+
_get_nested_items(nested, f"{parent_location}.cols[{idx}]")
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return items
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _is_special_reference(ref: str) -> bool:
|
|
286
|
+
"""Check if a reference is a special type (partial, remote, etc.).
|
|
287
|
+
|
|
288
|
+
Special references are validated during normalization, not here.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
ref: Reference string to check
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True if this is a special reference type
|
|
295
|
+
"""
|
|
296
|
+
# Partial references start with underscore
|
|
297
|
+
if ref.startswith("_"):
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
# Cross-file references contain dots or slashes
|
|
301
|
+
return bool("." in ref or "/" in ref)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Variable utilities for Dataface.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE (utilities used by both execute and render)
|
|
4
|
+
Purpose: Shared variable processing functions.
|
|
5
|
+
|
|
6
|
+
This module provides utilities for working with variables that are needed
|
|
7
|
+
by both the execute and render stages.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from dataface.core.compile.models.face.compiled import VariableValues
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_variable_json_strings(variables: VariableValues) -> VariableValues:
|
|
17
|
+
"""Parse JSON strings in variables (e.g., from URL parameters).
|
|
18
|
+
|
|
19
|
+
When variables come from URL query parameters, complex values like
|
|
20
|
+
date ranges are often serialized as JSON strings. This function
|
|
21
|
+
parses them back to their native Python types.
|
|
22
|
+
|
|
23
|
+
This function is idempotent - calling it on already-parsed values
|
|
24
|
+
returns them unchanged.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
variables: Variable values that may contain JSON strings
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Variables with JSON strings parsed to their actual types
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> parse_variable_json_strings({"date_range": '["2024-01-01", "2024-01-31"]'})
|
|
34
|
+
{"date_range": ["2024-01-01", "2024-01-31"]}
|
|
35
|
+
"""
|
|
36
|
+
parsed: dict[str, Any] = {}
|
|
37
|
+
for key, value in variables.items():
|
|
38
|
+
if isinstance(value, str):
|
|
39
|
+
# Try to parse as JSON (for date ranges, arrays, objects)
|
|
40
|
+
if value.startswith("[") or value.startswith("{"):
|
|
41
|
+
try:
|
|
42
|
+
parsed[key] = json.loads(value)
|
|
43
|
+
except (json.JSONDecodeError, ValueError):
|
|
44
|
+
# Not valid JSON, keep as string
|
|
45
|
+
parsed[key] = value
|
|
46
|
+
else:
|
|
47
|
+
parsed[key] = value
|
|
48
|
+
else:
|
|
49
|
+
parsed[key] = value
|
|
50
|
+
return parsed
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["parse_variable_json_strings"]
|