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,610 @@
1
+ """External MCP client: discover and connect to user-configured MCP servers.
2
+
3
+ On `dft chat` start, reads the user's existing MCP client configs (same files
4
+ that `dft init mcp` writes), filters out the dft self-entry, spawns the remaining
5
+ servers over stdio using the `mcp` Python client, and merges their tools into the
6
+ agent's tool list namespaced as `<server>__<tool>` (double underscore, same convention
7
+ as Claude Code's MCP client).
8
+
9
+ Lifecycle: `ExternalMCPManager` is created once per `dft chat` session, opened
10
+ before the chat loop starts, and closed in the CLI's top-level finally block.
11
+ Per-session is mandatory because dbt MCP cold-starts in 5–10s; per-message
12
+ respawn would make the agent unusable.
13
+
14
+ Async/sync bridge: a single asyncio event loop lives on a background thread.
15
+ `call_tool` bridges to it via `asyncio.run_coroutine_threadsafe(...).result()`.
16
+ Never use `asyncio.run()` here — it tears down the loop and terminates the long-
17
+ lived stdio subprocess sessions.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import json
24
+ import re
25
+ import sys
26
+ import threading
27
+ import time
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ if sys.version_info >= (3, 11):
33
+ import tomllib
34
+ else:
35
+ import tomli as tomllib
36
+
37
+ import mcp
38
+ from mcp import ClientSession
39
+ from mcp.client.stdio import StdioServerParameters, stdio_client
40
+ from rich.console import Console
41
+
42
+ _console = Console(stderr=True)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Data types
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ @dataclass
51
+ class ServerConfig:
52
+ """Parsed MCP server entry from a client config file."""
53
+
54
+ name: str
55
+ command: str
56
+ args: list[str] = field(default_factory=list)
57
+ env: dict[str, str] | None = None
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Self-MCP detection
62
+ # ---------------------------------------------------------------------------
63
+
64
+ _SELF_BASENAMES = {"dft", "dataface"}
65
+ _UV_BASENAMES = {"uvx", "uv"}
66
+ # Match python, python3, python3.10, python3.13, etc. — but not `pythonista` or similar
67
+ _PYTHON_RE = re.compile(r"python\d*(\.\d+)?$")
68
+
69
+
70
+ def is_self_mcp(cfg: ServerConfig) -> bool:
71
+ """Return True if cfg points at dft's own MCP server.
72
+
73
+ Patterns detected:
74
+ 1. command basename in {dft, dataface} AND args[:2] == ["mcp", "serve"]
75
+ 2. command basename in {uvx, uv} AND args contains a dataface/dft token
76
+ followed eventually by "mcp" "serve"
77
+ 3. command is a Python interpreter AND args invoke -m dataface/-m dft
78
+ followed by "mcp" "serve"
79
+ """
80
+ basename = Path(cfg.command).name
81
+ args = cfg.args
82
+
83
+ # Pattern 1: dft/dataface directly
84
+ if basename in _SELF_BASENAMES:
85
+ return _has_mcp_serve(args)
86
+
87
+ # Pattern 2: uvx / uv run dataface mcp serve
88
+ # Require the three tokens to be immediately adjacent: <dft|dataface> mcp serve
89
+ if basename in _UV_BASENAMES:
90
+ for i, token in enumerate(args):
91
+ if token in _SELF_BASENAMES:
92
+ return (
93
+ i + 2 < len(args)
94
+ and args[i + 1] == "mcp"
95
+ and args[i + 2] == "serve"
96
+ )
97
+ return False
98
+
99
+ # Pattern 3: python / python3 / python3.13 -m dataface mcp serve
100
+ # Require the three tokens immediately: <dft|dataface> mcp serve (no flags between)
101
+ if _PYTHON_RE.match(basename):
102
+ try:
103
+ m_idx = args.index("-m")
104
+ except ValueError:
105
+ return False
106
+ module_args = args[m_idx + 1 :]
107
+ if not module_args:
108
+ return False
109
+ if module_args[0] in _SELF_BASENAMES:
110
+ return (
111
+ len(module_args) >= 3
112
+ and module_args[1] == "mcp"
113
+ and module_args[2] == "serve"
114
+ )
115
+ return False
116
+
117
+ return False
118
+
119
+
120
+ def _has_mcp_serve(args: list[str]) -> bool:
121
+ """True if args contains 'mcp' immediately followed by 'serve' anywhere."""
122
+ for i in range(len(args) - 1):
123
+ if args[i] == "mcp" and args[i + 1] == "serve":
124
+ return True
125
+ return False
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Config discovery
130
+ # ---------------------------------------------------------------------------
131
+
132
+ # (relative_path, servers_key) for each JSON-based client config
133
+ _JSON_CLIENT_CONFIGS: list[tuple[Path, str]] = [
134
+ (Path(".cursor/mcp.json"), "mcpServers"),
135
+ (Path(".mcp.json"), "mcpServers"), # claude-code
136
+ (Path(".vscode/mcp.json"), "servers"),
137
+ (Path(".github/copilot/mcp.json"), "servers"),
138
+ ]
139
+
140
+ # (relative_path, servers_key) for each TOML-based client config
141
+ _TOML_CLIENT_CONFIGS: list[tuple[Path, str]] = [
142
+ (Path(".codex/config.toml"), "mcp_servers"), # OpenAI Codex CLI
143
+ ]
144
+
145
+ # Absolute paths (global configs, not project-relative).
146
+ # These mirror the paths that `dft init mcp` writes, plus vendor-specific locations.
147
+ #
148
+ # ~/.cursor/mcp.json — Cursor global MCP config
149
+ # ~/.config/claude/config.json — written by `dft init mcp claude` (Claude Code/CLI)
150
+ # ~/Library/Application Support/Claude/claude_desktop_config.json — macOS Claude Desktop
151
+ _GLOBAL_JSON_CONFIGS: list[tuple[Path, str]] = [
152
+ (Path.home() / ".cursor" / "mcp.json", "mcpServers"),
153
+ (Path.home() / ".config" / "claude" / "config.json", "mcpServers"),
154
+ ]
155
+ if sys.platform == "darwin":
156
+ _GLOBAL_JSON_CONFIGS.append(
157
+ (
158
+ Path.home()
159
+ / "Library"
160
+ / "Application Support"
161
+ / "Claude"
162
+ / "claude_desktop_config.json",
163
+ "mcpServers",
164
+ )
165
+ )
166
+
167
+
168
+ def discover_servers(
169
+ config_dirs: list[Path] | None = None,
170
+ *,
171
+ global_configs: list[tuple[Path, str]] | None = None,
172
+ ) -> list[ServerConfig]:
173
+ """Read user's MCP configs, return external server configs (self filtered out).
174
+
175
+ Args:
176
+ config_dirs: directories to search for relative configs (default: [Path.cwd()])
177
+ global_configs: absolute-path configs to search (default: _GLOBAL_JSON_CONFIGS).
178
+ Pass [] in tests to prevent reading the developer's home directory.
179
+ """
180
+ search_dirs = config_dirs if config_dirs is not None else [Path.cwd()]
181
+ effective_globals = (
182
+ _GLOBAL_JSON_CONFIGS if global_configs is None else global_configs
183
+ )
184
+ seen_names: set[str] = set()
185
+ results: list[ServerConfig] = []
186
+
187
+ for search_dir in search_dirs:
188
+ for rel_path, servers_key in _JSON_CLIENT_CONFIGS:
189
+ cfg_path = search_dir / rel_path
190
+ _parse_json_config(cfg_path, servers_key, seen_names, results)
191
+ for rel_path, servers_key in _TOML_CLIENT_CONFIGS:
192
+ cfg_path = search_dir / rel_path
193
+ _parse_toml_config(cfg_path, servers_key, seen_names, results)
194
+
195
+ for cfg_path, servers_key in effective_globals:
196
+ _parse_json_config(cfg_path, servers_key, seen_names, results)
197
+
198
+ return results
199
+
200
+
201
+ def _accept_server(
202
+ name: str,
203
+ cfg: ServerConfig,
204
+ seen_names: set[str],
205
+ results: list[ServerConfig],
206
+ ) -> None:
207
+ """Validate and register a ServerConfig if it passes all guards."""
208
+ if "__" in name:
209
+ _console.print(
210
+ f'[yellow]MCP: skipping "{name}" — server names must not contain __ '
211
+ f"(would break tool-name routing)[/yellow]"
212
+ )
213
+ return
214
+ if name in seen_names:
215
+ return # first config wins
216
+ if is_self_mcp(cfg):
217
+ # Claim the name so a later global config with the same name cannot sneak in
218
+ # a non-dft server that happens to share the self-entry's name.
219
+ seen_names.add(name)
220
+ _console.print(
221
+ f'[dim]MCP: skipping self-entry "{name}" (using in-process tools)[/dim]'
222
+ )
223
+ return
224
+ seen_names.add(name)
225
+ results.append(cfg)
226
+
227
+
228
+ def _parse_json_config(
229
+ path: Path,
230
+ servers_key: str,
231
+ seen_names: set[str],
232
+ results: list[ServerConfig],
233
+ ) -> None:
234
+ if not path.exists():
235
+ return
236
+ try:
237
+ data = json.loads(path.read_text())
238
+ except json.JSONDecodeError:
239
+ _console.print(f"[yellow]MCP: {path} has invalid JSON; skipping[/yellow]")
240
+ return
241
+ except OSError as exc:
242
+ _console.print(f"[yellow]MCP: could not read {path}: {exc}; skipping[/yellow]")
243
+ return
244
+ if not isinstance(data, dict):
245
+ return
246
+ servers = data.get(servers_key, {})
247
+ if not isinstance(servers, dict):
248
+ return
249
+ for name, entry in servers.items():
250
+ if not isinstance(entry, dict):
251
+ continue
252
+ command = entry.get("command", "")
253
+ if not command:
254
+ continue
255
+ cfg = ServerConfig(
256
+ name=name,
257
+ command=command,
258
+ args=entry.get("args") or [],
259
+ env=entry.get("env"),
260
+ )
261
+ _accept_server(name, cfg, seen_names, results)
262
+
263
+
264
+ def _parse_toml_config(
265
+ path: Path,
266
+ servers_key: str,
267
+ seen_names: set[str],
268
+ results: list[ServerConfig],
269
+ ) -> None:
270
+ """Parse a TOML-format MCP config (Codex CLI's config.toml)."""
271
+ if not path.exists():
272
+ return
273
+ try:
274
+ data = tomllib.loads(path.read_text())
275
+ except (tomllib.TOMLDecodeError, OSError) as exc:
276
+ _console.print(
277
+ f"[yellow]MCP: {path} has invalid TOML: {exc}; skipping[/yellow]"
278
+ )
279
+ return
280
+ servers = data.get(servers_key, {})
281
+ if not isinstance(servers, dict):
282
+ return
283
+ for name, entry in servers.items():
284
+ if not isinstance(entry, dict):
285
+ continue
286
+ command = entry.get("command", "")
287
+ if not command:
288
+ continue
289
+ cfg = ServerConfig(
290
+ name=name,
291
+ command=command,
292
+ args=entry.get("args") or [],
293
+ env=entry.get("env"),
294
+ )
295
+ _accept_server(name, cfg, seen_names, results)
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Tool namespacing helpers
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ def _namespace_tool(server_name: str, tool: dict[str, Any]) -> dict[str, Any]:
304
+ """Rewrite tool name to <server>__<tool>.
305
+
306
+ Uses `__` as the separator — matches Claude Code's MCP convention and avoids
307
+ `:`, which both Anthropic and OpenAI APIs reject in tool names. Built-in
308
+ Dataface tool names do not contain `__`, so no collision is possible.
309
+ """
310
+ return {**tool, "name": f"{server_name}__{tool['name']}"}
311
+
312
+
313
+ def _tools_for_config(server_name: str, mcp_tools: list[Any]) -> list[dict[str, Any]]:
314
+ """Convert mcp.types.Tool list to canonical tool dicts with namespaced names."""
315
+ result = []
316
+ for t in mcp_tools:
317
+ tool_dict = {
318
+ "name": t.name,
319
+ "description": t.description or "",
320
+ "input_schema": t.inputSchema,
321
+ }
322
+ result.append(_namespace_tool(server_name, tool_dict))
323
+ return result
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Per-server holder: persistent async task that keeps context managers alive
328
+ # ---------------------------------------------------------------------------
329
+
330
+
331
+ class _ServerHolder:
332
+ """Runs a long-lived MCP session inside a persistent asyncio task.
333
+
334
+ The MCP stdio_client and ClientSession use anyio context managers that must
335
+ be entered AND exited within the same task. This holder runs a coroutine on
336
+ the background loop that keeps the `async with` blocks open for the session's
337
+ lifetime and processes call_tool requests via a shared Queue.
338
+ """
339
+
340
+ def __init__(self, name: str) -> None:
341
+ self.name = name
342
+ self.tools: list[dict[str, Any]] = []
343
+ # Set once the session is ready (or failed)
344
+ self._ready_event = threading.Event()
345
+ self._error: Exception | None = None
346
+ # Async queue: each item is (tool_name, args, threading.Event, result_box)
347
+ # None is the shutdown sentinel.
348
+ self._call_queue: (
349
+ asyncio.Queue[tuple[str, dict[str, Any], threading.Event, list[Any]] | None]
350
+ | None
351
+ ) = None
352
+ self._loop: asyncio.AbstractEventLoop | None = None
353
+
354
+ def error(self) -> Exception | None:
355
+ """Return the startup error, if any. Thread-safe after _ready_event is set."""
356
+ return self._error
357
+
358
+ def wait_ready(self, timeout: float) -> bool:
359
+ """Block until ready or timed out. Returns True if connected with no error."""
360
+ self._ready_event.wait(timeout=timeout)
361
+ return self.is_connected()
362
+
363
+ async def run(self, cfg: ServerConfig, startup_timeout: float) -> None:
364
+ """Persistent coroutine: open the MCP session, signal ready, service calls.
365
+
366
+ The startup_timeout applies only to initialize + tools/list. The service
367
+ loop has no timeout — it runs until the shutdown sentinel is received.
368
+ """
369
+ self._loop = asyncio.get_running_loop()
370
+ self._call_queue = asyncio.Queue()
371
+
372
+ params = StdioServerParameters(
373
+ command=cfg.command,
374
+ args=cfg.args,
375
+ env=cfg.env,
376
+ )
377
+ try:
378
+ async with stdio_client(params) as (read, write): # noqa: SIM117
379
+ async with ClientSession(read, write) as session:
380
+ # Apply timeout only to the startup phase — cannot combine these
381
+ # three async with blocks: stdio_client + ClientSession must stay
382
+ # alive for the whole session; the timeout wraps only startup.
383
+ await asyncio.wait_for(
384
+ session.initialize(), timeout=startup_timeout
385
+ )
386
+ tools_result = await asyncio.wait_for(
387
+ session.list_tools(), timeout=startup_timeout
388
+ )
389
+ self.tools = _tools_for_config(cfg.name, tools_result.tools)
390
+ self._ready_event.set()
391
+ # Service call requests until shutdown sentinel (no timeout)
392
+ while True:
393
+ item = await self._call_queue.get()
394
+ if item is None:
395
+ break
396
+ tool_name, arguments, done_event, result_box = item
397
+ try:
398
+ result = await session.call_tool(tool_name, arguments)
399
+ result_box.append(result)
400
+ except Exception as exc: # noqa: BLE001 — keep loop alive
401
+ result_box.append(exc)
402
+ done_event.set()
403
+ except (mcp.McpError, asyncio.TimeoutError, OSError, EOFError) as exc:
404
+ # Startup/connection failure — unblock waiters with the error recorded.
405
+ self._error = exc
406
+ self._ready_event.set()
407
+
408
+ def is_connected(self) -> bool:
409
+ """True if session started successfully (no error, ready event set)."""
410
+ return self._error is None and self._ready_event.is_set()
411
+
412
+ def call_tool_sync(
413
+ self, tool_name: str, arguments: dict[str, Any], timeout: float | None
414
+ ) -> Any:
415
+ """Enqueue a call_tool request and wait for the result (sync)."""
416
+ if self._call_queue is None or self._loop is None:
417
+ raise RuntimeError("Session not running")
418
+ done_event = threading.Event()
419
+ result_box: list[Any] = []
420
+ item = (tool_name, arguments, done_event, result_box)
421
+ self._loop.call_soon_threadsafe(self._call_queue.put_nowait, item)
422
+ if not done_event.wait(timeout=timeout):
423
+ raise TimeoutError(f"call_tool({tool_name!r}) timed out after {timeout}s")
424
+ result = result_box[0]
425
+ if isinstance(result, Exception):
426
+ raise result
427
+ return result
428
+
429
+ def shutdown(self) -> None:
430
+ if self._call_queue is not None and self._loop is not None:
431
+ self._loop.call_soon_threadsafe(self._call_queue.put_nowait, None)
432
+
433
+
434
+ # ---------------------------------------------------------------------------
435
+ # ExternalMCPManager — lifecycle + dispatch
436
+ # ---------------------------------------------------------------------------
437
+
438
+
439
+ class ExternalMCPManager:
440
+ """Manages external MCP server sessions for the lifetime of a dft chat session.
441
+
442
+ Usage::
443
+
444
+ manager = ExternalMCPManager(discover_servers())
445
+ manager.start(timeout=10.0) # parallel startup; prints status
446
+ # ...run agent with manager.all_tools() merged in...
447
+ manager.close() # in a finally block
448
+
449
+ Tool calls route via::
450
+
451
+ manager.call_tool("dbt__list_metrics", {"filter": "rev"})
452
+ """
453
+
454
+ def __init__(self, configs: list[ServerConfig]) -> None:
455
+ self._configs = configs
456
+ self._holders: dict[str, _ServerHolder] = {}
457
+ self._loop: asyncio.AbstractEventLoop | None = None
458
+ self._loop_thread: threading.Thread | None = None
459
+
460
+ # ------------------------------------------------------------------
461
+ # Startup
462
+ # ------------------------------------------------------------------
463
+
464
+ def start(self, timeout: float = 10.0) -> None:
465
+ """Spawn all servers in parallel and collect their tools.
466
+
467
+ Per-server timeout: `timeout` seconds for initialize + tools/list.
468
+ On failure, log a yellow warning and continue without that server.
469
+ """
470
+ if not self._configs:
471
+ return
472
+
473
+ self._loop = asyncio.new_event_loop()
474
+ self._loop_thread = threading.Thread(
475
+ target=self._loop.run_forever, daemon=True, name="dft-mcp-loop"
476
+ )
477
+ self._loop_thread.start()
478
+
479
+ names = ", ".join(c.name for c in self._configs)
480
+ _console.print(f"[dim]MCP: starting {names}...[/dim]")
481
+
482
+ # Launch one persistent task per server on the background loop.
483
+ for cfg in self._configs:
484
+ holder = _ServerHolder(cfg.name)
485
+ self._holders[cfg.name] = holder
486
+ asyncio.run_coroutine_threadsafe(
487
+ holder.run(cfg, startup_timeout=timeout), self._loop
488
+ )
489
+
490
+ # Wait for all servers in parallel — each holder has its own threading.Event.
491
+ # Use a shared deadline so servers that start fast don't wait for slow ones.
492
+ deadline = time.monotonic() + timeout + 1.0
493
+ for name, holder in self._holders.items():
494
+ remaining = max(0.0, deadline - time.monotonic())
495
+ holder.wait_ready(remaining)
496
+ if not holder.is_connected():
497
+ err = holder.error()
498
+ _console.print(
499
+ f"[yellow]MCP: {name} failed to start "
500
+ f"({err}); continuing without it[/yellow]"
501
+ )
502
+
503
+ loaded = [
504
+ f"{n} ({len(h.tools)} tools)"
505
+ for n, h in self._holders.items()
506
+ if h.is_connected()
507
+ ]
508
+ failed = [n for n, h in self._holders.items() if not h.is_connected()]
509
+ parts = loaded + ([f"{', '.join(failed)} failed"] if failed else [])
510
+ _console.print(
511
+ f"[dim]MCP: {'; '.join(parts) if parts else 'no external tools'}[/dim]"
512
+ )
513
+
514
+ # ------------------------------------------------------------------
515
+ # Tool access
516
+ # ------------------------------------------------------------------
517
+
518
+ def all_tools(self) -> list[dict[str, Any]]:
519
+ """Return all namespaced tool dicts from all loaded servers."""
520
+ result = []
521
+ for h in self._holders.values():
522
+ if h.is_connected():
523
+ result.extend(h.tools)
524
+ return result
525
+
526
+ # Default per-call timeout. dbt semantic-layer queries can legitimately take
527
+ # 60–120s on large warehouses; 120s gives a wide margin without hanging forever.
528
+ _DEFAULT_TOOL_TIMEOUT: float = 120.0
529
+
530
+ def call_tool(
531
+ self,
532
+ namespaced_name: str,
533
+ arguments: dict[str, Any],
534
+ timeout: float | None = None,
535
+ ) -> dict[str, Any]:
536
+ """Call an external tool by 'server__tool' name. Bridges async → sync.
537
+
538
+ Args:
539
+ timeout: per-call timeout in seconds (default: 120s). Pass None only
540
+ if you are sure the upstream tool is bounded by its own logic.
541
+ """
542
+ if timeout is None:
543
+ timeout = self._DEFAULT_TOOL_TIMEOUT
544
+ if "__" not in namespaced_name:
545
+ raise ValueError(
546
+ f"Expected 'server__tool' format, got: {namespaced_name!r}"
547
+ )
548
+ server_name, tool_name = namespaced_name.split("__", 1)
549
+ holder = self._holders.get(server_name)
550
+ if holder is None or not holder.is_connected():
551
+ return {"error": f"MCP server '{server_name}' is not connected"}
552
+
553
+ try:
554
+ result = holder.call_tool_sync(tool_name, arguments, timeout=timeout)
555
+ except TimeoutError:
556
+ return {
557
+ "error": f"Tool call '{namespaced_name}' timed out after {timeout}s"
558
+ }
559
+ except Exception as exc: # noqa: BLE001 — surface any unexpected failure
560
+ return {"error": f"Tool call '{namespaced_name}' failed: {exc}"}
561
+
562
+ return _flatten_call_result(result)
563
+
564
+ # ------------------------------------------------------------------
565
+ # Shutdown
566
+ # ------------------------------------------------------------------
567
+
568
+ def close(self) -> None:
569
+ """Signal all sessions to shut down and stop the event loop."""
570
+ for holder in self._holders.values():
571
+ holder.shutdown()
572
+ if self._loop is None:
573
+ return
574
+ # Brief sleep lets the shutdown sentinel drain the queue and the
575
+ # stdio_client/ClientSession context managers exit cleanly before we
576
+ # stop the loop. This is best-effort CLI teardown — precision is not
577
+ # required here; the daemon thread and subprocess will be killed on
578
+ # process exit regardless.
579
+ time.sleep(0.3)
580
+ self._loop.call_soon_threadsafe(self._loop.stop)
581
+ if self._loop_thread:
582
+ self._loop_thread.join(timeout=3.0)
583
+ self._holders.clear()
584
+
585
+
586
+ # ---------------------------------------------------------------------------
587
+ # Flatten CallToolResult → plain dict
588
+ # ---------------------------------------------------------------------------
589
+
590
+
591
+ def _flatten_call_result(result: Any) -> dict[str, Any]:
592
+ """Convert mcp.types.CallToolResult to a plain dict the agent can use."""
593
+ if result.isError:
594
+ texts = [c.text for c in result.content if hasattr(c, "text")]
595
+ return {"error": "\n".join(texts) or "Tool returned an error"}
596
+
597
+ if result.structuredContent:
598
+ return result.structuredContent
599
+
600
+ texts = [c.text for c in result.content if hasattr(c, "text")]
601
+ if len(texts) == 1:
602
+ # Try to parse as JSON for structured results; only treat dicts as structured.
603
+ try:
604
+ parsed = json.loads(texts[0])
605
+ if isinstance(parsed, dict):
606
+ return parsed
607
+ except (json.JSONDecodeError, ValueError):
608
+ pass
609
+ return {"content": texts[0]}
610
+ return {"content": "\n".join(texts)}
@@ -0,0 +1,96 @@
1
+ """Shared text-to-SQL generation function.
2
+
3
+ Single code path for SQL generation used by:
4
+ - Eval runner (standalone question → SQL)
5
+ - Cloud AIService (thin async wrapper)
6
+ - Playground (SQL guidance for system prompts)
7
+
8
+ Uses the rich schema context from get_schema_context() and the same
9
+ prompt quality across all consumers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ from dataface.ai.llm import OpenAIClient
17
+
18
+ _SQL_SYSTEM_PROMPT = """You are a SQL expert. Generate a single SQL query that answers the user's question.
19
+
20
+ {schema_context}
21
+
22
+ Rules:
23
+ - Return ONLY valid SQL for the database dialect shown above.
24
+ - Use clear column aliases so results are self-describing.
25
+ - Include appropriate aggregations, groupings, and ordering.
26
+ - Prefer explicit column names over SELECT *.
27
+ - Do not wrap the SQL in markdown code fences.
28
+
29
+ Respond with JSON: {{"sql": "<your SQL query>"}}"""
30
+
31
+ _SQL_GUIDANCE = """## Shared SQL Generation Rules
32
+
33
+ - Write SQL that matches the database dialect in the schema context.
34
+ - Use real table and column names from the provided schema context.
35
+ - Use clear column aliases so results are self-describing.
36
+ - Include appropriate aggregations, groupings, and ordering.
37
+ - Prefer explicit column names over `SELECT *`.
38
+ - Return executable SQL, not pseudocode or placeholders.
39
+ """
40
+
41
+
42
+ def get_sql_generation_guidance() -> str:
43
+ """Return the shared SQL rules used across text-to-SQL consumers."""
44
+ return _SQL_GUIDANCE
45
+
46
+
47
+ def build_sql_system_prompt(schema_context: str) -> str:
48
+ """Build the shared system prompt for standalone SQL generation."""
49
+ return _SQL_SYSTEM_PROMPT.format(schema_context=schema_context)
50
+
51
+
52
+ def generate_sql(
53
+ question: str,
54
+ schema_context: str,
55
+ *,
56
+ client: OpenAIClient,
57
+ ) -> str:
58
+ """Generate SQL from a natural-language question using the LLM.
59
+
60
+ Args:
61
+ question: Natural-language question about the data.
62
+ schema_context: Rich schema context string from get_schema_context()
63
+ or format_schema_context(). Includes table names, column types,
64
+ roles, semantic types, and value distributions.
65
+ client: An ``OpenAIClient`` instance.
66
+
67
+ Returns:
68
+ The generated SQL query string.
69
+ """
70
+ system_prompt = build_sql_system_prompt(schema_context)
71
+
72
+ response = client.create(
73
+ model=client.model,
74
+ input=[
75
+ {"role": "system", "content": system_prompt},
76
+ {"role": "user", "content": question},
77
+ ],
78
+ text={
79
+ "format": {
80
+ "type": "json_schema",
81
+ "name": "SQLResponse",
82
+ "schema": {
83
+ "type": "object",
84
+ "properties": {
85
+ "sql": {"type": "string"},
86
+ },
87
+ "required": ["sql"],
88
+ "additionalProperties": False,
89
+ },
90
+ "strict": True,
91
+ }
92
+ },
93
+ )
94
+
95
+ parsed = json.loads(response.output_text)
96
+ return parsed["sql"]