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
+ """Terminal layout rendering functions.
2
+
3
+ Stage: RENDER
4
+ Purpose: Render different layout types (rows, cols, grid, tabs) to terminal output.
5
+
6
+ This module provides functions to render layouts in terminal-friendly format:
7
+ - Rows: Vertical stacking
8
+ - Cols: Horizontal distribution (if terminal width allows)
9
+ - Grid: Character-based grid layout
10
+ - Tabs: Text-based tabs
11
+ """
12
+
13
+ from dataface.core.compile.models.face.compiled import LayoutItem, VariableValues
14
+ from dataface.core.execute.executor import Executor
15
+ from dataface.core.render.terminal_text import (
16
+ pad_visible,
17
+ truncate_visible,
18
+ visible_len,
19
+ )
20
+
21
+
22
+ def _layout_item_is_table(item: LayoutItem) -> bool:
23
+ if item.type != "chart":
24
+ return False
25
+ assert item.chart is not None # normalizer: chart layout items always carry chart
26
+ return item.chart.type == "table"
27
+
28
+
29
+ def _cols_should_stack_vertically(items: list[LayoutItem], col_width: int) -> bool:
30
+ if col_width < 50:
31
+ return True
32
+ return any(_layout_item_is_table(item) for item in items)
33
+
34
+
35
+ def render_rows_layout_terminal(
36
+ items: list[LayoutItem],
37
+ executor: Executor,
38
+ variables: VariableValues,
39
+ available_width: int,
40
+ available_height: int,
41
+ gap: int = 1,
42
+ background: str | None = None,
43
+ ) -> str:
44
+ """Render items in vertical stack for terminal.
45
+
46
+ Args:
47
+ items: Layout items to render
48
+ executor: Executor for query execution
49
+ variables: Variable values for queries
50
+ available_width: Available terminal width in characters
51
+ available_height: Available terminal height in characters
52
+ gap: Gap between items (in lines)
53
+ background: Optional background color (not used in terminal)
54
+
55
+ Returns:
56
+ Terminal-formatted string for rows layout
57
+ """
58
+ from dataface.core.render.terminal import render_layout_item_terminal
59
+
60
+ if not items:
61
+ return ""
62
+
63
+ rendered_items: list[str] = []
64
+ for item in items:
65
+ item_output = render_layout_item_terminal(
66
+ item, executor, variables, available_width, available_height
67
+ )
68
+ if item_output:
69
+ rendered_items.append(item_output)
70
+ # Add gap between items
71
+ if gap > 0:
72
+ rendered_items.append("")
73
+
74
+ return "\n".join(rendered_items)
75
+
76
+
77
+ def render_cols_layout_terminal(
78
+ items: list[LayoutItem],
79
+ executor: Executor,
80
+ variables: VariableValues,
81
+ available_width: int,
82
+ available_height: int,
83
+ gap: int = 2,
84
+ background: str | None = None,
85
+ ) -> str:
86
+ """Render items side-by-side for terminal.
87
+
88
+ Note: Terminal columns are limited by width. If items don't fit,
89
+ we fall back to vertical stacking.
90
+
91
+ Args:
92
+ items: Layout items to render
93
+ executor: Executor for query execution
94
+ variables: Variable values for queries
95
+ available_width: Available terminal width in characters
96
+ available_height: Available terminal height in characters
97
+ gap: Gap between items (in characters)
98
+ background: Optional background color (not used in terminal)
99
+
100
+ Returns:
101
+ Terminal-formatted string for columns layout
102
+ """
103
+ from dataface.core.render.terminal import render_layout_item_terminal
104
+
105
+ if not items:
106
+ return ""
107
+
108
+ # Calculate column width
109
+ n = len(items)
110
+ total_gap = gap * (n - 1)
111
+ col_width = (available_width - total_gap) // n
112
+
113
+ if _cols_should_stack_vertically(items, col_width):
114
+ return render_rows_layout_terminal(
115
+ items,
116
+ executor,
117
+ variables,
118
+ available_width,
119
+ available_height,
120
+ gap,
121
+ )
122
+
123
+ # Render each item in its column
124
+ rendered_items: list[list[str]] = []
125
+ max_lines = 0
126
+
127
+ for item in items:
128
+ item_output = render_layout_item_terminal(
129
+ item, executor, variables, col_width, available_height
130
+ )
131
+ lines = item_output.split("\n")
132
+ rendered_items.append(lines)
133
+ max_lines = max(max_lines, len(lines))
134
+
135
+ # Combine columns horizontally
136
+ output_lines: list[str] = []
137
+ for line_idx in range(max_lines):
138
+ line_parts = []
139
+ for item_lines in rendered_items:
140
+ if line_idx < len(item_lines):
141
+ line = item_lines[line_idx]
142
+ if visible_len(line) > col_width:
143
+ line = truncate_visible(line, col_width)
144
+ line_parts.append(pad_visible(line, col_width))
145
+ else:
146
+ line_parts.append(" " * col_width)
147
+
148
+ output_lines.append((" " * gap).join(line_parts))
149
+
150
+ return "\n".join(output_lines)
151
+
152
+
153
+ def render_grid_layout_terminal(
154
+ items: list[LayoutItem],
155
+ executor: Executor,
156
+ variables: VariableValues,
157
+ available_width: int,
158
+ available_height: int,
159
+ columns: int = 2,
160
+ gap: int = 1,
161
+ background: str | None = None,
162
+ ) -> str:
163
+ """Render items in a grid layout for terminal.
164
+
165
+ Args:
166
+ items: Layout items to render
167
+ executor: Executor for query execution
168
+ variables: Variable values for queries
169
+ available_width: Available terminal width in characters
170
+ available_height: Available terminal height in characters
171
+ columns: Number of columns in grid
172
+ gap: Gap between items (in characters/lines)
173
+ background: Optional background color (not used in terminal)
174
+
175
+ Returns:
176
+ Terminal-formatted string for grid layout
177
+ """
178
+ from dataface.core.render.terminal import render_layout_item_terminal
179
+
180
+ if not items:
181
+ return ""
182
+
183
+ # Calculate cell dimensions
184
+ total_gap = gap * (columns - 1)
185
+ cell_width = (available_width - total_gap) // columns
186
+
187
+ # Group items into rows
188
+ rows: list[list[LayoutItem]] = []
189
+ current_row: list[LayoutItem] = []
190
+ for item in items:
191
+ current_row.append(item)
192
+ if len(current_row) >= columns:
193
+ rows.append(current_row)
194
+ current_row = []
195
+ if current_row:
196
+ rows.append(current_row)
197
+
198
+ # Render each row
199
+ rendered_rows: list[str] = []
200
+ for row_items in rows:
201
+ # Render items in this row
202
+ row_rendered: list[list[str]] = []
203
+ max_lines = 0
204
+
205
+ for item in row_items:
206
+ item_output = render_layout_item_terminal(
207
+ item, executor, variables, cell_width, available_height
208
+ )
209
+ lines = item_output.split("\n")
210
+ row_rendered.append(lines)
211
+ max_lines = max(max_lines, len(lines))
212
+
213
+ # Combine row items horizontally
214
+ row_lines: list[str] = []
215
+ for line_idx in range(max_lines):
216
+ line_parts = []
217
+ for item_lines in row_rendered:
218
+ if line_idx < len(item_lines):
219
+ line = item_lines[line_idx]
220
+ if visible_len(line) > cell_width:
221
+ line = truncate_visible(line, cell_width)
222
+ line_parts.append(pad_visible(line, cell_width))
223
+ else:
224
+ line_parts.append(" " * cell_width)
225
+
226
+ row_lines.append((" " * gap).join(line_parts))
227
+
228
+ rendered_rows.append("\n".join(row_lines))
229
+ if gap > 0:
230
+ rendered_rows.append("") # Gap between rows
231
+
232
+ return "\n".join(rendered_rows)
233
+
234
+
235
+ def render_tabs_layout_terminal(
236
+ items: list[LayoutItem],
237
+ executor: Executor,
238
+ variables: VariableValues,
239
+ available_width: int,
240
+ available_height: int,
241
+ tab_titles: list[str] | None = None,
242
+ default_tab: int = 0,
243
+ tab_position: str = "top",
244
+ background: str | None = None,
245
+ ) -> str:
246
+ """Render tabs layout for terminal.
247
+
248
+ Note: For terminal, we render all tabs sequentially with headers,
249
+ since terminal doesn't support interactive tabs.
250
+
251
+ Args:
252
+ items: Layout items (one per tab)
253
+ executor: Executor for query execution
254
+ variables: Variable values for queries
255
+ available_width: Available terminal width in characters
256
+ available_height: Available terminal height in characters
257
+ tab_titles: Optional list of tab titles
258
+ default_tab: Default active tab (not used in terminal)
259
+ tab_position: Tab position ("top" or "bottom", not used in terminal)
260
+ background: Optional background color (not used in terminal)
261
+
262
+ Returns:
263
+ Terminal-formatted string for tabs layout
264
+ """
265
+ from dataface.core.render.terminal import render_layout_item_terminal
266
+
267
+ if not items:
268
+ return ""
269
+
270
+ output_lines: list[str] = []
271
+
272
+ for idx, item in enumerate(items):
273
+ # Add tab header
274
+ tab_title = (
275
+ tab_titles[idx]
276
+ if tab_titles and idx < len(tab_titles)
277
+ else f"Tab {idx + 1}"
278
+ )
279
+
280
+ # Create header line
281
+ header_line = f"┌─ {tab_title} {'─' * (available_width - len(tab_title) - 5)}┐"
282
+ output_lines.append(header_line)
283
+
284
+ # Render tab content
285
+ item_output = render_layout_item_terminal(
286
+ item, executor, variables, available_width - 4, available_height
287
+ )
288
+
289
+ # Indent content
290
+ content_lines = item_output.split("\n")
291
+ for line in content_lines:
292
+ output_lines.append(f"│ {line.ljust(available_width - 4)} │")
293
+
294
+ # Add footer
295
+ footer_line = f"└{'─' * (available_width - 2)}┘"
296
+ output_lines.append(footer_line)
297
+ output_lines.append("") # Gap between tabs
298
+
299
+ return "\n".join(output_lines)
@@ -0,0 +1,31 @@
1
+ """ANSI-aware string width helpers for terminal layout joining."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
8
+
9
+
10
+ def strip_ansi(text: str) -> str:
11
+ return _ANSI_ESCAPE_RE.sub("", text)
12
+
13
+
14
+ def visible_len(text: str) -> int:
15
+ return len(strip_ansi(text))
16
+
17
+
18
+ def pad_visible(text: str, width: int) -> str:
19
+ padding = width - visible_len(text)
20
+ if padding <= 0:
21
+ return text
22
+ return text + (" " * padding)
23
+
24
+
25
+ def truncate_visible(text: str, max_width: int) -> str:
26
+ if visible_len(text) <= max_width:
27
+ return text
28
+ plain = strip_ansi(text)
29
+ if max_width <= 3:
30
+ return plain[:max_width]
31
+ return plain[: max_width - 3] + "..."
@@ -0,0 +1 @@
1
+ """Text transform utilities for DFT render layer."""
@@ -0,0 +1,113 @@
1
+ """Letter-case transform dispatcher for DFT typography.
2
+
3
+ Applies at render time — after Jinja resolution but before the string reaches
4
+ an SVG <text> node or a Vega-Lite spec string field.
5
+
6
+ Supported values (matching FontStyle.case):
7
+ none — no-op; string emitted as authored / as normalized by slug_to_text.
8
+ upper — str.upper() (Unicode-aware).
9
+ lower — str.lower() (Unicode-aware).
10
+ sentence — first character uppercased, remainder unchanged.
11
+ Preserves acronyms and proper nouns that the author
12
+ already capitalized. Does NOT lowercase the rest of the
13
+ string — that would corrupt ARR, iOS, etc.
14
+ title — Chicago/Gruber algorithm via the `titlecase` library.
15
+ Lowercases stopwords (a, an, and, as, at, but, by, en,
16
+ for, if, in, of, on, or, the, to, v[.], vs[.], via).
17
+ Always capitalizes first and last words.
18
+ Preserves any token with internal capitals (ARR, iPhone,
19
+ MRR, SQL) — the key differentiator from naive
20
+ "capitalize every word" algorithms.
21
+ slug — Machine identifier form: spaces/hyphens → underscore, lower.
22
+ "Order Status" → "order_status".
23
+ camel — camelCase: first word lower, subsequent words capitalized.
24
+ "order status" → "orderStatus".
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import TYPE_CHECKING, Literal
30
+
31
+ from titlecase import titlecase as _titlecase
32
+
33
+ from dataface.core.utils import slug_to_text
34
+
35
+ if TYPE_CHECKING:
36
+ from dataface.core.compile.models.primitives import MergedFontStyle
37
+
38
+ CaseValue = Literal["none", "sentence", "title", "upper", "lower", "slug", "camel"]
39
+
40
+
41
+ def apply_case(text: str, case: CaseValue) -> str:
42
+ """Apply a letter-case transform to *text* and return the result.
43
+
44
+ Args:
45
+ text: The string to transform. May be empty.
46
+ case: One of 'none', 'upper', 'lower', 'sentence', 'title', 'slug', 'camel'.
47
+
48
+ Returns:
49
+ The transformed string.
50
+
51
+ Raises:
52
+ ValueError: If *case* is not a recognised value.
53
+ """
54
+ if case == "none":
55
+ return text
56
+ if case == "upper":
57
+ return text.upper()
58
+ if case == "lower":
59
+ return text.lower()
60
+ if case == "sentence":
61
+ # Cautious sentence case: uppercase only the first character, leave
62
+ # the rest unchanged. Preserves acronyms/proper nouns.
63
+ return text[:1].upper() + text[1:] if text else text
64
+ if case == "title":
65
+ return _titlecase(text)
66
+ if case == "slug":
67
+ # Machine form: spaces/hyphens → underscore, all lowercase.
68
+ return text.replace(" ", "_").replace("-", "_").lower()
69
+ if case == "camel":
70
+ # camelCase: lower the first word, capitalize subsequent words.
71
+ words = text.replace("-", " ").replace("_", " ").split()
72
+ if not words:
73
+ return text
74
+ return words[0].lower() + "".join(w.capitalize() for w in words[1:])
75
+ raise ValueError(
76
+ f"Unknown case: {case!r}. Expected one of 'none', 'upper', "
77
+ "'lower', 'sentence', 'title', 'slug', 'camel'."
78
+ )
79
+
80
+
81
+ def apply_font_case(text: str, font: MergedFontStyle) -> str:
82
+ """Apply the letter-case transform specified by font.case.
83
+
84
+ Args:
85
+ text: The string to transform.
86
+ font: A fully-resolved font; font.case drives the transform.
87
+
88
+ Returns:
89
+ The transformed string.
90
+ """
91
+ return apply_case(text, font.case)
92
+
93
+
94
+ def format_display_text(
95
+ text: str,
96
+ *,
97
+ from_slug: bool,
98
+ font: MergedFontStyle,
99
+ ) -> str:
100
+ """Two-step render pipeline: optional slug tokenization then font case.
101
+
102
+ Args:
103
+ text: The raw input string — either an authored string or a slug.
104
+ from_slug: True when *text* is a field slug / identifier that needs
105
+ ``slug_to_text`` tokenization first (underscore→space, unit/abbreviation
106
+ expansion). False when *text* is already human-readable authored text.
107
+ font: The resolved font for this slot; font.case drives the case transform.
108
+
109
+ Returns:
110
+ The display-ready string.
111
+ """
112
+ normalized = slug_to_text(text) if from_slug else text
113
+ return apply_font_case(normalized, font)
@@ -0,0 +1,129 @@
1
+ """Text render output format for AI agents.
2
+
3
+ Walks the layout tree via face_to_dict (shared with JSON format),
4
+ then templates into compact markdown text.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from dataface.core.compile.models.face.compiled import Face, VariableValues
10
+ from dataface.core.errors import StructuredError
11
+ from dataface.core.execute.executor import Executor
12
+ from dataface.core.render.json_format import face_to_dict
13
+
14
+
15
+ def render_face_text(
16
+ face: Face,
17
+ executor: Executor,
18
+ variables: VariableValues,
19
+ error_collector: list[StructuredError] | None = None,
20
+ ) -> str:
21
+ """Render a compiled face to compact markdown text for AI agents."""
22
+ d = face_to_dict(face, executor, variables, error_collector)
23
+ lines: list[str] = []
24
+ _render_face(d, lines, depth=1)
25
+ return "\n".join(lines)
26
+
27
+
28
+ def _render_face(face: dict[str, Any], lines: list[str], depth: int) -> None:
29
+ """Render a face dict to markdown lines at the given heading depth."""
30
+ title = face.get("title") or face.get("id", "Untitled")
31
+ lines.append(f"{'#' * depth} {title}")
32
+ for item in face.get("items", []):
33
+ if item["type"] == "chart":
34
+ _render_chart(item, lines, depth + 1)
35
+ elif item["type"] == "face":
36
+ lines.append("")
37
+ _render_face(item["face"], lines, depth + 1)
38
+
39
+
40
+ def _render_chart(item: dict[str, Any], lines: list[str], depth: int) -> None:
41
+ """Render a chart item to markdown lines."""
42
+ # Chart error: emit a compact error line instead of data
43
+ if "_error" in item:
44
+ err = item["_error"]
45
+ code = err.get("code", "")
46
+ msg = err.get("message", "error")
47
+ chart_id = err.get("fields", {}).get("chart_id", "unknown")
48
+ lines.append(f"\n[chart error: {code} {msg} (chart: {chart_id})]")
49
+ return
50
+
51
+ chart = item["chart"]
52
+ data = item.get("data", [])
53
+
54
+ chart_type = chart.get("chart_type", "unknown")
55
+ title = chart.get("title") or chart.get("id", "chart")
56
+ lines.append("")
57
+ lines.append(f"{'#' * depth} {title} ({chart_type})")
58
+
59
+ # KPI: show value, optionally resolved against the bound row when the
60
+ # authored value is a column reference.
61
+ if chart_type == "kpi":
62
+ raw = chart.get("value")
63
+ cell = (
64
+ data[0].get(raw)
65
+ if isinstance(raw, str) and data and raw in data[0]
66
+ else raw
67
+ )
68
+ if isinstance(cell, (int, float)):
69
+ display = f"{cell:,}" if cell == int(cell) else f"{cell:,.2f}"
70
+ elif cell is not None:
71
+ display = str(cell)
72
+ else:
73
+ display = ""
74
+ if display:
75
+ lines.append(f"- value: {display}")
76
+ return
77
+
78
+ # Field mappings
79
+ fields = _field_mappings(chart)
80
+ if fields:
81
+ lines.append(f"- {', '.join(fields)}")
82
+
83
+ # Data summary
84
+ if data:
85
+ summary = _data_summary(chart, data)
86
+ if summary:
87
+ lines.append(f"- {summary}")
88
+
89
+
90
+ def _field_mappings(chart: dict[str, Any]) -> list[str]:
91
+ """Extract field mapping strings like 'x: month, y: revenue'."""
92
+ mappings = []
93
+ for key in ("x", "y", "color", "size", "theta", "value"):
94
+ val = chart.get(key)
95
+ if val:
96
+ mappings.append(f"{key}: {val}")
97
+ return mappings
98
+
99
+
100
+ def _data_summary(chart: dict[str, Any], data: list[dict]) -> str:
101
+ """Build a compact data summary string."""
102
+ parts = [f"{len(data)} rows"]
103
+
104
+ # Collect chart field names by role
105
+ field_roles: dict[str, str] = {}
106
+ for key in ("x", "y", "color", "size", "theta"):
107
+ val = chart.get(key)
108
+ if val:
109
+ field_roles[val] = key
110
+
111
+ for col, role in field_roles.items():
112
+ values: list[Any] = [row[col] for row in data if row.get(col) is not None]
113
+ if not values:
114
+ continue
115
+ if all(isinstance(v, (int, float)) for v in values) and role in (
116
+ "y",
117
+ "size",
118
+ "theta",
119
+ ):
120
+ lo, hi = min(values), max(values)
121
+ parts.append(f"{col}: {lo}–{hi}")
122
+ elif all(isinstance(v, str) for v in values) and role in ("x", "color"):
123
+ distinct = sorted(set(values))
124
+ if len(distinct) <= 5:
125
+ parts.append(f"{col}: {', '.join(distinct)}")
126
+ else:
127
+ parts.append(f"{col}: {len(distinct)} distinct")
128
+
129
+ return " | ".join(parts)
@@ -0,0 +1,106 @@
1
+ """Shared utility functions for the render module.
2
+
3
+ This module contains common utilities used across multiple render modules
4
+ to avoid code duplication.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from decimal import Decimal
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from dataface.core.compile.models.chart.compiled import (
15
+ Chart,
16
+ )
17
+
18
+ SQL_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
19
+
20
+
21
+ def is_valid_sql_identifier(identifier: str) -> bool:
22
+ """Validate that a string is a safe SQL identifier.
23
+
24
+ Prevents SQL injection by ensuring identifiers only contain
25
+ alphanumeric characters and underscores, starting with a letter
26
+ or underscore.
27
+ """
28
+ if not identifier:
29
+ return False
30
+ return SQL_IDENTIFIER_PATTERN.match(identifier) is not None
31
+
32
+
33
+ from dataface.core.utils import slug_to_text # noqa: F401 — re-exported
34
+
35
+
36
+ def resolve_field_names(
37
+ chart: Chart,
38
+ data: list[dict[str, Any]],
39
+ ) -> Chart:
40
+ """Resolve chart field references against actual data column names.
41
+
42
+ Handles case mismatches (e.g. chart says ``x: stage`` but data has
43
+ ``Stage``) by remapping the chart's field attributes to match the
44
+ real column keys. Returns a copy of the chart with updated fields
45
+ if any mismatches are found; returns the original chart unchanged
46
+ when all fields already match.
47
+ """
48
+ if not data:
49
+ return chart
50
+
51
+ actual_cols = set(data[0].keys())
52
+ lower_to_actual: dict[str, str] = {k.lower(): k for k in actual_cols}
53
+
54
+ field_attrs = [
55
+ "x",
56
+ "y",
57
+ "color",
58
+ "size",
59
+ "shape",
60
+ "theta",
61
+ "lookup",
62
+ "value", # always a column reference; case-insensitive remapping applies
63
+ ]
64
+ updates: dict[str, Any] = {}
65
+
66
+ for attr in field_attrs:
67
+ val = getattr(chart, attr, None)
68
+ if val is None:
69
+ continue
70
+ if isinstance(val, list):
71
+ resolved_fields = [
72
+ lower_to_actual.get(f.lower(), f) if isinstance(f, str) else f
73
+ for f in val
74
+ ]
75
+ if resolved_fields != val:
76
+ updates[attr] = resolved_fields
77
+ elif isinstance(val, str) and val not in actual_cols:
78
+ resolved_field = lower_to_actual.get(val.lower())
79
+ if resolved_field:
80
+ updates[attr] = resolved_field
81
+
82
+ if updates:
83
+ return chart.model_copy(update=updates)
84
+ return chart
85
+
86
+
87
+ def normalize_data_types(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
88
+ """Normalize data types for JSON serialization and rendering.
89
+
90
+ Converts Decimal to float, preserves lists/tuples, and stringifies
91
+ other non-JSON-serializable types.
92
+ """
93
+ normalized: list[dict[str, Any]] = []
94
+ for row in data:
95
+ normalized_row: dict[str, Any] = {}
96
+ for key, value in row.items():
97
+ if isinstance(value, Decimal):
98
+ normalized_row[key] = float(value)
99
+ elif isinstance(value, (int, float, str, bool, type(None))):
100
+ normalized_row[key] = value
101
+ elif isinstance(value, (list, tuple)):
102
+ normalized_row[key] = list(value)
103
+ else:
104
+ normalized_row[key] = str(value)
105
+ normalized.append(normalized_row)
106
+ return normalized