dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__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 (185) hide show
  1. dataface/__init__.py +70 -23
  2. dataface/agent_api/__init__.py +162 -68
  3. dataface/agent_api/_paths.py +9 -3
  4. dataface/agent_api/docs/yaml-reference.md +240 -106
  5. dataface/agent_api/init.py +1 -1
  6. dataface/agent_api/project_session.py +2 -1
  7. dataface/agent_api/serve.py +115 -0
  8. dataface/ai/agent.py +28 -2
  9. dataface/cli/_project.py +100 -1
  10. dataface/cli/commands/board_artifact.py +5 -2
  11. dataface/cli/commands/describe.py +7 -3
  12. dataface/cli/commands/docs.py +12 -3
  13. dataface/cli/commands/init.py +7 -14
  14. dataface/cli/commands/inspect.py +6 -1
  15. dataface/cli/commands/mcp_init.py +23 -43
  16. dataface/cli/commands/migrate.py +8 -3
  17. dataface/cli/commands/query.py +11 -8
  18. dataface/cli/commands/render.py +20 -9
  19. dataface/cli/commands/search.py +8 -2
  20. dataface/cli/commands/serve.py +26 -76
  21. dataface/cli/commands/skills_init.py +2 -19
  22. dataface/cli/commands/validate.py +7 -2
  23. dataface/cli/filesystem_project.py +5 -4
  24. dataface/cli/main.py +2 -19
  25. dataface/core/__init__.py +73 -28
  26. dataface/core/{compile/colors.py → colors.py} +20 -5
  27. dataface/core/compile/compiler.py +97 -34
  28. dataface/core/compile/data_table.py +7 -2
  29. dataface/core/compile/enrich.py +18 -44
  30. dataface/core/compile/face_warnings.py +46 -8
  31. dataface/core/compile/format.py +47 -1
  32. dataface/core/compile/inherit_graph.py +3 -3
  33. dataface/core/compile/inherit_registry.yaml +1 -1
  34. dataface/core/compile/migrations.py +112 -9
  35. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  36. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  37. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  38. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  39. dataface/core/compile/models/chart/resolved/table.py +16 -2
  40. dataface/core/compile/models/markers.py +1 -1
  41. dataface/core/compile/models/primitives.py +175 -2
  42. dataface/core/compile/models/source.py +5 -5
  43. dataface/core/compile/models/style/authored/__init__.py +32 -8
  44. dataface/core/compile/models/style/authored/_base.py +126 -69
  45. dataface/core/compile/models/style/authored/table.py +61 -3
  46. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  47. dataface/core/compile/models/style/resolved/_base.py +142 -57
  48. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  49. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  50. dataface/core/compile/models/style/resolved/area.py +1 -1
  51. dataface/core/compile/models/style/resolved/bar.py +1 -1
  52. dataface/core/compile/models/style/resolved/line.py +1 -1
  53. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  54. dataface/core/compile/models/style/theme/__init__.py +36 -14
  55. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  56. dataface/core/compile/models/style/theme/axis.py +432 -312
  57. dataface/core/compile/models/style/theme/charts.py +10 -6
  58. dataface/core/compile/models/style/theme/kpi.py +2 -1
  59. dataface/core/compile/models/style/theme/page.py +5 -2
  60. dataface/core/compile/models/style/theme/table.py +2 -2
  61. dataface/core/compile/models/vega_lite/config.py +1 -1
  62. dataface/core/compile/normalize_charts.py +6 -2
  63. dataface/core/compile/normalize_queries.py +21 -7
  64. dataface/core/compile/resolve/__init__.py +2 -0
  65. dataface/core/compile/resolve/_axes.py +147 -44
  66. dataface/core/compile/resolve/_channels.py +1 -1
  67. dataface/core/compile/resolve/_dispatch.py +1 -1
  68. dataface/core/compile/resolve/_domain.py +49 -32
  69. dataface/core/compile/resolve/_kwargs.py +39 -12
  70. dataface/core/compile/resolve/_marks.py +8 -3
  71. dataface/core/compile/resolve/_palette.py +7 -3
  72. dataface/core/compile/resolve/_table.py +124 -33
  73. dataface/core/compile/resolve/area.py +48 -18
  74. dataface/core/compile/resolve/bar.py +245 -64
  75. dataface/core/compile/resolve/geo.py +10 -3
  76. dataface/core/compile/resolve/heatmap.py +16 -8
  77. dataface/core/compile/resolve/line.py +35 -12
  78. dataface/core/compile/resolve/pie.py +4 -1
  79. dataface/core/compile/resolve/pie_attachment.py +1 -1
  80. dataface/core/compile/resolve/scatter.py +25 -13
  81. dataface/core/compile/resolve/simple.py +25 -2
  82. dataface/core/compile/schema_renderers/prompt.py +1 -1
  83. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  84. dataface/core/compile/source_map.py +211 -28
  85. dataface/core/compile/style_cascade.py +126 -55
  86. dataface/core/compile/yaml_error_formatter.py +1 -1
  87. dataface/core/connections.py +10 -3
  88. dataface/core/dashboard.py +24 -18
  89. dataface/core/defaults/themes/_base.yaml +49 -39
  90. dataface/core/defaults/themes/cream.yaml +4 -4
  91. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  92. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  93. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  94. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  95. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  96. dataface/core/defaults/themes/editorial.yaml +6 -6
  97. dataface/core/defaults/themes/neon.yaml +15 -8
  98. dataface/core/defaults/themes/plain.yaml +6 -4
  99. dataface/core/defaults/themes/stark.yaml +16 -7
  100. dataface/core/defaults/themes/vivid.yaml +15 -8
  101. dataface/core/diagnostics/__init__.py +2 -2
  102. dataface/core/diagnostics/codes_compile.py +13 -31
  103. dataface/core/diagnostics/codes_execute.py +79 -0
  104. dataface/core/diagnostics/codes_render.py +87 -28
  105. dataface/core/diagnostics/diagnostic.py +30 -0
  106. dataface/core/diagnostics/hints.py +19 -0
  107. dataface/core/dialects/base.py +17 -0
  108. dataface/core/dialects/postgres.py +14 -0
  109. dataface/core/dialects/redshift.py +5 -0
  110. dataface/core/execute/__init__.py +59 -14
  111. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  112. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  113. dataface/core/execute/adapters/dbt_utils.py +84 -20
  114. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  115. dataface/core/execute/adapters/sql_adapter.py +72 -4
  116. dataface/core/execute/dbt_jinja.py +65 -10
  117. dataface/core/execute/executor.py +69 -16
  118. dataface/core/font_measure.py +28 -0
  119. dataface/core/fonts.py +8 -5
  120. dataface/core/inspect/__init__.py +46 -6
  121. dataface/core/project.py +13 -1
  122. dataface/core/render/chart/data_table_attachment.py +14 -12
  123. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  124. dataface/core/render/chart/emitters/_channels.py +56 -8
  125. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  126. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  127. dataface/core/render/chart/emitters/area.py +3 -3
  128. dataface/core/render/chart/emitters/bar.py +60 -35
  129. dataface/core/render/chart/emitters/geo.py +8 -13
  130. dataface/core/render/chart/emitters/heatmap.py +20 -26
  131. dataface/core/render/chart/emitters/line.py +2 -2
  132. dataface/core/render/chart/emitters/scatter.py +7 -30
  133. dataface/core/render/chart/features/baseline.py +22 -9
  134. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  135. dataface/core/render/chart/features/mirror_axis.py +12 -9
  136. dataface/core/render/chart/features/value_labels.py +1 -1
  137. dataface/core/render/chart/kpi.py +11 -90
  138. dataface/core/render/chart/presentation.py +6 -8
  139. dataface/core/render/chart/rendering.py +3 -12
  140. dataface/core/render/chart/session.py +10 -0
  141. dataface/core/render/chart/spec.py +3 -0
  142. dataface/core/render/chart/spec_builders.py +18 -18
  143. dataface/core/render/chart/table.py +81 -36
  144. dataface/core/render/chart/table_support.py +2 -53
  145. dataface/core/render/chart/time_unit_detect.py +3 -2
  146. dataface/core/render/chart/translate.py +82 -44
  147. dataface/core/render/chart/type_inference.py +45 -25
  148. dataface/core/render/chart/validation.py +91 -0
  149. dataface/core/render/chart/vega_lite.py +30 -11
  150. dataface/core/render/chart/vl_field_maps.py +111 -76
  151. dataface/core/render/converters/chart.py +16 -6
  152. dataface/core/render/faces.py +3 -2
  153. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  154. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  155. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  156. dataface/core/render/fonts/_font_face.css +10 -0
  157. dataface/core/render/layout_sizing.py +267 -238
  158. dataface/core/render/numeral_expr.py +80 -0
  159. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  160. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  161. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  162. dataface/core/render/warnings/palette_unsupported.py +6 -0
  163. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  164. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  165. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  166. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  167. dataface/core/render/warnings/redundant_encoding.py +20 -1
  168. dataface/core/render/warnings/registry.py +7 -3
  169. dataface/core/render/warnings/temporal_single_point.py +1 -0
  170. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  171. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  172. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  173. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  174. dataface/core/render_format.py +18 -0
  175. dataface/core/text/format_d3.py +56 -0
  176. dataface/core/text/numeral_scale.py +218 -0
  177. dataface/core/utils.py +103 -10
  178. dataface/data/schemas/board-resolved/board-resolved.schema.json +9487 -7978
  179. dataface/data/schemas/yaml/0.3.2.json +1 -0
  180. dataface/data/schemas/yaml/manifest.json +7 -0
  181. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/METADATA +1 -1
  182. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
  183. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
  184. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
  185. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.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):
