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,1000 @@
1
+ """Layout resolution and unified layout construction."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+ from pydantic import ValidationError as PydanticValidationError
8
+
9
+ from dataface.core.compile.errors import CompilationError, ReferenceError
10
+ from dataface.core.compile.models.chart.authored import (
11
+ _SharedChartFields,
12
+ )
13
+ from dataface.core.compile.models.chart.compiled import (
14
+ Chart,
15
+ )
16
+ from dataface.core.compile.models.face.authored import (
17
+ AuthoredFace,
18
+ ForeachConfig,
19
+ ForeachItem,
20
+ ForeachQuery,
21
+ GridLayout,
22
+ LayoutType,
23
+ TabLayout,
24
+ )
25
+ from dataface.core.compile.models.face.compiled import Face, Layout, LayoutItem
26
+ from dataface.core.compile.models.query.compiled import AnyQuery
27
+ from dataface.core.compile.models.style.authored import StylePatch
28
+ from dataface.core.compile.models.style.merged import MergedStyle
29
+ from dataface.core.compile.normalize_charts import (
30
+ _generate_inline_chart_id,
31
+ _normalize_chart,
32
+ )
33
+
34
+
35
+ def _validate_style_patch(style_dict: dict[str, Any]) -> StylePatch:
36
+ """Validate a raw style dict and return a StylePatch.
37
+
38
+ Re-raises PydanticValidationError with ``style.`` prepended to each loc
39
+ so errors from nested-face style blocks say ``style.bad_key`` rather than
40
+ the bare ``bad_key`` that a standalone StylePatch.model_validate raises.
41
+ """
42
+ try:
43
+ return StylePatch.model_validate(style_dict)
44
+ except PydanticValidationError as exc:
45
+ raise PydanticValidationError.from_exception_data(
46
+ title=exc.title,
47
+ input_type="python",
48
+ line_errors=[
49
+ {
50
+ "type": err["type"],
51
+ "loc": ("style",) + err["loc"],
52
+ "input": err["input"],
53
+ "ctx": err.get("ctx", {}),
54
+ }
55
+ for err in exc.errors()
56
+ ],
57
+ ) from exc
58
+
59
+
60
+ def _validate_dimension(value: Any, name: str) -> None:
61
+ """Reject zero/negative dimension values at compile time.
62
+
63
+ Uses ``parse_dimension`` from the sizing module so the validator and
64
+ the sizing engine agree on what constitutes a valid dimension.
65
+ ``total=1.0`` makes percentages yield their numeric fraction (e.g.
66
+ ``"50%"`` → 0.5) so the sign check works uniformly.
67
+ """
68
+ if value is None:
69
+ return
70
+ from dataface.core.compile.sizing import parse_dimension
71
+
72
+ parsed = parse_dimension(str(value), 1.0)
73
+ if parsed is None:
74
+ raise CompilationError(
75
+ f"'{name}: {value}' is not a valid dimension"
76
+ " (use e.g. '200', '200px', or '50%')"
77
+ )
78
+ if parsed <= 0:
79
+ raise CompilationError(f"'{name}: {value}' must be positive (got {parsed})")
80
+
81
+
82
+ def build_unified_layout(
83
+ face: AuthoredFace,
84
+ charts: dict[str, Chart],
85
+ query_registry: dict[str, AnyQuery],
86
+ face_id: str,
87
+ depth: int,
88
+ base_path: Path | None = None,
89
+ default_source: str | None = None,
90
+ chart_registry: dict[str, Any] | None = None,
91
+ theme: str | None = None,
92
+ parent_variables: dict[str, Any] | None = None,
93
+ *,
94
+ _resolve_jinja: bool = False,
95
+ resolved_style: MergedStyle,
96
+ parent_level: int = 0,
97
+ ) -> Layout:
98
+ """Build a unified Layout from face layout fields.
99
+
100
+ Converts rows/cols/grid/tabs to unified Layout structure.
101
+
102
+ Args:
103
+ face: AuthoredFace with layout fields
104
+ charts: Available charts for resolution
105
+ query_registry: Query registry for inline charts
106
+ face_id: Parent face ID
107
+ depth: Nesting depth
108
+ base_path: Base path for resolving external file references
109
+ default_source: Default source for inline queries
110
+ theme: Vega-Lite theme to inherit to nested faces
111
+ parent_variables: Variables from parent scope for template resolution
112
+ parent_level: Semantic heading level of the enclosing face. Passed
113
+ through to nested normalize_face calls so children compute their
114
+ own level as parent_level + (1 if they have a title else 0).
115
+
116
+ Returns:
117
+ Unified Layout structure
118
+ """
119
+ from dataface.core.compile.normalizer import slugify
120
+
121
+ # Track used chart IDs across all layout items to avoid duplicates
122
+ used_chart_ids = set(charts.keys())
123
+
124
+ if face.rows:
125
+ items = _resolve_layout_items(
126
+ face.rows,
127
+ charts,
128
+ query_registry,
129
+ face_id,
130
+ depth,
131
+ "row",
132
+ base_path,
133
+ default_source,
134
+ chart_registry,
135
+ theme,
136
+ used_chart_ids,
137
+ parent_variables,
138
+ _resolve_jinja=_resolve_jinja,
139
+ parent_level=parent_level,
140
+ )
141
+ return Layout(type=LayoutType.ROWS, items=items)
142
+
143
+ elif face.cols:
144
+ items = _resolve_layout_items(
145
+ face.cols,
146
+ charts,
147
+ query_registry,
148
+ face_id,
149
+ depth,
150
+ "col",
151
+ base_path,
152
+ default_source,
153
+ chart_registry,
154
+ theme,
155
+ used_chart_ids,
156
+ parent_variables,
157
+ _resolve_jinja=_resolve_jinja,
158
+ parent_level=parent_level,
159
+ )
160
+ return Layout(type=LayoutType.COLS, items=items)
161
+
162
+ elif face.grid:
163
+ grid_obj = face.grid
164
+ items = _resolve_grid_items(
165
+ grid_obj,
166
+ charts,
167
+ query_registry,
168
+ face_id,
169
+ depth,
170
+ base_path,
171
+ default_source,
172
+ chart_registry,
173
+ theme,
174
+ used_chart_ids,
175
+ parent_variables,
176
+ _resolve_jinja=_resolve_jinja,
177
+ parent_level=parent_level,
178
+ )
179
+ return Layout(
180
+ type=LayoutType.GRID,
181
+ items=items,
182
+ columns=grid_obj.columns,
183
+ gap=grid_obj.gap,
184
+ row_height=grid_obj.row_height,
185
+ )
186
+
187
+ elif face.tabs:
188
+ tabs_obj = face.tabs
189
+ items, tab_titles = _resolve_tab_items(
190
+ tabs_obj,
191
+ query_registry,
192
+ face_id,
193
+ depth,
194
+ base_path,
195
+ default_source,
196
+ chart_registry,
197
+ theme,
198
+ _resolve_jinja=_resolve_jinja,
199
+ resolved_style=resolved_style,
200
+ parent_level=parent_level,
201
+ )
202
+
203
+ # Compute tab slugs and variable name
204
+ tab_slugs = [slugify(title) for title in tab_titles]
205
+ # Validate slugs
206
+ for i, slug in enumerate(tab_slugs):
207
+ if not slug:
208
+ raise CompilationError(
209
+ f"Tab title '{tab_titles[i]}' produces an empty slug. "
210
+ "Tab titles must contain at least one alphanumeric character."
211
+ )
212
+ # Check for slug collisions
213
+ if len(set(tab_slugs)) != len(tab_slugs):
214
+ seen: set[str] = set()
215
+ for slug in tab_slugs:
216
+ if slug in seen:
217
+ raise CompilationError(
218
+ f"Duplicate tab slug '{slug}'. Tab titles must produce "
219
+ "unique slugs (after lowercasing and replacing spaces with _)."
220
+ )
221
+ seen.add(slug)
222
+
223
+ # Use explicit id, or generate a unique name from face_id
224
+ # (avoids collisions when multiple tabs layouts exist in nested faces)
225
+ tab_variable = tabs_obj.id or f"_tab_{face_id}"
226
+
227
+ # Resolve default tab from slug
228
+ default_slug = slugify(tabs_obj.default) if tabs_obj.default else tab_slugs[0]
229
+ default_tab = tab_slugs.index(default_slug) if default_slug in tab_slugs else 0
230
+
231
+ return Layout(
232
+ type=LayoutType.TABS,
233
+ items=items,
234
+ tab_titles=tab_titles,
235
+ tab_slugs=tab_slugs,
236
+ tab_variable=tab_variable,
237
+ default_tab=default_tab,
238
+ tab_position=tabs_obj.position,
239
+ )
240
+
241
+ # Content-only face. Orphan-chart detection lives post-compile so
242
+ # cross-face references work — see _collect_orphan_charts in compiler.py.
243
+ return Layout(type=LayoutType.ROWS, items=[])
244
+
245
+
246
+ def _resolve_layout_items(
247
+ items: list[Any],
248
+ charts: dict[str, Chart],
249
+ query_registry: dict[str, AnyQuery],
250
+ face_id: str,
251
+ depth: int,
252
+ prefix: str,
253
+ base_path: Path | None = None,
254
+ default_source: str | None = None,
255
+ chart_registry: dict[str, Any] | None = None,
256
+ theme: str | None = None,
257
+ used_chart_ids: set | None = None,
258
+ parent_variables: dict[str, Any] | None = None,
259
+ _resolve_jinja: bool = False,
260
+ parent_level: int = 0,
261
+ ) -> list[LayoutItem]:
262
+ """Resolve layout items to LayoutItem objects.
263
+
264
+ Args:
265
+ items: List of layout items (strings, dicts, charts)
266
+ charts: Available charts
267
+ query_registry: Query registry
268
+ face_id: Parent face ID
269
+ depth: Nesting depth
270
+ prefix: Item prefix for ID generation
271
+ base_path: Base path for resolving external file references
272
+ default_source: Default source for inline queries
273
+ theme: Vega-Lite theme to inherit to nested faces
274
+ chart_registry: Raw chart definitions for nested face normalization
275
+ used_chart_ids: Set of already-used chart IDs for uniqueness
276
+ parent_variables: Variables from parent scope for template resolution
277
+
278
+ Returns:
279
+ List of resolved LayoutItem objects
280
+ """
281
+ resolved: list[LayoutItem] = []
282
+
283
+ # Initialize used_chart_ids with existing chart names if not provided
284
+ if used_chart_ids is None:
285
+ used_chart_ids = set(charts.keys())
286
+
287
+ for idx, item in enumerate(items):
288
+ # Handle foreach construct
289
+ if isinstance(item, ForeachItem):
290
+ foreach_items = _resolve_foreach_item(
291
+ item.foreach,
292
+ charts,
293
+ query_registry,
294
+ face_id,
295
+ depth,
296
+ f"{prefix}{idx}",
297
+ base_path,
298
+ default_source,
299
+ chart_registry,
300
+ theme,
301
+ used_chart_ids,
302
+ parent_variables,
303
+ parent_level=parent_level,
304
+ )
305
+ resolved.extend(foreach_items)
306
+ else:
307
+ layout_item = _resolve_single_item(
308
+ item,
309
+ charts,
310
+ query_registry,
311
+ face_id,
312
+ depth,
313
+ f"{prefix}{idx}",
314
+ base_path,
315
+ default_source,
316
+ chart_registry,
317
+ theme,
318
+ used_chart_ids,
319
+ parent_variables,
320
+ _resolve_jinja=_resolve_jinja,
321
+ parent_level=parent_level,
322
+ )
323
+ resolved.append(layout_item)
324
+
325
+ return resolved
326
+
327
+
328
+ def _resolve_single_item(
329
+ item: Any,
330
+ charts: dict[str, Chart],
331
+ query_registry: dict[str, AnyQuery],
332
+ face_id: str,
333
+ depth: int,
334
+ item_id: str,
335
+ base_path: Path | None = None,
336
+ default_source: str | None = None,
337
+ chart_registry: dict[str, Any] | None = None,
338
+ theme: str | None = None,
339
+ used_chart_ids: set | None = None,
340
+ parent_variables: dict[str, Any] | None = None,
341
+ _resolve_jinja: bool = False,
342
+ parent_level: int = 0,
343
+ ) -> LayoutItem:
344
+ """Resolve a single layout item.
345
+
346
+ Args:
347
+ item: Layout item to resolve
348
+ charts: Available charts
349
+ query_registry: Query registry
350
+ theme: Vega-Lite theme to inherit to nested faces
351
+ face_id: Parent face ID
352
+ depth: Nesting depth
353
+ item_id: ID for this item (fallback for inline charts)
354
+ base_path: Base path for resolving external file references
355
+ default_source: Default source for inline queries
356
+ chart_registry: Raw chart definitions for nested face normalization
357
+ used_chart_ids: Set of already-used chart IDs for uniqueness
358
+ parent_variables: Variables from parent scope for template resolution
359
+
360
+ Returns:
361
+ Resolved LayoutItem
362
+ """
363
+ # String reference to chart OR board file import
364
+ if isinstance(item, str):
365
+ # Check for board file import (ends with .yml or .yaml)
366
+ if item.endswith(".yml") or item.endswith(".yaml"):
367
+ return _resolve_board_file_import(
368
+ item,
369
+ query_registry,
370
+ face_id,
371
+ depth,
372
+ item_id,
373
+ base_path,
374
+ default_source,
375
+ chart_registry,
376
+ theme,
377
+ parent_variables,
378
+ _resolve_jinja=_resolve_jinja,
379
+ parent_level=parent_level,
380
+ )
381
+ elif item in charts:
382
+ return LayoutItem(type="chart", chart=charts[item])
383
+ else:
384
+ raise ReferenceError(item, "layout item")
385
+
386
+ # Already a Chart
387
+ if isinstance(item, Chart):
388
+ return LayoutItem(type="chart", chart=item)
389
+
390
+ # dict[str, AuthoredChart]: {"my_chart": AuthoredChart(...)}
391
+ if isinstance(item, dict):
392
+ if len(item) != 1:
393
+ raise CompilationError(
394
+ f"Named chart dict must have exactly one key, got: {list(item.keys())}"
395
+ )
396
+ _chart_name, chart_def = next(iter(item.items()))
397
+ chart_dict = chart_def.model_dump(exclude_none=True)
398
+ if used_chart_ids is None:
399
+ used_chart_ids = set(charts.keys())
400
+ inline_id = _generate_inline_chart_id(chart_dict, item_id, used_chart_ids)
401
+ source_path = f"{item_id.replace('row', 'rows[').replace('col', 'cols[')}]"
402
+ chart = _normalize_chart(
403
+ inline_id,
404
+ chart_dict,
405
+ query_registry,
406
+ base_path,
407
+ default_source,
408
+ source_path=source_path,
409
+ )
410
+ return LayoutItem(type="chart", chart=chart)
411
+
412
+ # Pre-validated AuthoredFace (typed inline nested face from rows/cols)
413
+ if isinstance(item, AuthoredFace):
414
+ from dataface.core.compile.normalizer import normalize_face
415
+
416
+ # In foreach context, resolve Jinja templates baked into the typed face.
417
+ # Pydantic parsed templates as literal strings; dump to dict, resolve, re-validate.
418
+ if _resolve_jinja and parent_variables:
419
+ face_dict = item.model_dump(exclude_none=True, by_alias=False)
420
+ face_dict = _resolve_dict_templates(face_dict, parent_variables)
421
+ item = AuthoredFace.model_validate(face_dict)
422
+
423
+ details = item.details
424
+ user_width = str(item.width) if item.width is not None else None
425
+ layout_height = str(item.height) if item.height is not None else None
426
+ _validate_dimension(user_width, "width")
427
+ _validate_dimension(layout_height, "height")
428
+
429
+ compiled_nested = normalize_face(
430
+ item,
431
+ face_id=f"{face_id}_{item_id}",
432
+ parent_context={
433
+ "parent_id": face_id,
434
+ "base_path": base_path,
435
+ "default_source": default_source,
436
+ "theme": theme,
437
+ "variables": parent_variables,
438
+ "_resolve_jinja": _resolve_jinja,
439
+ },
440
+ query_registry=query_registry,
441
+ chart_registry=chart_registry,
442
+ depth=depth + 1,
443
+ base_path=base_path,
444
+ parent_level=parent_level,
445
+ )
446
+
447
+ details_kwargs: dict[str, Any] = {}
448
+ if details:
449
+ details_id = item.id or f"_details_{item_id}"
450
+ details_kwargs = {
451
+ "details_variable": details_id,
452
+ "details_summary": details.summary,
453
+ "details_expanded_summary": details.expanded_title or details.summary,
454
+ }
455
+ compiled_nested.meta["details_expanded_default"] = details.expanded
456
+
457
+ return LayoutItem(
458
+ type="face",
459
+ face=compiled_nested,
460
+ user_width=user_width,
461
+ layout_height=layout_height,
462
+ description=item.description,
463
+ visible=item.visible,
464
+ **details_kwargs,
465
+ )
466
+
467
+ # Pre-validated chart patch (typed inline chart from rows/cols)
468
+ if isinstance(item, _SharedChartFields):
469
+ if used_chart_ids is None:
470
+ used_chart_ids = set(charts.keys())
471
+ # exclude_none=True: None-defaulted fields must be absent so _normalize_chart's
472
+ # auto-derive guards ("title" not in chart_dict) fire correctly.
473
+ # visible is excluded=True on AuthoredChart so it won't appear in chart_dict.
474
+ chart_dict = item.model_dump(exclude_none=True)
475
+ inline_id = _generate_inline_chart_id(chart_dict, item_id, used_chart_ids)
476
+ source_path = f"{item_id.replace('row', 'rows[').replace('col', 'cols[')}]"
477
+ chart = _normalize_chart(
478
+ inline_id,
479
+ chart_dict,
480
+ query_registry,
481
+ base_path,
482
+ default_source,
483
+ source_path=source_path,
484
+ )
485
+ return LayoutItem(type="chart", chart=chart, visible=item.visible)
486
+
487
+ raise CompilationError(f"Invalid layout item type: {type(item)}")
488
+
489
+
490
+ # Layout keys that should not have templates resolved (processed by layout resolution)
491
+ _LAYOUT_KEYS = frozenset({"rows", "cols", "grid", "tabs"})
492
+
493
+
494
+ def _resolve_dict_templates(
495
+ data: dict[str, Any],
496
+ variables: dict[str, Any],
497
+ ) -> dict[str, Any]:
498
+ """Resolve Jinja templates recursively in dict values.
499
+
500
+ Used to resolve loop variables and inherited variables in nested face
501
+ definitions created by foreach (compile-time static expansion).
502
+
503
+ Args:
504
+ data: Dict with potentially templated string values
505
+ variables: Variables for Jinja resolution
506
+
507
+ Returns:
508
+ Dict with resolved string values
509
+ """
510
+ from dataface.core.compile.jinja import resolve_jinja_template
511
+
512
+ result: dict[str, Any] = {}
513
+ for key, value in data.items():
514
+ if isinstance(value, str):
515
+ # Resolve Jinja templates in strings
516
+ result[key] = resolve_jinja_template(value, variables, strict=False)
517
+ elif isinstance(value, dict):
518
+ # Recurse into nested dicts
519
+ result[key] = _resolve_dict_templates(value, variables)
520
+ elif isinstance(value, list):
521
+ # Don't recurse into layout items - those are processed by _resolve_layout_items
522
+ if key in _LAYOUT_KEYS:
523
+ result[key] = value
524
+ else:
525
+ result[key] = [
526
+ (
527
+ _resolve_dict_templates(v, variables)
528
+ if isinstance(v, dict)
529
+ else (
530
+ resolve_jinja_template(v, variables, strict=False)
531
+ if isinstance(v, str)
532
+ else v
533
+ )
534
+ )
535
+ for v in value
536
+ ]
537
+ else:
538
+ result[key] = value
539
+ return result
540
+
541
+
542
+ def _resolve_board_file_import(
543
+ file_path: str,
544
+ query_registry: dict[str, AnyQuery],
545
+ face_id: str,
546
+ depth: int,
547
+ item_id: str,
548
+ base_path: Path | None = None,
549
+ default_source: str | None = None,
550
+ chart_registry: dict[str, Any] | None = None,
551
+ theme: str | None = None,
552
+ parent_variables: dict[str, Any] | None = None,
553
+ _resolve_jinja: bool = False,
554
+ parent_level: int = 0,
555
+ ) -> LayoutItem:
556
+ """Load and resolve a board file import.
557
+
558
+ Board file imports allow including external YAML face files in layouts.
559
+ The imported face inherits variables from the parent context.
560
+
561
+ Args:
562
+ file_path: Path to the YAML file (relative to base_path or absolute)
563
+ charts: Available charts
564
+ query_registry: Query registry
565
+ face_id: Parent face ID
566
+ depth: Nesting depth
567
+ item_id: ID for this item
568
+ base_path: Base path for resolving relative file paths
569
+ default_source: Default source for inline queries
570
+ chart_registry: Raw chart definitions for nested face normalization
571
+ theme: Vega-Lite theme to inherit
572
+ used_chart_ids: Set of already-used chart IDs for uniqueness
573
+ parent_variables: Variables from parent scope for template resolution
574
+ _resolve_jinja: If True, resolve Jinja templates in the YAML content
575
+ (foreach creates static faces; all templates baked at compile time).
576
+ If False, skip YAML content resolution so interactive variables
577
+ remain as templates and are resolved at render time.
578
+
579
+ Returns:
580
+ LayoutItem containing the compiled nested face
581
+
582
+ Raises:
583
+ CompilationError: If file not found or invalid
584
+ """
585
+ from dataface.core.compile.jinja import resolve_jinja_template
586
+ from dataface.core.compile.normalizer import normalize_face
587
+
588
+ # Resolve the file path (might contain Jinja templates for parameterised imports
589
+ # like `partials/{{ chart_type }}.yml`). This always uses parent_variables so
590
+ # file paths with interactive variable defaults still resolve at compile time.
591
+ parent_variables = parent_variables or {}
592
+ resolved_path = resolve_jinja_template(file_path, parent_variables, strict=False)
593
+
594
+ # Resolve relative to base_path
595
+ full_path = base_path / resolved_path if base_path else Path(resolved_path)
596
+
597
+ if not full_path.exists():
598
+ raise CompilationError(
599
+ f"Board file not found: '{resolved_path}' "
600
+ f"(resolved from '{file_path}', base_path={base_path})"
601
+ )
602
+
603
+ # Load and parse the YAML file
604
+ try:
605
+ yaml_content = full_path.read_text()
606
+ # Only resolve Jinja templates in the YAML content when inside a foreach.
607
+ # Foreach creates static compile-time faces, so all templates (loop vars +
608
+ # interactive defaults) must be baked in. For regular imports, leave
609
+ # templates intact so they are resolved dynamically at render time.
610
+ if _resolve_jinja:
611
+ yaml_content = resolve_jinja_template(
612
+ yaml_content, parent_variables, strict=False
613
+ )
614
+ face_data = yaml.safe_load(yaml_content)
615
+ except yaml.YAMLError as e:
616
+ raise CompilationError(
617
+ f"Failed to parse board file '{resolved_path}': {e}"
618
+ ) from e
619
+ except OSError as e:
620
+ raise CompilationError(
621
+ f"Failed to read board file '{resolved_path}': {e}"
622
+ ) from e
623
+
624
+ if not isinstance(face_data, dict):
625
+ raise CompilationError(
626
+ f"Board file '{resolved_path}' must contain a YAML dictionary"
627
+ )
628
+
629
+ # Convert raw YAML dict to AuthoredFace; model_validate handles all nested models.
630
+ nested_face = AuthoredFace.model_validate(face_data)
631
+
632
+ # Build parent context with variables from parent scope
633
+ parent_context = {
634
+ "parent_id": face_id,
635
+ "base_path": full_path.parent, # Use imported file's directory as base
636
+ "default_source": default_source,
637
+ "theme": theme,
638
+ "variables": parent_variables, # Pass variables to child
639
+ "_resolve_jinja": _resolve_jinja, # Propagate foreach context to children
640
+ }
641
+
642
+ # Compile the nested face
643
+ compiled_nested = normalize_face(
644
+ nested_face,
645
+ face_id=f"{face_id}_{item_id}",
646
+ parent_context=parent_context,
647
+ query_registry=query_registry,
648
+ chart_registry=chart_registry,
649
+ depth=depth + 1,
650
+ base_path=full_path.parent,
651
+ parent_level=parent_level,
652
+ )
653
+
654
+ return LayoutItem(type="face", face=compiled_nested)
655
+
656
+
657
+ def _resolve_foreach_item(
658
+ foreach_config: ForeachConfig,
659
+ charts: dict[str, Chart],
660
+ query_registry: dict[str, AnyQuery],
661
+ face_id: str,
662
+ depth: int,
663
+ item_id: str,
664
+ base_path: Path | None = None,
665
+ default_source: str | None = None,
666
+ chart_registry: dict[str, Any] | None = None,
667
+ theme: str | None = None,
668
+ used_chart_ids: set | None = None,
669
+ parent_variables: dict[str, Any] | None = None,
670
+ parent_level: int = 0,
671
+ ) -> list[LayoutItem]:
672
+ """Resolve a foreach construct that iterates over inline data.
673
+
674
+ foreach allows dynamically generating layout items at compile time.
675
+ Each row from the inline data becomes available as a loop variable,
676
+ accessible via dot notation (e.g., {{ col.name }}).
677
+
678
+ Example YAML:
679
+ rows:
680
+ - foreach:
681
+ query:
682
+ data:
683
+ - name: "revenue"
684
+ type: "numeric"
685
+ - name: "date"
686
+ type: "date"
687
+ as: col
688
+ items:
689
+ - 'partials/{{ col.type }}.yml'
690
+
691
+ Note: At compile time, foreach only works with inline data (query.data).
692
+ Query references are not executed during compilation.
693
+
694
+ Args:
695
+ foreach_config: Validated ForeachConfig with query, as_, and items.
696
+ charts: Available charts
697
+ query_registry: Query registry
698
+ face_id: Parent face ID
699
+ depth: Nesting depth
700
+ item_id: ID prefix for generated items
701
+ base_path: Base path for resolving external file references
702
+ default_source: Default source for inline queries
703
+ chart_registry: Raw chart definitions for nested face normalization
704
+ theme: Vega-Lite theme to inherit
705
+ used_chart_ids: Set of already-used chart IDs for uniqueness
706
+ parent_variables: Variables from parent scope
707
+
708
+ Returns:
709
+ List of LayoutItems generated from the foreach iteration
710
+ """
711
+ query_config = foreach_config.query
712
+ loop_var = foreach_config.as_
713
+ item_templates = foreach_config.items
714
+ parent_variables = parent_variables or {}
715
+
716
+ # Extract inline data from query config
717
+ # At compile time, foreach only works with inline data
718
+ static_data = _get_foreach_data(query_config)
719
+ if not static_data:
720
+ return []
721
+
722
+ # Generate layout items for each row
723
+ resolved_items: list[LayoutItem] = []
724
+ for idx, row in enumerate(static_data):
725
+ loop_context = _build_loop_context(row, loop_var, parent_variables)
726
+
727
+ for template_idx, template_item in enumerate(item_templates):
728
+ layout_item = _resolve_single_item(
729
+ template_item,
730
+ charts,
731
+ query_registry,
732
+ face_id,
733
+ depth,
734
+ f"{item_id}_foreach{idx}_{template_idx}",
735
+ base_path,
736
+ default_source,
737
+ chart_registry,
738
+ theme,
739
+ used_chart_ids,
740
+ loop_context,
741
+ _resolve_jinja=True,
742
+ parent_level=parent_level,
743
+ )
744
+ resolved_items.append(layout_item)
745
+
746
+ return resolved_items
747
+
748
+
749
+ def _get_foreach_data(query_config: ForeachQuery) -> list[dict[str, Any]] | None:
750
+ """Extract data for foreach iteration from a ForeachQuery.
751
+
752
+ Args:
753
+ query_config: Validated ForeachQuery with optional data or static_data.
754
+
755
+ Returns:
756
+ List of row dicts for iteration, or None if no data.
757
+ """
758
+ data = query_config.data or query_config.static_data
759
+ if isinstance(data, list):
760
+ return data
761
+ return None
762
+
763
+
764
+ def _build_loop_context(
765
+ row: Any,
766
+ loop_var: str,
767
+ parent_variables: dict[str, Any],
768
+ ) -> dict[str, Any]:
769
+ """Build the variable context for a foreach iteration.
770
+
771
+ The loop variable is accessible via dot notation: {{ col.field }}
772
+ This keeps variable scoping explicit and avoids shadowing parent variables.
773
+
774
+ Args:
775
+ row: Current row data (dict or scalar)
776
+ loop_var: Name of the loop variable
777
+ parent_variables: Variables inherited from parent scope
778
+
779
+ Returns:
780
+ Combined context dict for Jinja resolution
781
+ """
782
+ loop_context = dict(parent_variables)
783
+
784
+ if isinstance(row, dict):
785
+ # Row is a dict - accessible as {{ loop_var.field }}
786
+ loop_context[loop_var] = row
787
+ else:
788
+ # Scalar value - wrap in dict with 'value' key
789
+ loop_context[loop_var] = {"value": row}
790
+
791
+ return loop_context
792
+
793
+
794
+ def _resolve_grid_items(
795
+ grid: GridLayout,
796
+ charts: dict[str, Chart],
797
+ query_registry: dict[str, AnyQuery],
798
+ face_id: str,
799
+ depth: int,
800
+ base_path: Path | None = None,
801
+ default_source: str | None = None,
802
+ chart_registry: dict[str, Any] | None = None,
803
+ theme: str | None = None,
804
+ used_chart_ids: set | None = None,
805
+ parent_variables: dict[str, Any] | None = None,
806
+ _resolve_jinja: bool = False,
807
+ parent_level: int = 0,
808
+ ) -> list[LayoutItem]:
809
+ """Resolve grid layout items.
810
+
811
+ Auto-calculates col/row positions for items without explicit positioning.
812
+ Items flow left-to-right, wrapping to the next row when they exceed
813
+ the column count.
814
+
815
+ Args:
816
+ grid: GridLayout definition
817
+ charts: Available charts
818
+ query_registry: Query registry
819
+ face_id: Parent face ID
820
+ depth: Nesting depth
821
+ base_path: Base path for resolving external file references
822
+ default_source: Default source for inline queries
823
+ chart_registry: Raw chart definitions for nested face normalization
824
+ theme: Vega-Lite theme to inherit to nested faces
825
+ parent_variables: Variables from parent scope for template resolution
826
+
827
+ Returns:
828
+ List of LayoutItem with grid positioning
829
+ """
830
+ resolved: list[LayoutItem] = []
831
+ columns = grid.columns
832
+
833
+ # Track grid occupancy for auto-positioning
834
+ # For items without explicit position, place left-to-right, top-to-bottom
835
+ current_col = 0
836
+ current_row = 0
837
+
838
+ for idx, grid_item in enumerate(grid.items):
839
+ item = grid_item.item
840
+ layout_item = _resolve_single_item(
841
+ item,
842
+ charts,
843
+ query_registry,
844
+ face_id,
845
+ depth,
846
+ f"grid{idx}",
847
+ base_path,
848
+ default_source,
849
+ chart_registry,
850
+ theme,
851
+ used_chart_ids,
852
+ parent_variables,
853
+ _resolve_jinja=_resolve_jinja,
854
+ parent_level=parent_level,
855
+ )
856
+
857
+ # Get span values (default to 1)
858
+ # Support 'width' as alias for 'col_span' and 'height' as alias for 'row_span'
859
+ # This is more intuitive for users (width: 6 means span 6 columns)
860
+ col_span = grid_item.col_span or grid_item.width or 1
861
+ row_span = grid_item.row_span or grid_item.height or 1
862
+
863
+ # Use explicit position if provided, otherwise auto-calculate
864
+ if grid_item.col is not None:
865
+ item_col = grid_item.col
866
+ else:
867
+ # Auto-position: check if item fits in current row
868
+ if current_col + col_span > columns:
869
+ # Wrap to next row
870
+ current_col = 0
871
+ current_row += 1
872
+ item_col = current_col
873
+ # Advance position for next item
874
+ current_col += col_span
875
+
876
+ item_row = grid_item.row if grid_item.row is not None else current_row
877
+
878
+ # Set grid positioning
879
+ layout_item.col = item_col
880
+ layout_item.row = item_row
881
+ layout_item.col_span = col_span
882
+ layout_item.row_span = row_span
883
+ if grid_item.description and not layout_item.description:
884
+ layout_item.description = grid_item.description
885
+ resolved.append(layout_item)
886
+
887
+ return resolved
888
+
889
+
890
+ def _resolve_tab_items(
891
+ tabs: TabLayout,
892
+ query_registry: dict[str, AnyQuery],
893
+ face_id: str,
894
+ depth: int,
895
+ base_path: Path | None = None,
896
+ default_source: str | None = None,
897
+ chart_registry: dict[str, Any] | None = None,
898
+ theme: str | None = None,
899
+ *,
900
+ _resolve_jinja: bool = False,
901
+ resolved_style: MergedStyle,
902
+ parent_level: int = 0,
903
+ ) -> tuple[list[LayoutItem], list[str]]:
904
+ """Resolve tab layout items.
905
+
906
+ Args:
907
+ tabs: TabLayout definition
908
+ charts: Available charts
909
+ query_registry: Query registry
910
+ face_id: Parent face ID
911
+ theme: Vega-Lite theme to inherit to nested faces
912
+ depth: Nesting depth
913
+ base_path: Base path for resolving external file references
914
+ default_source: Default source for inline queries
915
+ parent_variables: Variables from parent scope for template resolution
916
+ chart_registry: Raw chart definitions for nested face normalization
917
+
918
+ Returns:
919
+ Tuple of (items, tab_titles)
920
+ """
921
+ from dataface.core.compile.normalizer import normalize_face
922
+
923
+ resolved: list[LayoutItem] = []
924
+ titles: list[str] = []
925
+
926
+ for idx, tab_item in enumerate(tabs.items):
927
+ titles.append(tab_item.title)
928
+
929
+ # Tab items can have nested layouts
930
+ tab_dict = tab_item.model_dump(exclude_none=True)
931
+
932
+ if any(k in tab_dict for k in ["rows", "cols", "grid", "tabs"]):
933
+ # Tab is a nested face. Pop TabItem-only fields not on AuthoredFace,
934
+ # then model_validate so style errors include the "style." loc prefix.
935
+ tab_dict.pop("icon", None)
936
+ nested_face = AuthoredFace.model_validate(tab_dict)
937
+ compiled_nested = normalize_face(
938
+ nested_face,
939
+ face_id=f"{face_id}_tab{idx}",
940
+ parent_context={
941
+ "parent_id": face_id,
942
+ "base_path": base_path,
943
+ "default_source": default_source,
944
+ "theme": theme,
945
+ "_resolve_jinja": _resolve_jinja,
946
+ },
947
+ query_registry=query_registry,
948
+ chart_registry=chart_registry,
949
+ depth=depth + 1,
950
+ base_path=base_path,
951
+ parent_level=parent_level,
952
+ )
953
+ resolved.append(
954
+ LayoutItem(
955
+ type="face",
956
+ face=compiled_nested,
957
+ description=tab_item.description,
958
+ )
959
+ )
960
+
961
+ elif tab_item.text:
962
+ # Content-only tab
963
+ content_face = Face(
964
+ id=f"{face_id}_tab{idx}",
965
+ title=tab_item.title,
966
+ description=tab_item.description or "",
967
+ text=tab_item.text,
968
+ layout=Layout(type=LayoutType.ROWS, items=[]),
969
+ theme=theme,
970
+ resolved_style=resolved_style,
971
+ level=parent_level + 1,
972
+ )
973
+ resolved.append(
974
+ LayoutItem(
975
+ type="face",
976
+ face=content_face,
977
+ description=tab_item.description,
978
+ )
979
+ )
980
+
981
+ else:
982
+ # Empty tab
983
+ empty_face = Face(
984
+ id=f"{face_id}_tab{idx}",
985
+ title=tab_item.title,
986
+ description=tab_item.description or "",
987
+ layout=Layout(type=LayoutType.ROWS, items=[]),
988
+ theme=theme,
989
+ resolved_style=resolved_style,
990
+ level=parent_level + 1,
991
+ )
992
+ resolved.append(
993
+ LayoutItem(
994
+ type="face",
995
+ face=empty_face,
996
+ description=tab_item.description,
997
+ )
998
+ )
999
+
1000
+ return resolved, titles