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,387 @@
1
+ """SQL statement-type guard for dft-core.
2
+
3
+ Two public validators:
4
+ validate_select_only(sql, dialect=None) — read-only queries only
5
+ validate_setup_sql(sql, dialect=None) — TEMP CREATE + DuckDB MACRO only
6
+
7
+ Both raise MutatingSqlError on policy violation, UnparseableSqlError when
8
+ the SQL skeleton cannot be statically determined (Jinja shapes we don't model,
9
+ or sqlglot parse failure). Callers decide what to do with each error type.
10
+
11
+ The approach:
12
+ 1. Build a SQL skeleton from the Jinja AST: TemplateData verbatim, {{ expr }}
13
+ as placeholder identifiers, {% if %}/{% for %}/{% set %} walked structurally
14
+ so both branches of if are included (catching malicious-branch injection).
15
+ Unknown node types raise UnparseableSqlError (default-deny).
16
+ 2. Strip a leading EXPLAIN / EXPLAIN ANALYZE keyword — sqlglot opaquely wraps
17
+ `EXPLAIN <anything>` as a Command node, so the inner statement is invisible
18
+ to AST allowlist checks. Strip-and-revalidate forces the inner body through
19
+ the same gate, so `EXPLAIN DROP TABLE x` fails like the bare DROP would.
20
+ 3. sqlglot.parse(skeleton, read=dialect) to get top-level statements.
21
+ 4. Walk statements against the function's allowlist, then scan descendants for
22
+ mutating-statement types so CTE-laundered DML (`WITH x AS (DELETE …) SELECT
23
+ * FROM x`) is rejected.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+
30
+ import jinja2
31
+ import jinja2.nodes
32
+ import sqlglot
33
+ import sqlglot.errors
34
+ import sqlglot.expressions as exp
35
+
36
+ from dataface.core.execute.errors import MutatingSqlError, UnparseableSqlError
37
+
38
+ # Dataface dialect names → sqlglot dialect names. sqlglot calls SQL Server
39
+ # "tsql"; we present it as "sqlserver"/"mssql". "mariadb" is an alias for
40
+ # "mysql" in both. This is the canonical mapping for the guard; the wider
41
+ # dataface.core.execute.dialects registry does not currently expose one.
42
+ _DIALECT_MAP: dict[str, str] = {
43
+ "sqlserver": "tsql",
44
+ "mssql": "tsql",
45
+ "mariadb": "mysql",
46
+ }
47
+
48
+
49
+ def sqlglot_dialect(dialect: str | None) -> str | None:
50
+ """Translate a Dataface dialect name to the sqlglot equivalent.
51
+
52
+ sqlglot knows 'tsql' (not 'sqlserver'/'mssql') and 'mysql' (not 'mariadb').
53
+ Returns None unchanged (sqlglot default dialect).
54
+ """
55
+ if dialect is None:
56
+ return None
57
+ return _DIALECT_MAP.get(dialect, dialect)
58
+
59
+
60
+ # Top-level statements allowed by validate_select_only. exp.Select is further
61
+ # narrowed by an INTO-anywhere descendant scan below.
62
+ _ALLOWED_QUERY_NODES: frozenset[type[exp.Expression]] = frozenset(
63
+ {
64
+ exp.Select,
65
+ exp.Subquery,
66
+ exp.Union,
67
+ exp.Intersect,
68
+ exp.Except,
69
+ exp.Describe,
70
+ exp.Show,
71
+ }
72
+ )
73
+
74
+ # Mutating statement types that must not appear anywhere in a SELECT-family
75
+ # tree — including inside CTEs, subqueries, and UNION arms. The CTE-laundered
76
+ # bypass shape `WITH x AS (DELETE … RETURNING *) SELECT * FROM x` parses as a
77
+ # top-level Select with the DML buried as a descendant; descendant-scan closes
78
+ # the gap. Top-level type-allowlist would otherwise admit the outer Select.
79
+ _DISALLOWED_DESCENDANT_NODES: tuple[type[exp.Expression], ...] = (
80
+ exp.Insert,
81
+ exp.Update,
82
+ exp.Delete,
83
+ exp.Merge,
84
+ exp.Drop,
85
+ exp.Alter,
86
+ exp.TruncateTable,
87
+ exp.Create,
88
+ exp.Pragma,
89
+ exp.Grant,
90
+ exp.Attach,
91
+ exp.Detach,
92
+ # sqlglot has no exp.Revoke as of 26.x; REVOKE parses as a Command at the
93
+ # top level and is caught there. If a future sqlglot adds Revoke as a
94
+ # dedicated node, add it here so a buried REVOKE inside a CTE is caught.
95
+ )
96
+
97
+ _ALLOWED_SETUP_NODES: frozenset[type[exp.Expression]] = frozenset({exp.Create})
98
+
99
+
100
+ # sqlglot models `EXPLAIN <stmt>` as an opaque Command — the inner statement
101
+ # is invisible to the AST. Strip the EXPLAIN keyword (and common modifiers)
102
+ # from the skeleton so the body re-parses through the normal allowlist and
103
+ # `EXPLAIN DROP TABLE x` fails like the bare DROP would. Only the leading
104
+ # EXPLAIN is stripped — a mid-stream `…; EXPLAIN …` parses as Command and
105
+ # is rejected by the allowlist (acceptable fail-closed for an uncommon shape).
106
+ _EXPLAIN_PREFIX_RE = re.compile(
107
+ r"^\s*EXPLAIN"
108
+ r"(?:\s+(?:ANALYZE|VERBOSE|QUERY\s+PLAN))*"
109
+ r"(?:\s*\([^)]*\))?"
110
+ r"\s+",
111
+ re.IGNORECASE,
112
+ )
113
+
114
+
115
+ def _strip_explain_prefix(skeleton: str) -> str:
116
+ return _EXPLAIN_PREFIX_RE.sub("", skeleton, count=1)
117
+
118
+
119
+ # Databricks Delta Lake adds DETAIL / HISTORY variants to DESCRIBE that
120
+ # sqlglot does not model — `DESCRIBE DETAIL my_table` fails to parse. Both
121
+ # variants are read-only metadata reads (mirrors of Delta-log inspection).
122
+ # Strip the variant keyword so the body re-parses as a standard exp.Describe.
123
+ _DESCRIBE_VARIANT_PREFIX_RE = re.compile(
124
+ r"^\s*DESCRIBE\s+(?:DETAIL|HISTORY)\s+",
125
+ re.IGNORECASE,
126
+ )
127
+
128
+
129
+ def _strip_describe_variant_prefix(skeleton: str) -> str:
130
+ return _DESCRIBE_VARIANT_PREFIX_RE.sub("DESCRIBE ", skeleton, count=1)
131
+
132
+
133
+ # Read-only commands sqlglot falls back to `Command` for on dialects that
134
+ # lack a dedicated node (postgres / bigquery / redshift / databricks / default
135
+ # all parse `SHOW TABLES` as `Command(name="SHOW")`). Terminal commands —
136
+ # no inner statement to revalidate.
137
+ _ALLOWED_TOP_LEVEL_COMMAND_KEYWORDS: frozenset[str] = frozenset({"SHOW"})
138
+
139
+
140
+ def _is_allowed_top_level_command(stmt: exp.Expression) -> bool:
141
+ return (
142
+ isinstance(stmt, exp.Command)
143
+ and (stmt.name or "").upper() in _ALLOWED_TOP_LEVEL_COMMAND_KEYWORDS
144
+ )
145
+
146
+
147
+ def _walk_jinja(node: jinja2.nodes.Node, out: list[str], counter: list[int]) -> None:
148
+ """Walk a jinja2 AST, appending SQL fragments to *out*.
149
+
150
+ counter is a one-element list used as a mutable int for placeholder naming.
151
+ Raises UnparseableSqlError for any node type outside the explicit table —
152
+ default-deny so AssignBlock / Macro / Include / extension nodes never
153
+ silently swallow embedded SQL.
154
+ """
155
+ if isinstance(node, jinja2.nodes.Template):
156
+ for child in node.body:
157
+ _walk_jinja(child, out, counter)
158
+
159
+ elif isinstance(node, jinja2.nodes.TemplateData):
160
+ out.append(node.data)
161
+
162
+ elif isinstance(node, jinja2.nodes.Output):
163
+ # Output interleaves TemplateData (literal SQL) and expression nodes
164
+ # ({{ ref('orders') }}, {{ var }}, …). Literals pass through; each
165
+ # expression becomes a placeholder identifier.
166
+ for child in node.nodes:
167
+ if isinstance(child, jinja2.nodes.TemplateData):
168
+ out.append(child.data)
169
+ else:
170
+ out.append(f"__dft_j{counter[0]}__")
171
+ counter[0] += 1
172
+
173
+ elif isinstance(node, jinja2.nodes.If):
174
+ # jinja2 represents elif_ as a flat list of If nodes (each with empty
175
+ # elif_), not a linked-list chain. Iterate every element so DROPs
176
+ # buried in the 2nd / 3rd elif slot are walked, not just the first.
177
+ for child in node.body:
178
+ _walk_jinja(child, out, counter)
179
+ for elif_node in node.elif_:
180
+ out.append(" ; ")
181
+ for child in elif_node.body:
182
+ _walk_jinja(child, out, counter)
183
+ if node.else_:
184
+ out.append(" ; ")
185
+ for child in node.else_:
186
+ _walk_jinja(child, out, counter)
187
+
188
+ elif isinstance(node, jinja2.nodes.For):
189
+ # Walk body once and the `else_` clause if present — jinja2 runs the
190
+ # else clause when the iterable is empty. Same branch-coverage posture
191
+ # as If: SQL hiding in the else branch must reach the skeleton.
192
+ for child in node.body:
193
+ _walk_jinja(child, out, counter)
194
+ if node.else_:
195
+ out.append(" ; ")
196
+ for child in node.else_:
197
+ _walk_jinja(child, out, counter)
198
+
199
+ elif isinstance(node, jinja2.nodes.Assign):
200
+ # {% set x = ... %} — emits nothing.
201
+ pass
202
+
203
+ else:
204
+ # AssignBlock ({% set sql %}...{% endset %}), Macro, Include, Import,
205
+ # FromImport, Extends, Block, FilterBlock, CallBlock, future jinja2
206
+ # additions — all default-deny. If AssignBlock fell through, the body's
207
+ # SQL would never reach the parser.
208
+ raise UnparseableSqlError(f"unsupported_jinja_node:{type(node).__name__}")
209
+
210
+
211
+ def _build_skeleton(template_str: str) -> str:
212
+ """Return a SQL skeleton by walking the jinja2 AST.
213
+
214
+ TemplateData becomes literal SQL; `{{ expr }}` becomes a placeholder
215
+ identifier; `{% if %}` recurses all branches. Unknown jinja nodes raise
216
+ UnparseableSqlError so no SQL can hide inside them.
217
+ """
218
+ if "{{" not in template_str and "{%" not in template_str:
219
+ return template_str
220
+
221
+ env = jinja2.Environment()
222
+ try:
223
+ ast = env.parse(template_str)
224
+ except jinja2.TemplateSyntaxError as exc:
225
+ raise UnparseableSqlError(exc) from exc
226
+
227
+ out: list[str] = []
228
+ counter = [0]
229
+ _walk_jinja(ast, out, counter)
230
+ return "".join(out)
231
+
232
+
233
+ def _parse(skeleton: str, dialect: str | None) -> list[exp.Expression | None]:
234
+ try:
235
+ return sqlglot.parse(skeleton, read=sqlglot_dialect(dialect))
236
+ except sqlglot.errors.ParseError as exc:
237
+ raise UnparseableSqlError(exc) from exc
238
+
239
+
240
+ def _reject_mutating_descendants(
241
+ stmt: exp.Expression, allowlist_label: str = "query"
242
+ ) -> None:
243
+ """Scan stmt for mutating-statement descendants and raise on first hit.
244
+
245
+ Catches CTE-laundered DML (`WITH x AS (DELETE …) SELECT *`), DML inside
246
+ subqueries, DML inside UNION arms, and `SELECT … INTO` at any depth —
247
+ all of which would otherwise pass a top-level type-only check. Used by
248
+ both validate_select_only and validate_setup_sql; the latter passes
249
+ allowlist_label="setup" so the error message names the right policy.
250
+
251
+ sqlglot's find_all includes the calling node, so the outer Create on the
252
+ setup path would re-match itself — skip self explicitly. Nested CREATEs
253
+ inside `CREATE TEMP TABLE t AS (CREATE …)` still get rejected.
254
+ """
255
+ for desc in stmt.find_all(*_DISALLOWED_DESCENDANT_NODES):
256
+ if desc is stmt:
257
+ continue
258
+ raise MutatingSqlError(
259
+ rejected_node_kind=type(desc).__name__,
260
+ fragment_preview=desc.sql()[:60],
261
+ allowlist_label=allowlist_label,
262
+ )
263
+ for select in stmt.find_all(exp.Select):
264
+ if select.args.get("into") is not None:
265
+ raise MutatingSqlError(
266
+ rejected_node_kind="Select(into)",
267
+ fragment_preview=stmt.sql()[:60],
268
+ allowlist_label=allowlist_label,
269
+ )
270
+
271
+
272
+ def _is_bq_temp_function_create(stmt: exp.Expression) -> bool:
273
+ """Return True iff stmt is a BigQuery-style CREATE TEMP FUNCTION.
274
+
275
+ Used by validate_select_only to permit the BigQuery scripting pattern
276
+ `CREATE TEMP FUNCTION ... ; SELECT ...`. Only FUNCTION kind is accepted;
277
+ CREATE TEMP TABLE and CREATE TEMP VIEW belong in validate_setup_sql, not
278
+ inline query scripts.
279
+ """
280
+ if not isinstance(stmt, exp.Create):
281
+ return False
282
+ kind = (stmt.args.get("kind") or "").upper()
283
+ if kind != "FUNCTION":
284
+ return False
285
+ props = stmt.args.get("properties")
286
+ return props is not None and any(
287
+ isinstance(p, exp.TemporaryProperty) for p in props.expressions
288
+ )
289
+
290
+
291
+ def validate_select_only(sql: str, dialect: str | None = None) -> None:
292
+ """Raise MutatingSqlError on non-read-only SQL; UnparseableSqlError when undetermined.
293
+
294
+ Accepts: SELECT (no INTO), WITH, UNION, INTERSECT, EXCEPT, DESCRIBE, SHOW,
295
+ EXPLAIN of an allowed inner statement.
296
+ On BigQuery only: also accepts a script of one or more CREATE TEMP FUNCTION
297
+ statements followed by a final SELECT-family statement (inline scalar UDF
298
+ pattern from Looker-migrated dashboards).
299
+ Rejects: DROP, DELETE, INSERT, UPDATE, ALTER, GRANT, REVOKE, TRUNCATE,
300
+ ATTACH, LOAD, CALL, PRAGMA, CREATE, and any other mutating
301
+ statement — including DML hidden inside a CTE or subquery.
302
+
303
+ Args:
304
+ sql: Raw SQL string, possibly containing Jinja expressions.
305
+ dialect: sqlglot dialect name (e.g. "duckdb", "postgres"). None = default.
306
+ """
307
+ skeleton = _strip_describe_variant_prefix(
308
+ _strip_explain_prefix(_build_skeleton(sql))
309
+ )
310
+ statements = [
311
+ s
312
+ for s in _parse(skeleton, dialect)
313
+ if s is not None and not isinstance(s, exp.Semicolon)
314
+ ]
315
+
316
+ # BigQuery inline-UDF scripting pattern: one or more CREATE TEMP FUNCTION
317
+ # statements followed by a SELECT-family statement. This is a read-only
318
+ # pattern — the TEMP FUNCTIONs are ephemeral UDFs, not persistent DDL.
319
+ # Only allowed on the bigquery dialect; other dialects keep the full ban.
320
+ if dialect == "bigquery" and statements:
321
+ leading_temp_fns = [
322
+ s for s in statements[:-1] if _is_bq_temp_function_create(s)
323
+ ]
324
+ if leading_temp_fns and len(leading_temp_fns) == len(statements) - 1:
325
+ final = statements[-1]
326
+ # Validate the TEMP FUNCTION creates for buried DML.
327
+ for fn_stmt in leading_temp_fns:
328
+ _reject_mutating_descendants(fn_stmt)
329
+ # Validate the trailing statement as a normal query.
330
+ if type(final) in _ALLOWED_QUERY_NODES:
331
+ _reject_mutating_descendants(final)
332
+ return
333
+ if _is_allowed_top_level_command(final):
334
+ return
335
+ # Trailing statement is not a SELECT-family — fall through to
336
+ # per-statement validation which will reject it.
337
+
338
+ for stmt in statements:
339
+ if type(stmt) in _ALLOWED_QUERY_NODES:
340
+ _reject_mutating_descendants(stmt)
341
+ continue
342
+ if _is_allowed_top_level_command(stmt):
343
+ continue
344
+ raise MutatingSqlError(
345
+ rejected_node_kind=type(stmt).__name__,
346
+ fragment_preview=stmt.sql()[:60],
347
+ )
348
+
349
+
350
+ def validate_setup_sql(sql: str, dialect: str | None = None) -> None:
351
+ """Raise MutatingSqlError on non-setup SQL; UnparseableSqlError when undetermined.
352
+
353
+ Accepts:
354
+ - CREATE TEMP FUNCTION/TABLE/VIEW (any dialect)
355
+ - CREATE [OR REPLACE] MACRO (DuckDB; no TEMP form exists)
356
+
357
+ Rejects everything else, including non-TEMP CREATE, DROP, INSERT, etc.
358
+ """
359
+ skeleton = _build_skeleton(sql)
360
+ for stmt in _parse(skeleton, dialect):
361
+ if stmt is None or isinstance(stmt, exp.Semicolon):
362
+ continue
363
+ if type(stmt) not in _ALLOWED_SETUP_NODES:
364
+ raise MutatingSqlError(
365
+ rejected_node_kind=type(stmt).__name__,
366
+ fragment_preview=stmt.sql()[:60],
367
+ allowlist_label="setup",
368
+ )
369
+ kind = (stmt.args.get("kind") or "").upper()
370
+ # sqlglot represents TEMP via TemporaryProperty in the properties list,
371
+ # not as args["temporary"] = True (that field is always None in practice).
372
+ props = stmt.args.get("properties")
373
+ is_temp = props is not None and any(
374
+ isinstance(p, exp.TemporaryProperty) for p in props.expressions
375
+ )
376
+ is_macro = kind == "MACRO"
377
+ is_allowed_temp_kind = kind in {"FUNCTION", "TABLE", "VIEW"}
378
+
379
+ if not (is_macro or (is_temp and is_allowed_temp_kind)):
380
+ raise MutatingSqlError(
381
+ rejected_node_kind=f"Create(kind={kind}, temporary={is_temp})",
382
+ fragment_preview=stmt.sql()[:60],
383
+ allowlist_label="setup",
384
+ )
385
+ # CTAS bodies can smuggle DML through CTEs or SELECT … INTO. Scan
386
+ # descendants the same way validate_select_only does.
387
+ _reject_mutating_descendants(stmt, allowlist_label="setup")
@@ -0,0 +1,199 @@
1
+ """SQL literal inlining for dbt-adapter execution paths.
2
+
3
+ dbt adapter.execute() accepts plain SQL only — no parameterized queries.
4
+ This module provides functions for inlining positional $N params or named
5
+ dialect-specific params as properly escaped SQL literals. Used by SqlAdapter
6
+ and InspectConnection.
7
+
8
+ Values passed here must come from validated sources (compiled dashboard YAML
9
+ variables, or schema/table names from the inspector). Do not pass raw HTTP
10
+ input through this function.
11
+ """
12
+
13
+ import math
14
+ import re
15
+ from collections.abc import Callable
16
+ from datetime import date, datetime, time
17
+ from decimal import Decimal
18
+ from typing import Any
19
+
20
+ _DOLLAR_RE = re.compile(r"\$(\d+)")
21
+ _PERCENT_S_RE = re.compile(r"%s")
22
+ _QMARK_RE = re.compile(r"\?")
23
+
24
+
25
+ def _to_sql_literal(value: Any) -> str:
26
+ """Convert a Python value to a SQL literal string."""
27
+ if value is None:
28
+ return "NULL"
29
+ if isinstance(value, bool):
30
+ return "TRUE" if value else "FALSE"
31
+ if isinstance(value, int):
32
+ return str(value)
33
+ if isinstance(value, float):
34
+ if math.isnan(value):
35
+ raise ValueError(
36
+ "NaN cannot be inlined as a SQL literal — different warehouses use "
37
+ "different syntax (FLOAT64 / DOUBLE / DOUBLE PRECISION / FLOAT). "
38
+ "Filter the value out before passing to inline_params, or pass a "
39
+ "string like 'NaN' if your warehouse supports it."
40
+ )
41
+ if math.isinf(value):
42
+ raise ValueError(
43
+ "Infinity cannot be inlined as a SQL literal — different warehouses "
44
+ "use different syntax. Filter the value out before passing to "
45
+ "inline_params, or pass a string."
46
+ )
47
+ return repr(value)
48
+ if isinstance(value, Decimal):
49
+ return str(value)
50
+ if isinstance(value, datetime):
51
+ return f"TIMESTAMP '{value.isoformat()}'"
52
+ if isinstance(value, date):
53
+ return f"DATE '{value.isoformat()}'"
54
+ if isinstance(value, time):
55
+ return f"TIME '{value.isoformat()}'"
56
+ if isinstance(value, bytes):
57
+ return f"X'{value.hex()}'"
58
+ # String — escape single quotes to prevent injection
59
+ escaped = str(value).replace("'", "''")
60
+ return f"'{escaped}'"
61
+
62
+
63
+ def inline_params(sql: str, params: list[Any]) -> str:
64
+ """Inline $N-style positional params into a SQL string as SQL literals.
65
+
66
+ Replaces $1, $2, ... placeholders with properly escaped SQL literals.
67
+ Uses re.sub with a callback so each match position is visited exactly once —
68
+ substituted text is never re-scanned, preventing re-substitution attacks.
69
+
70
+ Args:
71
+ sql: SQL string with $1, $2, ... positional placeholders.
72
+ params: Ordered list of parameter values to inline.
73
+
74
+ Returns:
75
+ SQL string with all $N placeholders replaced by SQL literals.
76
+
77
+ Raises:
78
+ IndexError: A $N placeholder references an index outside [1, len(params)].
79
+ """
80
+
81
+ def repl(m: re.Match[str]) -> str:
82
+ idx = int(m.group(1)) - 1
83
+ if not 0 <= idx < len(params):
84
+ raise IndexError(
85
+ f"Parameter index ${m.group(1)} out of range "
86
+ f"for {len(params)} param(s)"
87
+ )
88
+ return _to_sql_literal(params[idx])
89
+
90
+ return _DOLLAR_RE.sub(repl, sql)
91
+
92
+
93
+ def inline_dialect_params(
94
+ sql: str, params: list[Any], param_fn: Callable[[int], str]
95
+ ) -> str:
96
+ """Inline dialect-specific named params into SQL as SQL literals.
97
+
98
+ For dialects whose placeholder format is not `$N` (e.g., BigQuery's `@paramN`,
99
+ Databricks' `:param1`, SQL Server's `@p1`), replaces each placeholder with a
100
+ properly escaped SQL literal.
101
+
102
+ Uses a single-pass regex so substituted text is never re-scanned — a param
103
+ value that happens to contain another placeholder string is not re-substituted.
104
+
105
+ The dbt adapter.execute() accepts only plain SQL, so this function is used
106
+ when executing against named-param dialects via dbt adapters.
107
+
108
+ Args:
109
+ sql: SQL string with dialect-specific parameter placeholders.
110
+ params: Ordered list of parameter values (1-indexed by placeholder number).
111
+ param_fn: Callable(index: int) -> str — returns the placeholder string for
112
+ the given 1-based index. Pass the dialect's ``param`` method.
113
+
114
+ Returns:
115
+ SQL string with all dialect placeholders replaced by SQL literals.
116
+ """
117
+ if not params:
118
+ return sql
119
+
120
+ # Build a pattern that alternates all placeholders, sorted longest-first so
121
+ # @param10 is matched before @param1 when they share a common prefix.
122
+ placeholders = sorted(
123
+ (re.escape(param_fn(i)), i) for i in range(1, len(params) + 1)
124
+ )
125
+ # Sort by length descending to avoid partial-match of @param1 inside @param10
126
+ placeholders.sort(key=lambda t: len(t[0]), reverse=True)
127
+ idx_by_placeholder = dict(placeholders)
128
+ pattern = re.compile("|".join(ph for ph, _ in placeholders))
129
+
130
+ def repl(m: re.Match[str]) -> str:
131
+ ph = re.escape(m.group(0))
132
+ return _to_sql_literal(params[idx_by_placeholder[ph] - 1])
133
+
134
+ return pattern.sub(repl, sql)
135
+
136
+
137
+ def inline_qmark_params(sql: str, params: list[Any]) -> str:
138
+ """Inline ?-style positional params into a SQL string as SQL literals.
139
+
140
+ Snowflake uses ? for positional parameters. Each ? is replaced in order
141
+ with the corresponding parameter value as a SQL literal.
142
+
143
+ Args:
144
+ sql: SQL string with ? positional placeholders.
145
+ params: Ordered list of parameter values to inline.
146
+
147
+ Returns:
148
+ SQL string with all ? placeholders replaced by SQL literals.
149
+
150
+ Raises:
151
+ ValueError: Param count does not match placeholder count.
152
+ """
153
+ n_placeholders = len(_QMARK_RE.findall(sql))
154
+ if len(params) != n_placeholders:
155
+ raise ValueError(
156
+ f"inline_qmark_params: {n_placeholders} ? placeholder(s) in SQL "
157
+ f"but {len(params)} param(s) provided"
158
+ )
159
+
160
+ it = iter(params)
161
+
162
+ def repl(m: re.Match[str]) -> str:
163
+ return _to_sql_literal(next(it))
164
+
165
+ return _QMARK_RE.sub(repl, sql)
166
+
167
+
168
+ def inline_percent_params(sql: str, params: list[Any]) -> str:
169
+ """Inline %s-style positional params into a SQL string as SQL literals.
170
+
171
+ Used by InspectConnection where legacy code used %s placeholders for
172
+ schema/table name parameters that now need literal inlining via dbt adapters.
173
+ Uses re.sub with a callback so each match is visited exactly once —
174
+ substituted text is never re-scanned, preventing re-substitution attacks.
175
+
176
+ Args:
177
+ sql: SQL string with %s positional placeholders.
178
+ params: Ordered list of parameter values to inline. Count must match
179
+ exactly the number of %s placeholders — too few or too many raises.
180
+
181
+ Returns:
182
+ SQL string with all %s placeholders replaced by SQL literals.
183
+
184
+ Raises:
185
+ ValueError: Param count does not match placeholder count.
186
+ """
187
+ n_placeholders = len(_PERCENT_S_RE.findall(sql))
188
+ if len(params) != n_placeholders:
189
+ raise ValueError(
190
+ f"inline_percent_params: {n_placeholders} %s placeholder(s) in SQL "
191
+ f"but {len(params)} param(s) provided"
192
+ )
193
+
194
+ it = iter(params)
195
+
196
+ def repl(m: re.Match[str]) -> str:
197
+ return _to_sql_literal(next(it))
198
+
199
+ return _PERCENT_S_RE.sub(repl, sql)
dataface/core/fonts.py ADDED
@@ -0,0 +1,52 @@
1
+ """Shared font path accessors for compile and render layers."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def get_fonts_dir() -> Path:
7
+ """Return the directory holding every vendored font asset."""
8
+ return Path(__file__).parent / "render" / "fonts"
9
+
10
+
11
+ def get_inter_font_path() -> Path:
12
+ """Return the vendored Inter variable font file."""
13
+ return get_fonts_dir() / "InterVariable.ttf"
14
+
15
+
16
+ def get_dft_sans_tabular_font_path() -> Path:
17
+ """Return the vendored DFT Sans Tabular font file."""
18
+ return get_fonts_dir() / "DFTSansTabular-Regular.ttf"
19
+
20
+
21
+ def get_dft_serif_oldstyle_tabular_font_path() -> Path:
22
+ """Return the vendored DFT Serif Oldstyle Tabular font file."""
23
+ return get_fonts_dir() / "DFTSerifOldstyleTabular-Regular.ttf"
24
+
25
+
26
+ def get_dft_serif_oldstyle_proportional_font_path() -> Path:
27
+ """Return the vendored DFT Serif Oldstyle Proportional font file."""
28
+ return get_fonts_dir() / "DFTSerifOldstyleProportional-Regular.ttf"
29
+
30
+
31
+ def get_source_serif_4_ttf_path() -> Path:
32
+ """Return the vendored Source Serif 4 TTF for ReportLab."""
33
+ return get_fonts_dir() / "SourceSerif4-Regular.ttf"
34
+
35
+
36
+ NOTO_EMOJI_FONT_FAMILY = "Noto Emoji"
37
+ NOTO_COLOR_EMOJI_FONT_FAMILY = "Noto Color Emoji"
38
+
39
+
40
+ def get_noto_emoji_font_path() -> Path:
41
+ """Return the vendored Noto Emoji TTF for ReportLab and vl-convert."""
42
+ return get_fonts_dir() / "NotoEmoji-Regular.ttf"
43
+
44
+
45
+ def get_noto_color_emoji_font_path() -> Path:
46
+ """Return the vendored Noto Color Emoji TTF for ReportLab and vl-convert."""
47
+ return get_fonts_dir() / "NotoColorEmoji-Regular.ttf"
48
+
49
+
50
+ def get_mono_font_path() -> Path:
51
+ """Return the vendored monospace font used for strict code measurement."""
52
+ return get_fonts_dir() / "SourceCodePro-Regular.ttf"
@@ -0,0 +1,32 @@
1
+ """OSS inspect module — schema sources, resolver, search, and HTML templates.
2
+
3
+ The profiler engine (TableInspector, detectors, cache I/O) lives in the
4
+ private ``dataface-super-schema`` package. This module only contains the
5
+ open-source components: DbtSchemaSource, LayeredSchemaResolver, schema
6
+ search, and the HTML inspect templates.
7
+ """
8
+
9
+ from dataface.core.inspect.renderer import (
10
+ InspectProfileCompileError,
11
+ render_inspect_dashboard,
12
+ validate_inspect_variables,
13
+ )
14
+
15
+ # Static list of available inspect templates.
16
+ # Template files live in dataface/core/inspect/templates/{name}.yml
17
+ INSPECT_TEMPLATES: list[str] = [
18
+ "categorical_column",
19
+ "charts",
20
+ "date_column",
21
+ "model",
22
+ "numeric_column",
23
+ "quality",
24
+ "string_column",
25
+ ]
26
+
27
+ __all__ = [
28
+ "render_inspect_dashboard",
29
+ "InspectProfileCompileError",
30
+ "validate_inspect_variables",
31
+ "INSPECT_TEMPLATES",
32
+ ]