@@ -204,6 +204,13 @@ def render_command(
204
204
  Returns:
205
205
  Output file path on success, None for terminal/stdout output
206
206
  """
207
+ from dataface.agent_api import ProjectSession
208
+ from dataface.agent_api._paths import (
209
+ build_face_render_context,
210
+ resolve_face_or_error,
211
+ )
212
+ from dataface.agent_api.cache import project_cache_ctx
213
+
207
214
  if ignore_codes:
208
215
  _validate_ignore_codes(ignore_codes)
209
216
 
@@ -296,6 +303,10 @@ def render_command_from_yaml(
296
303
  Output file path on success, None for terminal/stdout output. Exits 1
297
304
  (typer.Exit) after reporting on a validation/chart failure.
298
305
  """
306
+ from dataface.agent_api import InMemoryFace, ProjectSession
307
+ from dataface.agent_api._paths import build_yaml_render_context
308
+ from dataface.agent_api.cache import project_cache_ctx
309
+
299
310
  if ignore_codes:
300
311
  _validate_ignore_codes(ignore_codes)
301
312
 
@@ -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
 
@@ -68,86 +62,43 @@ def serve_command(
68
62
  None (exits with code 0 on success, 1 on errors)
69
63
  """
70
64
  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
65
+ from dataface.agent_api.serve import format_startup_failure, prepare_serve
86
66
 
87
67
  # Narrows the root callback's "cli": a served dashboard's queries are a
88
68
  # different cost story from a one-shot `dft render`.
89
69
  set_surface("serve")
90
70
 
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.
71
+ # Validate DFT_DEFAULT_THEME/dataface.yml, resolve port/dialect, and build the
72
+ # ASGI app — a dataface.yml validation failure raises pydantic's ValidationError
73
+ # uncaught (matches pre-existing behavior: a plain echo, not the Diagnostic
74
+ # envelope used everywhere else in this command).
115
75
  from pydantic import ValidationError as _PydanticError
116
76
 
117
77
  try:
118
- load_config(project)
78
+ setup = prepare_serve(
79
+ project,
80
+ port=port,
81
+ host=host,
82
+ dialect=dialect,
83
+ target=target,
84
+ max_workers=max_workers,
85
+ no_cache=no_cache,
86
+ cache_path=cache_path,
87
+ allow_external_access=allow_external_access,
88
+ )
119
89
  except _PydanticError as e:
120
90
  typer.echo(f"dataface.yml validation error: {e}", err=True)
121
91
  raise typer.Exit(1) from None
122
92
 
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
- )
93
+ if isinstance(setup, Diagnostic):
94
+ print_diagnostics([setup])
95
+ raise typer.Exit(1) from None
129
96
 
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
- )
97
+ if setup.dialect_inferred:
98
+ typer.echo(f" Auto-detected dialect: {setup.dialect} (from dbt profile)")
99
+
100
+ port = setup.port
101
+ app = setup.app
151
102
 
152
103
  # Start server
153
104
  typer.echo("🚀 Starting dashboard server")
@@ -175,6 +126,5 @@ def serve_command(
175
126
  typer.echo("\n👋 Server stopped")
176
127
  sys.exit(0)
177
128
  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()])
129
+ print_diagnostics([format_startup_failure(e)])
180
130
  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
 
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))
@@ -1,6 +1,8 @@
1
- """Color utility functions for Dataface renderers.
1
+ """CSS/SVG color-string validation.
2
2
 
3
- Provides color helpers for use across compile and render layers.
3
+ Lives in core (below compile/execute/render) so both compile (pie-attachment
4
+ table style resolution) and render (KPI/table SVG emission) can validate a
5
+ foreign color string without either reaching into the other's layer.
4
6
  """
5
7
 
6
8
  from __future__ import annotations
@@ -8,11 +10,24 @@ from __future__ import annotations
8
10
  import re
9
11
  from typing import overload
10
12
 
11
- from dataface.core.compile.errors import CompilationError
13
+ from dataface.core.diagnostics.base import DatafaceError
12
14
 
13
15
  _CSS_HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
14
16
 
15
17
 
18
+ class InvalidColorError(DatafaceError):
19
+ """A color string is neither a valid hex color nor a recognized CSS keyword."""
20
+
21
+ def __init__(self, message: str):
22
+ self.message = message
23
+ self.fields = {}
24
+ super().__init__(message)
25
+ if self.code is None:
26
+ from dataface.core.diagnostics.codes_unknown import ERR_INTERNAL
27
+
28
+ self.code = ERR_INTERNAL
29
+
30
+
16
31
  def is_sanitizable_color(color: str) -> bool:
17
32
  """Return True if ``color`` is accepted by sanitize_color (hex or transparent/none)."""
18
33
  return bool(_CSS_HEX_COLOR_PATTERN.match(color)) or color.lower() in {
@@ -39,7 +54,7 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
39
54
  inherited fill).
40
55
 
41
56
  ``fallback`` is returned when ``color`` is None or empty. A non-None
42
- ``color`` that is not a valid hex or keyword raises ``CompilationError``
57
+ ``color`` that is not a valid hex or keyword raises ``InvalidColorError``
43
58
  immediately — authored colors that are not valid are a configuration error,
44
59
  not a case for silent defaulting.
45
60
  """
@@ -49,4 +64,4 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
49
64
  return color
50
65
  if color.lower() in {"transparent", "none"}:
51
66
  return "transparent"
52
- raise CompilationError(f"Invalid color value: {color!r}")
67
+ raise InvalidColorError(f"Invalid color value: {color!r}")