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,299 @@
1
+ """DFT typographic system: title level, width offsets, sizes, weights, and family.
2
+
3
+ A title's rendered size is determined by two orthogonal axes:
4
+
5
+ 1. **Heading level** (``face.level``): the H-tag the title corresponds to.
6
+ Root face = 1 (H1), nested face = 2 (H2), chart title = parent face level + 1.
7
+
8
+ 2. **Width offset**: an additive shift on the heading level based on card pixel
9
+ width. Narrow cards "demote" a title one step down the size ramp; tiny cards
10
+ demote it further. Offsets live in theme YAML under
11
+ ``style.title.width_offsets.*``. Width breakpoints are engine constants here.
12
+
13
+ Together they compute:
14
+ effective_level = clamp(level + width_offset(width), 1, len(sizes))
15
+ font_size = sizes[effective_level - 1]
16
+
17
+ Width tiers and their default offsets:
18
+
19
+ tiny (< 360px): offset +3 / weight 600 (legibility floor)
20
+ narrow (3–11 cols, 360–559px): offset +1 / weight from config
21
+ medium (12–23 cols, 560–1099px): offset 0 / weight from config
22
+ wide (24 cols, ≥ 1100px): offset 0 / weight from config
23
+
24
+ Font family rules (theme-driven; values below are per-theme):
25
+ - Tiny + narrow chart titles always resolve to ``style.font.family`` (the body
26
+ family). On every shipped theme that's a sans family.
27
+ - Medium + wide chart titles resolve to ``style.title.font.family`` (the
28
+ title-slot family). On ``stark`` this is also sans, so titles are sans at
29
+ every width. On the shipped ``default`` (and ``cream``) the title-slot
30
+ family is serif (Source Serif 4), so medium/wide chart titles render serif
31
+ while smaller cards stay sans for legibility.
32
+ - Prose content and their titles: serif at any width, regardless of theme.
33
+ Prose is detected via ``is_prose(text)`` — true when word count ≥ 100.
34
+
35
+ Page and section titles (face/board headers) always resolve to
36
+ ``style.font.family`` (sans on every shipped theme) at every width unless the
37
+ face contains prose content. Face titles clamp the width used for offset
38
+ computation to the narrow boundary (≥ 360px) — a face header should never
39
+ shrink below the narrow tier even on a narrow nested container.
40
+
41
+ Chart labels (axis, legend, tick) are separately themed — not controlled here.
42
+
43
+ Usage::
44
+
45
+ from dataface.core.compile.typography import chart_title_spec, face_title_spec
46
+
47
+ font_size, weight, family = chart_title_spec(
48
+ width, level=face_level + 1, resolved_chart_style=resolved_style.charts
49
+ )
50
+ font_size, weight, family = face_title_spec(width, level=face_level)
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ from typing import TYPE_CHECKING
56
+
57
+ if TYPE_CHECKING:
58
+ from dataface.core.compile.models.style.merged import MergedChartsStyle
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Pixel-width thresholds that separate the four width tiers.
62
+ #
63
+ # narrow/medium/wide approximate 11-col and 23-col boundaries for the default
64
+ # 24-column, 1152px-content board (1200px total − 2 × 24px padding).
65
+ # tiny catches sub-narrow charts (small-multiples, 5+ columns) where the flat
66
+ # narrow size feels too large relative to chart width.
67
+ # They are intentionally constant — column-grid geometry, not user preference.
68
+ # Level offsets for each tier live in theme YAML under style.title.width_offsets.*.
69
+ # ---------------------------------------------------------------------------
70
+
71
+ _TINY_MAX: float = 360.0 # < 360px → tiny
72
+ _NARROW_MAX: float = 560.0 # 360–559px → narrow (3–11 cols)
73
+ _WIDE_MIN: float = 1100.0 # ≥ 1100px → wide (24 cols)
74
+ # 560–1099px → medium (12–23 cols)
75
+
76
+
77
+ def width_tier(width: float) -> str:
78
+ """Return the tier name for a card's pixel width.
79
+
80
+ Single source of truth for the tier breakpoints — render-side
81
+ callers (e.g. donut-attached-table trigger) call this rather than
82
+ importing the module-private ``_TINY_MAX`` / ``_NARROW_MAX`` /
83
+ ``_WIDE_MIN`` constants.
84
+ """
85
+ if width < _TINY_MAX:
86
+ return "tiny"
87
+ if width < _NARROW_MAX:
88
+ return "narrow"
89
+ if width < _WIDE_MIN:
90
+ return "medium"
91
+ return "wide"
92
+
93
+
94
+ # Tiny-tier weight floor. At small widths the theme's default weight (500)
95
+ # reads as anemic; 600 is the legibility floor. Other tiers use
96
+ # theme-configured weight.
97
+ _TINY_WEIGHT: int = 600
98
+
99
+ _PROSE_WORD_THRESHOLD = 100
100
+
101
+
102
+ def _coerce_weight(raw: str | float | int) -> int | str:
103
+ """Return a CSS-valid font-weight value from FontStyle.weight.
104
+
105
+ FontStyle.weight is ``str | float | None``. SVG and CSS both accept
106
+ numeric weights (int) and keyword weights (``"bold"``, ``"normal"``).
107
+ Cast numeric values to int so SVG emits ``font-weight="500"`` not
108
+ ``font-weight="500.0"``; leave string keywords unchanged.
109
+ """
110
+ if isinstance(raw, str):
111
+ return raw
112
+ return int(raw)
113
+
114
+
115
+ def _width_offset(width: float, resolved_chart_style: MergedChartsStyle) -> int:
116
+ """Return the additive level offset for a card at the given pixel width.
117
+
118
+ Reads ``title.width_offsets`` from the active resolved style so face-local
119
+ theme overrides flow through. Width breakpoints are engine constants
120
+ (``_TINY_MAX``, ``_NARROW_MAX``, ``_WIDE_MIN``).
121
+
122
+ Args:
123
+ width: Pixel width of the card/chart.
124
+ resolved_chart_style: Active ``MergedChartsStyle`` for the face.
125
+
126
+ Returns:
127
+ Integer offset to add to the base heading level before indexing ``sizes``.
128
+ """
129
+ wo = resolved_chart_style.title.width_offsets
130
+ if width >= _WIDE_MIN:
131
+ return wo.wide
132
+ if width >= _NARROW_MAX:
133
+ return wo.medium
134
+ if width >= _TINY_MAX:
135
+ return wo.narrow
136
+ return wo.tiny
137
+
138
+
139
+ def is_prose(text: str) -> bool:
140
+ """Return True if *text* qualifies as prose.
141
+
142
+ Prose is defined as body text with at least ``_PROSE_WORD_THRESHOLD`` words.
143
+ Prose content and its surrounding title render in serif at any card width.
144
+
145
+ Args:
146
+ text: Raw text content (Markdown or plain).
147
+
148
+ Returns:
149
+ ``True`` when the word count is ≥ 100.
150
+ """
151
+ return len(text.split()) >= _PROSE_WORD_THRESHOLD
152
+
153
+
154
+ def chart_title_spec(
155
+ width: float,
156
+ *,
157
+ level: int,
158
+ resolved_chart_style: MergedChartsStyle,
159
+ use_title_family: bool | None = None,
160
+ ) -> tuple[int, int | str, str]:
161
+ """Return ``(font_size, font_weight, font_family)`` for a chart title.
162
+
163
+ Computes ``effective_level = clamp(level + width_offset(width), 1, len(sizes))``
164
+ then returns ``sizes[effective_level - 1]``.
165
+
166
+ Reads from the *active* resolved style passed in — not from
167
+ ``get_config()`` (the global default). That distinction is the whole point:
168
+ pre-fix the function was theme-blind and always returned the default
169
+ theme's families regardless of which theme the face actually used.
170
+
171
+ The resolved style has emoji families already baked into every
172
+ ``FontStyle.family`` by ``resolve_style``'s ``_append_emoji_family`` pass,
173
+ so no further emoji handling is needed here.
174
+
175
+ Level offsets per tier come from
176
+ ``resolved_chart_style.title.width_offsets.{tiny|narrow|medium|wide}``.
177
+ Font sizes come from ``resolved_chart_style.title.sizes`` (the H1–H6 ramp).
178
+
179
+ Family resolution by width tier:
180
+ - tiny + narrow → ``resolved_chart_style.font_family`` (body family — sans on
181
+ every shipped theme)
182
+ - medium + wide → ``resolved_chart_style.title.font.family`` (title-slot
183
+ family — sans on ``stark``, serif on the shipped ``default``/``cream``),
184
+ falling back to the body family when the title slot is unset.
185
+
186
+ Font weight comes from ``resolved_chart_style.title.font.weight`` for all
187
+ widths except tiny, which floors at 600 for legibility at small sizes.
188
+
189
+ Args:
190
+ width: Pixel width of the chart card.
191
+ level: Base heading level for this title (typically ``face.level + 1``
192
+ for chart titles).
193
+ resolved_chart_style: Active ``MergedChartsStyle`` for the face. Produced
194
+ upstream by ``resolve_style(face_style).charts`` or
195
+ ``build_resolved_style(...).charts`` so face-local and
196
+ chart-local style patches are already merged in.
197
+ use_title_family: ``True`` forces the title-slot family even on
198
+ narrow/tiny cards (so prose-tagged titles use serif at every
199
+ width on themes that define a serif title slot). ``False``
200
+ forces the body family at every width. ``None`` uses the
201
+ width default (title-slot family at medium/wide, body family
202
+ at narrow/tiny).
203
+
204
+ Returns:
205
+ ``(font_size_px, css_font_weight, css_font_family_string)``
206
+ """
207
+ sizes = resolved_chart_style.title.sizes
208
+ offset = _width_offset(width, resolved_chart_style)
209
+ effective_level = max(1, min(level + offset, len(sizes)))
210
+ font_size = int(sizes[effective_level - 1])
211
+
212
+ weight: int | str = (
213
+ _TINY_WEIGHT
214
+ if width < _TINY_MAX
215
+ else _coerce_weight(resolved_chart_style.title.font.weight or 500)
216
+ )
217
+
218
+ pick_title = (
219
+ (width >= _NARROW_MAX) if use_title_family is None else use_title_family
220
+ )
221
+ body = resolved_chart_style.font_family
222
+ assert body is not None, "resolved_chart_style.font_family must be populated"
223
+ title_family = resolved_chart_style.title.font.family
224
+ family = (title_family or body) if pick_title else body
225
+ return font_size, weight, family
226
+
227
+
228
+ def face_title_spec(
229
+ width: float,
230
+ *,
231
+ level: int,
232
+ ) -> tuple[int, int | str, str]:
233
+ """Return ``(font_size, font_weight, font_family)`` for a face/page title.
234
+
235
+ Same logic as ``chart_title_spec`` but:
236
+ - The width used for offset computation is clamped to ``_TINY_MAX`` — a page
237
+ header should not shrink below the narrow tier even on a narrow container.
238
+ - Always returns sans (Inter) regardless of width.
239
+
240
+ Note: callers that render prose faces override the returned family to serif
241
+ by passing ``font_family=config.style.title.font.family`` to
242
+ ``get_compact_style``. The function itself is family-agnostic; the prose
243
+ exception lives in the render layer (see ``render_title`` in
244
+ ``render/svg_utils.py``).
245
+
246
+ Args:
247
+ width: Pixel width of the face/board.
248
+ level: Heading level for this face title (``face.level``).
249
+
250
+ Returns:
251
+ ``(font_size_px, css_font_weight, css_font_family_string)``
252
+ """
253
+ from dataface.core.compile.config import get_config
254
+ from dataface.core.compile.models.style.merged import apply_emoji_to_family
255
+
256
+ cfg = get_config()
257
+ sizes = cfg.style.title.sizes
258
+ wo = cfg.style.title.width_offsets
259
+ # Clamp tiny → narrow: face headers don't shrink below the narrow tier.
260
+ effective_width = max(width, _TINY_MAX)
261
+ if effective_width >= _WIDE_MIN:
262
+ offset = wo.wide
263
+ elif effective_width >= _NARROW_MAX:
264
+ offset = wo.medium
265
+ else:
266
+ offset = wo.narrow
267
+ effective_level = max(1, min(level + offset, len(sizes)))
268
+ font_size = int(sizes[effective_level - 1])
269
+
270
+ weight = _coerce_weight(cfg.style.title.font.weight or 500)
271
+ _root = cfg.style.font.family
272
+ assert _root is not None, "style.font.family must be configured"
273
+ _family = apply_emoji_to_family(_root, cfg.style.font.emoji)
274
+ return font_size, weight, _family
275
+
276
+
277
+ def face_title_markdown(
278
+ title: str, width: float, *, level: int = 1
279
+ ) -> tuple[str, float, int | str]:
280
+ """Return ``(markdown, h1_size, font_weight)`` for a face title.
281
+
282
+ Formats the title as an h1 heading (``# title``). Callers pass ``h1_size``
283
+ to ``get_compact_style(h1_size=…)`` so the h1 renders at the exact pixel
284
+ size, and ``font_weight`` to ``heading_font_weight`` so the DFT weight tier
285
+ is applied. The pixel size comes from ``style.title.sizes`` indexed by the
286
+ effective level.
287
+
288
+ Args:
289
+ title: Title text (Jinja already resolved by the caller).
290
+ width: Pixel width of the face/board (drives width-offset selection).
291
+ level: Heading level for this face's title (``face.level``; default 1
292
+ for root faces).
293
+
294
+ Returns:
295
+ ``(markdown_string, h1_size, font_weight)`` — pass to
296
+ ``get_compact_style(h1_size=…, heading_font_weight=…)``.
297
+ """
298
+ font_size, weight, _family = face_title_spec(width, level=level)
299
+ return f"# {title}", float(font_size), weight
@@ -0,0 +1,301 @@
1
+ """Schema validation module.
2
+
3
+ Stage: COMPILE (Step 2 of 4)
4
+ Purpose: Validate AuthoredFace structure and cross-references.
5
+
6
+ Entry Points:
7
+ - validate_face(face: AuthoredFace) -> List[ValidationError]
8
+
9
+ Inputs:
10
+ - AuthoredFace (from parser)
11
+
12
+ Outputs:
13
+ - List[ValidationError] (empty if valid)
14
+
15
+ This validator focuses on semantic validation that Pydantic cannot handle:
16
+ - Cross-reference validation (chart → query)
17
+ - Layout item references (layout → chart)
18
+ - Consistency checks between related fields
19
+
20
+ Pydantic already handles:
21
+ - Type validation
22
+ - Required fields
23
+ - Field constraints
24
+ - Nested validation
25
+
26
+ Dependencies:
27
+ - .types (AuthoredFace, Chart)
28
+ - .errors (ValidationError)
29
+
30
+ See also:
31
+ - compile/parser.py: Previous step
32
+ - compile/normalizer.py: Next step
33
+ """
34
+
35
+ from collections.abc import Mapping
36
+ from typing import Any
37
+
38
+ from dataface.core.compile.errors import ValidationError
39
+ from dataface.core.compile.models.chart.authored import (
40
+ CalloutChart,
41
+ _SharedChartFields,
42
+ )
43
+ from dataface.core.compile.models.face.authored import AuthoredFace
44
+ from dataface.core.compile.models.refs import ChartRef
45
+ from dataface.core.compile.parser import looks_like_sql
46
+ from dataface.core.errors.codes_compile import DF_COMPILE_UNKNOWN_QUERY
47
+
48
+
49
+ def validate_face(face: AuthoredFace) -> list[ValidationError]:
50
+ """Validate a AuthoredFace structure and cross-references.
51
+
52
+ Stage: COMPILE (Step 2 of 4: Validation)
53
+
54
+ Performs semantic validation beyond what Pydantic provides:
55
+ - Chart → Query references exist
56
+ - Layout items reference existing charts
57
+
58
+ Note: Pydantic already validates types, required fields, and nested
59
+ structures during parsing. This function adds cross-reference validation.
60
+
61
+ Args:
62
+ face: Parsed AuthoredFace to validate
63
+
64
+ Returns:
65
+ List of ValidationError objects (empty if valid)
66
+
67
+ Example:
68
+ >>> face = parse_yaml(yaml_content)
69
+ >>> errors = validate_face(face)
70
+ >>> if errors:
71
+ ... for e in errors:
72
+ ... print(f"Error: {e}")
73
+ """
74
+ errors: list[ValidationError] = []
75
+
76
+ # Collect available names
77
+ query_names = set((face.queries or {}).keys())
78
+ chart_names = set((face.charts or {}).keys())
79
+
80
+ # Validate chart → query references
81
+ errors.extend(_validate_chart_query_references(face.charts or {}, query_names))
82
+
83
+ # Validate layout → chart references
84
+ errors.extend(_validate_layout_references(face, chart_names, query_names))
85
+
86
+ return errors
87
+
88
+
89
+ def _validate_chart_query_references(
90
+ charts: Mapping[str, _SharedChartFields | CalloutChart | ChartRef],
91
+ query_names: set[str],
92
+ ) -> list[ValidationError]:
93
+ """Validate that charts reference existing queries.
94
+
95
+ Args:
96
+ charts: Chart definitions (chart patch instance or ChartRef cross-file references)
97
+ query_names: Available query names
98
+
99
+ Returns:
100
+ List of validation errors
101
+ """
102
+ errors: list[ValidationError] = []
103
+
104
+ for chart_name, chart in charts.items():
105
+ # Skip cross-file chart references — resolved during normalization
106
+ if isinstance(chart, ChartRef):
107
+ continue
108
+
109
+ # CalloutChart is minimal — no query field by design.
110
+ if isinstance(chart, CalloutChart):
111
+ continue
112
+
113
+ query_ref = chart.query
114
+
115
+ # Skip validation for blank charts (no query)
116
+ if query_ref is None:
117
+ continue
118
+
119
+ # Skip validation for inline query definitions (dicts)
120
+ # These are handled during normalization
121
+ if isinstance(query_ref, dict):
122
+ continue
123
+
124
+ # Strip "queries." prefix if present
125
+ if query_ref.startswith("queries."):
126
+ query_ref = query_ref[8:]
127
+
128
+ # Cross-file references (e.g., "other_file.queries.name") are resolved later
129
+ if "." in query_ref:
130
+ continue
131
+
132
+ if query_ref not in query_names:
133
+ if looks_like_sql(query_ref):
134
+ # Bare SQL string — normalizer will promote it to an inline query; skip here.
135
+ continue
136
+
137
+ errors.append(
138
+ ValidationError.from_code(
139
+ DF_COMPILE_UNKNOWN_QUERY,
140
+ chart_name=chart_name,
141
+ query_name=query_ref,
142
+ )
143
+ )
144
+
145
+ return errors
146
+
147
+
148
+ def _validate_layout_references(
149
+ face: AuthoredFace,
150
+ chart_names: set[str],
151
+ query_names: set[str],
152
+ ) -> list[ValidationError]:
153
+ """Validate that layout items reference existing charts.
154
+
155
+ Args:
156
+ face: AuthoredFace to validate
157
+ chart_names: Available chart names
158
+ query_names: Available query names
159
+
160
+ Returns:
161
+ List of validation errors
162
+ """
163
+ errors: list[ValidationError] = []
164
+
165
+ # Collect all layout items
166
+ layout_items = _get_layout_items(face)
167
+
168
+ for location, item in layout_items:
169
+ if isinstance(item, str):
170
+ # String reference - must be a chart name (or special reference)
171
+ if _is_special_reference(item):
172
+ continue
173
+
174
+ if item not in chart_names:
175
+ errors.append(
176
+ ValidationError(
177
+ f"Layout references unknown chart '{item}'", location=location
178
+ )
179
+ )
180
+
181
+ elif isinstance(item, (dict, _SharedChartFields)):
182
+ # Inline chart - validate its query reference
183
+ query_ref = item.get("query") if isinstance(item, dict) else item.query
184
+
185
+ # Skip validation for blank charts (no query)
186
+ if query_ref is None:
187
+ continue
188
+
189
+ # Skip validation for inline query definitions (dicts)
190
+ # These are handled during normalization
191
+ if isinstance(query_ref, dict):
192
+ continue
193
+
194
+ if query_ref.startswith("queries."):
195
+ query_ref = query_ref[8:]
196
+
197
+ # Skip cross-file references
198
+ if "." not in query_ref and query_ref not in query_names:
199
+ if looks_like_sql(query_ref):
200
+ continue
201
+ errors.append(
202
+ ValidationError.from_code(
203
+ DF_COMPILE_UNKNOWN_QUERY,
204
+ chart_name=location,
205
+ query_name=query_ref,
206
+ )
207
+ )
208
+
209
+ return errors
210
+
211
+
212
+ def _get_layout_items(face: AuthoredFace) -> list[tuple[str, Any]]:
213
+ """Extract all layout items with their locations.
214
+
215
+ Args:
216
+ face: AuthoredFace to extract items from
217
+
218
+ Returns:
219
+ List of (location, item) tuples
220
+ """
221
+ items: list[tuple[str, Any]] = []
222
+
223
+ if face.rows:
224
+ for idx, item in enumerate(face.rows):
225
+ items.append((f"rows[{idx}]", item))
226
+ items.extend(_get_nested_items(item, f"rows[{idx}]"))
227
+
228
+ if face.cols:
229
+ for idx, item in enumerate(face.cols):
230
+ items.append((f"cols[{idx}]", item))
231
+ items.extend(_get_nested_items(item, f"cols[{idx}]"))
232
+
233
+ if face.grid:
234
+ for idx, grid_item in enumerate(face.grid.items):
235
+ items.append((f"grid.items[{idx}]", grid_item.item))
236
+
237
+ if face.tabs:
238
+ for idx, tab_item in enumerate(face.tabs.items):
239
+ if tab_item.rows:
240
+ for i, item in enumerate(tab_item.rows):
241
+ items.append((f"tabs.items[{idx}].rows[{i}]", item))
242
+ if tab_item.cols:
243
+ for i, item in enumerate(tab_item.cols):
244
+ items.append((f"tabs.items[{idx}].cols[{i}]", item))
245
+
246
+ return items
247
+
248
+
249
+ def _get_nested_items(item: Any, parent_location: str) -> list[tuple[str, Any]]:
250
+ """Extract items from nested face structures.
251
+
252
+ Args:
253
+ item: Item that might be a nested face
254
+ parent_location: Parent location string
255
+
256
+ Returns:
257
+ List of (location, item) tuples from nested structures
258
+ """
259
+ items: list[tuple[str, Any]] = []
260
+
261
+ if isinstance(item, dict):
262
+ # If nested face has its own charts, skip validation here -
263
+ # the nested face's layout items will reference its own charts,
264
+ # which will be validated during normalization
265
+ if "charts" in item:
266
+ return items
267
+
268
+ # Nested face without its own charts - extract its layout items
269
+ if "rows" in item:
270
+ for idx, nested in enumerate(item["rows"]):
271
+ items.append((f"{parent_location}.rows[{idx}]", nested))
272
+ items.extend(
273
+ _get_nested_items(nested, f"{parent_location}.rows[{idx}]")
274
+ )
275
+ if "cols" in item:
276
+ for idx, nested in enumerate(item["cols"]):
277
+ items.append((f"{parent_location}.cols[{idx}]", nested))
278
+ items.extend(
279
+ _get_nested_items(nested, f"{parent_location}.cols[{idx}]")
280
+ )
281
+
282
+ return items
283
+
284
+
285
+ def _is_special_reference(ref: str) -> bool:
286
+ """Check if a reference is a special type (partial, remote, etc.).
287
+
288
+ Special references are validated during normalization, not here.
289
+
290
+ Args:
291
+ ref: Reference string to check
292
+
293
+ Returns:
294
+ True if this is a special reference type
295
+ """
296
+ # Partial references start with underscore
297
+ if ref.startswith("_"):
298
+ return True
299
+
300
+ # Cross-file references contain dots or slashes
301
+ return bool("." in ref or "/" in ref)
@@ -0,0 +1,53 @@
1
+ """Variable utilities for Dataface.
2
+
3
+ Stage: COMPILE (utilities used by both execute and render)
4
+ Purpose: Shared variable processing functions.
5
+
6
+ This module provides utilities for working with variables that are needed
7
+ by both the execute and render stages.
8
+ """
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from dataface.core.compile.models.face.compiled import VariableValues
14
+
15
+
16
+ def parse_variable_json_strings(variables: VariableValues) -> VariableValues:
17
+ """Parse JSON strings in variables (e.g., from URL parameters).
18
+
19
+ When variables come from URL query parameters, complex values like
20
+ date ranges are often serialized as JSON strings. This function
21
+ parses them back to their native Python types.
22
+
23
+ This function is idempotent - calling it on already-parsed values
24
+ returns them unchanged.
25
+
26
+ Args:
27
+ variables: Variable values that may contain JSON strings
28
+
29
+ Returns:
30
+ Variables with JSON strings parsed to their actual types
31
+
32
+ Example:
33
+ >>> parse_variable_json_strings({"date_range": '["2024-01-01", "2024-01-31"]'})
34
+ {"date_range": ["2024-01-01", "2024-01-31"]}
35
+ """
36
+ parsed: dict[str, Any] = {}
37
+ for key, value in variables.items():
38
+ if isinstance(value, str):
39
+ # Try to parse as JSON (for date ranges, arrays, objects)
40
+ if value.startswith("[") or value.startswith("{"):
41
+ try:
42
+ parsed[key] = json.loads(value)
43
+ except (json.JSONDecodeError, ValueError):
44
+ # Not valid JSON, keep as string
45
+ parsed[key] = value
46
+ else:
47
+ parsed[key] = value
48
+ else:
49
+ parsed[key] = value
50
+ return parsed
51
+
52
+
53
+ __all__ = ["parse_variable_json_strings"]