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,658 @@
1
+ """Parameterized query rendering module.
2
+
3
+ Stage: COMPILE
4
+ Purpose: Render Jinja templates to parameterized SQL for SQL injection prevention.
5
+
6
+ Instead of interpolating values directly into SQL strings (vulnerable to injection):
7
+ "SELECT * FROM orders WHERE date >= '2024-01-01'"
8
+
9
+ This module renders to parameterized queries (safe):
10
+ sql = "SELECT * FROM orders WHERE date >= $1"
11
+ params = ["2024-01-01"]
12
+
13
+ Entry Points:
14
+ - render_parameterized(template, variables, dialect) -> ParameterizedQuery
15
+
16
+ The executor then uses: cursor.execute(sql, params)
17
+
18
+ Security Notes:
19
+ - All variable values are passed as parameters, never interpolated
20
+ - Operator validation uses VALID_OPERATORS allowlist
21
+ - Column/table names are validated to prevent injection via identifiers
22
+ - Legacy filter helpers in jinja.py should NOT be used (deprecated)
23
+
24
+ Dependencies:
25
+ - jinja2
26
+ - dataface.execute.dialects (for parameter placeholder styles)
27
+ """
28
+
29
+ import hashlib
30
+ import re
31
+ from collections.abc import Callable, Iterator
32
+ from dataclasses import dataclass, field
33
+ from typing import Any
34
+
35
+ from jinja2 import (
36
+ Environment,
37
+ StrictUndefined,
38
+ TemplateSyntaxError,
39
+ UndefinedError,
40
+ )
41
+
42
+ from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
43
+ from dataface.core.compile.errors import JinjaError
44
+ from dataface.core.execute.dialects import VALID_OPERATORS, SQLDialect, get_dialect
45
+
46
+ # Regex pattern for valid SQL identifiers (column names, table names)
47
+ # Allows: alphanumeric, underscore, and dot (for table.column)
48
+ # Must start with letter or underscore
49
+ _VALID_IDENTIFIER_PATTERN = re.compile(
50
+ r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$"
51
+ )
52
+
53
+
54
+ def _validate_identifier(identifier: str, identifier_type: str = "column") -> None:
55
+ """Validate SQL identifier (column or table.column) to prevent injection.
56
+
57
+ Args:
58
+ identifier: The identifier to validate
59
+ identifier_type: Type for error messages ("column", "table", etc.)
60
+
61
+ Raises:
62
+ ValueError: If identifier is invalid (empty, not a string, or contains invalid chars)
63
+
64
+ Security:
65
+ Only allows alphanumeric characters, underscores, and dots (for table.column).
66
+ Must start with a letter or underscore.
67
+
68
+ Limitations:
69
+ - Does not support schema-qualified names (schema.table.column)
70
+ - Does not support quoted identifiers ("Column Name")
71
+ - Does not support Unicode identifiers
72
+ For these cases, use raw SQL with manual parameterization.
73
+ """
74
+ if not isinstance(identifier, str):
75
+ raise ValueError(
76
+ f"Invalid {identifier_type} name: must be a string, got {type(identifier).__name__}"
77
+ )
78
+
79
+ if not identifier:
80
+ raise ValueError(f"Invalid {identifier_type} name: cannot be empty")
81
+
82
+ if not _VALID_IDENTIFIER_PATTERN.match(identifier):
83
+ raise ValueError(
84
+ f"Invalid {identifier_type} name: {identifier!r}. "
85
+ f"Must contain only letters, numbers, underscores, and optionally "
86
+ f"one dot for table.column format. Must start with letter or underscore."
87
+ )
88
+
89
+
90
+ def _validate_operator(operator: str) -> str:
91
+ """Validate SQL operator against allowlist to prevent injection.
92
+
93
+ Args:
94
+ operator: The operator to validate
95
+
96
+ Returns:
97
+ The normalized (uppercase) operator
98
+
99
+ Raises:
100
+ ValueError: If operator is not in the allowlist
101
+ """
102
+ op_upper = operator.upper().strip()
103
+ if op_upper not in VALID_OPERATORS:
104
+ raise ValueError(
105
+ f"Invalid SQL operator: {operator!r}. "
106
+ f"Must be one of: {', '.join(sorted(VALID_OPERATORS))}"
107
+ )
108
+ return operator
109
+
110
+
111
+ @dataclass
112
+ class ParameterizedQuery:
113
+ """Result of parameterized SQL rendering.
114
+
115
+ Attributes:
116
+ sql: SQL with parameter placeholders (e.g., $1, $2)
117
+ params: List of parameter values in order
118
+ template_hash: Hash of the original template for caching
119
+ """
120
+
121
+ sql: str
122
+ params: list[Any] = field(default_factory=list)
123
+ template_hash: str = ""
124
+
125
+
126
+ class _ParameterCollector:
127
+ """Collects parameters during Jinja rendering.
128
+
129
+ This class intercepts variable access during Jinja rendering,
130
+ collecting values for parameterization and returning placeholders.
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ variables: dict[str, Any],
136
+ dialect: SQLDialect,
137
+ exclude_vars: set | None = None,
138
+ ):
139
+ """Initialize parameter collector.
140
+
141
+ Args:
142
+ variables: Original variable values
143
+ dialect: SQL dialect for placeholder generation
144
+ exclude_vars: Variable names to exclude from parameterization
145
+ (e.g., 'queries' namespace, helper functions)
146
+ """
147
+ self.variables = variables
148
+ self.dialect = dialect
149
+ self.exclude_vars = exclude_vars or set()
150
+ self.params: list[Any] = []
151
+ self._param_index = 0
152
+ # Deduplication only works for indexed-placeholder dialects ($1, $2, …) where
153
+ # the same $N can repeat in SQL against a single param entry. For positional-only
154
+ # placeholders (?, %s) every occurrence needs its own param entry.
155
+ self._deduplicate = dialect.param(1) != dialect.param(2)
156
+ self._seen_params: dict[tuple, int] = {}
157
+
158
+ def get_param(self, name: str, value: Any) -> str:
159
+ """Get parameter placeholder for a variable.
160
+
161
+ If the same variable with the same value is used multiple times,
162
+ reuses the same parameter. Different values get new parameters,
163
+ even if they have the same variable name (handles {% set %} reassignment).
164
+
165
+ Args:
166
+ name: Variable name
167
+ value: Variable value
168
+
169
+ Returns:
170
+ Parameter placeholder string (e.g., '$1')
171
+ """
172
+ # Create a key based on name and value
173
+ # Use id() for mutable objects, value itself for immutable primitives
174
+ try:
175
+ # Try to use the value directly (works for hashable types)
176
+ key = (name, value)
177
+ hash(key) # Test if hashable
178
+ except TypeError:
179
+ # For unhashable types (lists, dicts), use object identity
180
+ key = (name, id(value))
181
+
182
+ # Reuse the same placeholder index only for indexed-placeholder dialects.
183
+ if self._deduplicate and key in self._seen_params:
184
+ return self.dialect.param(self._seen_params[key])
185
+
186
+ # Add new parameter
187
+ self._param_index += 1
188
+ self._seen_params[key] = self._param_index
189
+ self.params.append(value)
190
+ return self.dialect.param(self._param_index)
191
+
192
+
193
+ class _NullValue:
194
+ """Marker for NULL values in parameterized context.
195
+
196
+ Renders as 'NULL' in SQL and is recognized by filter helpers
197
+ as requiring special handling (returning 1=1 for nullable filters).
198
+ """
199
+
200
+ def __str__(self) -> str:
201
+ return "NULL"
202
+
203
+ def __repr__(self) -> str:
204
+ return "NULL"
205
+
206
+ def __bool__(self) -> bool:
207
+ # NULL values are falsy for conditionals
208
+ return False
209
+
210
+ def __eq__(self, other: Any) -> bool:
211
+ return isinstance(other, _NullValue) or other is None
212
+
213
+ def __ne__(self, other: Any) -> bool:
214
+ return not self.__eq__(other)
215
+
216
+
217
+ class _ParameterizedValue:
218
+ """Wrapper for parameterized variable values.
219
+
220
+ When rendered by Jinja, returns the parameter placeholder.
221
+ """
222
+
223
+ def __init__(self, name: str, value: Any, collector: _ParameterCollector):
224
+ self._name = name
225
+ self._value = value
226
+ self._collector = collector
227
+
228
+ def __str__(self) -> str:
229
+ """Return parameter placeholder when rendered."""
230
+ return self._collector.get_param(self._name, self._value)
231
+
232
+ def __repr__(self) -> str:
233
+ return self.__str__()
234
+
235
+ # Support common operations that might be used in templates
236
+ def __eq__(self, other: Any) -> bool:
237
+ return self._value == other
238
+
239
+ def __ne__(self, other: Any) -> bool:
240
+ return self._value != other
241
+
242
+ def __bool__(self) -> bool:
243
+ return bool(self._value)
244
+
245
+ def __iter__(self) -> Iterator[Any]:
246
+ if hasattr(self._value, "__iter__"):
247
+ return iter(self._value)
248
+ raise TypeError(f"'{type(self._value).__name__}' is not iterable")
249
+
250
+ def __getitem__(self, key: Any) -> Any:
251
+ if hasattr(self._value, "__getitem__"):
252
+ return self._value[key]
253
+ raise TypeError(f"'{type(self._value).__name__}' is not subscriptable")
254
+
255
+
256
+ def _compute_template_hash(template: str) -> str:
257
+ """Compute hash of template for caching.
258
+
259
+ Args:
260
+ template: SQL template string
261
+
262
+ Returns:
263
+ SHA-256 hash of template (first 16 chars)
264
+ """
265
+ return hashlib.sha256(template.encode()).hexdigest()[:16]
266
+
267
+
268
+ def render_parameterized(
269
+ template: str,
270
+ variables: dict[str, Any] | None = None,
271
+ dialect: SQLDialect | None = None,
272
+ profile_type: str = "postgres",
273
+ strict: bool = True,
274
+ ) -> ParameterizedQuery:
275
+ """Render Jinja template to parameterized SQL.
276
+
277
+ Converts {{ variable }} expressions to database parameter placeholders.
278
+
279
+ For simple variable substitution like:
280
+ "SELECT * FROM orders WHERE region = '{{ region }}'"
281
+
282
+ Produces:
283
+ sql = "SELECT * FROM orders WHERE region = $1"
284
+ params = ["North"]
285
+
286
+ Complex Jinja expressions (conditionals, loops, filters) are still
287
+ evaluated, but variable values within them become parameters.
288
+
289
+ Args:
290
+ template: SQL template with Jinja expressions
291
+ variables: Variable values for substitution
292
+ dialect: SQL dialect instance (overrides profile_type)
293
+ profile_type: Database type string (e.g., 'postgres', 'duckdb')
294
+ strict: If True (default), raises error on undefined variables.
295
+ If False, undefined variables become empty strings (useful for
296
+ chart editor where variables may not all be set).
297
+
298
+ Returns:
299
+ ParameterizedQuery with SQL, params, and template_hash
300
+
301
+ Raises:
302
+ JinjaError: If template syntax is invalid or required variable is missing
303
+ (only in strict mode)
304
+
305
+ Example:
306
+ >>> result = render_parameterized(
307
+ ... "SELECT * FROM users WHERE status = '{{ status }}'",
308
+ ... variables={"status": "active"},
309
+ ... profile_type="postgres"
310
+ ... )
311
+ >>> result.sql
312
+ "SELECT * FROM users WHERE status = '$1'"
313
+ >>> result.params
314
+ ['active']
315
+ """
316
+ if not template:
317
+ return ParameterizedQuery(sql="", params=[], template_hash="")
318
+
319
+ # Quick check for Jinja syntax - if no Jinja, return as-is
320
+ if "{{" not in template and "{%" not in template:
321
+ return ParameterizedQuery(
322
+ sql=template,
323
+ params=[],
324
+ template_hash=_compute_template_hash(template),
325
+ )
326
+
327
+ variables = variables or {}
328
+
329
+ # Get dialect
330
+ if dialect is None:
331
+ dialect = get_dialect(profile_type)
332
+
333
+ # Create parameter collector
334
+ collector = _ParameterCollector(
335
+ variables=variables,
336
+ dialect=dialect,
337
+ exclude_vars={"queries", "filter", "filter_date_range"},
338
+ )
339
+
340
+ # Build context with parameterized wrapper for each variable
341
+ context: dict[str, Any] = {}
342
+
343
+ for name, value in variables.items():
344
+ if name in collector.exclude_vars:
345
+ # Keep special context items as-is
346
+ context[name] = value
347
+ elif value is None:
348
+ # None values should render as NULL and be tracked as None
349
+ # We use a special marker that the filter helpers recognize
350
+ context[name] = _NullValue()
351
+ else:
352
+ # Wrap value for parameterization
353
+ context[name] = _ParameterizedValue(name, value, collector)
354
+
355
+ # Add parameterized filter helpers
356
+ context["filter"] = _make_filter_helper(collector, dialect)
357
+ context["filter_date_range"] = _make_filter_date_range_helper(collector, dialect)
358
+
359
+ # Handle queries namespace if present
360
+ if "queries" in variables:
361
+ context["queries"] = variables["queries"]
362
+
363
+ try:
364
+ undefined_cls = StrictUndefined if strict else _LenientUndefined
365
+ env = Environment(undefined=undefined_cls)
366
+ jinja_template = env.from_string(template)
367
+ rendered_sql = jinja_template.render(context)
368
+
369
+ # Clean up any surrounding quotes around parameter placeholders
370
+ # e.g., "'$1'" -> "$1" for proper parameterization
371
+ rendered_sql = _clean_parameter_quotes(
372
+ rendered_sql, dialect, len(collector.params)
373
+ )
374
+
375
+ return ParameterizedQuery(
376
+ sql=rendered_sql,
377
+ params=collector.params,
378
+ template_hash=_compute_template_hash(template),
379
+ )
380
+
381
+ except UndefinedError as e:
382
+ raise JinjaError(f"Undefined variable: {e}", template) from e
383
+ except TemplateSyntaxError as e:
384
+ raise JinjaError(f"Template syntax error: {e}", template) from e
385
+ except ValueError as e:
386
+ # Re-raise validation errors (operator, column name) with context
387
+ raise JinjaError(f"Validation error: {e}", template) from e
388
+ except (KeyError, TypeError) as e:
389
+ # Key lookup failures, type errors
390
+ raise JinjaError(f"Template error: {e}", template) from e
391
+
392
+
393
+ def _clean_parameter_quotes(sql: str, dialect: SQLDialect, param_count: int) -> str:
394
+ """Remove quotes around parameter placeholders.
395
+
396
+ SQL like "WHERE name = '$1'" should become "WHERE name = $1"
397
+ because the database driver handles string quoting for parameters.
398
+
399
+ Uses regex patterns to avoid incorrectly matching placeholders that appear
400
+ inside SQL string literals or comments.
401
+
402
+ Args:
403
+ sql: Rendered SQL string
404
+ dialect: SQL dialect
405
+ param_count: Number of parameters
406
+
407
+ Returns:
408
+ SQL with quotes removed from around parameter placeholders
409
+ """
410
+ if param_count == 0:
411
+ return sql
412
+
413
+ result = sql
414
+ dialect_name = dialect.name
415
+
416
+ if dialect_name in ("postgres", "postgresql", "duckdb", "redshift"):
417
+ # $1, $2, ... style - remove surrounding quotes using regex
418
+ # Pattern matches quotes immediately around the placeholder
419
+ for i in range(1, param_count + 1):
420
+ # Escape $ for regex
421
+ placeholder_pattern = rf"'(\${i})'"
422
+ result = re.sub(placeholder_pattern, r"\1", result)
423
+ placeholder_pattern = rf'"(\${i})"'
424
+ result = re.sub(placeholder_pattern, r"\1", result)
425
+
426
+ elif dialect_name in ("mysql", "mariadb"):
427
+ # %s style - use regex to match exactly '%s' or "%s"
428
+ result = re.sub(r"'(%s)'", r"\1", result)
429
+ result = re.sub(r'"(%s)"', r"\1", result)
430
+
431
+ elif dialect_name == "snowflake":
432
+ # ? style - use regex to match exactly '?' or "?"
433
+ result = re.sub(r"'(\?)'", r"\1", result)
434
+ result = re.sub(r'"(\?)"', r"\1", result)
435
+
436
+ elif dialect_name == "bigquery":
437
+ # @param_name style - named params, would need different handling
438
+ # BigQuery uses @name format which is already safe
439
+ pass
440
+
441
+ elif dialect_name in ("databricks", "spark"):
442
+ # :param style - named params
443
+ pass
444
+
445
+ return result
446
+
447
+
448
+ def _make_filter_helper(
449
+ collector: _ParameterCollector,
450
+ dialect: SQLDialect,
451
+ ) -> Callable[..., str]:
452
+ """Create parameterized filter helper.
453
+
454
+ Returns a function that generates parameterized filter clauses:
455
+ {{ filter('column', value) }} -> "column = $1"
456
+ {{ filter('column', value, '!=') }} -> "column != $1"
457
+
458
+ Security:
459
+ - Column names are validated against SQL injection
460
+ - Operators are validated against VALID_OPERATORS allowlist
461
+ - Values are always passed as parameters, never interpolated
462
+
463
+ Args:
464
+ collector: Parameter collector
465
+ dialect: SQL dialect
466
+
467
+ Returns:
468
+ Filter helper function
469
+ """
470
+
471
+ def filter_helper(
472
+ column: str,
473
+ value: Any,
474
+ operator: str = "=",
475
+ *,
476
+ none: str = "allow",
477
+ ) -> str:
478
+ """Generate parameterized filter clause.
479
+
480
+ Args:
481
+ column: Column name (must be valid SQL identifier, not user input)
482
+ value: Filter value (will be parameterized)
483
+ operator: SQL operator (validated against allowlist)
484
+ none: Fallback when value is null/empty/_NullValue. 'allow' (default)
485
+ returns '1=1' (no constraint, show all rows). 'deny' returns
486
+ '1=0' (zero rows).
487
+
488
+ Returns:
489
+ SQL clause with parameter placeholder, '1=1' (allow all) on missing
490
+ value, or '1=0' (deny all) on missing value when none='deny'.
491
+
492
+ Raises:
493
+ ValueError: If column name, operator, or `none` is invalid.
494
+ """
495
+ if none not in ("allow", "deny"):
496
+ raise ValueError(f"none= must be 'allow' or 'deny', got {none!r}")
497
+
498
+ # Validate column name to prevent SQL injection
499
+ _validate_identifier(column, "column")
500
+
501
+ # Handle None/empty/NullValue - return fallback per `none` kwarg
502
+ if value is None or value == "" or isinstance(value, _NullValue):
503
+ return "1=0" if none == "deny" else "1=1"
504
+
505
+ # Unwrap if it's already a ParameterizedValue
506
+ actual_value = value._value if isinstance(value, _ParameterizedValue) else value
507
+
508
+ # Handle list for IN clause
509
+ if isinstance(actual_value, list):
510
+ if not actual_value:
511
+ return "1=0" if none == "deny" else "1=1"
512
+
513
+ # Validate column for IN clause too
514
+ # For IN clause, we need multiple parameters
515
+ placeholders = []
516
+ for item in actual_value:
517
+ collector._param_index += 1
518
+ collector.params.append(item)
519
+ placeholders.append(dialect.param(collector._param_index))
520
+
521
+ return f"{column} IN ({', '.join(placeholders)})"
522
+
523
+ # Validate operator to prevent SQL injection
524
+ _validate_operator(operator)
525
+
526
+ # Single value - get parameter placeholder
527
+ collector._param_index += 1
528
+ collector.params.append(actual_value)
529
+ placeholder = dialect.param(collector._param_index)
530
+
531
+ return f"{column} {operator} {placeholder}"
532
+
533
+ return filter_helper
534
+
535
+
536
+ def _make_filter_date_range_helper(
537
+ collector: _ParameterCollector,
538
+ dialect: SQLDialect,
539
+ ) -> Callable[[str, Any], str]:
540
+ """Create parameterized date range filter helper.
541
+
542
+ Returns a function that generates parameterized BETWEEN clauses:
543
+ {{ filter_date_range('date', date_range) }}
544
+ -> "date BETWEEN $1 AND $2"
545
+
546
+ Security:
547
+ - Column names are validated against SQL injection
548
+ - Date values are always passed as parameters
549
+
550
+ Args:
551
+ collector: Parameter collector
552
+ dialect: SQL dialect
553
+
554
+ Returns:
555
+ Date range filter helper function
556
+ """
557
+
558
+ def filter_date_range_helper(
559
+ column: str,
560
+ date_range: Any,
561
+ ) -> str:
562
+ """Generate parameterized date range filter.
563
+
564
+ Args:
565
+ column: Date column name (must be valid SQL identifier)
566
+ date_range: Tuple/list of [start, end] dates
567
+
568
+ Returns:
569
+ SQL BETWEEN clause with parameter placeholders
570
+
571
+ Raises:
572
+ ValueError: If column name is invalid
573
+ """
574
+ # Validate column name to prevent SQL injection
575
+ _validate_identifier(column, "column")
576
+
577
+ if not date_range or isinstance(date_range, _NullValue):
578
+ return "1=1"
579
+
580
+ # Unwrap if it's a ParameterizedValue
581
+ actual_value = (
582
+ date_range._value
583
+ if isinstance(date_range, _ParameterizedValue)
584
+ else date_range
585
+ )
586
+
587
+ # Handle JSON string
588
+ if isinstance(actual_value, str):
589
+ import json
590
+
591
+ try:
592
+ actual_value = json.loads(actual_value)
593
+ except (json.JSONDecodeError, ValueError) as e:
594
+ raise ValueError(f"Invalid date_range JSON: {actual_value!r}") from e
595
+
596
+ if not isinstance(actual_value, (list, tuple)):
597
+ raise ValueError(
598
+ f"date_range must be a list/tuple [start, end], got {type(actual_value).__name__}"
599
+ )
600
+
601
+ if len(actual_value) != 2:
602
+ raise ValueError(
603
+ f"date_range must have exactly 2 elements [start, end], got {len(actual_value)}"
604
+ )
605
+
606
+ start, end = actual_value
607
+ if not start or not end:
608
+ return "1=1" # Empty dates = no filter (intentional)
609
+
610
+ # Add parameters for start and end
611
+ collector._param_index += 1
612
+ collector.params.append(start)
613
+ start_placeholder = dialect.param(collector._param_index)
614
+
615
+ collector._param_index += 1
616
+ collector.params.append(end)
617
+ end_placeholder = dialect.param(collector._param_index)
618
+
619
+ return f"{column} BETWEEN {start_placeholder} AND {end_placeholder}"
620
+
621
+ return filter_date_range_helper
622
+
623
+
624
+ def render_parameterized_with_queries(
625
+ template: str,
626
+ variables: dict[str, Any] | None = None,
627
+ queries: dict[str, Any] | None = None,
628
+ dialect: SQLDialect | None = None,
629
+ profile_type: str = "postgres",
630
+ ) -> ParameterizedQuery:
631
+ """Render parameterized SQL with query reference support.
632
+
633
+ Like render_parameterized but also handles {{ queries.* }} references.
634
+
635
+ Args:
636
+ template: SQL template with Jinja expressions
637
+ variables: Variable values for substitution
638
+ queries: Query registry for {{ queries.* }} resolution
639
+ dialect: SQL dialect instance
640
+ profile_type: Database type string
641
+
642
+ Returns:
643
+ ParameterizedQuery with SQL, params, and template_hash
644
+ """
645
+ variables_with_queries: dict[str, Any] | None
646
+ if queries:
647
+ # Include queries in variables for resolution
648
+ variables_with_queries = dict(variables or {})
649
+ variables_with_queries["queries"] = _QueryNamespace(queries)
650
+ else:
651
+ variables_with_queries = variables
652
+
653
+ return render_parameterized(
654
+ template=template,
655
+ variables=variables_with_queries,
656
+ dialect=dialect,
657
+ profile_type=profile_type,
658
+ )