dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__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 (227) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/__init__.py +70 -23
  3. dataface/agent_api/__init__.py +162 -68
  4. dataface/agent_api/_paths.py +9 -3
  5. dataface/agent_api/docs/yaml-reference.md +273 -239
  6. dataface/agent_api/init.py +1 -1
  7. dataface/agent_api/project_session.py +2 -1
  8. dataface/agent_api/serve.py +113 -0
  9. dataface/agent_api/validate.py +8 -0
  10. dataface/ai/agent.py +28 -2
  11. dataface/cli/_project.py +100 -1
  12. dataface/cli/commands/board_artifact.py +5 -2
  13. dataface/cli/commands/describe.py +7 -3
  14. dataface/cli/commands/docs.py +12 -3
  15. dataface/cli/commands/init.py +7 -14
  16. dataface/cli/commands/inspect.py +6 -1
  17. dataface/cli/commands/mcp_init.py +23 -43
  18. dataface/cli/commands/migrate.py +8 -3
  19. dataface/cli/commands/query.py +11 -8
  20. dataface/cli/commands/render.py +26 -9
  21. dataface/cli/commands/search.py +8 -2
  22. dataface/cli/commands/serve.py +25 -81
  23. dataface/cli/commands/skills_init.py +2 -19
  24. dataface/cli/commands/validate.py +7 -2
  25. dataface/cli/filesystem_project.py +5 -4
  26. dataface/cli/main.py +22 -33
  27. dataface/core/__init__.py +73 -28
  28. dataface/core/aliases.py +27 -0
  29. dataface/core/{compile/colors.py → colors.py} +20 -5
  30. dataface/core/compile/__init__.py +0 -8
  31. dataface/core/compile/channel.py +0 -13
  32. dataface/core/compile/compiler.py +161 -96
  33. dataface/core/compile/data_table.py +7 -2
  34. dataface/core/compile/enrich.py +18 -44
  35. dataface/core/compile/face_warnings.py +46 -8
  36. dataface/core/compile/format.py +47 -1
  37. dataface/core/compile/inherit_graph.py +3 -3
  38. dataface/core/compile/inherit_registry.yaml +1 -1
  39. dataface/core/compile/introspection.py +2 -13
  40. dataface/core/compile/migrations.py +112 -9
  41. dataface/core/compile/models/chart/authored/_base.py +1 -1
  42. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  43. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  44. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  45. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  46. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  47. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  48. dataface/core/compile/models/chart/resolved/table.py +16 -2
  49. dataface/core/compile/models/config.py +5 -0
  50. dataface/core/compile/models/face/authored.py +24 -89
  51. dataface/core/compile/models/markers.py +1 -1
  52. dataface/core/compile/models/primitives.py +175 -2
  53. dataface/core/compile/models/query/authored.py +1 -119
  54. dataface/core/compile/models/refs.py +0 -2
  55. dataface/core/compile/models/source.py +78 -164
  56. dataface/core/compile/models/style/authored/__init__.py +32 -8
  57. dataface/core/compile/models/style/authored/_base.py +126 -69
  58. dataface/core/compile/models/style/authored/table.py +61 -3
  59. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  60. dataface/core/compile/models/style/resolved/_base.py +142 -57
  61. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  62. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  63. dataface/core/compile/models/style/resolved/area.py +1 -1
  64. dataface/core/compile/models/style/resolved/bar.py +1 -1
  65. dataface/core/compile/models/style/resolved/line.py +1 -1
  66. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  67. dataface/core/compile/models/style/theme/__init__.py +36 -14
  68. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  69. dataface/core/compile/models/style/theme/axis.py +432 -312
  70. dataface/core/compile/models/style/theme/charts.py +10 -6
  71. dataface/core/compile/models/style/theme/kpi.py +2 -1
  72. dataface/core/compile/models/style/theme/page.py +5 -2
  73. dataface/core/compile/models/style/theme/table.py +2 -2
  74. dataface/core/compile/models/style/theme/variables.py +0 -3
  75. dataface/core/compile/models/vega_lite/config.py +1 -1
  76. dataface/core/compile/normalize_charts.py +10 -154
  77. dataface/core/compile/normalize_layout.py +20 -284
  78. dataface/core/compile/normalize_queries.py +24 -513
  79. dataface/core/compile/normalizer.py +4 -10
  80. dataface/core/compile/palette.py +19 -7
  81. dataface/core/compile/resolve/__init__.py +2 -0
  82. dataface/core/compile/resolve/_axes.py +147 -44
  83. dataface/core/compile/resolve/_channels.py +1 -1
  84. dataface/core/compile/resolve/_dispatch.py +1 -1
  85. dataface/core/compile/resolve/_domain.py +49 -32
  86. dataface/core/compile/resolve/_kwargs.py +39 -12
  87. dataface/core/compile/resolve/_marks.py +8 -3
  88. dataface/core/compile/resolve/_palette.py +7 -3
  89. dataface/core/compile/resolve/_table.py +124 -33
  90. dataface/core/compile/resolve/area.py +48 -18
  91. dataface/core/compile/resolve/bar.py +245 -64
  92. dataface/core/compile/resolve/geo.py +10 -3
  93. dataface/core/compile/resolve/heatmap.py +16 -8
  94. dataface/core/compile/resolve/line.py +35 -12
  95. dataface/core/compile/resolve/pie.py +4 -1
  96. dataface/core/compile/resolve/pie_attachment.py +1 -1
  97. dataface/core/compile/resolve/scatter.py +25 -13
  98. dataface/core/compile/resolve/simple.py +25 -2
  99. dataface/core/compile/schema_renderers/prompt.py +1 -1
  100. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  101. dataface/core/compile/source_map.py +211 -28
  102. dataface/core/compile/style_cascade.py +126 -55
  103. dataface/core/compile/validator.py +121 -44
  104. dataface/core/compile/yaml_error_formatter.py +108 -3
  105. dataface/core/connections.py +10 -3
  106. dataface/core/dashboard.py +27 -19
  107. dataface/core/defaults/default_config.yml +4 -0
  108. dataface/core/defaults/themes/_base.yaml +49 -40
  109. dataface/core/defaults/themes/cream.yaml +4 -4
  110. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  111. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  112. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  113. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  114. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  115. dataface/core/defaults/themes/editorial.yaml +9 -6
  116. dataface/core/defaults/themes/neon.yaml +15 -8
  117. dataface/core/defaults/themes/plain.yaml +6 -4
  118. dataface/core/defaults/themes/stark.yaml +24 -7
  119. dataface/core/defaults/themes/vivid.yaml +15 -8
  120. dataface/core/diagnostics/__init__.py +12 -2
  121. dataface/core/diagnostics/codes_compile.py +17 -33
  122. dataface/core/diagnostics/codes_execute.py +149 -0
  123. dataface/core/diagnostics/codes_render.py +144 -31
  124. dataface/core/diagnostics/diagnostic.py +30 -0
  125. dataface/core/diagnostics/hints.py +19 -0
  126. dataface/core/dialects/base.py +17 -0
  127. dataface/core/dialects/postgres.py +14 -0
  128. dataface/core/dialects/redshift.py +5 -0
  129. dataface/core/execute/__init__.py +59 -14
  130. dataface/core/execute/adapters/__init__.py +1 -1
  131. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  132. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  133. dataface/core/execute/adapters/dbt_utils.py +84 -20
  134. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  135. dataface/core/execute/adapters/sql_adapter.py +72 -4
  136. dataface/core/execute/cache_keys.py +4 -9
  137. dataface/core/execute/dbt_jinja.py +65 -10
  138. dataface/core/execute/executor.py +94 -19
  139. dataface/core/execute/file_source_materializer.py +185 -32
  140. dataface/core/execute/trivial_local_cache.py +5 -2
  141. dataface/core/font_measure.py +28 -0
  142. dataface/core/fonts.py +8 -5
  143. dataface/core/inspect/__init__.py +46 -7
  144. dataface/core/project.py +19 -3
  145. dataface/core/render/chart/data_table_attachment.py +14 -12
  146. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  147. dataface/core/render/chart/emitters/_channels.py +56 -8
  148. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  149. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  150. dataface/core/render/chart/emitters/area.py +3 -3
  151. dataface/core/render/chart/emitters/bar.py +60 -35
  152. dataface/core/render/chart/emitters/geo.py +8 -13
  153. dataface/core/render/chart/emitters/heatmap.py +20 -26
  154. dataface/core/render/chart/emitters/line.py +2 -2
  155. dataface/core/render/chart/emitters/scatter.py +7 -30
  156. dataface/core/render/chart/features/baseline.py +22 -9
  157. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  158. dataface/core/render/chart/features/mirror_axis.py +12 -9
  159. dataface/core/render/chart/features/value_labels.py +1 -1
  160. dataface/core/render/chart/kpi.py +11 -90
  161. dataface/core/render/chart/mark_extents.py +339 -0
  162. dataface/core/render/chart/presentation.py +6 -8
  163. dataface/core/render/chart/rendering.py +100 -46
  164. dataface/core/render/chart/session.py +10 -0
  165. dataface/core/render/chart/spec.py +3 -0
  166. dataface/core/render/chart/spec_builders.py +18 -18
  167. dataface/core/render/chart/table.py +81 -36
  168. dataface/core/render/chart/table_support.py +2 -53
  169. dataface/core/render/chart/time_unit_detect.py +3 -2
  170. dataface/core/render/chart/translate.py +82 -44
  171. dataface/core/render/chart/type_inference.py +45 -25
  172. dataface/core/render/chart/validation.py +91 -0
  173. dataface/core/render/chart/vega_lite.py +30 -11
  174. dataface/core/render/chart/vl_field_maps.py +111 -76
  175. dataface/core/render/conditions.py +122 -0
  176. dataface/core/render/controls.py +60 -187
  177. dataface/core/render/converters/chart.py +16 -6
  178. dataface/core/render/data_format.py +1 -1
  179. dataface/core/render/face_resolve.py +7 -0
  180. dataface/core/render/faces.py +97 -32
  181. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  182. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  183. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  184. dataface/core/render/fonts/_font_face.css +10 -0
  185. dataface/core/render/layout_sizing.py +313 -238
  186. dataface/core/render/numeral_expr.py +80 -0
  187. dataface/core/render/renderer.py +14 -1
  188. dataface/core/render/sizing.py +42 -105
  189. dataface/core/render/svg_utils.py +60 -14
  190. dataface/core/render/terminal.py +1 -1
  191. dataface/core/render/variables_layout.py +211 -0
  192. dataface/core/render/variables_resolve.py +376 -0
  193. dataface/core/render/variables_strip.py +291 -368
  194. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  195. dataface/core/render/warnings/base.py +18 -0
  196. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  197. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  198. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  199. dataface/core/render/warnings/palette_unsupported.py +6 -0
  200. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  201. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  202. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  203. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  204. dataface/core/render/warnings/redundant_encoding.py +20 -1
  205. dataface/core/render/warnings/registry.py +9 -3
  206. dataface/core/render/warnings/temporal_single_point.py +1 -0
  207. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  208. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  209. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  210. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  211. dataface/core/render_format.py +18 -0
  212. dataface/core/serve/alias_index.py +16 -22
  213. dataface/core/text/format_d3.py +56 -0
  214. dataface/core/text/numeral_scale.py +218 -0
  215. dataface/core/utils.py +103 -10
  216. dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
  217. dataface/data/schemas/yaml/0.3.2.json +1 -0
  218. dataface/data/schemas/yaml/manifest.json +7 -0
  219. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  220. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
  221. dataface/core/inspect/partials/categorical.yml +0 -38
  222. dataface/core/inspect/partials/date.yml +0 -38
  223. dataface/core/inspect/partials/numeric.yml +0 -53
  224. dataface/core/inspect/templates/charts.yml +0 -94
  225. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  226. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  227. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,22 @@
