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,511 @@
1
+ """Jinja template resolution module.
2
+
3
+ Stage: COMPILE (Part of Step 3: Normalization)
4
+ Purpose: Resolve Jinja templates in queries and SQL strings.
5
+
6
+ Entry Points:
7
+ - resolve_jinja_template(template: str, variables: Dict) -> str
8
+ - resolve_query_filters(filters: Dict, variables: Dict) -> Dict
9
+ - extract_variable_dependencies(template: str) -> Set[str]
10
+
11
+ Inputs:
12
+ - Template strings with {{ variable }} syntax
13
+ - Variable values for substitution
14
+
15
+ Outputs:
16
+ - Resolved strings with variables substituted
17
+ - Set of variable names referenced in templates
18
+
19
+ Dependencies:
20
+ - jinja2 (Environment, Template, meta)
21
+
22
+ Errors:
23
+ - JinjaError: Template syntax or resolution errors
24
+
25
+ See also:
26
+ - compile/normalizer.py: Uses this module
27
+
28
+ Security Note:
29
+ When using strict=False (lenient mode), undefined variables are silently
30
+ converted to empty strings. This mode should ONLY be used in interactive
31
+ contexts (chart editor) where the user is actively editing and expects
32
+ partial results. Production rendering should always use strict=True.
33
+ """
34
+
35
+ import logging
36
+ import re
37
+ from typing import Any
38
+
39
+ from jinja2 import (
40
+ Environment,
41
+ StrictUndefined,
42
+ TemplateError,
43
+ TemplateSyntaxError,
44
+ Undefined,
45
+ UndefinedError,
46
+ meta,
47
+ )
48
+
49
+ from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
50
+ from dataface.core.compile.errors import JinjaError
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ # Jinja environment with strict undefined (errors on missing variables)
56
+ _jinja_env = Environment(undefined=StrictUndefined)
57
+
58
+ # Lenient Jinja environment (undefined variables become empty/None)
59
+ _jinja_env_lenient = Environment(undefined=_LenientUndefined)
60
+
61
+ # Known context items that are NOT user variables (helper functions, namespaces)
62
+ _KNOWN_CONTEXT = {"filter", "filter_date_range", "queries"}
63
+
64
+
65
+ def extract_variable_dependencies(template_str: str) -> set[str]:
66
+ """Extract variable names referenced in a Jinja template.
67
+
68
+ Uses Jinja's AST parser to find all undeclared variables in the template.
69
+ Filters out known helper functions and namespaces.
70
+
71
+ This is used during normalization to track which variables a query,
72
+ chart title, or other template depends on.
73
+
74
+ Args:
75
+ template_str: String potentially containing Jinja expressions
76
+
77
+ Returns:
78
+ Set of variable names referenced in the template
79
+
80
+ Example:
81
+ >>> extract_variable_dependencies(
82
+ ... "SELECT * FROM orders WHERE region = '{{ region }}'"
83
+ ... )
84
+ {'region'}
85
+
86
+ >>> extract_variable_dependencies(
87
+ ... "SELECT * WHERE {{ filter('region', region) }}"
88
+ ... )
89
+ {'region'}
90
+ """
91
+ if not template_str or not isinstance(template_str, str):
92
+ return set()
93
+
94
+ # Quick check - no Jinja syntax
95
+ if "{{" not in template_str and "{%" not in template_str:
96
+ return set()
97
+
98
+ try:
99
+ env = Environment()
100
+ ast = env.parse(template_str)
101
+ undeclared = meta.find_undeclared_variables(ast)
102
+ # Filter out known context items (helpers, not user variables)
103
+ return undeclared - _KNOWN_CONTEXT
104
+ except TemplateSyntaxError:
105
+ # Jinja syntax errors are expected here for invalid templates.
106
+ # Return empty set - the actual render will catch and report the syntax error
107
+ # with proper context. This is intentionally silent because this function
108
+ # is only used for dependency detection, not validation.
109
+ return set()
110
+
111
+
112
+ def resolve_jinja_template(
113
+ template: str,
114
+ variables: dict[str, Any] | None = None,
115
+ queries: dict[str, Any] | None = None,
116
+ strict: bool = True,
117
+ ) -> str:
118
+ """Resolve a Jinja template string.
119
+
120
+ Stage: COMPILE (Step 3: Normalization - Jinja Resolution)
121
+
122
+ Resolves {{ variable }} expressions in template strings.
123
+ Also handles {{ queries.query_name }} references for SQL composition.
124
+
125
+ Args:
126
+ template: String potentially containing Jinja expressions
127
+ variables: Variable values for substitution
128
+ queries: Query registry for {{ queries.* }} resolution
129
+ strict: If True (default), raises error on undefined variables.
130
+ If False, undefined variables become None/empty (useful for
131
+ chart editor where variables may not all be set).
132
+
133
+ Returns:
134
+ Resolved string with variables substituted
135
+
136
+ Raises:
137
+ JinjaError: If template syntax is invalid or (if strict) variable not found
138
+
139
+ Example:
140
+ >>> resolve_jinja_template(
141
+ ... "SELECT * FROM users WHERE status = '{{ status }}'",
142
+ ... variables={"status": "active"}
143
+ ... )
144
+ "SELECT * FROM users WHERE status = 'active'"
145
+ """
146
+ if not template or not isinstance(template, str):
147
+ return template
148
+
149
+ # Quick check for Jinja syntax
150
+ if "{{" not in template and "{%" not in template:
151
+ return template
152
+
153
+ variables = variables or {}
154
+ queries = queries or {}
155
+
156
+ # Build context with variables and query helper
157
+ context = {**variables}
158
+
159
+ # Add queries namespace for {{ queries.query_name }} resolution
160
+ if queries:
161
+ context["queries"] = _QueryNamespace(queries)
162
+
163
+ # Add helper functions
164
+ context["filter"] = _filter_helper
165
+ context["filter_date_range"] = _filter_date_range_helper
166
+
167
+ # Use strict or lenient environment
168
+ jinja_env = _jinja_env if strict else _jinja_env_lenient
169
+
170
+ try:
171
+ jinja_template = jinja_env.from_string(template)
172
+ return jinja_template.render(context)
173
+ except UndefinedError as e:
174
+ raise JinjaError(f"Undefined variable: {e}", template) from e
175
+ except TemplateSyntaxError as e:
176
+ raise JinjaError(f"Template syntax error: {e}", template) from e
177
+ except TemplateError as e:
178
+ raise JinjaError(f"Template error: {e}", template) from e
179
+
180
+
181
+ def resolve_query_filters(
182
+ filters: dict[str, Any],
183
+ variables: dict[str, Any] | None = None,
184
+ ) -> dict[str, Any]:
185
+ """Resolve Jinja expressions in filter values.
186
+
187
+ Args:
188
+ filters: Dictionary of filter conditions
189
+ variables: Variable values for substitution
190
+
191
+ Returns:
192
+ Filters with resolved values
193
+
194
+ Example:
195
+ >>> resolve_query_filters(
196
+ ... {"status": "{{ selected_status }}"},
197
+ ... variables={"selected_status": "active"}
198
+ ... )
199
+ {"status": "active"}
200
+ """
201
+ if not filters:
202
+ return filters
203
+
204
+ resolved: dict[str, Any] = {}
205
+ for key, value in filters.items():
206
+ if isinstance(value, str):
207
+ resolved[key] = resolve_jinja_template(value, variables)
208
+ elif isinstance(value, dict):
209
+ resolved[key] = resolve_query_filters(value, variables)
210
+ elif isinstance(value, list):
211
+ resolved[key] = [
212
+ resolve_jinja_template(v, variables) if isinstance(v, str) else v
213
+ for v in value
214
+ ]
215
+ else:
216
+ resolved[key] = value
217
+
218
+ return resolved
219
+
220
+
221
+ def detect_query_dependencies(queries: dict[str, Any]) -> dict[str, list[str]]:
222
+ """Detect dependencies between queries for circular reference detection.
223
+
224
+ Scans query SQL for {{ queries.* }} references and builds a dependency graph.
225
+ Raises an error if circular dependencies are detected.
226
+
227
+ Args:
228
+ queries: Dictionary of query definitions
229
+
230
+ Returns:
231
+ Dictionary mapping query name to list of dependencies
232
+
233
+ Raises:
234
+ JinjaError: If circular dependencies detected
235
+ """
236
+ dependencies: dict[str, list[str]] = {}
237
+
238
+ for name, query in queries.items():
239
+ deps: list[str] = []
240
+
241
+ # Get SQL content
242
+ sql = None
243
+ if hasattr(query, "sql"):
244
+ sql = query.sql
245
+ elif isinstance(query, dict):
246
+ sql = query.get("sql")
247
+
248
+ if sql:
249
+ # Find {{ queries.* }} references
250
+ pattern = r"\{\{\s*queries\.(\w+)\s*\}\}"
251
+ matches = re.findall(pattern, sql)
252
+ deps.extend(matches)
253
+
254
+ dependencies[name] = deps
255
+
256
+ # Check for circular dependencies
257
+ _detect_circular(dependencies)
258
+
259
+ return dependencies
260
+
261
+
262
+ def _detect_circular(dependencies: dict[str, list[str]]) -> None:
263
+ """Detect circular dependencies using DFS.
264
+
265
+ Args:
266
+ dependencies: Dependency graph
267
+
268
+ Raises:
269
+ JinjaError: If circular dependency found
270
+ """
271
+ visited: set = set()
272
+ rec_stack: set = set()
273
+
274
+ def dfs(node: str, path: list[str]) -> None:
275
+ if node in rec_stack:
276
+ cycle = path[path.index(node) :] + [node]
277
+ raise JinjaError(
278
+ f"Circular query dependency detected: {' -> '.join(cycle)}"
279
+ )
280
+
281
+ if node in visited:
282
+ return
283
+
284
+ visited.add(node)
285
+ rec_stack.add(node)
286
+
287
+ for dep in dependencies.get(node, []):
288
+ if dep in dependencies: # Only follow known queries
289
+ dfs(dep, path + [node])
290
+
291
+ rec_stack.remove(node)
292
+
293
+ for query_name in dependencies:
294
+ dfs(query_name, [])
295
+
296
+
297
+ def _topological_sort(dependencies: dict[str, list[str]]) -> list[str]:
298
+ """Return query names in topological order (dependencies first).
299
+
300
+ Args:
301
+ dependencies: Dependency graph from detect_query_dependencies()
302
+
303
+ Returns:
304
+ List of query names with leaf nodes first
305
+ """
306
+ visited: set[str] = set()
307
+ order: list[str] = []
308
+
309
+ def visit(name: str) -> None:
310
+ if name in visited:
311
+ return
312
+ visited.add(name)
313
+ for dep in dependencies.get(name, []):
314
+ if dep in dependencies:
315
+ visit(dep)
316
+ order.append(name)
317
+
318
+ for name in dependencies:
319
+ visit(name)
320
+
321
+ return order
322
+
323
+
324
+ def _get_query_sql(query: Any) -> str:
325
+ """Extract the SQL string a _QueryNamespace would return for a query."""
326
+ if hasattr(query, "sql"):
327
+ sql = query.sql
328
+ return sql if isinstance(sql, str) else ""
329
+ elif isinstance(query, dict):
330
+ sql = query.get("sql", "")
331
+ return sql if isinstance(sql, str) else ""
332
+ return str(query)
333
+
334
+
335
+ def _substitute_query_refs(sql: str, resolved: dict[str, str]) -> str:
336
+ """Replace {{ queries.X }} tokens with already-resolved SQL.
337
+
338
+ Uses regex substitution so that only query references are expanded;
339
+ variable expressions ({{ var }}) and other Jinja constructs are
340
+ left untouched for a later resolve_jinja_template pass.
341
+ """
342
+ _QUERY_REF = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
343
+
344
+ def _replacer(m: re.Match) -> str:
345
+ name = m.group(1)
346
+ if name in resolved:
347
+ return resolved[name]
348
+ return m.group(0) # leave unresolved if not in map
349
+
350
+ return _QUERY_REF.sub(_replacer, sql)
351
+
352
+
353
+ def resolve_query_sql_with_dependencies(
354
+ query_name: str,
355
+ queries: dict[str, Any],
356
+ variables: dict[str, Any] | None = None,
357
+ ) -> str:
358
+ """Resolve a query's SQL, fully expanding nested {{ queries.* }} refs.
359
+
360
+ Two-phase approach:
361
+ Phase 1 — Expand {{ queries.* }} in topological order using regex so
362
+ each query sees fully expanded dependency SQL. Variable
363
+ expressions are left untouched.
364
+ Phase 2 — Run the target query's expanded SQL through Jinja to resolve
365
+ variables, filters, and other template constructs.
366
+
367
+ Args:
368
+ query_name: The target query to resolve
369
+ queries: Full query registry (name -> query object or dict)
370
+ variables: Variable values for Jinja substitution
371
+
372
+ Returns:
373
+ Fully resolved SQL string for the target query
374
+
375
+ Raises:
376
+ JinjaError: If circular dependencies detected or resolution fails
377
+ """
378
+ # Build dependency graph (validates no cycles)
379
+ deps = detect_query_dependencies(queries)
380
+
381
+ # Topological sort — leaves (no deps) come first
382
+ order = _topological_sort(deps)
383
+
384
+ # Phase 1: expand {{ queries.* }} refs in dependency order
385
+ expanded: dict[str, str] = {}
386
+ for name in order:
387
+ if name not in queries:
388
+ continue
389
+ raw_sql = _get_query_sql(queries[name])
390
+ if raw_sql and ("{{ queries." in raw_sql or "{{queries." in raw_sql):
391
+ expanded[name] = _substitute_query_refs(raw_sql, expanded)
392
+ else:
393
+ expanded[name] = raw_sql
394
+
395
+ target_sql = expanded.get(query_name, _get_query_sql(queries.get(query_name, "")))
396
+
397
+ # Phase 2: resolve variables / filters via Jinja
398
+ if target_sql and ("{{" in target_sql or "{%" in target_sql):
399
+ target_sql = resolve_jinja_template(target_sql, variables=variables)
400
+
401
+ return target_sql
402
+
403
+
404
+ def _filter_helper(
405
+ column: str,
406
+ value: Any,
407
+ operator: str = "=",
408
+ none: str = "allow",
409
+ ) -> str:
410
+ """Generate SQL filter clause (DEPRECATED - use parameterized version).
411
+
412
+ .. deprecated::
413
+ This helper uses string interpolation which is vulnerable to SQL injection.
414
+ Use the parameterized filter helper from dataface.core.compile.parameterized instead,
415
+ which is automatically used when executing queries via the SqlAdapter.
416
+
417
+ Security Warning:
418
+ This function directly interpolates values into SQL strings. If values
419
+ come from user input, this creates SQL injection vulnerabilities.
420
+ The parameterized version in parameterized.py should be used instead.
421
+
422
+ Helper function available in templates as {{ filter(...) }}.
423
+
424
+ Args:
425
+ column: Column name
426
+ value: Filter value
427
+ operator: SQL operator (=, !=, >, <, etc.)
428
+ none: Fallback when value is null/empty/undefined. 'allow' (default) returns
429
+ '1=1' (no constraint, show all rows). 'deny' returns '1=0' (zero rows).
430
+
431
+ Returns:
432
+ SQL clause like "column = 'value'", "1=1" (allow all) if value is None/undefined,
433
+ or "1=0" (deny all) if value is None/undefined and none='deny'.
434
+
435
+ Raises:
436
+ ValueError: If none is not 'allow' or 'deny'.
437
+ """
438
+ if none not in ("allow", "deny"):
439
+ raise ValueError(f"none= must be 'allow' or 'deny', got {none!r}")
440
+
441
+ # Handle undefined Jinja variables (from lenient mode)
442
+ if isinstance(value, Undefined):
443
+ return "1=0" if none == "deny" else "1=1"
444
+ if value is None or value == "":
445
+ return "1=0" if none == "deny" else "1=1"
446
+
447
+ if isinstance(value, str):
448
+ return f"{column} {operator} '{value}'"
449
+ elif isinstance(value, (int, float)):
450
+ return f"{column} {operator} {value}"
451
+ elif isinstance(value, list):
452
+ if not value:
453
+ return "1=0" if none == "deny" else "1=1"
454
+ quoted = [f"'{v}'" if isinstance(v, str) else str(v) for v in value]
455
+ return f"{column} IN ({', '.join(quoted)})"
456
+
457
+ return f"{column} {operator} '{value}'"
458
+
459
+
460
+ def _filter_date_range_helper(
461
+ column: str,
462
+ date_range: Any,
463
+ ) -> str:
464
+ """Generate SQL date range filter (DEPRECATED - use parameterized version).
465
+
466
+ .. deprecated::
467
+ This helper uses string interpolation which is vulnerable to SQL injection.
468
+ Use the parameterized filter_date_range helper from dataface.core.compile.parameterized
469
+ instead, which is automatically used when executing queries via the SqlAdapter.
470
+
471
+ Security Warning:
472
+ This function directly interpolates values into SQL strings. If values
473
+ come from user input, this creates SQL injection vulnerabilities.
474
+ The parameterized version in parameterized.py should be used instead.
475
+
476
+ Helper function for {{ filter_date_range(column, date_range) }}.
477
+
478
+ Args:
479
+ column: Date column name
480
+ date_range: Tuple/list of [start, end] dates, or JSON string
481
+
482
+ Returns:
483
+ SQL BETWEEN clause or "1=1" (always true) if invalid/undefined
484
+ """
485
+ if not date_range:
486
+ return "1=1"
487
+
488
+ # Handle JSON string (from URL parameters)
489
+ if isinstance(date_range, str):
490
+ import json
491
+
492
+ try:
493
+ date_range = json.loads(date_range)
494
+ except (json.JSONDecodeError, ValueError) as e:
495
+ raise ValueError(f"Invalid date_range JSON: {date_range!r}") from e
496
+
497
+ if not isinstance(date_range, (list, tuple)):
498
+ raise ValueError(
499
+ f"date_range must be a list/tuple [start, end], got {type(date_range).__name__}"
500
+ )
501
+
502
+ if len(date_range) != 2:
503
+ raise ValueError(
504
+ f"date_range must have exactly 2 elements [start, end], got {len(date_range)}"
505
+ )
506
+
507
+ start, end = date_range
508
+ if not start or not end:
509
+ return "1=1" # Empty dates = no filter (intentional)
510
+
511
+ return f"{column} BETWEEN '{start}' AND '{end}'"
@@ -0,0 +1,52 @@
1
+ """Jinja env for chart-label templates and ``where:`` expressions.
2
+
3
+ Lives in compile/ so ChartLabels' pydantic validators can parse without
4
+ reaching into the render layer (the dependency direction stays
5
+ compile → shared, render → shared, never render → compile or compile →
6
+ render).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from jinja2 import Environment, StrictUndefined
14
+
15
+ from dataface.core.render.format_utils import format_d3
16
+
17
+
18
+ def label_jinja_env() -> Environment:
19
+ """Return the Jinja env used to compile and render label templates.
20
+
21
+ Strict-undefined: a typo like ``{{ pct }}`` (instead of ``percent``)
22
+ raises immediately rather than silently rendering empty.
23
+ """
24
+ env = Environment(undefined=StrictUndefined, autoescape=False)
25
+ env.filters["format"] = _format_filter
26
+ return env
27
+
28
+
29
+ def _format_filter(value: Any, spec: str) -> str:
30
+ """Jinja filter: ``{{ value | format('$,.0f') }}`` → d3-formatted."""
31
+ return format_d3(value, spec)
32
+
33
+
34
+ def strip_jinja_braces(expr: str) -> str:
35
+ """Accept ``{{ x }}`` or bare ``x`` for ``where:`` — return the bare expression.
36
+
37
+ Authors naturally write ``where: "{{ value > 5 }}"`` (matching the
38
+ ``template:`` syntax), but Jinja's ``compile_expression`` wants the
39
+ bare expression (``value > 5``). Tolerate the single-outer-pair case;
40
+ reject mixed forms (``"{{ a }} and {{ b }}"`` etc.) loudly.
41
+ """
42
+ s = expr.strip()
43
+ if not (s.startswith("{{") and s.endswith("}}")):
44
+ return s
45
+ inner = s[2:-2]
46
+ if "{{" in inner or "}}" in inner:
47
+ raise ValueError(
48
+ "where: expression contains nested {{ ... }}; use a bare Python "
49
+ "expression instead (e.g. ``value > 5`` not "
50
+ "``{{ value > 5 }} and {{ other }}``)."
51
+ )
52
+ return inner.strip()