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,710 @@
1
+ """SQL query adapter for executing raw SQL queries.
2
+
3
+ Stage: EXECUTE
4
+ Purpose: Execute SQL queries via dbt adapters or direct DuckDB connections.
5
+
6
+ DuckDB queries execute via the raw duckdb driver (already a hard dep) with
7
+ full parameterized query support ($1, $2 placeholders). All other warehouses
8
+ execute via build_adapter() from dbt_adapter_factory — the adapter package for
9
+ that warehouse must be installed (e.g. pip install dbt-postgres).
10
+
11
+ Security: DuckDB uses parameterized queries. Non-DuckDB queries inline params
12
+ after Jinja rendering — values are validated dashboard YAML variables, not
13
+ raw user input from HTTP.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ import threading
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ if TYPE_CHECKING:
25
+ from dataface.core.compile.models.source import SourceConfig
26
+ from dataface.core.execute.adapters.base import ResolvedRelation
27
+
28
+ from dataface.core.compile.filter_injection import inject_filters
29
+ from dataface.core.compile.models.face.compiled import VariableValues
30
+ from dataface.core.compile.models.query.compiled import (
31
+ AnyQuery,
32
+ is_sql_query,
33
+ )
34
+ from dataface.core.compile.parameterized import render_parameterized
35
+ from dataface.core.execute.adapters.base import (
36
+ BaseAdapter,
37
+ QueryParams,
38
+ QueryResult,
39
+ handle_adapter_error,
40
+ )
41
+ from dataface.core.execute.dbt_jinja import has_dbt_jinja
42
+ from dataface.core.execute.dialects import get_dialect
43
+ from dataface.core.execute.duckdb_config import normalize_duckdb_config
44
+ from dataface.core.execute.sql_guard import validate_select_only, validate_setup_sql
45
+ from dataface.core.execute.sql_literals import (
46
+ inline_dialect_params as _inline_dialect_params,
47
+ inline_params as _inline_params,
48
+ inline_qmark_params as _inline_qmark_params,
49
+ )
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ # DuckDB resolves relative read_csv()/read_parquet() paths against the process cwd.
54
+ # The adapter temporarily switches cwd to the face's project root so those queries
55
+ # work, but that global mutation must be serialized across requests.
56
+ _DUCKDB_CWD_LOCK = threading.RLock()
57
+
58
+
59
+ class SqlAdapter(BaseAdapter):
60
+ """Adapter for executing raw SQL queries.
61
+
62
+ DuckDB sources use the raw duckdb driver (parameterized, fast).
63
+ All other warehouses use build_adapter() from dbt_adapter_factory.
64
+
65
+ Supported query types: sql
66
+
67
+ Example:
68
+ >>> adapter = SqlAdapter()
69
+ >>> query = SqlQuery(sql="SELECT * FROM users WHERE id = {{ user_id }}")
70
+ >>> result = adapter.execute(query, {"user_id": 1})
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ dbt_project_path: str | None = None,
76
+ use_example_db: bool = False,
77
+ connection_string: str | None = None,
78
+ project_root: Path | None = None,
79
+ profile_type: str = "duckdb",
80
+ read_only: bool = True,
81
+ duckdb_config: dict[str, Any] | None = None,
82
+ allow_external_access_in_readonly: bool = False,
83
+ ):
84
+ """Initialize SQL adapter.
85
+
86
+ Args:
87
+ dbt_project_path: Path to dbt project (default: current directory)
88
+ use_example_db: If True, use DuckDB example database instead of dbt config
89
+ connection_string: DuckDB connection string (e.g., ":memory:" or file path)
90
+ project_root: Root directory for resolving relative file paths in read_csv()
91
+ profile_type: Database type for dialect selection (e.g., 'postgres', 'duckdb')
92
+ read_only: If True (default), open DuckDB connections in read-only mode.
93
+ File-based DuckDB: opened with read_only=True — the driver refuses all writes.
94
+ In-memory DuckDB (:memory:): always opened read-write regardless of this flag
95
+ because there is no other way to populate an in-memory database; this is a
96
+ known exception. enable_external_access=False is still forced for :memory:
97
+ when read_only=True unless allow_external_access_in_readonly=True.
98
+
99
+ When read_only=True (and allow_external_access_in_readonly=False),
100
+ enable_external_access=False is forced on the DuckDB config regardless of
101
+ any user-supplied duckdb_config. This blocks httpfs URL fetches and
102
+ read_csv/read_parquet/read_json against local and remote paths.
103
+ To allow external access on the default path, use read_only=False.
104
+
105
+ Non-DuckDB warehouses (Postgres, Snowflake, BigQuery, etc.) are not affected
106
+ by this flag — they have no native read-only driver knob. Use SELECT-only
107
+ credentials/roles at the warehouse level for those.
108
+
109
+ Callers that must write (cache backends, test fixture setup) should pass
110
+ read_only=False explicitly.
111
+ duckdb_config: Optional DuckDB config dict for the default connection.
112
+ When read_only=True and allow_external_access_in_readonly=False,
113
+ enable_external_access in this dict is overridden to False.
114
+ To enable external access with read_only=True, set both
115
+ allow_external_access_in_readonly=True AND
116
+ duckdb_config={"enable_external_access": True}.
117
+ allow_external_access_in_readonly: Security opt-in. When True AND
118
+ read_only=True AND duckdb_config contains enable_external_access=True,
119
+ the adapter passes enable_external_access=True through to DuckDB instead
120
+ of forcing it to False. Default False preserves the existing defense:
121
+ read_only=True always implies enable_external_access=False.
122
+
123
+ This flag is for local-dev surfaces (e.g. the playground server) that
124
+ need BOTH a non-exclusive file lock (for coexistence with concurrent
125
+ dft render / Cursor preview processes) AND external file access
126
+ (read_csv, read_json_auto). It must NOT be set in multi-tenant or
127
+ deployed surfaces. Approved callsites are listed on
128
+ LOCAL_AUTHORING_REGISTRY_KWARGS in adapter_registry.py
129
+ (playground, MCP, chat, serve).
130
+ """
131
+ self.dbt_project_path = Path(dbt_project_path) if dbt_project_path else None
132
+ self.use_example_db = use_example_db
133
+ self.connection_string = connection_string or ":memory:"
134
+ self.project_root = project_root
135
+ self.profile_type = profile_type
136
+ self.read_only = read_only
137
+ self.allow_external_access_in_readonly = allow_external_access_in_readonly
138
+ self._duckdb_config = duckdb_config
139
+ self._connection: Any = None # Lazy-loaded DuckDB connection (default)
140
+ # Thread-local storage for per-source DuckDB connection caches.
141
+ self._tls = threading.local()
142
+ # Track all connections opened across all threads so close() can clean up.
143
+ self._all_conns: list[Any] = []
144
+ self._all_conns_lock = threading.Lock()
145
+ self._manifest: dict[str, Any] | None = None # Lazy-loaded dbt manifest
146
+ self._manifest_from_snapshot: bool = False
147
+ self._prod_manifest: dict[str, Any] | None = None
148
+ self._prod_manifest_loaded: bool = False
149
+ self._dialect = get_dialect(profile_type)
150
+
151
+ @property
152
+ def supported_types(self) -> set[str]:
153
+ """Return supported query types."""
154
+ return {"sql"}
155
+
156
+ def _can_execute(self, query: AnyQuery) -> bool:
157
+ """Check if this adapter can execute the query.
158
+
159
+ DbtAdapter owns: SQL with dbt jinja and no source (needs manifest resolution).
160
+ SqlAdapter owns: everything else — plain SQL, or any query with a source.
161
+ """
162
+ if not is_sql_query(query):
163
+ return False
164
+ # Dbt-jinja with no source → DbtAdapter's territory (manifest resolution needed)
165
+ return not (has_dbt_jinja(query.sql) and query.source is None)
166
+
167
+ def _execute(
168
+ self,
169
+ query: AnyQuery,
170
+ variables: VariableValues | None = None,
171
+ params: QueryParams = None,
172
+ source_config: SourceConfig | None = None,
173
+ ) -> QueryResult:
174
+ """Execute a SQL query.
175
+
176
+ Uses parameterized queries (DuckDB) or safe literal inlining (dbt adapters).
177
+
178
+ Args:
179
+ query: AnyQuery object (SqlQuery expected)
180
+ variables: Variable values for Jinja resolution
181
+ params: Optional pre-computed parameter values.
182
+ source_config: Typed source config from SourceResolver. When None,
183
+ the adapter uses its default connection and configured profile_type.
184
+
185
+ Returns:
186
+ QueryResult with data or error
187
+ """
188
+ if not is_sql_query(query):
189
+ return QueryResult(
190
+ data=[],
191
+ error=f"Expected SQL query, got {query.query_type}",
192
+ )
193
+
194
+ sql = query.sql
195
+ resolved_relations: list[ResolvedRelation] = []
196
+
197
+ if self.dbt_project_path:
198
+ sql, resolved_relations = self._resolve_dbt_sql(sql)
199
+
200
+ # Resolver-provided source_config is the single source of truth. When
201
+ # absent, the adapter falls through to its own default connection using
202
+ # the configured profile_type.
203
+ raw_config: dict[str, Any] | None = (
204
+ source_config.model_dump(by_alias=True)
205
+ if source_config is not None
206
+ else None
207
+ )
208
+ dialect_name: str = (
209
+ source_config.type if source_config is not None else self.profile_type
210
+ )
211
+
212
+ # Resolve setup_sql Jinja before branching on dialect.
213
+ resolved_setup_sql: str | None = None
214
+ if query.setup_sql:
215
+ resolved = self._resolve_setup_sql(query.setup_sql, variables)
216
+ if isinstance(resolved, QueryResult):
217
+ return resolved
218
+ resolved_setup_sql = resolved
219
+
220
+ if params is not None:
221
+ resolved_sql = sql
222
+ resolved_params = params
223
+ else:
224
+ try:
225
+ parameterized = render_parameterized(
226
+ sql,
227
+ variables=variables or {},
228
+ dialect=get_dialect(dialect_name),
229
+ )
230
+ resolved_sql = parameterized.sql
231
+ resolved_params = parameterized.params
232
+ except (ValueError, KeyError, TypeError) as e:
233
+ return handle_adapter_error("SQL parameterization", e)
234
+
235
+ if query.filters:
236
+ try:
237
+ dialect_obj = get_dialect(dialect_name)
238
+ injected_sql, filter_params = inject_filters(
239
+ resolved_sql,
240
+ query.filters,
241
+ variables or {},
242
+ dialect_obj,
243
+ param_offset=len(resolved_params),
244
+ )
245
+ resolved_sql = injected_sql
246
+ resolved_params = list(resolved_params) + filter_params
247
+ except (ValueError, KeyError, TypeError) as e:
248
+ return handle_adapter_error("filter injection", e)
249
+
250
+ if self.use_example_db or dialect_name == "duckdb":
251
+ # Execute DuckDB setup_sql before main query on the same connection.
252
+ if resolved_setup_sql:
253
+ err = self._execute_setup_sql_duckdb(resolved_setup_sql, raw_config)
254
+ if err is not None:
255
+ return err
256
+ result = self._execute_duckdb(
257
+ resolved_sql, resolved_params, query, raw_config
258
+ )
259
+ else:
260
+ result = self._execute_via_dbt_adapter(
261
+ resolved_sql,
262
+ resolved_params,
263
+ query,
264
+ raw_config,
265
+ dialect_name,
266
+ setup_sql=resolved_setup_sql,
267
+ )
268
+
269
+ if resolved_relations:
270
+ result.resolved_relations = resolved_relations
271
+
272
+ return result
273
+
274
+ def _resolve_setup_sql(
275
+ self,
276
+ setup_sql: str,
277
+ variables: VariableValues | None,
278
+ ) -> str | QueryResult:
279
+ """Resolve Jinja in setup_sql. Returns resolved string or a QueryResult on error."""
280
+ from dataface.core.compile.jinja import resolve_jinja_template
281
+
282
+ try:
283
+ return resolve_jinja_template(setup_sql, variables=variables or {})
284
+ except Exception as e: # noqa: BLE001
285
+ return handle_adapter_error("setup_sql template resolution", e)
286
+
287
+ def _execute_setup_sql_duckdb(
288
+ self,
289
+ resolved_setup_sql: str,
290
+ source_config: dict[str, Any] | None = None,
291
+ ) -> QueryResult | None:
292
+ """Execute setup_sql on a DuckDB connection. Returns None on success."""
293
+ try:
294
+ validate_setup_sql(resolved_setup_sql, dialect="duckdb")
295
+ with _DUCKDB_CWD_LOCK:
296
+ duckdb_conn = self._get_duckdb_connection_for_query(source_config)
297
+ duckdb_conn.execute(resolved_setup_sql)
298
+ except Exception as e: # noqa: BLE001
299
+ return handle_adapter_error("setup_sql execution", e)
300
+ return None
301
+
302
+ def _execute_duckdb(
303
+ self,
304
+ sql: str,
305
+ params: list[Any],
306
+ query: AnyQuery,
307
+ source_config: dict[str, Any] | None = None,
308
+ ) -> QueryResult:
309
+ """Execute SQL query using DuckDB with parameterized execution.
310
+
311
+ Uses parameterized queries to prevent SQL injection attacks.
312
+ Parameters are passed separately from the SQL string.
313
+ """
314
+ import importlib.util
315
+ import os
316
+
317
+ if importlib.util.find_spec("duckdb") is None:
318
+ return QueryResult(
319
+ data=[],
320
+ error="DuckDB not installed - install with: pip install duckdb",
321
+ )
322
+
323
+ try:
324
+ validate_select_only(sql, dialect="duckdb")
325
+ with _DUCKDB_CWD_LOCK:
326
+ conn = self._get_duckdb_connection_for_query(source_config)
327
+
328
+ original_cwd = os.getcwd()
329
+ if self.project_root:
330
+ os.chdir(self.project_root)
331
+
332
+ try:
333
+ result = conn.execute(sql, params or [])
334
+ columns = (
335
+ [desc[0] for desc in result.description]
336
+ if result.description
337
+ else []
338
+ )
339
+ rows = result.fetchall()
340
+ finally:
341
+ os.chdir(original_cwd)
342
+
343
+ if query.limit and query.limit > 0:
344
+ rows = rows[: query.limit]
345
+
346
+ data = [dict(zip(columns, row, strict=False)) for row in rows]
347
+ col_descs = (
348
+ {desc[0]: tuple(desc) for desc in result.description}
349
+ if result.description
350
+ else None
351
+ )
352
+
353
+ return QueryResult(
354
+ data=data, columns=columns, column_descriptions=col_descs
355
+ )
356
+
357
+ except Exception as e: # noqa: BLE001
358
+ return handle_adapter_error("DuckDB SQL execution", e)
359
+
360
+ @staticmethod
361
+ def _push_limit_into_sql(sql: str, limit: int) -> str:
362
+ """Wrap sql with an outer LIMIT N.
363
+
364
+ Always wraps — the outer LIMIT bounds the inner one when both are present,
365
+ yielding the smaller of the two values (safe and idempotent). Checking for
366
+ 'limit' in sql.lower() is brittle: it matches column names (limit_amount),
367
+ CTEs, comments, and string literals, causing silent full-table scans on
368
+ large tables when the wrapping is skipped.
369
+
370
+ Trailing semicolons are stripped before wrapping — they appear in SQL pasted
371
+ from IDEs and would produce a parse error inside the subquery on BigQuery,
372
+ Snowflake, Redshift, and Postgres.
373
+ """
374
+ stripped = sql.rstrip().rstrip(";").rstrip()
375
+ return f"SELECT * FROM ({stripped}) AS _dft_limit_wrap LIMIT {limit}"
376
+
377
+ def _execute_via_dbt_adapter(
378
+ self,
379
+ sql: str,
380
+ params: list[Any],
381
+ query: AnyQuery,
382
+ source_config: dict[str, Any] | None,
383
+ dialect_name: str,
384
+ setup_sql: str | None = None,
385
+ ) -> QueryResult:
386
+ """Execute SQL via the dbt adapter for non-DuckDB warehouses.
387
+
388
+ Params are inlined as SQL literals — dbt adapter.execute() only accepts
389
+ plain SQL. This is safe because params come from compiled dashboard YAML
390
+ variables, not raw user input.
391
+
392
+ For dialects that return the full result set (e.g. BigQuery), query.limit
393
+ is pushed into the SQL string before execution to avoid full-table scans.
394
+
395
+ setup_sql (if any) runs inside the same connection_named("dataface_query")
396
+ block as the main query so temp functions/tables are visible to the query.
397
+ BigQuery CREATE TEMP FUNCTION is session-scoped — two connection_named calls
398
+ produce two distinct connections and the temp function would not be visible.
399
+ """
400
+ from dataface.core.execute.adapters.dbt_adapter_factory import build_adapter
401
+
402
+ if source_config is None:
403
+ return QueryResult(
404
+ data=[],
405
+ error=(
406
+ f"No source config found for dialect '{dialect_name}'. "
407
+ f"Provide an inline source: block or a named source in _sources.yaml."
408
+ ),
409
+ )
410
+
411
+ try:
412
+ param_list = list(params) if params else []
413
+ # The dbt adapter.execute() accepts only plain SQL — no bound params.
414
+ # Inline params as SQL literals before passing the SQL string.
415
+ # Named-param dialects (BigQuery @paramN, Databricks :paramN, SQL
416
+ # Server @pN) use inline_dialect_params with the dialect's param()
417
+ # method so each placeholder format is matched correctly.
418
+ # All other dialects use $N positional placeholders.
419
+ dialect_obj = get_dialect(dialect_name)
420
+ if dialect_obj.uses_named_params:
421
+ inlined_sql = _inline_dialect_params(sql, param_list, dialect_obj.param)
422
+ elif dialect_obj.param(1) == "?":
423
+ inlined_sql = _inline_qmark_params(sql, param_list)
424
+ else:
425
+ inlined_sql = _inline_params(sql, param_list)
426
+
427
+ # Push LIMIT into the SQL for all non-DuckDB dialects. dbt-adapters'
428
+ # execute(..., fetch=True) materializes the full agate.Table in memory
429
+ # before returning — Python slicing after the fact pulls the entire
430
+ # result set over the wire. DuckDB is excluded because it executes
431
+ # locally and its native cursor handles LIMIT correctly without rewriting.
432
+ if dialect_name != "duckdb" and query.limit and query.limit > 0:
433
+ inlined_sql = self._push_limit_into_sql(inlined_sql, query.limit)
434
+
435
+ # Validate setup_sql + main SQL separately (different allowlists)
436
+ # before opening the dbt connection — fail-closed if either piece
437
+ # contains mutating SQL. validate_setup_sql also runs in the DuckDB
438
+ # setup path; running it here too is the correct re-check for the
439
+ # combined-script branch since the strings hit a different driver.
440
+ if setup_sql:
441
+ validate_setup_sql(setup_sql, dialect=dialect_name)
442
+ validate_select_only(inlined_sql, dialect=dialect_name)
443
+
444
+ adapter = build_adapter(source_config)
445
+ with adapter.connection_named("dataface_query"):
446
+ if setup_sql:
447
+ # BigQuery rejects standalone "CREATE TEMP FUNCTION" — it
448
+ # must appear in the same script as a query that uses
449
+ # (or at least follows) it. Sending setup_sql in a separate
450
+ # execute() call also drops session-scoped TEMP entities
451
+ # before the main query runs. Concatenate so dbt sends them
452
+ # as one multi-statement script.
453
+ combined_sql = setup_sql.rstrip().rstrip(";") + ";\n" + inlined_sql
454
+ _, table = adapter.execute(
455
+ combined_sql, auto_begin=True, fetch=True
456
+ )
457
+ else:
458
+ _, table = adapter.execute(inlined_sql, auto_begin=True, fetch=True)
459
+
460
+ columns = [col.lower() for col in table.column_names]
461
+ rows = list(table.rows)
462
+
463
+ # Python-side limit as safety net for non-BQ dialects
464
+ if query.limit and query.limit > 0:
465
+ rows = rows[: query.limit]
466
+
467
+ data = [dict(zip(columns, row, strict=False)) for row in rows]
468
+ return QueryResult(data=data, columns=columns)
469
+
470
+ except Exception as e: # noqa: BLE001
471
+ return handle_adapter_error(f"{dialect_name} SQL execution", e)
472
+
473
+ def _get_duckdb_connection_for_query(
474
+ self, source_config: dict[str, Any] | None = None
475
+ ) -> Any:
476
+ """Get DuckDB connection for a query.
477
+
478
+ Uses the typed ``source_config`` (resolver-provided) when it points at a
479
+ DuckDB source; otherwise falls through to the adapter's default
480
+ connection. The thread-local connection cache is keyed by
481
+ ``source_config['path']`` — two named DuckDB sources that point at the
482
+ same path share a connection. For ``:memory:`` this means two distinct
483
+ named sources both using ``:memory:`` collapse into one in-memory DB;
484
+ callers needing isolated in-memory DBs must use distinct paths (e.g.
485
+ ``:memory:db_a``, ``:memory:db_b``) or different connection_strings.
486
+ """
487
+ if source_config and source_config.get("type") == "duckdb":
488
+ schema = source_config.get("schema") or ""
489
+ path = str(source_config.get("path") or ":memory:")
490
+ cache_key = f"{path}\0{schema}" if schema else path
491
+ return self._create_duckdb_connection_from_config(source_config, cache_key)
492
+ return self._get_duckdb_connection()
493
+
494
+ def _resolve_duckdb_config(
495
+ self, source_config: dict[str, Any]
496
+ ) -> dict[str, Any] | None:
497
+ """Resolve DuckDB config: source-level overrides adapter-level default."""
498
+ return normalize_duckdb_config(source_config) or self._duckdb_config
499
+
500
+ def _create_duckdb_connection_from_config(
501
+ self, source_config: dict[str, Any], cache_key: str
502
+ ) -> Any:
503
+ """Create or retrieve a cached DuckDB connection from source config."""
504
+ import duckdb
505
+
506
+ if "database" in source_config and "path" not in source_config:
507
+ raise ValueError(
508
+ "DuckDB source_config uses the unsupported 'database' key. "
509
+ 'Use \'path\' instead (e.g. {"type": "duckdb", "path": "db.duckdb"}).'
510
+ )
511
+ db_path = source_config.get("path", ":memory:")
512
+ raw_config = self._resolve_duckdb_config(source_config)
513
+ duckdb_config = raw_config or None
514
+
515
+ sources = getattr(self._tls, "sources", None)
516
+ if sources is None:
517
+ sources = self._tls.sources = {}
518
+
519
+ if cache_key not in sources:
520
+ resolved_path = db_path
521
+ if (
522
+ self.project_root
523
+ and db_path != ":memory:"
524
+ and not Path(db_path).is_absolute()
525
+ ):
526
+ resolved_path = str(self.project_root / db_path)
527
+
528
+ conn = duckdb.connect(
529
+ resolved_path,
530
+ **self._resolve_duckdb_connect_kwargs(
531
+ resolved_path,
532
+ self.read_only,
533
+ duckdb_config,
534
+ self.allow_external_access_in_readonly,
535
+ ),
536
+ )
537
+ schema = source_config.get("schema") or ""
538
+ if schema:
539
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", schema):
540
+ raise ValueError(
541
+ f"Invalid schema name {schema!r}: must match "
542
+ r"^[A-Za-z_][A-Za-z0-9_]*$"
543
+ )
544
+ conn.execute("SET search_path = ?", [f"{schema},main"])
545
+ sources[cache_key] = conn
546
+ with self._all_conns_lock:
547
+ self._all_conns.append(conn)
548
+
549
+ return sources[cache_key]
550
+
551
+ def close(self) -> None:
552
+ """Close all database connections."""
553
+ if self._connection:
554
+ self._connection.close()
555
+ self._connection = None
556
+
557
+ with self._all_conns_lock:
558
+ conns, self._all_conns = self._all_conns, []
559
+ for conn in conns:
560
+ conn.close()
561
+
562
+ if hasattr(self._tls, "sources"):
563
+ self._tls.sources = {}
564
+
565
+ @staticmethod
566
+ def _resolve_duckdb_connect_kwargs(
567
+ path: str,
568
+ read_only: bool,
569
+ duckdb_config: dict[str, Any] | None,
570
+ allow_external_access_in_readonly: bool = False,
571
+ ) -> dict[str, Any]:
572
+ """Build duckdb.connect() kwargs for the given path and security posture.
573
+
574
+ When read_only=True and allow_external_access_in_readonly=False (default):
575
+ - Adds read_only=True for file-based paths (not :memory: — in-memory DuckDB
576
+ must always be opened read-write; this is a known exception).
577
+ - Forces enable_external_access=False in the config, overriding any
578
+ user-supplied duckdb_config value. This is the defense-in-depth contract:
579
+ no quiet bypass via config when the adapter is set to read-only.
580
+
581
+ When read_only=True and allow_external_access_in_readonly=True:
582
+ - Same read_only=True driver flag for file-based paths (non-exclusive lock).
583
+ - enable_external_access is NOT forced to False; caller's duckdb_config value
584
+ passes through. This is the deliberate local-dev opt-in for playground,
585
+ MCP, chat, and serve: non-exclusive lock + external file reads. Approved
586
+ callsites are listed on LOCAL_AUTHORING_REGISTRY_KWARGS in adapter_registry.py.
587
+
588
+ When read_only=False:
589
+ - No read_only driver flag.
590
+ - duckdb_config is passed through as-is.
591
+ """
592
+ kwargs: dict[str, Any] = {}
593
+ if read_only and path != ":memory:":
594
+ kwargs["read_only"] = True
595
+ if duckdb_config is not None or read_only:
596
+ config = dict(duckdb_config or {})
597
+ if read_only and not (
598
+ allow_external_access_in_readonly
599
+ and config.get("enable_external_access") is True
600
+ ):
601
+ config["enable_external_access"] = False
602
+ if config:
603
+ kwargs["config"] = config
604
+ return kwargs
605
+
606
+ def _get_duckdb_connection(self) -> Any:
607
+ """Get or create the default DuckDB connection."""
608
+ if self._connection is None:
609
+ import duckdb
610
+
611
+ if self.use_example_db:
612
+ self._connection = duckdb.connect(":memory:")
613
+ elif self.dbt_project_path:
614
+ from dataface.core.execute.adapters.dbt_utils import (
615
+ DBT_PROJECT_DB_NAMES,
616
+ )
617
+
618
+ conn = None
619
+ for db_name in DBT_PROJECT_DB_NAMES:
620
+ db_path = self.dbt_project_path / "data" / db_name
621
+ if db_path.exists():
622
+ conn = duckdb.connect(
623
+ str(db_path),
624
+ **self._resolve_duckdb_connect_kwargs(
625
+ str(db_path),
626
+ self.read_only,
627
+ self._duckdb_config,
628
+ self.allow_external_access_in_readonly,
629
+ ),
630
+ )
631
+ break
632
+
633
+ if conn is None:
634
+ conn = duckdb.connect(
635
+ self.connection_string,
636
+ **self._resolve_duckdb_connect_kwargs(
637
+ self.connection_string,
638
+ self.read_only,
639
+ self._duckdb_config,
640
+ self.allow_external_access_in_readonly,
641
+ ),
642
+ )
643
+
644
+ self._connection = conn
645
+ else:
646
+ self._connection = duckdb.connect(
647
+ self.connection_string,
648
+ **self._resolve_duckdb_connect_kwargs(
649
+ self.connection_string,
650
+ self.read_only,
651
+ self._duckdb_config,
652
+ self.allow_external_access_in_readonly,
653
+ ),
654
+ )
655
+
656
+ return self._connection
657
+
658
+ def _get_manifest(self) -> dict[str, Any] | None:
659
+ """Load dbt manifest.json if available."""
660
+ if self._manifest is not None:
661
+ return self._manifest
662
+ if not self.dbt_project_path:
663
+ return None
664
+
665
+ target_path = self.dbt_project_path / "target" / "manifest.json"
666
+ snapshot_path = self.dbt_project_path / "manifest.snapshot.json"
667
+
668
+ from dataface.core.execute.adapters.dbt_utils import load_dbt_manifest
669
+
670
+ self._manifest = load_dbt_manifest(self.dbt_project_path)
671
+ self._manifest_from_snapshot = (
672
+ self._manifest is not None
673
+ and not target_path.exists()
674
+ and snapshot_path.exists()
675
+ )
676
+ return self._manifest
677
+
678
+ def _get_prod_manifest(self) -> dict[str, Any] | None:
679
+ """Load the committed prod manifest snapshot, if available."""
680
+ if self._prod_manifest_loaded:
681
+ return self._prod_manifest
682
+ self._prod_manifest_loaded = True
683
+ if not self.dbt_project_path:
684
+ return None
685
+ if self._manifest_from_snapshot:
686
+ return None
687
+ snapshot_path = self.dbt_project_path / "manifest.snapshot.json"
688
+ if not snapshot_path.exists():
689
+ return None
690
+ import json
691
+
692
+ try:
693
+ with open(snapshot_path) as f:
694
+ self._prod_manifest = json.load(f)
695
+ except (OSError, json.JSONDecodeError) as e:
696
+ logger.debug("Failed to load prod manifest snapshot: %s", e)
697
+ return self._prod_manifest
698
+
699
+ def _resolve_dbt_sql(self, sql: str) -> tuple[str, list[ResolvedRelation]]:
700
+ """Resolve SQL with dbt-specific Jinja functions (ref, source)."""
701
+ from dataface.core.execute.adapters.dbt_utils import (
702
+ resolve_dbt_refs_with_provenance,
703
+ )
704
+
705
+ manifest = self._get_manifest()
706
+ if not manifest:
707
+ return sql, []
708
+
709
+ prod_manifest = self._get_prod_manifest()
710
+ return resolve_dbt_refs_with_provenance(sql, manifest, prod_manifest)