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,263 @@
1
+ # String Column Profile Template
2
+ #
3
+ # Analysis of a string/text column. Schema panel pulls from the
4
+ # LayeredSchemaResolver; live aggregations stay on warehouse SQL.
5
+ #
6
+ # URL: /inspect/string_column/?model=<table>&column=<col>
7
+ #
8
+ # Required variables:
9
+ # - model: Table/model name
10
+ # - column: Column name to analyze
11
+ #
12
+ # Optional variables:
13
+ # - connection: DuckDB file path or :memory: (default: :memory:)
14
+ #
15
+ # Theme is set via DFT_DEFAULT_THEME env var at serve startup.
16
+
17
+ title: "String Column: {{ model }}.{{ column }}"
18
+
19
+ text: |
20
+ [Back to {{ model }}](/inspect/model/?model={{ model }}&connection={{ connection }})
21
+
22
+ variables:
23
+ model:
24
+ input: text
25
+ visible: false
26
+ default: ""
27
+ column:
28
+ input: text
29
+ visible: false
30
+ default: ""
31
+ connection:
32
+ input: text
33
+ visible: false
34
+ default: ":memory:"
35
+ source_name:
36
+ input: text
37
+ visible: false
38
+ default: ""
39
+ schema_name:
40
+ input: text
41
+ visible: false
42
+ default: ""
43
+
44
+ queries:
45
+ column_schema:
46
+ type: schema_resolver
47
+ source: "{{ source_name }}"
48
+ schema: "{{ schema_name }}"
49
+ table: "{{ model }}"
50
+ column: "{{ column }}"
51
+
52
+ # Basic string stats
53
+ stats:
54
+ source:
55
+ type: duckdb
56
+ path: "{{ connection }}"
57
+ sql: |
58
+ SELECT
59
+ COUNT(*) as total_count,
60
+ COUNT({{ column }}) as non_null_count,
61
+ COUNT(*) - COUNT({{ column }}) as null_count,
62
+ ROUND(100.0 * (COUNT(*) - COUNT({{ column }})) / COUNT(*), 2) as null_pct,
63
+ COUNT(DISTINCT {{ column }}) as distinct_count,
64
+ COUNT(CASE WHEN TRIM({{ column }}) = '' THEN 1 END) as empty_count,
65
+ MIN(LENGTH({{ column }})) as min_length,
66
+ MAX(LENGTH({{ column }})) as max_length,
67
+ ROUND(AVG(LENGTH({{ column }})), 1) as avg_length
68
+ FROM {{ model }}
69
+
70
+ # Top values by frequency
71
+ top_values:
72
+ source:
73
+ type: duckdb
74
+ path: "{{ connection }}"
75
+ sql: |
76
+ SELECT
77
+ COALESCE({{ column }}, '(null)') as value,
78
+ COUNT(*) as count,
79
+ ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM {{ model }}), 2) as pct
80
+ FROM {{ model }}
81
+ GROUP BY {{ column }}
82
+ ORDER BY COUNT(*) DESC
83
+ LIMIT 20
84
+
85
+ # Length distribution
86
+ length_distribution:
87
+ source:
88
+ type: duckdb
89
+ path: "{{ connection }}"
90
+ sql: |
91
+ SELECT
92
+ LENGTH({{ column }}) as length,
93
+ COUNT(*) as count
94
+ FROM {{ model }}
95
+ WHERE {{ column }} IS NOT NULL
96
+ GROUP BY LENGTH({{ column }})
97
+ ORDER BY length
98
+
99
+ # Pattern analysis (first character distribution)
100
+ first_char:
101
+ source:
102
+ type: duckdb
103
+ path: "{{ connection }}"
104
+ sql: |
105
+ SELECT
106
+ UPPER(SUBSTRING({{ column }}, 1, 1)) as first_char,
107
+ COUNT(*) as count
108
+ FROM {{ model }}
109
+ WHERE {{ column }} IS NOT NULL AND LENGTH({{ column }}) > 0
110
+ GROUP BY UPPER(SUBSTRING({{ column }}, 1, 1))
111
+ ORDER BY COUNT(*) DESC
112
+ LIMIT 26
113
+
114
+ # Sample values
115
+ samples:
116
+ source:
117
+ type: duckdb
118
+ path: "{{ connection }}"
119
+ sql: |
120
+ SELECT DISTINCT {{ column }} as value
121
+ FROM {{ model }}
122
+ WHERE {{ column }} IS NOT NULL
123
+ LIMIT 20
124
+
125
+ charts:
126
+ schema_panel:
127
+ title: Schema
128
+ type: table
129
+ query: column_schema
130
+ style:
131
+ header_overflow: wrap-two
132
+ columns:
133
+ name:
134
+ label: Column
135
+ type:
136
+ label: Type
137
+ role:
138
+ label: Role
139
+ semantic_type:
140
+ label: Semantic
141
+ description:
142
+ label: Description
143
+
144
+ # Stats KPIs
145
+ distinct:
146
+ label: Distinct Values
147
+ type: kpi
148
+ query: stats
149
+ value: distinct_count
150
+ format: ",.0f"
151
+
152
+ null_pct:
153
+ label: "Null %"
154
+ type: kpi
155
+ query: stats
156
+ value: null_pct
157
+ format: ".1f"
158
+
159
+ empty_count:
160
+ label: Empty Strings
161
+ type: kpi
162
+ query: stats
163
+ value: empty_count
164
+ format: ",.0f"
165
+
166
+ min_len:
167
+ label: Min Length
168
+ type: kpi
169
+ query: stats
170
+ value: min_length
171
+ format: ",.0f"
172
+
173
+ max_len:
174
+ label: Max Length
175
+ type: kpi
176
+ query: stats
177
+ value: max_length
178
+ format: ",.0f"
179
+
180
+ avg_len:
181
+ label: Avg Length
182
+ type: kpi
183
+ query: stats
184
+ value: avg_length
185
+ format: ".1f"
186
+
187
+ # Spark bar (compact top values for profiler cards)
188
+ top_values_spark:
189
+ title: Top Values
190
+ type: spark_bar
191
+ query: top_values
192
+ x: count
193
+ y: value
194
+
195
+ # Top values chart (full-size)
196
+ top_values_chart:
197
+ title: Top Values (Full)
198
+ type: bar
199
+ query: top_values
200
+ x: value
201
+ y: count
202
+ style:
203
+ orientation: horizontal
204
+
205
+ # Top values table
206
+ top_values_table:
207
+ title: Value Frequency
208
+ type: table
209
+ query: top_values
210
+ style:
211
+ header_overflow: wrap-two
212
+ columns:
213
+ value:
214
+ label: Value
215
+ count:
216
+ label: Count
217
+ format: ",.0f"
218
+ pct:
219
+ label: "%"
220
+ format: ".1f"
221
+
222
+ # Length distribution
223
+ length_chart:
224
+ title: Length Distribution
225
+ type: bar
226
+ query: length_distribution
227
+ x: length
228
+ y: count
229
+ x_label: String Length
230
+ y_label: Count
231
+
232
+ # First character distribution
233
+ first_char_chart:
234
+ title: First Character
235
+ type: bar
236
+ query: first_char
237
+ x: first_char
238
+ y: count
239
+
240
+ # Sample values
241
+ samples_table:
242
+ title: Sample Values
243
+ type: table
244
+ query: samples
245
+ style:
246
+ header_overflow: wrap-two
247
+
248
+ rows:
249
+ - schema_panel
250
+ - cols:
251
+ - distinct
252
+ - null_pct
253
+ - empty_count
254
+ - min_len
255
+ - max_len
256
+ - avg_len
257
+ - cols:
258
+ - top_values_spark
259
+ - top_values_table
260
+ - cols:
261
+ - length_chart
262
+ - first_char_chart
263
+ - samples_table
@@ -0,0 +1,165 @@
1
+ """Shared project/dbt root discovery used by render and MCP commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Marker files that indicate a project root, checked in priority order.
11
+ PROJECT_MARKERS = ("dataface.yml", "dataface.yaml", "dbt_project.yml")
12
+
13
+
14
+ def discovery_boundary_for_face(
15
+ face_parent: Path, project_dir: Path | str | None
16
+ ) -> Path | None:
17
+ """Return the directory ceiling for upward discovery, or None to walk to the filesystem root.
18
+
19
+ When ``--project-dir`` is omitted, callers should pass ``project_dir=None`` so we can walk
20
+ above the face directory (required for ``…/tutorial_dbt/faces/*.yml``).
21
+
22
+ When ``project_dir`` resolves to the same directory as ``face_parent`` (e.g. cwd is the
23
+ faces folder and no ``--project-dir`` was passed, so Python uses ``.``), treat the
24
+ ceiling as unbounded so we still find ``dbt_project.yml`` in a parent folder.
25
+ """
26
+ if project_dir is None:
27
+ return None
28
+ boundary = Path(project_dir).resolve()
29
+ if boundary == face_parent.resolve():
30
+ return None
31
+ return boundary
32
+
33
+
34
+ def discover_render_context(
35
+ start_dir: Path,
36
+ boundary: Path | None,
37
+ ) -> tuple[Path, Path | None]:
38
+ """Infer project and dbt roots for a render request.
39
+
40
+ Walks upward from ``start_dir``. If ``boundary`` is None, walks until the filesystem
41
+ root. If set, stops after processing the ``boundary`` directory (inclusive).
42
+ """
43
+ project_root: Path | None = None
44
+ dbt_project_path: Path | None = None
45
+ start_resolved = start_dir.resolve()
46
+ current = start_resolved
47
+ boundary_resolved = boundary.resolve() if boundary is not None else None
48
+
49
+ while True:
50
+ if dbt_project_path is None and (current / "dbt_project.yml").exists():
51
+ dbt_project_path = current
52
+
53
+ if project_root is None and (
54
+ (current / "dataface.yml").exists()
55
+ or (current / "dataface.yaml").exists()
56
+ or (current / "_sources.yaml").exists()
57
+ or (current / "_sources.yml").exists()
58
+ ):
59
+ project_root = current
60
+
61
+ if current.parent == current:
62
+ break
63
+ if boundary_resolved is not None and current == boundary_resolved:
64
+ break
65
+
66
+ current = current.parent
67
+
68
+ if project_root is None:
69
+ project_root = dbt_project_path or boundary_resolved or start_resolved
70
+
71
+ return project_root, dbt_project_path
72
+
73
+
74
+ def discover_project_root(start_dir: Path | None = None) -> Path:
75
+ """Walk up from *start_dir* to find the project root for ``dft serve``.
76
+
77
+ Looks for ``dataface.yml``, ``dataface.yaml``, or ``dbt_project.yml``.
78
+ Returns the directory containing the first match, or *start_dir* if no
79
+ marker is found.
80
+ """
81
+ start = (start_dir or Path.cwd()).resolve()
82
+ return find_dataface_project(start) or start
83
+
84
+
85
+ def find_dataface_project(start_dir: Path) -> Path | None:
86
+ """Walk up from *start_dir* looking for a Dataface or dbt project marker.
87
+
88
+ Returns the first directory containing ``dataface.yml``, ``dataface.yaml``,
89
+ or ``dbt_project.yml``. Returns ``None`` if no marker is found before the
90
+ filesystem root.
91
+
92
+ Distinct from :func:`discover_project_root`, which falls back to ``start_dir``
93
+ when nothing is found — that fallback is right for render context but
94
+ wrong for MCP project targeting, where the caller must distinguish
95
+ "found a project" from "user is in the wrong place."
96
+ """
97
+ current = start_dir.resolve()
98
+ while True:
99
+ for marker in PROJECT_MARKERS:
100
+ if (current / marker).exists():
101
+ return current
102
+ if current.parent == current:
103
+ return None
104
+ current = current.parent
105
+
106
+
107
+ def find_git_root(start: Path) -> Path | None:
108
+ """Walk up from start looking for a .git directory.
109
+
110
+ Returns the directory containing .git, or None if not found before the
111
+ filesystem root.
112
+ """
113
+ current = start.resolve()
114
+ while True:
115
+ if (current / ".git").exists():
116
+ return current
117
+ parent = current.parent
118
+ if parent == current:
119
+ return None
120
+ current = parent
121
+
122
+
123
+ def infer_dialect_from_dbt(
124
+ project_dir: Path,
125
+ target_name: str | None = None,
126
+ ) -> str | None:
127
+ """Read the resolved dbt profile target and return its adapter ``type``.
128
+
129
+ Resolution order for profiles.yml:
130
+ 1. ``project_dir/profiles.yml``
131
+ 2. ``~/.dbt/profiles.yml``
132
+
133
+ Returns ``None`` when the dbt project or profile cannot be resolved.
134
+ """
135
+ import yaml
136
+
137
+ dbt_project_path = project_dir / "dbt_project.yml"
138
+ if not dbt_project_path.exists():
139
+ return None
140
+
141
+ try:
142
+ dbt_config = yaml.safe_load(dbt_project_path.read_text()) or {}
143
+ except Exception: # noqa: BLE001
144
+ return None
145
+
146
+ profile_name = dbt_config.get("profile")
147
+ if not profile_name:
148
+ return None
149
+
150
+ # Locate profiles.yml
151
+ profiles_path = project_dir / "profiles.yml"
152
+ if not profiles_path.exists():
153
+ profiles_path = Path.home() / ".dbt" / "profiles.yml"
154
+ if not profiles_path.exists():
155
+ return None
156
+
157
+ try:
158
+ profiles = yaml.safe_load(profiles_path.read_text()) or {}
159
+ except Exception: # noqa: BLE001
160
+ return None
161
+
162
+ profile = profiles.get(profile_name, {})
163
+ target = target_name or profile.get("target", "dev")
164
+ target_config = profile.get("outputs", {}).get(target, {})
165
+ return target_config.get("type")
@@ -0,0 +1,87 @@
1
+ """Rendering stage: Face + data → output.
2
+
3
+ Stage: RENDER
4
+ Purpose: Transform compiled datafaces into visual output formats.
5
+
6
+ This module provides the rendering pipeline that takes a Face
7
+ and data (from the execute stage) and produces output in various formats
8
+ (SVG, HTML, PNG, PDF, Terminal).
9
+
10
+ Main Entry Points:
11
+ render(): Render a full dataface
12
+
13
+ The render stage:
14
+ 1. Takes a Face (from compile stage)
15
+ 2. Uses Executor to fetch data lazily as needed
16
+ 3. Generates Vega-Lite specs for charts
17
+ 4. Produces output in requested format
18
+
19
+ Supported Formats:
20
+ - svg: Scalable vector graphics (default)
21
+ - html: Interactive HTML pages with embedded charts
22
+ - png: Raster image format
23
+ - pdf: PDF documents
24
+ - terminal: Terminal output with ASCII/Unicode charts
25
+ """
26
+
27
+ from dataface.core.render.chart import (
28
+ generate_vega_lite_spec,
29
+ render_table_svg,
30
+ slug_to_text,
31
+ )
32
+ from dataface.core.render.control_registry import ControlRegistry
33
+ from dataface.core.render.errors import (
34
+ ChartDataError,
35
+ MissingRequiredVariablesError,
36
+ MissingVariable,
37
+ RenderError,
38
+ )
39
+ from dataface.core.render.render_result import RenderResult
40
+ from dataface.core.render.renderer import render
41
+
42
+ # Terminal rendering
43
+ from dataface.core.render.terminal import (
44
+ render_chart_item_terminal,
45
+ render_face_terminal,
46
+ render_layout_item_terminal,
47
+ )
48
+ from dataface.core.render.terminal_charts import (
49
+ render_chart_terminal,
50
+ render_kpi_terminal,
51
+ render_table_terminal,
52
+ )
53
+ from dataface.core.render.variable_controls import (
54
+ generate_svg_variable_icons,
55
+ generate_svg_variable_script,
56
+ render_interactive_variables_svg,
57
+ render_variables_svg,
58
+ )
59
+
60
+ __all__ = [
61
+ # Main functions
62
+ "render",
63
+ "RenderResult",
64
+ # Errors
65
+ "ChartDataError",
66
+ "MissingRequiredVariablesError",
67
+ "MissingVariable",
68
+ "RenderError",
69
+ # Variable controls
70
+ "render_variables_svg",
71
+ "render_interactive_variables_svg",
72
+ "generate_svg_variable_script",
73
+ "generate_svg_variable_icons",
74
+ "ControlRegistry",
75
+ # Vega-Lite
76
+ "generate_vega_lite_spec",
77
+ "render_chart",
78
+ "render_table_svg",
79
+ "slug_to_text",
80
+ # Terminal rendering
81
+ "render_face_terminal",
82
+ "render_layout_item_terminal",
83
+ "render_chart_item_terminal",
84
+ "render_chart_terminal",
85
+ "render_table_terminal",
86
+ "render_kpi_terminal",
87
+ ]
@@ -0,0 +1,176 @@
1
+ """Board link resolution.
2
+
3
+ Rewrites markdown board links at render time so authors can use
4
+ dashboard-root-relative paths (e.g. ``/zendesk/tickets/list?status=open``)
5
+ that work in both ``dft serve`` and Cloud.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from contextvars import ContextVar
12
+ from dataclasses import dataclass
13
+ from posixpath import normpath
14
+
15
+ # Markdown link pattern: [text](url), excluding image syntax ![alt](src)
16
+ _MD_LINK_RE = re.compile(r"(?<!!)\[([^\]]*)\]\(([^)]+)\)")
17
+
18
+ # Suffixes stripped as author sugar
19
+ _STRIP_SUFFIXES = (".md", ".yml", ".yaml")
20
+
21
+ # Schemes that bypass rewriting. cursor://, vscode://, file:// pass through so
22
+ # editor-deeplink URLs (e.g. ``cursor://file/{abs}/face.yaml`` from the compare
23
+ # UI) reach the browser unchanged — both the scheme and the .yaml suffix.
24
+ # "?" passes through so variable-update links (?var=value) reach variables.js
25
+ # unchanged instead of being treated as relative board paths.
26
+ _PASSTHROUGH_PREFIXES = (
27
+ "http://",
28
+ "https://",
29
+ "mailto:",
30
+ "#",
31
+ "?",
32
+ "command:",
33
+ "cursor://",
34
+ "vscode://",
35
+ "file://",
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class LinkContext:
41
+ """Runtime context for resolving board links.
42
+
43
+ Attributes:
44
+ runtime: ``"serve"`` or ``"cloud"``.
45
+ current_board_slug: Author-space slug of the board being rendered
46
+ (e.g. ``"zendesk/tickets/list"``).
47
+ storage_prefix: On-disk directory prefix for serve mode (default ``"faces"``).
48
+ org_slug: Cloud organization slug (Cloud only).
49
+ project_slug: Cloud project slug (Cloud only).
50
+ branch: Current branch name to merge into outbound links (Cloud only).
51
+ """
52
+
53
+ runtime: str # "serve" | "cloud"
54
+ current_board_slug: str = ""
55
+ storage_prefix: str = "faces"
56
+ org_slug: str = ""
57
+ project_slug: str = ""
58
+ branch: str = ""
59
+
60
+
61
+ def resolve_href(href: str, ctx: LinkContext) -> str:
62
+ """Resolve a single href against *ctx*.
63
+
64
+ - External URLs (``http:``, ``https:``, ``mailto:``, ``#``) pass through.
65
+ - Query-string-only URLs (``?…``) pass through unchanged for variables.js interception.
66
+ - Root-relative (``/path``) maps from author namespace.
67
+ - Relative (``../``, ``./``, bare) resolves against current board directory.
68
+ - ``.md`` / ``.yml`` / ``.yaml`` suffixes are stripped.
69
+ - Cloud URLs get ``/{org}/{project}/d/{slug}/`` shape + branch merge.
70
+ - Serve URLs get ``/{storage_prefix}/{slug}`` shape.
71
+ """
72
+ for prefix in _PASSTHROUGH_PREFIXES:
73
+ if href.startswith(prefix):
74
+ return href
75
+
76
+ # Split path and query (can't use urlparse fully — Jinja braces aren't valid)
77
+ path, query = _split_path_query(href)
78
+
79
+ # Resolve path
80
+ if path.startswith("/"):
81
+ # Root-relative: strip leading slash
82
+ resolved = path.lstrip("/")
83
+ else:
84
+ # Relative: resolve against current board's directory
85
+ board_dir = "/".join(ctx.current_board_slug.split("/")[:-1])
86
+ resolved = normpath(f"{board_dir}/{path}") if board_dir else normpath(path)
87
+ # normpath may produce leading ".." — clamp to root
88
+ if resolved.startswith(".."):
89
+ resolved = resolved.lstrip("./")
90
+
91
+ # Strip author-sugar suffixes
92
+ for suffix in _STRIP_SUFFIXES:
93
+ if resolved.endswith(suffix):
94
+ resolved = resolved[: -len(suffix)]
95
+ break
96
+
97
+ # Build final URL based on runtime
98
+ if ctx.runtime == "cloud":
99
+ return _build_cloud_url(resolved, query, ctx)
100
+ return _build_serve_url(resolved, query, ctx)
101
+
102
+
103
+ def _split_path_query(href: str) -> tuple[str, str]:
104
+ """Split href into (path, query) preserving Jinja templates in query."""
105
+ idx = href.find("?")
106
+ if idx == -1:
107
+ return href, ""
108
+ return href[:idx], href[idx + 1 :]
109
+
110
+
111
+ def _build_serve_url(slug: str, query: str, ctx: LinkContext) -> str:
112
+ if ctx.storage_prefix:
113
+ url = f"/{ctx.storage_prefix}/{slug}"
114
+ else:
115
+ url = f"/{slug}" if slug else "/"
116
+ if query:
117
+ url = f"{url}?{query}"
118
+ return url
119
+
120
+
121
+ def _build_cloud_url(slug: str, query: str, ctx: LinkContext) -> str:
122
+ base = f"/{ctx.org_slug}/{ctx.project_slug}/d/{slug}/"
123
+
124
+ # Merge branch if present in context and not already in query
125
+ if ctx.branch and not _query_has_key(query, "branch"):
126
+ separator = "&" if query else ""
127
+ query = f"{query}{separator}branch={ctx.branch}"
128
+
129
+ if query:
130
+ base = f"{base}?{query}"
131
+ return base
132
+
133
+
134
+ def _query_has_key(query: str, key: str) -> bool:
135
+ """Check if *key* appears in a query string (handles Jinja-laden strings)."""
136
+ # Simple prefix check — good enough for branch= detection
137
+ return f"{key}=" in query
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Markdown-level rewriting
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def rewrite_board_links(markdown: str, ctx: LinkContext | None) -> str:
146
+ """Rewrite board links in *markdown* using *ctx*.
147
+
148
+ Returns *markdown* unchanged when *ctx* is ``None``.
149
+ """
150
+ if ctx is None:
151
+ return markdown
152
+
153
+ def _replace(m: re.Match[str]) -> str:
154
+ text, href = m.group(1), m.group(2)
155
+ return f"[{text}]({resolve_href(href, ctx)})"
156
+
157
+ return _MD_LINK_RE.sub(_replace, markdown)
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Render-scoped context variable
162
+ # ---------------------------------------------------------------------------
163
+
164
+ _current_link_context: ContextVar[LinkContext | None] = ContextVar(
165
+ "_current_link_context", default=None
166
+ )
167
+
168
+
169
+ def get_link_context() -> LinkContext | None:
170
+ """Return the active link context (set by the renderer)."""
171
+ return _current_link_context.get()
172
+
173
+
174
+ def set_link_context(ctx: LinkContext | None) -> None:
175
+ """Set the active link context for the current render pass."""
176
+ _current_link_context.set(ctx)