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.
Files changed (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. 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