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,76 @@
|
|
|
1
|
+
"""Detector: Y_ENCODING_MOSTLY_NULL.
|
|
2
|
+
|
|
3
|
+
Fires on any chart where the y-encoding field is >50% NULL in the query
|
|
4
|
+
result rows. Mostly-empty visual marks with no explanation usually indicate
|
|
5
|
+
a broken join or a nullable source column.
|
|
6
|
+
|
|
7
|
+
Detection rule:
|
|
8
|
+
NULL count / total rows > 0.5 (strictly greater-than)
|
|
9
|
+
|
|
10
|
+
Multi-y charts: the rule is evaluated per column; one warning is emitted per
|
|
11
|
+
offending column so the consumer can identify exactly which series is broken.
|
|
12
|
+
|
|
13
|
+
Missing-key semantics: a row that does not contain the y column key at all is
|
|
14
|
+
counted as NULL. This matches the rendering behavior — Vega-Lite treats missing
|
|
15
|
+
values the same as explicit null.
|
|
16
|
+
|
|
17
|
+
NULL-only (not NULL+zero): zero is a valid measurement (e.g. count of refunds).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataface.core.render.warnings.base import RenderWarning, WarningContext
|
|
23
|
+
|
|
24
|
+
CODE = "Y_ENCODING_MOSTLY_NULL"
|
|
25
|
+
|
|
26
|
+
# Strictly-greater-than threshold: null_count / row_count must exceed this.
|
|
27
|
+
_NULL_FRACTION_THRESHOLD = 0.5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def detect(ctx: WarningContext) -> list[RenderWarning]:
|
|
31
|
+
"""Return one RenderWarning per y field that is >50% NULL across result rows."""
|
|
32
|
+
warnings: list[RenderWarning] = []
|
|
33
|
+
|
|
34
|
+
for chart_id, chart in ctx.face_spec.charts.items():
|
|
35
|
+
# Skip charts with no y encoding (kpi, text, markdown, callout, etc.).
|
|
36
|
+
if not chart.y:
|
|
37
|
+
continue
|
|
38
|
+
# A chart absent from chart_results failed execution; nothing to analyse.
|
|
39
|
+
if chart_id not in ctx.chart_results:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
rows = ctx.chart_results[chart_id]
|
|
43
|
+
# Zero-row results are handled by the QUERY_RETURNED_ZERO_ROWS detector.
|
|
44
|
+
if not rows:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Normalise single-string y to a list so the loop below handles both cases.
|
|
48
|
+
# chart.y is str | list[str] here (None guarded above).
|
|
49
|
+
raw_y = chart.y
|
|
50
|
+
y_fields: list[str] = raw_y if isinstance(raw_y, list) else [raw_y]
|
|
51
|
+
total = len(rows)
|
|
52
|
+
|
|
53
|
+
for y_field in y_fields:
|
|
54
|
+
# Rows missing the key entirely are counted as NULL.
|
|
55
|
+
null_count = sum(1 for row in rows if row.get(y_field) is None)
|
|
56
|
+
if null_count / total > _NULL_FRACTION_THRESHOLD:
|
|
57
|
+
warnings.append(
|
|
58
|
+
RenderWarning(
|
|
59
|
+
code=CODE,
|
|
60
|
+
chart=chart_id,
|
|
61
|
+
field=y_field,
|
|
62
|
+
message=(
|
|
63
|
+
f"Y-encoding field '{y_field}' is NULL in "
|
|
64
|
+
f"{null_count}/{total} rows ({null_count / total:.0%}). "
|
|
65
|
+
"Mostly-empty visual encoding usually indicates a broken "
|
|
66
|
+
"join or a nullable source column."
|
|
67
|
+
),
|
|
68
|
+
fix=(
|
|
69
|
+
"Check the join condition producing this field and verify "
|
|
70
|
+
"source field nullability. Consider a COALESCE or filter "
|
|
71
|
+
"to remove rows where the metric is undefined."
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return warnings
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""YAML render output format — resolved dataface YAML.
|
|
2
|
+
|
|
3
|
+
Walks the layout tree via face_to_dict (shared with JSON/text formats),
|
|
4
|
+
then maps the resolved structure back to valid dataface YAML schema.
|
|
5
|
+
The output can be re-compiled and rendered without a database connection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from datetime import date, datetime
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from dataface.core.compile.models.face.compiled import Face, VariableValues
|
|
17
|
+
from dataface.core.errors import StructuredError
|
|
18
|
+
from dataface.core.execute.executor import Executor
|
|
19
|
+
from dataface.core.render.json_format import face_to_dict
|
|
20
|
+
|
|
21
|
+
# Fields to emit on chart definitions (order matters for readability)
|
|
22
|
+
_CHART_FIELDS = (
|
|
23
|
+
"type",
|
|
24
|
+
"query",
|
|
25
|
+
"title",
|
|
26
|
+
# ``label`` is the KPI-only label slot (sibling of ``value``); the
|
|
27
|
+
# chart-type-aware gate keeps it off other chart types.
|
|
28
|
+
"label",
|
|
29
|
+
"description",
|
|
30
|
+
"x",
|
|
31
|
+
"y",
|
|
32
|
+
"color",
|
|
33
|
+
"size",
|
|
34
|
+
"shape",
|
|
35
|
+
"theta",
|
|
36
|
+
"format",
|
|
37
|
+
"x_label",
|
|
38
|
+
"y_label",
|
|
39
|
+
"geo",
|
|
40
|
+
"geo_source",
|
|
41
|
+
"lookup",
|
|
42
|
+
"value",
|
|
43
|
+
# KPI quantitative-text-object fields (siblings of `value`).
|
|
44
|
+
"support",
|
|
45
|
+
"projection",
|
|
46
|
+
"latitude",
|
|
47
|
+
"longitude",
|
|
48
|
+
"basemap",
|
|
49
|
+
"sort",
|
|
50
|
+
"style",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _clean_value(v: Any) -> Any:
|
|
55
|
+
"""Convert non-YAML-native types to serializable equivalents."""
|
|
56
|
+
if isinstance(v, BaseModel):
|
|
57
|
+
return _clean_value(v.model_dump(exclude_none=True))
|
|
58
|
+
if isinstance(v, Mapping):
|
|
59
|
+
return {k: _clean_value(val) for k, val in v.items()}
|
|
60
|
+
if isinstance(v, list):
|
|
61
|
+
return [_clean_value(item) for item in v]
|
|
62
|
+
if isinstance(v, Decimal):
|
|
63
|
+
# Use int if lossless, else float
|
|
64
|
+
if v == v.to_integral_value():
|
|
65
|
+
return int(v)
|
|
66
|
+
return float(v)
|
|
67
|
+
if isinstance(v, datetime):
|
|
68
|
+
return v.isoformat()
|
|
69
|
+
if isinstance(v, date):
|
|
70
|
+
return v.isoformat()
|
|
71
|
+
if isinstance(v, set):
|
|
72
|
+
return sorted(v)
|
|
73
|
+
return v
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _chart_to_yaml_dict(item: dict[str, Any], query_name: str) -> dict[str, Any]:
|
|
77
|
+
"""Convert a resolved chart item to a dataface YAML chart definition."""
|
|
78
|
+
chart = item["chart"]
|
|
79
|
+
d: dict[str, Any] = {}
|
|
80
|
+
|
|
81
|
+
# Map chart_type → type
|
|
82
|
+
if chart.get("chart_type"):
|
|
83
|
+
d["type"] = chart["chart_type"]
|
|
84
|
+
|
|
85
|
+
# Reference the query by name
|
|
86
|
+
d["query"] = query_name
|
|
87
|
+
|
|
88
|
+
# Copy known chart fields. ``title`` and ``label`` default to ``""`` on
|
|
89
|
+
# Chart; an empty string would emit `title: ''` / `label: ''`
|
|
90
|
+
# next to the real authored slot, so treat empty strings as unauthored.
|
|
91
|
+
for field in _CHART_FIELDS:
|
|
92
|
+
if field in ("type", "query"):
|
|
93
|
+
continue # already handled
|
|
94
|
+
val = chart.get(field)
|
|
95
|
+
if val is None:
|
|
96
|
+
continue
|
|
97
|
+
if field in ("title", "label") and val == "":
|
|
98
|
+
continue
|
|
99
|
+
d[field] = _clean_value(val)
|
|
100
|
+
|
|
101
|
+
return d
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _face_to_yaml_dict(
|
|
105
|
+
face_dict: dict[str, Any],
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Convert a face_to_dict result into valid dataface YAML schema."""
|
|
108
|
+
result: dict[str, Any] = {}
|
|
109
|
+
|
|
110
|
+
if face_dict.get("title"):
|
|
111
|
+
result["title"] = face_dict["title"]
|
|
112
|
+
|
|
113
|
+
# Collect queries and charts from items
|
|
114
|
+
queries: dict[str, Any] = {}
|
|
115
|
+
charts: dict[str, Any] = {}
|
|
116
|
+
layout_refs: list[Any] = []
|
|
117
|
+
|
|
118
|
+
for item in face_dict.get("items", []):
|
|
119
|
+
if item["type"] == "chart":
|
|
120
|
+
chart = item["chart"]
|
|
121
|
+
chart_id = chart.get("id", "chart")
|
|
122
|
+
query_name = chart.get("query_name") or chart_id
|
|
123
|
+
|
|
124
|
+
# Query: inline data via rows
|
|
125
|
+
data = item.get("data", [])
|
|
126
|
+
queries[query_name] = {"type": "values", "rows": _clean_value(data)}
|
|
127
|
+
|
|
128
|
+
# Chart definition
|
|
129
|
+
charts[chart_id] = _chart_to_yaml_dict(item, query_name)
|
|
130
|
+
layout_refs.append(chart_id)
|
|
131
|
+
|
|
132
|
+
elif item["type"] == "face":
|
|
133
|
+
# Nested face — inline as a sub-face in layout
|
|
134
|
+
nested = _face_to_yaml_dict(item["face"])
|
|
135
|
+
layout_refs.append(nested)
|
|
136
|
+
|
|
137
|
+
if queries:
|
|
138
|
+
result["queries"] = queries
|
|
139
|
+
if charts:
|
|
140
|
+
result["charts"] = charts
|
|
141
|
+
if layout_refs:
|
|
142
|
+
result["rows"] = layout_refs
|
|
143
|
+
|
|
144
|
+
# Variables
|
|
145
|
+
if face_dict.get("variables"):
|
|
146
|
+
result["variables"] = _clean_value(face_dict["variables"])
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def render_face_yaml(
|
|
152
|
+
face: Face,
|
|
153
|
+
executor: Executor,
|
|
154
|
+
variables: VariableValues,
|
|
155
|
+
error_collector: list[StructuredError] | None = None,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Render a compiled face to resolved dataface YAML.
|
|
158
|
+
|
|
159
|
+
Walks the layout tree, executes queries, resolves charts, and produces
|
|
160
|
+
valid dataface YAML where queries use `values:` with inline data rows.
|
|
161
|
+
The output can be fed back into `compile()` as valid input.
|
|
162
|
+
"""
|
|
163
|
+
face_dict = face_to_dict(face, executor, variables, error_collector)
|
|
164
|
+
yaml_dict = _face_to_yaml_dict(face_dict)
|
|
165
|
+
return yaml.dump(
|
|
166
|
+
yaml_dict, default_flow_style=False, sort_keys=False, allow_unicode=True
|
|
167
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Resolution layer: Face + Config → MergedFace.
|
|
2
|
+
|
|
3
|
+
Stage: RESOLVE (between COMPILE and RENDER)
|
|
4
|
+
|
|
5
|
+
Architecture (ADR-008):
|
|
6
|
+
Compile → Resolve → Render (executor, data-aware)
|
|
7
|
+
|
|
8
|
+
resolve_face() reads face.resolved_style and bakes it into the returned
|
|
9
|
+
MergedFace. Call sync_face_resolved_style(face) before resolve_face()
|
|
10
|
+
when the face has been mutated after compile. It does not access
|
|
11
|
+
databases or pre-render charts.
|
|
12
|
+
|
|
13
|
+
Import boundary:
|
|
14
|
+
Allowed: compile/* (types, config, sizing, style)
|
|
15
|
+
Allowed: render/chart/presentation.py for compile_effective_vega_config
|
|
16
|
+
(pure function — no executor, no data — lives in render/ only for
|
|
17
|
+
historical reasons)
|
|
18
|
+
Forbidden: execute/* (no Executor here)
|
|
19
|
+
Forbidden: render/layout_sizing.py (data-aware sizing is render-phase)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from dataface.core.compile.models.face.compiled import (
|
|
27
|
+
Face,
|
|
28
|
+
MergedFace,
|
|
29
|
+
ResolvedLayout,
|
|
30
|
+
ResolvedLayoutItem,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from dataface.core.compile.models.config import Config
|
|
35
|
+
from dataface.core.compile.models.face.compiled import Layout
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_resolved_layout(layout: Layout, gap: float = 0.0) -> ResolvedLayout:
|
|
39
|
+
"""Wrap a compile-time Layout in the frozen ResolvedLayout type.
|
|
40
|
+
|
|
41
|
+
Dimensions come from whatever static sizing the compile/normalizer phase
|
|
42
|
+
produced. If a field is falsy (no static estimate), 0.0 is used as a
|
|
43
|
+
sentinel so the render phase knows to compute the real value.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
layout: Compiled layout tree.
|
|
47
|
+
gap: Config-driven gap between items (baked from resolve_face).
|
|
48
|
+
"""
|
|
49
|
+
items = tuple(
|
|
50
|
+
ResolvedLayoutItem(
|
|
51
|
+
type=item.type,
|
|
52
|
+
chart=item.chart,
|
|
53
|
+
face=_build_resolved_nested_face(item.face) if item.face else None,
|
|
54
|
+
x=item.x or 0.0,
|
|
55
|
+
y=item.y or 0.0,
|
|
56
|
+
width=item.width or 0.0,
|
|
57
|
+
height=item.height or 0.0,
|
|
58
|
+
details_variable=getattr(item, "details_variable", None),
|
|
59
|
+
details_summary=getattr(item, "details_summary", None),
|
|
60
|
+
details_expanded_summary=getattr(item, "details_expanded_summary", None),
|
|
61
|
+
description=getattr(item, "description", None),
|
|
62
|
+
visible=item.visible,
|
|
63
|
+
)
|
|
64
|
+
for item in (layout.items or [])
|
|
65
|
+
)
|
|
66
|
+
return ResolvedLayout(
|
|
67
|
+
type=layout.type,
|
|
68
|
+
items=items,
|
|
69
|
+
width=layout.width or 0.0,
|
|
70
|
+
height=layout.height or 0.0,
|
|
71
|
+
content_width=layout.content_width or 0.0,
|
|
72
|
+
content_height=layout.content_height or 0.0,
|
|
73
|
+
columns=getattr(layout, "columns", None),
|
|
74
|
+
gap=gap,
|
|
75
|
+
tab_titles=tuple(getattr(layout, "tab_titles", None) or ()),
|
|
76
|
+
tab_slugs=tuple(getattr(layout, "tab_slugs", None) or ()),
|
|
77
|
+
tab_variable=getattr(layout, "tab_variable", None),
|
|
78
|
+
default_tab=getattr(layout, "default_tab", None) or 0,
|
|
79
|
+
tab_position=getattr(layout, "tab_position", None),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_nested_face(face: Face) -> MergedFace:
|
|
84
|
+
"""Resolve a nested Face to MergedFace without global config.
|
|
85
|
+
|
|
86
|
+
For nested faces, board-level config (page_padding, card_padding, card_gap)
|
|
87
|
+
is None — those apply only to the root renderable face.
|
|
88
|
+
"""
|
|
89
|
+
return _build_resolved_nested_face(face)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_resolved_nested_face(face: Face) -> MergedFace:
|
|
93
|
+
"""Resolve a nested face using only static/compile-time information.
|
|
94
|
+
|
|
95
|
+
Board-level config (page_padding, card_padding, card_gap) are None —
|
|
96
|
+
those apply only to the root renderable face.
|
|
97
|
+
vega_config is compiled from face.theme so the render pass and sizing
|
|
98
|
+
pass use the same Vega-Lite config for nested charts.
|
|
99
|
+
Uses face.resolved_style as set by the compiler's propagation walk,
|
|
100
|
+
which correctly carries the parent→child semantic color cascade.
|
|
101
|
+
"""
|
|
102
|
+
from dataface.core.compile.vega_config import compile_effective_vega_config
|
|
103
|
+
|
|
104
|
+
return MergedFace(
|
|
105
|
+
id=face.id,
|
|
106
|
+
title=face.title,
|
|
107
|
+
description=face.description,
|
|
108
|
+
tags=tuple(face.tags),
|
|
109
|
+
text=face.text,
|
|
110
|
+
level=face.level,
|
|
111
|
+
style=face.resolved_style,
|
|
112
|
+
page_padding=None,
|
|
113
|
+
card_padding=None,
|
|
114
|
+
card_gap=None,
|
|
115
|
+
width=face.layout.width or 0.0,
|
|
116
|
+
height=face.layout.height or 0.0,
|
|
117
|
+
vega_config=compile_effective_vega_config(face.theme),
|
|
118
|
+
layout=_build_resolved_layout(face.layout),
|
|
119
|
+
charts=face.charts,
|
|
120
|
+
variables=face.variables,
|
|
121
|
+
queries=face.queries,
|
|
122
|
+
variable_defaults=face.variable_defaults,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resolve_face(
|
|
127
|
+
face: Face,
|
|
128
|
+
config: Config,
|
|
129
|
+
) -> MergedFace:
|
|
130
|
+
"""Resolve a compiled face into a render-ready object.
|
|
131
|
+
|
|
132
|
+
Reads face.resolved_style as-is; callers are responsible for calling
|
|
133
|
+
sync_face_resolved_style(face) first if the face was mutated after
|
|
134
|
+
compile. No database access. No chart pre-rendering. The render
|
|
135
|
+
phase applies data-aware sizing on top of the static estimates this
|
|
136
|
+
layer provides.
|
|
137
|
+
|
|
138
|
+
Board-level config fields (page_padding, card_padding, card_gap) are baked
|
|
139
|
+
for the root face. Nested faces carry None for those fields — they are not
|
|
140
|
+
independently renderable and do not own board-level layout config.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
face: Compiled face from the YAML → compile pipeline.
|
|
144
|
+
config: Global runtime config (provides board dimensions etc.).
|
|
145
|
+
variables: Active variable values (reserved for future static height
|
|
146
|
+
estimates that depend on variable-driven content; currently unused).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
MergedFace with baked config constants, merged style, resolved Vega
|
|
150
|
+
config, and static layout estimates. Width/height are static estimates;
|
|
151
|
+
the render phase will refine them with data-aware sizing.
|
|
152
|
+
"""
|
|
153
|
+
from dataface.core.compile.sizing import get_face_gap
|
|
154
|
+
from dataface.core.compile.vega_config import compile_effective_vega_config
|
|
155
|
+
|
|
156
|
+
# 1. Bake board config constants (root face only)
|
|
157
|
+
page_padding = float(config.style.board.margin)
|
|
158
|
+
card_padding = float(config.style.board.card_padding)
|
|
159
|
+
card_gap = float(config.style.board.card_gap) if face.card_gap else 0.0
|
|
160
|
+
layout_gap = get_face_gap(face) # reads config internally; canonical implementation
|
|
161
|
+
|
|
162
|
+
# 2. Static dimension estimates from the compile-time layout tree.
|
|
163
|
+
# face.layout.width/height are 0.0 when the normalizer has not yet run
|
|
164
|
+
# calculate_layout_height. Fall back to config defaults; render refines.
|
|
165
|
+
width = float(face.layout.width or config.style.board.width)
|
|
166
|
+
height = float(face.layout.height or config.style.board.default_height)
|
|
167
|
+
|
|
168
|
+
# 3. Resolve Vega-Lite config from theme name (pure, no data access)
|
|
169
|
+
vega_config = compile_effective_vega_config(face.theme)
|
|
170
|
+
|
|
171
|
+
# 4. Wrap the layout tree in the frozen ResolvedLayout type (with baked gap)
|
|
172
|
+
resolved_layout = _build_resolved_layout(face.layout, gap=layout_gap)
|
|
173
|
+
|
|
174
|
+
# 5. Use the face's resolved style. renderer.py calls sync_face_resolved_style
|
|
175
|
+
# before this point so face.resolved_style is up-to-date.
|
|
176
|
+
return MergedFace(
|
|
177
|
+
id=face.id,
|
|
178
|
+
title=face.title,
|
|
179
|
+
description=face.description,
|
|
180
|
+
tags=tuple(face.tags),
|
|
181
|
+
text=face.text,
|
|
182
|
+
level=face.level,
|
|
183
|
+
style=face.resolved_style,
|
|
184
|
+
page_padding=page_padding,
|
|
185
|
+
card_padding=card_padding,
|
|
186
|
+
card_gap=card_gap,
|
|
187
|
+
width=width,
|
|
188
|
+
height=height,
|
|
189
|
+
vega_config=vega_config,
|
|
190
|
+
layout=resolved_layout,
|
|
191
|
+
charts=face.charts,
|
|
192
|
+
variables=face.variables,
|
|
193
|
+
queries=face.queries,
|
|
194
|
+
variable_defaults=face.variable_defaults,
|
|
195
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Empty-table-list discoverability hints for the schema verb.
|
|
2
|
+
|
|
3
|
+
Pure helpers — no agent-api or Pydantic coupling. Called by
|
|
4
|
+
``dataface.agent_api.schema:schema()`` when a level-3 query returns no
|
|
5
|
+
tables so the agent can understand why and what to try next.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from dataface.core.execute.adapters import AdapterRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EmptyTableGuidance:
|
|
19
|
+
"""Typed guidance payload for an empty level-3 table list."""
|
|
20
|
+
|
|
21
|
+
message: str
|
|
22
|
+
warnings: list[str] = field(default_factory=list)
|
|
23
|
+
hints: list[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def empty_table_list_guidance(
|
|
27
|
+
registry: AdapterRegistry,
|
|
28
|
+
source: str | None,
|
|
29
|
+
*,
|
|
30
|
+
uses_direct_file_queries: Callable[[], bool],
|
|
31
|
+
surface: Literal["cli", "mcp"] = "mcp",
|
|
32
|
+
) -> EmptyTableGuidance:
|
|
33
|
+
"""Build discoverability guidance when no tables were found.
|
|
34
|
+
|
|
35
|
+
The schema verb errors on missing-source upstream, so ``source`` is always
|
|
36
|
+
a real string by the time callers reach this helper. ``uses_direct_file_queries``
|
|
37
|
+
is a callable so the (I/O-heavy) dashboard-walk is deferred until we
|
|
38
|
+
actually need its answer — it carries no dependency on ``agent_api`` here.
|
|
39
|
+
|
|
40
|
+
``surface`` controls whether hints use CLI verbs (``dft inspect``,
|
|
41
|
+
``dft search``, ``dft query``) or MCP tool names (``search_dashboards``,
|
|
42
|
+
``execute_query``). Default is ``"mcp"`` so existing MCP callers need no
|
|
43
|
+
change.
|
|
44
|
+
"""
|
|
45
|
+
configured_sources = registry.list_sql_sources()
|
|
46
|
+
selected_source = next(
|
|
47
|
+
(item for item in configured_sources if item["name"] == source),
|
|
48
|
+
None,
|
|
49
|
+
)
|
|
50
|
+
is_in_memory = False
|
|
51
|
+
# Connection path for the inspect hint — must be a DB path, not the source name.
|
|
52
|
+
# Absent when the source is in-memory (profiling :memory: across processes is useless).
|
|
53
|
+
connection_path: str | None = None
|
|
54
|
+
if selected_source is not None:
|
|
55
|
+
is_in_memory = bool(selected_source.get("in_memory"))
|
|
56
|
+
if not is_in_memory:
|
|
57
|
+
connection_path = selected_source.get("path")
|
|
58
|
+
|
|
59
|
+
warnings: list[str] = []
|
|
60
|
+
hints: list[str] = []
|
|
61
|
+
message = "No registered tables were found in the schema cache for this project."
|
|
62
|
+
|
|
63
|
+
if is_in_memory and selected_source is not None:
|
|
64
|
+
warnings.append(
|
|
65
|
+
f"Source '{selected_source['name']}' uses in-memory DuckDB, so schema() "
|
|
66
|
+
"can only see tables that have been created in the current session."
|
|
67
|
+
)
|
|
68
|
+
message = (
|
|
69
|
+
f"Source '{selected_source['name']}' is an in-memory DuckDB with no "
|
|
70
|
+
"registered tables loaded into the schema cache."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if uses_direct_file_queries():
|
|
74
|
+
warnings.append(
|
|
75
|
+
"This project appears to query files directly in dashboard SQL "
|
|
76
|
+
"(for example via read_csv(...)) rather than relying on persistent tables."
|
|
77
|
+
)
|
|
78
|
+
if surface == "cli":
|
|
79
|
+
hints.extend(
|
|
80
|
+
[
|
|
81
|
+
"Use `dft search` to find existing dashboards with relevant SQL patterns.",
|
|
82
|
+
"Use `dft query` with the discovered read_csv(...) pattern to inspect the underlying data directly.",
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
if connection_path:
|
|
86
|
+
hints.append(
|
|
87
|
+
f"Run `dft inspect --connection {connection_path}` to populate the schema cache."
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
hints.extend(
|
|
91
|
+
[
|
|
92
|
+
"Use search_dashboards to find existing dashboards with relevant SQL patterns.",
|
|
93
|
+
"Use render_dashboard(path=<source_path>, format='text' or 'json') to inspect the dashboard you found.",
|
|
94
|
+
"Use execute_query with the discovered read_csv(...) pattern to inspect the underlying data directly.",
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not hints:
|
|
99
|
+
if surface == "cli":
|
|
100
|
+
hints.append(
|
|
101
|
+
"Use `dft search` to inspect existing dashboards or `dft query` to probe data sources directly."
|
|
102
|
+
)
|
|
103
|
+
if connection_path:
|
|
104
|
+
hints.append(
|
|
105
|
+
f"Run `dft inspect --connection {connection_path}` to populate the schema cache."
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
hints.append(
|
|
109
|
+
"Use search_dashboards to inspect existing dashboards or execute_query "
|
|
110
|
+
"to probe data sources directly."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return EmptyTableGuidance(message=message, warnings=warnings, hints=hints)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def attach_empty_table_list_hints(
|
|
117
|
+
response: object,
|
|
118
|
+
registry: AdapterRegistry,
|
|
119
|
+
source: str,
|
|
120
|
+
*,
|
|
121
|
+
uses_direct_file_queries: Callable[[], bool],
|
|
122
|
+
surface: Literal["cli", "mcp"] = "mcp",
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Attach level-3 discoverability hints to response when no tables matched.
|
|
125
|
+
|
|
126
|
+
``response`` must expose mutable ``.message``, ``.warnings``, and
|
|
127
|
+
``.hints`` attributes (the ``SchemaResponse`` Pydantic model). Typed
|
|
128
|
+
loosely so ``core/`` carries no dependency on ``agent_api`` Pydantic models.
|
|
129
|
+
``uses_direct_file_queries`` is a callable so the (I/O-heavy) dashboard-walk
|
|
130
|
+
is skipped on the common path where the response already has tables.
|
|
131
|
+
``surface`` controls CLI vs MCP verb vocabulary in the hints.
|
|
132
|
+
"""
|
|
133
|
+
source_branch = getattr(response, "sources", {}).get(source, {})
|
|
134
|
+
schemas_branch = source_branch.get("schemas", {}) or {}
|
|
135
|
+
any_tables = any(
|
|
136
|
+
(s_entry.get("tables") or {}) for s_entry in schemas_branch.values()
|
|
137
|
+
)
|
|
138
|
+
if any_tables:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
guidance = empty_table_list_guidance(
|
|
142
|
+
registry,
|
|
143
|
+
source,
|
|
144
|
+
uses_direct_file_queries=uses_direct_file_queries,
|
|
145
|
+
surface=surface,
|
|
146
|
+
)
|
|
147
|
+
response.message = guidance.message # type: ignore[attr-defined]
|
|
148
|
+
if guidance.warnings:
|
|
149
|
+
response.warnings = guidance.warnings # type: ignore[attr-defined]
|
|
150
|
+
if guidance.hints:
|
|
151
|
+
response.hints = guidance.hints # type: ignore[attr-defined]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Path-scoping helpers used by the dashboard render orchestrator.
|
|
2
|
+
|
|
3
|
+
These resolve user-supplied face paths against a Dataface project root, with a
|
|
4
|
+
containment check so absolute paths cannot escape the project. They sit on top
|
|
5
|
+
of `core.project_roots` discovery and have no agent_api-shape concerns, so they
|
|
6
|
+
live in core where both `core.render.dashboard` and the agent_api verbs can
|
|
7
|
+
reach them without inverting the layer stack.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from dataface.core.project_roots import discover_project_root
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def project_root_for(project_dir: Path | None) -> Path:
|
|
18
|
+
"""Resolve project root: explicit project_dir or walk up from cwd."""
|
|
19
|
+
if project_dir is not None:
|
|
20
|
+
return project_dir.resolve()
|
|
21
|
+
return discover_project_root()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_against_project(path: Path, project_dir: Path | None) -> Path:
|
|
25
|
+
"""Resolve a non-absolute path: '..' from cwd, bare from project root."""
|
|
26
|
+
if ".." in path.parts:
|
|
27
|
+
return (Path.cwd() / path).resolve()
|
|
28
|
+
return (project_root_for(project_dir) / path).resolve()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_scoped_path(path: Path, project_dir: Path | None = None) -> Path:
|
|
32
|
+
"""Resolve a path against a scoped base directory.
|
|
33
|
+
|
|
34
|
+
Absolute paths must remain inside `project_dir` when one is provided;
|
|
35
|
+
otherwise they are resolved as-is. When `project_dir` is set, the resolved
|
|
36
|
+
path must remain inside it (containment check).
|
|
37
|
+
|
|
38
|
+
Bare relative paths resolve from the project root (walking up from cwd when
|
|
39
|
+
no project_dir is given). Paths containing '..' resolve from cwd so shell
|
|
40
|
+
navigation feels natural.
|
|
41
|
+
"""
|
|
42
|
+
raw_path = path
|
|
43
|
+
if raw_path.is_absolute():
|
|
44
|
+
if project_dir is None:
|
|
45
|
+
return raw_path.resolve()
|
|
46
|
+
resolved = raw_path.resolve()
|
|
47
|
+
try:
|
|
48
|
+
resolved.relative_to(project_dir.resolve())
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise ValueError(f"Absolute path escapes project_dir: {path}") from exc
|
|
51
|
+
return resolved
|
|
52
|
+
|
|
53
|
+
resolved_base = project_root_for(project_dir)
|
|
54
|
+
resolved = _resolve_against_project(raw_path, project_dir)
|
|
55
|
+
try:
|
|
56
|
+
resolved.relative_to(resolved_base)
|
|
57
|
+
except ValueError as exc:
|
|
58
|
+
raise ValueError(f"Path escapes base directory: {path}") from exc
|
|
59
|
+
return resolved
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""HTTP server for Dataface.
|
|
2
|
+
|
|
3
|
+
Stage: SERVE
|
|
4
|
+
Purpose: Provide HTTP endpoints for dashboard rendering.
|
|
5
|
+
|
|
6
|
+
Routes:
|
|
7
|
+
- /health - Health check
|
|
8
|
+
- /inspect/{template}/?model=X - Inspect templates
|
|
9
|
+
- /{path}/?var=value - Any face file (path.yml → /path/)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataface.core.serve.server import create_server
|
|
13
|
+
|
|
14
|
+
__all__ = ["create_server"]
|