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,904 @@
1
+ """AuthoredFace compiler module.
2
+
3
+ Stage: COMPILE
4
+ Purpose: Compile YAML face definitions into Face objects
5
+ ready for execution and rendering.
6
+
7
+ Entry Points:
8
+ - compile(yaml_content: str) -> CompileResult
9
+ - compile_file(file_path: Path) -> CompileResult
10
+
11
+ This is the main orchestrator for compilation. It:
12
+ 1. Parses YAML to AuthoredFace (parser.py)
13
+ 2. Validates the face structure (validator.py)
14
+ 3. Normalizes references and adds metadata (normalizer.py)
15
+
16
+ Layout dimensions are calculated later in the render pipeline (after query
17
+ execution) so table heights can use actual row counts.
18
+
19
+ Dependencies:
20
+ - .parser (parse_yaml)
21
+ - .validator (validate_face)
22
+ - .normalizer (normalize_face)
23
+ - .jinja (detect_query_dependencies)
24
+ - .errors (CompilationError, etc.)
25
+
26
+ See also:
27
+ - docs/docs/contributing/architecture.md for the pipeline overview
28
+ - execute/executor.py for the next stage
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ from dataclasses import dataclass, field
35
+ from pathlib import Path
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ import yaml
39
+ from pydantic import ValidationError as PydanticValidationError
40
+
41
+ from dataface.core.compile.models.face.compiled import Face
42
+ from dataface.core.compile.models.query.compiled import AnyQuery
43
+ from dataface.core.errors.structured import StructuredError
44
+ from dataface.core.render.warnings import unreferenced_chart
45
+ from dataface.core.render.warnings.base import RenderWarning
46
+ from dataface.core.render.warnings.from_query_diagnostic import from_query_diagnostic
47
+
48
+ logger = logging.getLogger(__name__)
49
+ from dataface.core.compile.errors import (
50
+ CompilationError,
51
+ JinjaError,
52
+ ParseError,
53
+ ReferenceError,
54
+ )
55
+ from dataface.core.compile.jinja import detect_query_dependencies
56
+ from dataface.core.compile.meta import get_meta_for_face, merge_meta_with_face
57
+ from dataface.core.compile.models.face.authored import AuthoredFace
58
+ from dataface.core.compile.models.refs import ChartRef, QueryRef, VariableRef
59
+ from dataface.core.compile.models.variable.authored import Variable
60
+ from dataface.core.compile.normalizer import normalize_face, normalize_query
61
+ from dataface.core.compile.parser import parse_yaml
62
+ from dataface.core.compile.validator import validate_face
63
+ from dataface.core.compile.yaml_error_formatter import (
64
+ format_validation_errors_structured,
65
+ )
66
+
67
+ if TYPE_CHECKING:
68
+ from dataface.core.inspect.query_validator import (
69
+ QueryDiagnostic,
70
+ RelationshipContext,
71
+ )
72
+
73
+
74
+ def _to_plain_dict(obj: Any) -> Any:
75
+ """Recursively convert DotDict (or any dict subclass) to plain dict."""
76
+ if isinstance(obj, dict):
77
+ return {k: _to_plain_dict(v) for k, v in obj.items()}
78
+ if isinstance(obj, list):
79
+ return [_to_plain_dict(v) for v in obj]
80
+ return obj
81
+
82
+
83
+ def resolve_project_source_paths(
84
+ sources: dict[str, dict[str, Any]],
85
+ project_dir: Path,
86
+ ) -> dict[str, dict[str, Any]]:
87
+ """Resolve relative file/database paths in project-level source configs."""
88
+ resolved_sources: dict[str, dict[str, Any]] = {}
89
+
90
+ for name, config in sources.items():
91
+ resolved_config = dict(config)
92
+ source_type = resolved_config.get("type")
93
+
94
+ if source_type == "duckdb":
95
+ path_value = resolved_config.get("path")
96
+ if (
97
+ isinstance(path_value, str)
98
+ and path_value != ":memory:"
99
+ and not Path(path_value).is_absolute()
100
+ ):
101
+ resolved_config["path"] = str((project_dir / path_value).resolve())
102
+ elif source_type in {"csv", "parquet", "json"}:
103
+ file_value = resolved_config.get("file")
104
+ if isinstance(file_value, str) and not Path(file_value).is_absolute():
105
+ resolved_config["file"] = str((project_dir / file_value).resolve())
106
+
107
+ resolved_sources[name] = resolved_config
108
+
109
+ return resolved_sources
110
+
111
+
112
+ @dataclass
113
+ class CompileResult:
114
+ """Result of compilation.
115
+
116
+ Contains the compiled face (if successful), any errors encountered,
117
+ and warnings that don't prevent compilation.
118
+
119
+ Attributes:
120
+ face: Compiled face (None if errors occurred)
121
+ errors: Structured compile errors (list[StructuredError] — union-aware collapse applied)
122
+ warnings: Non-fatal warnings as ``RenderWarning`` objects with stable
123
+ SCREAMING_SNAKE_CASE codes. Each emitter (orphan-chart check,
124
+ validate_compiled_queries) owns its code on a module under
125
+ ``dataface.core.render.warnings``.
126
+ suppressed_warnings: ``RenderWarning`` entries that matched a
127
+ suppression layer (query-level ``ignore:`` or meta.yaml lint
128
+ config). Kept separate so consumers can surface "would have
129
+ warned" without re-running validation.
130
+ diagnostics: Structured query diagnostics from validate_compiled_queries.
131
+ Includes all severity levels; ``warnings`` only includes warning+error.
132
+ Infrastructure for structured consumers (UI, AI agents).
133
+ query_registry: All normalized queries (for executor)
134
+ sources: Source configurations (for resolving named source references)
135
+
136
+ Example:
137
+ >>> result = compile(yaml_content)
138
+ >>> if result.success:
139
+ ... print(f"Compiled: {result.face.title}")
140
+ ... else:
141
+ ... for error in result.errors:
142
+ ... print(f"Error: {error}")
143
+ """
144
+
145
+ face: Face | None = None
146
+ errors: list[StructuredError] = field(default_factory=list)
147
+ warnings: list[RenderWarning] = field(default_factory=list)
148
+ suppressed_warnings: list[RenderWarning] = field(default_factory=list)
149
+ diagnostics: list[QueryDiagnostic] = field(default_factory=list)
150
+ query_registry: dict[str, AnyQuery] = field(default_factory=dict)
151
+ meta_config: Any = None
152
+
153
+ @property
154
+ def success(self) -> bool:
155
+ """Check if compilation was successful."""
156
+ return self.face is not None and len(self.errors) == 0
157
+
158
+
159
+ def compile(
160
+ yaml_content: str,
161
+ options: dict[str, Any] | None = None,
162
+ base_dir: Path | None = None,
163
+ project_dir: Path | None = None,
164
+ ) -> CompileResult:
165
+ """Compile YAML content to a Face.
166
+
167
+ Stage: COMPILE (Full Pipeline)
168
+
169
+ This is the main entry point for compilation. It orchestrates:
170
+ 1. PARSE: YAML string → AuthoredFace object
171
+ 2. VALIDATE: Check structure and references
172
+ 3. NORMALIZE: Resolve references, add metadata
173
+
174
+ Layout sizing happens later in the render pipeline (data-aware).
175
+
176
+ Args:
177
+ yaml_content: YAML string to compile
178
+ options: Optional compilation options
179
+ base_dir: Base directory for resolving file references
180
+ project_dir: Project directory for resolving project-level sources
181
+
182
+ Returns:
183
+ CompileResult with compiled face or errors
184
+
185
+ Example:
186
+ >>> yaml_content = '''
187
+ ... title: My Dataface
188
+ ... queries:
189
+ ... users: SELECT * FROM users
190
+ ... charts:
191
+ ... user_count:
192
+ ... query: users
193
+ ... type: kpi
194
+ ... value: count
195
+ ... rows:
196
+ ... - user_count
197
+ ... '''
198
+ >>> result = compile(yaml_content)
199
+ >>> if result.success:
200
+ ... face = result.face
201
+ ... print(face.title) # "My Dataface"
202
+ """
203
+ options = options or {}
204
+ errors: list[StructuredError] = []
205
+ warnings: list[RenderWarning] = []
206
+
207
+ # ════════════════════════════════════════════════════════════════════
208
+ # STEP 1: Parse YAML
209
+ # ════════════════════════════════════════════════════════════════════
210
+ # Convert YAML string to AuthoredFace object. Handles syntax errors.
211
+ try:
212
+ face = parse_yaml(yaml_content)
213
+ except ParseError as e:
214
+ # When the parse error was caused by a PydanticValidationError, use the
215
+ # union-aware collapse to produce structured errors from the raw Pydantic
216
+ # error list. Other parse errors (bad YAML syntax, empty doc) produce one
217
+ # structured error from the CompilationError shape.
218
+ if isinstance(e.__cause__, PydanticValidationError):
219
+ return CompileResult(
220
+ errors=format_validation_errors_structured(e.__cause__, yaml_content)
221
+ )
222
+ return CompileResult(errors=[e.to_structured()])
223
+
224
+ # ════════════════════════════════════════════════════════════════════
225
+ # STEP 2: Validate Structure
226
+ # ════════════════════════════════════════════════════════════════════
227
+ # Check references and semantic constraints.
228
+ validation_errors = validate_face(face)
229
+ if validation_errors:
230
+ return CompileResult(errors=[e.to_structured() for e in validation_errors])
231
+
232
+ # ════════════════════════════════════════════════════════════════════
233
+ # STEP 3: Get Default Source and Extract Named Sources
234
+ # ════════════════════════════════════════════════════════════════════
235
+ # Get project-level sources when available so named source references and
236
+ # project defaults behave consistently for file-based compiles.
237
+ project_sources = None
238
+ if project_dir is not None:
239
+ from dataface.core.compile.config import get_project_sources
240
+
241
+ project_sources = get_project_sources(project_dir)
242
+
243
+ # Get the face's default source to pass to query normalization.
244
+ default_source = face.get_default_source()
245
+ if default_source is None and project_sources is not None:
246
+ default_source = project_sources.default
247
+
248
+ # Extract named source configurations from the sources section
249
+ # These are used to resolve source references in queries (e.g., source: profiles)
250
+ sources_registry = {}
251
+ if project_sources is not None:
252
+ assert project_dir is not None
253
+ sources_registry.update(
254
+ resolve_project_source_paths(project_sources.sources, project_dir)
255
+ )
256
+ sources_registry.update(_extract_sources(face))
257
+
258
+ # ════════════════════════════════════════════════════════════════════
259
+ # STEP 4: Build Query Registry
260
+ # ════════════════════════════════════════════════════════════════════
261
+ # Collect and normalize all queries before normalization.
262
+ # This allows charts to reference queries from any part of the face.
263
+ try:
264
+ query_registry = build_query_registry(
265
+ face, base_dir, default_source=default_source
266
+ )
267
+ except CompilationError as e:
268
+ return CompileResult(errors=[e.to_structured()])
269
+
270
+ # ════════════════════════════════════════════════════════════════════
271
+ # STEP 5: Detect Circular Dependencies
272
+ # ════════════════════════════════════════════════════════════════════
273
+ # Check for {{ queries.* }} cycles in SQL.
274
+ if query_registry:
275
+ try:
276
+ detect_query_dependencies(query_registry)
277
+ except JinjaError as e:
278
+ return CompileResult(errors=[e.to_structured()])
279
+
280
+ # ════════════════════════════════════════════════════════════════════
281
+ # STEP 5b: Build Chart Registry
282
+ # ════════════════════════════════════════════════════════════════════
283
+ # Collect all charts from the entire face tree (global namespace).
284
+ # Charts are global like queries - any layout can reference any chart.
285
+ try:
286
+ chart_registry = build_chart_registry(face, base_dir=base_dir)
287
+ except CompilationError as e:
288
+ return CompileResult(errors=[e.to_structured()])
289
+
290
+ # ════════════════════════════════════════════════════════════════════
291
+ # STEP 6: Normalize (Transform to Face)
292
+ # ════════════════════════════════════════════════════════════════════
293
+ # Resolve references, add metadata, create unified layout.
294
+ # Note: Variable registry is built by renderer at render time (not needed here)
295
+ try:
296
+ compiled = normalize_face(
297
+ face,
298
+ query_registry=query_registry,
299
+ chart_registry=chart_registry,
300
+ base_path=base_dir,
301
+ )
302
+ except ReferenceError as e:
303
+ return CompileResult(errors=[e.to_structured()])
304
+ except CompilationError as e:
305
+ return CompileResult(errors=[e.to_structured()])
306
+ except PydanticValidationError as e:
307
+ return CompileResult(
308
+ errors=format_validation_errors_structured(e, yaml_content)
309
+ )
310
+
311
+ compiled.sources = _to_plain_dict(sources_registry)
312
+
313
+ # Orphan charts: defined but never referenced from any layout in the tree.
314
+ # Warn rather than error — a content-only face still renders, the warning
315
+ # surfaces lazy authoring. Render-side has the harder check for the
316
+ # specific case of "face has charts but layout is empty" (Case A) which
317
+ # would produce a silently empty dashboard.
318
+ for chart_id in _collect_orphan_charts(compiled):
319
+ warnings.append(
320
+ RenderWarning(
321
+ code=unreferenced_chart.CODE,
322
+ chart=chart_id,
323
+ message=(
324
+ f"Chart '{chart_id}' is defined but not referenced in any layout."
325
+ ),
326
+ fix=(
327
+ "Add it to `rows:`/`cols:`/`grid:`/`tabs:` "
328
+ "or delete the definition."
329
+ ),
330
+ )
331
+ )
332
+
333
+ # Layout dimensions are calculated later in the render pipeline
334
+ # (after query execution) so table heights can use actual row counts.
335
+
336
+ return CompileResult(
337
+ face=compiled,
338
+ errors=errors,
339
+ warnings=warnings,
340
+ query_registry=query_registry,
341
+ meta_config=options.get("meta_config"),
342
+ )
343
+
344
+
345
+ def _collect_orphan_charts(face: Face) -> list[str]:
346
+ """Return chart IDs defined somewhere in the face tree but never placed
347
+ in any layout. Walks nested faces; chart references resolve globally
348
+ (a parent-defined chart referenced from a nested face is not an orphan).
349
+ """
350
+ defined: set[str] = set()
351
+ referenced: set[str] = set()
352
+
353
+ def walk(f: Face) -> None:
354
+ if f.charts:
355
+ defined.update(f.charts.keys())
356
+ for item in f.layout.items:
357
+ if item.type == "chart" and item.chart and item.chart.id:
358
+ referenced.add(item.chart.id)
359
+ elif item.type == "face" and item.face is not None:
360
+ walk(item.face)
361
+
362
+ walk(face)
363
+ return sorted(defined - referenced)
364
+
365
+
366
+ def _collect_suppressed_codes(
367
+ result: CompileResult,
368
+ meta_config: Any = None,
369
+ ) -> dict[str, set[str]]:
370
+ """Build per-query suppression sets from query ignore + meta.yaml lint config.
371
+
372
+ Returns a dict mapping query_name → set of suppressed diagnostic codes.
373
+ """
374
+ per_query: dict[str, set[str]] = {}
375
+ # Layer 2: per-query ignore field
376
+ for name, query in result.query_registry.items():
377
+ codes: set[str] = set()
378
+ if query.ignore:
379
+ codes.update(query.ignore)
380
+ per_query[name] = codes
381
+
382
+ # Layer 3: meta.yaml lint config
383
+ if meta_config is not None:
384
+ global_ignore = set(getattr(meta_config, "lint_ignore", []))
385
+ per_query_meta = getattr(meta_config, "lint_ignore_queries", {})
386
+ for name in per_query:
387
+ per_query[name] |= global_ignore
388
+ if name in per_query_meta:
389
+ per_query[name].update(per_query_meta[name])
390
+
391
+ return per_query
392
+
393
+
394
+ def validate_compiled_queries(
395
+ result: CompileResult,
396
+ relationship_context: RelationshipContext | None = None,
397
+ ) -> None:
398
+ """Run query validation on all SQL queries in a compile result (mutates in place).
399
+
400
+ Calls ``validate_query`` for each SQL query, appends structured
401
+ ``QueryDiagnostic`` objects to ``result.diagnostics``, and emits
402
+ ``RenderWarning`` entries onto ``result.warnings`` for findings at
403
+ warning severity or above. Each warning carries a stable code owned by
404
+ a module under ``dataface.core.render.warnings`` (see
405
+ ``from_query_diagnostic`` for the mapping).
406
+
407
+ Honors diagnostic suppressions from:
408
+ - SQL-inline ``-- dft:ignore`` comments (handled by validate_query)
409
+ - Per-query ``ignore`` field
410
+ - meta.yaml ``lint`` config
411
+
412
+ Args:
413
+ result: CompileResult to enrich.
414
+ relationship_context: Optional RelationshipContext for severity calibration.
415
+ """
416
+ if not result.success or not result.query_registry:
417
+ return
418
+
419
+ from dataface.core.compile.models.query.compiled import is_sql_query
420
+ from dataface.core.inspect.query_validator import validate_query
421
+
422
+ suppressed = _collect_suppressed_codes(result, result.meta_config)
423
+
424
+ for name, query in result.query_registry.items():
425
+ if not is_sql_query(query):
426
+ continue
427
+ query_suppress = suppressed.get(name, set())
428
+ diags, suppressed_diags = validate_query(
429
+ query.sql,
430
+ relationship_context=relationship_context,
431
+ suppress=query_suppress,
432
+ return_suppressed=True,
433
+ )
434
+ for d in diags:
435
+ result.diagnostics.append(d)
436
+ if d.severity in ("error", "warning"):
437
+ result.warnings.append(from_query_diagnostic(name, d))
438
+ for d in suppressed_diags:
439
+ result.suppressed_warnings.append(from_query_diagnostic(name, d))
440
+
441
+
442
+ def compile_file(
443
+ file_path: Path,
444
+ options: dict[str, Any] | None = None,
445
+ apply_meta: bool = True,
446
+ root_path: Path | None = None,
447
+ ) -> CompileResult:
448
+ """Compile a YAML file to a Face.
449
+
450
+ Convenience function that reads a file and compiles it. Optionally
451
+ applies meta.yaml cascading configuration from parent directories.
452
+
453
+ Args:
454
+ file_path: Path to YAML file
455
+ options: Optional compilation options
456
+ apply_meta: If True, resolve and apply meta.yaml chain (default: True)
457
+ root_path: Project root for meta resolution (default: auto-detect)
458
+
459
+ Returns:
460
+ CompileResult with compiled face or errors
461
+
462
+ Raises:
463
+ FileNotFoundError: If file doesn't exist
464
+
465
+ Example:
466
+ >>> result = compile_file(Path("face.yml"))
467
+ >>> if result.success:
468
+ ... print(result.face.title)
469
+ """
470
+
471
+ if isinstance(file_path, str):
472
+ file_path = Path(file_path)
473
+
474
+ if not file_path.exists():
475
+ return CompileResult(
476
+ errors=[CompilationError(f"File not found: {file_path}").to_structured()]
477
+ )
478
+
479
+ # Markdown report files: translate to YAML before compiling
480
+ if file_path.suffix.lower() == ".md":
481
+ from dataface.core.compile.markdown import is_markdown_face, markdown_to_yaml
482
+
483
+ if not is_markdown_face(file_path):
484
+ return CompileResult(
485
+ errors=[
486
+ CompilationError(
487
+ "Not a Dataface face file: .md files require YAML frontmatter "
488
+ "with at least one of: queries, charts, variables, source, sources"
489
+ ).to_structured()
490
+ ]
491
+ )
492
+
493
+ try:
494
+ raw_text = file_path.read_text()
495
+ yaml_content = markdown_to_yaml(raw_text)
496
+ except (OSError, ValueError) as e:
497
+ return CompileResult(
498
+ errors=[CompilationError(f"Markdown parse error: {e}").to_structured()]
499
+ )
500
+ else:
501
+ try:
502
+ yaml_content = file_path.read_text()
503
+ except OSError as e:
504
+ return CompileResult(
505
+ errors=[CompilationError(f"Failed to read file: {e}").to_structured()]
506
+ )
507
+
508
+ # Apply meta.yaml chain if enabled
509
+ meta_config = None
510
+ if apply_meta:
511
+ try:
512
+ # Parse YAML to dict first
513
+ face_data = yaml.safe_load(yaml_content) or {}
514
+
515
+ if not isinstance(face_data, dict):
516
+ return CompileResult(
517
+ errors=[
518
+ CompilationError(
519
+ f"Face YAML must be a mapping at the top level, got {type(face_data).__name__}"
520
+ ).to_structured()
521
+ ]
522
+ )
523
+
524
+ # Resolve meta chain and merge with face
525
+ meta_config = get_meta_for_face(file_path, root_path=root_path)
526
+ if not meta_config.is_empty():
527
+ merged_data = merge_meta_with_face(meta_config, face_data)
528
+ # Re-serialize to YAML for the compile function
529
+ yaml_content = yaml.dump(
530
+ merged_data, default_flow_style=False, sort_keys=False
531
+ )
532
+ except CompilationError as e:
533
+ return CompileResult(errors=[e.to_structured()])
534
+ except (yaml.YAMLError, OSError) as e:
535
+ # Meta resolution errors are warnings, not failures
536
+ # Continue with original content, but log the issue for debugging
537
+ logger.warning(
538
+ "Meta resolution failed for %s, continuing with original content: %s",
539
+ file_path,
540
+ e,
541
+ )
542
+
543
+ project_dir = root_path
544
+ if project_dir is None:
545
+ from dataface.core.project_roots import discover_render_context
546
+
547
+ compile_boundary = Path(file_path.anchor) if file_path.anchor else None
548
+ project_dir, _ = discover_render_context(file_path.parent, compile_boundary)
549
+
550
+ # Thread meta lint config through to compile for query diagnostics
551
+ compile_options = dict(options) if options else {}
552
+ if meta_config is not None:
553
+ compile_options["meta_config"] = meta_config
554
+
555
+ result = compile(
556
+ yaml_content,
557
+ options=compile_options,
558
+ base_dir=file_path.parent,
559
+ project_dir=project_dir,
560
+ )
561
+ # Stamp file path and add validate next_command on each compile error so
562
+ # UI surfaces and CLI can surface "dft validate <file>" without re-deriving the path.
563
+ if result.errors:
564
+ file_str = str(file_path)
565
+ result.errors = [_stamp_error_file(e, file_str) for e in result.errors]
566
+ return result
567
+
568
+
569
+ def _stamp_error_file(err: StructuredError, file_str: str) -> StructuredError:
570
+ """Return a copy of err with file set and a Validate next_command added."""
571
+ from dataface.core.errors.structured import NextCommand
572
+
573
+ next_commands = list(err.next_commands)
574
+ if not any(nc.label == "Validate" for nc in next_commands):
575
+ next_commands.append(
576
+ NextCommand(label="Validate", command=f"dft validate {file_str}")
577
+ )
578
+ return err.model_copy(update={"file": file_str, "next_commands": next_commands})
579
+
580
+
581
+ def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
582
+ """Extract all nested faces from a face's layout.
583
+
584
+ Recursively traverses rows, cols, grid, and tabs to find all nested faces.
585
+
586
+ Args:
587
+ face: AuthoredFace to extract nested faces from
588
+
589
+ Returns:
590
+ List of nested AuthoredFace objects found in the layout
591
+ """
592
+ from dataface.core.compile.models.face.authored import (
593
+ ForeachItem,
594
+ TabItem,
595
+ )
596
+
597
+ nested_faces: list[AuthoredFace] = []
598
+ items: list[Any] = []
599
+
600
+ # Collect items from rows and cols
601
+ if face.rows:
602
+ items.extend(face.rows)
603
+ if face.cols:
604
+ items.extend(face.cols)
605
+
606
+ # Collect items from grid (always GridLayout now — dict form removed)
607
+ if face.grid:
608
+ items.extend([gi.item for gi in face.grid.items])
609
+
610
+ # Collect items from tabs (always TabLayout now — dict form removed)
611
+ if face.tabs:
612
+ items.extend(face.tabs.items)
613
+
614
+ # Process all items to find nested faces
615
+ for item in items:
616
+ if isinstance(item, AuthoredFace):
617
+ nested_faces.append(item)
618
+ elif isinstance(item, TabItem):
619
+ # Tab items contain their own rows/cols — treat as an inline face
620
+ tab_face = AuthoredFace.model_construct(
621
+ rows=item.rows,
622
+ cols=item.cols,
623
+ grid=item.grid,
624
+ tabs=item.tabs,
625
+ )
626
+ nested_faces.append(tab_face)
627
+ elif isinstance(item, ForeachItem):
628
+ # Recurse into foreach items to find any nested faces
629
+ for foreach_subitem in item.foreach.items:
630
+ if isinstance(foreach_subitem, AuthoredFace):
631
+ nested_faces.append(foreach_subitem)
632
+
633
+ return nested_faces
634
+
635
+
636
+ def _build_registry(
637
+ face: AuthoredFace,
638
+ registry_type: str,
639
+ registry: dict[str, Any] | None = None,
640
+ **kwargs: Any,
641
+ ) -> dict[str, Any]:
642
+ """Build a registry for queries or charts recursively.
643
+
644
+ This unified function traverses the entire face tree to collect definitions
645
+ of the specified type. Each face processes its own definitions, then recursively
646
+ processes all nested faces and merges their results.
647
+
648
+ Note: Variables are built at render time, not compile time, so they don't use
649
+ this registry building function.
650
+
651
+ Args:
652
+ face: AuthoredFace to process
653
+ registry_type: Type of registry to build ("queries" or "charts")
654
+ registry: Existing registry to add to (will be created if None)
655
+ **kwargs: Additional arguments specific to registry type:
656
+ - For "queries": base_dir, default_source
657
+ - For "charts": base_dir
658
+
659
+ Returns:
660
+ Complete registry dictionary
661
+
662
+ Raises:
663
+ CompilationError: If duplicate names found or invalid registry_type
664
+ """
665
+ if registry is None:
666
+ registry = {}
667
+
668
+ # Process definitions at this face level
669
+ if registry_type == "queries":
670
+ _process_queries_for_registry(face, registry, **kwargs)
671
+ elif registry_type == "charts":
672
+ _process_charts_for_registry(face, registry, **kwargs)
673
+ else:
674
+ raise CompilationError(f"Invalid registry type: {registry_type}")
675
+
676
+ # Recursively process nested faces
677
+ nested_faces = _extract_nested_faces(face)
678
+ for nested_face in nested_faces:
679
+ _build_registry(nested_face, registry_type, registry, **kwargs)
680
+
681
+ return registry
682
+
683
+
684
+ def _process_queries_for_registry(
685
+ face: AuthoredFace,
686
+ registry: dict[str, AnyQuery],
687
+ base_dir: Path | None = None,
688
+ default_source: str | None = None,
689
+ ) -> None:
690
+ """Process queries from a face and add to registry.
691
+
692
+ Args:
693
+ face: AuthoredFace to process queries from
694
+ registry: Registry to add queries to
695
+ base_dir: Base directory for cross-file references
696
+ default_source: Default source to apply to queries without explicit source
697
+
698
+ Raises:
699
+ CompilationError: If duplicate query names found
700
+ """
701
+ # Get default source from this face level (may override parent)
702
+ face_default_source = face.get_default_source()
703
+ effective_default_source = face_default_source or default_source
704
+
705
+ # Process queries at this level
706
+ for name, query_def in (face.queries or {}).items():
707
+ if name in registry:
708
+ raise CompilationError(
709
+ f"Duplicate query name '{name}'. Query names must be unique "
710
+ "within a file (and those that are imported)."
711
+ )
712
+
713
+ # Handle cross-file references
714
+ if isinstance(query_def, QueryRef):
715
+ registry[name] = load_from_reference(query_def, base_dir=base_dir)
716
+ else:
717
+ registry[name] = normalize_query(
718
+ name, query_def, default_source=effective_default_source
719
+ )
720
+
721
+
722
+ def _process_charts_for_registry(
723
+ face: AuthoredFace,
724
+ registry: dict[str, Any],
725
+ base_dir: Path | None = None,
726
+ **kwargs: Any,
727
+ ) -> None:
728
+ """Process charts from a face and add to registry.
729
+
730
+ Args:
731
+ face: AuthoredFace to process charts from
732
+ registry: Registry to add charts to
733
+ base_dir: Base directory for cross-file references
734
+ **kwargs: Additional arguments (e.g., query_registry for future use)
735
+
736
+ Raises:
737
+ CompilationError: If duplicate chart names found
738
+ """
739
+ # Process charts at this level
740
+ for name, chart_def in (face.charts or {}).items():
741
+ if name in registry:
742
+ raise CompilationError(
743
+ f"Duplicate chart name '{name}'. Chart names must be unique "
744
+ "within a file (and those that are imported)."
745
+ )
746
+
747
+ # Handle cross-file references
748
+ if isinstance(chart_def, ChartRef):
749
+ registry[name] = load_from_reference(chart_def, base_dir=base_dir)
750
+ else:
751
+ registry[name] = chart_def
752
+
753
+
754
+ def build_query_registry(
755
+ face: AuthoredFace,
756
+ base_dir: Path | None = None,
757
+ registry: dict[str, AnyQuery] | None = None,
758
+ default_source: str | None = None,
759
+ ) -> dict[str, AnyQuery]:
760
+ """Build complete query registry from face and nested faces.
761
+
762
+ Traverses the entire face structure to collect all query definitions,
763
+ including from nested faces.
764
+
765
+ Args:
766
+ face: AuthoredFace to process
767
+ base_dir: Base directory for cross-file references
768
+ registry: Existing registry to add to
769
+ default_source: Default source to apply to queries without explicit source
770
+
771
+ Returns:
772
+ Complete query registry
773
+
774
+ Raises:
775
+ CompilationError: If duplicate query names found
776
+ """
777
+ return _build_registry(
778
+ face,
779
+ "queries",
780
+ registry=registry,
781
+ base_dir=base_dir,
782
+ default_source=default_source,
783
+ )
784
+
785
+
786
+ def build_chart_registry(
787
+ face: AuthoredFace,
788
+ base_dir: Path | None = None,
789
+ registry: dict[str, Any] | None = None,
790
+ ) -> dict[str, Any]:
791
+ """Build complete chart registry from face and nested faces.
792
+
793
+ Traverses the entire face structure to collect all chart definitions,
794
+ including from nested faces. Charts are global - any layout can reference any chart.
795
+
796
+ Args:
797
+ face: AuthoredFace to process
798
+ base_dir: Base directory for cross-file references
799
+ registry: Existing registry to add to
800
+
801
+ Returns:
802
+ Complete chart registry (raw chart definitions, not normalized)
803
+
804
+ Raises:
805
+ CompilationError: If duplicate chart names found
806
+ """
807
+ return _build_registry(face, "charts", registry=registry, base_dir=base_dir)
808
+
809
+
810
+ def load_from_reference(
811
+ reference: VariableRef | QueryRef | ChartRef,
812
+ base_dir: Path | None = None,
813
+ ) -> Any:
814
+ """Load an item from a typed cross-file reference.
815
+
816
+ Args:
817
+ reference: A typed ref model (VariableRef, QueryRef, or ChartRef).
818
+ The grammar has already been validated by the Pydantic model.
819
+ base_dir: Base directory for resolving paths
820
+
821
+ Returns:
822
+ - QueryRef → AnyQuery
823
+ - ChartRef → chart dict
824
+ - VariableRef → Variable
825
+
826
+ Raises:
827
+ CompilationError: If reference cannot be resolved
828
+ """
829
+ if isinstance(reference, VariableRef):
830
+ section_name = "variables"
831
+ elif isinstance(reference, QueryRef):
832
+ section_name = "queries"
833
+ elif isinstance(reference, ChartRef):
834
+ section_name = "charts"
835
+ else:
836
+ raise CompilationError(f"Unexpected reference type: {type(reference)!r}")
837
+
838
+ # Grammar already validated by the typed model; split is deterministic.
839
+ file_path_str, item_name = reference.ref.rsplit(f".{section_name}.", 1)
840
+
841
+ # Add extension if needed
842
+ if not (file_path_str.endswith(".yml") or file_path_str.endswith(".yaml")):
843
+ file_path_str += ".yml"
844
+
845
+ full_path = base_dir / file_path_str if base_dir else Path(file_path_str)
846
+
847
+ if not full_path.exists():
848
+ raise CompilationError(f"Referenced file not found: {file_path_str}")
849
+
850
+ try:
851
+ with open(full_path) as f:
852
+ content = yaml.safe_load(f)
853
+ except (OSError, yaml.YAMLError) as e:
854
+ raise CompilationError(f"Failed to load {file_path_str}: {e}") from e
855
+
856
+ if not content or section_name not in content:
857
+ raise CompilationError(f"No {section_name} found in {file_path_str}")
858
+
859
+ section = content[section_name]
860
+ singular = {"queries": "query", "charts": "chart", "variables": "variable"}.get(
861
+ section_name, section_name[:-1]
862
+ )
863
+
864
+ if not isinstance(section, dict) or item_name not in section:
865
+ raise CompilationError(
866
+ f"{singular.capitalize()} '{item_name}' not found in {file_path_str}"
867
+ )
868
+
869
+ item_def = section[item_name]
870
+
871
+ if section_name == "queries":
872
+ return normalize_query(item_name, item_def)
873
+ if section_name == "charts":
874
+ return item_def
875
+ # section_name == "variables"
876
+ if isinstance(item_def, Variable):
877
+ return item_def
878
+ if isinstance(item_def, dict):
879
+ return Variable(**item_def)
880
+ raise CompilationError(
881
+ f"Invalid variable definition for '{item_name}' in {file_path_str}"
882
+ )
883
+
884
+
885
+ def _extract_sources(face: AuthoredFace) -> dict[str, dict]:
886
+ """Extract named source configurations from AuthoredFace.
887
+
888
+ Extracts named source definitions (excluding 'default') into a dict.
889
+
890
+ Args:
891
+ face: AuthoredFace with optional sources section
892
+
893
+ Returns:
894
+ Dict mapping source name to source config dict
895
+ """
896
+ if not face.sources:
897
+ return {}
898
+
899
+ # Extract named sources (everything except 'default')
900
+ return {
901
+ k: v
902
+ for k, v in face.sources.model_dump(exclude_none=True).items()
903
+ if k != "default"
904
+ }