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,263 @@
|
|
|
1
|
+
# String Column Profile Template
|
|
2
|
+
#
|
|
3
|
+
# Analysis of a string/text column. Schema panel pulls from the
|
|
4
|
+
# LayeredSchemaResolver; live aggregations stay on warehouse SQL.
|
|
5
|
+
#
|
|
6
|
+
# URL: /inspect/string_column/?model=<table>&column=<col>
|
|
7
|
+
#
|
|
8
|
+
# Required variables:
|
|
9
|
+
# - model: Table/model name
|
|
10
|
+
# - column: Column name to analyze
|
|
11
|
+
#
|
|
12
|
+
# Optional variables:
|
|
13
|
+
# - connection: DuckDB file path or :memory: (default: :memory:)
|
|
14
|
+
#
|
|
15
|
+
# Theme is set via DFT_DEFAULT_THEME env var at serve startup.
|
|
16
|
+
|
|
17
|
+
title: "String Column: {{ model }}.{{ column }}"
|
|
18
|
+
|
|
19
|
+
text: |
|
|
20
|
+
[Back to {{ model }}](/inspect/model/?model={{ model }}&connection={{ connection }})
|
|
21
|
+
|
|
22
|
+
variables:
|
|
23
|
+
model:
|
|
24
|
+
input: text
|
|
25
|
+
visible: false
|
|
26
|
+
default: ""
|
|
27
|
+
column:
|
|
28
|
+
input: text
|
|
29
|
+
visible: false
|
|
30
|
+
default: ""
|
|
31
|
+
connection:
|
|
32
|
+
input: text
|
|
33
|
+
visible: false
|
|
34
|
+
default: ":memory:"
|
|
35
|
+
source_name:
|
|
36
|
+
input: text
|
|
37
|
+
visible: false
|
|
38
|
+
default: ""
|
|
39
|
+
schema_name:
|
|
40
|
+
input: text
|
|
41
|
+
visible: false
|
|
42
|
+
default: ""
|
|
43
|
+
|
|
44
|
+
queries:
|
|
45
|
+
column_schema:
|
|
46
|
+
type: schema_resolver
|
|
47
|
+
source: "{{ source_name }}"
|
|
48
|
+
schema: "{{ schema_name }}"
|
|
49
|
+
table: "{{ model }}"
|
|
50
|
+
column: "{{ column }}"
|
|
51
|
+
|
|
52
|
+
# Basic string stats
|
|
53
|
+
stats:
|
|
54
|
+
source:
|
|
55
|
+
type: duckdb
|
|
56
|
+
path: "{{ connection }}"
|
|
57
|
+
sql: |
|
|
58
|
+
SELECT
|
|
59
|
+
COUNT(*) as total_count,
|
|
60
|
+
COUNT({{ column }}) as non_null_count,
|
|
61
|
+
COUNT(*) - COUNT({{ column }}) as null_count,
|
|
62
|
+
ROUND(100.0 * (COUNT(*) - COUNT({{ column }})) / COUNT(*), 2) as null_pct,
|
|
63
|
+
COUNT(DISTINCT {{ column }}) as distinct_count,
|
|
64
|
+
COUNT(CASE WHEN TRIM({{ column }}) = '' THEN 1 END) as empty_count,
|
|
65
|
+
MIN(LENGTH({{ column }})) as min_length,
|
|
66
|
+
MAX(LENGTH({{ column }})) as max_length,
|
|
67
|
+
ROUND(AVG(LENGTH({{ column }})), 1) as avg_length
|
|
68
|
+
FROM {{ model }}
|
|
69
|
+
|
|
70
|
+
# Top values by frequency
|
|
71
|
+
top_values:
|
|
72
|
+
source:
|
|
73
|
+
type: duckdb
|
|
74
|
+
path: "{{ connection }}"
|
|
75
|
+
sql: |
|
|
76
|
+
SELECT
|
|
77
|
+
COALESCE({{ column }}, '(null)') as value,
|
|
78
|
+
COUNT(*) as count,
|
|
79
|
+
ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM {{ model }}), 2) as pct
|
|
80
|
+
FROM {{ model }}
|
|
81
|
+
GROUP BY {{ column }}
|
|
82
|
+
ORDER BY COUNT(*) DESC
|
|
83
|
+
LIMIT 20
|
|
84
|
+
|
|
85
|
+
# Length distribution
|
|
86
|
+
length_distribution:
|
|
87
|
+
source:
|
|
88
|
+
type: duckdb
|
|
89
|
+
path: "{{ connection }}"
|
|
90
|
+
sql: |
|
|
91
|
+
SELECT
|
|
92
|
+
LENGTH({{ column }}) as length,
|
|
93
|
+
COUNT(*) as count
|
|
94
|
+
FROM {{ model }}
|
|
95
|
+
WHERE {{ column }} IS NOT NULL
|
|
96
|
+
GROUP BY LENGTH({{ column }})
|
|
97
|
+
ORDER BY length
|
|
98
|
+
|
|
99
|
+
# Pattern analysis (first character distribution)
|
|
100
|
+
first_char:
|
|
101
|
+
source:
|
|
102
|
+
type: duckdb
|
|
103
|
+
path: "{{ connection }}"
|
|
104
|
+
sql: |
|
|
105
|
+
SELECT
|
|
106
|
+
UPPER(SUBSTRING({{ column }}, 1, 1)) as first_char,
|
|
107
|
+
COUNT(*) as count
|
|
108
|
+
FROM {{ model }}
|
|
109
|
+
WHERE {{ column }} IS NOT NULL AND LENGTH({{ column }}) > 0
|
|
110
|
+
GROUP BY UPPER(SUBSTRING({{ column }}, 1, 1))
|
|
111
|
+
ORDER BY COUNT(*) DESC
|
|
112
|
+
LIMIT 26
|
|
113
|
+
|
|
114
|
+
# Sample values
|
|
115
|
+
samples:
|
|
116
|
+
source:
|
|
117
|
+
type: duckdb
|
|
118
|
+
path: "{{ connection }}"
|
|
119
|
+
sql: |
|
|
120
|
+
SELECT DISTINCT {{ column }} as value
|
|
121
|
+
FROM {{ model }}
|
|
122
|
+
WHERE {{ column }} IS NOT NULL
|
|
123
|
+
LIMIT 20
|
|
124
|
+
|
|
125
|
+
charts:
|
|
126
|
+
schema_panel:
|
|
127
|
+
title: Schema
|
|
128
|
+
type: table
|
|
129
|
+
query: column_schema
|
|
130
|
+
style:
|
|
131
|
+
header_overflow: wrap-two
|
|
132
|
+
columns:
|
|
133
|
+
name:
|
|
134
|
+
label: Column
|
|
135
|
+
type:
|
|
136
|
+
label: Type
|
|
137
|
+
role:
|
|
138
|
+
label: Role
|
|
139
|
+
semantic_type:
|
|
140
|
+
label: Semantic
|
|
141
|
+
description:
|
|
142
|
+
label: Description
|
|
143
|
+
|
|
144
|
+
# Stats KPIs
|
|
145
|
+
distinct:
|
|
146
|
+
label: Distinct Values
|
|
147
|
+
type: kpi
|
|
148
|
+
query: stats
|
|
149
|
+
value: distinct_count
|
|
150
|
+
format: ",.0f"
|
|
151
|
+
|
|
152
|
+
null_pct:
|
|
153
|
+
label: "Null %"
|
|
154
|
+
type: kpi
|
|
155
|
+
query: stats
|
|
156
|
+
value: null_pct
|
|
157
|
+
format: ".1f"
|
|
158
|
+
|
|
159
|
+
empty_count:
|
|
160
|
+
label: Empty Strings
|
|
161
|
+
type: kpi
|
|
162
|
+
query: stats
|
|
163
|
+
value: empty_count
|
|
164
|
+
format: ",.0f"
|
|
165
|
+
|
|
166
|
+
min_len:
|
|
167
|
+
label: Min Length
|
|
168
|
+
type: kpi
|
|
169
|
+
query: stats
|
|
170
|
+
value: min_length
|
|
171
|
+
format: ",.0f"
|
|
172
|
+
|
|
173
|
+
max_len:
|
|
174
|
+
label: Max Length
|
|
175
|
+
type: kpi
|
|
176
|
+
query: stats
|
|
177
|
+
value: max_length
|
|
178
|
+
format: ",.0f"
|
|
179
|
+
|
|
180
|
+
avg_len:
|
|
181
|
+
label: Avg Length
|
|
182
|
+
type: kpi
|
|
183
|
+
query: stats
|
|
184
|
+
value: avg_length
|
|
185
|
+
format: ".1f"
|
|
186
|
+
|
|
187
|
+
# Spark bar (compact top values for profiler cards)
|
|
188
|
+
top_values_spark:
|
|
189
|
+
title: Top Values
|
|
190
|
+
type: spark_bar
|
|
191
|
+
query: top_values
|
|
192
|
+
x: count
|
|
193
|
+
y: value
|
|
194
|
+
|
|
195
|
+
# Top values chart (full-size)
|
|
196
|
+
top_values_chart:
|
|
197
|
+
title: Top Values (Full)
|
|
198
|
+
type: bar
|
|
199
|
+
query: top_values
|
|
200
|
+
x: value
|
|
201
|
+
y: count
|
|
202
|
+
style:
|
|
203
|
+
orientation: horizontal
|
|
204
|
+
|
|
205
|
+
# Top values table
|
|
206
|
+
top_values_table:
|
|
207
|
+
title: Value Frequency
|
|
208
|
+
type: table
|
|
209
|
+
query: top_values
|
|
210
|
+
style:
|
|
211
|
+
header_overflow: wrap-two
|
|
212
|
+
columns:
|
|
213
|
+
value:
|
|
214
|
+
label: Value
|
|
215
|
+
count:
|
|
216
|
+
label: Count
|
|
217
|
+
format: ",.0f"
|
|
218
|
+
pct:
|
|
219
|
+
label: "%"
|
|
220
|
+
format: ".1f"
|
|
221
|
+
|
|
222
|
+
# Length distribution
|
|
223
|
+
length_chart:
|
|
224
|
+
title: Length Distribution
|
|
225
|
+
type: bar
|
|
226
|
+
query: length_distribution
|
|
227
|
+
x: length
|
|
228
|
+
y: count
|
|
229
|
+
x_label: String Length
|
|
230
|
+
y_label: Count
|
|
231
|
+
|
|
232
|
+
# First character distribution
|
|
233
|
+
first_char_chart:
|
|
234
|
+
title: First Character
|
|
235
|
+
type: bar
|
|
236
|
+
query: first_char
|
|
237
|
+
x: first_char
|
|
238
|
+
y: count
|
|
239
|
+
|
|
240
|
+
# Sample values
|
|
241
|
+
samples_table:
|
|
242
|
+
title: Sample Values
|
|
243
|
+
type: table
|
|
244
|
+
query: samples
|
|
245
|
+
style:
|
|
246
|
+
header_overflow: wrap-two
|
|
247
|
+
|
|
248
|
+
rows:
|
|
249
|
+
- schema_panel
|
|
250
|
+
- cols:
|
|
251
|
+
- distinct
|
|
252
|
+
- null_pct
|
|
253
|
+
- empty_count
|
|
254
|
+
- min_len
|
|
255
|
+
- max_len
|
|
256
|
+
- avg_len
|
|
257
|
+
- cols:
|
|
258
|
+
- top_values_spark
|
|
259
|
+
- top_values_table
|
|
260
|
+
- cols:
|
|
261
|
+
- length_chart
|
|
262
|
+
- first_char_chart
|
|
263
|
+
- samples_table
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Shared project/dbt root discovery used by render and MCP commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# Marker files that indicate a project root, checked in priority order.
|
|
11
|
+
PROJECT_MARKERS = ("dataface.yml", "dataface.yaml", "dbt_project.yml")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def discovery_boundary_for_face(
|
|
15
|
+
face_parent: Path, project_dir: Path | str | None
|
|
16
|
+
) -> Path | None:
|
|
17
|
+
"""Return the directory ceiling for upward discovery, or None to walk to the filesystem root.
|
|
18
|
+
|
|
19
|
+
When ``--project-dir`` is omitted, callers should pass ``project_dir=None`` so we can walk
|
|
20
|
+
above the face directory (required for ``…/tutorial_dbt/faces/*.yml``).
|
|
21
|
+
|
|
22
|
+
When ``project_dir`` resolves to the same directory as ``face_parent`` (e.g. cwd is the
|
|
23
|
+
faces folder and no ``--project-dir`` was passed, so Python uses ``.``), treat the
|
|
24
|
+
ceiling as unbounded so we still find ``dbt_project.yml`` in a parent folder.
|
|
25
|
+
"""
|
|
26
|
+
if project_dir is None:
|
|
27
|
+
return None
|
|
28
|
+
boundary = Path(project_dir).resolve()
|
|
29
|
+
if boundary == face_parent.resolve():
|
|
30
|
+
return None
|
|
31
|
+
return boundary
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def discover_render_context(
|
|
35
|
+
start_dir: Path,
|
|
36
|
+
boundary: Path | None,
|
|
37
|
+
) -> tuple[Path, Path | None]:
|
|
38
|
+
"""Infer project and dbt roots for a render request.
|
|
39
|
+
|
|
40
|
+
Walks upward from ``start_dir``. If ``boundary`` is None, walks until the filesystem
|
|
41
|
+
root. If set, stops after processing the ``boundary`` directory (inclusive).
|
|
42
|
+
"""
|
|
43
|
+
project_root: Path | None = None
|
|
44
|
+
dbt_project_path: Path | None = None
|
|
45
|
+
start_resolved = start_dir.resolve()
|
|
46
|
+
current = start_resolved
|
|
47
|
+
boundary_resolved = boundary.resolve() if boundary is not None else None
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
if dbt_project_path is None and (current / "dbt_project.yml").exists():
|
|
51
|
+
dbt_project_path = current
|
|
52
|
+
|
|
53
|
+
if project_root is None and (
|
|
54
|
+
(current / "dataface.yml").exists()
|
|
55
|
+
or (current / "dataface.yaml").exists()
|
|
56
|
+
or (current / "_sources.yaml").exists()
|
|
57
|
+
or (current / "_sources.yml").exists()
|
|
58
|
+
):
|
|
59
|
+
project_root = current
|
|
60
|
+
|
|
61
|
+
if current.parent == current:
|
|
62
|
+
break
|
|
63
|
+
if boundary_resolved is not None and current == boundary_resolved:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
current = current.parent
|
|
67
|
+
|
|
68
|
+
if project_root is None:
|
|
69
|
+
project_root = dbt_project_path or boundary_resolved or start_resolved
|
|
70
|
+
|
|
71
|
+
return project_root, dbt_project_path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def discover_project_root(start_dir: Path | None = None) -> Path:
|
|
75
|
+
"""Walk up from *start_dir* to find the project root for ``dft serve``.
|
|
76
|
+
|
|
77
|
+
Looks for ``dataface.yml``, ``dataface.yaml``, or ``dbt_project.yml``.
|
|
78
|
+
Returns the directory containing the first match, or *start_dir* if no
|
|
79
|
+
marker is found.
|
|
80
|
+
"""
|
|
81
|
+
start = (start_dir or Path.cwd()).resolve()
|
|
82
|
+
return find_dataface_project(start) or start
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_dataface_project(start_dir: Path) -> Path | None:
|
|
86
|
+
"""Walk up from *start_dir* looking for a Dataface or dbt project marker.
|
|
87
|
+
|
|
88
|
+
Returns the first directory containing ``dataface.yml``, ``dataface.yaml``,
|
|
89
|
+
or ``dbt_project.yml``. Returns ``None`` if no marker is found before the
|
|
90
|
+
filesystem root.
|
|
91
|
+
|
|
92
|
+
Distinct from :func:`discover_project_root`, which falls back to ``start_dir``
|
|
93
|
+
when nothing is found — that fallback is right for render context but
|
|
94
|
+
wrong for MCP project targeting, where the caller must distinguish
|
|
95
|
+
"found a project" from "user is in the wrong place."
|
|
96
|
+
"""
|
|
97
|
+
current = start_dir.resolve()
|
|
98
|
+
while True:
|
|
99
|
+
for marker in PROJECT_MARKERS:
|
|
100
|
+
if (current / marker).exists():
|
|
101
|
+
return current
|
|
102
|
+
if current.parent == current:
|
|
103
|
+
return None
|
|
104
|
+
current = current.parent
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def find_git_root(start: Path) -> Path | None:
|
|
108
|
+
"""Walk up from start looking for a .git directory.
|
|
109
|
+
|
|
110
|
+
Returns the directory containing .git, or None if not found before the
|
|
111
|
+
filesystem root.
|
|
112
|
+
"""
|
|
113
|
+
current = start.resolve()
|
|
114
|
+
while True:
|
|
115
|
+
if (current / ".git").exists():
|
|
116
|
+
return current
|
|
117
|
+
parent = current.parent
|
|
118
|
+
if parent == current:
|
|
119
|
+
return None
|
|
120
|
+
current = parent
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def infer_dialect_from_dbt(
|
|
124
|
+
project_dir: Path,
|
|
125
|
+
target_name: str | None = None,
|
|
126
|
+
) -> str | None:
|
|
127
|
+
"""Read the resolved dbt profile target and return its adapter ``type``.
|
|
128
|
+
|
|
129
|
+
Resolution order for profiles.yml:
|
|
130
|
+
1. ``project_dir/profiles.yml``
|
|
131
|
+
2. ``~/.dbt/profiles.yml``
|
|
132
|
+
|
|
133
|
+
Returns ``None`` when the dbt project or profile cannot be resolved.
|
|
134
|
+
"""
|
|
135
|
+
import yaml
|
|
136
|
+
|
|
137
|
+
dbt_project_path = project_dir / "dbt_project.yml"
|
|
138
|
+
if not dbt_project_path.exists():
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
dbt_config = yaml.safe_load(dbt_project_path.read_text()) or {}
|
|
143
|
+
except Exception: # noqa: BLE001
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
profile_name = dbt_config.get("profile")
|
|
147
|
+
if not profile_name:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Locate profiles.yml
|
|
151
|
+
profiles_path = project_dir / "profiles.yml"
|
|
152
|
+
if not profiles_path.exists():
|
|
153
|
+
profiles_path = Path.home() / ".dbt" / "profiles.yml"
|
|
154
|
+
if not profiles_path.exists():
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
profiles = yaml.safe_load(profiles_path.read_text()) or {}
|
|
159
|
+
except Exception: # noqa: BLE001
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
profile = profiles.get(profile_name, {})
|
|
163
|
+
target = target_name or profile.get("target", "dev")
|
|
164
|
+
target_config = profile.get("outputs", {}).get(target, {})
|
|
165
|
+
return target_config.get("type")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Rendering stage: Face + data → output.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Transform compiled datafaces into visual output formats.
|
|
5
|
+
|
|
6
|
+
This module provides the rendering pipeline that takes a Face
|
|
7
|
+
and data (from the execute stage) and produces output in various formats
|
|
8
|
+
(SVG, HTML, PNG, PDF, Terminal).
|
|
9
|
+
|
|
10
|
+
Main Entry Points:
|
|
11
|
+
render(): Render a full dataface
|
|
12
|
+
|
|
13
|
+
The render stage:
|
|
14
|
+
1. Takes a Face (from compile stage)
|
|
15
|
+
2. Uses Executor to fetch data lazily as needed
|
|
16
|
+
3. Generates Vega-Lite specs for charts
|
|
17
|
+
4. Produces output in requested format
|
|
18
|
+
|
|
19
|
+
Supported Formats:
|
|
20
|
+
- svg: Scalable vector graphics (default)
|
|
21
|
+
- html: Interactive HTML pages with embedded charts
|
|
22
|
+
- png: Raster image format
|
|
23
|
+
- pdf: PDF documents
|
|
24
|
+
- terminal: Terminal output with ASCII/Unicode charts
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataface.core.render.chart import (
|
|
28
|
+
generate_vega_lite_spec,
|
|
29
|
+
render_table_svg,
|
|
30
|
+
slug_to_text,
|
|
31
|
+
)
|
|
32
|
+
from dataface.core.render.control_registry import ControlRegistry
|
|
33
|
+
from dataface.core.render.errors import (
|
|
34
|
+
ChartDataError,
|
|
35
|
+
MissingRequiredVariablesError,
|
|
36
|
+
MissingVariable,
|
|
37
|
+
RenderError,
|
|
38
|
+
)
|
|
39
|
+
from dataface.core.render.render_result import RenderResult
|
|
40
|
+
from dataface.core.render.renderer import render
|
|
41
|
+
|
|
42
|
+
# Terminal rendering
|
|
43
|
+
from dataface.core.render.terminal import (
|
|
44
|
+
render_chart_item_terminal,
|
|
45
|
+
render_face_terminal,
|
|
46
|
+
render_layout_item_terminal,
|
|
47
|
+
)
|
|
48
|
+
from dataface.core.render.terminal_charts import (
|
|
49
|
+
render_chart_terminal,
|
|
50
|
+
render_kpi_terminal,
|
|
51
|
+
render_table_terminal,
|
|
52
|
+
)
|
|
53
|
+
from dataface.core.render.variable_controls import (
|
|
54
|
+
generate_svg_variable_icons,
|
|
55
|
+
generate_svg_variable_script,
|
|
56
|
+
render_interactive_variables_svg,
|
|
57
|
+
render_variables_svg,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Main functions
|
|
62
|
+
"render",
|
|
63
|
+
"RenderResult",
|
|
64
|
+
# Errors
|
|
65
|
+
"ChartDataError",
|
|
66
|
+
"MissingRequiredVariablesError",
|
|
67
|
+
"MissingVariable",
|
|
68
|
+
"RenderError",
|
|
69
|
+
# Variable controls
|
|
70
|
+
"render_variables_svg",
|
|
71
|
+
"render_interactive_variables_svg",
|
|
72
|
+
"generate_svg_variable_script",
|
|
73
|
+
"generate_svg_variable_icons",
|
|
74
|
+
"ControlRegistry",
|
|
75
|
+
# Vega-Lite
|
|
76
|
+
"generate_vega_lite_spec",
|
|
77
|
+
"render_chart",
|
|
78
|
+
"render_table_svg",
|
|
79
|
+
"slug_to_text",
|
|
80
|
+
# Terminal rendering
|
|
81
|
+
"render_face_terminal",
|
|
82
|
+
"render_layout_item_terminal",
|
|
83
|
+
"render_chart_item_terminal",
|
|
84
|
+
"render_chart_terminal",
|
|
85
|
+
"render_table_terminal",
|
|
86
|
+
"render_kpi_terminal",
|
|
87
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Board link resolution.
|
|
2
|
+
|
|
3
|
+
Rewrites markdown board links at render time so authors can use
|
|
4
|
+
dashboard-root-relative paths (e.g. ``/zendesk/tickets/list?status=open``)
|
|
5
|
+
that work in both ``dft serve`` and Cloud.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from contextvars import ContextVar
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from posixpath import normpath
|
|
14
|
+
|
|
15
|
+
# Markdown link pattern: [text](url), excluding image syntax 
|
|
16
|
+
_MD_LINK_RE = re.compile(r"(?<!!)\[([^\]]*)\]\(([^)]+)\)")
|
|
17
|
+
|
|
18
|
+
# Suffixes stripped as author sugar
|
|
19
|
+
_STRIP_SUFFIXES = (".md", ".yml", ".yaml")
|
|
20
|
+
|
|
21
|
+
# Schemes that bypass rewriting. cursor://, vscode://, file:// pass through so
|
|
22
|
+
# editor-deeplink URLs (e.g. ``cursor://file/{abs}/face.yaml`` from the compare
|
|
23
|
+
# UI) reach the browser unchanged — both the scheme and the .yaml suffix.
|
|
24
|
+
# "?" passes through so variable-update links (?var=value) reach variables.js
|
|
25
|
+
# unchanged instead of being treated as relative board paths.
|
|
26
|
+
_PASSTHROUGH_PREFIXES = (
|
|
27
|
+
"http://",
|
|
28
|
+
"https://",
|
|
29
|
+
"mailto:",
|
|
30
|
+
"#",
|
|
31
|
+
"?",
|
|
32
|
+
"command:",
|
|
33
|
+
"cursor://",
|
|
34
|
+
"vscode://",
|
|
35
|
+
"file://",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class LinkContext:
|
|
41
|
+
"""Runtime context for resolving board links.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
runtime: ``"serve"`` or ``"cloud"``.
|
|
45
|
+
current_board_slug: Author-space slug of the board being rendered
|
|
46
|
+
(e.g. ``"zendesk/tickets/list"``).
|
|
47
|
+
storage_prefix: On-disk directory prefix for serve mode (default ``"faces"``).
|
|
48
|
+
org_slug: Cloud organization slug (Cloud only).
|
|
49
|
+
project_slug: Cloud project slug (Cloud only).
|
|
50
|
+
branch: Current branch name to merge into outbound links (Cloud only).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
runtime: str # "serve" | "cloud"
|
|
54
|
+
current_board_slug: str = ""
|
|
55
|
+
storage_prefix: str = "faces"
|
|
56
|
+
org_slug: str = ""
|
|
57
|
+
project_slug: str = ""
|
|
58
|
+
branch: str = ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_href(href: str, ctx: LinkContext) -> str:
|
|
62
|
+
"""Resolve a single href against *ctx*.
|
|
63
|
+
|
|
64
|
+
- External URLs (``http:``, ``https:``, ``mailto:``, ``#``) pass through.
|
|
65
|
+
- Query-string-only URLs (``?…``) pass through unchanged for variables.js interception.
|
|
66
|
+
- Root-relative (``/path``) maps from author namespace.
|
|
67
|
+
- Relative (``../``, ``./``, bare) resolves against current board directory.
|
|
68
|
+
- ``.md`` / ``.yml`` / ``.yaml`` suffixes are stripped.
|
|
69
|
+
- Cloud URLs get ``/{org}/{project}/d/{slug}/`` shape + branch merge.
|
|
70
|
+
- Serve URLs get ``/{storage_prefix}/{slug}`` shape.
|
|
71
|
+
"""
|
|
72
|
+
for prefix in _PASSTHROUGH_PREFIXES:
|
|
73
|
+
if href.startswith(prefix):
|
|
74
|
+
return href
|
|
75
|
+
|
|
76
|
+
# Split path and query (can't use urlparse fully — Jinja braces aren't valid)
|
|
77
|
+
path, query = _split_path_query(href)
|
|
78
|
+
|
|
79
|
+
# Resolve path
|
|
80
|
+
if path.startswith("/"):
|
|
81
|
+
# Root-relative: strip leading slash
|
|
82
|
+
resolved = path.lstrip("/")
|
|
83
|
+
else:
|
|
84
|
+
# Relative: resolve against current board's directory
|
|
85
|
+
board_dir = "/".join(ctx.current_board_slug.split("/")[:-1])
|
|
86
|
+
resolved = normpath(f"{board_dir}/{path}") if board_dir else normpath(path)
|
|
87
|
+
# normpath may produce leading ".." — clamp to root
|
|
88
|
+
if resolved.startswith(".."):
|
|
89
|
+
resolved = resolved.lstrip("./")
|
|
90
|
+
|
|
91
|
+
# Strip author-sugar suffixes
|
|
92
|
+
for suffix in _STRIP_SUFFIXES:
|
|
93
|
+
if resolved.endswith(suffix):
|
|
94
|
+
resolved = resolved[: -len(suffix)]
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
# Build final URL based on runtime
|
|
98
|
+
if ctx.runtime == "cloud":
|
|
99
|
+
return _build_cloud_url(resolved, query, ctx)
|
|
100
|
+
return _build_serve_url(resolved, query, ctx)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _split_path_query(href: str) -> tuple[str, str]:
|
|
104
|
+
"""Split href into (path, query) preserving Jinja templates in query."""
|
|
105
|
+
idx = href.find("?")
|
|
106
|
+
if idx == -1:
|
|
107
|
+
return href, ""
|
|
108
|
+
return href[:idx], href[idx + 1 :]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_serve_url(slug: str, query: str, ctx: LinkContext) -> str:
|
|
112
|
+
if ctx.storage_prefix:
|
|
113
|
+
url = f"/{ctx.storage_prefix}/{slug}"
|
|
114
|
+
else:
|
|
115
|
+
url = f"/{slug}" if slug else "/"
|
|
116
|
+
if query:
|
|
117
|
+
url = f"{url}?{query}"
|
|
118
|
+
return url
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_cloud_url(slug: str, query: str, ctx: LinkContext) -> str:
|
|
122
|
+
base = f"/{ctx.org_slug}/{ctx.project_slug}/d/{slug}/"
|
|
123
|
+
|
|
124
|
+
# Merge branch if present in context and not already in query
|
|
125
|
+
if ctx.branch and not _query_has_key(query, "branch"):
|
|
126
|
+
separator = "&" if query else ""
|
|
127
|
+
query = f"{query}{separator}branch={ctx.branch}"
|
|
128
|
+
|
|
129
|
+
if query:
|
|
130
|
+
base = f"{base}?{query}"
|
|
131
|
+
return base
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _query_has_key(query: str, key: str) -> bool:
|
|
135
|
+
"""Check if *key* appears in a query string (handles Jinja-laden strings)."""
|
|
136
|
+
# Simple prefix check — good enough for branch= detection
|
|
137
|
+
return f"{key}=" in query
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Markdown-level rewriting
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def rewrite_board_links(markdown: str, ctx: LinkContext | None) -> str:
|
|
146
|
+
"""Rewrite board links in *markdown* using *ctx*.
|
|
147
|
+
|
|
148
|
+
Returns *markdown* unchanged when *ctx* is ``None``.
|
|
149
|
+
"""
|
|
150
|
+
if ctx is None:
|
|
151
|
+
return markdown
|
|
152
|
+
|
|
153
|
+
def _replace(m: re.Match[str]) -> str:
|
|
154
|
+
text, href = m.group(1), m.group(2)
|
|
155
|
+
return f"[{text}]({resolve_href(href, ctx)})"
|
|
156
|
+
|
|
157
|
+
return _MD_LINK_RE.sub(_replace, markdown)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Render-scoped context variable
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
_current_link_context: ContextVar[LinkContext | None] = ContextVar(
|
|
165
|
+
"_current_link_context", default=None
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_link_context() -> LinkContext | None:
|
|
170
|
+
"""Return the active link context (set by the renderer)."""
|
|
171
|
+
return _current_link_context.get()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def set_link_context(ctx: LinkContext | None) -> None:
|
|
175
|
+
"""Set the active link context for the current render pass."""
|
|
176
|
+
_current_link_context.set(ctx)
|