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,658 @@
1
+ """Typed schema verb — hierarchical data drill-down: source → schema → table → column.
2
+
3
+ Every response is a single ``SchemaResponse`` envelope wrapped in
4
+ ``sources``, regardless of drill level. Leaves are bare scalars;
5
+ response-level provenance lives in the ``_meta`` footer.
6
+
7
+ Wildcard targeting at every level: ``*`` for everything, ``"a,b"`` for a
8
+ finite list, ``"stg_*"`` for fnmatch globs. The resolver expands
9
+ wildcards once at entry; this verb is just the wire-shape adapter.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any, Literal
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+ from dataface.core.compile.errors import DatafaceError
22
+ from dataface.core.errors import (
23
+ DF_EXECUTE_COLUMN_NOT_FOUND,
24
+ DF_EXECUTE_SCHEMA_NOT_FOUND,
25
+ DF_EXECUTE_TABLE_NOT_FOUND,
26
+ )
27
+ from dataface.core.errors.structured import StructuredError
28
+ from dataface.core.execute.adapters import AdapterRegistry
29
+ from dataface.core.inspect.cache_factory import (
30
+ build_resolver,
31
+ project_root_for_registry,
32
+ )
33
+ from dataface.core.inspect.partition_types import TablePartitions
34
+ from dataface.core.inspect.resolver import LayeredSchemaResolver, is_exact_target
35
+ from dataface.core.schema.guidance import attach_empty_table_list_hints
36
+
37
+ if TYPE_CHECKING:
38
+ # SuperSchemaSource lives in the private dataface-super-schema package.
39
+ # Accepted by schema() as an injection point for callers with the private
40
+ # package installed. At runtime the type is Any; mypy sees the class via
41
+ # the guard.
42
+ from dataface_super_schema.inspect.sources.super_schema import SuperSchemaSource
43
+
44
+
45
+ class SchemaArgs(BaseModel):
46
+ """Browse the data hierarchy: source → schema → table → column.
47
+
48
+ The deepest non-None arg determines the return tier. No args: lists
49
+ configured data sources (level 1). source=X: lists schema/namespace
50
+ names in that source (level 2). source=X, schema=Y: lists tables with
51
+ lean summaries (level 3 — name, kind, row_count, description, tags
52
+ when known; full column profiles require drilling). source=X,
53
+ schema=Y, table=T: deep-profiles that table (level 4). source=X,
54
+ schema=Y, table=T, column=C: column profile (level 5). Each level
55
+ accepts ``*``, comma-lists (``"a,b,c"``), and fnmatch globs
56
+ (``"stg_*"``) for multi-target queries. Always wrapped in ``sources``;
57
+ response-level provenance lives in the ``_meta`` footer. If
58
+ it returns zero tables, do not assume the project has no data — some
59
+ projects query files directly (for example via read_csv(...)) instead
60
+ of persistent database tables.
61
+
62
+ Power-user note: ``--json | jq`` is the escape hatch for ad-hoc
63
+ cross-cutting queries the curated verbs don't anticipate — e.g. find
64
+ every column ending in ``_at``, or list tables above a row-count
65
+ threshold. The JSON shape is a stable named-dict tree; pipe it into
66
+ ``jq`` and use ``.sources.<src>.schemas.<schema>.tables`` as the root.
67
+ For keyword and predicate search across descriptions, tags, and roles
68
+ use ``dft schema -s <keyword>`` instead.
69
+ """
70
+
71
+ model_config = ConfigDict(populate_by_name=True)
72
+
73
+ source: str | None = Field(
74
+ None,
75
+ description="Data source name. Omit to list all configured sources.",
76
+ )
77
+ schema_name: str | None = Field(
78
+ None,
79
+ alias="schema",
80
+ description=(
81
+ "Schema/dataset/namespace name. Requires source. Omit to list "
82
+ "schemas in the source. Accepts wildcards: '*', comma-lists, "
83
+ "fnmatch globs ('stg_*')."
84
+ ),
85
+ )
86
+ table: str | None = Field(
87
+ None,
88
+ description=(
89
+ "Table name. Requires source and schema. Accepts wildcards: "
90
+ "'*', comma-lists, fnmatch globs ('stg_*')."
91
+ ),
92
+ )
93
+ column: str | None = Field(
94
+ None,
95
+ description=(
96
+ "Column name. Requires source, schema, and table. Accepts "
97
+ "wildcards: '*', comma-lists, fnmatch globs ('*_id')."
98
+ ),
99
+ )
100
+ force_refresh: bool = Field(
101
+ False,
102
+ description=(
103
+ "Re-profile a specific table even if cached data exists; valid only "
104
+ "with source, schema, and table, and without column."
105
+ ),
106
+ )
107
+ table_search: str | None = Field(
108
+ None,
109
+ description=(
110
+ "Regexp filter (re.search, re.IGNORECASE) applied at level 3 "
111
+ "(source + schema → tables). A table is included when the pattern matches "
112
+ "its name or any cached column name — the match is unanchored, so 'id' "
113
+ "matches 'user_id'. Un-profiled tables filter on name only; no extra DB "
114
+ "queries are issued. Example: 'revenue|arr|mrr'."
115
+ ),
116
+ )
117
+
118
+
119
+ class ResponseMeta(BaseModel):
120
+ """Response-level provenance footer.
121
+
122
+ Present on successful resolver responses. Validation-error envelopes may
123
+ omit it. ``sources_consulted`` reports which layers contributed:
124
+ ``["super_schema"]`` on cache hit, ``["dbt_adapter", "dbt_manifest"]``
125
+ on cache miss with manifest, ``["dbt_adapter"]`` without manifest, ``[]``
126
+ for level 1 (registry-only).
127
+
128
+ ``retrieved_at`` and ``cache_built_at`` are ISO-8601 strings, not
129
+ ``datetime`` objects — the MCP transport calls plain ``json.dumps``
130
+ on the model_dump output and would choke on a datetime instance.
131
+ The resolver stamps them via ``datetime.isoformat()`` upstream.
132
+ """
133
+
134
+ model_config = ConfigDict(populate_by_name=True)
135
+
136
+ retrieved_at: str
137
+ sources_consulted: list[Literal["super_schema", "dbt_adapter", "dbt_manifest"]] = []
138
+ cache_built_at: str | None = None
139
+
140
+
141
+ class TableProfile(BaseModel):
142
+ """Typed level-4 table profile fields added by the schema verb.
143
+
144
+ The named fields (``partitions``, ``last_modified``) enumerate the
145
+ agent-api typed contract; additional fields emitted by the
146
+ ``LayeredSchemaResolver`` (rows, description, columns, …) flow through
147
+ via ``extra='allow'`` because the resolver owns the level-4 wire shape
148
+ and this model is the bridge — not the spec.
149
+ """
150
+
151
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
152
+
153
+ partitions: TablePartitions | None = None
154
+ last_modified: datetime | None = None
155
+
156
+
157
+ class SchemaResponse(BaseModel):
158
+ """Single envelope for every drill level.
159
+
160
+ ``sources`` is the named-dict tree (``dict[str, ...]`` at every level).
161
+ ``_meta`` carries response-level provenance. ``success`` and ``errors``
162
+ preserve the MCP envelope guarantee. ``structured_errors`` carries
163
+ fully-typed error objects for MCP consumers that can act on code/doc_url.
164
+ """
165
+
166
+ model_config = ConfigDict(populate_by_name=True)
167
+
168
+ success: bool = True
169
+ sources: dict[str, dict[str, Any]] = Field(
170
+ default_factory=dict,
171
+ description="Schema data keyed by source name (tables, columns, etc.).",
172
+ )
173
+ supported_query_types: list[str] | None = None
174
+ meta: ResponseMeta | None = Field(
175
+ default=None, alias="_meta", description="Pagination and timing metadata."
176
+ )
177
+ errors: list[str] = []
178
+ structured_errors: list[StructuredError] = []
179
+ # Empty-tables UX: when level 3 returns nothing the verb attaches
180
+ # discoverability hints. Optional; absent on success-with-results.
181
+ message: str | None = None
182
+ warnings: list[str] | None = None
183
+ hints: list[str] | None = None
184
+
185
+
186
+ _LINEAGE_DEPTH_MIN = 1
187
+ _LINEAGE_DEPTH_MAX = 10
188
+
189
+
190
+ def schema(
191
+ source: str | None = None,
192
+ schema: str | None = None,
193
+ table: str | None = None,
194
+ column: str | None = None,
195
+ *,
196
+ cache_path: Path | None = None,
197
+ cache_source: SuperSchemaSource | None = None,
198
+ force_refresh: bool = False,
199
+ lineage_depth: int = 1,
200
+ table_search: str | None = None,
201
+ surface: Literal["cli", "mcp"] = "mcp",
202
+ adapter_registry: AdapterRegistry,
203
+ ) -> SchemaResponse:
204
+ """Browse the data hierarchy: source → schema → table → column.
205
+
206
+ ``table_search`` is a regexp (``re.IGNORECASE``) applied at level 3 (schema
207
+ → tables). A table passes if the pattern matches its name OR any cached
208
+ column name. For un-profiled tables (no cached column list), name-only
209
+ matching is applied — no extra DB queries are issued.
210
+
211
+ ``cache_path``: Explicit path to ``super_schema.json``. When ``None``
212
+ (default), the path is auto-derived from the registry's project root.
213
+ Pass an explicit path only when the cache lives outside the project root.
214
+
215
+ ``cache_source``: Inject a pre-constructed ``SuperSchemaSource`` directly.
216
+ Takes priority over ``cache_path`` when both are provided. Use when
217
+ the caller has already constructed the cache source.
218
+
219
+ ``surface``: Controls vocabulary in empty-cache hints. ``"cli"`` emits
220
+ ``dft inspect`` / ``dft search`` / ``dft query`` verbs; ``"mcp"`` (default)
221
+ emits MCP tool names (``search_dashboards``, ``execute_query``).
222
+ """
223
+ if not (_LINEAGE_DEPTH_MIN <= lineage_depth <= _LINEAGE_DEPTH_MAX):
224
+ return SchemaResponse(
225
+ success=False,
226
+ errors=[
227
+ f"lineage_depth must be between {_LINEAGE_DEPTH_MIN} and "
228
+ f"{_LINEAGE_DEPTH_MAX}, got {lineage_depth}"
229
+ ],
230
+ )
231
+
232
+ invalid = _validate_schema_request(
233
+ source=source,
234
+ schema=schema,
235
+ table=table,
236
+ column=column,
237
+ force_refresh=force_refresh,
238
+ table_search=table_search,
239
+ )
240
+ if invalid is not None:
241
+ return invalid
242
+
243
+ compiled_search: re.Pattern[str] | None = None
244
+ if table_search is not None:
245
+ try:
246
+ compiled_search = re.compile(table_search, re.IGNORECASE)
247
+ except re.error as exc:
248
+ return SchemaResponse(
249
+ success=False,
250
+ errors=[f"table_search is not a valid regexp: {exc}"],
251
+ )
252
+
253
+ if source is None:
254
+ response = _list_sources_response(adapter_registry)
255
+ response.supported_query_types = sorted(adapter_registry.supported_types)
256
+ return response
257
+
258
+ try:
259
+ resolver = build_resolver(
260
+ adapter_registry, cache_path, cache_source=cache_source
261
+ )
262
+ raw = _dispatch_schema_request(
263
+ resolver=resolver,
264
+ source=source,
265
+ schema=schema,
266
+ table=table,
267
+ column=column,
268
+ force_refresh=force_refresh,
269
+ lineage_depth=lineage_depth,
270
+ )
271
+ response = SchemaResponse.model_validate(raw)
272
+ _validate_table_profiles(
273
+ response,
274
+ source=source,
275
+ schema=schema,
276
+ table=table,
277
+ column=column,
278
+ )
279
+ not_found = _validate_exact_targets_found(
280
+ response,
281
+ resolver=resolver,
282
+ source=source,
283
+ schema=schema,
284
+ table=table,
285
+ column=column,
286
+ )
287
+ if not_found is not None:
288
+ return not_found
289
+ except DatafaceError as exc:
290
+ return SchemaResponse(
291
+ success=False,
292
+ errors=[str(exc)],
293
+ structured_errors=[exc.to_structured()],
294
+ )
295
+ except Exception as exc: # noqa: BLE001 — verb returns errors via response envelope
296
+ return SchemaResponse(success=False, errors=[str(exc)])
297
+
298
+ if schema is not None and table is None:
299
+ assert source is not None
300
+ if compiled_search is not None:
301
+ _filter_tables_by_search(
302
+ response, source=source, rx=compiled_search, resolver=resolver
303
+ )
304
+ # Skip the "no tables found" cache-guidance hints — the empty result is
305
+ # from the filter, not a missing cache. Attach a filter-specific message.
306
+ # compiled_search was built from table_search, so table_search is str here.
307
+ assert isinstance(table_search, str)
308
+ _attach_filtered_empty_message(
309
+ response, source=source, pattern=table_search
310
+ )
311
+ else:
312
+ project_root = project_root_for_registry(adapter_registry)
313
+ attach_empty_table_list_hints(
314
+ response,
315
+ adapter_registry,
316
+ source,
317
+ uses_direct_file_queries=lambda: _project_uses_direct_file_queries(
318
+ project_root
319
+ ),
320
+ surface=surface,
321
+ )
322
+
323
+ return response
324
+
325
+
326
+ def _validate_exact_targets_found(
327
+ response: SchemaResponse,
328
+ resolver: LayeredSchemaResolver,
329
+ source: str,
330
+ schema: str | None,
331
+ table: str | None,
332
+ column: str | None,
333
+ ) -> SchemaResponse | None:
334
+ """Check whether every exact drill-down target exists in the source universe.
335
+
336
+ Only validates tiers where the spec is exact AND all parent tiers are also
337
+ exact. Returns a ``SchemaResponse(success=False, ...)`` envelope on the
338
+ first missing tier, or ``None`` when all exact targets were found (or a
339
+ wildcard appears at some tier).
340
+
341
+ Universe queries (``list_schemas``, ``list_tables``) are deferred to the
342
+ error path — only called when the response tree is missing the target.
343
+ This avoids extra adapter round-trips on successful requests.
344
+ """
345
+ if not is_exact_target(schema):
346
+ return None
347
+
348
+ # Check schema tier using the response tree (fast path).
349
+ # The resolver omits a schema entry when none of its tables matched the
350
+ # table spec, so absence can mean either "schema not found" or "schema
351
+ # exists but no tables matched". We resolve the ambiguity lazily by
352
+ # querying the universe only when the schema is absent from the response.
353
+ source_data = response.sources.get(source, {})
354
+ schema_in_response = schema in source_data.get("schemas", {})
355
+
356
+ if not schema_in_response:
357
+ # Query the real schema universe to distinguish "schema not found"
358
+ # from "schema found but no tables matched".
359
+ all_schemas = sorted(
360
+ resolver.list_schemas(source=source)
361
+ .get("sources", {})
362
+ .get(source, {})
363
+ .get("schemas", {})
364
+ .keys()
365
+ )
366
+ if schema not in all_schemas:
367
+ exc = DatafaceError.from_code(
368
+ DF_EXECUTE_SCHEMA_NOT_FOUND,
369
+ schema=schema,
370
+ source=source,
371
+ available=", ".join(all_schemas) if all_schemas else "(none)",
372
+ )
373
+ return SchemaResponse(
374
+ success=False,
375
+ errors=[str(exc)],
376
+ structured_errors=[exc.to_structured()],
377
+ )
378
+ # Schema exists but was omitted because no tables matched.
379
+ # Fall through to the table-tier check.
380
+
381
+ if not is_exact_target(table):
382
+ return None
383
+
384
+ # is_exact_target(schema) returned True above, so schema is not None.
385
+ assert schema is not None
386
+
387
+ # Check table tier using the response tree.
388
+ schema_data = source_data.get("schemas", {}).get(schema, {})
389
+ table_in_response = table in schema_data.get("tables", {})
390
+
391
+ if not table_in_response:
392
+ all_tables = sorted(
393
+ resolver.list_tables(source=source, schema=schema)
394
+ .get("sources", {})
395
+ .get(source, {})
396
+ .get("schemas", {})
397
+ .get(schema, {})
398
+ .get("tables", {})
399
+ .keys()
400
+ )
401
+ if table not in all_tables:
402
+ exc = DatafaceError.from_code(
403
+ DF_EXECUTE_TABLE_NOT_FOUND,
404
+ table=table,
405
+ schema=schema,
406
+ source=source,
407
+ available=", ".join(all_tables) if all_tables else "(none)",
408
+ )
409
+ return SchemaResponse(
410
+ success=False,
411
+ errors=[str(exc)],
412
+ structured_errors=[exc.to_structured()],
413
+ )
414
+ # Table exists but is absent from the response because the column
415
+ # filter already dropped it. Fall through to the column-tier check.
416
+
417
+ if not is_exact_target(column):
418
+ return None
419
+
420
+ # Check column tier.
421
+ # When the table is in the response, the column list is reliable (exact
422
+ # table profile was dispatched). When the table was dropped by the column
423
+ # filter (table_in_response is False), the column is definitively absent —
424
+ # we confirmed the table exists above, so the column is not found.
425
+ if table_in_response:
426
+ table_data = schema_data.get("tables", {}).get(table, {})
427
+ columns_present = set((table_data.get("columns") or {}).keys())
428
+ else:
429
+ # Table was dropped by the column filter — fetch the column universe
430
+ # from the live resolver to build the "available" hint.
431
+ # is_exact_target(table) returned True above, so table is not None.
432
+ assert table is not None
433
+ full_profile = resolver.profile_table(source=source, schema=schema, table=table)
434
+ raw_cols = (
435
+ full_profile.get("sources", {})
436
+ .get(source, {})
437
+ .get("schemas", {})
438
+ .get(schema, {})
439
+ .get("tables", {})
440
+ .get(table, {})
441
+ .get("columns", {})
442
+ )
443
+ columns_present = set(raw_cols.keys())
444
+
445
+ if column not in columns_present:
446
+ exc = DatafaceError.from_code(
447
+ DF_EXECUTE_COLUMN_NOT_FOUND,
448
+ col_name=column,
449
+ table=table,
450
+ schema=schema,
451
+ source=source,
452
+ available=(
453
+ ", ".join(sorted(columns_present)) if columns_present else "(none)"
454
+ ),
455
+ )
456
+ return SchemaResponse(
457
+ success=False,
458
+ errors=[str(exc)],
459
+ structured_errors=[exc.to_structured()],
460
+ )
461
+
462
+ return None
463
+
464
+
465
+ def _validate_table_profiles(
466
+ response: SchemaResponse,
467
+ source: str,
468
+ schema: str | None,
469
+ table: str | None,
470
+ column: str | None,
471
+ ) -> None:
472
+ """Validate level-4 profile-table dicts against the agent API shape."""
473
+ if schema is None or table is None or column is not None:
474
+ return
475
+ schemas = response.sources.get(source, {}).get("schemas", {})
476
+ for schema_entry in schemas.values():
477
+ tables = schema_entry.get("tables", {})
478
+ for raw_profile in tables.values():
479
+ if isinstance(raw_profile, dict):
480
+ TableProfile.model_validate(raw_profile)
481
+
482
+
483
+ def _list_sources_response(adapter_registry: AdapterRegistry) -> SchemaResponse:
484
+ sources: dict[str, dict[str, Any]] = {}
485
+ for entry in adapter_registry.list_sql_sources():
486
+ source_entry = dict(entry)
487
+ name = str(source_entry.pop("name"))
488
+ sources[name] = source_entry
489
+ return SchemaResponse(
490
+ sources=sources,
491
+ _meta=ResponseMeta(
492
+ retrieved_at=datetime.now(timezone.utc).isoformat(),
493
+ sources_consulted=[],
494
+ ),
495
+ )
496
+
497
+
498
+ def _filter_tables_by_search(
499
+ response: SchemaResponse,
500
+ source: str,
501
+ rx: re.Pattern[str],
502
+ resolver: LayeredSchemaResolver,
503
+ ) -> None:
504
+ """Remove tables from a level-3 response that don't match ``rx``.
505
+
506
+ A table passes when the compiled pattern matches its name, or when the
507
+ cache has a profile for the table and any cached column name matches.
508
+ Un-profiled tables (no cache entry) are filtered on name only —
509
+ no extra DB queries are issued.
510
+
511
+ Reads the cache file at most once per schema (not once per table).
512
+
513
+ Mutates ``response`` in place.
514
+ """
515
+ schemas = response.sources.get(source, {}).get("schemas", {})
516
+ for schema_name, schema_entry in schemas.items():
517
+ tables = schema_entry.get("tables")
518
+ if not tables:
519
+ continue
520
+ # Single read for the whole schema — avoids N file-parses in the loop.
521
+ # When no cache source is configured (OSS dbt-only mode), there are no
522
+ # profiled column names; filtering falls back to name-only matching.
523
+ col_names_by_table: dict[str, set[str]] = (
524
+ resolver.cache.column_names_for_schema(schema_name)
525
+ if resolver.cache is not None
526
+ else {}
527
+ )
528
+ to_drop = [
529
+ name
530
+ for name in tables
531
+ if not _table_matches(rx, name, col_names_by_table.get(name))
532
+ ]
533
+ for name in to_drop:
534
+ del tables[name]
535
+
536
+
537
+ def _attach_filtered_empty_message(
538
+ response: SchemaResponse,
539
+ source: str,
540
+ pattern: str,
541
+ ) -> None:
542
+ """Set a filter-specific message when table_search empties the table list.
543
+
544
+ Prevents the "no cache / run dft inspect" guidance from being shown when
545
+ the result is empty solely because the regexp matched nothing.
546
+ """
547
+ schemas = response.sources.get(source, {}).get("schemas", {})
548
+ total_tables = sum(len(se.get("tables") or {}) for se in schemas.values())
549
+ if total_tables == 0:
550
+ response.message = f"No tables matched table_search={pattern!r}."
551
+
552
+
553
+ def _table_matches(
554
+ rx: re.Pattern[str],
555
+ table_name: str,
556
+ col_names: set[str] | None,
557
+ ) -> bool:
558
+ """Return True if ``rx`` matches the table name or any column name.
559
+
560
+ ``col_names`` is ``None`` for un-profiled tables — name-only match applies.
561
+ No DB queries are issued.
562
+ """
563
+ if rx.search(table_name):
564
+ return True
565
+ return col_names is not None and any(rx.search(c) for c in col_names)
566
+
567
+
568
+ def _validate_schema_request(
569
+ source: str | None,
570
+ schema: str | None,
571
+ table: str | None,
572
+ column: str | None,
573
+ force_refresh: bool,
574
+ table_search: str | None,
575
+ ) -> SchemaResponse | None:
576
+ if table_search is not None and (
577
+ source is None or schema is None or table is not None or column is not None
578
+ ):
579
+ return SchemaResponse(
580
+ success=False,
581
+ errors=[
582
+ "table_search is only valid at the schema level "
583
+ "(source and schema provided, without table or column)"
584
+ ],
585
+ )
586
+ if force_refresh and (
587
+ source is None or schema is None or table is None or column is not None
588
+ ):
589
+ return SchemaResponse(
590
+ success=False,
591
+ errors=[
592
+ "force_refresh is only valid when querying a specific table "
593
+ "(source, schema, and table, without column)"
594
+ ],
595
+ )
596
+ if column is not None and (table is None or source is None or schema is None):
597
+ return SchemaResponse(
598
+ success=False,
599
+ errors=["Must provide source, schema, and table when querying a column"],
600
+ )
601
+ if table is not None and (source is None or schema is None):
602
+ return SchemaResponse(
603
+ success=False,
604
+ errors=["Must provide source and schema when querying a table"],
605
+ )
606
+ if schema is not None and source is None:
607
+ return SchemaResponse(
608
+ success=False,
609
+ errors=["Must provide source when querying a schema"],
610
+ )
611
+ return None
612
+
613
+
614
+ def _dispatch_schema_request(
615
+ resolver: LayeredSchemaResolver,
616
+ source: str,
617
+ schema: str | None,
618
+ table: str | None,
619
+ column: str | None,
620
+ force_refresh: bool,
621
+ lineage_depth: int,
622
+ ) -> dict[str, Any]:
623
+ if column is not None:
624
+ # mypy: guaranteed by _validate_schema_request before dispatch.
625
+ assert schema is not None and table is not None
626
+ return resolver.profile_column(
627
+ source=source, schema=schema, table=table, column=column
628
+ )
629
+ if table is not None:
630
+ # mypy: guaranteed by _validate_schema_request before dispatch.
631
+ assert schema is not None
632
+ return resolver.profile_table(
633
+ source=source,
634
+ schema=schema,
635
+ table=table,
636
+ fresh=force_refresh,
637
+ lineage_depth=lineage_depth,
638
+ )
639
+ if schema is not None:
640
+ return resolver.list_tables(source=source, schema=schema)
641
+ return resolver.list_schemas(source=source)
642
+
643
+
644
+ def _project_uses_direct_file_queries(project_root: Path) -> bool:
645
+ from dataface.agent_api.dashboards import list_dashboards
646
+
647
+ for dashboard in list_dashboards(directory=project_root, recursive=True).dashboards:
648
+ try:
649
+ content = dashboard.absolute_path.read_text()
650
+ except OSError:
651
+ continue
652
+ if (
653
+ "read_csv(" in content
654
+ or "read_parquet(" in content
655
+ or "read_json(" in content
656
+ ):
657
+ return True
658
+ return False