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,938 @@
|
|
|
1
|
+
"""Unified Dataface server.
|
|
2
|
+
|
|
3
|
+
Stage: SERVE
|
|
4
|
+
Purpose: Single HTTP server for all Dataface rendering.
|
|
5
|
+
|
|
6
|
+
Routes:
|
|
7
|
+
- /health - Health check
|
|
8
|
+
- /templates - List available inspect templates
|
|
9
|
+
- /{path}/?var=value - Any face file (path.yml → /path/)
|
|
10
|
+
|
|
11
|
+
For /inspect/* paths, the server checks faces/inspect/{template}.yml first,
|
|
12
|
+
then falls back to built-in templates with Jinja2 pre-processing.
|
|
13
|
+
|
|
14
|
+
Example URLs:
|
|
15
|
+
- /health
|
|
16
|
+
- /inspect/model/?model=fct_orders&theme=dark
|
|
17
|
+
- /inspect/numeric_column/?model=fct_orders&column=revenue
|
|
18
|
+
- /sales/?region=West (renders faces/sales.yml when faces_at_root=True)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import importlib.util
|
|
22
|
+
import logging
|
|
23
|
+
import textwrap
|
|
24
|
+
from collections.abc import AsyncGenerator
|
|
25
|
+
from contextlib import asynccontextmanager
|
|
26
|
+
from html import escape as html_escape
|
|
27
|
+
from importlib.resources import files
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
_SUPER_SCHEMA_AVAILABLE: bool = (
|
|
32
|
+
importlib.util.find_spec("dataface_super_schema") is not None
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
36
|
+
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
|
37
|
+
from fastapi.staticfiles import StaticFiles
|
|
38
|
+
from jinja2 import Environment, select_autoescape
|
|
39
|
+
|
|
40
|
+
from dataface.core.compile.errors import DatafaceError
|
|
41
|
+
from dataface.core.dashboard import RenderedDashboard, render_dashboard
|
|
42
|
+
from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
|
|
43
|
+
from dataface.core.execute.adapters.adapter_registry import (
|
|
44
|
+
LOCAL_AUTHORING_REGISTRY_KWARGS,
|
|
45
|
+
)
|
|
46
|
+
from dataface.core.execute.duckdb_cache import DuckDBCache, open_cache_from_env
|
|
47
|
+
from dataface.core.fonts import get_inter_font_path
|
|
48
|
+
from dataface.core.inspect import INSPECT_TEMPLATES
|
|
49
|
+
from dataface.core.inspect.renderer import (
|
|
50
|
+
InspectProfileCompileError,
|
|
51
|
+
render_inspect_dashboard,
|
|
52
|
+
validate_inspect_variables,
|
|
53
|
+
)
|
|
54
|
+
from dataface.core.project_roots import (
|
|
55
|
+
discover_render_context,
|
|
56
|
+
discovery_boundary_for_face,
|
|
57
|
+
)
|
|
58
|
+
from dataface.core.render.errors import MissingVariable
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
# Module-level cache singleton — opened once at first request, closed on shutdown.
|
|
63
|
+
_cache: DuckDBCache | None = None
|
|
64
|
+
_cache_initialised = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_default_cache() -> DuckDBCache | None:
|
|
68
|
+
"""Return the module-level cache, opening it on first call."""
|
|
69
|
+
global _cache, _cache_initialised
|
|
70
|
+
if not _cache_initialised:
|
|
71
|
+
_cache = open_cache_from_env()
|
|
72
|
+
_cache_initialised = True
|
|
73
|
+
return _cache
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_within_project(candidate: Path, project_dir: Path) -> Path:
|
|
77
|
+
"""Reject paths that escape the project root."""
|
|
78
|
+
try:
|
|
79
|
+
candidate.resolve().relative_to(project_dir.resolve())
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
raise HTTPException(
|
|
82
|
+
status_code=403, detail="Path outside project directory"
|
|
83
|
+
) from e
|
|
84
|
+
return candidate
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_link_context(
|
|
88
|
+
file_path: Path,
|
|
89
|
+
project_dir: Path,
|
|
90
|
+
faces_at_root: bool,
|
|
91
|
+
serve_storage_prefix: str | None = None,
|
|
92
|
+
) -> Any:
|
|
93
|
+
"""Build board-link rewrite context for a served face."""
|
|
94
|
+
from dataface.core.render.board_links import LinkContext
|
|
95
|
+
|
|
96
|
+
rel = file_path.relative_to(project_dir)
|
|
97
|
+
rel_no_ext = rel.with_suffix("")
|
|
98
|
+
parts = rel_no_ext.parts
|
|
99
|
+
storage_prefix = parts[0] if len(parts) > 1 else "faces"
|
|
100
|
+
board_slug = "/".join(parts[1:]) if len(parts) > 1 else str(rel_no_ext)
|
|
101
|
+
if faces_at_root and parts and parts[0] == "faces":
|
|
102
|
+
storage_prefix = ""
|
|
103
|
+
board_slug = "/".join(parts[1:])
|
|
104
|
+
elif faces_at_root:
|
|
105
|
+
storage_prefix = ""
|
|
106
|
+
if serve_storage_prefix is not None:
|
|
107
|
+
storage_prefix = serve_storage_prefix.strip("/")
|
|
108
|
+
return LinkContext(
|
|
109
|
+
runtime="serve",
|
|
110
|
+
current_board_slug=board_slug,
|
|
111
|
+
storage_prefix=storage_prefix,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _apply_server_defaults(
|
|
116
|
+
variables: dict[str, Any],
|
|
117
|
+
connection: str | None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Apply connection default to request variables."""
|
|
120
|
+
resolved = dict(variables)
|
|
121
|
+
if "connection" not in resolved and connection:
|
|
122
|
+
resolved["connection"] = connection
|
|
123
|
+
return resolved
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _resolve_root_index_face(project_dir: Path) -> Path | None:
|
|
127
|
+
"""Return the project-level index face for the root URL, if present."""
|
|
128
|
+
for candidate in (
|
|
129
|
+
project_dir / "index.yml",
|
|
130
|
+
project_dir / "index.yaml",
|
|
131
|
+
project_dir / "index.md",
|
|
132
|
+
):
|
|
133
|
+
if candidate.exists():
|
|
134
|
+
return candidate
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _directory_candidates(
|
|
139
|
+
project_dir: Path,
|
|
140
|
+
faces_dir: Path,
|
|
141
|
+
clean_path: str,
|
|
142
|
+
faces_at_root: bool,
|
|
143
|
+
) -> list[tuple[Path, str]]:
|
|
144
|
+
"""Build candidate directories to render for the request path."""
|
|
145
|
+
candidates: list[tuple[Path, str]] = []
|
|
146
|
+
if clean_path:
|
|
147
|
+
candidates.append((project_dir / clean_path, clean_path))
|
|
148
|
+
if faces_at_root and not clean_path.startswith("faces/"):
|
|
149
|
+
candidates.append((faces_dir / clean_path, clean_path))
|
|
150
|
+
return candidates
|
|
151
|
+
if faces_at_root and faces_dir.is_dir():
|
|
152
|
+
candidates.append((faces_dir, ""))
|
|
153
|
+
candidates.append((project_dir, clean_path))
|
|
154
|
+
return candidates
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_face_file_path(
|
|
158
|
+
project_dir: Path,
|
|
159
|
+
faces_dir: Path,
|
|
160
|
+
clean_path: str,
|
|
161
|
+
faces_at_root: bool,
|
|
162
|
+
) -> Path:
|
|
163
|
+
"""Resolve the request path to a face file path, preserving existing fallback order."""
|
|
164
|
+
file_candidates: list[Path] = [
|
|
165
|
+
project_dir / f"{clean_path}.yml",
|
|
166
|
+
project_dir / f"{clean_path}.yaml",
|
|
167
|
+
project_dir / f"{clean_path}.md",
|
|
168
|
+
project_dir / clean_path,
|
|
169
|
+
]
|
|
170
|
+
if faces_at_root and not clean_path.startswith("faces/"):
|
|
171
|
+
file_candidates.extend(
|
|
172
|
+
[
|
|
173
|
+
faces_dir / f"{clean_path}.yml",
|
|
174
|
+
faces_dir / f"{clean_path}.yaml",
|
|
175
|
+
faces_dir / f"{clean_path}.md",
|
|
176
|
+
faces_dir / clean_path,
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
file_path: Path | None = None
|
|
181
|
+
first_yml_candidate: Path | None = None
|
|
182
|
+
for candidate in file_candidates:
|
|
183
|
+
_ensure_within_project(candidate, project_dir)
|
|
184
|
+
if candidate.exists():
|
|
185
|
+
if candidate.suffix in (".yml", ".yaml", ".md"):
|
|
186
|
+
file_path = candidate
|
|
187
|
+
break
|
|
188
|
+
elif candidate.suffix == ".yml" and first_yml_candidate is None:
|
|
189
|
+
first_yml_candidate = candidate
|
|
190
|
+
|
|
191
|
+
if file_path is not None:
|
|
192
|
+
return file_path
|
|
193
|
+
return first_yml_candidate or (project_dir / f"{clean_path}.yml")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_error_jinja_template: Any = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _error_template() -> Any:
|
|
200
|
+
"""Load the structured-error Jinja2 template (lazy, cached at module level)."""
|
|
201
|
+
global _error_jinja_template
|
|
202
|
+
if _error_jinja_template is None:
|
|
203
|
+
raw = (
|
|
204
|
+
files("dataface.core.serve.templates").joinpath("error.html.j2").read_text()
|
|
205
|
+
)
|
|
206
|
+
env = Environment(autoescape=select_autoescape(["html", "j2"]))
|
|
207
|
+
_error_jinja_template = env.from_string(raw)
|
|
208
|
+
return _error_jinja_template
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render_structured_errors_html(
|
|
212
|
+
result: RenderedDashboard,
|
|
213
|
+
request_path: str,
|
|
214
|
+
status_override: int | None = None,
|
|
215
|
+
) -> HTMLResponse:
|
|
216
|
+
"""Render a RenderedDashboard error result as a structured HTML page."""
|
|
217
|
+
if status_override is not None:
|
|
218
|
+
status = status_override
|
|
219
|
+
elif result.face_error is not None:
|
|
220
|
+
status = 500 if result.face_error.code.startswith("DF-UNKNOWN-") else 422
|
|
221
|
+
elif result.validation_errors:
|
|
222
|
+
status = 422
|
|
223
|
+
elif result.chart_errors:
|
|
224
|
+
status = 200
|
|
225
|
+
else:
|
|
226
|
+
status = 200
|
|
227
|
+
|
|
228
|
+
face_html = ""
|
|
229
|
+
if result.chart_errors and result.data:
|
|
230
|
+
data = result.data
|
|
231
|
+
face_html = data.decode("utf-8") if isinstance(data, bytes) else str(data)
|
|
232
|
+
|
|
233
|
+
html = _error_template().render(
|
|
234
|
+
result=result,
|
|
235
|
+
request_path=request_path,
|
|
236
|
+
face_html=face_html,
|
|
237
|
+
)
|
|
238
|
+
return HTMLResponse(content=html, status_code=status)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _synthetic_error(message: str) -> RenderedDashboard:
|
|
242
|
+
"""Build a RenderedDashboard with a synthetic DF-UNKNOWN-INTERNAL face_error."""
|
|
243
|
+
return RenderedDashboard(
|
|
244
|
+
success=False,
|
|
245
|
+
face_error=StructuredError(
|
|
246
|
+
code="DF-UNKNOWN-INTERNAL",
|
|
247
|
+
message=message,
|
|
248
|
+
domain="unknown",
|
|
249
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _render_face_file(
|
|
255
|
+
file_path: Path,
|
|
256
|
+
variables: dict[str, Any],
|
|
257
|
+
project_dir: Path,
|
|
258
|
+
connection: str | None = None,
|
|
259
|
+
dialect: str = "duckdb",
|
|
260
|
+
faces_at_root: bool = False,
|
|
261
|
+
target: str | None = None,
|
|
262
|
+
serve_storage_prefix: str | None = None,
|
|
263
|
+
read_only: bool = True,
|
|
264
|
+
allow_external_access_in_readonly: bool = False,
|
|
265
|
+
duckdb_config: dict[str, Any] | None = None,
|
|
266
|
+
) -> Response:
|
|
267
|
+
"""Render a face file to HTML."""
|
|
268
|
+
if not file_path.exists():
|
|
269
|
+
raise HTTPException(status_code=404, detail=f"Face file not found: {file_path}")
|
|
270
|
+
|
|
271
|
+
project_root, dbt_project_path = discover_render_context(
|
|
272
|
+
file_path.parent,
|
|
273
|
+
discovery_boundary_for_face(file_path.parent, project_dir),
|
|
274
|
+
)
|
|
275
|
+
working_dir = dbt_project_path or project_root
|
|
276
|
+
|
|
277
|
+
from dataface.core.compile.config import invalidate_project_sources
|
|
278
|
+
from dataface.core.execute.adapters import build_adapter_registry
|
|
279
|
+
|
|
280
|
+
invalidate_project_sources(working_dir)
|
|
281
|
+
|
|
282
|
+
adapter_registry = build_adapter_registry(
|
|
283
|
+
project_root,
|
|
284
|
+
read_only=read_only,
|
|
285
|
+
allow_external_access_in_readonly=allow_external_access_in_readonly,
|
|
286
|
+
duckdb_config=duckdb_config,
|
|
287
|
+
dbt_project_path=dbt_project_path,
|
|
288
|
+
connection_string=connection,
|
|
289
|
+
profile_type=dialect,
|
|
290
|
+
target=target,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
link_context = _build_link_context(
|
|
294
|
+
file_path,
|
|
295
|
+
project_dir=project_dir,
|
|
296
|
+
faces_at_root=faces_at_root,
|
|
297
|
+
serve_storage_prefix=serve_storage_prefix,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
result = render_dashboard(
|
|
301
|
+
path=file_path,
|
|
302
|
+
variables=variables,
|
|
303
|
+
adapter_registry=adapter_registry,
|
|
304
|
+
format="html",
|
|
305
|
+
link_context=link_context,
|
|
306
|
+
duckdb_cache=_get_default_cache(),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if result.success and not result.chart_errors:
|
|
310
|
+
data = result.data
|
|
311
|
+
html = data.decode("utf-8") if isinstance(data, bytes) else str(data)
|
|
312
|
+
return HTMLResponse(content=html)
|
|
313
|
+
|
|
314
|
+
# MissingRequiredVariablesError comes through render_dashboard as a face_error
|
|
315
|
+
# with fields["missing"] populated. Render the HTML prompt card (200) so the
|
|
316
|
+
# browser shows the blocking form — same UX surface the Cloud face viewer uses.
|
|
317
|
+
if result.face_error is not None and result.face_error.fields.get("missing"):
|
|
318
|
+
from dataface.core.render.missing_vars_prompt import (
|
|
319
|
+
render_missing_variables_prompt,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
missing = [MissingVariable(**d) for d in result.face_error.fields["missing"]]
|
|
323
|
+
prompt_card = render_missing_variables_prompt(
|
|
324
|
+
missing,
|
|
325
|
+
# Empty action resubmits to the current URL (GET with query params)
|
|
326
|
+
form_action="",
|
|
327
|
+
form_method="get",
|
|
328
|
+
)
|
|
329
|
+
return HTMLResponse(
|
|
330
|
+
content=_render_prompt_page(
|
|
331
|
+
f"Configuration required — {file_path.name}",
|
|
332
|
+
prompt_card,
|
|
333
|
+
),
|
|
334
|
+
status_code=200,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return _render_structured_errors_html(result, request_path=str(file_path.name))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _handle_inspect_route(
|
|
341
|
+
template_name: str,
|
|
342
|
+
variables: dict[str, Any],
|
|
343
|
+
project_dir: Path,
|
|
344
|
+
connection: str | None = None,
|
|
345
|
+
target: str | None = None,
|
|
346
|
+
read_only: bool = False,
|
|
347
|
+
) -> Response:
|
|
348
|
+
"""Handle /inspect/* routes with custom template fallback.
|
|
349
|
+
|
|
350
|
+
The inspect HTML server is DuckDB-only. Project templates under
|
|
351
|
+
``faces/inspect/`` use the same Jinja-then-compile pipeline as the
|
|
352
|
+
built-in copies (``dft init`` ejects Jinja-heavy YAML).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
template_name: Name of the inspect template (e.g., "model" or "model.yml")
|
|
356
|
+
variables: Variables from query params
|
|
357
|
+
project_dir: Project directory for custom template lookup
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Rendered HTML response
|
|
361
|
+
"""
|
|
362
|
+
_ = (
|
|
363
|
+
target,
|
|
364
|
+
read_only,
|
|
365
|
+
) # Inspect HTML uses render_inspect_dashboard; registry is fixed.
|
|
366
|
+
|
|
367
|
+
template_name = template_name.strip().strip("/")
|
|
368
|
+
if template_name.endswith((".yml", ".yaml")):
|
|
369
|
+
template_name = template_name[: template_name.rindex(".")]
|
|
370
|
+
|
|
371
|
+
custom_path = project_dir / "faces" / "inspect" / f"{template_name}.yml"
|
|
372
|
+
has_custom = custom_path.is_file()
|
|
373
|
+
|
|
374
|
+
if not has_custom and template_name not in INSPECT_TEMPLATES:
|
|
375
|
+
return _render_structured_errors_html(
|
|
376
|
+
_synthetic_error(
|
|
377
|
+
f"Inspect template '{template_name}' not found. "
|
|
378
|
+
f"Available: {INSPECT_TEMPLATES}"
|
|
379
|
+
),
|
|
380
|
+
request_path=f"/inspect/{template_name}/",
|
|
381
|
+
status_override=404,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
variables = validate_inspect_variables(variables)
|
|
386
|
+
except ValueError as e:
|
|
387
|
+
return _render_structured_errors_html(
|
|
388
|
+
RenderedDashboard(
|
|
389
|
+
success=False,
|
|
390
|
+
validation_errors=[
|
|
391
|
+
StructuredError(
|
|
392
|
+
code="DF-UNKNOWN-INTERNAL",
|
|
393
|
+
message=str(e),
|
|
394
|
+
domain="unknown",
|
|
395
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
396
|
+
)
|
|
397
|
+
],
|
|
398
|
+
),
|
|
399
|
+
request_path=f"/inspect/{template_name}/",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
template_file = f"{template_name}.yml"
|
|
403
|
+
if has_custom:
|
|
404
|
+
try:
|
|
405
|
+
template_yaml = custom_path.read_text(encoding="utf-8")
|
|
406
|
+
except FileNotFoundError:
|
|
407
|
+
return _render_structured_errors_html(
|
|
408
|
+
_synthetic_error(f"Inspect template file not found: {template_file}"),
|
|
409
|
+
request_path=f"/inspect/{template_name}/",
|
|
410
|
+
status_override=404,
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
template_path = files("dataface.core.inspect.templates").joinpath(template_file)
|
|
414
|
+
try:
|
|
415
|
+
template_yaml = template_path.read_text()
|
|
416
|
+
except FileNotFoundError:
|
|
417
|
+
return _render_structured_errors_html(
|
|
418
|
+
_synthetic_error(f"Template file '{template_file}' not found"),
|
|
419
|
+
request_path=f"/inspect/{template_name}/",
|
|
420
|
+
status_override=404,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
return HTMLResponse(
|
|
425
|
+
content=render_inspect_dashboard(
|
|
426
|
+
template_yaml,
|
|
427
|
+
variables,
|
|
428
|
+
project_root=project_dir,
|
|
429
|
+
connection=connection,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
except InspectProfileCompileError as e:
|
|
433
|
+
# Stamp file context for custom profile templates so the validate button appears.
|
|
434
|
+
from dataface.core.dashboard import _stamp_file_commands
|
|
435
|
+
|
|
436
|
+
errs = (
|
|
437
|
+
_stamp_file_commands(e.result.errors, custom_path)
|
|
438
|
+
if has_custom
|
|
439
|
+
else list(e.result.errors)
|
|
440
|
+
)
|
|
441
|
+
return _render_structured_errors_html(
|
|
442
|
+
RenderedDashboard(
|
|
443
|
+
success=False,
|
|
444
|
+
validation_errors=errs,
|
|
445
|
+
warnings=list(e.result.warnings),
|
|
446
|
+
suppressed_warnings=list(e.result.suppressed_warnings),
|
|
447
|
+
),
|
|
448
|
+
request_path=f"/inspect/{template_name}/",
|
|
449
|
+
)
|
|
450
|
+
except (DatafaceError, ValueError) as e:
|
|
451
|
+
error_msg = str(e)
|
|
452
|
+
# Surface table-not-found errors as 404 for better UX
|
|
453
|
+
if "does not exist" in error_msg:
|
|
454
|
+
return _render_structured_errors_html(
|
|
455
|
+
_synthetic_error(error_msg),
|
|
456
|
+
request_path=f"/inspect/{template_name}/",
|
|
457
|
+
status_override=404,
|
|
458
|
+
)
|
|
459
|
+
log_label = "custom" if has_custom else "built-in"
|
|
460
|
+
logger.exception("Error rendering %s inspect template", log_label)
|
|
461
|
+
err: RenderedDashboard
|
|
462
|
+
if isinstance(e, DatafaceError):
|
|
463
|
+
err = RenderedDashboard(success=False, face_error=e.to_structured())
|
|
464
|
+
else:
|
|
465
|
+
err = _synthetic_error(error_msg)
|
|
466
|
+
return _render_structured_errors_html(
|
|
467
|
+
err, request_path=f"/inspect/{template_name}/"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _render_prompt_page(title: str, prompt_card_html: str) -> str:
|
|
472
|
+
"""Wrap a prompt card in a standalone styled HTML page for dft serve."""
|
|
473
|
+
safe_title = html_escape(title, quote=True)
|
|
474
|
+
return textwrap.dedent(
|
|
475
|
+
f"""\
|
|
476
|
+
<!doctype html>
|
|
477
|
+
<html lang="en">
|
|
478
|
+
<head>
|
|
479
|
+
<meta charset="utf-8">
|
|
480
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
481
|
+
<title>{safe_title}</title>
|
|
482
|
+
<style>
|
|
483
|
+
:root {{
|
|
484
|
+
color-scheme: light;
|
|
485
|
+
--page-bg: #f6f2e7;
|
|
486
|
+
--panel-bg: #fffdf8;
|
|
487
|
+
--text: #2f2a22;
|
|
488
|
+
--muted: #6d6558;
|
|
489
|
+
--border: #d7c7aa;
|
|
490
|
+
--accent: #8d4d00;
|
|
491
|
+
}}
|
|
492
|
+
body {{
|
|
493
|
+
margin: 0;
|
|
494
|
+
padding: 40px 20px;
|
|
495
|
+
background: var(--page-bg);
|
|
496
|
+
color: var(--text);
|
|
497
|
+
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
|
498
|
+
}}
|
|
499
|
+
.missing-variables-prompt {{
|
|
500
|
+
max-width: 640px;
|
|
501
|
+
margin: 0 auto;
|
|
502
|
+
}}
|
|
503
|
+
.missing-variables-card {{
|
|
504
|
+
background: var(--panel-bg);
|
|
505
|
+
border: 1px solid var(--border);
|
|
506
|
+
border-radius: 18px;
|
|
507
|
+
padding: 28px 32px;
|
|
508
|
+
box-shadow: 0 12px 32px rgba(58, 41, 16, 0.08);
|
|
509
|
+
}}
|
|
510
|
+
h2 {{
|
|
511
|
+
margin: 0 0 8px;
|
|
512
|
+
color: var(--accent);
|
|
513
|
+
font-size: 1.5rem;
|
|
514
|
+
}}
|
|
515
|
+
p {{
|
|
516
|
+
margin: 0 0 20px;
|
|
517
|
+
color: var(--muted);
|
|
518
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
519
|
+
}}
|
|
520
|
+
.missing-var-field {{
|
|
521
|
+
margin-bottom: 16px;
|
|
522
|
+
}}
|
|
523
|
+
label {{
|
|
524
|
+
display: block;
|
|
525
|
+
font-weight: 600;
|
|
526
|
+
margin-bottom: 4px;
|
|
527
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
528
|
+
font-size: 0.9rem;
|
|
529
|
+
}}
|
|
530
|
+
.missing-var-description {{
|
|
531
|
+
color: var(--muted);
|
|
532
|
+
font-size: 0.85rem;
|
|
533
|
+
margin: 0 0 4px;
|
|
534
|
+
}}
|
|
535
|
+
.missing-var-input {{
|
|
536
|
+
width: 100%;
|
|
537
|
+
padding: 8px 12px;
|
|
538
|
+
border: 1px solid var(--border);
|
|
539
|
+
border-radius: 8px;
|
|
540
|
+
font-size: 1rem;
|
|
541
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
542
|
+
box-sizing: border-box;
|
|
543
|
+
}}
|
|
544
|
+
.missing-variables-submit {{
|
|
545
|
+
margin-top: 8px;
|
|
546
|
+
padding: 10px 24px;
|
|
547
|
+
background: var(--accent);
|
|
548
|
+
color: #fff;
|
|
549
|
+
border: none;
|
|
550
|
+
border-radius: 8px;
|
|
551
|
+
font-size: 1rem;
|
|
552
|
+
cursor: pointer;
|
|
553
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
554
|
+
}}
|
|
555
|
+
.missing-variables-submit:hover {{
|
|
556
|
+
opacity: 0.9;
|
|
557
|
+
}}
|
|
558
|
+
</style>
|
|
559
|
+
</head>
|
|
560
|
+
<body>
|
|
561
|
+
{prompt_card_html}
|
|
562
|
+
</body>
|
|
563
|
+
</html>
|
|
564
|
+
"""
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _render_directory_listing(dir_path: Path, url_path: str) -> HTMLResponse:
|
|
569
|
+
"""Render a directory listing through a built-in dataface face."""
|
|
570
|
+
from dataface.core.compile import compile
|
|
571
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
572
|
+
from dataface.core.execute import Executor
|
|
573
|
+
from dataface.core.execute.adapters import build_adapter_registry
|
|
574
|
+
from dataface.core.render import render
|
|
575
|
+
|
|
576
|
+
listing_lines: list[str] = []
|
|
577
|
+
if url_path:
|
|
578
|
+
parent_parts = url_path.strip("/").split("/")[:-1]
|
|
579
|
+
parent_url = "/" + "/".join(parent_parts)
|
|
580
|
+
listing_lines.append(f"- [../]({parent_url or '/'})")
|
|
581
|
+
|
|
582
|
+
for entry in sorted(
|
|
583
|
+
dir_path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())
|
|
584
|
+
):
|
|
585
|
+
if entry.name.startswith("."):
|
|
586
|
+
continue
|
|
587
|
+
# /inspect/<template> links require ?model=...&column=... query params;
|
|
588
|
+
# advertising them as bare entries in the root listing 500s on click.
|
|
589
|
+
if entry.name == "inspect" and not url_path:
|
|
590
|
+
continue
|
|
591
|
+
item_url = f"/{url_path}/{entry.name}".replace("//", "/")
|
|
592
|
+
if entry.is_dir():
|
|
593
|
+
listing_lines.append(f"- [{entry.name}/]({item_url}/)")
|
|
594
|
+
elif entry.suffix in (".yml", ".yaml", ".md"):
|
|
595
|
+
listing_lines.append(
|
|
596
|
+
f"- [{entry.name}]({item_url.removesuffix(entry.suffix)})"
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
template_path = files("dataface.core.serve.templates").joinpath("directory.yml")
|
|
601
|
+
rendered_yaml = resolve_jinja_template(
|
|
602
|
+
template_path.read_text(),
|
|
603
|
+
variables={
|
|
604
|
+
"directory_title": f"/{url_path}" if url_path else "/",
|
|
605
|
+
"listing_markdown": "\n".join(listing_lines)
|
|
606
|
+
or "No faces or directories found.",
|
|
607
|
+
},
|
|
608
|
+
strict=False,
|
|
609
|
+
)
|
|
610
|
+
result = compile(rendered_yaml)
|
|
611
|
+
if result.errors or not result.face:
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
f"Directory face failed: {[e.message for e in result.errors]}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Directory listings are cache-miss-dominant in practice, but consistency
|
|
617
|
+
# with the face-render path matters more than the micro-optimisation.
|
|
618
|
+
html = render(
|
|
619
|
+
result.face,
|
|
620
|
+
Executor(
|
|
621
|
+
result.face,
|
|
622
|
+
adapter_registry=build_adapter_registry(dir_path),
|
|
623
|
+
duckdb_cache=_get_default_cache(),
|
|
624
|
+
),
|
|
625
|
+
format="html",
|
|
626
|
+
).output
|
|
627
|
+
if isinstance(html, bytes):
|
|
628
|
+
html = html.decode("utf-8")
|
|
629
|
+
return HTMLResponse(content=html)
|
|
630
|
+
except (DatafaceError, ValueError, FileNotFoundError) as e:
|
|
631
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
632
|
+
raise
|
|
633
|
+
logger.exception("Error rendering directory listing for %s", dir_path)
|
|
634
|
+
if isinstance(e, DatafaceError):
|
|
635
|
+
err = RenderedDashboard(success=False, face_error=e.to_structured())
|
|
636
|
+
else:
|
|
637
|
+
err = _synthetic_error(str(e))
|
|
638
|
+
return _render_structured_errors_html(err, request_path=url_path)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def create_server(
|
|
642
|
+
project_dir: Path | None = None,
|
|
643
|
+
connection: str | None = None,
|
|
644
|
+
dialect: str = "duckdb",
|
|
645
|
+
faces_at_root: bool | None = None,
|
|
646
|
+
target: str | None = None,
|
|
647
|
+
serve_storage_prefix: str | None = None,
|
|
648
|
+
read_only: bool = LOCAL_AUTHORING_REGISTRY_KWARGS["read_only"],
|
|
649
|
+
allow_external_access_in_readonly: bool = LOCAL_AUTHORING_REGISTRY_KWARGS[
|
|
650
|
+
"allow_external_access_in_readonly"
|
|
651
|
+
],
|
|
652
|
+
duckdb_config: dict[str, Any] | None = None,
|
|
653
|
+
) -> FastAPI:
|
|
654
|
+
"""Create unified Dataface server.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
project_dir: Project directory for resolving face file paths
|
|
658
|
+
connection: Database connection string
|
|
659
|
+
dialect: SQL dialect for face rendering (duckdb, postgres, etc.).
|
|
660
|
+
The /inspect/* HTML path is always DuckDB — dialect is not forwarded there.
|
|
661
|
+
faces_at_root: Serve faces at / instead of /faces/. Auto-detected
|
|
662
|
+
from project structure when not specified (True if faces/ exists).
|
|
663
|
+
target: dbt target name override for DbtAdapter
|
|
664
|
+
serve_storage_prefix: Override for rendered board-link URLs in serve mode
|
|
665
|
+
read_only: DuckDB read-only driver flag for face rendering (default True).
|
|
666
|
+
allow_external_access_in_readonly: When True with read_only, allow SQL
|
|
667
|
+
read_csv/read_json_auto on local paths (local dev default True).
|
|
668
|
+
duckdb_config: DuckDB config dict; defaults to enable_external_access when
|
|
669
|
+
allow_external_access_in_readonly is True.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Configured FastAPI application
|
|
673
|
+
"""
|
|
674
|
+
if duckdb_config is None and allow_external_access_in_readonly:
|
|
675
|
+
duckdb_config = LOCAL_AUTHORING_REGISTRY_KWARGS["duckdb_config"]
|
|
676
|
+
|
|
677
|
+
@asynccontextmanager
|
|
678
|
+
async def _lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
|
679
|
+
global _cache, _cache_initialised
|
|
680
|
+
yield
|
|
681
|
+
if _cache is not None:
|
|
682
|
+
_cache.close()
|
|
683
|
+
_cache = None
|
|
684
|
+
_cache_initialised = False
|
|
685
|
+
|
|
686
|
+
app = FastAPI(
|
|
687
|
+
title="Dataface Server",
|
|
688
|
+
description="Unified server for Dataface rendering",
|
|
689
|
+
version="0.1.0",
|
|
690
|
+
lifespan=_lifespan,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
app.state.project_dir = project_dir or Path.cwd()
|
|
694
|
+
app.state.connection = connection
|
|
695
|
+
app.state.dialect = dialect
|
|
696
|
+
if faces_at_root is None:
|
|
697
|
+
faces_at_root = (app.state.project_dir / "faces").is_dir()
|
|
698
|
+
app.state.faces_at_root = faces_at_root
|
|
699
|
+
app.state.target = target
|
|
700
|
+
app.state.serve_storage_prefix = serve_storage_prefix
|
|
701
|
+
app.state.read_only = read_only
|
|
702
|
+
app.state.allow_external_access_in_readonly = allow_external_access_in_readonly
|
|
703
|
+
app.state.duckdb_config = duckdb_config
|
|
704
|
+
|
|
705
|
+
# Mount the bundled webfonts at /static/fonts so the @font-face URLs
|
|
706
|
+
# injected into rendered HTML/SVG (Inter Variable, DFT Sans Tabular,
|
|
707
|
+
# Source Serif 4, Noto Emoji) resolve in the browser. Register before the
|
|
708
|
+
# `/{path:path}` catch-all — Starlette matches routes in insertion order.
|
|
709
|
+
app.mount(
|
|
710
|
+
"/static/fonts",
|
|
711
|
+
StaticFiles(directory=get_inter_font_path().parent),
|
|
712
|
+
name="fonts_static",
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
@app.get("/health")
|
|
716
|
+
async def health() -> dict[str, str]:
|
|
717
|
+
"""Health check endpoint."""
|
|
718
|
+
return {"status": "ok", "service": "dataface-server"}
|
|
719
|
+
|
|
720
|
+
@app.get("/templates")
|
|
721
|
+
async def list_templates() -> dict[str, Any]:
|
|
722
|
+
"""List available inspect templates."""
|
|
723
|
+
return {"inspect_templates": INSPECT_TEMPLATES}
|
|
724
|
+
|
|
725
|
+
@app.post("/inspect/profile/", response_class=HTMLResponse)
|
|
726
|
+
async def profile_table(request: Request) -> str:
|
|
727
|
+
"""Run table profiling, save results, and render the dashboard.
|
|
728
|
+
|
|
729
|
+
Requires ``dataface-super-schema`` to be installed. Without the private
|
|
730
|
+
package, this endpoint returns 503 with an install hint.
|
|
731
|
+
|
|
732
|
+
Uses POST to avoid accidental invocation by prefetchers/crawlers.
|
|
733
|
+
"""
|
|
734
|
+
if not _SUPER_SCHEMA_AVAILABLE:
|
|
735
|
+
raise HTTPException(
|
|
736
|
+
status_code=503,
|
|
737
|
+
detail=(
|
|
738
|
+
"Table profiling requires the dataface-super-schema package. "
|
|
739
|
+
"Install it from the monorepo or your private registry."
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
from dataface_super_schema.inspect.connection import ( # noqa: PLC0415
|
|
743
|
+
source_config_from_url,
|
|
744
|
+
)
|
|
745
|
+
from dataface_super_schema.inspect.inspector import ( # noqa: PLC0415
|
|
746
|
+
TableInspector,
|
|
747
|
+
)
|
|
748
|
+
from dataface_super_schema.inspect.storage import ( # noqa: PLC0415
|
|
749
|
+
InspectionStorage,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
variables = dict(request.query_params)
|
|
753
|
+
model_name = variables.get("model", "")
|
|
754
|
+
if not model_name:
|
|
755
|
+
raise HTTPException(status_code=400, detail="model parameter required")
|
|
756
|
+
|
|
757
|
+
# Apply server defaults so browser-initiated profiling uses the
|
|
758
|
+
# configured connection, not the fallback `:memory:`.
|
|
759
|
+
if "connection" not in variables and app.state.connection:
|
|
760
|
+
variables["connection"] = app.state.connection
|
|
761
|
+
|
|
762
|
+
try:
|
|
763
|
+
variables = validate_inspect_variables(variables)
|
|
764
|
+
except ValueError as e:
|
|
765
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
766
|
+
|
|
767
|
+
super_schema_path = app.state.project_dir / InspectionStorage.DEFAULT_PATH
|
|
768
|
+
storage = InspectionStorage(output_path=super_schema_path)
|
|
769
|
+
|
|
770
|
+
# Inspect HTML server is DuckDB-only.
|
|
771
|
+
profile_connection = variables.get("connection", ":memory:")
|
|
772
|
+
profile_dialect = "duckdb"
|
|
773
|
+
try:
|
|
774
|
+
with TableInspector(
|
|
775
|
+
source_config_from_url(profile_connection, profile_dialect)
|
|
776
|
+
) as inspector:
|
|
777
|
+
profile = inspector.inspect_table(model_name)
|
|
778
|
+
storage.save_inspection(profile)
|
|
779
|
+
except Exception as e:
|
|
780
|
+
logger.exception("Profile failed for %s", model_name)
|
|
781
|
+
raise HTTPException(
|
|
782
|
+
status_code=500,
|
|
783
|
+
detail="Profiling failed. Check server logs for details.",
|
|
784
|
+
) from e
|
|
785
|
+
|
|
786
|
+
template_path = files("dataface.core.inspect.templates").joinpath("model.yml")
|
|
787
|
+
return render_inspect_dashboard(
|
|
788
|
+
template_path.read_text(),
|
|
789
|
+
variables,
|
|
790
|
+
project_root=app.state.project_dir,
|
|
791
|
+
connection=profile_connection,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
@app.get("/inspect/exists/")
|
|
795
|
+
async def table_exists(model: str, connection: str | None = None) -> Response:
|
|
796
|
+
"""Check if a table exists in the warehouse without reading profile data.
|
|
797
|
+
|
|
798
|
+
Requires ``dataface-super-schema`` to be installed.
|
|
799
|
+
Returns 200 {"exists": true} when found, 404 when not found.
|
|
800
|
+
Returns 500 if the check itself fails (caller falls through to generic error UI).
|
|
801
|
+
"""
|
|
802
|
+
if not _SUPER_SCHEMA_AVAILABLE:
|
|
803
|
+
raise HTTPException(
|
|
804
|
+
status_code=503,
|
|
805
|
+
detail=(
|
|
806
|
+
"Table existence check requires the dataface-super-schema package."
|
|
807
|
+
),
|
|
808
|
+
)
|
|
809
|
+
from dataface_super_schema.inspect.connection import ( # noqa: PLC0415
|
|
810
|
+
InspectConnection,
|
|
811
|
+
source_config_from_url,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
variables: dict[str, str] = {"model": model}
|
|
815
|
+
if connection:
|
|
816
|
+
variables["connection"] = connection
|
|
817
|
+
try:
|
|
818
|
+
variables = validate_inspect_variables(variables)
|
|
819
|
+
except ValueError as e:
|
|
820
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
821
|
+
|
|
822
|
+
conn_str = variables.get("connection") or app.state.connection or ":memory:"
|
|
823
|
+
try:
|
|
824
|
+
with InspectConnection(
|
|
825
|
+
source_config_from_url(conn_str, "duckdb"), read_only=True
|
|
826
|
+
) as conn:
|
|
827
|
+
rows = conn.execute(
|
|
828
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ? LIMIT 1",
|
|
829
|
+
[model],
|
|
830
|
+
).fetchall()
|
|
831
|
+
except (
|
|
832
|
+
Exception
|
|
833
|
+
) as e: # noqa: BLE001 — HTTP boundary: any DB error must surface as 500, not crash server
|
|
834
|
+
logger.warning("inspect/exists check failed for %r: %s", model, e)
|
|
835
|
+
raise HTTPException(
|
|
836
|
+
status_code=500, detail="Warehouse existence check failed"
|
|
837
|
+
) from e
|
|
838
|
+
if rows:
|
|
839
|
+
return JSONResponse({"exists": True})
|
|
840
|
+
raise HTTPException(
|
|
841
|
+
status_code=404, detail=f"Table '{model}' not found in warehouse"
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
@app.get("/{path:path}", response_class=HTMLResponse)
|
|
845
|
+
async def get_face(path: str, request: Request) -> Response:
|
|
846
|
+
"""Serve any face file with query params as variables.
|
|
847
|
+
|
|
848
|
+
URL path maps to file path:
|
|
849
|
+
/sales/ → faces/sales.yml (when faces_at_root=True)
|
|
850
|
+
/dashboards/exec/ → dashboards/exec.yml
|
|
851
|
+
|
|
852
|
+
When faces_at_root is active, /faces/* returns 404.
|
|
853
|
+
|
|
854
|
+
Directories show a listing of renderable files and subdirectories.
|
|
855
|
+
|
|
856
|
+
Special case - /inspect/* paths:
|
|
857
|
+
1. Check faces/inspect/{template}.yml for custom templates
|
|
858
|
+
2. Fall back to built-in templates with Jinja2 pre-processing
|
|
859
|
+
|
|
860
|
+
Query params become variables:
|
|
861
|
+
/sales/?region=West → variables["region"] = "West"
|
|
862
|
+
"""
|
|
863
|
+
clean_path = path.strip("/")
|
|
864
|
+
faces_dir = app.state.project_dir / "faces"
|
|
865
|
+
|
|
866
|
+
if app.state.faces_at_root and (
|
|
867
|
+
clean_path == "faces" or clean_path.startswith("faces/")
|
|
868
|
+
):
|
|
869
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
870
|
+
|
|
871
|
+
variables = _apply_server_defaults(
|
|
872
|
+
dict(request.query_params),
|
|
873
|
+
connection=app.state.connection,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
# Prefer a project-level index face for the root URL before directory listing.
|
|
877
|
+
if not clean_path:
|
|
878
|
+
candidate = _resolve_root_index_face(app.state.project_dir)
|
|
879
|
+
if candidate is not None:
|
|
880
|
+
return _render_face_file(
|
|
881
|
+
candidate,
|
|
882
|
+
variables,
|
|
883
|
+
app.state.project_dir,
|
|
884
|
+
connection=app.state.connection,
|
|
885
|
+
dialect=app.state.dialect,
|
|
886
|
+
target=app.state.target,
|
|
887
|
+
faces_at_root=app.state.faces_at_root,
|
|
888
|
+
serve_storage_prefix=app.state.serve_storage_prefix,
|
|
889
|
+
read_only=app.state.read_only,
|
|
890
|
+
allow_external_access_in_readonly=app.state.allow_external_access_in_readonly,
|
|
891
|
+
duckdb_config=app.state.duckdb_config,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# Directory listing for root or any directory
|
|
895
|
+
for dir_path, listing_path in _directory_candidates(
|
|
896
|
+
project_dir=app.state.project_dir,
|
|
897
|
+
faces_dir=faces_dir,
|
|
898
|
+
clean_path=clean_path,
|
|
899
|
+
faces_at_root=app.state.faces_at_root,
|
|
900
|
+
):
|
|
901
|
+
_ensure_within_project(dir_path, app.state.project_dir)
|
|
902
|
+
if dir_path.is_dir():
|
|
903
|
+
return _render_directory_listing(dir_path, listing_path)
|
|
904
|
+
|
|
905
|
+
# Special case: /inspect/* paths use built-in template fallback (DuckDB-only).
|
|
906
|
+
if clean_path.startswith("inspect/"):
|
|
907
|
+
template_name = clean_path.removeprefix("inspect/")
|
|
908
|
+
return _handle_inspect_route(
|
|
909
|
+
template_name,
|
|
910
|
+
variables,
|
|
911
|
+
app.state.project_dir,
|
|
912
|
+
connection=app.state.connection,
|
|
913
|
+
target=app.state.target,
|
|
914
|
+
read_only=app.state.read_only,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
file_path = _resolve_face_file_path(
|
|
918
|
+
project_dir=app.state.project_dir,
|
|
919
|
+
faces_dir=faces_dir,
|
|
920
|
+
clean_path=clean_path,
|
|
921
|
+
faces_at_root=app.state.faces_at_root,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
return _render_face_file(
|
|
925
|
+
file_path,
|
|
926
|
+
variables,
|
|
927
|
+
app.state.project_dir,
|
|
928
|
+
connection=app.state.connection,
|
|
929
|
+
dialect=app.state.dialect,
|
|
930
|
+
faces_at_root=app.state.faces_at_root,
|
|
931
|
+
target=app.state.target,
|
|
932
|
+
serve_storage_prefix=app.state.serve_storage_prefix,
|
|
933
|
+
read_only=app.state.read_only,
|
|
934
|
+
allow_external_access_in_readonly=app.state.allow_external_access_in_readonly,
|
|
935
|
+
duckdb_config=app.state.duckdb_config,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
return app
|