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,94 @@
1
+ """Inspect command — OSS template-management verbs only.
2
+
3
+ The profiler verbs (inspect_command, inspect_all_command, audit_command) live
4
+ in the private ``dataface-super-schema`` package and are registered via the
5
+ ``dataface.cli_plugins`` entry-point when that package is installed.
6
+
7
+ This module contains only the template-management commands that ship with the
8
+ OSS ``dataface`` wheel: eject, templates, and validate-templates.
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ import typer
14
+
15
+ from dataface.agent_api import inspect as _api
16
+ from dataface.cli._console import dft_console
17
+
18
+ console = dft_console()
19
+
20
+
21
+ def eject_command(
22
+ templates: list[str],
23
+ all_templates: bool = False,
24
+ force: bool = False,
25
+ output_dir: Path | None = None,
26
+ ) -> None:
27
+ target_dir = output_dir or (Path.cwd() / "faces" / "inspect")
28
+ to_eject: list[str] | None = None if all_templates else (templates or None)
29
+ if not all_templates and not templates:
30
+ console.print("[bold red]Error:[/bold red] Specify template names or --all")
31
+ console.print(f"Available: {', '.join(t.name for t in _api.list_templates())}")
32
+ raise typer.Exit(1)
33
+ try:
34
+ ejected = _api.eject_templates(target_dir, templates=to_eject, force=force)
35
+ except ValueError as e:
36
+ console.print(f"[bold red]Error:[/bold red] {e}")
37
+ raise typer.Exit(1) from None
38
+ skipped_count = len(to_eject or [t.name for t in _api.list_templates()]) - len(
39
+ ejected
40
+ )
41
+ console.print(f"[dim]Output directory: {target_dir}[/dim]\n")
42
+ for p in ejected:
43
+ console.print(f" [green]✓[/green] {p.name}")
44
+ console.print()
45
+ if ejected:
46
+ console.print(
47
+ f"[bold green]✓[/bold green] Ejected {len(ejected)} template(s) to {target_dir}"
48
+ )
49
+ if skipped_count > 0:
50
+ console.print(f"[dim]Skipped {skipped_count} existing file(s)[/dim]")
51
+ if ejected and not force:
52
+ console.print(
53
+ "\n[dim]To reset to built-in defaults: dft inspect eject --all --force[/dim]"
54
+ )
55
+
56
+
57
+ def validate_ejected_templates_command(output_dir: Path | None = None) -> None:
58
+ try:
59
+ result = _api.validate_ejected_templates(output_dir)
60
+ except FileNotFoundError as e:
61
+ console.print(f"[bold red]Error:[/bold red] {e}")
62
+ raise typer.Exit(1) from None
63
+ for label, names, style in [
64
+ ("Missing ejected templates", result.missing, "bold red"),
65
+ (
66
+ "Upstream changed since eject (manual rebase needed)",
67
+ result.upstream_changed,
68
+ "bold yellow",
69
+ ),
70
+ (
71
+ "Customized templates still aligned to built-ins",
72
+ result.custom_safe,
73
+ "bold cyan",
74
+ ),
75
+ ("Unchanged templates", result.unchanged, "dim"),
76
+ ]:
77
+ if names:
78
+ console.print(f"[{style}]{label}:[/{style}]")
79
+ for name in names:
80
+ console.print(f" - {name}")
81
+ if not result.success:
82
+ raise typer.Exit(1)
83
+ console.print("\n[bold green]✓[/bold green] Ejected templates are compatible.")
84
+
85
+
86
+ def templates_command() -> None:
87
+ console.print("\n[bold]Available Inspect Templates[/bold]\n")
88
+ for t in _api.list_templates():
89
+ console.print(f" • {t.name}")
90
+ console.print("\n[dim]To customize a template:[/dim]")
91
+ console.print(" dft inspect eject <template> # Copy to faces/inspect/")
92
+ console.print(" dft inspect eject --all # Copy all templates")
93
+ console.print(" dft inspect validate-templates # Check compatibility")
94
+ console.print()
@@ -0,0 +1,167 @@
1
+ """`dft init mcp` thin wrapper: parse args, call agent_api.mcp_install, format output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ from dataface.agent_api import mcp_install
13
+ from dataface.core.compile.meta import find_project_root
14
+ from dataface.core.project_roots import PROJECT_MARKERS, find_dataface_project
15
+
16
+
17
+ def _resolve_dft_executable() -> str:
18
+ """Path to this `dft` install for embedding in MCP client configs.
19
+
20
+ GUI apps (Cursor, VS Code) often spawn MCP with a minimal ``PATH``. A
21
+ config entry ``"command": "dft"`` then fails with ``spawn dft ENOENT``.
22
+ When this code runs as the real ``dft`` console script, ``sys.argv[0]``
23
+ is an absolute (or resolvable) path to that binary — use it first, then
24
+ ``shutil.which``, then fall back to the bare name.
25
+ """
26
+ invoked = Path(sys.argv[0]).expanduser()
27
+ try:
28
+ resolved = invoked.resolve(strict=False)
29
+ except OSError:
30
+ resolved = invoked
31
+ name = resolved.name.casefold()
32
+ if name in ("dft", "dft.exe") and resolved.is_file():
33
+ return str(resolved)
34
+ return shutil.which("dft") or "dft"
35
+
36
+
37
+ def _resolve_project_dir(explicit: Path | None) -> Path:
38
+ markers_str = ", ".join(PROJECT_MARKERS)
39
+ if explicit is not None:
40
+ nearest = find_dataface_project(explicit)
41
+ if nearest != explicit:
42
+ msg = (
43
+ f"Error: --project-dir {explicit} does not contain a Dataface "
44
+ f"or dbt project.\n"
45
+ f"Looked for: {markers_str}."
46
+ )
47
+ if nearest is not None:
48
+ msg += f"\nTip: did you mean {nearest}?"
49
+ typer.echo(msg, err=True)
50
+ raise typer.Exit(1)
51
+ return explicit
52
+
53
+ found = find_dataface_project(Path.cwd())
54
+ if found is None:
55
+ typer.echo(
56
+ f"Error: No Dataface or dbt project found at or above {Path.cwd()}.\n"
57
+ f"Looked for: {markers_str}.\n"
58
+ f"Re-run from inside your Dataface project, "
59
+ f"or pass --project-dir <path>.",
60
+ err=True,
61
+ )
62
+ raise typer.Exit(1)
63
+ return found
64
+
65
+
66
+ def run_init(
67
+ client: str | None,
68
+ all_clients: bool,
69
+ force: bool,
70
+ project_dir: Path | None = None,
71
+ ) -> None:
72
+ """Shared implementation for `dft init mcp`."""
73
+ config_root = find_project_root(Path.cwd())
74
+ project_dir_resolved = _resolve_project_dir(project_dir)
75
+
76
+ dft_path = _resolve_dft_executable()
77
+ server_command = [dft_path, "mcp", "serve"]
78
+ if project_dir_resolved != config_root:
79
+ server_command += ["--project-dir", str(project_dir_resolved)]
80
+
81
+ if client and client.lower() == "print":
82
+ entry = {"command": server_command[0], "args": server_command[1:]}
83
+ typer.echo(json.dumps({"mcpServers": {"dataface": entry}}, indent=2))
84
+ return
85
+
86
+ if client and all_clients:
87
+ typer.echo("Specify either a client or --all, not both.", err=True)
88
+ raise typer.Exit(1)
89
+
90
+ all_known = mcp_install.list_clients()
91
+ known_names = {c.name for c in all_known}
92
+
93
+ if all_clients:
94
+ targets = all_known
95
+ elif client:
96
+ target = client.lower()
97
+ if target not in known_names:
98
+ typer.echo(
99
+ f"Unknown client: {client}. "
100
+ f"Supported: {', '.join(sorted(known_names) + ['print'])}",
101
+ err=True,
102
+ )
103
+ raise typer.Exit(1)
104
+ targets = [c for c in all_known if c.name == target]
105
+ else:
106
+ targets = [
107
+ c
108
+ for c in all_known
109
+ if any((config_root / dp).exists() for dp in c.detect_paths)
110
+ ]
111
+ if not targets:
112
+ typer.echo(
113
+ "No AI client markers detected (.cursor/, .codex/, .vscode/, "
114
+ ".github/, CLAUDE.md, AGENTS.md, ~/.config/claude/)."
115
+ )
116
+ typer.echo("")
117
+ typer.echo("To configure manually, specify a client:")
118
+ typer.echo(" dft init mcp cursor # Cursor")
119
+ typer.echo(" dft init mcp vscode # VS Code")
120
+ typer.echo(" dft init mcp claude # Claude Desktop")
121
+ typer.echo(" dft init mcp claude-code # Claude Code")
122
+ typer.echo(" dft init mcp codex # OpenAI Codex CLI")
123
+ typer.echo(" dft init mcp copilot # GitHub Copilot Coding Agent")
124
+ typer.echo(" dft init mcp --all # Write every supported config")
125
+ typer.echo(" dft init mcp print # Print JSON config to stdout")
126
+ return
127
+
128
+ configured = []
129
+ for c in targets:
130
+ result = mcp_install.install_for_client(
131
+ c, server_command=server_command, config_root=config_root, force=force
132
+ )
133
+ typer.echo(result.message)
134
+ if not result.already_configured:
135
+ configured.append(c.name)
136
+
137
+ if configured:
138
+ typer.echo("")
139
+ # Trailer pinned to dataface.ai.tool_schemas.ALL_TOOLS and
140
+ # dataface.ai.mcp.server._BASE_RESOURCES by
141
+ # test_configured_trailer_advertises_every_tool_and_resource_family.
142
+ # Update these strings when either registry changes.
143
+ typer.echo(
144
+ " MCP tools: describe_dashboard, describe_query, docs, execute_query, "
145
+ "get_skill, list_skills, query_face, render_dashboard, schema, "
146
+ "schema_search, search_dashboards, search_skills, validate_dashboard"
147
+ )
148
+ typer.echo(
149
+ " Resources: dataface://dashboards, dataface://dashboard/{path}, "
150
+ "dataface://docs/{all,reference,<topic>}, "
151
+ "dataface://guide/{dashboard-design,dashboard-build,dashboard-review,report-design}"
152
+ )
153
+
154
+ typer.echo("")
155
+ if configured:
156
+ if "cursor" in configured:
157
+ typer.echo(
158
+ " Next: Open Cursor Settings → MCP and enable the 'dataface' server."
159
+ )
160
+ elif "claude" in configured:
161
+ typer.echo(" Restart Claude Desktop for changes to take effect.")
162
+ else:
163
+ clients_str = ", ".join(sorted(configured))
164
+ typer.echo(f" Restart {clients_str} for changes to take effect.")
165
+ typer.echo(
166
+ " Install workflow skills: dft init skills (separate from MCP wiring)."
167
+ )
@@ -0,0 +1,386 @@
1
+ """Query command implementation — named face queries and raw SQL dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, NoReturn
9
+
10
+ import typer
11
+ from rich.markup import escape
12
+ from rich.table import Table
13
+
14
+ if TYPE_CHECKING:
15
+ from rich.console import Console
16
+
17
+ from dataface.agent_api._paths import project_root_for
18
+ from dataface.agent_api.describe_query import DescribeQueryResult, describe_query
19
+ from dataface.agent_api.query import (
20
+ ExecuteQueryResult,
21
+ FaceQueryLookupResult,
22
+ QueryFaceResult,
23
+ execute_query,
24
+ lookup_face_query_sql,
25
+ query_face,
26
+ )
27
+ from dataface.agent_api.validate_query import ValidateQueryResult, validate_query
28
+ from dataface.cli._console import dft_console
29
+ from dataface.cli._json_output import print_json_result
30
+ from dataface.core.execute.adapters import build_adapter_registry
31
+
32
+ console = dft_console()
33
+ err_console = dft_console(stderr=True)
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Output helpers
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _SEVERITY_ICON = {"error": "❌", "warning": "⚠️ ", "info": "ℹ️ "}
40
+
41
+
42
+ def _rows_table(columns: list[str], data: list[dict[str, Any]]) -> Table:
43
+ t = Table(show_header=True, header_style="bold", box=None, padding=(0, 2, 0, 0))
44
+ for col in columns:
45
+ t.add_column(escape(col))
46
+ for row in data:
47
+ t.add_row(*(escape(str(row.get(col, ""))) for col in columns))
48
+ return t
49
+
50
+
51
+ def _print_rows(
52
+ columns: list[str], data: list[dict[str, Any]], row_count: int, truncated: bool
53
+ ) -> None:
54
+ if columns:
55
+ console.print(_rows_table(columns, data))
56
+ err_console.print(f"\n{row_count} rows{' (truncated)' if truncated else ''}")
57
+
58
+
59
+ def _print_diagnostic(d: dict[str, Any], target: Console = err_console) -> None:
60
+ icon = _SEVERITY_ICON.get(d.get("severity", ""), "⚠️ ")
61
+ target.print(f"{icon} [{d['code']}] {d['message']}", markup=False)
62
+ if d.get("detail"):
63
+ target.print(f" {d['detail']}", markup=False)
64
+ if d.get("recommendation"):
65
+ target.print(f" → {d['recommendation']}", markup=False)
66
+ if d.get("confidence") is not None:
67
+ target.print(f" confidence: {d['confidence']:.0%}")
68
+ for ev in d.get("evidence", []):
69
+ target.print(f" evidence: {ev}", markup=False)
70
+
71
+
72
+ def _print_query_face_rich(result: QueryFaceResult) -> None:
73
+ if not result.success:
74
+ for err in result.errors:
75
+ err_console.print(f"[red]Error:[/red] {escape(err)}")
76
+ if result.available_queries:
77
+ matches = difflib.get_close_matches(
78
+ result.name, result.available_queries, n=3
79
+ )
80
+ if matches:
81
+ err_console.print(f"Did you mean: {escape(', '.join(matches))}?")
82
+ elif _looks_like_sql(result.name):
83
+ err_console.print("Did you mean to drop --in for raw SQL?")
84
+ err_console.print(
85
+ f"Available queries: {escape(', '.join(result.available_queries))}"
86
+ )
87
+ raise typer.Exit(1)
88
+ _print_rows(result.columns, result.data, result.row_count, result.truncated)
89
+
90
+
91
+ def _print_execute_query_rich(result: ExecuteQueryResult) -> None:
92
+ if not result.success:
93
+ err_console.print(f"[red]Error:[/red] {escape(result.error or '')}")
94
+ raise typer.Exit(1)
95
+ _print_rows(result.columns, result.data, result.row_count, result.truncated)
96
+
97
+
98
+ def _print_validate_result(
99
+ result: ValidateQueryResult,
100
+ show_suppressed: bool = False,
101
+ json_output: bool = False,
102
+ ) -> None:
103
+ """Print validate result and exit 1 on errors."""
104
+ if json_output:
105
+ payload: Any = (
106
+ {"diagnostics": result.diagnostics, "suppressed": result.suppressed or []}
107
+ if show_suppressed
108
+ else result.diagnostics
109
+ )
110
+ typer.echo(json.dumps(payload, indent=2))
111
+ elif not result.diagnostics and not (show_suppressed and result.suppressed):
112
+ console.print("No issues found.")
113
+ else:
114
+ for d in result.diagnostics:
115
+ _print_diagnostic(d, target=console)
116
+ if show_suppressed and result.suppressed:
117
+ console.print(f"\nSuppressed ({len(result.suppressed)}):")
118
+ for d in result.suppressed:
119
+ console.print(f" ~ [{d['code']}] {d['message']}", markup=False)
120
+
121
+ if result.has_errors:
122
+ raise typer.Exit(1)
123
+
124
+
125
+ def _print_describe_result(
126
+ result: DescribeQueryResult, json_output: bool = False
127
+ ) -> None:
128
+ """Print describe result and exit 1 on failure."""
129
+ if json_output:
130
+ print_json_result(result)
131
+ if not result.success:
132
+ raise typer.Exit(1)
133
+ return
134
+
135
+ if not result.success:
136
+ err_console.print(f"[red]Error:[/red] {escape(result.error or '')}")
137
+ for d in result.diagnostics:
138
+ _print_diagnostic(d)
139
+ raise typer.Exit(1)
140
+
141
+ if result.columns:
142
+ t = Table(show_header=True, header_style="bold", box=None, padding=(0, 2, 0, 0))
143
+ t.add_column("name")
144
+ t.add_column("type")
145
+ for col in result.columns:
146
+ t.add_row(escape(col.name), escape(col.type))
147
+ console.print(t)
148
+
149
+ for d in result.diagnostics:
150
+ _print_diagnostic(d)
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # SQL-source resolution helpers
155
+ # ---------------------------------------------------------------------------
156
+
157
+
158
+ def _resolve_sql_text(
159
+ target: str | None,
160
+ file: Path | None,
161
+ face: Path | None,
162
+ ) -> tuple[str, bool, str | None]:
163
+ """Resolve the SQL text and whether we are in named-query mode.
164
+
165
+ Returns (sql_text, mode_is_named, name_or_none).
166
+ Raises typer.Exit(1) on any resolution error.
167
+ """
168
+ if face and file:
169
+ typer.echo(
170
+ "Error: use --in (named query) or --file (SQL file), not both.", err=True
171
+ )
172
+ raise typer.Exit(1)
173
+
174
+ if face:
175
+ # Named-query mode — TARGET is the query name, SQL resolved later.
176
+ if not target:
177
+ typer.echo(
178
+ "Error: provide a query name when using --in"
179
+ " (e.g. dft query revenue --in face.yml).",
180
+ err=True,
181
+ )
182
+ raise typer.Exit(1)
183
+ return "", True, target
184
+
185
+ if file:
186
+ if not file.exists():
187
+ typer.echo(f"Error: file not found: {file}", err=True)
188
+ raise typer.Exit(1)
189
+ return file.read_text(), False, None
190
+
191
+ if not target:
192
+ typer.echo(
193
+ "Error: provide SQL, --file, or NAME --in face.yml.",
194
+ err=True,
195
+ )
196
+ raise typer.Exit(1)
197
+
198
+ return target, False, None
199
+
200
+
201
+ _SQL_SHAPE_CHARS = frozenset(" \t\n()")
202
+
203
+
204
+ def _looks_like_sql(name: str) -> bool:
205
+ """Return True if name resembles a SQL expression rather than a query identifier."""
206
+ upper = name.upper()
207
+ return (
208
+ upper.startswith("SELECT")
209
+ or upper.startswith("WITH")
210
+ or any(c in name for c in _SQL_SHAPE_CHARS)
211
+ )
212
+
213
+
214
+ def _handle_lookup_failure(lr: FaceQueryLookupResult, query_name: str) -> NoReturn:
215
+ """Emit errors from a failed FaceQueryLookupResult and raise typer.Exit(1)."""
216
+ for err in lr.errors:
217
+ typer.echo(f"Error: {err}", err=True)
218
+ if lr.available_queries:
219
+ matches = difflib.get_close_matches(query_name, lr.available_queries, n=3)
220
+ if matches:
221
+ typer.echo("Did you mean: " + ", ".join(matches) + "?", err=True)
222
+ elif _looks_like_sql(query_name):
223
+ typer.echo("Did you mean to drop --in for raw SQL?", err=True)
224
+ typer.echo("Available queries: " + ", ".join(lr.available_queries), err=True)
225
+ raise typer.Exit(1)
226
+
227
+
228
+ def _resolve_source(source_flag: str | None, face_source: str | None) -> str:
229
+ """Resolve the effective source, raising if neither is available."""
230
+ resolved = source_flag or face_source
231
+ if resolved is None:
232
+ typer.echo(
233
+ "Error: SQL execute requires --source. "
234
+ "Add --validate for static lint, or --source <name> to execute.",
235
+ err=True,
236
+ )
237
+ raise typer.Exit(1)
238
+ return resolved
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Public entry point
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ def query_command(
247
+ target: str | None,
248
+ face: Path | None = None,
249
+ source: str | None = None,
250
+ validate: bool = False,
251
+ describe: bool = False,
252
+ file: Path | None = None,
253
+ dialect: str | None = None,
254
+ vars: dict[str, Any] | None = None,
255
+ limit: int = 20,
256
+ show_suppressed: bool = False,
257
+ json_output: bool = False,
258
+ project_dir: Path | None = None,
259
+ ) -> None:
260
+ """Run a named face query, raw SQL, or static SQL lint/describe — selected from the input args."""
261
+ project_root = project_root_for(project_dir)
262
+
263
+ # --- 1. Resolve input shape ---
264
+ sql_text, mode_is_named, query_name = _resolve_sql_text(target, file, face)
265
+
266
+ # --- 2. Apply mode flags ---
267
+ if not validate and not describe:
268
+ # Execute path — keep the named-query branch as a direct query_face call so
269
+ # that file-not-found and compile errors are returned as structured JSON
270
+ # (QueryFaceResult.success=False) rather than plain stderr messages.
271
+ if mode_is_named:
272
+ assert query_name is not None
273
+ assert face is not None
274
+ adapter_registry = build_adapter_registry(project_root, read_only=True)
275
+ face_result = query_face(
276
+ name=query_name,
277
+ path=face,
278
+ project_dir=project_root,
279
+ vars=vars,
280
+ limit=limit,
281
+ adapter_registry=adapter_registry,
282
+ )
283
+ if json_output:
284
+ print_json_result(face_result)
285
+ if not face_result.success:
286
+ raise SystemExit(1)
287
+ else:
288
+ _print_query_face_rich(face_result)
289
+ else:
290
+ # Raw SQL — if the positional doesn't look like SQL, hint --in instead.
291
+ if not _looks_like_sql(sql_text):
292
+ typer.echo(
293
+ f"Did you mean `dft query {sql_text} --in <face.yml>`?",
294
+ err=True,
295
+ )
296
+ raise typer.Exit(1)
297
+ # Requires --source
298
+ resolved_source = _resolve_source(source, None)
299
+ adapter_registry = build_adapter_registry(project_root, read_only=True)
300
+ exec_result = execute_query(
301
+ sql_text,
302
+ variables=vars,
303
+ source=resolved_source,
304
+ limit=limit,
305
+ adapter_registry=adapter_registry,
306
+ )
307
+ if json_output:
308
+ print_json_result(exec_result)
309
+ if not exec_result.success:
310
+ raise SystemExit(1)
311
+ else:
312
+ _print_execute_query_rich(exec_result)
313
+ return
314
+
315
+ # For validate/describe modes on named queries, extract SQL from the compiled face.
316
+ face_source: str | None = None
317
+ if mode_is_named:
318
+ assert query_name is not None
319
+ assert face is not None
320
+ lr = lookup_face_query_sql(query_name, face, project_root)
321
+ if not lr.success:
322
+ _handle_lookup_failure(lr, query_name)
323
+ sql_text = lr.sql
324
+ face_source = lr.source
325
+
326
+ if validate and describe:
327
+ # Run validate first; skip describe on error-severity diagnostics.
328
+ vr = validate_query(
329
+ sql_text, dialect=dialect, return_suppressed=show_suppressed
330
+ )
331
+ # Build the validate payload the same way on both error and success paths.
332
+ validate_payload: Any = (
333
+ {"diagnostics": vr.diagnostics, "suppressed": vr.suppressed or []}
334
+ if show_suppressed
335
+ else vr.diagnostics
336
+ )
337
+ if vr.has_errors:
338
+ if json_output:
339
+ typer.echo(json.dumps({"validate": validate_payload}, indent=2))
340
+ else:
341
+ _print_validate_result(vr, show_suppressed=show_suppressed)
342
+ raise typer.Exit(1)
343
+
344
+ resolved_source = _resolve_source(source, face_source)
345
+ adapter_registry = build_adapter_registry(project_root, read_only=True)
346
+ dr = describe_query(
347
+ sql_text,
348
+ source=resolved_source,
349
+ dialect=dialect,
350
+ adapter_registry=adapter_registry,
351
+ )
352
+
353
+ if json_output:
354
+ typer.echo(
355
+ json.dumps(
356
+ {
357
+ "validate": validate_payload,
358
+ "describe": dr.model_dump(mode="json", exclude_none=True),
359
+ },
360
+ indent=2,
361
+ )
362
+ )
363
+ if not dr.success:
364
+ raise typer.Exit(1)
365
+ else:
366
+ _print_validate_result(vr, show_suppressed=show_suppressed)
367
+ _print_describe_result(dr)
368
+
369
+ elif validate:
370
+ vr = validate_query(
371
+ sql_text, dialect=dialect, return_suppressed=show_suppressed
372
+ )
373
+ _print_validate_result(
374
+ vr, show_suppressed=show_suppressed, json_output=json_output
375
+ )
376
+
377
+ else: # describe only
378
+ resolved_source = _resolve_source(source, face_source)
379
+ adapter_registry = build_adapter_registry(project_root, read_only=True)
380
+ dr = describe_query(
381
+ sql_text,
382
+ source=resolved_source,
383
+ dialect=dialect,
384
+ adapter_registry=adapter_registry,
385
+ )
386
+ _print_describe_result(dr, json_output=json_output)