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,291 @@
1
+ """Render command implementation."""
2
+
3
+ import json as _json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from dataface.agent_api._paths import (
8
+ project_root_for,
9
+ setup_render_for_face,
10
+ setup_render_for_yaml,
11
+ )
12
+ from dataface.agent_api.dashboards import RenderedDashboard, render_dashboard
13
+ from dataface.cli._error_format import print_structured_errors
14
+ from dataface.core.errors import StructuredError
15
+ from dataface.core.render.warnings import (
16
+ registry as _warnings_registry,
17
+ unreferenced_chart,
18
+ )
19
+ from dataface.core.render.warnings.base import RenderWarning
20
+ from dataface.core.render.warnings.from_query_diagnostic import KNOWN_RENDER_CODES
21
+
22
+ # Compile-time warning producers don't live on detector modules in the registry,
23
+ # but their codes appear on the unified RenderedDashboard.warnings channel and
24
+ # must be recognized by --ignore-warning. Adapter map is the source of truth
25
+ # for query-validator codes; only orphan emitters (e.g. unreferenced_chart) add here.
26
+ _COMPILE_PRODUCER_CODES: frozenset[str] = KNOWN_RENDER_CODES | {unreferenced_chart.CODE}
27
+
28
+
29
+ def _exit_with_errors(errs: list[StructuredError], json_errors: bool) -> None:
30
+ print_structured_errors(errs, json_output=json_errors)
31
+ sys.exit(1)
32
+
33
+
34
+ def _emit_unknown_code_notices(ignore_codes: set[str]) -> None:
35
+ """Print a one-line notice for each code not found in the registry.
36
+
37
+ Unknown codes are forward-compatible suppression stubs (a dataface.yml may
38
+ list codes that haven't shipped yet). They do NOT cause a non-zero exit.
39
+ Uses live module reference so tests can monkeypatch DETECTORS on the registry.
40
+ """
41
+ known = {d.CODE for d in _warnings_registry.DETECTORS} | _COMPILE_PRODUCER_CODES
42
+ for code in sorted(ignore_codes):
43
+ if code not in known:
44
+ print(f"unknown warning code: {code}", file=sys.stderr)
45
+
46
+
47
+ def _format_warnings_stderr(warnings: list[RenderWarning]) -> None:
48
+ """Write the active warnings block to stderr in the canonical format.
49
+
50
+ Format:
51
+ ⚠ N warning(s):
52
+ [CODE] chart-id: message
53
+ → fix
54
+ """
55
+ if not warnings:
56
+ return
57
+ n = len(warnings)
58
+ label = "warning" if n == 1 else "warnings"
59
+ print(f"⚠ {n} {label}:", file=sys.stderr)
60
+ for w in warnings:
61
+ chart_prefix = f"{w.chart}: " if w.chart else ""
62
+ print(f" [{w.code}] {chart_prefix}{w.message}", file=sys.stderr)
63
+ if w.fix:
64
+ print(f" → {w.fix}", file=sys.stderr)
65
+
66
+
67
+ def _merge_warnings_into_json(
68
+ data: dict[str, object],
69
+ result: RenderedDashboard,
70
+ ) -> str:
71
+ """For JSON format, inject warnings and suppressed_warnings.
72
+
73
+ Agents and consumers reading --format json must see warnings regardless of
74
+ --no-warnings (that flag is a human-output convenience only).
75
+ data is always a dict here (dashboard.py JSON path returns json.loads output).
76
+ """
77
+ base = dict(data)
78
+ base["warnings"] = [w.model_dump() for w in result.warnings]
79
+ base["suppressed_warnings"] = [w.model_dump() for w in result.suppressed_warnings]
80
+ return _json.dumps(base)
81
+
82
+
83
+ def render_command(
84
+ face_path: Path,
85
+ output: str | None = None,
86
+ format: str = "svg",
87
+ project_dir: Path | None = None,
88
+ variables: dict[str, str] | None = None,
89
+ use_cache: bool = True,
90
+ json_errors: bool = False,
91
+ no_warnings: bool = False,
92
+ ignore_codes: set[str] | None = None,
93
+ ) -> str | None:
94
+ """Render a dashboard to SVG, HTML, PNG, PDF, or terminal.
95
+
96
+ Args:
97
+ face_path: Path to face YAML file
98
+ output: Output file path. For binary formats (png, pdf) and svg/html,
99
+ default is renders/ folder with face name. For json/text/yaml,
100
+ default is stdout. Use "-" to force stdout. Ignored for terminal
101
+ format (always prints to stdout).
102
+ format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
103
+ project_dir: Project directory for resolving relative paths
104
+ variables: Variable values to pass to the render (key=value pairs)
105
+ use_cache: Whether to use cached query results
106
+ json_errors: Emit errors as JSON to stdout instead of Rich panels to stderr
107
+ no_warnings: Suppress stderr warning output (warnings still appear in JSON)
108
+ ignore_codes: Warning codes to suppress via the partition seam
109
+
110
+ Returns:
111
+ Output file path on success, None for terminal/stdout output or on error
112
+ """
113
+ if ignore_codes:
114
+ _emit_unknown_code_notices(ignore_codes)
115
+
116
+ output_dir = project_root_for(project_dir)
117
+ setup, face_file = setup_render_for_face(face_path, project_dir)
118
+ result = render_dashboard(
119
+ path=setup.scoped_path,
120
+ project_dir=setup.scoped_base,
121
+ format=format,
122
+ variables=variables,
123
+ adapter_registry=setup.adapter_registry,
124
+ use_cache=use_cache,
125
+ # Match pre-PR behaviour: 2x retina PNG by default for the CLI.
126
+ scale=2.0,
127
+ ignore_codes=ignore_codes,
128
+ )
129
+
130
+ if not result.success:
131
+ errors = result.validation_errors or (
132
+ [result.face_error] if result.face_error else []
133
+ )
134
+ _exit_with_errors(errors, json_errors=json_errors)
135
+
136
+ if not no_warnings:
137
+ _format_warnings_stderr(result.warnings)
138
+
139
+ if format == "json":
140
+ assert isinstance(result.data, dict)
141
+ rendered_content: str | bytes = _merge_warnings_into_json(result.data, result)
142
+ else:
143
+ rendered_content = (
144
+ result.data
145
+ if isinstance(result.data, (str, bytes))
146
+ else _json.dumps(result.data)
147
+ )
148
+ return _write_output(
149
+ rendered_content,
150
+ output=output,
151
+ format=format,
152
+ output_dir=output_dir,
153
+ default_output_stem=face_file.stem,
154
+ source_label=str(face_path),
155
+ )
156
+
157
+
158
+ def render_command_from_yaml(
159
+ yaml_content: str,
160
+ output: str | None = None,
161
+ format: str = "svg",
162
+ project_dir: Path | None = None,
163
+ variables: dict[str, str] | None = None,
164
+ use_cache: bool = True,
165
+ json_errors: bool = False,
166
+ no_warnings: bool = False,
167
+ ignore_codes: set[str] | None = None,
168
+ ) -> str | None:
169
+ """Render a dashboard from YAML content (e.g. stdin).
170
+
171
+ Args:
172
+ yaml_content: Raw YAML string
173
+ output: Output file path. Binary formats and svg/html default to a
174
+ renders/ file under project_dir; json/text/yaml default to stdout.
175
+ Use "-" to force stdout. Ignored for terminal format.
176
+ format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
177
+ project_dir: Project directory for resolving relative paths
178
+ variables: Variable values to pass to the render
179
+ use_cache: Whether to use cached query results
180
+ json_errors: Emit errors as JSON to stdout instead of Rich panels to stderr
181
+ no_warnings: Suppress stderr warning output (warnings still appear in JSON)
182
+ ignore_codes: Warning codes to suppress via the partition seam
183
+
184
+ Returns:
185
+ Output file path on success, None for terminal/stdout output or on error
186
+ """
187
+ if ignore_codes:
188
+ _emit_unknown_code_notices(ignore_codes)
189
+
190
+ output_dir = project_root_for(project_dir)
191
+ setup = setup_render_for_yaml(project_dir)
192
+ result = render_dashboard(
193
+ yaml_content=yaml_content,
194
+ project_dir=setup.scoped_base,
195
+ format=format,
196
+ variables=variables,
197
+ adapter_registry=setup.adapter_registry,
198
+ use_cache=use_cache,
199
+ scale=2.0,
200
+ ignore_codes=ignore_codes,
201
+ )
202
+
203
+ if not result.success:
204
+ errors = result.validation_errors or (
205
+ [result.face_error] if result.face_error else []
206
+ )
207
+ _exit_with_errors(errors, json_errors=json_errors)
208
+
209
+ if not no_warnings:
210
+ _format_warnings_stderr(result.warnings)
211
+
212
+ if format == "json":
213
+ assert isinstance(result.data, dict)
214
+ rendered_content: str | bytes = _merge_warnings_into_json(result.data, result)
215
+ else:
216
+ rendered_content = (
217
+ result.data
218
+ if isinstance(result.data, (str, bytes))
219
+ else _json.dumps(result.data)
220
+ )
221
+ return _write_output(
222
+ rendered_content,
223
+ output=output,
224
+ format=format,
225
+ output_dir=output_dir,
226
+ default_output_stem="stdin",
227
+ source_label="<stdin>",
228
+ )
229
+
230
+
231
+ def _write_output(
232
+ rendered_content: str | bytes,
233
+ output: str | None,
234
+ format: str,
235
+ output_dir: Path,
236
+ default_output_stem: str,
237
+ source_label: str,
238
+ ) -> str | None:
239
+ """Write rendered content to the appropriate destination."""
240
+ # `terminal` is stdout-only by design — --output is meaningless for the
241
+ # ANSI-formatted preview. Every other text format honors --output when set
242
+ # and defaults to stdout otherwise (no implicit `renders/` write).
243
+ if format == "terminal" or (format in ("json", "text", "yaml") and output is None):
244
+ text_output = (
245
+ rendered_content.decode()
246
+ if isinstance(rendered_content, bytes)
247
+ else rendered_content
248
+ )
249
+ print(text_output)
250
+ return None
251
+
252
+ if output == "-":
253
+ if isinstance(rendered_content, bytes):
254
+ sys.stdout.buffer.write(rendered_content)
255
+ else:
256
+ print(rendered_content)
257
+ return None
258
+
259
+ # Resolve output paths against output_dir (CWD / --project-dir), not project_root
260
+ is_binary = format in ("png", "pdf")
261
+ output_extension = f".{format}"
262
+
263
+ if output:
264
+ output_path = Path(output)
265
+ if not output_path.is_absolute():
266
+ output_path = output_dir / output_path
267
+ else:
268
+ renders_dir = output_dir / "renders"
269
+ renders_dir.mkdir(exist_ok=True)
270
+ output_path = renders_dir / f"{default_output_stem}{output_extension}"
271
+
272
+ if is_binary:
273
+ content_bytes = (
274
+ rendered_content.encode()
275
+ if isinstance(rendered_content, str)
276
+ else rendered_content
277
+ )
278
+ output_path.write_bytes(content_bytes)
279
+ else:
280
+ content_str = (
281
+ rendered_content.decode()
282
+ if isinstance(rendered_content, bytes)
283
+ else rendered_content
284
+ )
285
+ output_path.write_text(content_str, encoding="utf-8")
286
+
287
+ print(
288
+ f"Rendered {source_label} to {output_path} ({format} format)",
289
+ file=sys.stderr,
290
+ )
291
+ return str(output_path)
@@ -0,0 +1,411 @@
1
+ """CLI command for `dft schema`.
2
+
3
+ Thin wrapper. All logic lives in ``dataface.agent_api.schema`` (drill-down)
4
+ and ``dataface.agent_api.schema_search`` (search mode); this file parses CLI
5
+ args, calls the appropriate verb, and renders for the terminal.
6
+
7
+ Modes
8
+ -----
9
+ - Drill-down (default): positional ``SOURCE``, ``SCHEMA``, ``TABLE``, ``COLUMN``
10
+ - Search mode (``-s`` / ``--search`` present): dispatches to schema_search;
11
+ all filter flags (``--role``, ``--tag``, ``--scope``, etc.) apply.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import typer
21
+
22
+ warnings.filterwarnings(
23
+ "ignore",
24
+ message=r"Core Pydantic V1 functionality isn't compatible with Python 3\.14",
25
+ category=UserWarning,
26
+ module=r"dsi_pydantic_shim",
27
+ )
28
+
29
+ from dataface.agent_api.schema import SchemaResponse, schema as _schema
30
+ from dataface.agent_api.schema_search import (
31
+ SchemaSearchResult,
32
+ schema_search as _schema_search,
33
+ )
34
+ from dataface.cli._error_format import print_structured_errors
35
+ from dataface.cli._parsing import parse_kv_pairs
36
+ from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
37
+ from dataface.core.execute.adapters import build_adapter_registry
38
+
39
+
40
+ def schema_command(
41
+ source: str | None = None,
42
+ schema: str | None = None,
43
+ table: str | None = None,
44
+ column: str | None = None,
45
+ json_output: bool = False,
46
+ project_dir: Path | None = None,
47
+ lineage_depth: int = 1,
48
+ # Search-mode args (only used when search is not None)
49
+ search: str | None = None,
50
+ scope: str | None = None,
51
+ regex: bool = False,
52
+ role: str | None = None,
53
+ tag: str | None = None,
54
+ has_test: str | None = None,
55
+ missing: str | None = None,
56
+ column_name: str | None = None,
57
+ table_name: str | None = None,
58
+ fk_to: str | None = None,
59
+ meta: list[str] | None = None,
60
+ fields: str | None = None,
61
+ limit: int | None = None,
62
+ ) -> None:
63
+ """Browse the data hierarchy or search the schema corpus.
64
+
65
+ ``-s`` / ``--search`` has context-dependent semantics:
66
+
67
+ - With ``SOURCE`` + ``SCHEMA`` (no ``TABLE`` / ``COLUMN``): applies a
68
+ ``re.search`` regexp filter on the level-3 table list (table name or cached
69
+ column name match). This calls ``agent_api.schema(table_search=...)``.
70
+ - Without drill-down args: full-text + predicate corpus search via
71
+ ``agent_api.schema_search``. All ``--role``, ``--tag``, etc. flags apply here.
72
+ - With any other drill-down arg combination: raises ``BadParameter``.
73
+
74
+ MCP callers use ``table_search`` directly on the ``schema`` tool to get the
75
+ regexp filter without going through the ``-s`` routing.
76
+ """
77
+ project_root = (project_dir or Path(".")).resolve()
78
+ adapter_registry = build_adapter_registry(project_root, read_only=True)
79
+
80
+ if search is not None:
81
+ # Level-3 drill-down + regexp filter: dft schema SOURCE SCHEMA -s PATTERN
82
+ # When source + schema are given but table/column are not, -s applies a
83
+ # regexp table/column-name filter on the drill-down result instead of
84
+ # dispatching to schema_search.
85
+ if (
86
+ source is not None
87
+ and schema is not None
88
+ and table is None
89
+ and column is None
90
+ ):
91
+ # Reject search-mode-only flags — they have no meaning in filter mode
92
+ # and silently dropping them would produce a wrong-looking result.
93
+ search_only_flags = {
94
+ "--role": role,
95
+ "--tag": tag,
96
+ "--has-test": has_test,
97
+ "--missing": missing,
98
+ "--column-name": column_name,
99
+ "--table-name": table_name,
100
+ "--fk-to": fk_to,
101
+ "--meta": meta,
102
+ "--fields": fields,
103
+ "--limit": limit,
104
+ "--scope": scope,
105
+ "--regex": regex or None,
106
+ }
107
+ active_search_flags = [f for f, v in search_only_flags.items() if v]
108
+ if active_search_flags:
109
+ raise typer.BadParameter(
110
+ f"flags {active_search_flags} are search-mode-only and cannot "
111
+ "be combined with SOURCE + SCHEMA + -s (filter mode). "
112
+ "Drop the drill-down args to use search mode, or drop the "
113
+ "predicate flags to use filter mode."
114
+ )
115
+ drill_result = _schema(
116
+ source=source,
117
+ schema=schema,
118
+ lineage_depth=lineage_depth,
119
+ table_search=search,
120
+ surface="cli",
121
+ adapter_registry=adapter_registry,
122
+ )
123
+ if json_output:
124
+ typer.echo(
125
+ drill_result.model_dump_json(
126
+ by_alias=True, indent=2, exclude_none=True
127
+ )
128
+ )
129
+ if not drill_result.success:
130
+ raise typer.Exit(1)
131
+ return
132
+ _print_rich(
133
+ drill_result, source=source, schema=schema, table=None, column=None
134
+ )
135
+ if not drill_result.success:
136
+ raise typer.Exit(1)
137
+ return
138
+
139
+ drill_args = {
140
+ "SOURCE": source,
141
+ "SCHEMA": schema,
142
+ "TABLE": table,
143
+ "COLUMN": column,
144
+ }
145
+ conflicts = [arg for arg, val in drill_args.items() if val is not None]
146
+ if conflicts:
147
+ raise typer.BadParameter(
148
+ f"drill-down args {conflicts} cannot be combined with -s/--search. "
149
+ "Use -s for search mode or positional args for navigation, not both."
150
+ )
151
+
152
+ parsed_meta = parse_kv_pairs(meta, "--meta") if meta else None
153
+
154
+ result = _schema_search(
155
+ keyword=search,
156
+ scope=[s.strip() for s in scope.split(",") if s.strip()] if scope else None,
157
+ regex=regex,
158
+ role=role,
159
+ tag=tag,
160
+ has_test=has_test,
161
+ missing=missing,
162
+ meta=parsed_meta,
163
+ column_name=column_name,
164
+ table_name=table_name,
165
+ fk_to=fk_to,
166
+ fields=(
167
+ [f.strip() for f in fields.split(",") if f.strip()] if fields else None
168
+ ),
169
+ limit=limit,
170
+ adapter_registry=adapter_registry,
171
+ )
172
+ if json_output:
173
+ typer.echo(result.model_dump_json(by_alias=True, exclude_none=True))
174
+ if not result.success:
175
+ raise typer.Exit(1)
176
+ return
177
+ _print_rich_search(result)
178
+ if not result.success:
179
+ raise typer.Exit(1)
180
+ return
181
+
182
+ drill_result = _schema(
183
+ source=source,
184
+ schema=schema,
185
+ table=table,
186
+ column=column,
187
+ lineage_depth=lineage_depth,
188
+ surface="cli",
189
+ adapter_registry=adapter_registry,
190
+ )
191
+
192
+ if json_output:
193
+ typer.echo(
194
+ drill_result.model_dump_json(by_alias=True, indent=2, exclude_none=True)
195
+ )
196
+ if not drill_result.success:
197
+ raise typer.Exit(1)
198
+ return
199
+
200
+ _print_rich(drill_result, source=source, schema=schema, table=table, column=column)
201
+ if not drill_result.success:
202
+ raise typer.Exit(1)
203
+
204
+
205
+ def _print_rich_search(result: SchemaSearchResult) -> None:
206
+ if not result.success:
207
+ # SchemaSearchResult carries only plain strings in errors; synthesize a
208
+ # DF-UNKNOWN-INTERNAL wrapper so print_structured_errors renders cleanly.
209
+ print_structured_errors(
210
+ [
211
+ StructuredError(
212
+ code=DF_UNKNOWN_INTERNAL.code,
213
+ domain=DF_UNKNOWN_INTERNAL.domain,
214
+ doc_url=DF_UNKNOWN_INTERNAL.doc_url,
215
+ docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
216
+ message="; ".join(result.errors),
217
+ )
218
+ ]
219
+ )
220
+ return
221
+ if not result.hits:
222
+ typer.echo("No matches.")
223
+ return
224
+ for hit in result.hits:
225
+ typer.echo(f" [{hit.source}] {hit.location} ({hit.matched_field})")
226
+ if hit.snippet:
227
+ typer.echo(f" {hit.snippet}")
228
+ if result.truncated:
229
+ typer.echo(f"... truncated to {len(result.hits)} of {result.total} hits")
230
+
231
+
232
+ def _spec_is_exact(arg: str) -> bool:
233
+ """Return True when the CLI arg is a literal name (no glob wildcards)."""
234
+ return "*" not in arg and "?" not in arg and "[" not in arg
235
+
236
+
237
+ def _iter_drill_tables(
238
+ sources: dict[str, Any],
239
+ source: str,
240
+ schema: str,
241
+ ) -> list[tuple[str, str, dict[str, Any]]]:
242
+ """Yield (schema_name, table_name, table_data) over the matching subtree.
243
+
244
+ When schema is an exact name, only that schema is visited. When it contains
245
+ a wildcard, every schema under the source is visited.
246
+ """
247
+ schemas = sources.get(source, {}).get("schemas", {})
248
+ if _spec_is_exact(schema):
249
+ tables = schemas.get(schema, {}).get("tables") or {}
250
+ return [(schema, tname, tdata) for tname, tdata in tables.items()]
251
+ result = []
252
+ for sname, sdata in schemas.items():
253
+ for tname, tdata in (sdata.get("tables") or {}).items():
254
+ result.append((sname, tname, tdata))
255
+ return result
256
+
257
+
258
+ def _print_rich(
259
+ result: SchemaResponse,
260
+ source: str | None,
261
+ schema: str | None,
262
+ table: str | None,
263
+ column: str | None,
264
+ ) -> None:
265
+ if not result.success:
266
+ print_structured_errors(
267
+ result.structured_errors
268
+ or [
269
+ StructuredError(
270
+ code=DF_UNKNOWN_INTERNAL.code,
271
+ domain=DF_UNKNOWN_INTERNAL.domain,
272
+ doc_url=DF_UNKNOWN_INTERNAL.doc_url,
273
+ docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
274
+ message="; ".join(result.errors),
275
+ )
276
+ ]
277
+ )
278
+ return
279
+
280
+ sources = result.sources
281
+ if (
282
+ column is not None
283
+ and table is not None
284
+ and source is not None
285
+ and schema is not None
286
+ ):
287
+ rows = _iter_drill_tables(sources, source, schema)
288
+ schema_is_wildcard = not _spec_is_exact(schema)
289
+ first = True
290
+ for sname, tname, tdata in rows:
291
+ label = f"{sname}.{tname}" if schema_is_wildcard else tname
292
+ for cname, cdata in (tdata.get("columns") or {}).items():
293
+ if not first:
294
+ typer.echo("")
295
+ _echo_column(f"{label}.{cname}", cdata)
296
+ first = False
297
+ return
298
+
299
+ if table is not None and source is not None and schema is not None:
300
+ rows = _iter_drill_tables(sources, source, schema)
301
+ schema_is_wildcard = not _spec_is_exact(schema)
302
+ first = True
303
+ for sname, tname, tdata in rows:
304
+ if not first:
305
+ typer.echo("")
306
+ label = f"{sname}.{tname}" if schema_is_wildcard else tname
307
+ _echo_table_profile(label, tdata)
308
+ first = False
309
+ return
310
+
311
+ if schema is not None and source is not None:
312
+ if result.message:
313
+ typer.echo(result.message)
314
+ for hint in result.hints or []:
315
+ typer.echo(f" → {hint}")
316
+ for warning in result.warnings or []:
317
+ typer.echo(f" ⚠ {warning}")
318
+ schemas_branch = sources.get(source, {}).get("schemas", {})
319
+ for sname, sdata in schemas_branch.items():
320
+ tables_branch = sdata.get("tables") or {}
321
+ if not tables_branch:
322
+ continue
323
+ typer.echo(f" [{sname}]")
324
+ for tname, summary in tables_branch.items():
325
+ kind = summary.get("kind", "table")
326
+ rc = summary.get("row_count")
327
+ tag = f" rows={rc}" if rc is not None else ""
328
+ typer.echo(f" {tname} ({kind}){tag}")
329
+ return
330
+
331
+ if source is not None:
332
+ schemas_branch = sources.get(source, {}).get("schemas", {})
333
+ for sname, sdata in schemas_branch.items():
334
+ count = sdata.get("table_count")
335
+ tag = f" ({count} tables)" if count is not None else ""
336
+ typer.echo(f" {sname}{tag}")
337
+ return
338
+
339
+ if not sources:
340
+ typer.echo("No data sources configured.")
341
+ return
342
+ for sname, sdata in sources.items():
343
+ stype = sdata.get("type", "unknown")
344
+ path = sdata.get("path")
345
+ line = f" {sname} ({stype})"
346
+ if path:
347
+ line += f" {path}"
348
+ typer.echo(line)
349
+
350
+
351
+ def _echo_table_profile(name: str, data: dict[str, Any]) -> None:
352
+ typer.echo(f"Table: {name}")
353
+ if (rc := data.get("row_count")) is not None:
354
+ typer.echo(f" rows: {rc}")
355
+ if (desc := data.get("description")) is not None:
356
+ typer.echo(f" description: {desc}")
357
+ upstream = data.get("upstream") or []
358
+ downstream = data.get("downstream") or []
359
+ if upstream:
360
+ parts = [
361
+ f"{r['table']} ({'source' if r.get('kind') == 'source' else 'ref'})"
362
+ for r in upstream
363
+ ]
364
+ typer.echo(f" ↑ upstream: {', '.join(parts)}")
365
+ if downstream:
366
+ typer.echo(f" ↓ downstream: {', '.join(r['table'] for r in downstream)}")
367
+ partitions = data.get("partitions")
368
+ if (
369
+ partitions
370
+ and partitions.get("supported", False)
371
+ and partitions.get("type") not in {"none", "unpartitioned"}
372
+ ):
373
+ col = partitions.get("column")
374
+ n = len(partitions.get("entries") or [])
375
+ col_str = f" on {col}" if col else ""
376
+ noun = "entry" if n == 1 else "entries"
377
+ typer.echo(f" partitions: {partitions['type']}{col_str} ({n} {noun})")
378
+ if (lm := data.get("last_modified")) is not None:
379
+ typer.echo(f" last_modified: {lm}")
380
+ for cname, cdata in (data.get("columns") or {}).items():
381
+ ctype = cdata.get("type") or cdata.get("actual_type") or ""
382
+ sem = cdata.get("semantic_type") or ""
383
+ suffix = f" {sem}" if sem else ""
384
+ rel_suffix = ""
385
+ for rel in cdata.get("relationships") or []:
386
+ rel_suffix += f" → {rel['to_table']}.{rel['to_column']}"
387
+ typer.echo(f" {cname} {ctype}{suffix}{rel_suffix}")
388
+ for ref in data.get("referenced_by") or []:
389
+ typer.echo(f" ← {ref['from_table']}.{ref['from_column']}")
390
+ for hop in data.get("linked_via") or []:
391
+ typer.echo(
392
+ f" via {hop['through_column_a']} → {hop['hop_table_a']},"
393
+ f" via {hop['through_column_b']} → {hop['hop_table_b']}"
394
+ )
395
+
396
+
397
+ def _echo_column(label: str, data: dict[str, Any]) -> None:
398
+ typer.echo(f"Column: {label}")
399
+ ctype = data.get("type") or data.get("actual_type")
400
+ if ctype:
401
+ typer.echo(f" type: {ctype}")
402
+ if (sem := data.get("semantic_type")) is not None:
403
+ typer.echo(f" semantic_type: {sem}")
404
+ if (dist := data.get("distribution")) is not None:
405
+ typer.echo(f" distribution: {dist}")
406
+ if (dc := data.get("distinct_count")) is not None:
407
+ typer.echo(f" distinct_count: {dc}")
408
+ if (np := data.get("null_percentage")) is not None:
409
+ typer.echo(f" null_pct: {np:.1f}%")
410
+ for rel in data.get("relationships") or []:
411
+ typer.echo(f" → {rel['to_table']}.{rel['to_column']}")