dataface 0.1.6.dev82__py3-none-any.whl → 0.1.6.dev129__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 (36) hide show
  1. dataface/agent_api/_paths.py +7 -18
  2. dataface/agent_api/docs/yaml-reference.md +12 -11
  3. dataface/agent_api/project_session.py +3 -44
  4. dataface/agent_api/render_face.py +1 -14
  5. dataface/ai/context.py +1 -0
  6. dataface/cli/commands/render.py +2 -10
  7. dataface/core/compile/compiler.py +14 -10
  8. dataface/core/compile/config.py +10 -5
  9. dataface/core/compile/detect.py +32 -6
  10. dataface/core/compile/markdown.py +109 -18
  11. dataface/core/compile/models/chart/authored.py +25 -31
  12. dataface/core/compile/models/config.py +4 -0
  13. dataface/core/compile/models/style/theme.py +3 -1
  14. dataface/core/compile/yaml_error_formatter.py +102 -33
  15. dataface/core/defaults/default_config.yml +1 -0
  16. dataface/core/defaults/themes/stark.yaml +4 -0
  17. dataface/core/execute/adapters/adapter_registry.py +3 -8
  18. dataface/core/execute/duckdb_cache.py +4 -3
  19. dataface/core/project_roots.py +15 -35
  20. dataface/core/render/board_links.py +71 -18
  21. dataface/core/render/chart/geo.py +74 -11
  22. dataface/core/render/chart/profile.py +65 -17
  23. dataface/core/render/chart/table.py +4 -32
  24. dataface/core/render/chart/time_unit_detect.py +28 -19
  25. dataface/core/render/dir_context.py +5 -3
  26. dataface/core/render/faces.py +1 -2
  27. dataface/core/render/templates/nav/nav-fragment.html +1 -1
  28. dataface/core/render/templates/nav/nav.js +3 -3
  29. dataface/core/scoped_paths.py +30 -9
  30. dataface/core/serve/alias_index.py +9 -17
  31. dataface/core/serve/server.py +82 -37
  32. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev129.dist-info}/METADATA +1 -1
  33. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev129.dist-info}/RECORD +36 -36
  34. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev129.dist-info}/WHEEL +0 -0
  35. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev129.dist-info}/entry_points.txt +0 -0
  36. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev129.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,8 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
 
8
8
  from dataface.core.project_roots import (
9
- discover_render_context as discover_render_context,
10
- discovery_boundary_for_face as discovery_boundary_for_face,
11
9
  find_dft_root as find_dft_root,
10
+ find_project_root as find_project_root,
12
11
  )
