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,312 @@
1
+ """OpenAI function-calling adapters and MCP dispatch shim.
2
+
3
+ Thin wrappers over dataface.agent_api. No business logic here.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ import dataface.agent_api.describe_query as _describe_query
14
+ from dataface.agent_api import (
15
+ dashboards as _dash,
16
+ query as _query,
17
+ schema as _schema_mod,
18
+ schema_search as _schema_search_mod,
19
+ search as _search,
20
+ skills as _skills,
21
+ )
22
+ from dataface.agent_api.validate import (
23
+ ValidateDashboardArgs as _ValidateDashboardArgs,
24
+ validate as _validate_func,
25
+ )
26
+ from dataface.ai.context import DatafaceAIContext
27
+ from dataface.ai.tool_schemas import (
28
+ DESCRIBE_QUERY,
29
+ DOCS,
30
+ EXECUTE_QUERY,
31
+ QUERY_FACE,
32
+ RENDER_DASHBOARD,
33
+ SCHEMA,
34
+ SEARCH_DASHBOARDS,
35
+ to_openai_tool,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from dataface.ai.external_mcp import ExternalMCPManager
40
+
41
+ ToolHandler = Callable[[dict[str, Any], DatafaceAIContext], dict[str, Any]]
42
+
43
+ TOOL_RENDER_DASHBOARD: dict[str, Any] = to_openai_tool(
44
+ RENDER_DASHBOARD,
45
+ property_subset=[
46
+ "path",
47
+ "yaml_content",
48
+ "project_dir",
49
+ "variables",
50
+ "format",
51
+ "as_link",
52
+ ],
53
+ )
54
+ TOOL_EXECUTE_QUERY: dict[str, Any] = to_openai_tool(
55
+ EXECUTE_QUERY, property_subset=["sql", "variables", "limit"]
56
+ )
57
+ TOOL_DESCRIBE_QUERY: dict[str, Any] = to_openai_tool(
58
+ DESCRIBE_QUERY, property_subset=["sql", "source", "dialect"]
59
+ )
60
+ TOOL_SCHEMA: dict[str, Any] = to_openai_tool(
61
+ SCHEMA, property_subset=["source", "schema", "table", "column", "table_search"]
62
+ )
63
+ TOOL_SEARCH_DASHBOARDS: dict[str, Any] = to_openai_tool(SEARCH_DASHBOARDS)
64
+ TOOL_QUERY_FACE: dict[str, Any] = to_openai_tool(QUERY_FACE)
65
+ TOOL_DOCS: dict[str, Any] = to_openai_tool(DOCS)
66
+
67
+
68
+ def get_tools(include_data_tools: bool = True) -> list[dict[str, Any]]:
69
+ tools: list[dict[str, Any]] = [TOOL_RENDER_DASHBOARD, TOOL_DOCS]
70
+ if include_data_tools:
71
+ tools.extend(
72
+ [
73
+ TOOL_EXECUTE_QUERY,
74
+ TOOL_DESCRIBE_QUERY,
75
+ TOOL_QUERY_FACE,
76
+ TOOL_SCHEMA,
77
+ TOOL_SEARCH_DASHBOARDS,
78
+ ]
79
+ )
80
+ return tools
81
+
82
+
83
+ def _render_project_dir(args: dict[str, Any], ctx: DatafaceAIContext) -> Path | None:
84
+ project_dir = args.get("project_dir")
85
+ if project_dir is not None:
86
+ return Path(project_dir)
87
+ if ctx.default_project_dir is not None:
88
+ return ctx.default_project_dir
89
+ return ctx.adapter_registry.project_root
90
+
91
+
92
+ def _render_path(path: str | None, project_dir: Path | None) -> Path | None:
93
+ if not path:
94
+ return None
95
+ raw_path = Path(path)
96
+ if project_dir is None or not raw_path.is_absolute():
97
+ return raw_path
98
+ try:
99
+ return raw_path.resolve().relative_to(project_dir.resolve())
100
+ except ValueError:
101
+ return raw_path
102
+
103
+
104
+ def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
105
+ parsed = _dash.RenderDashboardArgs.model_validate(args)
106
+ project_dir = _render_project_dir(args, ctx)
107
+ return _dash.render_dashboard(
108
+ path=_render_path(str(parsed.path) if parsed.path else None, project_dir),
109
+ yaml_content=parsed.yaml_content,
110
+ project_dir=project_dir,
111
+ variables=parsed.variables,
112
+ format=parsed.format or "json",
113
+ as_link=parsed.as_link,
114
+ adapter_registry=ctx.adapter_registry,
115
+ server_port=ctx.server_port,
116
+ ).model_dump(mode="json", exclude_none=True)
117
+
118
+
119
+ def _handle_query(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
120
+ return _query.execute_query(
121
+ sql=args.get("sql", ""),
122
+ variables=args.get("variables"),
123
+ source=args.get("source"),
124
+ limit=args.get("limit", 50),
125
+ adapter_registry=ctx.adapter_registry,
126
+ ).model_dump(mode="json", exclude_none=True)
127
+
128
+
129
+ def _handle_describe_query(
130
+ args: dict[str, Any], ctx: DatafaceAIContext
131
+ ) -> dict[str, Any]:
132
+ return _describe_query.describe_query(
133
+ sql=args.get("sql", ""),
134
+ source=args.get("source"),
135
+ dialect=args.get("dialect"),
136
+ adapter_registry=ctx.adapter_registry,
137
+ ).model_dump(mode="json", exclude_none=True)
138
+
139
+
140
+ def _handle_query_face(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
141
+ parsed = _query.QueryFaceArgs.model_validate(args)
142
+ return _query.query_face(
143
+ name=parsed.name,
144
+ path=parsed.path,
145
+ project_dir=_render_project_dir(args, ctx),
146
+ vars=parsed.vars,
147
+ limit=parsed.limit,
148
+ adapter_registry=ctx.adapter_registry,
149
+ ).model_dump(mode="json", exclude_none=True)
150
+
151
+
152
+ def _force_refresh_arg(args: dict[str, Any]) -> bool:
153
+ raw = args.get("force_refresh", False)
154
+ if raw is None:
155
+ return False
156
+ if isinstance(raw, bool):
157
+ return raw
158
+ raise ValueError("force_refresh must be a JSON boolean")
159
+
160
+
161
+ def _handle_schema(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
162
+ try:
163
+ force_refresh = _force_refresh_arg(args)
164
+ except ValueError as exc:
165
+ return {"success": False, "sources": {}, "errors": [str(exc)]}
166
+
167
+ return _schema_mod.schema(
168
+ source=args.get("source"),
169
+ schema=args.get("schema"),
170
+ table=args.get("table"),
171
+ column=args.get("column"),
172
+ force_refresh=force_refresh,
173
+ table_search=args.get("table_search"),
174
+ adapter_registry=ctx.adapter_registry,
175
+ ).model_dump(mode="json", by_alias=True, exclude_none=True)
176
+
177
+
178
+ def _handle_schema_search(
179
+ args: dict[str, Any], ctx: DatafaceAIContext
180
+ ) -> dict[str, Any]:
181
+ parsed = _schema_search_mod.SchemaSearchArgs.model_validate(args)
182
+ return _schema_search_mod.schema_search(
183
+ **parsed.model_dump(),
184
+ adapter_registry=ctx.adapter_registry,
185
+ ).model_dump(mode="json", by_alias=True, exclude_none=True)
186
+
187
+
188
+ def _handle_search(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
189
+ parsed = _search.SearchDashboardsArgs.model_validate(args)
190
+ kwargs: dict[str, Any] = {
191
+ "query": parsed.query,
192
+ "project_dir": parsed.project_dir or ctx.dashboards_directory,
193
+ "tags": parsed.tags,
194
+ }
195
+ if parsed.limit is not None:
196
+ kwargs["limit"] = parsed.limit
197
+ return _search.search_dashboards(**kwargs).model_dump(
198
+ mode="json", exclude_none=True
199
+ )
200
+
201
+
202
+ def _handle_docs(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
203
+ from dataface.agent_api.docs import DocsArgs, docs as _docs
204
+
205
+ parsed = DocsArgs.model_validate(args)
206
+ return _docs(
207
+ topic=parsed.topic,
208
+ search=parsed.search,
209
+ limit=parsed.limit,
210
+ ).model_dump(mode="json")
211
+
212
+
213
+ def _handle_describe_dashboard(
214
+ args: dict[str, Any], ctx: DatafaceAIContext
215
+ ) -> dict[str, Any]:
216
+ from dataface.agent_api.describe import DescribeFaceArgs, describe_face
217
+
218
+ parsed = DescribeFaceArgs.model_validate(args)
219
+ return describe_face(
220
+ path=parsed.path,
221
+ project_dir=_render_project_dir(args, ctx),
222
+ ).model_dump(mode="json", exclude_none=True)
223
+
224
+
225
+ # Wheel-internal bookkeeping fields that MCP clients don't need on the wire.
226
+ # `surfaces` is the per-skill visibility filter; `rendered_for` is the marker
227
+ # the registry stamps on after macro expansion. Both are useful for in-process
228
+ # callers (tests, the CLI) but only add noise to MCP tool responses.
229
+ _SKILL_FIELDS_INTERNAL = {"surfaces", "rendered_for"}
230
+
231
+
232
+ def _handle_list_skills(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
233
+ return _skills.list_skills(surface="mcp").model_dump(
234
+ mode="json",
235
+ exclude_none=True,
236
+ exclude={"skills": {"__all__": _SKILL_FIELDS_INTERNAL}},
237
+ )
238
+
239
+
240
+ def _handle_get_skill(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
241
+ parsed = _skills.GetSkillArgs.model_validate(args)
242
+ try:
243
+ skill = _skills.get_skill(parsed.name, surface="mcp")
244
+ except _skills.SkillNotFound as exc:
245
+ return {"success": False, "errors": [str(exc)]}
246
+ return skill.model_dump(
247
+ mode="json", exclude_none=True, exclude=_SKILL_FIELDS_INTERNAL
248
+ )
249
+
250
+
251
+ def _handle_search_skills(
252
+ args: dict[str, Any], ctx: DatafaceAIContext
253
+ ) -> dict[str, Any]:
254
+ parsed = _skills.SearchSkillsArgs.model_validate(args)
255
+ return _skills.search_skills(
256
+ parsed.query, limit=parsed.limit, surface="mcp"
257
+ ).model_dump(mode="json", exclude_none=True)
258
+
259
+
260
+ def _handle_validate(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
261
+ parsed = _ValidateDashboardArgs.model_validate(args)
262
+ return _validate_func(
263
+ path=parsed.path,
264
+ project_dir=_render_project_dir(args, ctx),
265
+ ).model_dump(mode="json", exclude_none=True)
266
+
267
+
268
+ TOOL_HANDLERS: dict[str, ToolHandler] = {
269
+ "validate_dashboard": _handle_validate,
270
+ "render_dashboard": _handle_render,
271
+ "execute_query": _handle_query,
272
+ "describe_query": _handle_describe_query,
273
+ "query_face": _handle_query_face,
274
+ "schema": _handle_schema,
275
+ "schema_search": _handle_schema_search,
276
+ "search_dashboards": _handle_search,
277
+ "docs": _handle_docs,
278
+ "describe_dashboard": _handle_describe_dashboard,
279
+ "list_skills": _handle_list_skills,
280
+ "get_skill": _handle_get_skill,
281
+ "search_skills": _handle_search_skills,
282
+ }
283
+
284
+
285
+ def handle_tool_call(
286
+ function_name: str, function_args: dict[str, Any], *, context: DatafaceAIContext
287
+ ) -> str:
288
+ return json.dumps(
289
+ dispatch_tool_call(function_name, function_args, context=context), default=str
290
+ )
291
+
292
+
293
+ def dispatch_tool_call(
294
+ function_name: str,
295
+ function_args: dict[str, Any],
296
+ *,
297
+ context: DatafaceAIContext,
298
+ external_manager: ExternalMCPManager | None = None,
299
+ ) -> dict[str, Any]:
300
+ if "__" in function_name:
301
+ if external_manager is None:
302
+ return {
303
+ "error": f"External tool '{function_name}' called but no MCP manager available"
304
+ }
305
+ return external_manager.call_tool(function_name, function_args)
306
+ handler = TOOL_HANDLERS.get(function_name)
307
+ if handler is None:
308
+ return {"error": f"Unknown tool: {function_name}"}
309
+ try:
310
+ return handler(dict(function_args), context)
311
+ except Exception as exc: # noqa: BLE001 — last-resort catch at dispatch boundary
312
+ return {"success": False, "errors": [str(exc)]}
@@ -0,0 +1,57 @@
1
+ """YAML extraction utilities for AI responses.
2
+
3
+ This module provides utilities for extracting YAML content from AI-generated
4
+ text responses.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+
12
+ def extract_yaml(text: str) -> str | None:
13
+ """Extract YAML code block from text.
14
+
15
+ Looks for YAML content in markdown code blocks. Supports both
16
+ explicit ```yaml blocks and generic ``` blocks that contain
17
+ YAML-like content.
18
+
19
+ Args:
20
+ text: Text that may contain YAML code blocks
21
+
22
+ Returns:
23
+ Extracted YAML content or None if not found
24
+
25
+ Example:
26
+ >>> text = '''Here's the dashboard:
27
+ ... ```yaml
28
+ ... title: "My Dashboard"
29
+ ... queries:
30
+ ... sales: { sql: "SELECT * FROM sales" }
31
+ ... ```
32
+ ... '''
33
+ >>> extract_yaml(text)
34
+ 'title: "My Dashboard"\\nqueries:\\n sales: { sql: "SELECT * FROM sales" }'
35
+ """
36
+ # Look for ```yaml ... ``` blocks (most specific)
37
+ # Allow optional whitespace before closing backticks (handles indented code blocks)
38
+ pattern = r"```yaml\s*\n(.*?)\n\s*```"
39
+ matches = re.findall(pattern, text, re.DOTALL)
40
+ if matches:
41
+ return matches[0].strip()
42
+
43
+ # Also try ``` ... ``` (without yaml tag) - check if it looks like YAML
44
+ pattern = r"```\s*\n(.*?)\n\s*```"
45
+ matches = re.findall(pattern, text, re.DOTALL)
46
+ if matches:
47
+ content = matches[0].strip()
48
+ # Check if it looks like YAML (starts with common YAML keys)
49
+ if content.startswith(("title:", "queries:", "charts:", "rows:", "variables:")):
50
+ return content
51
+ # Also check if it contains YAML-like structure
52
+ if ":" in content and (
53
+ "queries:" in content or "charts:" in content or "rows:" in content
54
+ ):
55
+ return content
56
+
57
+ return None
@@ -0,0 +1,3 @@
1
+ """CLI tooling for Dataface."""
2
+
3
+ __all__ = []
@@ -0,0 +1,48 @@
1
+ """Console factory and agent/pipe context detection for the dft CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ from rich.console import Console
9
+
10
+ # The list of env vars to check is fixed at module load; values are looked
11
+ # up per call via os.environ.get.
12
+ AGENT_ENV_VARS = (
13
+ "CLAUDECODE", # Claude Code (official)
14
+ "GEMINI_CLI", # Gemini CLI (official)
15
+ "CLINE_ACTIVE", # Cline v3.24+ (official)
16
+ "CURSOR_AGENT", # Cursor (acknowledged, undocumented)
17
+ "COPILOT_CLI", # GitHub Copilot CLI (community-observed)
18
+ "GOOSE_TERMINAL", # Goose (official)
19
+ "AGENT", # Goose + Amp + emerging cross-agent convention
20
+ "AI_AGENT", # Vercel-proposed universal fallback
21
+ )
22
+
23
+
24
+ def is_plain_output() -> bool:
25
+ """Return True when output should be plain (no Rich chrome).
26
+
27
+ True when running inside a known AI agent or when stdout is not a TTY
28
+ (e.g. piped to cat, redirected to a file).
29
+ """
30
+ return any(os.environ.get(v) for v in AGENT_ENV_VARS) or not sys.stdout.isatty()
31
+
32
+
33
+ def dft_console(*, stderr: bool = False) -> Console:
34
+ """Build a Rich Console that honors is_plain_output() at call time.
35
+
36
+ Use this in CLI command modules instead of bare Console(...) so that
37
+ agent-PTY and piped contexts (CLAUDECODE=1, stdout-to-file, ...) get
38
+ plain output uniformly.
39
+ """
40
+ plain = is_plain_output()
41
+ return Console(
42
+ stderr=stderr,
43
+ force_terminal=not plain,
44
+ no_color=plain,
45
+ )
46
+
47
+
48
+ __all__ = ["AGENT_ENV_VARS", "dft_console", "is_plain_output"]
@@ -0,0 +1,83 @@
1
+ """Rich-formatted output for StructuredError lists.
2
+
3
+ In plain mode (agent or pipe context) we construct the Console with
4
+ ``force_terminal=False, no_color=True`` and skip the surrounding
5
+ ``Panel`` wrapper. Inline Rich markup like ``[bold]``, ``[dim]`` is
6
+ consumed by Rich's parser either way — when the Console isn't emitting
7
+ ANSI, the markup leaves no trace in the output. So a single body
8
+ composition handles both surfaces; only the Panel wrapper needs gating.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Literal
16
+
17
+ from rich.panel import Panel
18
+
19
+ from dataface.cli._console import dft_console
20
+ from dataface.core.errors import StructuredError
21
+ from dataface.core.render.warnings.base import RenderWarning
22
+
23
+
24
+ def print_structured_errors(
25
+ errs: list[StructuredError],
26
+ *,
27
+ json_output: bool = False,
28
+ severity: Literal["error", "warning"] = "error",
29
+ ) -> None:
30
+ """Print structured errors as Rich panels (stderr) or JSON (stdout)."""
31
+ if json_output:
32
+ print(
33
+ json.dumps(
34
+ {"errors": [e.model_dump(exclude_none=True) for e in errs]}, indent=2
35
+ )
36
+ )
37
+ return
38
+ style = "red" if severity == "error" else "yellow"
39
+ console = dft_console(stderr=True)
40
+ for e in errs:
41
+ body_parts = [f"[bold {style}]{e.code}[/] {e.message}"]
42
+ if e.hint:
43
+ body_parts.append(f"[dim]Hint:[/] {e.hint}")
44
+ if e.file:
45
+ loc = e.file + (f":{e.line}" if e.line else "")
46
+ body_parts.append(f"[dim]At:[/] {loc}")
47
+ if e.docs_topic:
48
+ body_parts.append(f"[dim]Docs:[/] dft docs {e.docs_topic}")
49
+ elif e.doc_url:
50
+ body_parts.append(f"[dim]Docs:[/] {e.doc_url}")
51
+ for cmd in e.next_commands:
52
+ body_parts.append(f"[dim]→[/] {cmd.label}: [bold]$ {cmd.command}[/]")
53
+ body = "\n".join(body_parts)
54
+ if console.no_color:
55
+ console.print(body)
56
+ else:
57
+ console.print(Panel(body, border_style=style, expand=False))
58
+
59
+
60
+ def print_render_warnings(
61
+ warnings: list[RenderWarning],
62
+ *,
63
+ path: Path | None = None,
64
+ ) -> None:
65
+ """Print RenderWarning objects as yellow Rich panels (stderr).
66
+
67
+ Mirrors print_structured_errors' panel style at severity=warning so the
68
+ visual treatment matches across error and warning surfaces.
69
+ """
70
+ style = "yellow"
71
+ console = dft_console(stderr=True)
72
+ for w in warnings:
73
+ chart_prefix = f"{w.chart}: " if w.chart else ""
74
+ body_parts = [f"[bold {style}]{w.code}[/] {chart_prefix}{w.message}"]
75
+ if w.fix:
76
+ body_parts.append(f"[dim]Fix:[/] {w.fix}")
77
+ if path is not None:
78
+ body_parts.append(f"[dim]At:[/] {path}")
79
+ body = "\n".join(body_parts)
80
+ if console.no_color:
81
+ console.print(body)
82
+ else:
83
+ console.print(Panel(body, border_style=style, expand=False))
@@ -0,0 +1,190 @@
1
+ """Optional-dependency gate for CLI commands that need heavyweight extras."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.metadata
6
+ import importlib.util
7
+ import os
8
+ import re
9
+ import shlex
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from typing import Literal
14
+
15
+ import typer
16
+ from rich.markup import escape
17
+ from rich.panel import Panel
18
+
19
+ from dataface._install_hint import install_hint
20
+ from dataface.cli._console import dft_console
21
+
22
+ # dist-name → import-name overrides for packages where they differ
23
+ _DIST_TO_IMPORT: dict[str, str] = {
24
+ "prompt-toolkit": "prompt_toolkit",
25
+ }
26
+
27
+ _console = dft_console(stderr=True)
28
+
29
+
30
+ def _dist_to_import_name(dist_name: str) -> str:
31
+ """Convert a distribution name to its top-level import name."""
32
+ if dist_name in _DIST_TO_IMPORT:
33
+ return _DIST_TO_IMPORT[dist_name]
34
+ return dist_name.replace("-", "_")
35
+
36
+
37
+ def _agent_extra_packages(extra: str) -> list[str]:
38
+ """Return bare dist names declared under [project.optional-dependencies.<extra>]."""
39
+ try:
40
+ reqs = importlib.metadata.requires("dataface") or []
41
+ except importlib.metadata.PackageNotFoundError as exc:
42
+ _console.print(
43
+ f"[red]Cannot read package metadata for 'dataface' in {sys.executable}. "
44
+ "The package may not be installed correctly.[/red]"
45
+ )
46
+ raise typer.Exit(1) from exc
47
+
48
+ pattern = re.compile(rf"""extra\s*==\s*['"]({re.escape(extra)})['"]""")
49
+ result = []
50
+ for req in reqs:
51
+ if pattern.search(req):
52
+ # strip version specifier — take only the dist name before any space/>;
53
+ dist = re.split(r"[><=!;\s]", req)[0]
54
+ result.append(dist)
55
+ return result
56
+
57
+
58
+ def _missing_packages(extra: str) -> list[str]:
59
+ """Return dist names from <extra> whose top-level module cannot be imported."""
60
+ missing = []
61
+ for dist in _agent_extra_packages(extra):
62
+ import_name = _dist_to_import_name(dist)
63
+ if importlib.util.find_spec(import_name) is None:
64
+ missing.append(dist)
65
+ return missing
66
+
67
+
68
+ def _resolve_installer() -> Literal["pip", "uv"] | None:
69
+ """Detect the active installer. Returns 'pip', 'uv', or None if neither available.
70
+
71
+ Priority: UV env var (uv set this when it spawned us) → pip importable → uv on PATH.
72
+ """
73
+ if os.environ.get("UV"):
74
+ return "uv"
75
+ if importlib.util.find_spec("pip") is not None:
76
+ return "pip"
77
+ if shutil.which("uv"):
78
+ return "uv"
79
+ return None
80
+
81
+
82
+ def _installer_command(
83
+ installer: Literal["pip", "uv"], missing: list[str]
84
+ ) -> list[str]:
85
+ """Return the subprocess argv to install missing packages."""
86
+ if installer == "pip":
87
+ return [sys.executable, "-m", "pip", "install", "--no-input", *missing]
88
+ return ["uv", "pip", "install", "--python", sys.executable, *missing]
89
+
90
+
91
+ def _installer_prefix(installer: Literal["pip", "uv"]) -> str:
92
+ """Return the human-readable installer prefix for copy-paste commands."""
93
+ if installer == "pip":
94
+ return "pip install"
95
+ return f"uv pip install --python {shlex.quote(sys.executable)}"
96
+
97
+
98
+ def _installer_display_command(
99
+ installer: Literal["pip", "uv"], missing: list[str]
100
+ ) -> str:
101
+ """Return the human-readable install command for the missing packages."""
102
+ return f"{_installer_prefix(installer)} {' '.join(missing)}"
103
+
104
+
105
+ def _build_extras_panel(
106
+ extra: str,
107
+ missing: list[str],
108
+ installer: Literal["pip", "uv"] | None,
109
+ ) -> Panel:
110
+ if installer is None:
111
+ return Panel(
112
+ "[bold]dft chat[/bold] requires optional packages that are not installed:\n\n"
113
+ + "\n".join(f" • [cyan]{escape(p)}[/cyan]" for p in missing)
114
+ + "\n\nNo installer found in this environment. "
115
+ "Install uv (https://docs.astral.sh/uv/getting-started/installation/) "
116
+ "or [dim]pip[/dim] into this interpreter, then re-run — "
117
+ "or install these packages by whatever mechanism you used to install "
118
+ "[dim]dataface[/dim] itself.",
119
+ title="[yellow]Optional dependencies required for `dft chat`[/yellow]",
120
+ expand=False,
121
+ )
122
+ pip_cmd = _installer_display_command(installer, missing)
123
+ canonical_hint = install_hint(extra)
124
+ return Panel(
125
+ "[bold]dft chat[/bold] requires optional packages that are not installed:\n\n"
126
+ + "\n".join(f" • [cyan]{escape(p)}[/cyan]" for p in missing)
127
+ + f"\n\nTo install manually:\n [dim]{escape(pip_cmd)}[/dim]\n\n"
128
+ + f"Or reinstall Dataface with the {escape('[' + extra + ']')} extra:\n"
129
+ + f" [dim]{escape(canonical_hint)}[/dim]",
130
+ title="[yellow]Optional dependencies required for `dft chat`[/yellow]",
131
+ expand=False,
132
+ )
133
+
134
+
135
+ def install_extras(extra: str, *, interactive: bool) -> None:
136
+ """Install missing packages for <extra>.
137
+
138
+ interactive=False: silently install without prompting (caller already confirmed).
139
+ interactive=True: show info panel and prompt before installing.
140
+ Raises typer.Exit(1) on install failure or (when interactive) user decline.
141
+ """
142
+ missing = _missing_packages(extra)
143
+ if not missing:
144
+ return
145
+
146
+ installer = _resolve_installer()
147
+ if installer is None:
148
+ _console.print(_build_extras_panel(extra, missing, installer=None))
149
+ raise typer.Exit(1)
150
+
151
+ if interactive:
152
+ _console.print(_build_extras_panel(extra, missing, installer=installer))
153
+ answer = _console.input("Install now? [Y/n] ").strip().lower()
154
+ if answer not in ("", "y", "yes"):
155
+ raise typer.Exit(1)
156
+
157
+ display_cmd = _installer_display_command(installer, missing)
158
+ try:
159
+ subprocess.check_call(_installer_command(installer, missing))
160
+ except subprocess.CalledProcessError as exc:
161
+ _console.print(f"[red]Install failed. Try manually:\n {display_cmd}[/red]")
162
+ raise typer.Exit(1) from exc
163
+ importlib.invalidate_caches()
164
+ still_missing = _missing_packages(extra)
165
+ if still_missing:
166
+ _console.print(
167
+ f"[red]Install reported success but packages are still missing "
168
+ f"in {sys.executable}. Try:\n {display_cmd}[/red]"
169
+ )
170
+ raise typer.Exit(1)
171
+
172
+
173
+ def require_extras(extra: str) -> None:
174
+ """Raise typer.Exit(1) (or offer to install) if <extra> packages are missing.
175
+
176
+ - On a TTY without DFT_NO_AUTO_INSTALL=1: prompt the user; install on yes.
177
+ - Otherwise: print the install command and raise Exit(1).
178
+ """
179
+ missing = _missing_packages(extra)
180
+ if not missing:
181
+ return
182
+
183
+ interactive = sys.stdin.isatty() and os.environ.get("DFT_NO_AUTO_INSTALL") != "1"
184
+
185
+ if interactive:
186
+ install_extras(extra, interactive=True)
187
+ else:
188
+ installer = _resolve_installer()
189
+ _console.print(_build_extras_panel(extra, missing, installer=installer))
190
+ raise typer.Exit(1)