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,118 @@
1
+ """File-ref expansion for Dataface agent surfaces.
2
+
3
+ Scans a prompt for @<path> tokens and inlines matching file contents.
4
+ Reusable from any surface: CLI, Cloud chat, MCP tools, IDE clients.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from pathlib import Path
11
+
12
+ from pydantic import BaseModel
13
+
14
+ _AT_REF_RE = re.compile(r"(?<![\w.])@(?P<path>[\w./-]+)")
15
+ _MAX_INLINE_BYTES = 200 * 1024
16
+ _STUB_LINES = 50
17
+ _STUB_HEAD_BYTES = 64 * 1024
18
+
19
+
20
+ class FileRef(BaseModel):
21
+ token: str # original @token (e.g. "@faces/foo.yml")
22
+ path: Path # resolved absolute path
23
+
24
+
25
+ class ExpandedPrompt(BaseModel):
26
+ text: str
27
+ references: list[FileRef]
28
+
29
+
30
+ def _has_dotdot(path_str: str) -> bool:
31
+ return any(seg == ".." for seg in path_str.split("/"))
32
+
33
+
34
+ def _fenced(text: str) -> str:
35
+ longest = run = 0
36
+ for ch in text:
37
+ if ch == "`":
38
+ run += 1
39
+ longest = max(longest, run)
40
+ else:
41
+ run = 0
42
+ fence = "`" * max(3, longest + 1)
43
+ return f"{fence}\n{text}\n{fence}"
44
+
45
+
46
+ def _inline(path_str: str, project_dir: Path) -> tuple[str, Path] | None:
47
+ """Return (replacement_text, resolved_path) or None if token is unresolvable."""
48
+ if _has_dotdot(path_str):
49
+ return None
50
+
51
+ resolved = (project_dir / path_str).resolve()
52
+ try:
53
+ resolved.relative_to(project_dir.resolve())
54
+ except ValueError:
55
+ return None
56
+
57
+ if not resolved.exists() or not resolved.is_file():
58
+ return None
59
+
60
+ try:
61
+ size = resolved.stat().st_size
62
+ except OSError:
63
+ return None
64
+
65
+ if size > _MAX_INLINE_BYTES:
66
+ try:
67
+ with resolved.open("rb") as f:
68
+ head_bytes = f.read(_STUB_HEAD_BYTES)
69
+ except OSError:
70
+ return None
71
+ try:
72
+ head_text = head_bytes.decode("utf-8")
73
+ except UnicodeDecodeError:
74
+ return None
75
+ head_lines = head_text.splitlines()
76
+ truncated = "\n".join(head_lines[:_STUB_LINES])
77
+ if size <= len(head_bytes):
78
+ line_count = len(head_lines)
79
+ else:
80
+ avg_line_bytes = max(1, len(head_bytes) // max(1, len(head_lines)))
81
+ line_count = size // avg_line_bytes
82
+ header = (
83
+ f"(@{path_str}: {size // 1024} KB, ~{line_count} lines,"
84
+ f" first {_STUB_LINES} lines below)"
85
+ )
86
+ return f"{header}\n{_fenced(truncated)}", resolved
87
+
88
+ try:
89
+ text = resolved.read_text(encoding="utf-8")
90
+ except (OSError, UnicodeDecodeError):
91
+ return None
92
+
93
+ return _fenced(text), resolved
94
+
95
+
96
+ def expand_file_refs(message: str, *, project_dir: Path) -> ExpandedPrompt:
97
+ """Replace @<path> tokens in *message* with file contents.
98
+
99
+ Tokens that cannot be resolved (out of project_dir, non-existent, binary,
100
+ dotdot traversal) are left as-is and not recorded in references.
101
+ """
102
+ references: list[FileRef] = []
103
+
104
+ def replace(match: re.Match[str]) -> str:
105
+ full_match = match.group(0)
106
+ path_str = match.group("path").rstrip(".")
107
+ if not path_str:
108
+ return full_match
109
+ suffix = full_match[1 + len(path_str) :]
110
+ result = _inline(path_str, project_dir)
111
+ if result is None:
112
+ return full_match
113
+ replacement, resolved_path = result
114
+ references.append(FileRef(token=f"@{path_str}", path=resolved_path))
115
+ return replacement + suffix
116
+
117
+ text = _AT_REF_RE.sub(replace, message)
118
+ return ExpandedPrompt(text=text, references=references)
@@ -0,0 +1,126 @@
1
+ """Bootstrap (or refresh) a Dataface project layout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from dataface.agent_api._project_agents_md import (
11
+ load_agents_snippet,
12
+ merge_agents_snippet,
13
+ )
14
+
15
+ GITIGNORE_ENTRIES = ("renders/", ".venv/", "__pycache__/", "*.duckdb")
16
+
17
+ _TEMPLATES = importlib.resources.files("dataface.agent_api._init_templates")
18
+
19
+
20
+ class InitResult(BaseModel):
21
+ project_dir: Path
22
+ dbt_detected: bool
23
+ created_files: list[Path] = []
24
+ skipped_files: list[Path] = []
25
+ refreshed_files: list[Path] = []
26
+ appended_files: list[Path] = []
27
+ hints: list[str] = []
28
+
29
+
30
+ def init_project(
31
+ project_dir: Path | None = None,
32
+ *,
33
+ force: bool = False,
34
+ write_agents_md: bool = True,
35
+ write_claude_md: bool = True,
36
+ eject_inspect: bool = False,
37
+ ) -> InitResult:
38
+ """Bootstrap (or refresh) a Dataface project layout.
39
+
40
+ Safe to re-run: scaffold files are skipped unless *force* is set.
41
+ AGENTS.md: appends a short Dataface blurb when the file already exists;
42
+ creates the blurb alone when missing. Re-run refreshes only the marked
43
+ ``<!-- dft-dataface:* -->`` section. CLAUDE.md is create-only.
44
+ """
45
+ root = (project_dir or Path(".")).resolve()
46
+ dbt_detected = (root / "dbt_project.yml").exists()
47
+
48
+ result = InitResult(project_dir=root, dbt_detected=dbt_detected)
49
+
50
+ (root / "faces").mkdir(exist_ok=True)
51
+ (root / "faces" / "partials").mkdir(exist_ok=True)
52
+
53
+ scaffolds: list[tuple[str, str]] = [
54
+ ("dataface.yml", _TEMPLATES.joinpath("dataface.yml").read_text()),
55
+ ("faces/index.md", _TEMPLATES.joinpath("index.md").read_text()),
56
+ ("faces/dataface.yml", _TEMPLATES.joinpath("faces-dataface.yml").read_text()),
57
+ ("faces/partials/.gitkeep", ""),
58
+ ]
59
+
60
+ for rel, content in scaffolds:
61
+ target = root / rel
62
+ if target.exists() and not force:
63
+ result.skipped_files.append(Path(rel))
64
+ elif target.exists():
65
+ target.write_text(content)
66
+ result.refreshed_files.append(Path(rel))
67
+ else:
68
+ target.write_text(content)
69
+ result.created_files.append(Path(rel))
70
+
71
+ _ensure_gitignore_entries(root, result)
72
+
73
+ if write_agents_md:
74
+ snippet = load_agents_snippet()
75
+ agents_md_path = root / "AGENTS.md"
76
+ existing: str | None = (
77
+ agents_md_path.read_text() if agents_md_path.exists() else None
78
+ )
79
+ final_text, action = merge_agents_snippet(existing, snippet)
80
+ agents_md_path.write_text(final_text)
81
+ if action == "created":
82
+ result.created_files.append(Path("AGENTS.md"))
83
+ elif action == "refreshed":
84
+ result.refreshed_files.append(Path("AGENTS.md"))
85
+ else:
86
+ result.appended_files.append(Path("AGENTS.md"))
87
+
88
+ if write_claude_md:
89
+ claude_md_path = root / "CLAUDE.md"
90
+ if not claude_md_path.exists():
91
+ claude_md_path.write_text("@AGENTS.md\n")
92
+ result.created_files.append(Path("CLAUDE.md"))
93
+
94
+ if eject_inspect:
95
+ from dataface.agent_api import inspect as _api_inspect
96
+
97
+ try:
98
+ ejected = _api_inspect.eject_templates(
99
+ root / "faces" / "inspect", templates=None, force=False
100
+ )
101
+ result.created_files.extend(p.relative_to(root) for p in ejected)
102
+ except (FileNotFoundError, ModuleNotFoundError) as exc:
103
+ result.hints.append(
104
+ f"warning: inspect templates could not be ejected ({exc!r}); "
105
+ "your install may be partial. Run 'pip show dataface' to verify."
106
+ )
107
+
108
+ return result
109
+
110
+
111
+ def _ensure_gitignore_entries(root: Path, result: InitResult) -> None:
112
+ gitignore = root / ".gitignore"
113
+ if not gitignore.exists():
114
+ gitignore.write_text("\n".join(GITIGNORE_ENTRIES) + "\n")
115
+ result.created_files.append(Path(".gitignore"))
116
+ return
117
+
118
+ text = gitignore.read_text()
119
+ existing = set(text.splitlines())
120
+ missing = [e for e in GITIGNORE_ENTRIES if e not in existing]
121
+ if not missing:
122
+ return
123
+
124
+ separator = "" if not text or text.endswith("\n") else "\n"
125
+ gitignore.write_text(f"{text}{separator}" + "\n".join(missing) + "\n")
126
+ result.refreshed_files.append(Path(".gitignore"))
@@ -0,0 +1,128 @@
1
+ """Inspect template-management verbs (OSS).
2
+
3
+ Profiler verbs (inspect_table, inspect_all, audit_tables, bake_schema_metadata)
4
+ live in the private ``dataface-super-schema`` package and are exposed via the
5
+ CLI entry-point plugin. This module ships the template-management verbs that
6
+ belong in the OSS wheel: list, eject, validate.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timezone
12
+ from hashlib import sha256
13
+ from pathlib import Path
14
+
15
+ from pydantic import BaseModel
16
+
17
+ from dataface import __version__
18
+ from dataface.core.inspect import INSPECT_TEMPLATES
19
+ from dataface.core.inspect.manifest_utils import (
20
+ INSPECT_TEMPLATE_MANIFEST,
21
+ MANIFEST_SCHEMA_VERSION,
22
+ compare_templates,
23
+ load_manifest,
24
+ save_manifest,
25
+ )
26
+
27
+
28
+ class InspectTemplate(BaseModel):
29
+ name: str
30
+
31
+
32
+ class ValidateTemplatesResult(BaseModel):
33
+ success: bool
34
+ missing: list[str] = []
35
+ upstream_changed: list[str] = []
36
+ custom_safe: list[str] = []
37
+ unchanged: list[str] = []
38
+
39
+
40
+ def list_templates() -> list[InspectTemplate]:
41
+ """List all built-in inspect templates."""
42
+ return [InspectTemplate(name=name) for name in INSPECT_TEMPLATES]
43
+
44
+
45
+ def eject_templates(
46
+ target_dir: Path,
47
+ *,
48
+ templates: list[str] | None = None,
49
+ force: bool = False,
50
+ ) -> list[Path]:
51
+ """Write built-in inspect templates to target_dir.
52
+
53
+ Returns paths of files actually written (skipped files are omitted).
54
+ Raises ValueError for unknown template names.
55
+ """
56
+ from importlib.resources import files as _pkg_files
57
+
58
+ to_eject = templates if templates is not None else list(INSPECT_TEMPLATES)
59
+
60
+ invalid = [t for t in to_eject if t not in INSPECT_TEMPLATES]
61
+ if invalid:
62
+ raise ValueError(f"Unknown templates: {', '.join(invalid)}")
63
+
64
+ target_dir.mkdir(parents=True, exist_ok=True)
65
+ templates_pkg = _pkg_files("dataface.core.inspect.templates")
66
+ manifest = load_manifest(target_dir, dataface_version=__version__)
67
+ manifest["schema_version"] = MANIFEST_SCHEMA_VERSION
68
+ manifest["dataface_version"] = __version__
69
+ manifest["generated_at"] = datetime.now(timezone.utc).isoformat()
70
+ manifest_templates = manifest.setdefault("templates", {})
71
+
72
+ ejected: list[Path] = []
73
+
74
+ for name in to_eject:
75
+ source_file = templates_pkg.joinpath(f"{name}.yml")
76
+ target_file = target_dir / f"{name}.yml"
77
+
78
+ if target_file.exists() and not force:
79
+ continue
80
+
81
+ content = source_file.read_text()
82
+ source_hash = sha256(content.encode("utf-8")).hexdigest()
83
+ target_file.write_text(content)
84
+
85
+ manifest_templates[name] = {
86
+ "filename": f"{name}.yml",
87
+ "ejected_at": datetime.now(timezone.utc).isoformat(),
88
+ "source_version": __version__,
89
+ "source_sha256": source_hash,
90
+ }
91
+ ejected.append(target_file)
92
+
93
+ save_manifest(target_dir, manifest)
94
+ return ejected
95
+
96
+
97
+ def validate_ejected_templates(
98
+ target_dir: Path | None = None,
99
+ ) -> ValidateTemplatesResult:
100
+ """Compare ejected templates against built-in versions using the manifest.
101
+
102
+ Returns a ValidateTemplatesResult. Call .success to check overall status.
103
+ Raises FileNotFoundError if the manifest is missing.
104
+ """
105
+ from importlib.resources import files as _pkg_files
106
+
107
+ resolved = target_dir or (Path.cwd() / "faces" / "inspect")
108
+ manifest_path = resolved / INSPECT_TEMPLATE_MANIFEST
109
+ if not manifest_path.exists():
110
+ raise FileNotFoundError(
111
+ f"Missing manifest at {manifest_path}. Run eject_templates() first."
112
+ )
113
+
114
+ manifest = load_manifest(resolved, dataface_version=__version__)
115
+ manifest_templates: dict[str, dict] = manifest.get("templates", {})
116
+ templates_pkg = _pkg_files("dataface.core.inspect.templates")
117
+
118
+ missing, upstream_changed, custom_safe, unchanged = compare_templates(
119
+ resolved, manifest_templates, templates_pkg
120
+ )
121
+
122
+ return ValidateTemplatesResult(
123
+ success=not missing and not upstream_changed,
124
+ missing=missing,
125
+ upstream_changed=upstream_changed,
126
+ custom_safe=custom_safe,
127
+ unchanged=unchanged,
128
+ )
@@ -0,0 +1,170 @@
1
+ """Typed MCP client install verbs — per-client config writers for agent surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+
10
+ import tomli_w
11
+ from pydantic import BaseModel
12
+
13
+ if sys.version_info >= (3, 11):
14
+ import tomllib
15
+ else: # pragma: no cover - exercised on 3.10 CI
16
+ import tomli as tomllib
17
+
18
+
19
+ # Client name → (config file path, servers key, detection paths, format).
20
+ # Kept as a plain dict so tests can patch individual entries (e.g. redirect
21
+ # the Claude Desktop path into a tmp directory).
22
+ MCP_CLIENTS: dict[str, tuple[Path, str, tuple[Path, ...], str]] = {
23
+ "cursor": (Path(".cursor/mcp.json"), "mcpServers", (Path(".cursor"),), "json"),
24
+ "codex": (
25
+ Path(".codex/config.toml"),
26
+ "mcp_servers",
27
+ (Path(".codex"), Path("AGENTS.md")),
28
+ "toml",
29
+ ),
30
+ "claude": (
31
+ Path.home() / ".config" / "claude" / "config.json",
32
+ "mcpServers",
33
+ (Path.home() / ".config" / "claude",),
34
+ "json",
35
+ ),
36
+ "vscode": (Path(".vscode/mcp.json"), "servers", (Path(".vscode"),), "json"),
37
+ "claude-code": (Path(".mcp.json"), "mcpServers", (Path("CLAUDE.md"),), "json"),
38
+ "copilot": (
39
+ Path(".github/copilot/mcp.json"),
40
+ "servers",
41
+ (Path(".github"),),
42
+ "json",
43
+ ),
44
+ }
45
+
46
+
47
+ class McpClient(BaseModel):
48
+ name: str
49
+ config_path: Path
50
+ servers_key: str
51
+ detect_paths: tuple[Path, ...]
52
+ config_format: Literal["json", "toml"]
53
+ current_config: dict[str, Any] | None = None
54
+
55
+
56
+ class InstallResult(BaseModel):
57
+ client_name: str
58
+ config_path: Path
59
+ already_configured: bool
60
+ updated: bool
61
+ message: str
62
+
63
+
64
+ def list_clients() -> list[McpClient]:
65
+ """Return all supported MCP clients from the current MCP_CLIENTS registry."""
66
+ return [
67
+ McpClient(
68
+ name=name,
69
+ config_path=cfg,
70
+ servers_key=sk,
71
+ detect_paths=dp,
72
+ config_format=fmt, # type: ignore[arg-type]
73
+ )
74
+ for name, (cfg, sk, dp, fmt) in MCP_CLIENTS.items()
75
+ ]
76
+
77
+
78
+ def install_for_client(
79
+ client: McpClient,
80
+ *,
81
+ server_command: list[str],
82
+ config_root: Path,
83
+ force: bool = False,
84
+ ) -> InstallResult:
85
+ """Write the MCP server entry for one client config file.
86
+
87
+ server_command: full argv for the server, e.g. ["dft", "mcp", "serve"].
88
+ config_root: workspace root for resolving relative config paths.
89
+ """
90
+ server_entry: dict[str, Any] = {
91
+ "command": server_command[0],
92
+ "args": server_command[1:],
93
+ }
94
+ abs_path = (
95
+ client.config_path
96
+ if client.config_path.is_absolute()
97
+ else config_root / client.config_path
98
+ )
99
+ if client.config_format == "toml":
100
+ msg = _upsert_toml_mcp_config(abs_path, client.servers_key, server_entry, force)
101
+ else:
102
+ msg = _upsert_mcp_config(abs_path, client.servers_key, server_entry, force)
103
+
104
+ already = msg is None
105
+ return InstallResult(
106
+ client_name=client.name,
107
+ config_path=abs_path,
108
+ already_configured=already,
109
+ updated=not already and "Updated" in (msg or ""),
110
+ message=msg or f" {abs_path} already has dataface (use -f to update)",
111
+ )
112
+
113
+
114
+ def _upsert_mcp_config(
115
+ config_path: Path,
116
+ servers_key: str,
117
+ server_entry: dict[str, Any],
118
+ force: bool,
119
+ ) -> str | None:
120
+ """Add dataface to a JSON MCP config file, preserving existing content.
121
+
122
+ Returns a status message, or None if skipped (already configured).
123
+ """
124
+ config_path.parent.mkdir(parents=True, exist_ok=True)
125
+
126
+ existing: dict[str, Any] = {}
127
+ if config_path.exists():
128
+ try:
129
+ existing = json.loads(config_path.read_text())
130
+ if not isinstance(existing, dict):
131
+ existing = {}
132
+ except (json.JSONDecodeError, OSError):
133
+ existing = {}
134
+
135
+ already_has = servers_key in existing and "dataface" in existing.get(
136
+ servers_key, {}
137
+ )
138
+ if already_has and not force:
139
+ return None
140
+
141
+ existing.setdefault(servers_key, {})
142
+ existing[servers_key]["dataface"] = server_entry
143
+ config_path.write_text(json.dumps(existing, indent=2) + "\n")
144
+ return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"
145
+
146
+
147
+ def _upsert_toml_mcp_config(
148
+ config_path: Path,
149
+ servers_key: str,
150
+ server_entry: dict[str, Any],
151
+ force: bool,
152
+ ) -> str | None:
153
+ """Add dataface to a TOML MCP config file, preserving existing content.
154
+
155
+ Returns a status message, or None if skipped (already configured).
156
+ """
157
+ config_path.parent.mkdir(parents=True, exist_ok=True)
158
+
159
+ existing: dict[str, Any] = {}
160
+ if config_path.exists():
161
+ existing = tomllib.loads(config_path.read_text())
162
+
163
+ already_has = "dataface" in existing.get(servers_key, {})
164
+ if already_has and not force:
165
+ return None
166
+
167
+ existing.setdefault(servers_key, {})
168
+ existing[servers_key]["dataface"] = server_entry
169
+ config_path.write_text(tomli_w.dumps(existing))
170
+ return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"