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,823 @@
1
+ """Compilation configuration module.
2
+
3
+ Stage: COMPILE
4
+ Purpose: Provide default values and configuration for compilation.
5
+
6
+ Entry Points:
7
+ - get_config() -> Config
8
+ - get_theme(name) -> ThemeConfig
9
+ - reset_config() -> None
10
+
11
+ Configuration is loaded from the defaults stack and accessed via dot notation:
12
+ config = get_config()
13
+ config.style.board.width # 1200.0
14
+ config.style.background # "#ffffff"
15
+ config.style.charts.aspect_ratio # 1.6
16
+
17
+ Runtime defaults are assembled from:
18
+ - core/defaults/default_config.yml (app-wide defaults + vega.default_theme)
19
+ - core/defaults/themes/*.yaml (unified theme system)
20
+ - core/defaults/palettes/<family>/*.yml (color palettes)
21
+
22
+ YAML is the single source of truth - no Python dataclass defaults.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from collections.abc import Mapping
28
+ from copy import deepcopy
29
+ from functools import cache
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ import yaml
34
+
35
+ from dataface.core.compile.models.config import (
36
+ Config,
37
+ ConfigNode,
38
+ ConfigPatch,
39
+ MarkdownConfig,
40
+ RenderingConfig,
41
+ VegaRuntimeConfig,
42
+ as_plain_mapping,
43
+ is_mapping_like,
44
+ )
45
+ from dataface.core.compile.models.source import resolve_env_vars_in_dict
46
+ from dataface.core.compile.models.theme import ThemeConfig, normalize_raw_theme_config
47
+
48
+ # ============================================================================
49
+ # GLOBAL CONFIGURATION
50
+ # ============================================================================
51
+
52
+ _config: Config | None = None
53
+ _defaults_dir: Path = Path(__file__).parent.parent / "defaults"
54
+ _default_config_path: Path = _defaults_dir / "default_config.yml"
55
+ _built_in_unified_theme_dir: Path = _defaults_dir / "themes"
56
+ _palettes_dir: Path = _defaults_dir / "palettes"
57
+
58
+ # Per-module default YAML files colocated with their consumers.
59
+ # Each file contributes one or more top-level config namespaces.
60
+ # Add a path here when a new module ships its own defaults.
61
+ _core_dir: Path = Path(__file__).parent.parent
62
+ _MODULE_DEFAULT_PATHS: list[Path] = [
63
+ _core_dir / "inspect" / "defaults.yml",
64
+ _core_dir / "render" / "geo_defaults.yml",
65
+ _core_dir / "render" / "markdown_defaults.yml",
66
+ _core_dir / "render" / "terminal_defaults.yml",
67
+ ]
68
+
69
+ # Theme caches (avoid repeated resolution of inheritance)
70
+ _theme_cache: dict[str, ThemeConfig] = {}
71
+ _compiled_theme_cache: dict[str, Any] = {} # str -> Style
72
+
73
+ #: Name of the built-in default theme shipped with Dataface.
74
+ DEFAULT_THEME_NAME: str = "editorial"
75
+
76
+
77
+ def _load_yaml_data(path: Path) -> Any:
78
+ """Load YAML as plain Python containers."""
79
+ with path.open("r", encoding="utf-8") as fh:
80
+ result = yaml.safe_load(fh)
81
+ return {} if result is None else result
82
+
83
+
84
+ def _as_mapping(value: Any, context: str) -> dict[str, Any]:
85
+ """Normalize mapping-like content into a plain dict."""
86
+ if is_mapping_like(value):
87
+ return as_plain_mapping(value)
88
+ raise TypeError(f"{context} must decode to a mapping")
89
+
90
+
91
+ def get_config() -> Config:
92
+ """Get the current configuration.
93
+
94
+ Loads the defaults stack and built-in chart themes on first call.
95
+ All config is accessed via dot notation:
96
+ config.style.board.width
97
+ config.style.layout.rows.gap
98
+ config.style.charts.bar.padding
99
+
100
+ Returns:
101
+ Config with typed configuration values
102
+ """
103
+ global _config
104
+
105
+ if _config is None:
106
+ merged = _load_base_config()
107
+ merged = _resolve_style_extends(merged)
108
+ _config = Config.model_validate(merged)
109
+
110
+ return _config
111
+
112
+
113
+ def reset_config() -> None:
114
+ """Reset configuration to reload from YAML."""
115
+ global _config, _theme_cache, _compiled_theme_cache
116
+ from dataface.core.compile.models.style.merged import clear_resolve_style_cache
117
+
118
+ _config = None
119
+ _theme_cache = {}
120
+ _compiled_theme_cache = {}
121
+ clear_resolve_style_cache()
122
+
123
+
124
+ def get_chart_rendering() -> ConfigNode:
125
+ """Narrow getter — engine constants for chart layout and formatting."""
126
+ return get_config().chart_rendering
127
+
128
+
129
+ def get_markdown_config() -> MarkdownConfig:
130
+ """Narrow getter — markdown rendering color config."""
131
+ return get_config().markdown
132
+
133
+
134
+ def get_terminal_config() -> ConfigNode:
135
+ """Narrow getter — terminal rendering defaults."""
136
+ return get_config().terminal
137
+
138
+
139
+ def get_rendering_config() -> RenderingConfig:
140
+ """Narrow getter — export/output rendering settings."""
141
+ return get_config().rendering
142
+
143
+
144
+ def get_vega_config() -> VegaRuntimeConfig:
145
+ """Narrow getter — Vega-Lite runtime config."""
146
+ return get_config().vega
147
+
148
+
149
+ def load_config(path: Path) -> Config:
150
+ """Load configuration from a custom YAML file.
151
+
152
+ Merges with defaults - file values override defaults.
153
+
154
+ Args:
155
+ path: Path to configuration file
156
+
157
+ Returns:
158
+ Config with merged configuration
159
+ """
160
+ global _config, _theme_cache, _compiled_theme_cache
161
+ from dataface.core.compile.models.style.merged import clear_resolve_style_cache
162
+
163
+ # Start with the assembled defaults stack
164
+ config = _load_base_config()
165
+
166
+ # Merge with custom file
167
+ if path.exists():
168
+ custom_data = _load_yaml_data(path)
169
+ custom = ConfigPatch.model_validate(custom_data)
170
+ config = _deep_merge(
171
+ config,
172
+ custom.to_plain_dict(exclude_none=True),
173
+ )
174
+
175
+ config = _resolve_style_extends(config)
176
+ compiled = Config.model_validate(config)
177
+
178
+ _config = compiled
179
+ _theme_cache = {}
180
+ _compiled_theme_cache = {}
181
+ clear_resolve_style_cache()
182
+
183
+ return compiled
184
+
185
+
186
+ def _load_base_config() -> dict[str, Any]:
187
+ """Load and merge built-in default config files and themes."""
188
+ raw_default = _load_yaml_data(_default_config_path)
189
+ # Extract style before ConfigPatch validation — the style section contains
190
+ # {extends: "..."} which is not a valid Style. We restore it below
191
+ # and resolve it to a Style just before Config.model_validate.
192
+ raw_style = raw_default.pop("style", {}) if isinstance(raw_default, dict) else {}
193
+
194
+ config = ConfigPatch.model_validate(raw_default)
195
+ # ConfigPatch uses None as "not authored in this overlay". Preserve only
196
+ # explicitly authored keys when projecting patch models back to dicts.
197
+ config_data = config.to_plain_dict(exclude_none=True)
198
+ if raw_style:
199
+ config_data["style"] = raw_style
200
+
201
+ for module_path in _MODULE_DEFAULT_PATHS:
202
+ config_data = _deep_merge(config_data, _load_yaml_data(module_path))
203
+
204
+ # Load palette data from defaults/palettes/ into the config tree.
205
+ config_data = _deep_merge(config_data, _load_palettes())
206
+
207
+ return config_data
208
+
209
+
210
+ _SCAFFOLD_META_KEYS: frozenset[str] = frozenset({"name", "family", "description"})
211
+
212
+
213
+ def _load_palettes() -> dict[str, Any]:
214
+ """Load palette YAMLs from defaults/palettes/ into the config tree.
215
+
216
+ Returns a dict with:
217
+ - ``palettes``: categorical palettes keyed by name (category-10, hero-6, …)
218
+ from ``defaults/palettes/categorical/*.yml``.
219
+ - ``dft_grays`` / ``dft_creams``: flat hex mappings from
220
+ ``defaults/palettes/scaffold/dft-grays.yml`` and ``dft-creams.yml``.
221
+
222
+ Sequential, diverging, and tone palettes are accessed via
223
+ ``dataface.core.compile.palette`` — not the config tree.
224
+
225
+ Shipped YAMLs are expected to be well-formed. Any malformed file raises —
226
+ no silent skip.
227
+ """
228
+ overlay: dict[str, Any] = {}
229
+ cat_dir = _palettes_dir / "categorical"
230
+ if cat_dir.is_dir():
231
+ palettes: dict[str, list[str]] = {}
232
+ for path in sorted(cat_dir.glob("*.yml")):
233
+ data = _load_yaml_data(path)
234
+ name = data.get("name") if isinstance(data, Mapping) else None
235
+ # Categorical files use colors: (renamed from stops:).
236
+ colors = data.get("colors") if isinstance(data, Mapping) else None
237
+ if not isinstance(name, str) or not isinstance(colors, list):
238
+ raise ValueError(
239
+ f"{path}: categorical palette YAML must have string 'name' "
240
+ f"and list 'colors' keys"
241
+ )
242
+ palettes[name] = list(colors)
243
+ if palettes:
244
+ overlay["palettes"] = palettes
245
+
246
+ for legacy_attr, palette_name in (
247
+ ("dft_grays", "dft-grays"),
248
+ ("dft_creams", "dft-creams"),
249
+ ):
250
+ # Use _load_spine (which resolves extends: inheritance) so that child
251
+ # palettes like dft-creams inherit the full alias graph from their parent.
252
+ from dataface.core.compile.palette import _load_spine, resolve_alias_chain
253
+
254
+ spine = _load_spine(palette_name)
255
+ if spine.colors is None or spine.aliases is None:
256
+ raise ValueError(
257
+ f"scaffold palette '{palette_name}' must have 'colors:' list and "
258
+ f"'aliases:' mapping (unified palette shape)"
259
+ )
260
+ # Flatten: resolve every alias to a terminal hex for legacy consumers.
261
+ flat: dict[str, str] = {
262
+ alias_key: resolve_alias_chain(
263
+ alias_key, dict(spine.aliases), colors=list(spine.colors)
264
+ )
265
+ for alias_key in spine.aliases
266
+ }
267
+ if not flat:
268
+ raise ValueError(
269
+ f"scaffold palette '{palette_name}' has no aliases — check palette YAML shape"
270
+ )
271
+ overlay[legacy_attr] = flat
272
+
273
+ return overlay
274
+
275
+
276
+ def _deep_merge(base: Mapping[str, Any], overlay: Mapping[str, Any]) -> dict[str, Any]:
277
+ """Recursively merge overlay into base.
278
+
279
+ Rebuild only the branches touched by the overlay. Overlay leaves are
280
+ deep-copied so later mutation cannot bleed back into reusable defaults.
281
+ """
282
+ result = dict(base)
283
+ for key, value in overlay.items():
284
+ if (
285
+ key in result
286
+ and isinstance(result[key], Mapping)
287
+ and isinstance(value, Mapping)
288
+ ):
289
+ result[key] = _deep_merge(result[key], value)
290
+ else:
291
+ result[key] = deepcopy(value)
292
+ return result
293
+
294
+
295
+ def _load_built_in_unified_themes() -> dict[str, dict[str, Any]]:
296
+ """Load unified theme files from themes/ (.yaml extension).
297
+
298
+ These are pure Dataface Style-format files. Uses .yaml extension
299
+ to distinguish from the now-deleted chart_themes/*.yml legacy format.
300
+ """
301
+ themes: dict[str, dict[str, Any]] = {}
302
+ if not _built_in_unified_theme_dir.exists():
303
+ return themes
304
+
305
+ for path in sorted(_built_in_unified_theme_dir.glob("*.yaml")):
306
+ raw = _as_mapping(_load_yaml_data(path), f"Unified theme file {path.name}")
307
+ # Unified theme files have the style dict wrapped under 'style:'.
308
+ # Keep the full structure (extends + style) for _resolve_named_config to handle.
309
+ themes[path.stem] = raw
310
+
311
+ return themes
312
+
313
+
314
+ @cache
315
+ def list_built_in_themes() -> list[str]:
316
+ """Return sorted stem names of all built-in unified themes."""
317
+ return sorted(p.stem for p in _built_in_unified_theme_dir.glob("*.yaml"))
318
+
319
+
320
+ def get_compiled_theme(theme_name: str | None) -> Any: # -> Style
321
+ """Load a unified theme as a Style, resolving extends: inheritance.
322
+
323
+ Unified themes live in themes/*.yaml. Each file has:
324
+ extends: <parent-name> (optional)
325
+ style:
326
+ background: ...
327
+ charts: ...
328
+
329
+ The style dicts from the inheritance chain are deep-merged (child wins).
330
+ The merged dict is validated via Style.model_validate().
331
+
332
+ Returns Style with all defaults filled in.
333
+ """
334
+ from dataface.core.compile.models.style.compiled import Style
335
+
336
+ name = theme_name or DEFAULT_THEME_NAME
337
+
338
+ if name in _compiled_theme_cache:
339
+ return _compiled_theme_cache[name]
340
+
341
+ if not _built_in_unified_theme_dir.exists():
342
+ raise FileNotFoundError(
343
+ f"Unified themes directory not found: {_built_in_unified_theme_dir}."
344
+ )
345
+
346
+ unified_themes = _load_built_in_unified_themes()
347
+ if not unified_themes:
348
+ raise FileNotFoundError(
349
+ f"No unified theme files (*.yaml) found in {_built_in_unified_theme_dir}."
350
+ )
351
+
352
+ # Build a themes dict that maps name → style dict (without the 'style:' wrapper)
353
+ # for _resolve_named_config to merge. We expose 'extends' at top level and
354
+ # the style keys merged in at the same level so the resolver sees them.
355
+ flat_themes: dict[str, dict[str, Any]] = {}
356
+ for tname, raw in unified_themes.items():
357
+ entry: dict[str, Any] = {}
358
+ if "extends" in raw:
359
+ entry["extends"] = raw["extends"]
360
+ style_data = raw.get("style", {})
361
+ if style_data:
362
+ entry.update(style_data)
363
+ flat_themes[tname] = entry
364
+
365
+ if name not in flat_themes:
366
+ raise ValueError(
367
+ f"Theme '{name}' not found in {_built_in_unified_theme_dir}. "
368
+ f"Available themes: {sorted(flat_themes)}"
369
+ )
370
+
371
+ from dataface.core.compile.models.style.merged import (
372
+ _resolve_color_tokens,
373
+ _resolve_self_tokens,
374
+ )
375
+
376
+ resolved = _resolve_named_config(name, flat_themes, set())
377
+ compiled = Style.model_validate(resolved)
378
+ # Substitute theme-self tokens (e.g. ``theme.background`` for canvas-coupled
379
+ # fields like ``arc.stroke``) BEFORE color-token resolution so the
380
+ # substituted palette token is itself resolved by the next pass.
381
+ compiled = _resolve_self_tokens(compiled)
382
+ # Themes carry palette tokens (e.g. "dft-grays.gray-30") for human readability.
383
+ # Resolve them to concrete hex once, at theme-load time, so every consumer of
384
+ # Style sees concrete colors.
385
+ compiled = _resolve_color_tokens(compiled)
386
+ _compiled_theme_cache[name] = compiled
387
+ return compiled
388
+
389
+
390
+ def _resolve_style_extends(config: dict[str, Any]) -> dict[str, Any]:
391
+ """Resolve style.extends into a Style for Config.style.
392
+
393
+ Called after preset application, just before Config.model_validate.
394
+ All visual defaults live in themes/*.yaml; style.extends names the base theme.
395
+ """
396
+ style_section = config.get("style", {})
397
+ if isinstance(style_section, dict) and "extends" in style_section:
398
+ config = dict(config)
399
+ config["style"] = get_compiled_theme(style_section["extends"])
400
+ return config
401
+
402
+
403
+ # ============================================================================
404
+ # THEME ACCESS
405
+ # ============================================================================
406
+
407
+
408
+ def get_theme(theme_name: str | None) -> ThemeConfig:
409
+ """Get a typed theme config by name, resolving inheritance.
410
+
411
+ Themes can inherit via 'extends':
412
+ my-theme:
413
+ extends: default
414
+ charts:
415
+ table:
416
+ header_background: "#f0f0f0"
417
+
418
+ Args:
419
+ theme_name: Name of theme, or None for empty theme
420
+
421
+ Returns:
422
+ ThemeConfig with typed access to theme values
423
+ """
424
+ if not theme_name:
425
+ return ThemeConfig()
426
+
427
+ if theme_name in _theme_cache:
428
+ return _theme_cache[theme_name]
429
+
430
+ config = get_config()
431
+ resolved_dict = _resolve_named_config(
432
+ theme_name, _as_mapping(config.themes, "themes"), set()
433
+ )
434
+ resolved = ThemeConfig.model_validate(normalize_raw_theme_config(resolved_dict))
435
+ _theme_cache[theme_name] = resolved
436
+ return resolved
437
+
438
+
439
+ def get_theme_dict(theme_name: str | None) -> dict[str, Any]:
440
+ """Get a theme as raw dict for passing to Vega-Lite.
441
+
442
+ Use this when you need the raw dict to merge into a Vega-Lite spec.
443
+ For typed access to theme values, use get_theme() instead.
444
+
445
+ Args:
446
+ theme_name: Name of theme, or None for empty dict
447
+
448
+ Returns:
449
+ Raw theme dictionary suitable for Vega-Lite config
450
+ """
451
+ if not theme_name:
452
+ return {}
453
+ return get_theme(theme_name).to_vega_config()
454
+
455
+
456
+ def _resolve_named_config(
457
+ config_name: str, configs: Mapping[str, Any], seen: set[str]
458
+ ) -> dict[str, Any]:
459
+ """Resolve a named theme/preset config, handling inheritance via extends."""
460
+ if config_name in seen:
461
+ raise ValueError(
462
+ f"Circular config inheritance: {' -> '.join(seen)} -> {config_name}"
463
+ )
464
+
465
+ config = configs.get(config_name)
466
+ if not config:
467
+ return {}
468
+ if not is_mapping_like(config):
469
+ raise TypeError(f"Config '{config_name}' must be a mapping")
470
+
471
+ config_data = _as_mapping(config, config_name)
472
+
473
+ extends = config_data.get("extends")
474
+ if not extends:
475
+ return {k: v for k, v in config_data.items() if k != "extends"}
476
+
477
+ seen_with_current = seen | {config_name}
478
+ parent = _resolve_named_config(extends, configs, seen_with_current)
479
+ child = {k: v for k, v in config_data.items() if k != "extends"}
480
+ return _deep_merge(parent, child)
481
+
482
+
483
+ # ============================================================================
484
+ # HELPER FUNCTIONS
485
+ # ============================================================================
486
+
487
+
488
+ def get_default_theme_name() -> str:
489
+ """Return the designated default chart theme name."""
490
+ config = get_config()
491
+ return config.vega.default_theme
492
+
493
+
494
+ def set_default_theme_name(theme_name: str) -> None:
495
+ """Override the in-process default theme name.
496
+
497
+ Validates the theme exists before patching. Raises ValueError on unknown names.
498
+ Called at serve startup by apply_default_theme_from_env(); not for general use.
499
+
500
+ Args:
501
+ theme_name: A built-in theme stem (e.g. "carbong100", "light").
502
+
503
+ Raises:
504
+ ValueError: If theme_name is not a built-in theme.
505
+ """
506
+ available = list_built_in_themes()
507
+ if theme_name not in available:
508
+ raise ValueError(
509
+ f"Theme '{theme_name}' not found. Available built-in themes: {available}"
510
+ )
511
+ # Force initialization so the patch lands on the live singleton.
512
+ config = get_config()
513
+ config.vega.default_theme = theme_name
514
+
515
+
516
+ def get_palette(name: str = "default") -> list[str]:
517
+ """Get a named color palette."""
518
+ config = get_config()
519
+ palettes = config.palettes
520
+ palette_name = "category-10" if name == "default" else name
521
+ if palette_name not in palettes:
522
+ raise KeyError(f"Unknown palette '{palette_name}'")
523
+ palette = palettes[palette_name]
524
+ if not isinstance(palette, list):
525
+ raise TypeError(f"Palette '{palette_name}' must be a list")
526
+ return list(palette)
527
+
528
+
529
+ # ============================================================================
530
+ # PROJECT SOURCES
531
+ # ============================================================================
532
+
533
+ _project_sources: dict[Path, ProjectSourcesConfig] = {}
534
+
535
+
536
+ def get_project_sources(project_dir: Path | None = None) -> ProjectSourcesConfig:
537
+ """Get project-level sources configuration.
538
+
539
+ Loads from:
540
+ 1. Config sources section
541
+ 2. Project dataface.yml sources section
542
+ 3. _sources.yaml file (merged, takes precedence)
543
+
544
+ Args:
545
+ project_dir: Project directory (default: cwd)
546
+
547
+ Returns:
548
+ ProjectSourcesConfig with 'default' and 'sources' attributes
549
+ """
550
+ global _project_sources
551
+
552
+ project_dir = (project_dir or Path.cwd()).resolve()
553
+
554
+ cached_sources = _project_sources.get(project_dir)
555
+ if cached_sources is not None:
556
+ return cached_sources
557
+
558
+ config = get_config()
559
+
560
+ # Start with config sources
561
+ default_source = None
562
+ all_sources: dict[str, dict[str, Any]] = {}
563
+
564
+ if config.sources:
565
+ sources_data = _as_mapping(config.sources, "config.sources")
566
+ default_source = sources_data.pop("default", None)
567
+ all_sources.update(
568
+ {k: v for k, v in sources_data.items() if isinstance(v, dict)}
569
+ )
570
+
571
+ # Project dataface.yml overrides global defaults.
572
+ project_config_file = None
573
+ for candidate in (project_dir / "dataface.yml", project_dir / "dataface.yaml"):
574
+ if candidate.exists():
575
+ project_config_file = candidate
576
+ break
577
+
578
+ if project_config_file is not None:
579
+ file_data = _as_mapping(
580
+ _load_yaml_data(project_config_file), project_config_file.name
581
+ )
582
+ if "sources" in file_data:
583
+ sources_section = _as_mapping(
584
+ file_data["sources"], f"{project_config_file.name} sources section"
585
+ )
586
+
587
+ if "default" in sources_section:
588
+ default_source = sources_section.pop("default")
589
+
590
+ all_sources.update(
591
+ {k: v for k, v in sources_section.items() if isinstance(v, dict)}
592
+ )
593
+
594
+ # Load _sources.yaml if it exists
595
+ sources_file = project_dir / "_sources.yaml"
596
+ if not sources_file.exists():
597
+ sources_file = project_dir / "_sources.yml"
598
+
599
+ if sources_file.exists():
600
+ file_data = _as_mapping(_load_yaml_data(sources_file), sources_file.name)
601
+ # Handle structure where sources are nested under 'sources' key
602
+ if "sources" in file_data:
603
+ sources_section = _as_mapping(
604
+ file_data["sources"], f"{sources_file.name} sources section"
605
+ )
606
+ else:
607
+ sources_section = _as_mapping(file_data, sources_file.name)
608
+
609
+ # Extract default
610
+ if "default" in sources_section:
611
+ default_source = sources_section.pop("default")
612
+
613
+ # Merge source definitions
614
+ all_sources.update(
615
+ {k: dict(v) for k, v in sources_section.items() if isinstance(v, dict)}
616
+ )
617
+
618
+ project_sources = ProjectSourcesConfig(
619
+ default=default_source,
620
+ sources=_normalize_project_sources(all_sources),
621
+ )
622
+ _project_sources[project_dir] = project_sources
623
+ return project_sources
624
+
625
+
626
+ def reset_project_sources() -> None:
627
+ """Reset project sources cache."""
628
+ global _project_sources
629
+ _project_sources = {}
630
+ reset_project_warnings_ignore()
631
+
632
+
633
+ def invalidate_project_sources(project_dir: Path | None) -> None:
634
+ """Clear cached project sources for a single project directory."""
635
+ if project_dir is None:
636
+ return
637
+
638
+ _project_sources.pop(project_dir.resolve(), None)
639
+ invalidate_project_warnings_ignore(project_dir)
640
+
641
+
642
+ # ============================================================================
643
+ # PROJECT WARNINGS IGNORE
644
+ # ============================================================================
645
+
646
+ _project_warnings_ignore: dict[Path, frozenset[str]] = {}
647
+
648
+
649
+ def get_project_warnings_ignore(project_dir: Path | None = None) -> frozenset[str]:
650
+ """Return the set of warning codes to suppress project-wide.
651
+
652
+ Reads `warnings.ignore` from `dataface.yml` (or `dataface.yaml`) in
653
+ `project_dir`. Returns an empty frozenset when no config file exists or
654
+ the key is absent.
655
+
656
+ Args:
657
+ project_dir: Project directory. Defaults to cwd when None.
658
+
659
+ Returns:
660
+ Frozenset of warning code strings.
661
+ """
662
+ global _project_warnings_ignore
663
+
664
+ resolved_dir = (project_dir or Path.cwd()).resolve()
665
+ cached = _project_warnings_ignore.get(resolved_dir)
666
+ if cached is not None:
667
+ return cached
668
+
669
+ for candidate in (resolved_dir / "dataface.yml", resolved_dir / "dataface.yaml"):
670
+ if candidate.exists():
671
+ file_data = _as_mapping(_load_yaml_data(candidate), candidate.name)
672
+ warnings_section = file_data.get("warnings")
673
+ if warnings_section is not None:
674
+ warnings_map = _as_mapping(
675
+ warnings_section, f"{candidate.name} warnings section"
676
+ )
677
+ raw_ignore = warnings_map.get("ignore")
678
+ if raw_ignore is not None:
679
+ if not isinstance(raw_ignore, list):
680
+ raise TypeError(
681
+ f"{candidate.name}: warnings.ignore must be a list of strings"
682
+ )
683
+ for entry in raw_ignore:
684
+ if not isinstance(entry, str):
685
+ raise TypeError(
686
+ f"{candidate.name}: warnings.ignore entries must be "
687
+ f"strings, got {type(entry).__name__}: {entry!r}"
688
+ )
689
+ codes = frozenset(raw_ignore)
690
+ _project_warnings_ignore[resolved_dir] = codes
691
+ return codes
692
+ break
693
+
694
+ empty: frozenset[str] = frozenset()
695
+ _project_warnings_ignore[resolved_dir] = empty
696
+ return empty
697
+
698
+
699
+ def reset_project_warnings_ignore() -> None:
700
+ """Reset project warnings ignore cache (for tests)."""
701
+ global _project_warnings_ignore
702
+ _project_warnings_ignore = {}
703
+
704
+
705
+ def invalidate_project_warnings_ignore(project_dir: Path | None) -> None:
706
+ """Clear cached warnings ignore for a single project directory."""
707
+ if project_dir is None:
708
+ return
709
+ _project_warnings_ignore.pop(project_dir.resolve(), None)
710
+
711
+
712
+ # ============================================================================
713
+ # META CONFIG CLASSES
714
+ # ============================================================================
715
+ # These are used by meta.py for handling meta.yaml files
716
+
717
+
718
+ class AccessConfig:
719
+ """Access control configuration for meta.yaml."""
720
+
721
+ def __init__(
722
+ self,
723
+ rules: list[dict[str, Any]] | None = None,
724
+ ignore_meta: bool = False,
725
+ ):
726
+ self.rules = rules or []
727
+ self.ignore_meta = ignore_meta
728
+
729
+
730
+ class MetaConfig:
731
+ """Configuration from meta.yaml file."""
732
+
733
+ def __init__(
734
+ self,
735
+ path: Path | None = None,
736
+ ignore_meta: bool = False,
737
+ access: AccessConfig | None = None,
738
+ source: str | None = None,
739
+ queries: dict[str, Any] | None = None,
740
+ charts: dict[str, Any] | None = None,
741
+ face: dict[str, Any] | None = None,
742
+ style: dict[str, Any] | None = None,
743
+ typography: dict[str, Any] | None = None,
744
+ lint_ignore: list[str] | None = None,
745
+ lint_ignore_queries: dict[str, list[str]] | None = None,
746
+ ):
747
+ self.path = path
748
+ self.ignore_meta = ignore_meta
749
+ self.access = access
750
+ self.source = source
751
+ self.queries = queries or {}
752
+ self.charts = charts or {}
753
+ self.face = face or {}
754
+ self.style = style or {}
755
+ self.typography = typography or {}
756
+ self.lint_ignore: list[str] = lint_ignore or []
757
+ self.lint_ignore_queries: dict[str, list[str]] = lint_ignore_queries or {}
758
+
759
+ @classmethod
760
+ def from_dict(cls, data: dict[str, Any], path: Path | None = None) -> MetaConfig:
761
+ """Create MetaConfig from dictionary."""
762
+ access_data = data.get("access")
763
+ access_config = None
764
+ if access_data:
765
+ if isinstance(access_data, dict):
766
+ access_config = AccessConfig(
767
+ rules=access_data.get("rules", []),
768
+ ignore_meta=access_data.get("ignore_meta", False),
769
+ )
770
+ elif isinstance(access_data, list):
771
+ access_config = AccessConfig(rules=access_data)
772
+
773
+ lint_data = data.get("lint", {})
774
+ if lint_data and not isinstance(lint_data, dict):
775
+ raise ValueError(
776
+ f"meta.yaml 'lint' must be a mapping, got {type(lint_data).__name__}"
777
+ )
778
+
779
+ return cls(
780
+ path=path,
781
+ ignore_meta=data.get("ignore_meta", False),
782
+ access=access_config,
783
+ source=data.get("source"),
784
+ queries=data.get("queries", {}),
785
+ charts=data.get("charts", {}),
786
+ face=data.get("face", {}),
787
+ style=data.get("style", {}),
788
+ typography=data.get("typography", {}),
789
+ lint_ignore=lint_data.get("ignore", []),
790
+ lint_ignore_queries=lint_data.get("ignore_queries", {}),
791
+ )
792
+
793
+ def is_empty(self) -> bool:
794
+ """Check if this meta config has no actual configuration."""
795
+ return (
796
+ not self.access
797
+ and not self.source
798
+ and not self.queries
799
+ and not self.charts
800
+ and not self.face
801
+ and not self.style
802
+ and not self.typography
803
+ and not self.lint_ignore
804
+ and not self.lint_ignore_queries
805
+ )
806
+
807
+
808
+ class ProjectSourcesConfig:
809
+ """Project-level sources configuration."""
810
+
811
+ def __init__(
812
+ self,
813
+ default: str | None = None,
814
+ sources: dict[str, dict[str, Any]] | None = None,
815
+ ):
816
+ self.default = default
817
+ self.sources = sources or {}
818
+
819
+
820
+ def _normalize_project_sources(
821
+ sources: dict[str, dict[str, Any]],
822
+ ) -> dict[str, dict[str, Any]]:
823
+ return {name: resolve_env_vars_in_dict(config) for name, config in sources.items()}