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
dataface/cli/main.py ADDED
@@ -0,0 +1,1501 @@
1
+ """Main CLI entry point."""
2
+
3
+ import importlib.util
4
+ import logging
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Annotated, Any, Literal
9
+
10
+ import typer
11
+ from rich import box
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from typer.core import TyperGroup
15
+
16
+ from dataface._install_hint import install_hint
17
+ from dataface.cli._console import is_plain_output
18
+ from dataface.cli._parsing import parse_kv_pairs
19
+ from dataface.cli.commands import (
20
+ chat as chat_cmd,
21
+ describe as describe_cmd,
22
+ docs as docs_cmd,
23
+ extension as extension_cmd,
24
+ init as init_cmd,
25
+ inspect as inspect_cmd,
26
+ query as query_cmd,
27
+ render as render_cmd,
28
+ schema as schema_cmd,
29
+ search as search_cmd,
30
+ serve as serve_cmd,
31
+ skills as skills_cmd,
32
+ validate as validate_cmd,
33
+ )
34
+ from dataface.cli.commands.mcp_init import run_init as _mcp_run_init
35
+ from dataface.cli.commands.skills_init import run_init_skills as _skills_run_init
36
+ from dataface.core.dashboard import RenderFormat
37
+ from dataface.core.project_roots import find_dataface_project
38
+
39
+ # Evaluated once at process startup — mid-session env changes won't take effect.
40
+ _RICH_MARKUP_MODE: Literal["rich"] | None = None if is_plain_output() else "rich"
41
+ # Probe for private package once at startup; avoids repeated find_spec calls.
42
+ _SUPER_SCHEMA_AVAILABLE: bool = (
43
+ importlib.util.find_spec("dataface_super_schema") is not None
44
+ )
45
+
46
+ # MCP subcommand app
47
+ mcp_app = typer.Typer(
48
+ name="mcp",
49
+ help="MCP (Model Context Protocol) server commands for AI assistant integration.",
50
+ no_args_is_help=True,
51
+ pretty_exceptions_show_locals=False,
52
+ rich_markup_mode=_RICH_MARKUP_MODE,
53
+ )
54
+
55
+ # Init subcommand app — `dft init` (project scaffold), `dft init mcp [client]`
56
+ # (AI integration), `dft init code|cursor|vscode` (editor extension install).
57
+ init_app = typer.Typer(
58
+ name="init",
59
+ help="Bootstrap a Dataface project, plus its AI / editor integrations.",
60
+ invoke_without_command=True,
61
+ pretty_exceptions_show_locals=False,
62
+ rich_markup_mode=_RICH_MARKUP_MODE,
63
+ )
64
+
65
+ # Inspect subcommand app
66
+ inspect_app = typer.Typer(
67
+ name="inspect",
68
+ help="Inspect database tables and manage inspect templates.",
69
+ invoke_without_command=True,
70
+ pretty_exceptions_show_locals=False,
71
+ rich_markup_mode=_RICH_MARKUP_MODE,
72
+ )
73
+
74
+ # Shared --project-dir option. Every user-visible command that accepts a
75
+ # project root uses this alias so help text, validation, and DFT_PROJECT_DIR
76
+ # env-var wiring stay in lock-step.
77
+ ProjectDirOption = Annotated[
78
+ Path | None,
79
+ typer.Option(
80
+ "--project-dir",
81
+ exists=True,
82
+ file_okay=False,
83
+ dir_okay=True,
84
+ resolve_path=True,
85
+ envvar="DFT_PROJECT_DIR",
86
+ help="Project root for resolving face paths and finding project config",
87
+ ),
88
+ ]
89
+
90
+
91
+ # Configure logging for CLI (only if no handlers configured yet)
92
+ if not logging.getLogger().handlers:
93
+ logging.basicConfig(
94
+ level=logging.INFO,
95
+ format="%(name)s - %(levelname)s - %(message)s",
96
+ )
97
+
98
+
99
+ def _has_faces_marker(start: Path) -> bool:
100
+ """True iff *start* sits inside a Dataface project that has a ``faces/`` directory.
101
+
102
+ Anchors on :func:`find_dataface_project` so the walk-up stops at the
103
+ project root (any of ``dataface.yml``/``dataface.yaml``/``dbt_project.yml``)
104
+ rather than blindly hunting upward for stray ``faces/`` directories
105
+ elsewhere on the filesystem. A bare-dbt repo with no scaffolded
106
+ ``faces/`` returns False — that's the case the banner exists for.
107
+ """
108
+ if (start / "faces").is_dir():
109
+ return True
110
+ project = find_dataface_project(start)
111
+ return project is not None and (project / "faces").is_dir()
112
+
113
+
114
+ def _render_init_banner() -> None:
115
+ """Print a nudge to run ``dft init``.
116
+
117
+ Only called from :class:`_RootHelpGroup.format_help` when cwd is
118
+ not inside a scaffolded project. In plain mode (agent context or
119
+ piped stdout) the banner renders as a single plain line so context
120
+ captures do not pick up Panel chrome.
121
+ """
122
+ if is_plain_output():
123
+ Console(force_terminal=False, no_color=True).print(
124
+ "Welcome to Dataface — run `dft init` to start a new project."
125
+ )
126
+ return
127
+ Console().print(
128
+ Panel(
129
+ "Run [bold cyan]dft init[/bold cyan] to start a new project!",
130
+ title="Welcome to Dataface",
131
+ title_align="left",
132
+ border_style="yellow",
133
+ box=box.ROUNDED,
134
+ padding=(0, 1),
135
+ )
136
+ )
137
+
138
+
139
+ class _RootHelpGroup(TyperGroup):
140
+ """Root-only group: alphabetize the Options panel + commands and gate the init banner.
141
+
142
+ Click otherwise displays params in declaration order with the
143
+ auto-added `--help` last; that puts `--version` ahead of `--help`
144
+ on the root help. TyperGroup also overrides Click's sorted
145
+ list_commands with registration order, which buckets sub-typer
146
+ groups after leaf commands within the same rich_help_panel (e.g.
147
+ `init` after docs/playground/skills under Reference). Sorting here
148
+ interleaves them so each panel reads A-Z. Also prints the "run
149
+ `dft init`" banner above the help body when cwd isn't a scaffolded
150
+ Dataface project. Intentionally attached to the root Typer only —
151
+ sub-typer and leaf-command orders are left as authored, and
152
+ sub-typer help never shows the banner.
153
+ """
154
+
155
+ def get_params(self, ctx: Any) -> list[Any]:
156
+ def key(p: Any) -> str:
157
+ for opt in getattr(p, "opts", ()) or ():
158
+ if opt.startswith("--"):
159
+ return opt.lstrip("-").lower()
160
+ return (p.name or "").lower()
161
+
162
+ return sorted(super().get_params(ctx), key=key)
163
+
164
+ def list_commands(self, ctx: Any) -> list[str]:
165
+ # typer.rich_utils.rich_format_help iterates this list once and
166
+ # buckets by rich_help_panel; the per-panel order is the iteration
167
+ # order, and the panel-section order is determined by which panel
168
+ # gets its first command first. Sort by (panel-rank, name) so the
169
+ # documented panel order (Dashboards -> Data & SQL -> AI -> Reference)
170
+ # survives while sub-typer groups interleave alphabetically with
171
+ # leaf commands inside each panel.
172
+ panel_by_name: dict[str, str] = {}
173
+ for n in super().list_commands(ctx):
174
+ cmd = self.get_command(ctx, n)
175
+ panel_by_name[n] = getattr(cmd, "rich_help_panel", None) or ""
176
+ panel_rank: dict[str, int] = {}
177
+ for panel in panel_by_name.values():
178
+ panel_rank.setdefault(panel, len(panel_rank))
179
+ return sorted(panel_by_name, key=lambda n: (panel_rank[panel_by_name[n]], n))
180
+
181
+ def format_help(self, ctx: Any, formatter: Any) -> None:
182
+ if not _has_faces_marker(Path.cwd()):
183
+ _render_init_banner()
184
+ super().format_help(ctx, formatter)
185
+
186
+
187
+ # `help_option_names` on the root Click context inherits down to every
188
+ # sub-Typer's group context and every leaf command, so a single setting
189
+ # here gives `-h` to the whole CLI tree.
190
+ app = typer.Typer(
191
+ name="dft",
192
+ help="Declarative, dbt-native dashboards in YAML.",
193
+ no_args_is_help=True,
194
+ pretty_exceptions_show_locals=False,
195
+ add_completion=False,
196
+ context_settings={"help_option_names": ["-h", "--help"]},
197
+ cls=_RootHelpGroup,
198
+ rich_markup_mode=_RICH_MARKUP_MODE,
199
+ )
200
+
201
+
202
+ # =============================================================================
203
+ # init_app — `dft init …` subcommands (registered as a sub-typer at the bottom)
204
+ # =============================================================================
205
+
206
+
207
+ @init_app.callback(invoke_without_command=True)
208
+ def init_default(
209
+ ctx: typer.Context,
210
+ project_dir: ProjectDirOption = None,
211
+ force: Annotated[
212
+ bool,
213
+ typer.Option("--force", "-f", help="Overwrite existing scaffold files"),
214
+ ] = False,
215
+ yes: Annotated[
216
+ bool,
217
+ typer.Option("--yes", "-y", help="Accept all defaults without prompting"),
218
+ ] = False,
219
+ agents_md: Annotated[
220
+ bool | None,
221
+ typer.Option("--agents-md/--no-agents-md", help="Write/append AGENTS.md"),
222
+ ] = None,
223
+ claude_md: Annotated[
224
+ bool | None,
225
+ typer.Option("--claude-md/--no-claude-md", help="Create CLAUDE.md pointer"),
226
+ ] = None,
227
+ skills: Annotated[
228
+ bool | None,
229
+ typer.Option(
230
+ "--skills/--no-skills",
231
+ help="Install workflow skills to agent skill directories",
232
+ ),
233
+ ] = None,
234
+ mcp: Annotated[
235
+ bool | None,
236
+ typer.Option("--mcp/--no-mcp", help="Set up MCP server for AI assistants"),
237
+ ] = None,
238
+ chat_extra: Annotated[
239
+ bool | None,
240
+ typer.Option(
241
+ "--chat-extra/--no-chat-extra",
242
+ help="Install dataface[chat] extra (enables dft chat)",
243
+ ),
244
+ ] = None,
245
+ with_playground: Annotated[
246
+ bool | None,
247
+ typer.Option(
248
+ "--with-playground/--no-with-playground",
249
+ help="Install dataface[playground] extra (enables dft playground)",
250
+ ),
251
+ ] = None,
252
+ vscode: Annotated[
253
+ bool | None,
254
+ typer.Option(
255
+ "--vscode/--no-vscode", help="Install Dataface extension into VS Code"
256
+ ),
257
+ ] = None,
258
+ cursor: Annotated[
259
+ bool | None,
260
+ typer.Option(
261
+ "--cursor/--no-cursor", help="Install Dataface extension into Cursor"
262
+ ),
263
+ ] = None,
264
+ ) -> None:
265
+ """Bootstrap a Dataface project in an existing repo.
266
+
267
+ Detects dbt projects, creates faces/ and faces/partials/, ejects inspect
268
+ templates, and writes starter dashboards. Optionally wires up MCP for AI
269
+ assistants and installs the dataface[chat] extra and IDE extensions.
270
+
271
+ Safe to re-run — existing files are never overwritten unless --force is used.
272
+ AGENTS.md: appends a short Dataface blurb when the file exists without markers;
273
+ refreshes only the ``<!-- dft-dataface:* -->`` section when markers are present.
274
+
275
+ Subcommands:
276
+ dft init skills [target] # Install workflow skills for AI assistants
277
+ dft init mcp [client] # Wire up MCP server
278
+ dft init code # Install Dataface extension into VS Code
279
+ dft init cursor # …or Cursor
280
+
281
+ \b
282
+ Examples:
283
+ dft init # Init with interactive wizard
284
+ dft init --yes # Accept all defaults non-interactively
285
+ dft init --project-dir ./myrepo # Init in a specific directory
286
+ dft init --force # Re-scaffold, overwriting files
287
+ dft init --no-mcp --no-vscode # Skip MCP and IDE extension
288
+ dft init --with-playground # Also install dataface[playground]
289
+ """
290
+ if ctx.invoked_subcommand is not None:
291
+ return
292
+ init_cmd.run_wizard(
293
+ project_dir=project_dir,
294
+ force=force,
295
+ yes=yes,
296
+ agents_md=agents_md,
297
+ claude_md=claude_md,
298
+ skills=skills,
299
+ mcp=mcp,
300
+ chat_extra=chat_extra,
301
+ with_playground=with_playground,
302
+ vscode=vscode,
303
+ cursor=cursor,
304
+ )
305
+
306
+
307
+ @init_app.command("code")
308
+ def init_code() -> None:
309
+ """Install the Dataface extension into VS Code.
310
+
311
+ Installs the latest release of the Dataface extension into VS Code.
312
+ Idempotent — re-runs upgrade to the newest version.
313
+ """
314
+ raise typer.Exit(extension_cmd.install_extension("code", emit=typer.echo))
315
+
316
+
317
+ @init_app.command("cursor")
318
+ def init_cursor() -> None:
319
+ """Install the Dataface extension into Cursor.
320
+
321
+ Installs the latest release of the Dataface extension into Cursor.
322
+ Idempotent — re-runs upgrade to the newest version.
323
+ """
324
+ raise typer.Exit(extension_cmd.install_extension("cursor", emit=typer.echo))
325
+
326
+
327
+ @init_app.command("vscode")
328
+ def init_vscode() -> None:
329
+ """Alias for `dft init code`."""
330
+ raise typer.Exit(extension_cmd.install_extension("code", emit=typer.echo))
331
+
332
+
333
+ # `dft init skills [target]` — file-based workflow skill install.
334
+ @init_app.command("skills")
335
+ def init_skills(
336
+ target: Annotated[
337
+ str | None,
338
+ typer.Argument(
339
+ help="Install target: agents (Cursor/Codex/Copilot), codex, or claude"
340
+ ),
341
+ ] = None,
342
+ all_targets: Annotated[
343
+ bool,
344
+ typer.Option("--all", help="Install to every detected skill directory"),
345
+ ] = False,
346
+ dir_override: Annotated[
347
+ Path | None,
348
+ typer.Option(
349
+ "--dir",
350
+ help="Explicit skills directory (e.g. .agents/skills)",
351
+ file_okay=False,
352
+ dir_okay=True,
353
+ resolve_path=True,
354
+ ),
355
+ ] = None,
356
+ force: Annotated[
357
+ bool,
358
+ typer.Option("--force", "-f", help="Overwrite existing skill files"),
359
+ ] = False,
360
+ check: Annotated[
361
+ bool,
362
+ typer.Option(
363
+ "--check",
364
+ help="Dry run: show what would be installed without writing files",
365
+ ),
366
+ ] = False,
367
+ project_dir: ProjectDirOption = None,
368
+ ) -> None:
369
+ """Install Dataface workflow skills for file-based agent auto-discovery.
370
+
371
+ Writes CLI-rendered skill files to ``.agents/skills/`` (Cursor, Codex,
372
+ Copilot) and/or ``.claude/skills/`` (Claude Code). Does not configure MCP
373
+ or modify AGENTS.md / CLAUDE.md.
374
+
375
+ \b
376
+ Examples:
377
+ dft init skills # Detect targets and install
378
+ dft init skills agents # .agents/skills/ only
379
+ dft init skills claude # .claude/skills/ only
380
+ dft init skills --all # Every detected target dir
381
+ dft init skills --dir PATH # Explicit destination
382
+ dft init skills --check # Dry run
383
+ dft init skills -f # Overwrite existing files
384
+ """
385
+ _skills_run_init(
386
+ target=target,
387
+ all_targets=all_targets,
388
+ dir_override=dir_override,
389
+ force=force,
390
+ check=check,
391
+ project_dir=project_dir,
392
+ )
393
+
394
+
395
+ # `dft init mcp [client]` — MCP config only (skills: `dft init skills`).
396
+ @init_app.command("mcp")
397
+ def init_mcp(
398
+ client: Annotated[
399
+ str | None,
400
+ typer.Argument(
401
+ help=(
402
+ "Client to configure: cursor, vscode, claude, claude-code, "
403
+ "codex, copilot, or print. Omit to auto-detect."
404
+ )
405
+ ),
406
+ ] = None,
407
+ all_clients: Annotated[
408
+ bool,
409
+ typer.Option("--all", help="Write MCP config files for every supported client"),
410
+ ] = False,
411
+ force: Annotated[
412
+ bool,
413
+ typer.Option("--force", "-f", help="Overwrite existing MCP client config"),
414
+ ] = False,
415
+ project_dir: ProjectDirOption = None,
416
+ ) -> None:
417
+ """Add Dataface to your AI client's MCP configuration.
418
+
419
+ Writes MCP server entries only. Install workflow skills separately with
420
+ ``dft init skills``.
421
+
422
+ The project root is resolved by walking up from the current directory
423
+ looking for dataface.yml, dataface.yaml, or dbt_project.yml. Pass
424
+ --project-dir to override. When the project root differs from your AI
425
+ client's workspace, the recorded server command points at the project
426
+ so it starts in the right place.
427
+
428
+ \b
429
+ Examples:
430
+ dft init mcp # Auto-detect clients + project
431
+ dft init mcp cursor # Configure Cursor only
432
+ dft init mcp claude-code # Configure Claude Code
433
+ dft init mcp --all # Write every supported config file
434
+ dft init mcp --project-dir ./analytics # Target a specific project dir
435
+ dft init mcp print # Print config JSON (for manual setup)
436
+ """
437
+ _mcp_run_init(
438
+ client=client,
439
+ all_clients=all_clients,
440
+ force=force,
441
+ project_dir=project_dir,
442
+ )
443
+
444
+
445
+ # =============================================================================
446
+ # inspect_app — `dft inspect …` subcommands (registered hidden at the bottom)
447
+ # =============================================================================
448
+
449
+
450
+ @inspect_app.callback(invoke_without_command=True)
451
+ def inspect_default(
452
+ ctx: typer.Context,
453
+ connection: Annotated[
454
+ str,
455
+ typer.Option(help="Database connection string", hidden=True),
456
+ ] = ":memory:",
457
+ dialect: Annotated[
458
+ str,
459
+ typer.Option(
460
+ help="SQL dialect (duckdb, postgres, bigquery, etc.)", hidden=True
461
+ ),
462
+ ] = "duckdb",
463
+ schema: Annotated[
464
+ str | None,
465
+ typer.Option(help="Schema filter", hidden=True),
466
+ ] = None,
467
+ output: Annotated[
468
+ str,
469
+ typer.Option(help="Path to save inspection JSON", hidden=True),
470
+ ] = "target/super_schema.json",
471
+ approximate: Annotated[
472
+ str,
473
+ typer.Option(help="Approximate profiling mode: auto, on, off", hidden=True),
474
+ ] = "auto",
475
+ include: Annotated[
476
+ str | None,
477
+ typer.Option(help="Glob pattern to include tables", hidden=True),
478
+ ] = None,
479
+ exclude: Annotated[
480
+ str | None,
481
+ typer.Option(help="Glob pattern to exclude tables", hidden=True),
482
+ ] = None,
483
+ ) -> None:
484
+ """Manage inspect templates and profile database tables (with dataface-super-schema).
485
+
486
+ Without subcommand: profiles all tables (requires dataface-super-schema).
487
+
488
+ \b
489
+ Examples:
490
+ dft inspect eject model # Copy template to faces/inspect/
491
+ dft inspect templates # List built-in templates
492
+ dft inspect table orders # Profile a table (needs private pkg)
493
+ """
494
+ if ctx.invoked_subcommand is not None:
495
+ return
496
+ if not _SUPER_SCHEMA_AVAILABLE:
497
+ typer.echo(
498
+ "Batch profiling requires the dataface-super-schema package.\n"
499
+ "Install it from the monorepo or your private registry.\n\n"
500
+ "Run 'dft inspect --help' for available template-management commands.",
501
+ err=True,
502
+ )
503
+ raise typer.Exit(1)
504
+ from dataface_super_schema.cli.commands.inspect import ( # noqa: PLC0415
505
+ inspect_all_command,
506
+ )
507
+
508
+ inspect_all_command(
509
+ connection=connection,
510
+ dialect=dialect,
511
+ schema=schema,
512
+ output=output,
513
+ approximate=approximate,
514
+ include=include,
515
+ exclude=exclude,
516
+ )
517
+
518
+
519
+ @inspect_app.command("eject")
520
+ def inspect_eject(
521
+ templates: Annotated[
522
+ list[str] | None,
523
+ typer.Argument(help="Template names to eject (e.g., model quality)"),
524
+ ] = None,
525
+ all_templates: Annotated[
526
+ bool,
527
+ typer.Option("--all", help="Eject all available templates"),
528
+ ] = False,
529
+ force: Annotated[
530
+ bool,
531
+ typer.Option("--force", "-f", help="Overwrite existing files"),
532
+ ] = False,
533
+ output_dir: Annotated[
534
+ Path | None,
535
+ typer.Option(
536
+ "--output", "-o", help="Output directory (default: faces/inspect/)"
537
+ ),
538
+ ] = None,
539
+ ) -> None:
540
+ """Copy inspect templates to faces/inspect/ for customization.
541
+
542
+ Ejected templates can be modified to customize the inspect dashboards.
543
+ The server will use your customized version instead of the built-in template.
544
+
545
+ \b
546
+ Examples:
547
+ dft inspect eject model # Eject just the model template
548
+ dft inspect eject model quality # Eject specific templates
549
+ dft inspect eject --all # Eject all templates
550
+ dft inspect eject model --force # Overwrite existing
551
+ dft inspect eject --all -o custom/ # Custom output directory
552
+ """
553
+ inspect_cmd.eject_command(
554
+ templates=templates or [],
555
+ all_templates=all_templates,
556
+ force=force,
557
+ output_dir=output_dir,
558
+ )
559
+
560
+
561
+ @inspect_app.command("templates")
562
+ def inspect_templates() -> None:
563
+ """List available inspect templates.
564
+
565
+ Shows all built-in inspect templates that can be ejected and customized.
566
+
567
+ \b
568
+ Examples:
569
+ dft inspect templates
570
+ """
571
+ inspect_cmd.templates_command()
572
+
573
+
574
+ @inspect_app.command("validate-templates")
575
+ def inspect_validate_templates(
576
+ output_dir: Annotated[
577
+ Path | None,
578
+ typer.Option(
579
+ "--output",
580
+ "-o",
581
+ help="Inspect template directory (default: faces/inspect/)",
582
+ ),
583
+ ] = None,
584
+ ) -> None:
585
+ """Validate ejected inspect templates against current built-in template versions.
586
+
587
+ Helps teams detect when upstream template changes require rebasing custom templates.
588
+ """
589
+ inspect_cmd.validate_ejected_templates_command(output_dir=output_dir)
590
+
591
+
592
+ # =============================================================================
593
+ # mcp_app — `dft mcp …` subcommands (registered as a sub-typer at the bottom)
594
+ # =============================================================================
595
+
596
+
597
+ @mcp_app.command("serve")
598
+ def mcp_serve(
599
+ project_dir: ProjectDirOption = None,
600
+ ) -> None:
601
+ """Start the MCP server for AI assistant integration.
602
+
603
+ This command starts an MCP (Model Context Protocol) server that enables
604
+ AI assistants like Claude, Cursor, and ChatGPT to interact with Dataface
605
+ dashboards.
606
+
607
+ The server speaks the MCP protocol over standard input/output — the
608
+ transport AI clients expect.
609
+
610
+ Requires the ``mcp`` extra (run ``dft mcp serve`` with it missing and
611
+ the printed install command is the canonical one for your environment).
612
+
613
+ \b
614
+ Examples:
615
+ dft mcp serve
616
+ dft mcp serve --project-dir ./my-project
617
+
618
+ \b
619
+ Configuration for Claude Desktop (~/.config/claude/config.json):
620
+ {
621
+ "mcpServers": {
622
+ "dataface": {
623
+ "command": "dft",
624
+ "args": ["mcp", "serve"]
625
+ }
626
+ }
627
+ }
628
+ """
629
+ try:
630
+ from dataface.ai.mcp import run_server
631
+ except ImportError as e:
632
+ typer.echo("MCP server requires additional dependencies.", err=True)
633
+ typer.echo(f" Install with: {install_hint('mcp')}", err=True)
634
+ missing_module = str(e).split("'")[1] if "'" in str(e) else "unknown"
635
+ typer.echo(f" Missing: {missing_module}", err=True)
636
+ raise typer.Exit(1) from None
637
+
638
+ import asyncio
639
+
640
+ # Change to specified directory if provided
641
+ if project_dir:
642
+ os.chdir(project_dir)
643
+ typer.echo(f"Working directory: {project_dir}", err=True)
644
+
645
+ # Run the MCP server (stdio mode)
646
+ # Note: We don't print anything to stdout as MCP uses it for communication
647
+ try:
648
+ asyncio.run(run_server())
649
+ except KeyboardInterrupt:
650
+ # Clean exit on Ctrl+C
651
+ raise typer.Exit(0) from None
652
+
653
+
654
+ # =============================================================================
655
+ # Root commands — registered in alphabetical order within each panel so that
656
+ # commands appear A–Z inside Dashboards, Data & SQL, AI, and Reference.
657
+ # Panel order (Dashboards -> Data & SQL -> AI -> Reference) is determined by
658
+ # which panel's first command is registered first — so panel blocks must stay
659
+ # in that order. Sub-typer add_typer() calls are inlined at their correct
660
+ # alphabetical positions within each panel's block.
661
+ # =============================================================================
662
+
663
+
664
+ # --- Dashboards -------------------------------------------------------------
665
+
666
+
667
+ @app.command("describe", rich_help_panel="Dashboards")
668
+ def describe(
669
+ paths: Annotated[
670
+ list[Path],
671
+ typer.Argument(
672
+ metavar="[PATH]...",
673
+ help="Path(s) to face YAML files or directories to describe.",
674
+ ),
675
+ ],
676
+ json_output: Annotated[
677
+ bool,
678
+ typer.Option("--json", help="Output as JSON"),
679
+ ] = False,
680
+ project_dir: ProjectDirOption = None,
681
+ ) -> None:
682
+ """Describe a dashboard's queries, charts, variables, and layout.
683
+
684
+ Compiles the face YAML and returns a structured summary without executing
685
+ any queries. Use this for orientation when picking up an unfamiliar dashboard.
686
+ Accepts multiple paths; each may be a file or a directory (walked recursively).
687
+
688
+ \b
689
+ Examples:
690
+ dft describe faces/sales.yml
691
+ dft describe faces/sales.yml --json
692
+ dft describe faces/sales.yml --project-dir ./myrepo
693
+ dft describe faces/
694
+ dft describe faces/*.yml --json | jq '.[] | select(.charts | length > 5)'
695
+ """
696
+ describe_cmd.describe_command(
697
+ paths,
698
+ json_output=json_output,
699
+ project_dir=project_dir,
700
+ )
701
+
702
+
703
+ @app.command("render", rich_help_panel="Dashboards")
704
+ def render(
705
+ face: Annotated[
706
+ Path,
707
+ typer.Argument(
708
+ # No exists=/file_okay=: "-" is a valid stdin sentinel and must not fail the check.
709
+ help='Path to face YAML file, or "-" to read YAML from stdin',
710
+ ),
711
+ ],
712
+ output: Annotated[
713
+ Path | None,
714
+ typer.Option(help="Output file path (default: face name with extension)"),
715
+ ] = None,
716
+ format: Annotated[
717
+ RenderFormat,
718
+ typer.Option(
719
+ help="Output format: svg, html, png, pdf, terminal, json, text, or yaml"
720
+ ),
721
+ ] = "svg",
722
+ project_dir: ProjectDirOption = None,
723
+ var: Annotated[
724
+ list[str] | None,
725
+ typer.Option(help="Variable value as key=value (repeatable)"),
726
+ ] = None,
727
+ no_cache: Annotated[
728
+ bool,
729
+ typer.Option(
730
+ "--no-cache", help="Bypass all query caches and re-run from scratch"
731
+ ),
732
+ ] = False,
733
+ json_errors: Annotated[
734
+ bool,
735
+ typer.Option(
736
+ "--json-errors",
737
+ help=(
738
+ "Emit errors as JSON to stdout instead of formatted panels. "
739
+ "Only affects the error path; the success-path output is "
740
+ "still controlled by --format."
741
+ ),
742
+ ),
743
+ ] = False,
744
+ no_warnings: Annotated[
745
+ bool,
746
+ typer.Option(
747
+ "--no-warnings",
748
+ help=(
749
+ "Suppress warning output to stderr. Warnings are still included "
750
+ "in --format json output so agents and consumers always see them."
751
+ ),
752
+ ),
753
+ ] = False,
754
+ ignore_warning: Annotated[
755
+ list[str] | None,
756
+ typer.Option(
757
+ "--ignore-warning",
758
+ help=(
759
+ "Suppress a specific warning code (repeatable). Suppressed warnings "
760
+ "move to suppressed_warnings in --format json output. "
761
+ "Unknown codes print a notice but do not exit non-zero."
762
+ ),
763
+ ),
764
+ ] = None,
765
+ ) -> None:
766
+ """Render a dashboard to SVG, HTML, PNG, PDF, or terminal.
767
+
768
+ Use "-" as the face argument to read YAML from stdin (useful for agents
769
+ and shell pipelines).
770
+
771
+ \b
772
+ Examples:
773
+ dft render faces/sales.yml
774
+ dft render faces/sales.yml --format html
775
+ dft render faces/sales.yml --var region=West --var category=Electronics
776
+ dft render faces/sales.yml --format terminal
777
+ dft render faces/sales.yml --no-cache
778
+ dft render faces/sales.yml --json-errors
779
+ dft render faces/sales.yml --no-warnings
780
+ dft render faces/sales.yml --ignore-warning BAR_COLOR_1_TO_1_WITH_X
781
+ echo 'charts: {c: {query: {type: csv, file: data.csv}}}' | dft render - --format terminal
782
+ """
783
+ variables = parse_kv_pairs(var or [], "--var")
784
+
785
+ use_cache = not no_cache
786
+ ignore_codes = set(ignore_warning) if ignore_warning else None
787
+
788
+ if str(face) == "-":
789
+ yaml_content = sys.stdin.read()
790
+ if not yaml_content.strip():
791
+ print("Error: No YAML input received from stdin", file=sys.stderr)
792
+ raise typer.Exit(1)
793
+ render_cmd.render_command_from_yaml(
794
+ yaml_content=yaml_content,
795
+ output=str(output) if output else None,
796
+ format=format,
797
+ project_dir=project_dir,
798
+ variables=variables or None,
799
+ use_cache=use_cache,
800
+ json_errors=json_errors,
801
+ no_warnings=no_warnings,
802
+ ignore_codes=ignore_codes,
803
+ )
804
+ else:
805
+ render_cmd.render_command(
806
+ face_path=face,
807
+ output=str(output) if output else None,
808
+ format=format,
809
+ project_dir=project_dir,
810
+ variables=variables or None,
811
+ use_cache=use_cache,
812
+ json_errors=json_errors,
813
+ no_warnings=no_warnings,
814
+ ignore_codes=ignore_codes,
815
+ )
816
+
817
+
818
+ @app.command("search", rich_help_panel="Dashboards")
819
+ def search(
820
+ query: Annotated[
821
+ str,
822
+ typer.Argument(help="Keywords to match against dashboard metadata"),
823
+ ],
824
+ json_output: Annotated[
825
+ bool,
826
+ typer.Option("--json", help="Output as JSON"),
827
+ ] = False,
828
+ limit: Annotated[
829
+ int,
830
+ typer.Option("--limit", help="Maximum results to return (default 10, max 50)"),
831
+ ] = 10,
832
+ project_dir: ProjectDirOption = None,
833
+ ) -> None:
834
+ """Search dashboards by keyword with ranked results.
835
+
836
+ Returns dashboards ranked by relevance to the query. Each hit includes
837
+ the title, summary, match score, source path, and matched query/chart names.
838
+
839
+ \b
840
+ Examples:
841
+ dft search revenue
842
+ dft search "monthly trends" --limit 5
843
+ dft search orders --json
844
+ """
845
+ search_cmd.search_command(
846
+ query=query,
847
+ json_output=json_output,
848
+ limit=limit,
849
+ project_dir=project_dir,
850
+ )
851
+
852
+
853
+ @app.command("serve", rich_help_panel="Dashboards")
854
+ def serve(
855
+ port: Annotated[
856
+ int | None,
857
+ typer.Option(help="Port number (auto-resolved if not set)"),
858
+ ] = None,
859
+ host: Annotated[
860
+ str,
861
+ typer.Option(help="Host address"),
862
+ ] = "localhost",
863
+ project_dir: ProjectDirOption = None,
864
+ connection: Annotated[
865
+ str | None,
866
+ typer.Option(help="Database connection string"),
867
+ ] = None,
868
+ dialect: Annotated[
869
+ str | None,
870
+ typer.Option(help="SQL dialect (auto-detected from dbt, or duckdb)"),
871
+ ] = None,
872
+ target: Annotated[
873
+ str | None,
874
+ typer.Option(
875
+ help="dbt target name (default: DBT_TARGET env, then profile default)"
876
+ ),
877
+ ] = None,
878
+ ) -> None:
879
+ """Start the dashboard server.
880
+
881
+ Renders your dashboards in the browser. Face file paths map
882
+ to URLs (faces/sales.yml → /sales/). Query params become variables.
883
+
884
+ Auto-discovers the project root by walking up from the current directory
885
+ to find dataface.yml or dbt_project.yml. When a dbt project is found,
886
+ the SQL dialect is inferred from the profile target.
887
+
888
+ Port is auto-resolved: --port flag > DFT_PORT env var > dataface.yml
889
+ port field > deterministic hash of project directory. If the chosen port
890
+ is occupied, the next available port is used automatically.
891
+
892
+ \b
893
+ Examples:
894
+ dft serve
895
+ dft serve --port 3000
896
+ dft serve --connection ./data.db --dialect duckdb
897
+ dft serve --target prod
898
+ """
899
+ serve_cmd.serve_command(
900
+ port=port,
901
+ host=host,
902
+ project_dir=project_dir,
903
+ connection=connection,
904
+ dialect=dialect,
905
+ target=target,
906
+ )
907
+
908
+
909
+ @app.command("validate", rich_help_panel="Dashboards")
910
+ def validate(
911
+ paths: Annotated[
912
+ list[Path] | None,
913
+ typer.Argument(
914
+ metavar="[PATH]...",
915
+ help="Face YAML files or directories (default: faces/)",
916
+ ),
917
+ ] = None,
918
+ project_dir: ProjectDirOption = None,
919
+ json_output: Annotated[
920
+ bool,
921
+ typer.Option("--json", help="Output as JSON"),
922
+ ] = False,
923
+ strict: Annotated[
924
+ bool,
925
+ typer.Option(help="Errors always fail; with --strict, warnings also fail."),
926
+ ] = False,
927
+ ) -> None:
928
+ """Fast YAML schema + cross-reference validation. No DB, no execute.
929
+
930
+ \b
931
+ Examples:
932
+ dft validate # Validate all faces in faces/
933
+ dft validate faces/ # Validate all faces in a directory
934
+ dft validate faces/sales.yml # Validate one file
935
+ dft validate faces/*.yml # Shell-expanded glob
936
+ dft validate faces/*.yml --json | jq '.[] | select(.success == false)'
937
+ dft validate faces/sales.yml faces/orders.yml --strict
938
+ """
939
+ validate_cmd.validate_command(
940
+ paths=paths,
941
+ project_dir=project_dir,
942
+ json_output=json_output,
943
+ strict=strict,
944
+ )
945
+
946
+
947
+ # --- Data & SQL -------------------------------------------------------------
948
+
949
+
950
+ @app.command(
951
+ "query",
952
+ rich_help_panel="Data & SQL",
953
+ help="""\b
954
+ SQL and named face queries — common forms:
955
+ dft query NAME --in FACE.yml
956
+ dft query 'SQL' --source SRC
957
+ dft query 'SQL' --validate
958
+ dft query 'SQL' --describe --source SRC
959
+
960
+ TARGET is a query name when --in is set; otherwise it's a SQL string.
961
+ --file reads SQL from a file. SQL execute requires --source.
962
+ --validate runs offline.""",
963
+ )
964
+ def query(
965
+ target: Annotated[
966
+ str | None,
967
+ typer.Argument(
968
+ metavar="TARGET",
969
+ help="Query name (with --in) or SQL string",
970
+ ),
971
+ ] = None,
972
+ face: Annotated[
973
+ Path | None,
974
+ typer.Option(
975
+ "--in",
976
+ "-i",
977
+ metavar="PATH",
978
+ help="Face YAML path; switches TARGET to named-query mode",
979
+ ),
980
+ ] = None,
981
+ source: Annotated[
982
+ str | None,
983
+ typer.Option(
984
+ "--source",
985
+ help="Data source (required for raw SQL execute; overrides face source for --describe)",
986
+ ),
987
+ ] = None,
988
+ validate: Annotated[
989
+ bool,
990
+ typer.Option(
991
+ "--validate",
992
+ help="Static SQL lint only (works for named queries and raw SQL)",
993
+ ),
994
+ ] = False,
995
+ describe: Annotated[
996
+ bool,
997
+ typer.Option(
998
+ "--describe",
999
+ help="Return column names and types (runs --validate gate first)",
1000
+ ),
1001
+ ] = False,
1002
+ file: Annotated[
1003
+ Path | None,
1004
+ typer.Option("--file", "-f", metavar="PATH", help="Read SQL from file"),
1005
+ ] = None,
1006
+ dialect: Annotated[
1007
+ str | None,
1008
+ typer.Option("--dialect", help="SQL dialect for lint (duckdb, bigquery, …)"),
1009
+ ] = None,
1010
+ var: Annotated[
1011
+ list[str] | None,
1012
+ typer.Option("--var", help="Variable override as key=value (repeatable)"),
1013
+ ] = None,
1014
+ limit: Annotated[
1015
+ int,
1016
+ typer.Option("--limit", help="Max rows, execute path (default 20, max 1000)"),
1017
+ ] = 20,
1018
+ show_suppressed: Annotated[
1019
+ bool,
1020
+ typer.Option("--show-suppressed", help="Include suppressed lint diagnostics"),
1021
+ ] = False,
1022
+ json_output: Annotated[
1023
+ bool,
1024
+ typer.Option("--json", help="JSON output"),
1025
+ ] = False,
1026
+ project_dir: ProjectDirOption = None,
1027
+ ) -> None:
1028
+ variables = parse_kv_pairs(var or [], "--var")
1029
+
1030
+ query_cmd.query_command(
1031
+ target=target,
1032
+ face=face,
1033
+ source=source,
1034
+ validate=validate,
1035
+ describe=describe,
1036
+ file=file,
1037
+ dialect=dialect,
1038
+ vars=variables or None,
1039
+ limit=limit,
1040
+ show_suppressed=show_suppressed,
1041
+ json_output=json_output,
1042
+ project_dir=project_dir,
1043
+ )
1044
+
1045
+
1046
+ _SCHEMA_SEARCH_PANEL = "Search"
1047
+
1048
+
1049
+ @app.command("schema", rich_help_panel="Data & SQL")
1050
+ def schema(
1051
+ source: Annotated[
1052
+ str | None,
1053
+ typer.Argument(
1054
+ metavar="SOURCE",
1055
+ help="Data source name (omit to list all sources).",
1056
+ ),
1057
+ ] = None,
1058
+ schema_name: Annotated[
1059
+ str | None,
1060
+ typer.Argument(
1061
+ metavar="SCHEMA",
1062
+ help="Schema/namespace name (requires SOURCE).",
1063
+ ),
1064
+ ] = None,
1065
+ table: Annotated[
1066
+ str | None,
1067
+ typer.Argument(
1068
+ metavar="TABLE",
1069
+ help="Table name for deep profiling (requires SOURCE SCHEMA).",
1070
+ ),
1071
+ ] = None,
1072
+ column: Annotated[
1073
+ str | None,
1074
+ typer.Argument(
1075
+ metavar="COLUMN",
1076
+ help="Column name for distribution detail (requires SOURCE SCHEMA TABLE).",
1077
+ ),
1078
+ ] = None,
1079
+ search: Annotated[
1080
+ str | None,
1081
+ typer.Option(
1082
+ "-s",
1083
+ "--search",
1084
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1085
+ help=(
1086
+ "Keyword/regexp — filters the table list when given with "
1087
+ "SOURCE + SCHEMA, otherwise searches the schema corpus "
1088
+ "(see Modes above for examples)."
1089
+ ),
1090
+ ),
1091
+ ] = None,
1092
+ scope: Annotated[
1093
+ str | None,
1094
+ typer.Option(
1095
+ "--scope",
1096
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1097
+ help=(
1098
+ "Comma-list of fields to search "
1099
+ "(name, description, tag, tests, meta, owner). Default: all."
1100
+ ),
1101
+ ),
1102
+ ] = None,
1103
+ regex: Annotated[
1104
+ bool,
1105
+ typer.Option(
1106
+ "--regex",
1107
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1108
+ help="Treat -s keyword as a regular expression.",
1109
+ ),
1110
+ ] = False,
1111
+ role: Annotated[
1112
+ str | None,
1113
+ typer.Option(
1114
+ "--role",
1115
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1116
+ help="Filter columns by profiled role (e.g. identifier, measure, time).",
1117
+ ),
1118
+ ] = None,
1119
+ tag: Annotated[
1120
+ str | None,
1121
+ typer.Option(
1122
+ "--tag",
1123
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1124
+ help="Filter to rows tagged with this value.",
1125
+ ),
1126
+ ] = None,
1127
+ has_test: Annotated[
1128
+ str | None,
1129
+ typer.Option(
1130
+ "--has-test",
1131
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1132
+ help="Filter columns whose dbt tests include this name (e.g. not_null).",
1133
+ ),
1134
+ ] = None,
1135
+ missing: Annotated[
1136
+ str | None,
1137
+ typer.Option(
1138
+ "--missing",
1139
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1140
+ help="Include rows that are missing this field (e.g. description).",
1141
+ ),
1142
+ ] = None,
1143
+ column_name: Annotated[
1144
+ str | None,
1145
+ typer.Option(
1146
+ "--column-name",
1147
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1148
+ help="Filter to columns matching this glob (e.g. `*_id`).",
1149
+ ),
1150
+ ] = None,
1151
+ table_name: Annotated[
1152
+ str | None,
1153
+ typer.Option(
1154
+ "--table-name",
1155
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1156
+ help="Filter to tables matching this glob (e.g. `stg_*`).",
1157
+ ),
1158
+ ] = None,
1159
+ fk_to: Annotated[
1160
+ str | None,
1161
+ typer.Option(
1162
+ "--fk-to",
1163
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1164
+ help="Filter to columns with relationships pointing at this table.",
1165
+ ),
1166
+ ] = None,
1167
+ meta: Annotated[
1168
+ list[str] | None,
1169
+ typer.Option(
1170
+ "--meta",
1171
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1172
+ help="Filter by exact-match meta key=value (repeatable).",
1173
+ ),
1174
+ ] = None,
1175
+ fields: Annotated[
1176
+ str | None,
1177
+ typer.Option(
1178
+ "--fields",
1179
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1180
+ help=(
1181
+ "Show only these fields on each hit "
1182
+ "(comma-list, e.g. `location,matched_field`)."
1183
+ ),
1184
+ ),
1185
+ ] = None,
1186
+ limit: Annotated[
1187
+ int | None,
1188
+ typer.Option(
1189
+ "--limit",
1190
+ rich_help_panel=_SCHEMA_SEARCH_PANEL,
1191
+ help="Truncate to N hits (total/truncated reported).",
1192
+ ),
1193
+ ] = None,
1194
+ json_output: Annotated[
1195
+ bool,
1196
+ typer.Option(
1197
+ "--json",
1198
+ help=(
1199
+ "Output as JSON. "
1200
+ "Envelope: sources.<SRC>.schemas.<SCH>.tables.<TBL>.columns.<COL>."
1201
+ ),
1202
+ ),
1203
+ ] = False,
1204
+ project_dir: ProjectDirOption = None,
1205
+ lineage_depth: Annotated[
1206
+ int,
1207
+ typer.Option(
1208
+ "--lineage-depth",
1209
+ min=1,
1210
+ max=10,
1211
+ help="Hops of upstream/downstream lineage to surface (1–10, default 1)",
1212
+ ),
1213
+ ] = 1,
1214
+ ) -> None:
1215
+ """Browse the data hierarchy or search the schema corpus.
1216
+
1217
+ Modes
1218
+ -----
1219
+
1220
+ Drill-down (default) — deepest non-None arg determines the return tier:
1221
+
1222
+ \b
1223
+ dft schema # list configured data sources
1224
+ dft schema mydb # list schemas in that source
1225
+ dft schema mydb main # list tables
1226
+ dft schema mydb main orders # profile table
1227
+ dft schema mydb main orders id # profile column
1228
+
1229
+ Search mode (-s / --search) — full-text + filter across the schema corpus:
1230
+
1231
+ \b
1232
+ dft schema -s mrr
1233
+ dft schema -s "" --role primary_key # filter-only (empty keyword)
1234
+ dft schema -s customer --column-name "*_id" --json
1235
+
1236
+ Level-3 regexp filter (-s with SOURCE + SCHEMA) — filter the table list
1237
+ by regexp without leaving drill-down mode:
1238
+
1239
+ \b
1240
+ dft schema wh analytics -s 'revenue|arr'
1241
+ dft schema wh analytics -s 'revenue|arr' --json
1242
+ dft schema -s ".*_at$" --regex --json --fields location,matched_field
1243
+ dft schema -s "" --missing description --table-name "stg_*"
1244
+
1245
+ Use --json for stable JSON output suitable for agent consumption.
1246
+
1247
+ Piping (drill-down) — escape hatch for ad-hoc cross-cutting queries:
1248
+
1249
+ \b
1250
+ # Tables with more than 1000 rows:
1251
+ dft schema wh analytics '*' --json | jq '.sources.wh.schemas.analytics.tables | to_entries[] | select(.value.row_count > 1000) | .key'
1252
+
1253
+ \b
1254
+ # Every column whose name ends in _at:
1255
+ dft schema wh analytics '*' '*' --json | jq '[.sources.wh.schemas.analytics.tables | to_entries[] | {table: .key, cols: [.value.columns | to_entries[] | select(.key | test("_at$")) | .key]}] | map(select(.cols | length > 0))'
1256
+
1257
+ Always quote glob args to prevent shell expansion: TABLE '*', TABLE 'stg_*'.
1258
+ """
1259
+ schema_cmd.schema_command(
1260
+ source=source,
1261
+ schema=schema_name,
1262
+ table=table,
1263
+ column=column,
1264
+ json_output=json_output,
1265
+ project_dir=project_dir,
1266
+ lineage_depth=lineage_depth,
1267
+ search=search,
1268
+ scope=scope,
1269
+ regex=regex,
1270
+ role=role,
1271
+ tag=tag,
1272
+ has_test=has_test,
1273
+ missing=missing,
1274
+ column_name=column_name,
1275
+ table_name=table_name,
1276
+ fk_to=fk_to,
1277
+ meta=meta,
1278
+ fields=fields,
1279
+ limit=limit,
1280
+ )
1281
+
1282
+
1283
+ # --- AI ---------------------------------------------------------------------
1284
+
1285
+
1286
+ @app.command("chat", rich_help_panel="AI")
1287
+ def chat(
1288
+ prompt: Annotated[
1289
+ str | None,
1290
+ typer.Argument(help="Optional one-shot prompt"),
1291
+ ] = None,
1292
+ model: Annotated[
1293
+ str | None,
1294
+ typer.Option("--model", help="Model name, optionally provider-prefixed"),
1295
+ ] = None,
1296
+ continue_session: Annotated[
1297
+ bool,
1298
+ typer.Option(
1299
+ "-c",
1300
+ "--continue",
1301
+ help="Resume the most recent session for the current directory",
1302
+ ),
1303
+ ] = False,
1304
+ resume: Annotated[
1305
+ str | None,
1306
+ typer.Option(
1307
+ "-r",
1308
+ "--resume",
1309
+ help="Resume a specific session by id (see --pick for the interactive list)",
1310
+ ),
1311
+ ] = None,
1312
+ pick: Annotated[
1313
+ bool,
1314
+ typer.Option(
1315
+ "-p",
1316
+ "--pick",
1317
+ help="Open an interactive picker to choose a session to resume",
1318
+ ),
1319
+ ] = False,
1320
+ ) -> None:
1321
+ """Chat with a terminal AI agent.
1322
+
1323
+ Sessions are auto-saved to ~/.dft/sessions/ and indexed by working directory.
1324
+
1325
+ \b
1326
+ Examples:
1327
+ dft chat # Start a fresh session
1328
+ dft chat "What tables do I have?" # One-shot prompt
1329
+ dft chat -c # Resume last session for this directory
1330
+ dft chat -r <session-id> # Resume a specific session by id
1331
+ dft chat -p # Pick from a list of recent sessions
1332
+ """
1333
+ chat_cmd.chat_command(
1334
+ prompt=prompt,
1335
+ model=model,
1336
+ continue_session=continue_session,
1337
+ resume=resume,
1338
+ pick=pick,
1339
+ )
1340
+
1341
+
1342
+ app.add_typer(mcp_app, name="mcp", rich_help_panel="AI")
1343
+
1344
+
1345
+ # --- Reference --------------------------------------------------------------
1346
+
1347
+
1348
+ @app.command("docs", rich_help_panel="Reference")
1349
+ def docs(
1350
+ topic: Annotated[
1351
+ str | None,
1352
+ typer.Argument(
1353
+ help="Topic name (run `dft docs` for the index). Use 'all' for the whole reference or 'reference' for the YAML field reference."
1354
+ ),
1355
+ ] = None,
1356
+ search: Annotated[
1357
+ str | None,
1358
+ typer.Option("--search", "-s", help="Full-text query across all topics"),
1359
+ ] = None,
1360
+ json_output: Annotated[
1361
+ bool,
1362
+ typer.Option("--json", help="Output as JSON"),
1363
+ ] = False,
1364
+ limit: Annotated[
1365
+ int,
1366
+ typer.Option(
1367
+ "--limit",
1368
+ min=1,
1369
+ max=50,
1370
+ help="Max search hits to return (default 5, max 50)",
1371
+ ),
1372
+ ] = 5,
1373
+ ) -> None:
1374
+ """Browse the Dataface YAML reference offline (topics, search).
1375
+
1376
+ \b
1377
+ Modes:
1378
+ dft docs # Topic index (one row per H2 section)
1379
+ dft docs cheatsheet # One-page essentials
1380
+ dft docs <topic> # Full docs for one section
1381
+ dft docs all # Whole reference, unsliced
1382
+ dft docs reference # Generated YAML field reference
1383
+ dft docs --search "grid" # Substring search across all topics
1384
+
1385
+ Use --json for stable, machine-readable output.
1386
+ """
1387
+ docs_cmd.docs_command(
1388
+ topic=topic,
1389
+ search=search,
1390
+ json_output=json_output,
1391
+ limit=limit,
1392
+ )
1393
+
1394
+
1395
+ app.add_typer(init_app, name="init", rich_help_panel="Reference")
1396
+
1397
+
1398
+ # Split "extra not installed" (use stub) from "extra installed but broken"
1399
+ # (let the ImportError propagate) — a blanket except would swallow real
1400
+ # transitive-import failures into the install-hint stub.
1401
+ if importlib.util.find_spec("dataface_playground") is None:
1402
+
1403
+ @app.command("playground", rich_help_panel="Reference")
1404
+ def playground_not_installed() -> None:
1405
+ """Start interactive playground with YAML editor and live preview.
1406
+
1407
+ Requires the ``playground`` extra.
1408
+ """
1409
+ from dataface.cli._extras import require_extras
1410
+
1411
+ require_extras("playground")
1412
+
1413
+ else:
1414
+ from dataface_playground.cli import main as _playground_main
1415
+
1416
+ app.command("playground", rich_help_panel="Reference")(_playground_main)
1417
+
1418
+
1419
+ @app.command("skills", rich_help_panel="Reference")
1420
+ def skills(
1421
+ skill_name: Annotated[
1422
+ str | None,
1423
+ typer.Argument(help="Skill name to show (default: list all)"),
1424
+ ] = None,
1425
+ search: Annotated[
1426
+ str | None,
1427
+ typer.Option("--search", "-s", help="Search names, descriptions, and bodies"),
1428
+ ] = None,
1429
+ limit: Annotated[
1430
+ int,
1431
+ typer.Option("--limit", min=1, max=25, help="Max search hits (default 10)"),
1432
+ ] = 10,
1433
+ as_json: Annotated[
1434
+ bool,
1435
+ typer.Option("--json", help="Output as JSON"),
1436
+ ] = False,
1437
+ ) -> None:
1438
+ """List packaged agent skills, search them, or show one by name.
1439
+
1440
+ \b
1441
+ Modes:
1442
+ dft skills # List all skills (grouped: workflows + patterns)
1443
+ dft skills <name> # Show the named skill body
1444
+ dft skills -s "<query>" # Search names, descriptions, and bodies
1445
+ dft skills --json # Stable JSON for any of the above
1446
+
1447
+ Skills are agent workflows and layout patterns. For YAML field reference,
1448
+ use `dft docs` instead.
1449
+ """
1450
+ skills_cmd.skills_command(
1451
+ name=skill_name,
1452
+ search=search,
1453
+ limit=limit,
1454
+ as_json=as_json,
1455
+ )
1456
+
1457
+
1458
+ # Hidden — not shown in root help
1459
+ app.add_typer(inspect_app, name="inspect", hidden=True)
1460
+
1461
+ # Load CLI plugins registered via [project.entry-points."dataface.cli_plugins"].
1462
+ # Each plugin's ``register()`` callable mounts its subcommands onto Typer apps
1463
+ # (e.g. ``inspect_app``) before the app executes. Runs at import time so that
1464
+ # ``dft --help`` shows profiler commands when the private package is installed.
1465
+ # importlib.metadata is stdlib since Python 3.8; dataface requires >=3.10, so
1466
+ # the outer try is unnecessary. Plugin load errors surface to the user rather
1467
+ # than being silently swallowed.
1468
+ from importlib.metadata import entry_points as _entry_points
1469
+
1470
+ for _ep in _entry_points(group="dataface.cli_plugins"):
1471
+ _plugin = _ep.load()
1472
+ _plugin()
1473
+
1474
+
1475
+ def version_callback(value: bool) -> None:
1476
+ """Print version and exit."""
1477
+ if value:
1478
+ from dataface.cli._version_info import collect
1479
+
1480
+ typer.echo(collect().render())
1481
+ raise typer.Exit()
1482
+
1483
+
1484
+ @app.callback()
1485
+ def main(
1486
+ version: Annotated[
1487
+ bool | None,
1488
+ typer.Option(
1489
+ "--version",
1490
+ help="Print version and exit.",
1491
+ callback=version_callback,
1492
+ is_eager=True,
1493
+ ),
1494
+ ] = None,
1495
+ ) -> None:
1496
+ """dft - Declarative, dbt-native dashboards in YAML."""
1497
+ pass
1498
+
1499
+
1500
+ if __name__ == "__main__":
1501
+ app()