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