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,838 @@
1
+ """Enhanced YAML error formatting.
2
+
3
+ This module provides utilities to format YAML validation errors with:
4
+ 1. Line numbers where errors occur
5
+ 2. Context - the actual YAML snippet
6
+ 3. Helpful suggestions ("Did you mean?")
7
+ 4. Graceful handling of common mistakes
8
+
9
+ Refs #94
10
+ """
11
+
12
+ import difflib
13
+ import re
14
+ from collections.abc import Sequence
15
+ from functools import lru_cache
16
+ from typing import TYPE_CHECKING, Any, get_args
17
+
18
+ if TYPE_CHECKING:
19
+ from dataface.core.errors.structured import StructuredError
20
+
21
+ # Union-branch discriminator patterns: strings in Pydantic loc tuples that
22
+ # identify which branch of a union was being tried, not a real field name.
23
+ # These are NOT real YAML keys — they are Pydantic-internal identifiers.
24
+ _UNION_DISCRIMINATOR_PREFIXES = (
25
+ "function-after[",
26
+ "function-before[",
27
+ "function-wrap[",
28
+ "function-plain[",
29
+ "dict[",
30
+ "list[",
31
+ "tagged-union[",
32
+ "union[",
33
+ )
34
+
35
+ # Branch names that are literal Pydantic type names (not real YAML fields)
36
+ _PRIMITIVE_TYPE_DISCRIMINATORS = {"str", "int", "float", "bool", "bytes", "None"}
37
+
38
+ # Names of our known union-branch model types (not real YAML fields)
39
+ _MODEL_TYPE_DISCRIMINATORS = {
40
+ "ForeachItem",
41
+ "ChartPatch",
42
+ "AuthoredFace",
43
+ "Variable",
44
+ "VariableRef",
45
+ "QueryRef",
46
+ "ChartRef",
47
+ }
48
+
49
+
50
+ def _is_union_discriminator(part: Any) -> bool:
51
+ """Return True when a loc element is a Pydantic union-branch label, not a YAML key."""
52
+ if not isinstance(part, str):
53
+ return False
54
+ if part in _PRIMITIVE_TYPE_DISCRIMINATORS:
55
+ return True
56
+ if part in _MODEL_TYPE_DISCRIMINATORS:
57
+ return True
58
+ # Functional discriminator tags (authored.py) use '@' prefix to prevent collision
59
+ # with user-chosen YAML keys ('ref', 'inline', etc.).
60
+ if part.startswith("@"):
61
+ return True
62
+ return any(part.startswith(pfx) for pfx in _UNION_DISCRIMINATOR_PREFIXES)
63
+
64
+
65
+ def _find_discriminator_index(loc: tuple[Any, ...]) -> int | None:
66
+ """Return the index of the union-branch discriminator in loc, or None."""
67
+ for i, part in enumerate(loc):
68
+ if _is_union_discriminator(part):
69
+ return i
70
+ return None
71
+
72
+
73
+ def _branch_score(err: dict[str, Any]) -> int:
74
+ """Score a branch error — lower is better (more informative).
75
+
76
+ extra_forbidden → field we actually authored that's forbidden here → 0 (best)
77
+ missing → required field for this branch missing → 1
78
+ other → some other type mismatch → 2 (noisier)
79
+ """
80
+ t = err.get("type", "")
81
+ if t == "extra_forbidden":
82
+ return 0
83
+ if t == "missing":
84
+ return 1
85
+ return 2
86
+
87
+
88
+ def _collapse_union_validation_errors(
89
+ errors: list[dict[str, Any]],
90
+ ) -> list[dict[str, Any]]:
91
+ """Collapse Pydantic union-branch noise into the most informative errors.
92
+
93
+ Pydantic tries every branch of a union type annotation (str | AuthoredFace
94
+ | ChartPatch | ForeachItem | dict[str,ChartPatch]) for each
95
+ layout row. A single invalid row can produce 14+ error dicts — one per
96
+ (branch × wrong-field) combination. This function keeps only the errors
97
+ from the best-matching branch per unique loc prefix.
98
+
99
+ Algorithm:
100
+ 1. Partition errors into "union branch" (loc contains a discriminator) and
101
+ "non-union" (plain field errors with no discriminator in loc).
102
+ 2. For each unique prefix (loc up to the discriminator), collect all branches.
103
+ 3. Pick the branch whose errors have the lowest aggregate _branch_score.
104
+ 4. Keep the errors from that winning branch, mapped to the canonical loc
105
+ (prefix + remaining path after the discriminator).
106
+ 5. Return non-union errors + winning-branch errors, capped at MAX_GROUPS.
107
+ """
108
+ MAX_GROUPS = 8
109
+
110
+ non_union: list[dict[str, Any]] = []
111
+ # Maps prefix_key → {branch_name: [error_dicts]}
112
+ branch_groups: dict[tuple, dict[str, list[dict[str, Any]]]] = {}
113
+
114
+ for err in errors:
115
+ loc = err.get("loc", ())
116
+ disc_idx = _find_discriminator_index(loc)
117
+ if disc_idx is None:
118
+ non_union.append(err)
119
+ continue
120
+
121
+ prefix = loc[:disc_idx]
122
+ branch_name = str(loc[disc_idx])
123
+ branch_groups.setdefault(prefix, {}).setdefault(branch_name, []).append(err)
124
+
125
+ # Pick the best branch per prefix
126
+ collapsed: list[dict[str, Any]] = list(non_union)
127
+
128
+ for _prefix, branches in branch_groups.items():
129
+ # Score each branch: sum of individual error scores
130
+ scored = sorted(
131
+ branches.items(),
132
+ key=lambda kv: (
133
+ sum(_branch_score(e) for e in kv[1]),
134
+ len(kv[1]), # tie-break: fewer errors
135
+ ),
136
+ )
137
+ best_branch_name, best_errors = scored[0]
138
+
139
+ # Rewrite locs to strip the discriminator so downstream path logic works
140
+ for err in best_errors:
141
+ loc = err.get("loc", ())
142
+ disc_idx = _find_discriminator_index(loc)
143
+ if disc_idx is not None:
144
+ # Canonical loc: prefix + everything after the discriminator
145
+ canonical_loc = loc[:disc_idx] + loc[disc_idx + 1 :]
146
+ collapsed.append({**err, "loc": canonical_loc})
147
+ else:
148
+ collapsed.append(err)
149
+
150
+ return collapsed[:MAX_GROUPS]
151
+
152
+
153
+ def _all_authored_chart_fields() -> list[str]:
154
+ """Union of all field names declared across every per-family chart patch class."""
155
+ from dataface.core.compile.models.chart.authored import (
156
+ AreaChart,
157
+ BarChart,
158
+ CalloutChart,
159
+ GeoshapeChart,
160
+ HeatmapChart,
161
+ KpiChart,
162
+ LayeredChart,
163
+ LineChart,
164
+ PieChart,
165
+ PointMapChart,
166
+ ScatterChart,
167
+ SparkBarChart,
168
+ TableChart,
169
+ )
170
+
171
+ fields: set[str] = set()
172
+ for cls in (
173
+ BarChart,
174
+ LineChart,
175
+ AreaChart,
176
+ ScatterChart,
177
+ HeatmapChart,
178
+ PieChart,
179
+ KpiChart,
180
+ TableChart,
181
+ PointMapChart,
182
+ GeoshapeChart,
183
+ LayeredChart,
184
+ CalloutChart,
185
+ SparkBarChart,
186
+ ):
187
+ fields.update(cls.model_fields.keys())
188
+ return sorted(fields)
189
+
190
+
191
+ @lru_cache(maxsize=1)
192
+ def get_valid_chart_types() -> tuple[str, ...]:
193
+ """Dynamically extract valid chart types from the ChartType enum.
194
+
195
+ This ensures the valid types list stays in sync with the actual
196
+ type definitions in types.py. Uses lru_cache for thread-safe caching.
197
+
198
+ Returns:
199
+ Tuple of valid chart type strings (tuple for hashability/immutability)
200
+ """
201
+ from dataface.core.compile.models.chart.authored import (
202
+ ChartType,
203
+ )
204
+
205
+ return tuple(ct.value for ct in ChartType)
206
+
207
+
208
+ @lru_cache(maxsize=1)
209
+ def get_valid_input_types() -> tuple[str, ...]:
210
+ """Dynamically extract valid input types from VariableInputType.
211
+
212
+ This ensures the valid types list stays in sync with the actual
213
+ type definitions in types.py. Uses lru_cache for thread-safe caching.
214
+
215
+ Returns:
216
+ Tuple of valid input type strings (tuple for hashability/immutability)
217
+ """
218
+ from dataface.core.compile.models.variable.authored import VariableInputType
219
+
220
+ return tuple(get_args(VariableInputType))
221
+
222
+
223
+ @lru_cache(maxsize=1)
224
+ def get_valid_chart_types_with_aliases() -> tuple[str, ...]:
225
+ """Valid chart-type values including alias names (e.g. 'point', 'choropleth').
226
+
227
+ Tools that surface valid-name lists for users (LSP completion, hover,
228
+ diagnostics) need aliases included. The compiler does not — it normalizes
229
+ aliases to canonical names before validating.
230
+ """
231
+ from dataface.core.compile.normalize_charts import CHART_TYPE_ALIASES
232
+
233
+ return get_valid_chart_types() + tuple(CHART_TYPE_ALIASES)
234
+
235
+
236
+ def find_yaml_line_number(
237
+ yaml_content: str,
238
+ field_path: list[str],
239
+ ) -> int | None:
240
+ """Find the line number for a field path in YAML content.
241
+
242
+ Uses a simple regex-based approach to find where a field is defined.
243
+ More reliable than YAML parsing for error reporting since we need
244
+ to match the original source positions.
245
+
246
+ Args:
247
+ yaml_content: Raw YAML string
248
+ field_path: List of keys forming the path (e.g., ["charts", "revenue", "type"])
249
+
250
+ Returns:
251
+ Line number (1-indexed) or None if not found
252
+ """
253
+ if not yaml_content or not field_path:
254
+ return None
255
+
256
+ lines = yaml_content.split("\n")
257
+
258
+ # Track current position in path and indentation level
259
+ current_path_idx = 0
260
+ target_key = field_path[current_path_idx]
261
+ expected_indent = 0
262
+
263
+ for line_num, line in enumerate(lines, start=1):
264
+ # Skip empty lines and comments
265
+ stripped = line.strip()
266
+ if not stripped or stripped.startswith("#"):
267
+ continue
268
+
269
+ # Calculate current indentation
270
+ indent = len(line) - len(line.lstrip())
271
+
272
+ # Check if this line has a key
273
+ if ":" in stripped:
274
+ key_part = stripped.split(":")[0].strip()
275
+
276
+ # If we're looking for a key at this level and found it
277
+ if key_part == target_key and indent >= expected_indent:
278
+ # Move to next part of path
279
+ current_path_idx += 1
280
+
281
+ # If we've found all parts, this is the line
282
+ if current_path_idx >= len(field_path):
283
+ return line_num
284
+
285
+ # Otherwise, update expected indent and target key
286
+ expected_indent = indent + 2 # YAML typically uses 2-space indent
287
+ target_key = field_path[current_path_idx]
288
+
289
+ return None
290
+
291
+
292
+ def get_yaml_context(
293
+ yaml_content: str,
294
+ line_number: int,
295
+ context_lines: int = 2,
296
+ ) -> str:
297
+ """Get YAML context around an error line.
298
+
299
+ Returns the surrounding lines with the error line highlighted.
300
+
301
+ Args:
302
+ yaml_content: Raw YAML string
303
+ line_number: Line number of the error (1-indexed)
304
+ context_lines: Number of lines to show before/after
305
+
306
+ Returns:
307
+ Formatted context string with line numbers and error marker
308
+ """
309
+ lines = yaml_content.split("\n")
310
+
311
+ # Ensure valid line number
312
+ if line_number < 1 or line_number > len(lines):
313
+ return ""
314
+
315
+ # Calculate range
316
+ start = max(0, line_number - context_lines - 1)
317
+ end = min(len(lines), line_number + context_lines)
318
+
319
+ # Build context with line numbers
320
+ result_lines: list[str] = []
321
+ for idx in range(start, end):
322
+ actual_line_num = idx + 1
323
+ line = lines[idx]
324
+
325
+ # Mark the error line
326
+ if actual_line_num == line_number:
327
+ marker = ">>>"
328
+ suffix = " # <-- Error here"
329
+ else:
330
+ marker = " "
331
+ suffix = ""
332
+
333
+ result_lines.append(f"{marker} {actual_line_num:4d} | {line}{suffix}")
334
+
335
+ return "\n".join(result_lines)
336
+
337
+
338
+ def suggest_similar_value(
339
+ invalid_value: str,
340
+ valid_values: Sequence[str],
341
+ cutoff: float = 0.6,
342
+ ) -> str | None:
343
+ """Suggest a similar valid value for a typo.
344
+
345
+ Uses difflib to find close matches to the invalid value.
346
+
347
+ Args:
348
+ invalid_value: The invalid value that was provided
349
+ valid_values: List of valid values to match against
350
+ cutoff: Minimum similarity ratio (0-1) for a match
351
+
352
+ Returns:
353
+ The closest valid value, or None if no good match
354
+ """
355
+ if not invalid_value or not valid_values:
356
+ return None
357
+
358
+ # Normalize for comparison
359
+ normalized_input = invalid_value.lower().strip()
360
+
361
+ # First check for exact match (case insensitive)
362
+ for valid in valid_values:
363
+ if valid.lower() == normalized_input:
364
+ return valid
365
+
366
+ # Use difflib to find close matches
367
+ matches = difflib.get_close_matches(
368
+ normalized_input,
369
+ [v.lower() for v in valid_values],
370
+ n=1,
371
+ cutoff=cutoff,
372
+ )
373
+
374
+ if matches:
375
+ # Return the original case version
376
+ for valid in valid_values:
377
+ if valid.lower() == matches[0]:
378
+ return valid
379
+
380
+ return None
381
+
382
+
383
+ def parse_pydantic_error_path(error_message: str) -> tuple[list[str], str | None]:
384
+ """Parse a Pydantic error message to extract field path and invalid value.
385
+
386
+ Pydantic errors have patterns like:
387
+ - "Field 'charts -> revenue -> type': Input should be ..."
388
+ - "validation error for AuthoredFace\\ncharts -> revenue -> type\\n Input should be..."
389
+
390
+ Args:
391
+ error_message: The error message from Pydantic
392
+
393
+ Returns:
394
+ Tuple of (field_path as list, invalid_value or None)
395
+ """
396
+ field_path: list[str] = []
397
+ invalid_value: str | None = None
398
+
399
+ # Pattern for "Field 'path -> path -> field'" format
400
+ field_match = re.search(r"Field ['\"]([^'\"]+)['\"]", error_message)
401
+ if field_match:
402
+ path_str = field_match.group(1)
403
+ # Split by " -> " (with spaces)
404
+ field_path = [p.strip() for p in path_str.split("->")]
405
+
406
+ # Alternative pattern for raw Pydantic output
407
+ if not field_path:
408
+ # Look for "charts -> revenue -> type" style paths
409
+ path_match = re.search(r"(\w+(?:\s*->\s*\w+)+)", error_message)
410
+ if path_match:
411
+ path_str = path_match.group(1)
412
+ field_path = [p.strip() for p in path_str.split("->")]
413
+
414
+ # Try to extract the invalid value from "Input should be" messages
415
+ # These often list valid values, with the invalid one being what was provided
416
+ input_match = re.search(
417
+ r"input[_\s]?value['\"]?:?\s*['\"]?(\w+)", error_message, re.I
418
+ )
419
+ if input_match:
420
+ invalid_value = input_match.group(1)
421
+
422
+ return field_path, invalid_value
423
+
424
+
425
+ def extract_valid_values_from_error(error_message: str) -> list[str]:
426
+ """Extract list of valid values from an error message.
427
+
428
+ Pydantic validation errors often include messages like:
429
+ "Input should be 'bar', 'line', 'area', ..."
430
+
431
+ Args:
432
+ error_message: The error message to parse
433
+
434
+ Returns:
435
+ List of valid values extracted from the message
436
+ """
437
+ valid_values: list[str] = []
438
+
439
+ # Pattern for "should be 'val1', 'val2', ... or 'valN'"
440
+ should_be_match = re.search(
441
+ r"should be\s+['\"]?(\w+)['\"]?(?:,\s*['\"]?(\w+)['\"]?)*(?:\s+or\s+['\"]?(\w+)['\"]?)?",
442
+ error_message,
443
+ re.I,
444
+ )
445
+ if should_be_match:
446
+ # Extract all quoted values
447
+ values = re.findall(r"['\"](\w+)['\"]", error_message)
448
+ valid_values.extend(values)
449
+
450
+ return list(set(valid_values)) # Deduplicate
451
+
452
+
453
+ def format_validation_error(
454
+ error: Exception,
455
+ yaml_content: str | None = None,
456
+ ) -> str:
457
+ """Format a validation error with enhanced information.
458
+
459
+ Adds line numbers, context, and suggestions to validation errors.
460
+
461
+ Args:
462
+ error: The validation error (Pydantic or other)
463
+ yaml_content: Optional YAML content for context extraction
464
+
465
+ Returns:
466
+ Formatted error message with enhanced information
467
+ """
468
+ error_msg = str(error)
469
+
470
+ from pydantic import ValidationError as PydanticValidationError
471
+
472
+ if isinstance(error, PydanticValidationError):
473
+ return _format_pydantic_validation_error(error, yaml_content)
474
+
475
+ # For non-Pydantic errors, try to enhance based on message pattern
476
+ return _enhance_error_message(error_msg, yaml_content)
477
+
478
+
479
+ def format_validation_errors_structured(
480
+ error: Any,
481
+ yaml_content: str | None = None,
482
+ ) -> "list[StructuredError]":
483
+ """Format a Pydantic ValidationError into collapsed StructuredError objects.
484
+
485
+ Each returned StructuredError corresponds to one collapsed error group (not
486
+ one raw Pydantic error). Union-branch phantom failures are suppressed; the
487
+ best-matching branch per loc prefix is kept. Cap: MAX_GROUPS errors.
488
+
489
+ Args:
490
+ error: A pydantic.ValidationError instance.
491
+ yaml_content: Raw YAML string — used for line-number resolution.
492
+
493
+ Returns:
494
+ list of StructuredError (≤ MAX_GROUPS elements).
495
+ """
496
+ from dataface.core.errors.codes_compile import DF_COMPILE_EXTRA_FIELD
497
+ from dataface.core.errors.codes_unknown import DF_UNKNOWN_INTERNAL
498
+ from dataface.core.errors.structured import StructuredError
499
+
500
+ raw_errors = error.errors()
501
+ if not raw_errors:
502
+ return [
503
+ StructuredError(
504
+ code=DF_UNKNOWN_INTERNAL.code,
505
+ message=str(error),
506
+ domain=DF_UNKNOWN_INTERNAL.domain,
507
+ doc_url=DF_UNKNOWN_INTERNAL.doc_url,
508
+ )
509
+ ]
510
+
511
+ collapsed = _collapse_union_validation_errors(raw_errors)
512
+
513
+ structured: list[StructuredError] = []
514
+ for err in collapsed:
515
+ loc = err.get("loc", ())
516
+ err_type = err.get("type", "")
517
+ err_msg = err.get("msg", "Validation error")
518
+
519
+ field_path_parts = [str(p) for p in loc if not str(p).startswith("function-")]
520
+ field_path_str = " -> ".join(field_path_parts) if field_path_parts else ""
521
+
522
+ # Resolve line number from the field path.
523
+ # Strip numeric indices (list positions) — find_yaml_line_number only
524
+ # understands named keys and cannot navigate by index. Skipping them
525
+ # still yields a useful line for the surrounding block.
526
+ line_num: int | None = None
527
+ if yaml_content and field_path_parts:
528
+ named_parts = [p for p in field_path_parts if not p.isdigit()]
529
+ if named_parts:
530
+ line_num = find_yaml_line_number(yaml_content, named_parts)
531
+
532
+ # Pick the most specific error code based on Pydantic error type
533
+ if err_type == "extra_forbidden":
534
+ ec = DF_COMPILE_EXTRA_FIELD
535
+ else:
536
+ ec = DF_UNKNOWN_INTERNAL
537
+
538
+ # Include input value in message when it's a simple string (not a dict/list)
539
+ # so error consumers can see what value was actually provided.
540
+ input_val = err.get("input")
541
+ if field_path_str:
542
+ if isinstance(input_val, str):
543
+ message = f"Field '{field_path_str}': {err_msg} (got: {input_val!r})"
544
+ else:
545
+ message = f"Field '{field_path_str}': {err_msg}"
546
+ else:
547
+ message = err_msg
548
+
549
+ hint = _get_suggestion_for_error(
550
+ err, field_path_parts, err.get("input"), yaml_content
551
+ )
552
+
553
+ structured.append(
554
+ StructuredError(
555
+ code=ec.code,
556
+ message=message,
557
+ domain=ec.domain,
558
+ field_path=field_path_str or None,
559
+ line=line_num,
560
+ hint=hint,
561
+ doc_url=ec.doc_url,
562
+ docs_topic=ec.docs_topic,
563
+ )
564
+ )
565
+
566
+ return structured
567
+
568
+
569
+ def _format_pydantic_validation_error(
570
+ error: Any,
571
+ yaml_content: str | None = None,
572
+ ) -> str:
573
+ """Format a Pydantic ValidationError with enhanced information.
574
+
575
+ Args:
576
+ error: Pydantic ValidationError
577
+ yaml_content: Optional YAML content for context
578
+
579
+ Returns:
580
+ Formatted error message
581
+ """
582
+
583
+ errors = error.errors()
584
+ if not errors:
585
+ return str(error)
586
+
587
+ formatted_parts: list[str] = []
588
+
589
+ for err in errors:
590
+ loc = err.get("loc", [])
591
+ err_msg = err.get("msg", "Validation error")
592
+ input_value = err.get("input")
593
+
594
+ # Build human-readable field path, stripping Pydantic discriminated-union
595
+ # internals: "function-after[...](...)" wrappers and "tagged-union[...]" labels
596
+ # that appear in loc when using Annotated[..., Discriminator(...)].
597
+ field_path = [
598
+ str(p)
599
+ for p in loc
600
+ if not str(p).startswith("function-")
601
+ and not str(p).startswith("tagged-union[")
602
+ ]
603
+ path_str = " -> ".join(field_path) if field_path else "root"
604
+
605
+ # Determine what kind of error this is
606
+ parts: list[str] = []
607
+
608
+ # Find line number if we have YAML content
609
+ line_num = None
610
+ context = ""
611
+ if yaml_content and field_path:
612
+ line_num = find_yaml_line_number(yaml_content, field_path)
613
+ if line_num:
614
+ context = get_yaml_context(yaml_content, line_num)
615
+
616
+ # Build error header with location
617
+ if line_num:
618
+ parts.append(f"Error at line {line_num}:")
619
+ else:
620
+ parts.append("Validation error:")
621
+
622
+ # Add context if available
623
+ if context:
624
+ parts.append(context)
625
+ parts.append("") # Blank line
626
+
627
+ # Add the actual error message
628
+ parts.append(f" Field '{path_str}': {err_msg}")
629
+
630
+ # Add suggestion for invalid values
631
+ suggestion = _get_suggestion_for_error(
632
+ err, field_path, input_value, yaml_content
633
+ )
634
+ if suggestion:
635
+ parts.append(f"\n {suggestion}")
636
+
637
+ formatted_parts.append("\n".join(parts))
638
+
639
+ return "\n\n".join(formatted_parts)
640
+
641
+
642
+ def _get_suggestion_for_error(
643
+ err: dict[str, Any],
644
+ field_path: list[str],
645
+ input_value: Any,
646
+ yaml_content: str | None = None,
647
+ ) -> str | None:
648
+ """Get a helpful suggestion for an error.
649
+
650
+ Args:
651
+ err: Pydantic error dict
652
+ field_path: Path to the field
653
+ input_value: The invalid input value
654
+
655
+ Returns:
656
+ Suggestion string or None
657
+ """
658
+ err_type = err.get("type", "")
659
+ err_msg = err.get("msg", "")
660
+
661
+ # Check if this is a chart type error
662
+ if (
663
+ field_path
664
+ and field_path[-1] == "type"
665
+ and "charts" in field_path
666
+ and isinstance(input_value, str)
667
+ ):
668
+ valid_chart_types = get_valid_chart_types()
669
+ suggestion = suggest_similar_value(input_value, valid_chart_types)
670
+ if suggestion:
671
+ return f"💡 Did you mean '{suggestion}'? Valid chart types: {', '.join(valid_chart_types[:10])}..."
672
+ return f"💡 Valid chart types: {', '.join(valid_chart_types[:10])}..."
673
+
674
+ # Check if this is a variable input type error
675
+ if (
676
+ field_path
677
+ and field_path[-1] == "input"
678
+ and "variables" in field_path
679
+ and isinstance(input_value, str)
680
+ ):
681
+ valid_input_types = get_valid_input_types()
682
+ suggestion = suggest_similar_value(input_value, valid_input_types)
683
+ if suggestion:
684
+ return f"💡 Did you mean '{suggestion}'? Valid input types: {', '.join(valid_input_types)}"
685
+
686
+ # Missing required field
687
+ if "required" in err_type.lower() or "missing" in err_msg.lower():
688
+ field_name = field_path[-1] if field_path else "field"
689
+ return f"💡 The '{field_name}' field is required."
690
+
691
+ # Unknown fields. When AuthoredChart is nested inside AuthoredFace the Pydantic
692
+ # error loc is ("charts", chart_name, family_name, field_name) — length 4
693
+ # (with discriminated union: the family name like "bar", "kpi" is included).
694
+ # The legacy non-discriminated-union path was length 3.
695
+ # At the face root level the loc is length 1. Both cases use the same
696
+ # suggestion engine; only the path heuristic differs.
697
+ if err_type == "extra_forbidden":
698
+ if field_path[0:1] == ["charts"] and len(field_path) >= 3:
699
+ # Nested chart field: charts → chart_name → [family_name →] unknown_key
700
+ field_name = field_path[-1]
701
+ # Migration hint: height/width must be at chart root, not in style:.
702
+ if "style" in field_path and field_name in ("height", "width"):
703
+ return (
704
+ f"💡 chart.{field_name} must be set at the chart root, "
705
+ f"not under style: — move it up one level."
706
+ )
707
+ valid_fields = _all_authored_chart_fields()
708
+ suggestion = suggest_similar_value(field_name, valid_fields)
709
+ if suggestion:
710
+ return f"💡 Unknown chart field. Did you mean '{suggestion}'?"
711
+ return "💡 Unknown chart field. Check the chart schema for supported keys."
712
+
713
+ if len(field_path) == 1:
714
+ field_name = field_path[0]
715
+ yaml_parent_path = _find_yaml_parent_path_for_key(yaml_content, field_name)
716
+
717
+ if yaml_parent_path == []:
718
+ from dataface.core.compile.models.face.authored import AuthoredFace
719
+
720
+ valid_fields = sorted(AuthoredFace.model_fields)
721
+ suggestion = suggest_similar_value(field_name, valid_fields)
722
+ if suggestion:
723
+ return f"💡 Unknown field. Did you mean '{suggestion}'?"
724
+ return "💡 Unknown field. Check the face schema for supported keys."
725
+
726
+ if (
727
+ yaml_parent_path is not None
728
+ and len(yaml_parent_path) == 2
729
+ and yaml_parent_path[0] == "charts"
730
+ ):
731
+ valid_fields = _all_authored_chart_fields()
732
+ suggestion = suggest_similar_value(field_name, valid_fields)
733
+ if suggestion:
734
+ return f"💡 Unknown chart field. Did you mean '{suggestion}'?"
735
+ return (
736
+ "💡 Unknown chart field. Check the chart schema for supported keys."
737
+ )
738
+
739
+ return "💡 Unknown field. Check the schema for supported keys."
740
+
741
+ # For literal/enum type errors, extract valid values from message
742
+ if "literal" in err_type.lower() or "should be" in err_msg.lower():
743
+ valid_values = extract_valid_values_from_error(err_msg)
744
+ if valid_values and isinstance(input_value, str):
745
+ suggestion = suggest_similar_value(input_value, valid_values)
746
+ if suggestion:
747
+ return f"💡 Did you mean '{suggestion}'?"
748
+
749
+ return None
750
+
751
+
752
+ def _find_yaml_parent_path_for_key(
753
+ yaml_content: str | None,
754
+ field_name: str,
755
+ ) -> list[str] | None:
756
+ if not yaml_content:
757
+ return None
758
+
759
+ stack: list[tuple[int, str]] = []
760
+ for line in yaml_content.splitlines():
761
+ stripped = line.strip()
762
+ if not stripped or stripped.startswith("#") or ":" not in stripped:
763
+ continue
764
+
765
+ indent = len(line) - len(line.lstrip())
766
+ while stack and indent <= stack[-1][0]:
767
+ stack.pop()
768
+
769
+ key_part, value_part = stripped.split(":", 1)
770
+ if key_part.startswith("- "):
771
+ key_part = key_part[2:].strip()
772
+ key = key_part.strip().strip("'\"")
773
+ parent_path = [name for _, name in stack]
774
+
775
+ if key == field_name:
776
+ return parent_path
777
+
778
+ if value_part.strip() == "":
779
+ stack.append((indent, key))
780
+
781
+ return None
782
+
783
+
784
+ def _enhance_error_message(
785
+ error_msg: str,
786
+ yaml_content: str | None = None,
787
+ ) -> str:
788
+ """Enhance a non-Pydantic error message.
789
+
790
+ Args:
791
+ error_msg: The error message string
792
+ yaml_content: Optional YAML content for context
793
+
794
+ Returns:
795
+ Enhanced error message
796
+ """
797
+ # Try to find field path in the error message
798
+ field_path, invalid_value = parse_pydantic_error_path(error_msg)
799
+
800
+ parts: list[str] = []
801
+
802
+ # Find line number if possible
803
+ line_num = None
804
+ context = ""
805
+ if yaml_content and field_path:
806
+ line_num = find_yaml_line_number(yaml_content, field_path)
807
+ if line_num:
808
+ context = get_yaml_context(yaml_content, line_num)
809
+
810
+ # Add line number header
811
+ if line_num:
812
+ parts.append(f"Error at line {line_num}:")
813
+ else:
814
+ parts.append("Error:")
815
+
816
+ # Add context
817
+ if context:
818
+ parts.append(context)
819
+ parts.append("")
820
+
821
+ # Add the original error
822
+ parts.append(f" {error_msg}")
823
+
824
+ # Try to add suggestions based on content
825
+ if "chart" in error_msg.lower() and "type" in error_msg.lower():
826
+ # Extract potential invalid type from message
827
+ type_match = re.search(r"['\"](\w+)['\"]", error_msg)
828
+ if type_match:
829
+ invalid_type = type_match.group(1)
830
+ valid_chart_types = get_valid_chart_types()
831
+ if invalid_type not in valid_chart_types:
832
+ suggestion = suggest_similar_value(invalid_type, valid_chart_types)
833
+ if suggestion:
834
+ parts.append(
835
+ f"\n 💡 Did you mean '{suggestion}'? Valid types: {', '.join(valid_chart_types[:10])}..."
836
+ )
837
+
838
+ return "\n".join(parts)