13
12
  from dataface.core.scoped_paths import (
14
13
  resolve_scoped_path as resolve_scoped_path,
@@ -60,8 +59,8 @@ class FaceRenderContext:
60
59
  """Path resolution result from a face path + project root.
61
60
 
62
61
  Adapter registry is built by `ProjectSession.open`, not by the context — call sites
63
- open a `ProjectSession` with `project_root` + `dbt_project_path` and read the
64
- registry off `project.adapter_registry`.
62
+ open a `ProjectSession` with `project_root` and read the registry off
63
+ `project.adapter_registry`.
65
64
  """
66
65
 
67
66
  face_file: Path
@@ -69,18 +68,16 @@ class FaceRenderContext:
69
68
  scoped_base: Path
70
69
  project_root: Path
71
70
  output_dir: Path
72
- dbt_project_path: Path | None = None
73
71
 
74
72
 
75
73
  def build_face_render_context(
76
74
  face_path: Path,
77
75
  project_dir: Path,
78
76
  ) -> FaceRenderContext:
79
- """Resolve a face path and walk for dbt context.
77
+ """Resolve a face path against the given project root.
80
78
 
81
- ``project_dir`` is authoritative; the walk only contributes the dbt project
82
- path. Callers must resolve their project dir first (e.g. via
83
- ``resolve_project_dir(raw_dir)`` at the CLI boundary).
79
+ ``project_dir`` is authoritative. Callers must resolve their project dir first
80
+ (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
84
81
  """
85
82
  if face_path.is_absolute():
86
83
  face_file = face_path.resolve()
@@ -89,10 +86,6 @@ def build_face_render_context(
89
86
  else:
90
87
  face_file = (project_dir / face_path).resolve()
91
88
 
92
- _, dbt_project_path = discover_render_context(
93
- face_file.parent,
94
- discovery_boundary_for_face(face_file.parent, project_dir),
95
- )
96
89
  project_root = project_dir
97
90
 
98
91
  try:
@@ -109,7 +102,6 @@ def build_face_render_context(
109
102
  scoped_base=project_root,
110
103
  project_root=project_root,
111
104
  output_dir=project_root,
112
- dbt_project_path=dbt_project_path,
113
105
  )
114
106
 
115
107
 
@@ -122,21 +114,18 @@ class YamlRenderContext:
122
114
 
123
115
  project_root: Path
124
116
  output_dir: Path
125
- dbt_project_path: Path | None = None
126
117
 
127
118
 
128
119
  def build_yaml_render_context(
129
120
  project_dir: Path,
130
121
  ) -> YamlRenderContext:
131
- """Walk for dbt context anchored at the given project root.
122
+ """Resolve the project root for rendering inline YAML.
132
123
 
133
124
  ``project_dir`` is authoritative. Callers must resolve their project dir
134
125
  first (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
135
126
  """
136
127
  project_root = project_dir.resolve()
137
- _, dbt_project_path = discover_render_context(project_root, None)
138
128
  return YamlRenderContext(
139
129
  project_root=project_root,
140
130
  output_dir=project_root,
141
- dbt_project_path=dbt_project_path,
142
131
  )
@@ -128,8 +128,8 @@ Authored patch for bar and histogram charts.
128
128
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
129
129
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
130
130
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
131
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
132
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
131
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
132
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
133
133
  | `style` | [BarChartStyle](#barchartstyle) | ✓ | Chart-local style overrides. |
134
134
 
135
135
  <a id="linechart"></a>
@@ -156,8 +156,8 @@ Authored patch for line charts.
156
156
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
157
157
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
158
158
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
159
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
160
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
159
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
160
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
161
161
  | `style` | [LineChartStyle](#linechartstyle) | ✓ | Chart-local style overrides. |
162
162
 
163
163
  <a id="areachart"></a>
@@ -184,8 +184,8 @@ Authored patch for area charts.
184
184
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
185
185
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
186
186
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
187
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
188
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
187
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
188
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
189
189
  | `style` | [AreaChartStyle](#areachartstyle) | ✓ | Chart-local style overrides. |
190
190
 
191
191
  <a id="scatterchart"></a>
@@ -212,8 +212,8 @@ Authored patch for scatter charts.
212
212
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
213
213
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
214
214
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
215
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
216
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
215
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
216
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
217
217
  | `size` | str | ✓ | Field used to size-encode data points (quantitative). |
218
218
  | `shape` | str | ✓ | Field used to shape-encode data points (categorical). |
219
219
  | `style` | [ScatterChartStyle](#scatterchartstyle) | ✓ | Chart-local style overrides. |
@@ -242,8 +242,8 @@ Authored patch for heatmap charts.
242
242
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
243
243
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
244
244
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
245
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
246
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
245
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
246
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
247
247
  | `style` | [HeatmapChartStyle](#heatmapchartstyle) | ✓ | Chart-local style overrides. |
248
248
 
249
249
  <a id="piechart"></a>
@@ -2372,10 +2372,11 @@ Authored overlay for SparkAreaStyle.
2372
2372
 
2373
2373
  <a id="geoshapemarkstyle"></a>
2374
2374
  ## GeoshapeMarkStyle
2375
- Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark boundary stroke.
2375
+ Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark fill and boundary stroke.
2376
2376
 
2377
2377
  | Field | Type | Optional | Description |
2378
2378
  |-------|------|:--------:|-------------|
2379
+ | `fill` | str | ✓ | Neutral geoshape fill color. |
2379
2380
  | `stroke` | [StrokeStyle](#strokestyle) | ✓ | Geoshape boundary stroke style. |
2380
2381
 
2381
2382
  <a id="circlemarkstyle"></a>
@@ -17,7 +17,6 @@ else:
17
17
  # Verb-forwarder imports use SUBMODULES so test monkeypatches on the
18
18
  # module attribute affect the call site.
19
19
  from dataface.agent_api import (
20
- dashboards as _dashboards,
21
20
  describe as _describe,
22
21
  describe_query as _describe_query,
23
22
  query as _query,
@@ -42,7 +41,7 @@ from dataface.core.inspect.query_validator import (
42
41
  validate_query as _core_validate_query,
43
42
  )
44
43
  from dataface.core.project import Project
45
- from dataface.core.project_roots import discover_render_context
44
+ from dataface.core.project_roots import find_project_root
46
45
 
47
46
  if TYPE_CHECKING:
48
47
  from dataface.core.dashboard import RenderedDashboard, RenderFormat
@@ -71,7 +70,6 @@ class ProjectSession:
71
70
  cache: DuckDBCache | None
72
71
  _owns_registry: bool
73
72
  _read_only: bool
74
- _dbt_project_path: Path | None
75
73
  _connection_string: str | None
76
74
  _dialect: str
77
75
  _target: str
@@ -96,7 +94,6 @@ class ProjectSession:
96
94
  # short-circuit the build on first access.
97
95
  self.__dict__["adapter_registry"] = adapter_registry
98
96
  self._read_only = read_only
99
- self._dbt_project_path = None
100
97
  self._connection_string = None
101
98
  self._dialect = "duckdb"
102
99
  self._target = "dev"
@@ -111,7 +108,6 @@ class ProjectSession:
111
108
  *,
112
109
  cache: DuckDBCache | None = None,
113
110
  read_only: bool = True,
114
- dbt_project_path: Path | None = None,
115
111
  connection_string: str | None = None,
116
112
  dialect: str = "duckdb",
117
113
  target: str = "dev",
@@ -135,7 +131,6 @@ class ProjectSession:
135
131
  cache=cache,
136
132
  read_only=read_only,
137
133
  )
138
- session._dbt_project_path = dbt_project_path
139
134
  session._connection_string = connection_string
140
135
  session._dialect = dialect
141
136
  session._target = target
@@ -151,7 +146,6 @@ class ProjectSession:
151
146
  *,
152
147
  cache: DuckDBCache | None = None,
153
148
  read_only: bool = True,
154
- dbt_project_path: Path | None = None,
155
149
  connection_string: str | None = None,
156
150
  dialect: str = "duckdb",
157
151
  target: str = "dev",
@@ -166,7 +160,6 @@ class ProjectSession:
166
160
  this method stores it directly without re-wrapping.
167
161
  """
168
162
  session = cls(project=project, cache=cache, read_only=read_only)
169
- session._dbt_project_path = dbt_project_path
170
163
  session._connection_string = connection_string
171
164
  session._dialect = dialect
172
165
  session._target = target
@@ -185,13 +178,8 @@ class ProjectSession:
185
178
  ) -> Self:
186
179
  """Open a ProjectSession rooted at the project directory discovered upward from face_file."""
187
180
  resolved = Path(face_file).resolve()
188
- project_root, dbt_project_path = discover_render_context(resolved.parent, None)
189
- return cls.open(
190
- project_root,
191
- read_only=read_only,
192
- cache=cache,
193
- dbt_project_path=dbt_project_path,
194
- )
181
+ project_root = find_project_root(resolved.parent, boundary=None)
182
+ return cls.open(project_root, read_only=read_only, cache=cache)
195
183
 
196
184
  def __enter__(self) -> Self:
197
185
  return self
@@ -210,7 +198,6 @@ class ProjectSession:
210
198
  return build_adapter_registry(
211
199
  self.project,
212
200
  read_only=self._read_only,
213
- dbt_project_path=self._dbt_project_path,
214
201
  connection_string=self._connection_string,
215
202
  profile_type=self._dialect,
216
203
  target=self._target,
@@ -279,11 +266,6 @@ class ProjectSession:
279
266
 
280
267
  # ── Verb forwarders ──────────────────────────────────────────────────────
281
268
 
282
- def validate(self, face_path: Path) -> ValidateResult:
283
- result = _validate.validate(face_path, project=self.project)
284
- annotated = _validate.annotate_with_data_lint([result], project_session=self)
285
- return annotated[0]
286
-
287
269
  def validate_paths(self, paths: list[Path] | None) -> list[ValidateResult]:
288
270
  results = _validate.validate_paths(paths, project=self.project)
289
271
  return _validate.annotate_with_data_lint(results, project_session=self)
@@ -346,32 +328,9 @@ class ProjectSession:
346
328
  relationship_context=self._relationship_context,
347
329
  )
348
330
 
349
- def describe_face(self, path: Path) -> _describe.DescribeFaceResult:
350
- return _describe.describe_face(path, project=self.project)
351
-
352
331
  def describe_paths(self, paths: list[Path]) -> list[_describe.DescribeFaceResult]:
353
332
  return _describe.describe_paths(paths, project=self.project)
354
333
 
355
- def list_dashboards(
356
- self,
357
- directory: Path | None = None,
358
- recursive: bool = True,
359
- ) -> _dashboards.ListDashboardsResult:
360
- """When ``directory`` is None, defaults to ``self.project.root``. When provided, callers are responsible for passing an absolute path; relative paths resolve against the process working directory."""
361
- return _dashboards.list_dashboards(
362
- directory=directory if directory is not None else self.project.root,
363
- recursive=recursive,
364
- )
365
-
366
- def get_dashboard(
367
- self,
368
- path: Path,
369
- include_raw: bool = False,
370
- ) -> _dashboards.CompiledDashboard:
371
- return _dashboards.get_dashboard(
372
- path, include_raw=include_raw, project=self.project
373
- )
374
-
375
334
  def lookup_face_query_sql(
376
335
  self,
377
336
  name: str,
@@ -15,10 +15,6 @@ from dataface.core.compile.markdown import (
15
15
  )
16
16
  from dataface.core.execute.duckdb_cache import DuckDBCache
17
17
  from dataface.core.project import Project
18
- from dataface.core.project_roots import (
19
- discover_render_context,
20
- discovery_boundary_for_face,
21
- )
22
18
  from dataface.core.render.face_api import compile_and_render
23
19
 
24
20
 
@@ -72,16 +68,7 @@ def render_face(
72
68
  if project is not None:
73
69
  if warnings_ignore is None:
74
70
  warnings_ignore = project.warnings_ignore
75
- # Discover dbt_project.yml between the face and the project root.
76
- # build_adapter_registry's own fallback would only walk upward from
77
- # project.root, missing nested dbt projects under it.
78
- _, dbt_project_path = discover_render_context(
79
- face_file.parent,
80
- discovery_boundary_for_face(face_file.parent, project.root),
81
- )
82
- project_session = ProjectSession.open(
83
- project.root, cache=cache, dbt_project_path=dbt_project_path
84
- )
71
+ project_session = ProjectSession.open(project.root, cache=cache)
85
72
  else:
86
73
  if warnings_ignore is None:
87
74
  warnings_ignore = frozenset()
dataface/ai/context.py CHANGED
@@ -21,6 +21,7 @@ class DatafaceAIContext:
21
21
  # `dft chat` after binding; consumed by `_view_url` to build clickable
22
22
  # localhost URLs in `render_dashboard` responses (including as_link=True).
23
23
  server_port: int | None = None
24
+ prompt_context: dict[str, str] | None = None
24
25
 
25
26
  def resolve_dashboard_path(self, path: Path) -> Path:
26
27
  """Resolve a dashboard path inside the scoped dashboards directory.
@@ -129,11 +129,7 @@ def render_command(
129
129
 
130
130
  with (
131
131
  cache_from_env() as cache,
132
- ProjectSession.open(
133
- ctx.project_root,
134
- cache=cache,
135
- dbt_project_path=ctx.dbt_project_path,
136
- ) as project_session,
132
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
137
133
  ):
138
134
  result = project_session.render_dashboard(
139
135
  path=ctx.scoped_path,
@@ -219,11 +215,7 @@ def render_command_from_yaml(
219
215
 
220
216
  with (
221
217
  cache_from_env() as cache,
222
- ProjectSession.open(
223
- ctx.project_root,
224
- cache=cache,
225
- dbt_project_path=ctx.dbt_project_path,
226
- ) as project_session,
218
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
227
219
  ):
228
220
  result = project_session.render_dashboard(
229
221
  yaml_content=yaml_content,
@@ -271,9 +271,7 @@ def compile(
271
271
  yaml_content: YAML string to compile
272
272
  options: Optional compilation options
273
273
  base_dir: Base directory for resolving file references
274
- project_sources: Project-level sources, ready to use. Callers that
275
- hold a ``ProjectSession`` thread ``project_session.sources``;
276
- one-shot callers pre-load via ``load_project_sources(Project(project_dir))``.
274
+ project_sources: Project-level sources.
277
275
 
278
276
  Returns:
279
277
  CompileResult with compiled face or errors
@@ -440,6 +438,7 @@ def compile_file(
440
438
  apply_meta: bool = True,
441
439
  *,
442
440
  project: Project,
441
+ markdown_metadata_table: bool = False,
443
442
  ) -> CompileResult:
444
443
  """Compile a YAML file to a Face.
445
444
 
@@ -451,6 +450,8 @@ def compile_file(
451
450
  options: Optional compilation options
452
451
  apply_meta: If True, resolve and apply meta.yaml chain (default: True)
453
452
  project: Project for meta resolution and source loading
453
+ markdown_metadata_table: When True and file_path is a .md, prepend
454
+ non-face frontmatter keys as a metadata table before the body.
454
455
 
455
456
  Returns:
456
457
  CompileResult with compiled face or errors
@@ -471,13 +472,14 @@ def compile_file(
471
472
  )
472
473
 
473
474
  # Markdown report files: translate to YAML before compiling
474
- if file_path.suffix.lower() == ".md":
475
- from dataface.core.compile.markdown import (
476
- MARKDOWN_NOT_FACE_MESSAGE,
477
- is_markdown_face,
478
- markdown_to_yaml,
479
- )
475
+ from dataface.core.compile.markdown import (
476
+ MARKDOWN_NOT_FACE_MESSAGE,
477
+ MARKDOWN_SUFFIXES,
478
+ is_markdown_face,
479
+ markdown_to_yaml,
480
+ )
480
481
 
482
+ if file_path.suffix.lower() in MARKDOWN_SUFFIXES:
481
483
  if not is_markdown_face(file_path):
482
484
  return CompileResult(
483
485
  errors=[CompilationError(MARKDOWN_NOT_FACE_MESSAGE).to_structured()]
@@ -485,7 +487,9 @@ def compile_file(
485
487
 
486
488
  try:
487
489
  raw_text = file_path.read_text(encoding="utf-8")
488
- yaml_content = markdown_to_yaml(raw_text)
490
+ yaml_content = markdown_to_yaml(
491
+ raw_text, metadata_table=markdown_metadata_table
492
+ )
489
493
  except (OSError, ValueError) as e:
490
494
  return CompileResult(
491
495
  errors=[CompilationError(f"Markdown parse error: {e}").to_structured()]
@@ -200,6 +200,11 @@ def get_project_server_config(project: Project) -> ServerConfig:
200
200
  return ServerConfig.model_validate(_deep_merge(base, server_section))
201
201
 
202
202
 
203
+ def get_project_markdown_metadata_table(project: Project) -> bool:
204
+ """Return whether the project has the markdown_metadata_table flag enabled."""
205
+ return get_project_server_config(project).markdown_metadata_table
206
+
207
+
203
208
  def load_config(path: Path) -> Config:
204
209
  """Load configuration from a custom YAML file.
205
210
 
@@ -610,15 +615,15 @@ def load_project_sources(project: Project) -> ProjectSourcesConfig:
610
615
  config.default = yml_default
611
616
 
612
617
  # Absolutize paths now that all sources are merged
613
- config.sources = _absolutize_source_paths(config.sources, project.root)
618
+ config.sources = _absolutize_source_paths(config.sources, project)
614
619
  return config
615
620
 
616
621
 
617
622
  def _absolutize_source_paths(
618
623
  sources: dict[str, dict[str, Any]],
619
- project_dir: Path,
624
+ project: Project,
620
625
  ) -> dict[str, dict[str, Any]]:
621
- """Absolutize relative file/database paths against ``project_dir``."""
626
+ """Absolutize relative file/database paths against ``project.root``."""
622
627
  resolved: dict[str, dict[str, Any]] = {}
623
628
  for name, config in sources.items():
624
629
  cfg = dict(config)
@@ -630,11 +635,11 @@ def _absolutize_source_paths(
630
635
  and path_value != ":memory:"
631
636
  and not Path(path_value).is_absolute()
632
637
  ):
633
- cfg["path"] = str((project_dir / path_value).resolve())
638
+ cfg["path"] = str((project.root / path_value).resolve())
634
639
  elif source_type in {"csv", "parquet", "json"}:
635
640
  file_value = cfg.get("file")
636
641
  if isinstance(file_value, str) and not Path(file_value).is_absolute():
637
- cfg["file"] = str((project_dir / file_value).resolve())
642
+ cfg["file"] = str((project.root / file_value).resolve())
638
643
  resolved[name] = cfg
639
644
  return resolved
640
645
 
@@ -16,9 +16,21 @@ Detection methods (in order of precedence):
16
16
  4. Markdown reports: .md files with Dataface YAML frontmatter
17
17
  """
18
18
 
19
+ import os
19
20
  import re
20
21
  from pathlib import Path
21
22
 
23
+ _SKIP_SCAN_DIRS = {
24
+ ".git",
25
+ ".venv",
26
+ "__pycache__",
27
+ ".mypy_cache",
28
+ ".pytest_cache",
29
+ "build",
30
+ "dist",
31
+ "node_modules",
32
+ }
33
+
22
34
 
23
35
  def is_dataface_file(path: Path | str) -> bool:
24
36
  """
@@ -33,9 +45,9 @@ def is_dataface_file(path: Path | str) -> bool:
33
45
  path = Path(path)
34
46
 
35
47
  # Markdown report files
36
- if path.suffix.lower() == ".md":
37
- from dataface.core.compile.markdown import is_markdown_face
48
+ from dataface.core.compile.markdown import MARKDOWN_SUFFIXES, is_markdown_face
38
49
 
50
+ if path.suffix.lower() in MARKDOWN_SUFFIXES:
39
51
  return is_markdown_face(path)
40
52
 
41
53
  # Must be a YAML file
@@ -103,8 +115,22 @@ def find_dataface_files(directory: Path | str, recursive: bool = True) -> list[P
103
115
  List of paths to Dataface files
104
116
  """
105
117
  directory = Path(directory)
106
- yaml_pattern = "**/*.y*ml" if recursive else "*.y*ml"
107
- md_pattern = "**/*.md" if recursive else "*.md"
108
-
109
- candidates = list(directory.glob(yaml_pattern)) + list(directory.glob(md_pattern))
118
+ face_suffixes = (".yml", ".yaml", ".md", ".markdown")
119
+
120
+ if recursive:
121
+ candidates: list[Path] = []
122
+ for root, dirs, files in os.walk(directory, topdown=True):
123
+ dirs[:] = [d for d in dirs if d not in _SKIP_SCAN_DIRS]
124
+ root_path = Path(root)
125
+ candidates.extend(
126
+ root_path / name for name in files if name.endswith(face_suffixes)
127
+ )
128
+ else:
129
+ candidates = [
130
+ p
131
+ for p in directory.iterdir()
132
+ if p.is_file() and p.name.endswith(face_suffixes)
133
+ ]
134
+
135
+ candidates = sorted(candidates)
110
136
  return [p for p in candidates if is_dataface_file(p)]