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
|
+
"""Terminal layout rendering functions.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Render different layout types (rows, cols, grid, tabs) to terminal output.
|
|
5
|
+
|
|
6
|
+
This module provides functions to render layouts in terminal-friendly format:
|
|
7
|
+
- Rows: Vertical stacking
|
|
8
|
+
- Cols: Horizontal distribution (if terminal width allows)
|
|
9
|
+
- Grid: Character-based grid layout
|
|
10
|
+
- Tabs: Text-based tabs
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataface.core.compile.models.face.compiled import LayoutItem, VariableValues
|
|
14
|
+
from dataface.core.execute.executor import Executor
|
|
15
|
+
from dataface.core.render.terminal_text import (
|
|
16
|
+
pad_visible,
|
|
17
|
+
truncate_visible,
|
|
18
|
+
visible_len,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _layout_item_is_table(item: LayoutItem) -> bool:
|
|
23
|
+
if item.type != "chart":
|
|
24
|
+
return False
|
|
25
|
+
assert item.chart is not None # normalizer: chart layout items always carry chart
|
|
26
|
+
return item.chart.type == "table"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cols_should_stack_vertically(items: list[LayoutItem], col_width: int) -> bool:
|
|
30
|
+
if col_width < 50:
|
|
31
|
+
return True
|
|
32
|
+
return any(_layout_item_is_table(item) for item in items)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_rows_layout_terminal(
|
|
36
|
+
items: list[LayoutItem],
|
|
37
|
+
executor: Executor,
|
|
38
|
+
variables: VariableValues,
|
|
39
|
+
available_width: int,
|
|
40
|
+
available_height: int,
|
|
41
|
+
gap: int = 1,
|
|
42
|
+
background: str | None = None,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Render items in vertical stack for terminal.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
items: Layout items to render
|
|
48
|
+
executor: Executor for query execution
|
|
49
|
+
variables: Variable values for queries
|
|
50
|
+
available_width: Available terminal width in characters
|
|
51
|
+
available_height: Available terminal height in characters
|
|
52
|
+
gap: Gap between items (in lines)
|
|
53
|
+
background: Optional background color (not used in terminal)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Terminal-formatted string for rows layout
|
|
57
|
+
"""
|
|
58
|
+
from dataface.core.render.terminal import render_layout_item_terminal
|
|
59
|
+
|
|
60
|
+
if not items:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
rendered_items: list[str] = []
|
|
64
|
+
for item in items:
|
|
65
|
+
item_output = render_layout_item_terminal(
|
|
66
|
+
item, executor, variables, available_width, available_height
|
|
67
|
+
)
|
|
68
|
+
if item_output:
|
|
69
|
+
rendered_items.append(item_output)
|
|
70
|
+
# Add gap between items
|
|
71
|
+
if gap > 0:
|
|
72
|
+
rendered_items.append("")
|
|
73
|
+
|
|
74
|
+
return "\n".join(rendered_items)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def render_cols_layout_terminal(
|
|
78
|
+
items: list[LayoutItem],
|
|
79
|
+
executor: Executor,
|
|
80
|
+
variables: VariableValues,
|
|
81
|
+
available_width: int,
|
|
82
|
+
available_height: int,
|
|
83
|
+
gap: int = 2,
|
|
84
|
+
background: str | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Render items side-by-side for terminal.
|
|
87
|
+
|
|
88
|
+
Note: Terminal columns are limited by width. If items don't fit,
|
|
89
|
+
we fall back to vertical stacking.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
items: Layout items to render
|
|
93
|
+
executor: Executor for query execution
|
|
94
|
+
variables: Variable values for queries
|
|
95
|
+
available_width: Available terminal width in characters
|
|
96
|
+
available_height: Available terminal height in characters
|
|
97
|
+
gap: Gap between items (in characters)
|
|
98
|
+
background: Optional background color (not used in terminal)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Terminal-formatted string for columns layout
|
|
102
|
+
"""
|
|
103
|
+
from dataface.core.render.terminal import render_layout_item_terminal
|
|
104
|
+
|
|
105
|
+
if not items:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
# Calculate column width
|
|
109
|
+
n = len(items)
|
|
110
|
+
total_gap = gap * (n - 1)
|
|
111
|
+
col_width = (available_width - total_gap) // n
|
|
112
|
+
|
|
113
|
+
if _cols_should_stack_vertically(items, col_width):
|
|
114
|
+
return render_rows_layout_terminal(
|
|
115
|
+
items,
|
|
116
|
+
executor,
|
|
117
|
+
variables,
|
|
118
|
+
available_width,
|
|
119
|
+
available_height,
|
|
120
|
+
gap,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Render each item in its column
|
|
124
|
+
rendered_items: list[list[str]] = []
|
|
125
|
+
max_lines = 0
|
|
126
|
+
|
|
127
|
+
for item in items:
|
|
128
|
+
item_output = render_layout_item_terminal(
|
|
129
|
+
item, executor, variables, col_width, available_height
|
|
130
|
+
)
|
|
131
|
+
lines = item_output.split("\n")
|
|
132
|
+
rendered_items.append(lines)
|
|
133
|
+
max_lines = max(max_lines, len(lines))
|
|
134
|
+
|
|
135
|
+
# Combine columns horizontally
|
|
136
|
+
output_lines: list[str] = []
|
|
137
|
+
for line_idx in range(max_lines):
|
|
138
|
+
line_parts = []
|
|
139
|
+
for item_lines in rendered_items:
|
|
140
|
+
if line_idx < len(item_lines):
|
|
141
|
+
line = item_lines[line_idx]
|
|
142
|
+
if visible_len(line) > col_width:
|
|
143
|
+
line = truncate_visible(line, col_width)
|
|
144
|
+
line_parts.append(pad_visible(line, col_width))
|
|
145
|
+
else:
|
|
146
|
+
line_parts.append(" " * col_width)
|
|
147
|
+
|
|
148
|
+
output_lines.append((" " * gap).join(line_parts))
|
|
149
|
+
|
|
150
|
+
return "\n".join(output_lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def render_grid_layout_terminal(
|
|
154
|
+
items: list[LayoutItem],
|
|
155
|
+
executor: Executor,
|
|
156
|
+
variables: VariableValues,
|
|
157
|
+
available_width: int,
|
|
158
|
+
available_height: int,
|
|
159
|
+
columns: int = 2,
|
|
160
|
+
gap: int = 1,
|
|
161
|
+
background: str | None = None,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Render items in a grid layout for terminal.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
items: Layout items to render
|
|
167
|
+
executor: Executor for query execution
|
|
168
|
+
variables: Variable values for queries
|
|
169
|
+
available_width: Available terminal width in characters
|
|
170
|
+
available_height: Available terminal height in characters
|
|
171
|
+
columns: Number of columns in grid
|
|
172
|
+
gap: Gap between items (in characters/lines)
|
|
173
|
+
background: Optional background color (not used in terminal)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Terminal-formatted string for grid layout
|
|
177
|
+
"""
|
|
178
|
+
from dataface.core.render.terminal import render_layout_item_terminal
|
|
179
|
+
|
|
180
|
+
if not items:
|
|
181
|
+
return ""
|
|
182
|
+
|
|
183
|
+
# Calculate cell dimensions
|
|
184
|
+
total_gap = gap * (columns - 1)
|
|
185
|
+
cell_width = (available_width - total_gap) // columns
|
|
186
|
+
|
|
187
|
+
# Group items into rows
|
|
188
|
+
rows: list[list[LayoutItem]] = []
|
|
189
|
+
current_row: list[LayoutItem] = []
|
|
190
|
+
for item in items:
|
|
191
|
+
current_row.append(item)
|
|
192
|
+
if len(current_row) >= columns:
|
|
193
|
+
rows.append(current_row)
|
|
194
|
+
current_row = []
|
|
195
|
+
if current_row:
|
|
196
|
+
rows.append(current_row)
|
|
197
|
+
|
|
198
|
+
# Render each row
|
|
199
|
+
rendered_rows: list[str] = []
|
|
200
|
+
for row_items in rows:
|
|
201
|
+
# Render items in this row
|
|
202
|
+
row_rendered: list[list[str]] = []
|
|
203
|
+
max_lines = 0
|
|
204
|
+
|
|
205
|
+
for item in row_items:
|
|
206
|
+
item_output = render_layout_item_terminal(
|
|
207
|
+
item, executor, variables, cell_width, available_height
|
|
208
|
+
)
|
|
209
|
+
lines = item_output.split("\n")
|
|
210
|
+
row_rendered.append(lines)
|
|
211
|
+
max_lines = max(max_lines, len(lines))
|
|
212
|
+
|
|
213
|
+
# Combine row items horizontally
|
|
214
|
+
row_lines: list[str] = []
|
|
215
|
+
for line_idx in range(max_lines):
|
|
216
|
+
line_parts = []
|
|
217
|
+
for item_lines in row_rendered:
|
|
218
|
+
if line_idx < len(item_lines):
|
|
219
|
+
line = item_lines[line_idx]
|
|
220
|
+
if visible_len(line) > cell_width:
|
|
221
|
+
line = truncate_visible(line, cell_width)
|
|
222
|
+
line_parts.append(pad_visible(line, cell_width))
|
|
223
|
+
else:
|
|
224
|
+
line_parts.append(" " * cell_width)
|
|
225
|
+
|
|
226
|
+
row_lines.append((" " * gap).join(line_parts))
|
|
227
|
+
|
|
228
|
+
rendered_rows.append("\n".join(row_lines))
|
|
229
|
+
if gap > 0:
|
|
230
|
+
rendered_rows.append("") # Gap between rows
|
|
231
|
+
|
|
232
|
+
return "\n".join(rendered_rows)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def render_tabs_layout_terminal(
|
|
236
|
+
items: list[LayoutItem],
|
|
237
|
+
executor: Executor,
|
|
238
|
+
variables: VariableValues,
|
|
239
|
+
available_width: int,
|
|
240
|
+
available_height: int,
|
|
241
|
+
tab_titles: list[str] | None = None,
|
|
242
|
+
default_tab: int = 0,
|
|
243
|
+
tab_position: str = "top",
|
|
244
|
+
background: str | None = None,
|
|
245
|
+
) -> str:
|
|
246
|
+
"""Render tabs layout for terminal.
|
|
247
|
+
|
|
248
|
+
Note: For terminal, we render all tabs sequentially with headers,
|
|
249
|
+
since terminal doesn't support interactive tabs.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
items: Layout items (one per tab)
|
|
253
|
+
executor: Executor for query execution
|
|
254
|
+
variables: Variable values for queries
|
|
255
|
+
available_width: Available terminal width in characters
|
|
256
|
+
available_height: Available terminal height in characters
|
|
257
|
+
tab_titles: Optional list of tab titles
|
|
258
|
+
default_tab: Default active tab (not used in terminal)
|
|
259
|
+
tab_position: Tab position ("top" or "bottom", not used in terminal)
|
|
260
|
+
background: Optional background color (not used in terminal)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Terminal-formatted string for tabs layout
|
|
264
|
+
"""
|
|
265
|
+
from dataface.core.render.terminal import render_layout_item_terminal
|
|
266
|
+
|
|
267
|
+
if not items:
|
|
268
|
+
return ""
|
|
269
|
+
|
|
270
|
+
output_lines: list[str] = []
|
|
271
|
+
|
|
272
|
+
for idx, item in enumerate(items):
|
|
273
|
+
# Add tab header
|
|
274
|
+
tab_title = (
|
|
275
|
+
tab_titles[idx]
|
|
276
|
+
if tab_titles and idx < len(tab_titles)
|
|
277
|
+
else f"Tab {idx + 1}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Create header line
|
|
281
|
+
header_line = f"┌─ {tab_title} {'─' * (available_width - len(tab_title) - 5)}┐"
|
|
282
|
+
output_lines.append(header_line)
|
|
283
|
+
|
|
284
|
+
# Render tab content
|
|
285
|
+
item_output = render_layout_item_terminal(
|
|
286
|
+
item, executor, variables, available_width - 4, available_height
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Indent content
|
|
290
|
+
content_lines = item_output.split("\n")
|
|
291
|
+
for line in content_lines:
|
|
292
|
+
output_lines.append(f"│ {line.ljust(available_width - 4)} │")
|
|
293
|
+
|
|
294
|
+
# Add footer
|
|
295
|
+
footer_line = f"└{'─' * (available_width - 2)}┘"
|
|
296
|
+
output_lines.append(footer_line)
|
|
297
|
+
output_lines.append("") # Gap between tabs
|
|
298
|
+
|
|
299
|
+
return "\n".join(output_lines)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""ANSI-aware string width helpers for terminal layout joining."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def strip_ansi(text: str) -> str:
|
|
11
|
+
return _ANSI_ESCAPE_RE.sub("", text)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def visible_len(text: str) -> int:
|
|
15
|
+
return len(strip_ansi(text))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pad_visible(text: str, width: int) -> str:
|
|
19
|
+
padding = width - visible_len(text)
|
|
20
|
+
if padding <= 0:
|
|
21
|
+
return text
|
|
22
|
+
return text + (" " * padding)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def truncate_visible(text: str, max_width: int) -> str:
|
|
26
|
+
if visible_len(text) <= max_width:
|
|
27
|
+
return text
|
|
28
|
+
plain = strip_ansi(text)
|
|
29
|
+
if max_width <= 3:
|
|
30
|
+
return plain[:max_width]
|
|
31
|
+
return plain[: max_width - 3] + "..."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Text transform utilities for DFT render layer."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Letter-case transform dispatcher for DFT typography.
|
|
2
|
+
|
|
3
|
+
Applies at render time — after Jinja resolution but before the string reaches
|
|
4
|
+
an SVG <text> node or a Vega-Lite spec string field.
|
|
5
|
+
|
|
6
|
+
Supported values (matching FontStyle.case):
|
|
7
|
+
none — no-op; string emitted as authored / as normalized by slug_to_text.
|
|
8
|
+
upper — str.upper() (Unicode-aware).
|
|
9
|
+
lower — str.lower() (Unicode-aware).
|
|
10
|
+
sentence — first character uppercased, remainder unchanged.
|
|
11
|
+
Preserves acronyms and proper nouns that the author
|
|
12
|
+
already capitalized. Does NOT lowercase the rest of the
|
|
13
|
+
string — that would corrupt ARR, iOS, etc.
|
|
14
|
+
title — Chicago/Gruber algorithm via the `titlecase` library.
|
|
15
|
+
Lowercases stopwords (a, an, and, as, at, but, by, en,
|
|
16
|
+
for, if, in, of, on, or, the, to, v[.], vs[.], via).
|
|
17
|
+
Always capitalizes first and last words.
|
|
18
|
+
Preserves any token with internal capitals (ARR, iPhone,
|
|
19
|
+
MRR, SQL) — the key differentiator from naive
|
|
20
|
+
"capitalize every word" algorithms.
|
|
21
|
+
slug — Machine identifier form: spaces/hyphens → underscore, lower.
|
|
22
|
+
"Order Status" → "order_status".
|
|
23
|
+
camel — camelCase: first word lower, subsequent words capitalized.
|
|
24
|
+
"order status" → "orderStatus".
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import TYPE_CHECKING, Literal
|
|
30
|
+
|
|
31
|
+
from titlecase import titlecase as _titlecase
|
|
32
|
+
|
|
33
|
+
from dataface.core.utils import slug_to_text
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from dataface.core.compile.models.primitives import MergedFontStyle
|
|
37
|
+
|
|
38
|
+
CaseValue = Literal["none", "sentence", "title", "upper", "lower", "slug", "camel"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def apply_case(text: str, case: CaseValue) -> str:
|
|
42
|
+
"""Apply a letter-case transform to *text* and return the result.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: The string to transform. May be empty.
|
|
46
|
+
case: One of 'none', 'upper', 'lower', 'sentence', 'title', 'slug', 'camel'.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The transformed string.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If *case* is not a recognised value.
|
|
53
|
+
"""
|
|
54
|
+
if case == "none":
|
|
55
|
+
return text
|
|
56
|
+
if case == "upper":
|
|
57
|
+
return text.upper()
|
|
58
|
+
if case == "lower":
|
|
59
|
+
return text.lower()
|
|
60
|
+
if case == "sentence":
|
|
61
|
+
# Cautious sentence case: uppercase only the first character, leave
|
|
62
|
+
# the rest unchanged. Preserves acronyms/proper nouns.
|
|
63
|
+
return text[:1].upper() + text[1:] if text else text
|
|
64
|
+
if case == "title":
|
|
65
|
+
return _titlecase(text)
|
|
66
|
+
if case == "slug":
|
|
67
|
+
# Machine form: spaces/hyphens → underscore, all lowercase.
|
|
68
|
+
return text.replace(" ", "_").replace("-", "_").lower()
|
|
69
|
+
if case == "camel":
|
|
70
|
+
# camelCase: lower the first word, capitalize subsequent words.
|
|
71
|
+
words = text.replace("-", " ").replace("_", " ").split()
|
|
72
|
+
if not words:
|
|
73
|
+
return text
|
|
74
|
+
return words[0].lower() + "".join(w.capitalize() for w in words[1:])
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Unknown case: {case!r}. Expected one of 'none', 'upper', "
|
|
77
|
+
"'lower', 'sentence', 'title', 'slug', 'camel'."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def apply_font_case(text: str, font: MergedFontStyle) -> str:
|
|
82
|
+
"""Apply the letter-case transform specified by font.case.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
text: The string to transform.
|
|
86
|
+
font: A fully-resolved font; font.case drives the transform.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The transformed string.
|
|
90
|
+
"""
|
|
91
|
+
return apply_case(text, font.case)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_display_text(
|
|
95
|
+
text: str,
|
|
96
|
+
*,
|
|
97
|
+
from_slug: bool,
|
|
98
|
+
font: MergedFontStyle,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Two-step render pipeline: optional slug tokenization then font case.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
text: The raw input string — either an authored string or a slug.
|
|
104
|
+
from_slug: True when *text* is a field slug / identifier that needs
|
|
105
|
+
``slug_to_text`` tokenization first (underscore→space, unit/abbreviation
|
|
106
|
+
expansion). False when *text* is already human-readable authored text.
|
|
107
|
+
font: The resolved font for this slot; font.case drives the case transform.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The display-ready string.
|
|
111
|
+
"""
|
|
112
|
+
normalized = slug_to_text(text) if from_slug else text
|
|
113
|
+
return apply_font_case(normalized, font)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Text render output format for AI agents.
|
|
2
|
+
|
|
3
|
+
Walks the layout tree via face_to_dict (shared with JSON format),
|
|
4
|
+
then templates into compact markdown text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from dataface.core.compile.models.face.compiled import Face, VariableValues
|
|
10
|
+
from dataface.core.errors import StructuredError
|
|
11
|
+
from dataface.core.execute.executor import Executor
|
|
12
|
+
from dataface.core.render.json_format import face_to_dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_face_text(
|
|
16
|
+
face: Face,
|
|
17
|
+
executor: Executor,
|
|
18
|
+
variables: VariableValues,
|
|
19
|
+
error_collector: list[StructuredError] | None = None,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Render a compiled face to compact markdown text for AI agents."""
|
|
22
|
+
d = face_to_dict(face, executor, variables, error_collector)
|
|
23
|
+
lines: list[str] = []
|
|
24
|
+
_render_face(d, lines, depth=1)
|
|
25
|
+
return "\n".join(lines)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _render_face(face: dict[str, Any], lines: list[str], depth: int) -> None:
|
|
29
|
+
"""Render a face dict to markdown lines at the given heading depth."""
|
|
30
|
+
title = face.get("title") or face.get("id", "Untitled")
|
|
31
|
+
lines.append(f"{'#' * depth} {title}")
|
|
32
|
+
for item in face.get("items", []):
|
|
33
|
+
if item["type"] == "chart":
|
|
34
|
+
_render_chart(item, lines, depth + 1)
|
|
35
|
+
elif item["type"] == "face":
|
|
36
|
+
lines.append("")
|
|
37
|
+
_render_face(item["face"], lines, depth + 1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _render_chart(item: dict[str, Any], lines: list[str], depth: int) -> None:
|
|
41
|
+
"""Render a chart item to markdown lines."""
|
|
42
|
+
# Chart error: emit a compact error line instead of data
|
|
43
|
+
if "_error" in item:
|
|
44
|
+
err = item["_error"]
|
|
45
|
+
code = err.get("code", "")
|
|
46
|
+
msg = err.get("message", "error")
|
|
47
|
+
chart_id = err.get("fields", {}).get("chart_id", "unknown")
|
|
48
|
+
lines.append(f"\n[chart error: {code} {msg} (chart: {chart_id})]")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
chart = item["chart"]
|
|
52
|
+
data = item.get("data", [])
|
|
53
|
+
|
|
54
|
+
chart_type = chart.get("chart_type", "unknown")
|
|
55
|
+
title = chart.get("title") or chart.get("id", "chart")
|
|
56
|
+
lines.append("")
|
|
57
|
+
lines.append(f"{'#' * depth} {title} ({chart_type})")
|
|
58
|
+
|
|
59
|
+
# KPI: show value, optionally resolved against the bound row when the
|
|
60
|
+
# authored value is a column reference.
|
|
61
|
+
if chart_type == "kpi":
|
|
62
|
+
raw = chart.get("value")
|
|
63
|
+
cell = (
|
|
64
|
+
data[0].get(raw)
|
|
65
|
+
if isinstance(raw, str) and data and raw in data[0]
|
|
66
|
+
else raw
|
|
67
|
+
)
|
|
68
|
+
if isinstance(cell, (int, float)):
|
|
69
|
+
display = f"{cell:,}" if cell == int(cell) else f"{cell:,.2f}"
|
|
70
|
+
elif cell is not None:
|
|
71
|
+
display = str(cell)
|
|
72
|
+
else:
|
|
73
|
+
display = ""
|
|
74
|
+
if display:
|
|
75
|
+
lines.append(f"- value: {display}")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Field mappings
|
|
79
|
+
fields = _field_mappings(chart)
|
|
80
|
+
if fields:
|
|
81
|
+
lines.append(f"- {', '.join(fields)}")
|
|
82
|
+
|
|
83
|
+
# Data summary
|
|
84
|
+
if data:
|
|
85
|
+
summary = _data_summary(chart, data)
|
|
86
|
+
if summary:
|
|
87
|
+
lines.append(f"- {summary}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _field_mappings(chart: dict[str, Any]) -> list[str]:
|
|
91
|
+
"""Extract field mapping strings like 'x: month, y: revenue'."""
|
|
92
|
+
mappings = []
|
|
93
|
+
for key in ("x", "y", "color", "size", "theta", "value"):
|
|
94
|
+
val = chart.get(key)
|
|
95
|
+
if val:
|
|
96
|
+
mappings.append(f"{key}: {val}")
|
|
97
|
+
return mappings
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _data_summary(chart: dict[str, Any], data: list[dict]) -> str:
|
|
101
|
+
"""Build a compact data summary string."""
|
|
102
|
+
parts = [f"{len(data)} rows"]
|
|
103
|
+
|
|
104
|
+
# Collect chart field names by role
|
|
105
|
+
field_roles: dict[str, str] = {}
|
|
106
|
+
for key in ("x", "y", "color", "size", "theta"):
|
|
107
|
+
val = chart.get(key)
|
|
108
|
+
if val:
|
|
109
|
+
field_roles[val] = key
|
|
110
|
+
|
|
111
|
+
for col, role in field_roles.items():
|
|
112
|
+
values: list[Any] = [row[col] for row in data if row.get(col) is not None]
|
|
113
|
+
if not values:
|
|
114
|
+
continue
|
|
115
|
+
if all(isinstance(v, (int, float)) for v in values) and role in (
|
|
116
|
+
"y",
|
|
117
|
+
"size",
|
|
118
|
+
"theta",
|
|
119
|
+
):
|
|
120
|
+
lo, hi = min(values), max(values)
|
|
121
|
+
parts.append(f"{col}: {lo}–{hi}")
|
|
122
|
+
elif all(isinstance(v, str) for v in values) and role in ("x", "color"):
|
|
123
|
+
distinct = sorted(set(values))
|
|
124
|
+
if len(distinct) <= 5:
|
|
125
|
+
parts.append(f"{col}: {', '.join(distinct)}")
|
|
126
|
+
else:
|
|
127
|
+
parts.append(f"{col}: {len(distinct)} distinct")
|
|
128
|
+
|
|
129
|
+
return " | ".join(parts)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Shared utility functions for the render module.
|
|
2
|
+
|
|
3
|
+
This module contains common utilities used across multiple render modules
|
|
4
|
+
to avoid code duplication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from dataface.core.compile.models.chart.compiled import (
|
|
15
|
+
Chart,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SQL_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_valid_sql_identifier(identifier: str) -> bool:
|
|
22
|
+
"""Validate that a string is a safe SQL identifier.
|
|
23
|
+
|
|
24
|
+
Prevents SQL injection by ensuring identifiers only contain
|
|
25
|
+
alphanumeric characters and underscores, starting with a letter
|
|
26
|
+
or underscore.
|
|
27
|
+
"""
|
|
28
|
+
if not identifier:
|
|
29
|
+
return False
|
|
30
|
+
return SQL_IDENTIFIER_PATTERN.match(identifier) is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
from dataface.core.utils import slug_to_text # noqa: F401 — re-exported
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_field_names(
|
|
37
|
+
chart: Chart,
|
|
38
|
+
data: list[dict[str, Any]],
|
|
39
|
+
) -> Chart:
|
|
40
|
+
"""Resolve chart field references against actual data column names.
|
|
41
|
+
|
|
42
|
+
Handles case mismatches (e.g. chart says ``x: stage`` but data has
|
|
43
|
+
``Stage``) by remapping the chart's field attributes to match the
|
|
44
|
+
real column keys. Returns a copy of the chart with updated fields
|
|
45
|
+
if any mismatches are found; returns the original chart unchanged
|
|
46
|
+
when all fields already match.
|
|
47
|
+
"""
|
|
48
|
+
if not data:
|
|
49
|
+
return chart
|
|
50
|
+
|
|
51
|
+
actual_cols = set(data[0].keys())
|
|
52
|
+
lower_to_actual: dict[str, str] = {k.lower(): k for k in actual_cols}
|
|
53
|
+
|
|
54
|
+
field_attrs = [
|
|
55
|
+
"x",
|
|
56
|
+
"y",
|
|
57
|
+
"color",
|
|
58
|
+
"size",
|
|
59
|
+
"shape",
|
|
60
|
+
"theta",
|
|
61
|
+
"lookup",
|
|
62
|
+
"value", # always a column reference; case-insensitive remapping applies
|
|
63
|
+
]
|
|
64
|
+
updates: dict[str, Any] = {}
|
|
65
|
+
|
|
66
|
+
for attr in field_attrs:
|
|
67
|
+
val = getattr(chart, attr, None)
|
|
68
|
+
if val is None:
|
|
69
|
+
continue
|
|
70
|
+
if isinstance(val, list):
|
|
71
|
+
resolved_fields = [
|
|
72
|
+
lower_to_actual.get(f.lower(), f) if isinstance(f, str) else f
|
|
73
|
+
for f in val
|
|
74
|
+
]
|
|
75
|
+
if resolved_fields != val:
|
|
76
|
+
updates[attr] = resolved_fields
|
|
77
|
+
elif isinstance(val, str) and val not in actual_cols:
|
|
78
|
+
resolved_field = lower_to_actual.get(val.lower())
|
|
79
|
+
if resolved_field:
|
|
80
|
+
updates[attr] = resolved_field
|
|
81
|
+
|
|
82
|
+
if updates:
|
|
83
|
+
return chart.model_copy(update=updates)
|
|
84
|
+
return chart
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def normalize_data_types(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
88
|
+
"""Normalize data types for JSON serialization and rendering.
|
|
89
|
+
|
|
90
|
+
Converts Decimal to float, preserves lists/tuples, and stringifies
|
|
91
|
+
other non-JSON-serializable types.
|
|
92
|
+
"""
|
|
93
|
+
normalized: list[dict[str, Any]] = []
|
|
94
|
+
for row in data:
|
|
95
|
+
normalized_row: dict[str, Any] = {}
|
|
96
|
+
for key, value in row.items():
|
|
97
|
+
if isinstance(value, Decimal):
|
|
98
|
+
normalized_row[key] = float(value)
|
|
99
|
+
elif isinstance(value, (int, float, str, bool, type(None))):
|
|
100
|
+
normalized_row[key] = value
|
|
101
|
+
elif isinstance(value, (list, tuple)):
|
|
102
|
+
normalized_row[key] = list(value)
|
|
103
|
+
else:
|
|
104
|
+
normalized_row[key] = str(value)
|
|
105
|
+
normalized.append(normalized_row)
|
|
106
|
+
return normalized
|