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,744 @@
1
+ """Batch query execution with temp table optimization.
2
+
3
+ Stage: EXECUTE
4
+ Purpose: Generate batch SQL with temp tables for queries that are referenced
5
+ by multiple other queries via {{ queries.X }} syntax.
6
+
7
+ Entry Points:
8
+ - extract_query_refs(sql: str) -> Set[str]
9
+ - build_dependency_graph(queries: Dict[str, Query]) -> DependencyGraph
10
+ - generate_batch_sql(queries, graph, dialect) -> List[BatchStatement]
11
+ - collect_face_queries(face) -> Dict[str, AnyQuery]
12
+ - group_by_source(queries) -> Dict[str, Dict[str, AnyQuery]]
13
+
14
+ When multiple queries reference the same base query:
15
+ - The base query is executed ONCE and stored in a temp table
16
+ - Dependent queries SELECT from the temp table
17
+
18
+ Example:
19
+ queries:
20
+ orders:
21
+ sql: SELECT * FROM orders WHERE status = 'completed'
22
+ high_value:
23
+ sql: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
24
+ recent:
25
+ sql: SELECT * FROM {{ queries.orders }} WHERE date >= CURRENT_DATE - 7
26
+
27
+ Generated batch:
28
+ 1. CREATE TEMP TABLE _df_temp_orders AS SELECT * FROM orders WHERE status = 'completed'
29
+ 2. SELECT * FROM _df_temp_orders WHERE amount > 1000 (for high_value)
30
+ 3. SELECT * FROM _df_temp_orders WHERE date >= CURRENT_DATE - 7 (for recent)
31
+
32
+ Dependencies:
33
+ - dataface.execute.dialects (for temp table syntax)
34
+ - dataface.core.compile.models.query.compiled (AnyQuery, is_sql_query)
35
+
36
+ Errors:
37
+ - CrossSourceReferenceError: When {{ queries.X }} references a query
38
+ on a different database source
39
+ """
40
+
41
+ import re
42
+ from dataclasses import dataclass, field
43
+ from typing import Any
44
+
45
+ from dataface.core.compile.models.face.compiled import Face
46
+ from dataface.core.compile.models.query.compiled import (
47
+ AnyQuery,
48
+ is_sql_query,
49
+ )
50
+ from dataface.core.execute.dialects import SQLDialect, get_dialect
51
+
52
+
53
+ class CrossSourceReferenceError(Exception):
54
+ """Error raised when a query references another query on a different source.
55
+
56
+ Cross-database references are not supported because temp tables are
57
+ connection-local and cannot be shared between different database connections.
58
+
59
+ Example:
60
+ queries:
61
+ orders:
62
+ source: postgres
63
+ analysis:
64
+ sql: SELECT * FROM {{ queries.orders }}
65
+ source: mysql # ERROR: Can't reference postgres query from mysql
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ source_query: str,
71
+ query_source: str,
72
+ target_query: str,
73
+ target_source: str,
74
+ ):
75
+ """Initialize CrossSourceReferenceError.
76
+
77
+ Args:
78
+ source_query: Name of the query making the reference
79
+ query_source: Source of the query making the reference
80
+ target_query: Name of the referenced query
81
+ target_source: Source of the referenced query
82
+ """
83
+ self.source_query = source_query
84
+ self.query_source = query_source
85
+ self.target_query = target_query
86
+ self.target_source = target_source
87
+ super().__init__(
88
+ f"Query '{source_query}' (source: {query_source}) cannot reference "
89
+ f"query '{target_query}' (source: {target_source}). "
90
+ f"Cross-database references are not supported."
91
+ )
92
+
93
+
94
+ @dataclass
95
+ class DependencyGraph:
96
+ """Directed graph representing query dependencies.
97
+
98
+ Nodes are query names. Edges point from a query to queries that depend on it.
99
+ For example, if query B references {{ queries.A }}, there's an edge A -> B.
100
+
101
+ Attributes:
102
+ edges: Dict mapping query name to set of queries that depend on it
103
+ reverse_edges: Dict mapping query name to set of queries it depends on
104
+ all_nodes: Set of all query names in the graph
105
+ """
106
+
107
+ edges: dict[str, set[str]] = field(default_factory=dict)
108
+ reverse_edges: dict[str, set[str]] = field(default_factory=dict)
109
+ all_nodes: set[str] = field(default_factory=set)
110
+
111
+ def add_node(self, name: str) -> None:
112
+ """Add a node to the graph.
113
+
114
+ Args:
115
+ name: Query name to add
116
+ """
117
+ self.all_nodes.add(name)
118
+ if name not in self.edges:
119
+ self.edges[name] = set()
120
+ if name not in self.reverse_edges:
121
+ self.reverse_edges[name] = set()
122
+
123
+ def add_edge(self, from_node: str, to_node: str) -> None:
124
+ """Add a directed edge from from_node to to_node.
125
+
126
+ Indicates that to_node depends on from_node.
127
+
128
+ Args:
129
+ from_node: The referenced query (dependency)
130
+ to_node: The query that references from_node (dependent)
131
+ """
132
+ self.add_node(from_node)
133
+ self.add_node(to_node)
134
+ self.edges[from_node].add(to_node)
135
+ self.reverse_edges[to_node].add(from_node)
136
+
137
+ def get_dependents(self, node: str) -> set[str]:
138
+ """Get all queries that depend on this node.
139
+
140
+ Args:
141
+ node: Query name
142
+
143
+ Returns:
144
+ Set of query names that reference this query
145
+ """
146
+ return self.edges.get(node, set())
147
+
148
+ def get_dependencies(self, node: str) -> set[str]:
149
+ """Get all queries this node depends on.
150
+
151
+ Args:
152
+ node: Query name
153
+
154
+ Returns:
155
+ Set of query names this query references
156
+ """
157
+ return self.reverse_edges.get(node, set())
158
+
159
+ def out_degree(self, node: str) -> int:
160
+ """Get the number of queries that depend on this node.
161
+
162
+ Args:
163
+ node: Query name
164
+
165
+ Returns:
166
+ Number of dependent queries (out-edges)
167
+ """
168
+ return len(self.get_dependents(node))
169
+
170
+ def get_temp_table_candidates(self) -> set[str]:
171
+ """Get queries that should be materialized as temp tables.
172
+
173
+ A query is a temp table candidate if it is referenced by at least
174
+ one other query (out_degree > 0).
175
+
176
+ Returns:
177
+ Set of query names that should become temp tables
178
+ """
179
+ return {node for node in self.all_nodes if self.out_degree(node) > 0}
180
+
181
+
182
+ @dataclass
183
+ class BatchStatement:
184
+ """A single statement in a batch execution plan.
185
+
186
+ Attributes:
187
+ name: Identifier for this statement (query name or _create_X for temp tables)
188
+ sql: The SQL to execute
189
+ is_temp_table_create: True if this creates a temp table
190
+ query_name: The logical query name this statement is for (may differ from name)
191
+ """
192
+
193
+ name: str
194
+ sql: str
195
+ is_temp_table_create: bool = False
196
+ query_name: str | None = None
197
+
198
+ def __post_init__(self) -> None:
199
+ """Set query_name to name if not provided."""
200
+ if self.query_name is None:
201
+ self.query_name = self.name
202
+
203
+
204
+ # Pattern to match {{ queries.X }} references
205
+ # Handles variations like {{queries.X}}, {{ queries.X }}, {{ queries.X }}
206
+ _QUERY_REF_PATTERN = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
207
+
208
+ # Prefix for temp table names created by batch execution
209
+ # This prefix helps identify dataface-created temp tables and avoids conflicts
210
+ TEMP_TABLE_PREFIX = "_df_temp_"
211
+
212
+
213
+ def extract_query_refs(sql: str) -> set[str]:
214
+ """Extract query references from SQL.
215
+
216
+ Finds all {{ queries.X }} patterns in the SQL string and returns
217
+ the set of referenced query names.
218
+
219
+ Args:
220
+ sql: SQL string potentially containing {{ queries.X }} references
221
+
222
+ Returns:
223
+ Set of query names referenced in the SQL
224
+
225
+ Example:
226
+ >>> extract_query_refs("SELECT * FROM {{ queries.orders }} WHERE amount > 100")
227
+ {'orders'}
228
+ >>> extract_query_refs("SELECT a.*, b.* FROM {{ queries.a }} a JOIN {{ queries.b }} b")
229
+ {'a', 'b'}
230
+ """
231
+ if not sql or "queries." not in sql:
232
+ return set()
233
+
234
+ return set(_QUERY_REF_PATTERN.findall(sql))
235
+
236
+
237
+ def build_dependency_graph(
238
+ queries: dict[str, AnyQuery],
239
+ ) -> DependencyGraph:
240
+ """Build a dependency graph from a set of queries.
241
+
242
+ Scans each query's SQL for {{ queries.X }} references and builds
243
+ a directed graph where edges point from referenced queries to
244
+ the queries that reference them.
245
+
246
+ Args:
247
+ queries: Dict mapping query names to AnyQuery objects
248
+
249
+ Returns:
250
+ DependencyGraph with nodes and edges
251
+
252
+ Example:
253
+ Given queries:
254
+ orders: SELECT * FROM orders
255
+ high_value: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
256
+
257
+ Returns graph with:
258
+ nodes: {orders, high_value}
259
+ edges: {orders: {high_value}} (orders -> high_value)
260
+ """
261
+ graph = DependencyGraph()
262
+
263
+ for name, query in queries.items():
264
+ graph.add_node(name)
265
+
266
+ # Only SQL queries can have query references
267
+ if not is_sql_query(query):
268
+ continue
269
+
270
+ sql = query.sql
271
+ refs = extract_query_refs(sql)
272
+
273
+ for ref in refs:
274
+ if ref in queries: # Only add edges for known queries
275
+ graph.add_edge(ref, name)
276
+
277
+ return graph
278
+
279
+
280
+ def _find_cycle(graph: DependencyGraph) -> list[str]:
281
+ """Find a cycle in the dependency graph using DFS.
282
+
283
+ Args:
284
+ graph: The dependency graph to search
285
+
286
+ Returns:
287
+ List of query names forming the cycle, or empty list if no cycle
288
+ """
289
+ visited: set[str] = set()
290
+ rec_stack: set[str] = set()
291
+ path: list[str] = []
292
+
293
+ def dfs(node: str) -> list[str] | None:
294
+ visited.add(node)
295
+ rec_stack.add(node)
296
+ path.append(node)
297
+
298
+ for dep in graph.get_dependencies(node):
299
+ if dep not in visited:
300
+ cycle = dfs(dep)
301
+ if cycle:
302
+ return cycle
303
+ elif dep in rec_stack:
304
+ # Found cycle - extract the cycle path
305
+ cycle_start = path.index(dep)
306
+ return path[cycle_start:] + [dep]
307
+
308
+ path.pop()
309
+ rec_stack.remove(node)
310
+ return None
311
+
312
+ for node in graph.all_nodes:
313
+ if node not in visited:
314
+ cycle = dfs(node)
315
+ if cycle:
316
+ return cycle
317
+
318
+ return []
319
+
320
+
321
+ def topological_sort(graph: DependencyGraph) -> list[str]:
322
+ """Topologically sort nodes in the dependency graph.
323
+
324
+ Returns nodes in an order where dependencies come before dependents.
325
+ Uses Kahn's algorithm for topological sorting.
326
+
327
+ Args:
328
+ graph: The dependency graph to sort
329
+
330
+ Returns:
331
+ List of query names in topological order (dependencies first)
332
+
333
+ Raises:
334
+ ValueError: If the graph contains a cycle (includes cycle path in message)
335
+
336
+ Example:
337
+ If orders -> high_value (high_value depends on orders),
338
+ returns ['orders', 'high_value']
339
+ """
340
+ # Compute in-degree (number of dependencies) for each node
341
+ in_degree: dict[str, int] = dict.fromkeys(graph.all_nodes, 0)
342
+ for node in graph.all_nodes:
343
+ for dependent in graph.get_dependents(node):
344
+ in_degree[dependent] = in_degree.get(dependent, 0) + 1
345
+
346
+ # Start with nodes that have no dependencies
347
+ queue: list[str] = [node for node in graph.all_nodes if in_degree[node] == 0]
348
+ result: list[str] = []
349
+
350
+ while queue:
351
+ # Sort for deterministic ordering
352
+ queue.sort()
353
+ node = queue.pop(0)
354
+ result.append(node)
355
+
356
+ # Reduce in-degree for dependents
357
+ for dependent in sorted(graph.get_dependents(node)):
358
+ in_degree[dependent] -= 1
359
+ if in_degree[dependent] == 0:
360
+ queue.append(dependent)
361
+
362
+ if len(result) != len(graph.all_nodes):
363
+ # Find and report the actual cycle for better debugging
364
+ cycle = _find_cycle(graph)
365
+ if cycle:
366
+ cycle_str = " -> ".join(cycle)
367
+ raise ValueError(
368
+ f"Circular dependency detected in query references: {cycle_str}"
369
+ )
370
+ raise ValueError("Circular dependency detected in query references")
371
+
372
+ return result
373
+
374
+
375
+ def collect_layout_chart_query_names(face: Face) -> set[str]:
376
+ """Walk the layout tree and return query names directly used by charts.
377
+
378
+ Shared layout-walk utility used by both ``collect_face_queries``
379
+ (which expands transitive deps) and the parallel pre-execution path
380
+ in ``renderer.py`` (which does not, since ``{{ queries.X }}`` is
381
+ Jinja-inlined).
382
+ """
383
+ names: set[str] = set()
384
+
385
+ def _walk(items: list[Any]) -> None:
386
+ for item in items:
387
+ if item.chart and item.chart.query_name:
388
+ names.add(item.chart.query_name)
389
+ if item.face and item.face.layout:
390
+ _walk(item.face.layout.items)
391
+
392
+ _walk(face.layout.items)
393
+
394
+ return names
395
+
396
+
397
+ def collect_face_queries(
398
+ face: Face,
399
+ chart_queries_only: bool = True,
400
+ ) -> dict[str, AnyQuery]:
401
+ """Collect all queries needed for a dataface.
402
+
403
+ Collects queries directly used by charts, plus any queries they
404
+ reference via {{ queries.X }}.
405
+
406
+ Args:
407
+ face: Face to collect queries from
408
+ chart_queries_only: If True, only include queries used by charts.
409
+ If False, include all queries defined in the face.
410
+
411
+ Returns:
412
+ Dict mapping query names to AnyQuery objects
413
+
414
+ Example:
415
+ Given face with:
416
+ queries: {orders, high_value, recent, unused}
417
+ charts: [chart using high_value, chart using recent]
418
+
419
+ Returns {orders, high_value, recent} (orders is a dependency)
420
+ """
421
+ queries: dict[str, AnyQuery] = {}
422
+
423
+ if not chart_queries_only:
424
+ # Include all queries from the face
425
+ return dict(face.queries)
426
+
427
+ # Collect queries used by charts
428
+ chart_query_names = collect_layout_chart_query_names(face)
429
+
430
+ # Also check charts dict directly
431
+ for chart in face.charts.values():
432
+ if chart.query_name:
433
+ chart_query_names.add(chart.query_name)
434
+
435
+ # Collect queries and their dependencies
436
+ def add_query_and_deps(name: str) -> None:
437
+ """Recursively add a query and its dependencies."""
438
+ if name in queries or name not in face.queries:
439
+ return
440
+
441
+ query = face.queries[name]
442
+ queries[name] = query
443
+
444
+ # Find dependencies
445
+ if is_sql_query(query):
446
+ refs = extract_query_refs(query.sql)
447
+ for ref in refs:
448
+ add_query_and_deps(ref)
449
+
450
+ for query_name in chart_query_names:
451
+ add_query_and_deps(query_name)
452
+
453
+ return queries
454
+
455
+
456
+ def get_query_source(query: AnyQuery) -> str:
457
+ """Get the database source for a query.
458
+
459
+ Args:
460
+ query: AnyQuery object
461
+
462
+ Returns:
463
+ Source string (defaults to 'default' if not specified)
464
+ """
465
+ source = getattr(query, "source", None)
466
+ if isinstance(source, str):
467
+ return source
468
+ return "default"
469
+
470
+
471
+ def group_by_source(
472
+ queries: dict[str, AnyQuery],
473
+ ) -> dict[str, dict[str, AnyQuery]]:
474
+ """Group queries by their database source.
475
+
476
+ Args:
477
+ queries: Dict mapping query names to queries
478
+
479
+ Returns:
480
+ Dict mapping source to dict of queries with that source
481
+
482
+ Example:
483
+ Given queries with sources:
484
+ orders: postgres
485
+ users: postgres
486
+ events: mysql
487
+
488
+ Returns:
489
+ {'postgres': {'orders': ..., 'users': ...}, 'mysql': {'events': ...}}
490
+ """
491
+ groups: dict[str, dict[str, AnyQuery]] = {}
492
+
493
+ for name, query in queries.items():
494
+ source = get_query_source(query)
495
+ if source not in groups:
496
+ groups[source] = {}
497
+ groups[source][name] = query
498
+
499
+ return groups
500
+
501
+
502
+ def validate_cross_source_references(
503
+ queries: dict[str, AnyQuery],
504
+ graph: DependencyGraph,
505
+ ) -> None:
506
+ """Validate that no queries reference queries on different sources.
507
+
508
+ Args:
509
+ queries: Dict of queries to validate
510
+ graph: Dependency graph for the queries
511
+
512
+ Raises:
513
+ CrossSourceReferenceError: If a cross-source reference is detected
514
+ """
515
+ for name, query in queries.items():
516
+ if not is_sql_query(query):
517
+ continue
518
+
519
+ query_source = get_query_source(query)
520
+ deps = graph.get_dependencies(name)
521
+
522
+ for dep_name in deps:
523
+ if dep_name not in queries:
524
+ continue
525
+
526
+ dep_query = queries[dep_name]
527
+ dep_source = get_query_source(dep_query)
528
+
529
+ if query_source != dep_source:
530
+ raise CrossSourceReferenceError(
531
+ source_query=name,
532
+ query_source=query_source,
533
+ target_query=dep_name,
534
+ target_source=dep_source,
535
+ )
536
+
537
+
538
+ class TempTableNotSupportedError(Exception):
539
+ """Error raised when the dialect doesn't support temporary tables.
540
+
541
+ Some databases (e.g., certain cloud analytics platforms) may not support
542
+ temp tables, in which case batch optimization cannot be used.
543
+ """
544
+
545
+ def __init__(self, dialect_name: str):
546
+ self.dialect_name = dialect_name
547
+ super().__init__(
548
+ f"Dialect '{dialect_name}' does not support temporary tables. "
549
+ f"Batch execution with temp table optimization is not available."
550
+ )
551
+
552
+
553
+ def generate_batch_sql(
554
+ queries: dict[str, AnyQuery],
555
+ graph: DependencyGraph,
556
+ dialect: SQLDialect,
557
+ direct_chart_queries: set[str] | None = None,
558
+ ) -> list[BatchStatement]:
559
+ """Generate batch SQL statements with temp table optimization.
560
+
561
+ For queries that are referenced by other queries:
562
+ 1. Create a temp table with the base query
563
+ 2. Rewrite dependent queries to SELECT from the temp table
564
+
565
+ Args:
566
+ queries: Dict of queries to include in the batch
567
+ graph: Dependency graph for the queries
568
+ dialect: SQL dialect for temp table syntax
569
+ direct_chart_queries: Set of query names directly used by charts.
570
+ If a temp table query is also used directly, we add a SELECT *
571
+ statement for it.
572
+
573
+ Returns:
574
+ List of BatchStatement objects in execution order
575
+
576
+ Raises:
577
+ TempTableNotSupportedError: If dialect doesn't support temp tables
578
+
579
+ Example:
580
+ Given:
581
+ orders: SELECT * FROM orders
582
+ high_value: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
583
+
584
+ Returns:
585
+ [
586
+ BatchStatement("_create_orders", "CREATE TEMP TABLE _df_temp_orders AS SELECT * FROM orders", True),
587
+ BatchStatement("orders", "SELECT * FROM _df_temp_orders"), # If orders is directly used
588
+ BatchStatement("high_value", "SELECT * FROM _df_temp_orders WHERE amount > 1000"),
589
+ ]
590
+ """
591
+ if direct_chart_queries is None:
592
+ direct_chart_queries = set()
593
+
594
+ statements: list[BatchStatement] = []
595
+ temp_candidates = graph.get_temp_table_candidates()
596
+ sorted_queries = topological_sort(graph)
597
+
598
+ # Track which temp tables have been created
599
+ created_temp_tables: set[str] = set()
600
+
601
+ # Check if dialect supports temp tables (only needed if we have candidates)
602
+ if temp_candidates and not dialect.supports_temp_tables:
603
+ raise TempTableNotSupportedError(dialect.name)
604
+
605
+ for query_name in sorted_queries:
606
+ if query_name not in queries:
607
+ continue
608
+
609
+ query = queries[query_name]
610
+
611
+ # Only SQL queries can be batched with temp tables
612
+ if not is_sql_query(query):
613
+ # For non-SQL queries, just add as-is (they'll be executed separately)
614
+ continue
615
+
616
+ sql = query.sql
617
+ setup = query.setup_sql.strip() + "\n" if query.setup_sql else ""
618
+
619
+ # Rewrite {{ queries.X }} refs to temp table names
620
+ # Note: We use a precompiled pattern (_QUERY_REF_PATTERN) for extraction,
621
+ # but need a dynamic pattern here to match specific query names
622
+ for ref in extract_query_refs(sql):
623
+ if ref in created_temp_tables:
624
+ temp_ref = dialect.temp_table_ref(f"{TEMP_TABLE_PREFIX}{ref}")
625
+ # Replace the Jinja template with the temp table reference
626
+ # Use re.escape() to handle any regex metacharacters in query names
627
+ escaped_ref = re.escape(ref)
628
+ pattern = rf"\{{\{{\s*queries\.{escaped_ref}\s*\}}\}}"
629
+ sql = re.sub(pattern, temp_ref, sql)
630
+
631
+ if query_name in temp_candidates:
632
+ # This query is referenced by other queries, so we create a temp table.
633
+ # This ensures the query runs only ONCE, and dependents read from the
634
+ # temp table instead of re-executing the query.
635
+ temp_name = f"{TEMP_TABLE_PREFIX}{query_name}"
636
+ temp_sql = setup + dialect.temp_table(temp_name, sql)
637
+ statements.append(
638
+ BatchStatement(
639
+ name=f"_create_{query_name}",
640
+ sql=temp_sql,
641
+ is_temp_table_create=True,
642
+ query_name=query_name,
643
+ )
644
+ )
645
+ created_temp_tables.add(query_name)
646
+
647
+ # Special case: Query is BOTH a dependency AND directly used by a chart.
648
+ # Example: "orders" is referenced by "high_value" AND has its own chart.
649
+ # We need TWO statements:
650
+ # 1. CREATE TEMP TABLE (above) - for dependents to reference
651
+ # 2. SELECT * FROM temp table (below) - to get results for the chart
652
+ if query_name in direct_chart_queries:
653
+ temp_ref = dialect.temp_table_ref(temp_name)
654
+ statements.append(
655
+ BatchStatement(
656
+ name=query_name,
657
+ sql=f"SELECT * FROM {temp_ref}",
658
+ is_temp_table_create=False,
659
+ query_name=query_name,
660
+ )
661
+ )
662
+ else:
663
+ # Regular query (not a temp table candidate)
664
+ statements.append(
665
+ BatchStatement(
666
+ name=query_name,
667
+ sql=setup + sql,
668
+ is_temp_table_create=False,
669
+ query_name=query_name,
670
+ )
671
+ )
672
+
673
+ return statements
674
+
675
+
676
+ def create_batch_execution_plan(
677
+ face: Face,
678
+ ) -> dict[str, list[BatchStatement]]:
679
+ """Create a complete batch execution plan for a dataface.
680
+
681
+ This is the main entry point for batch execution planning.
682
+ It collects queries, groups by source, validates cross-source
683
+ references, and generates batch SQL for each source.
684
+
685
+ Args:
686
+ face: Face to create execution plan for
687
+
688
+ Returns:
689
+ Dict mapping source to list of BatchStatements
690
+
691
+ Raises:
692
+ CrossSourceReferenceError: If a query references another
693
+ query on a different database source
694
+
695
+ Example:
696
+ >>> plan = create_batch_execution_plan(face)
697
+ >>> for source, statements in plan.items():
698
+ ... print(f"Source: {source}")
699
+ ... for stmt in statements:
700
+ ... print(f" {stmt.name}: {stmt.sql[:50]}...")
701
+ """
702
+ # Collect all queries used by the dataface
703
+ all_queries = collect_face_queries(face, chart_queries_only=True)
704
+
705
+ if not all_queries:
706
+ return {}
707
+
708
+ # Build the full dependency graph first (for validation)
709
+ full_graph = build_dependency_graph(all_queries)
710
+
711
+ # Validate no cross-source references
712
+ validate_cross_source_references(all_queries, full_graph)
713
+
714
+ # Collect query names directly used by charts
715
+ direct_chart_queries: set[str] = set()
716
+ for chart in face.charts.values():
717
+ if chart.query_name:
718
+ direct_chart_queries.add(chart.query_name)
719
+
720
+ # Group queries by source
721
+ source_groups = group_by_source(all_queries)
722
+
723
+ # Generate batch SQL for each source
724
+ result: dict[str, list[BatchStatement]] = {}
725
+
726
+ for source, source_queries in source_groups.items():
727
+ # Build graph for this source's queries
728
+ graph = build_dependency_graph(source_queries)
729
+
730
+ # Get dialect for this source
731
+ dialect = get_dialect(source)
732
+
733
+ # Generate batch statements
734
+ statements = generate_batch_sql(
735
+ queries=source_queries,
736
+ graph=graph,
737
+ dialect=dialect,
738
+ direct_chart_queries=direct_chart_queries,
739
+ )
740
+
741
+ if statements:
742
+ result[source] = statements
743
+
744
+ return result