1
1
  """Render command implementation."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import json as _json
4
6
  import sys
5
7
  from pathlib import Path
8
+ from typing import TYPE_CHECKING
6
9
 
7
10
  import typer
8
11
 
9
- from dataface.agent_api import Diagnostic, InMemoryFace, ProjectSession, RenderFormat
10
- from dataface.agent_api._paths import (
11
- build_face_render_context,
12
- build_yaml_render_context,
13
- resolve_face_or_error,
14
- )
15
- from dataface.agent_api.cache import project_cache_ctx
16
- from dataface.agent_api.dashboards import RenderedDashboard
17
- from dataface.agent_api.diagnostics import REGISTRY as _diag_registry
12
+ from dataface.agent_api import Diagnostic, RenderFormat
18
13
  from dataface.cli._error_format import print_diagnostics
19
14
  from dataface.cli._project import with_project
20
15
  from dataface.cli.filesystem_project import FilesystemProject
21
16
 
17
+ if TYPE_CHECKING:
18
+ from dataface.agent_api.dashboards import RenderedDashboard
19
+
22
20
  # Maps common file extensions to render formats for format inference.
23
21
  _EXT_TO_FORMAT: dict[str, RenderFormat] = {
24
22
  "svg": "svg",
@@ -64,6 +62,8 @@ def _validate_ignore_codes(ignore_codes: set[str]) -> None:
64
62
  at error level (suppressing an error is never valid) versus a code that
65
63
  isn't registered at all (a typo or stale suppression stub).
66
64
  """
65
+ from dataface.agent_api.diagnostics import REGISTRY as _diag_registry
66
+
67
67
  warning_codes = _diag_registry.codes(level="warning")
68
68
  error_codes = _diag_registry.codes(level="error")
69
69
  for code in sorted(ignore_codes):
@@ -172,6 +172,7 @@ def render_command(
172
172
  fail_on_chart_errors: bool = True,
173
173
  max_workers: int | None = None,
174
174
  print0: bool = False,
175
+ chart: str | None = None,
175
176
  *,
176
177
  project: FilesystemProject,
177
178
  ) -> str | None:
@@ -204,6 +205,13 @@ def render_command(
204
205
  Returns:
205
206
  Output file path on success, None for terminal/stdout output
206
207
  """
208
+ from dataface.agent_api import ProjectSession
209
+ from dataface.agent_api._paths import (
210
+ build_face_render_context,
211
+ resolve_face_or_error,
212
+ )
213
+ from dataface.agent_api.cache import project_cache_ctx
214
+
207
215
  if ignore_codes:
208
216
  _validate_ignore_codes(ignore_codes)
209
217
 
@@ -230,6 +238,7 @@ def render_command(
230
238
  raise RenderFailed([resolved])
231
239
  result = project_session.render_dashboard(
232
240
  face=resolved,
241
+ chart=chart,
233
242
  format=format,
234
243
  variables=variables,
235
244
  use_cache=use_cache,
@@ -266,6 +275,7 @@ def render_command_from_yaml(
266
275
  fail_on_chart_errors: bool = True,
267
276
  max_workers: int | None = None,
268
277
  print0: bool = False,
278
+ chart: str | None = None,
269
279
  *,
270
280
  project: FilesystemProject,
271
281
  ) -> str | None:
@@ -296,6 +306,10 @@ def render_command_from_yaml(
296
306
  Output file path on success, None for terminal/stdout output. Exits 1
297
307
  (typer.Exit) after reporting on a validation/chart failure.
298
308
  """
309
+ from dataface.agent_api import InMemoryFace, ProjectSession
310
+ from dataface.agent_api._paths import build_yaml_render_context
311
+ from dataface.agent_api.cache import project_cache_ctx
312
+
299
313
  if ignore_codes:
300
314
  _validate_ignore_codes(ignore_codes)
301
315
 
@@ -318,6 +332,7 @@ def render_command_from_yaml(
318
332
  face = InMemoryFace(yaml_content, path=None)
319
333
  result = project_session.render_dashboard(
320
334
  face=face,
335
+ chart=chart,
321
336
  format=format,
322
337
  variables=variables,
323
338
  use_cache=use_cache,
@@ -428,6 +443,7 @@ def render_commands(
428
443
  max_workers: int | None = None,
429
444
  print0: bool = False,
430
445
  fail_fast: bool = False,
446
+ chart: str | None = None,
431
447
  ) -> None:
432
448
  """Render one or more dashboards, printing output paths to stdout.
433
449
 
@@ -474,6 +490,7 @@ def render_commands(
474
490
  fail_on_chart_errors=fail_on_chart_errors,
475
491
  max_workers=max_workers,
476
492
  print0=print0,
493
+ chart=chart,
477
494
  )
478
495
  except RenderFailed as exc:
479
496
  print_diagnostics(exc.errors, json_output=json_errors)
@@ -2,13 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import TYPE_CHECKING
6
+
5
7
  import typer
6
8
 
7
- from dataface.agent_api import Project, ProjectSession
8
- from dataface.agent_api.search import SearchResult
9
+ from dataface.agent_api import Project
9
10
  from dataface.cli._error_format import print_diagnostics
10
11
  from dataface.cli._project import with_project
11
12
 
13
+ if TYPE_CHECKING:
14
+ from dataface.agent_api.search import SearchResult
15
+
12
16
 
13
17
  @with_project
14
18
  def search_command(
@@ -19,6 +23,8 @@ def search_command(
19
23
  limit: int = 10,
20
24
  ) -> None:
21
25
  """Search dashboards by keyword with ranked results."""
26
+ from dataface.agent_api import ProjectSession
27
+
22
28
  with ProjectSession.from_project(project) as project_session:
23
29
  result = project_session.search_dashboards(query, limit=limit)
24
30
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
5
  import sys
7
6
  from pathlib import Path
8
7
  from typing import TYPE_CHECKING
@@ -10,15 +9,10 @@ from typing import TYPE_CHECKING
10
9
  import typer
11
10
  import uvicorn
12
11
 
12
+ from dataface.agent_api import Diagnostic
13
13
  from dataface.cli._error_format import print_diagnostics
14
14
  from dataface.cli._project import with_project
15
15
 
16
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
17
- from dataface.core.diagnostics import ERR_STARTUP_FAILED
18
-
19
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
20
- from dataface.core.diagnostics.base import DatafaceError
21
-
22
16
  if TYPE_CHECKING:
23
17
  from dataface.cli.filesystem_project import FilesystemProject
24
18
 
@@ -34,7 +28,6 @@ def serve_command(
34
28
  max_workers: int | None = None,
35
29
  no_cache: bool = False,
36
30
  cache_path: Path | None = None,
37
- allow_external_access: bool = False,
38
31
  ) -> None:
39
32
  """Start unified Dataface server.
40
33
 
@@ -55,10 +48,6 @@ def serve_command(
55
48
  project: The resolved Dataface project (injected by @with_project)
56
49
  dialect: SQL dialect (auto-detected from dbt profile, or duckdb)
57
50
  target: dbt target name (defaults to DBT_TARGET env var, then profile default)
58
- allow_external_access: Allow author SQL to reach the filesystem/network via
59
- DuckDB (read_csv, read_json_auto, httpfs). Off by default — serve is
60
- strict read-only. Opt in only for local dashboards that must read files
61
- directly in SQL (e.g. glob-based analytics that cannot be file sources).
62
51
  no_cache: Skip the query-result cache entirely.
63
52
  cache_path: Persist the query-result cache to this DuckDB file (created
64
53
  if absent). None (the default) opens an in-memory cache — ephemeral,
@@ -68,86 +57,42 @@ def serve_command(
68
57
  None (exits with code 0 on success, 1 on errors)
69
58
  """
70
59
  from dataface.agent_api import set_surface
71
-
72
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
73
- from dataface.core.compile.config import list_built_in_themes, load_config
74
-
75
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
76
- from dataface.core.diagnostics import ERR_INVALID_DEFAULT_THEME
77
-
78
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
79
- from dataface.core.project_roots import infer_dialect_from_dbt
80
-
81
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
82
- from dataface.core.serve.port import resolve_port
83
-
84
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
85
- from dataface.core.serve.server import create_server
60
+ from dataface.agent_api.serve import format_startup_failure, prepare_serve
86
61
 
87
62
  # Narrows the root callback's "cli": a served dashboard's queries are a
88
63
  # different cost story from a one-shot `dft render`.
89
64
  set_surface("serve")
90
65
 
91
- project_dir = project.root
92
-
93
- # Validate DFT_DEFAULT_THEME at startup — fail fast before serving any request.
94
- # The env var is read per-compilation by get_default_theme_name(); validation
95
- # here ensures the user sees a clear error at startup rather than on first render.
96
- env_theme = os.environ.get(
97
- "DFT_DEFAULT_THEME"
98
- ) # noqa: TID251 — DFT_DEFAULT_THEME knob
99
- # Empty string is treated as unset (matches get_default_theme_name, which
100
- # falls back to the shipped default) — only a non-empty unknown name errors.
101
- if env_theme:
102
- available = list_built_in_themes()
103
- if env_theme not in available:
104
- err = DatafaceError.from_code(
105
- ERR_INVALID_DEFAULT_THEME,
106
- source="DFT_DEFAULT_THEME env var",
107
- theme=env_theme,
108
- available=available,
109
- )
110
- print_diagnostics([err.to_diagnostic()])
111
- raise typer.Exit(1) from None
112
-
113
- # Validate dataface.yml wholesale: stray keys (style:, board:, theme:, etc.) are a
114
- # hard error here at startup rather than silently ignored.
66
+ # Validate DFT_DEFAULT_THEME/dataface.yml, resolve port/dialect, and build the
67
+ # ASGI app — a dataface.yml validation failure raises pydantic's ValidationError
68
+ # uncaught (matches pre-existing behavior: a plain echo, not the Diagnostic
69
+ # envelope used everywhere else in this command).
115
70
  from pydantic import ValidationError as _PydanticError
116
71
 
117
72
  try:
118
- load_config(project)
73
+ setup = prepare_serve(
74
+ project,
75
+ port=port,
76
+ host=host,
77
+ dialect=dialect,
78
+ target=target,
79
+ max_workers=max_workers,
80
+ no_cache=no_cache,
81
+ cache_path=cache_path,
82
+ )
119
83
  except _PydanticError as e:
120
84
  typer.echo(f"dataface.yml validation error: {e}", err=True)
121
85
  raise typer.Exit(1) from None
122
86
 
123
- # Resolve port: --port > DFT_PORT > dataface.yml server.port > hash(project_dir)
124
- port = resolve_port(
125
- explicit_port=port,
126
- project_dir=project_dir,
127
- host=host,
128
- )
87
+ if isinstance(setup, Diagnostic):
88
+ print_diagnostics([setup])
89
+ raise typer.Exit(1) from None
129
90
 
130
- # Resolve dbt target: CLI flag > DBT_TARGET env > profile default
131
- effective_target = target or os.environ.get("DBT_TARGET") or None
132
-
133
- # Infer dialect from dbt profile when not explicitly provided
134
- effective_dialect = dialect
135
- if effective_dialect is None:
136
- inferred = infer_dialect_from_dbt(project_dir, effective_target)
137
- if inferred:
138
- typer.echo(f" Auto-detected dialect: {inferred} (from dbt profile)")
139
- effective_dialect = inferred or "duckdb"
140
-
141
- # Create FastAPI app
142
- app = create_server(
143
- project,
144
- dialect=effective_dialect,
145
- target=effective_target,
146
- max_workers=max_workers,
147
- no_cache=no_cache,
148
- cache_path=cache_path,
149
- allow_external_access_in_readonly=allow_external_access,
150
- )
91
+ if setup.dialect_inferred:
92
+ typer.echo(f" Auto-detected dialect: {setup.dialect} (from dbt profile)")
93
+
94
+ port = setup.port
95
+ app = setup.app
151
96
 
152
97
  # Start server
153
98
  typer.echo("🚀 Starting dashboard server")
@@ -175,6 +120,5 @@ def serve_command(
175
120
  typer.echo("\n👋 Server stopped")
176
121
  sys.exit(0)
177
122
  except Exception as e: # noqa: BLE001 — uvicorn surface
178
- err = DatafaceError.from_code(ERR_STARTUP_FAILED, detail=str(e))
179
- print_diagnostics([err.to_diagnostic()])
123
+ print_diagnostics([format_startup_failure(e)])
180
124
  raise typer.Exit(1) from None
@@ -7,28 +7,11 @@ from pathlib import Path
7
7
  import typer
8
8
 
9
9
  from dataface.agent_api import skill_install
10
-
11
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
12
- from dataface.core.project_roots import find_dft_root, find_repo_root
10
+ from dataface.cli._project import resolve_skill_install_root
13
11
 
14
12
  _VALID_TARGETS = skill_install.SKILL_INSTALL_TARGETS
15
13
 
16
14
 
17
- def _resolve_project_dir(start: Path) -> Path:
18
- """Return the directory where skills should be installed.
19
-
20
- Skills are agent tooling that belong at the git root, not the operational
21
- project root. When a .git directory exists above the dataface.yml, install
22
- at the git root so monorepos with nested dataface projects don't accumulate
23
- divergent skill copies.
24
- """
25
- project_root = find_dft_root(start) or start.resolve()
26
- git_root = find_repo_root(project_root)
27
- if git_root is not None:
28
- return git_root
29
- return project_root
30
-
31
-
32
15
  def _resolve_targets(
33
16
  target: str | None,
34
17
  *,
@@ -110,7 +93,7 @@ def run_init_skills(
110
93
  typer.echo("Specify either --all or --dir, not both.", err=True)
111
94
  raise typer.Exit(1)
112
95
 
113
- project_root = _resolve_project_dir(
96
+ project_root = resolve_skill_install_root(
114
97
  project_dir if project_dir is not None else Path.cwd()
115
98
  )
116
99
  target_dirs = _resolve_targets(
@@ -4,15 +4,18 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  from pathlib import Path
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  import typer
9
10
 
10
- from dataface.agent_api import Project, ProjectSession
11
- from dataface.agent_api.validate import ValidateResult
11
+ from dataface.agent_api import Project
12
12
  from dataface.cli._console import dft_console
13
13
  from dataface.cli._error_format import print_diagnostics
14
14
  from dataface.cli._project import with_project
15
15
 
16
+ if TYPE_CHECKING:
17
+ from dataface.agent_api.validate import ValidateResult
18
+
16
19
  _console = dft_console()
17
20
 
18
21
 
@@ -24,6 +27,8 @@ def validate_command(
24
27
  json_output: bool = False,
25
28
  strict: bool = False,
26
29
  ) -> None:
30
+ from dataface.agent_api import ProjectSession
31
+
27
32
  with ProjectSession.from_project(project) as project_session:
28
33
  results = project_session.validate_paths(paths)
29
34
  _emit(results, json_output=json_output, strict=strict)
@@ -30,6 +30,7 @@ from dataface.core.project import (
30
30
  assert_relpath,
31
31
  glob_literal_prefix,
32
32
  glob_to_regex,
33
+ posix_relpath,
33
34
  )
34
35
 
35
36
  if TYPE_CHECKING:
@@ -143,7 +144,7 @@ class FilesystemProject(Project):
143
144
  # First try: fully resolved path. Handles OS-level symlinks (/var on macOS).
144
145
  resolved = path.resolve()
145
146
  try:
146
- return str(resolved.relative_to(self.root))
147
+ return posix_relpath(resolved, self.root)
147
148
  except ValueError:
148
149
  pass
149
150
 
@@ -151,7 +152,7 @@ class FilesystemProject(Project):
151
152
  # paths through project-internal symlinks (e.g. faces/tasks/ -> ../../tasks/).
152
153
  normalized = Path(os.path.normpath(path.absolute()))
153
154
  try:
154
- return str(normalized.relative_to(self.root))
155
+ return posix_relpath(normalized, self.root)
155
156
  except ValueError:
156
157
  raise ValueError(
157
158
  f"Path {path!r} is outside project root {self.root!r}"
@@ -218,12 +219,12 @@ class FilesystemProject(Project):
218
219
  dirs[:] = sorted(d for d in dirs if d not in SKIP_SCAN_DIRS)
219
220
  root_path = Path(root_str)
220
221
  for name in files:
221
- candidates.append(str((root_path / name).relative_to(self.root)))
222
+ candidates.append(posix_relpath(root_path / name, self.root))
222
223
  yield from sorted(candidates)
223
224
  else:
224
225
  for p in sorted(subtree.iterdir()):
225
226
  if p.is_file():
226
- yield str(p.relative_to(self.root))
227
+ yield posix_relpath(p, self.root)
227
228
 
228
229
  def iter_dir(self, under: str) -> Iterator[ProjectPath | ProjectDirectory]:
229
230
  """``os.scandir`` over *under*, filtered/sorted per ``Project.iter_dir``.
dataface/cli/main.py CHANGED
@@ -16,6 +16,7 @@ from dataface.agent_api import RenderFormat, set_surface
16
16
  from dataface.cli._console import is_plain_output
17
17
  from dataface.cli._extras import require_extras
18
18
  from dataface.cli._parsing import parse_kv_pairs
19
+ from dataface.cli._project import has_faces_marker
19
20
  from dataface.cli.commands import (
20
21
  board_artifact as board_artifact_cmd,
21
22
  describe as describe_cmd,
@@ -35,9 +36,6 @@ from dataface.cli.commands import (
35
36
  from dataface.cli.commands.mcp_init import run_init as _mcp_run_init
36
37
  from dataface.cli.commands.skills_init import run_init_skills as _skills_run_init
37
38
 
38
- # tach-ignore(cli->core debt: route via agent_api; burn-down task)
39
- from dataface.core.project_roots import find_dft_root
40
-
41
39
  # Evaluated once at process startup — mid-session env changes won't take effect.
42
40
  _RICH_MARKUP_MODE: Literal["rich"] | None = None if is_plain_output() else "rich"
43
41
  # Probe for private package once at startup; avoids repeated find_spec calls.
@@ -107,21 +105,6 @@ if not logging.getLogger().handlers:
107
105
  )
108
106
 
109
107
 
110
- def _has_faces_marker(start: Path) -> bool:
111
- """True iff *start* sits inside a Dataface project that has a ``faces/`` directory.
112
-
113
- Anchors on :func:`find_dft_root` so the walk-up stops at the
114
- project root (any of ``dataface.yml``/``dataface.yaml``/``dbt_project.yml``)
115
- rather than blindly hunting upward for stray ``faces/`` directories
116
- elsewhere on the filesystem. A bare-dbt repo with no scaffolded
117
- ``faces/`` returns False — that's the case the banner exists for.
118
- """
119
- if (start / "faces").is_dir():
120
- return True
121
- project = find_dft_root(start)
122
- return project is not None and (project / "faces").is_dir()
123
-
124
-
125
108
  def _render_init_banner() -> None:
126
109
  """Print a nudge to run ``dft init``.
127
110
 
@@ -203,7 +186,7 @@ class _RootHelpGroup(TyperGroup):
203
186
  )
204
187
 
205
188
  def format_help(self, ctx: Any, formatter: Any) -> None:
206
- if not _has_faces_marker(Path.cwd()):
189
+ if not has_faces_marker(Path.cwd()):
207
190
  _render_init_banner()
208
191
  super().format_help(ctx, formatter)
209
192
 
@@ -813,6 +796,16 @@ def render(
813
796
  ),
814
797
  ),
815
798
  ] = False,
799
+ chart: Annotated[
800
+ str | None,
801
+ typer.Option(
802
+ "--chart",
803
+ help=(
804
+ "Render only this chart (by chart id). "
805
+ "Valid only with a single face argument or stdin."
806
+ ),
807
+ ),
808
+ ] = None,
816
809
  ) -> None:
817
810
  """Render one or more dashboards to SVG, HTML, PNG, PDF, or terminal.
818
811
 
@@ -876,6 +869,14 @@ def render(
876
869
  param_hint="--output",
877
870
  )
878
871
 
872
+ # --chart is ambiguous with multiple faces: no mapping syntax, no silent fanout.
873
+ if chart is not None and len(faces) > 1:
874
+ raise typer.BadParameter(
875
+ f"--chart is only valid with a single face argument; "
876
+ f"got {len(faces)} face arguments",
877
+ param_hint="--chart",
878
+ )
879
+
879
880
  # Stdin path (single face "-")
880
881
  if has_stdin:
881
882
  yaml_content = sys.stdin.read()
@@ -896,6 +897,7 @@ def render(
896
897
  fail_on_chart_errors=fail_on_chart_errors,
897
898
  max_workers=max_workers,
898
899
  print0=print0,
900
+ chart=chart,
899
901
  )
900
902
  return
901
903
 
@@ -914,6 +916,7 @@ def render(
914
916
  max_workers=max_workers,
915
917
  print0=print0,
916
918
  fail_fast=fail_fast,
919
+ chart=chart,
917
920
  )
918
921
 
919
922
 
@@ -1104,19 +1107,6 @@ def serve(
1104
1107
  ),
1105
1108
  ),
1106
1109
  ] = None,
1107
- allow_external_access: Annotated[
1108
- bool,
1109
- typer.Option(
1110
- "--allow-external-access",
1111
- help=(
1112
- "Allow author SQL to read local files / URLs through DuckDB "
1113
- "(read_csv, read_json_auto, httpfs). Off by default — serve runs "
1114
- "strict read-only. Opt in only for local dashboards that must read "
1115
- "files directly in SQL (e.g. glob analytics not expressible as file "
1116
- "sources); prefer type: csv/json/parquet sources otherwise."
1117
- ),
1118
- ),
1119
- ] = False,
1120
1110
  ) -> None:
1121
1111
  """Start the dashboard server.
1122
1112
 
@@ -1166,7 +1156,6 @@ def serve(
1166
1156
  max_workers=max_workers,
1167
1157
  no_cache=no_cache,
1168
1158
  cache_path=cache,
1169
- allow_external_access=allow_external_access,
1170
1159
  )
1171
1160
 
1172
1161
 
dataface/core/__init__.py CHANGED
@@ -13,8 +13,10 @@ Submodules:
13
13
 
14
14
  High-Level API:
15
15
  >>> from pathlib import Path
16
- >>> from dataface.core import compile, Executor, render
16
+ >>> from dataface.core.compile import compile
17
+ >>> from dataface.core.execute import Executor
17
18
  >>> from dataface.core.execute.adapters import build_adapter_registry
19
+ >>> from dataface.core.render import render
18
20
  >>> result = compile(yaml_content)
19
21
  >>> from dataface.cli.filesystem_project import FilesystemProject
20
22
  >>> registry = build_adapter_registry(FilesystemProject(Path.cwd()))
@@ -22,37 +24,45 @@ High-Level API:
22
24
  >>> html = render(result.face, executor, format="html")
23
25
  """
24
26
 
25
- # Re-export main compile functions and types
26
- # Re-export validate module
27
- from dataface.core import validate
28
- from dataface.core.compile import (
29
- AnyQuery,
30
- AuthoredFace,
31
- CompilationError,
32
- CompileResult,
33
- Face,
34
- Layout,
35
- LayoutItem,
36
- Variable,
37
- compile,
38
- compile_file,
39
- )
27
+ from __future__ import annotations
40
28
 
41
- # Re-export execute functions and types
42
- from dataface.core.execute import (
43
- ExecutionError,
44
- Executor,
45
- )
29
+ import importlib
30
+ from typing import TYPE_CHECKING, Any
46
31
 
47
- # Re-export render functions and types
48
- from dataface.core.render import (
49
- RenderError,
50
- render,
51
- )
32
+ if TYPE_CHECKING:
33
+ # Import-time-only: PEP 562 __getattr__ below resolves these lazily at
34
+ # runtime, so `import dataface.core.<anything>` (this file runs first,
35
+ # always) doesn't eagerly pull in the whole compile/execute/render stack
36
+ # just for this convenience re-export.
37
+ from dataface.core import validate as validate
38
+ from dataface.core.compile import (
39
+ AnyQuery as AnyQuery,
40
+ AuthoredFace as AuthoredFace,
41
+ CompilationError as CompilationError,
42
+ CompileResult as CompileResult,
43
+ Face as Face,
44
+ Layout as Layout,
45
+ LayoutItem as LayoutItem,
46
+ Variable as Variable,
47
+ compile_file as compile_file,
48
+ )
49
+ from dataface.core.execute import (
50
+ ExecutionError as ExecutionError,
51
+ Executor as Executor,
52
+ )
53
+ from dataface.core.render import RenderError as RenderError
52
54
 
55
+ # `compile` and `render` are deliberately NOT re-exported here: both names
56
+ # collide with a same-named submodule one level down (`dataface.core.compile`,
57
+ # `dataface.core.render`). Importing that submodule for *any* reason binds it
58
+ # onto this package under that exact name as a side effect of Python's own
59
+ # import machinery — a rebind PEP 562 __getattr__ cannot see or prevent — so a
60
+ # lazy alias sharing a submodule's leaf name silently flips from function to
61
+ # module depending on import order. Import the function directly from its
62
+ # submodule instead: `from dataface.core.compile import compile`,
63
+ # `from dataface.core.render import render`.
53
64
  __all__ = [
54
65
  # Compile
55
- "compile",
56
66
  "compile_file",
57
67
  "CompileResult",
58
68
  "Face",
@@ -66,8 +76,43 @@ __all__ = [
66
76
  "Executor",
67
77
  "ExecutionError",
68
78
  # Render
69
- "render",
70
79
  "RenderError",
71
80
  # Validate
72
81
  "validate",
73
82
  ]
83
+
84
+ _LAZY_ATTRS: dict[str, tuple[str, str]] = {
85
+ "validate": ("dataface.core", "validate"),
86
+ "AnyQuery": ("dataface.core.compile", "AnyQuery"),
87
+ "AuthoredFace": ("dataface.core.compile", "AuthoredFace"),
88
+ "CompilationError": ("dataface.core.compile", "CompilationError"),
89
+ "CompileResult": ("dataface.core.compile", "CompileResult"),
90
+ "Face": ("dataface.core.compile", "Face"),
91
+ "Layout": ("dataface.core.compile", "Layout"),
92
+ "LayoutItem": ("dataface.core.compile", "LayoutItem"),
93
+ "Variable": ("dataface.core.compile", "Variable"),
94
+ "compile_file": ("dataface.core.compile", "compile_file"),
95
+ "ExecutionError": ("dataface.core.execute", "ExecutionError"),
96
+ "Executor": ("dataface.core.execute", "Executor"),
97
+ "RenderError": ("dataface.core.render", "RenderError"),
98
+ }
99
+
100
+
101
+ def __getattr__(name: str) -> Any:
102
+ try:
103
+ module_path, attr_name = _LAZY_ATTRS[name]
104
+ except KeyError:
105
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
106
+ if module_path == __name__ and attr_name == name:
107
+ # "validate" is a submodule of this very package, not another
108
+ # module's attribute — import it directly to avoid recursing back
109
+ # into this __getattr__.
110
+ module = importlib.import_module(f"{__name__}.{attr_name}")
111
+ else:
112
+ module = getattr(importlib.import_module(module_path), attr_name)
113
+ globals()[name] = module # cache: repeated access is O(1) and `is` identity holds
114
+ return module
115
+
116
+
117
+ def __dir__() -> list[str]:
118
+ return sorted(set(globals()) | set(_LAZY_ATTRS))
@@ -0,0 +1,27 @@
1
+ """Alias-URL semantics, shared by every host that resolves a face's ``aliases:``.
2
+
3
+ A leaf module on purpose. An alias is claimed in a face and matched against an
4
+ incoming request path as a *string*, so the two sides must normalize it
5
+ identically or a claim resolves on one host and silently does nothing on the
6
+ other. Hosts that keep their own board index (rather than building an
7
+ ``AliasIndex``) need that rule without importing the ``dft serve`` HTTP
8
+ application to get it — ``core/serve/`` sits at the top of the dependency stack.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from urllib.parse import unquote
14
+
15
+
16
+ def normalize_alias_url(url: str) -> str:
17
+ """Normalize a URL key: percent-decode, ensure leading /, ensure trailing /."""
18
+ decoded = unquote(url)
19
+ if not decoded.startswith("/"):
20
+ raise ValueError(
21
+ f"Alias {url!r} is not absolute: aliases must start with a leading /. "
22
+ "Example: /old-reports/"
23
+ )
24
+ # Trailing-slash canonical: add if missing
25
+ if not decoded.endswith("/"):
26
+ decoded = decoded + "/"
27
+ return decoded