dataface 0.1.6.dev34__py3-none-any.whl → 0.1.6.dev76__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 (41) hide show
  1. dataface/__init__.py +1 -2
  2. dataface/agent_api/_paths.py +23 -43
  3. dataface/agent_api/chat.py +13 -10
  4. dataface/agent_api/dashboards.py +4 -3
  5. dataface/agent_api/describe.py +11 -10
  6. dataface/agent_api/pack.py +13 -17
  7. dataface/agent_api/project_session.py +39 -13
  8. dataface/agent_api/query.py +8 -7
  9. dataface/agent_api/render_face.py +19 -15
  10. dataface/agent_api/validate.py +13 -13
  11. dataface/ai/llm.py +64 -17
  12. dataface/ai/mcp/server.py +3 -2
  13. dataface/ai/skills/dashboard-pack-scaffolding/SKILL.md +2 -1
  14. dataface/ai/tools/__init__.py +5 -5
  15. dataface/cli/commands/chat.py +5 -4
  16. dataface/cli/commands/render.py +8 -2
  17. dataface/core/__init__.py +1 -2
  18. dataface/core/compile/__init__.py +1 -1
  19. dataface/core/compile/compiler.py +6 -14
  20. dataface/core/dashboard.py +13 -9
  21. dataface/core/errors/__init__.py +2 -0
  22. dataface/core/errors/codes_execute.py +10 -0
  23. dataface/core/errors/registry.py +4 -2
  24. dataface/core/execute/adapters/adapter_registry.py +11 -10
  25. dataface/core/execute/adapters/csv_adapter.py +2 -0
  26. dataface/core/execute/executor.py +7 -3
  27. dataface/core/fonts.py +10 -0
  28. dataface/core/inspect/renderer.py +7 -11
  29. dataface/core/render/chart/profile.py +57 -23
  30. dataface/core/render/face_api.py +6 -6
  31. dataface/core/render/font_support.py +18 -6
  32. dataface/core/render/fonts/InterVariable-Italic.ttf +0 -0
  33. dataface/core/render/fonts/SourceSerif4-Italic.ttf +0 -0
  34. dataface/core/render/fonts/_emoji_font_face.css +14 -0
  35. dataface/core/serve/server.py +57 -68
  36. dataface/integrations/markdown.py +1 -1
  37. {dataface-0.1.6.dev34.dist-info → dataface-0.1.6.dev76.dist-info}/METADATA +1 -1
  38. {dataface-0.1.6.dev34.dist-info → dataface-0.1.6.dev76.dist-info}/RECORD +41 -39
  39. {dataface-0.1.6.dev34.dist-info → dataface-0.1.6.dev76.dist-info}/WHEEL +0 -0
  40. {dataface-0.1.6.dev34.dist-info → dataface-0.1.6.dev76.dist-info}/entry_points.txt +0 -0
  41. {dataface-0.1.6.dev34.dist-info → dataface-0.1.6.dev76.dist-info}/licenses/LICENSE +0 -0
dataface/__init__.py CHANGED
@@ -22,9 +22,8 @@ Quick Start:
22
22
  ... face = result.face
23
23
  ...
24
24
  ... # Create executor and render
25
- ... from dataface.core.compile.config import load_project_sources
26
25
  ... from dataface.core.project import Project
27
- ... registry = build_adapter_registry(Path.cwd(), project_sources=load_project_sources(Project(Path.cwd())))
26
+ ... registry = build_adapter_registry(Project(Path.cwd()))
28
27
  ... executor = Executor(face, registry, query_registry=result.query_registry)
29
28
  ... svg = render(face, executor, format="svg")
30
29
  """
@@ -11,7 +11,7 @@ from dataface.core.project_roots import (
11
11
  find_dft_root as find_dft_root,
12
12
  )
13
13
  from dataface.core.scoped_paths import (
14
- resolve_scoped_path as _core_resolve_scoped_path,
14
+ resolve_scoped_path as resolve_scoped_path,
15
15
  )
16
16
 
17
17
 
@@ -45,26 +45,12 @@ def resolve_project_dir_from_paths(
45
45
  return resolve_project_dir(None)
46
46
 
47
47
 
48
- def resolve_scoped_path(path: Path, project_dir: Path | None = None) -> Path:
49
- """agent_api wrapper: resolves `None → cwd-walked root` at the boundary,
50
- then delegates to `core.scoped_paths.resolve_scoped_path`.
51
-
52
- Absolute paths with no explicit `project_dir` resolve directly (no
53
- containment check) — the caller supplied the path, so no project context is
54
- needed to validate containment.
55
- """
56
- if path.is_absolute() and project_dir is None:
57
- return path.resolve()
58
- return _core_resolve_scoped_path(path, resolve_project_dir(project_dir))
59
-
60
-
61
- def no_project_hint(project_dir: Path | None) -> str:
48
+ def no_project_hint(project_root: Path) -> str:
62
49
  """Return a hint string when no project marker is found, else empty string."""
63
- check = project_dir if project_dir is not None else Path.cwd()
64
- if find_dft_root(check) is not None:
50
+ if find_dft_root(project_root) is not None:
65
51
  return ""
66
52
  return (
67
- f" No Dataface project marker found at or above {check}."
53
+ f" No Dataface project marker found at or above {project_root}."
68
54
  f" Run from inside a Dataface project, or set project_dir."
69
55
  )
70
56
 
@@ -79,7 +65,7 @@ class FaceRenderContext:
79
65
  """
80
66
 
81
67
  face_file: Path
82
- scoped_path: Path | None
68
+ scoped_path: Path
83
69
  scoped_base: Path
84
70
  project_root: Path
85
71
  output_dir: Path
@@ -88,37 +74,34 @@ class FaceRenderContext:
88
74
 
89
75
  def build_face_render_context(
90
76
  face_path: Path,
91
- project_dir: Path | None = None,
77
+ project_dir: Path,
92
78
  ) -> FaceRenderContext:
93
79
  """Resolve a face path and walk for dbt context.
94
80
 
95
- ``project_dir=None`` means "walk freely from the face's parent" — used by
96
- the CLI when ``--project-dir`` is omitted so a face under a dbt sub-project
97
- still anchors on that sub-project's root. A given ``project_dir`` is
98
- authoritative; the walk only contributes the dbt project path.
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).
99
84
  """
100
85
  if face_path.is_absolute():
101
86
  face_file = face_path.resolve()
102
87
  elif ".." in face_path.parts:
103
88
  face_file = (Path.cwd() / face_path).resolve()
104
89
  else:
105
- anchor = (
106
- project_dir.resolve()
107
- if project_dir is not None
108
- else resolve_project_dir(None)
109
- )
110
- face_file = (anchor / face_path).resolve()
111
-
112
- walk_root, dbt_project_path = discover_render_context(
90
+ face_file = (project_dir / face_path).resolve()
91
+
92
+ _, dbt_project_path = discover_render_context(
113
93
  face_file.parent,
114
94
  discovery_boundary_for_face(face_file.parent, project_dir),
115
95
  )
116
- project_root = project_dir.resolve() if project_dir is not None else walk_root
96
+ project_root = project_dir
117
97
 
118
98
  try:
119
- scoped_path: Path | None = face_file.relative_to(project_root)
99
+ scoped_path: Path = face_file.relative_to(project_root)
120
100
  except ValueError:
121
- scoped_path = face_file
101
+ raise ValueError(
102
+ f"Face file {face_file} is outside project_dir {project_root}. "
103
+ f"Pass an explicit --project-dir that contains the face file."
104
+ ) from None
122
105
 
123
106
  return FaceRenderContext(
124
107
  face_file=face_file,
@@ -143,18 +126,15 @@ class YamlRenderContext:
143
126
 
144
127
 
145
128
  def build_yaml_render_context(
146
- project_dir: Path | None = None,
129
+ project_dir: Path,
147
130
  ) -> YamlRenderContext:
148
131
  """Walk for dbt context anchored at the given project root.
149
132
 
150
- ``project_dir=None`` walks from cwd to discover the project root; a given
151
- ``project_dir`` is authoritative.
133
+ ``project_dir`` is authoritative. Callers must resolve their project dir
134
+ first (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
152
135
  """
153
- anchor = (
154
- project_dir.resolve() if project_dir is not None else resolve_project_dir(None)
155
- )
156
- walk_root, dbt_project_path = discover_render_context(anchor, None)
157
- project_root = anchor if project_dir is not None else walk_root
136
+ project_root = project_dir.resolve()
137
+ _, dbt_project_path = discover_render_context(project_root, None)
158
138
  return YamlRenderContext(
159
139
  project_root=project_root,
160
140
  output_dir=project_root,
@@ -24,6 +24,7 @@ from dataface.ai.agent import run_agent
24
24
  from dataface.ai.context import DatafaceAIContext
25
25
  from dataface.ai.events import AgentEvent
26
26
  from dataface.ai.llm import create_client
27
+ from dataface.core.project import Project
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from dataface.ai.external_mcp import ExternalMCPManager
@@ -71,26 +72,27 @@ class ChatSessionSummary:
71
72
  def start_session(
72
73
  *,
73
74
  model: str | None = None,
74
- project_dir: Path | None = None,
75
+ project: Project,
75
76
  server_port: int | None = None,
76
77
  ) -> ChatSession:
77
78
  """Create a new chat session.
78
79
 
79
80
  Args:
80
81
  model: LLM model name, optionally provider-prefixed.
81
- project_dir: Working directory for the session. Defaults to cwd.
82
+ project: The Dataface project for this session.
82
83
  server_port: Port of the embedded HTTP preview server, if running.
83
84
  """
84
85
  from dataface.agent_api.project_session import ProjectSession
85
86
  from dataface.core.execute.adapters import LOCAL_AUTHORING_REGISTRY_KWARGS
86
87
 
87
- cwd = (project_dir or Path.cwd()).resolve()
88
88
  client = create_client(model=model)
89
89
  context = DatafaceAIContext(
90
- project_session=ProjectSession.open(cwd, **LOCAL_AUTHORING_REGISTRY_KWARGS),
90
+ project_session=ProjectSession.from_project(
91
+ project, **LOCAL_AUTHORING_REGISTRY_KWARGS
92
+ ),
91
93
  server_port=server_port,
92
94
  )
93
- writer, _ = new_session(cwd, provider=client.provider, model=client.model)
95
+ writer, _ = new_session(project.root, provider=client.provider, model=client.model)
94
96
  return ChatSession(
95
97
  session_id=writer.session_id,
96
98
  messages=[],
@@ -106,7 +108,7 @@ def start_session(
106
108
  def resume_session(
107
109
  session_id: str,
108
110
  *,
109
- project_dir: Path | None = None,
111
+ project: Project,
110
112
  server_port: int | None = None,
111
113
  model: str | None = None,
112
114
  max_tokens: int = 32_000,
@@ -115,7 +117,7 @@ def resume_session(
115
117
 
116
118
  Args:
117
119
  session_id: The session id to resume.
118
- project_dir: Working directory for the resumed session. Defaults to cwd.
120
+ project: The Dataface project for the resumed session.
119
121
  server_port: Port of the embedded HTTP preview server, if running.
120
122
  model: LLM model to use. Defaults to the same provider as the session.
121
123
  max_tokens: Token budget for trimming old messages. 0 disables trimming.
@@ -126,7 +128,6 @@ def resume_session(
126
128
  from dataface.agent_api.project_session import ProjectSession
127
129
  from dataface.core.execute.adapters import LOCAL_AUTHORING_REGISTRY_KWARGS
128
130
 
129
- cwd = (project_dir or Path.cwd()).resolve()
130
131
  client = create_client(model=model)
131
132
 
132
133
  messages, meta, _ = load_session_for_resume(
@@ -143,10 +144,12 @@ def resume_session(
143
144
  )
144
145
 
145
146
  context = DatafaceAIContext(
146
- project_session=ProjectSession.open(cwd, **LOCAL_AUTHORING_REGISTRY_KWARGS),
147
+ project_session=ProjectSession.from_project(
148
+ project, **LOCAL_AUTHORING_REGISTRY_KWARGS
149
+ ),
147
150
  server_port=server_port,
148
151
  )
149
- writer, _ = new_session(cwd, provider=client.provider, model=client.model)
152
+ writer, _ = new_session(project.root, provider=client.provider, model=client.model)
150
153
  return ChatSession(
151
154
  session_id=session_id,
152
155
  messages=messages,
@@ -18,6 +18,7 @@ from dataface.core.compile import Face, compile_file
18
18
  from dataface.core.compile.errors import DatafaceError
19
19
  from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
20
20
  from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
21
+ from dataface.core.project import Project
21
22
  from dataface.core.render.warnings.base import RenderWarning
22
23
 
23
24
  # ---------------------------------------------------------------------------
@@ -193,12 +194,12 @@ def list_dashboards(
193
194
  def get_dashboard(
194
195
  path: Path,
195
196
  *,
196
- project_dir: Path,
197
+ project: Project,
197
198
  include_raw: bool = False,
198
199
  ) -> CompiledDashboard:
199
200
  """Get the compiled structure of a dashboard."""
200
201
  try:
201
- file_path = resolve_scoped_path(path, project_dir)
202
+ file_path = resolve_scoped_path(path, project.root)
202
203
  except ValueError as exc:
203
204
  return CompiledDashboard(
204
205
  success=False,
@@ -243,7 +244,7 @@ def get_dashboard(
243
244
  ],
244
245
  )
245
246
 
246
- result = compile_file(file_path)
247
+ result = compile_file(file_path, project=project)
247
248
  return CompiledDashboard(
248
249
  success=result.success,
249
250
  dashboard=result.face if result.success else None,
@@ -21,6 +21,7 @@ from dataface.core.compile.models.query.normalized import (
21
21
  ValuesQuery,
22
22
  )
23
23
  from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
24
+ from dataface.core.project import Project
24
25
 
25
26
  # Chart-encoding fields surfaced to agents. Covers the stable channel fields
26
27
  # across chart families. `label` is excluded — it's a KPI-specific render hint,
@@ -190,14 +191,14 @@ def _encoding_for_chart(chart: Chart) -> dict[str, Any]:
190
191
  # ---------------------------------------------------------------------------
191
192
 
192
193
 
193
- def describe_face(path: Path, *, project_dir: Path) -> DescribeFaceResult:
194
+ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
194
195
  """Describe the structure of a face: queries, charts, variables, layout."""
195
196
  from dataface.agent_api._paths import no_project_hint, resolve_scoped_path
196
197
  from dataface.core.compile.compiler import compile_file
197
198
  from dataface.core.compile.errors import DatafaceError
198
199
 
199
200
  try:
200
- resolved = resolve_scoped_path(path, project_dir)
201
+ resolved = resolve_scoped_path(path, project.root)
201
202
  except ValueError as exc:
202
203
  return DescribeFaceResult(
203
204
  success=False,
@@ -210,7 +211,7 @@ def describe_face(path: Path, *, project_dir: Path) -> DescribeFaceResult:
210
211
  )
211
212
 
212
213
  if not resolved.exists():
213
- hint = no_project_hint(project_dir)
214
+ hint = no_project_hint(project.root)
214
215
  return DescribeFaceResult(
215
216
  success=False,
216
217
  path=path,
@@ -223,7 +224,7 @@ def describe_face(path: Path, *, project_dir: Path) -> DescribeFaceResult:
223
224
  )
224
225
 
225
226
  try:
226
- result = compile_file(resolved)
227
+ result = compile_file(resolved, project=project)
227
228
  except OSError as exc:
228
229
  return DescribeFaceResult(
229
230
  success=False,
@@ -305,7 +306,7 @@ def describe_face(path: Path, *, project_dir: Path) -> DescribeFaceResult:
305
306
  def describe_paths(
306
307
  paths: list[Path],
307
308
  *,
308
- project_dir: Path,
309
+ project: Project,
309
310
  ) -> list[DescribeFaceResult]:
310
311
  """Describe N face files / directories.
311
312
 
@@ -315,13 +316,13 @@ def describe_paths(
315
316
  """
316
317
  out: list[DescribeFaceResult] = []
317
318
  for p in paths:
318
- out.extend(_describe_one_path(p, project_dir))
319
+ out.extend(_describe_one_path(p, project))
319
320
  return out
320
321
 
321
322
 
322
323
  def _describe_one_path(
323
324
  path: Path,
324
- project_dir: Path,
325
+ project: Project,
325
326
  ) -> list[DescribeFaceResult]:
326
327
  """Per-argv expansion: file → [one], dir → walk."""
327
328
  # WHY: dataface.core.inspect.manifest_utils triggers the inspect package
@@ -332,7 +333,7 @@ def _describe_one_path(
332
333
  from dataface.core.inspect.manifest_utils import INSPECT_TEMPLATE_MANIFEST
333
334
 
334
335
  try:
335
- resolved = resolve_scoped_path(path, project_dir)
336
+ resolved = resolve_scoped_path(path, project.root)
336
337
  except ValueError as exc:
337
338
  return [
338
339
  DescribeFaceResult(
@@ -347,7 +348,7 @@ def _describe_one_path(
347
348
  ]
348
349
 
349
350
  if not resolved.is_dir():
350
- return [describe_face(resolved, project_dir=project_dir)]
351
+ return [describe_face(resolved, project=project)]
351
352
 
352
353
  template_dirs = {m.parent for m in resolved.glob(f"**/{INSPECT_TEMPLATE_MANIFEST}")}
353
354
  yaml_files = sorted(
@@ -368,4 +369,4 @@ def _describe_one_path(
368
369
  ],
369
370
  )
370
371
  ]
371
- return [describe_face(f, project_dir=project_dir) for f in yaml_files]
372
+ return [describe_face(f, project=project) for f in yaml_files]
@@ -18,7 +18,6 @@ from pathlib import Path
18
18
  import yaml
19
19
  from pydantic import BaseModel
20
20
 
21
- from dataface.core.compile.config import load_project_sources
22
21
  from dataface.core.execute.adapters import AdapterRegistry, build_adapter_registry
23
22
  from dataface.core.inspect.resolver import LayeredSchemaResolver
24
23
  from dataface.core.pack.models import PackProposal, ProposedDashboard
@@ -140,16 +139,16 @@ def _collect_source_entries(
140
139
 
141
140
 
142
141
  def propose_pack(
143
- project_dir: Path,
142
+ project: Project,
144
143
  mode: str | None = None,
145
144
  ) -> tuple[PackProposal, Path]:
146
145
  """Read schema via the resolver and emit a deterministic PackProposal.
147
146
 
148
147
  The proposal is written to
149
- ``<project_dir>/target/dataface/proposals/<slug>/proposal.yml``.
148
+ ``<project.root>/target/dataface/proposals/<slug>/proposal.yml``.
150
149
 
151
150
  Args:
152
- project_dir: Root of the Dataface project (contains ``dataface.yml``).
151
+ project: Dataface project (root must contain ``dataface.yml``).
153
152
  mode: Organization mode override (``"connector-first"``,
154
153
  ``"domain-first"``, or ``"hybrid"``). When ``None``, the planner
155
154
  chooses automatically.
@@ -160,17 +159,15 @@ def propose_pack(
160
159
  of the written YAML file.
161
160
 
162
161
  Raises:
163
- FileNotFoundError: When *project_dir* does not exist.
162
+ FileNotFoundError: When ``project.root`` does not exist.
164
163
  ValueError: When no SQL sources are configured in the project, or when
165
164
  all configured sources are unreachable.
166
165
  """
167
- project_dir = project_dir.resolve()
166
+ project_dir = project.root
168
167
  if not project_dir.exists():
169
168
  raise FileNotFoundError(f"Project directory not found: {project_dir}")
170
169
 
171
- registry = build_adapter_registry(
172
- project_dir, project_sources=load_project_sources(Project(project_dir))
173
- )
170
+ registry = build_adapter_registry(project)
174
171
  sql_sources = registry.list_sql_sources()
175
172
 
176
173
  if not sql_sources:
@@ -409,7 +406,7 @@ def _validate_scaffold_targets(
409
406
 
410
407
  def apply_proposal(
411
408
  proposal: PackProposal,
412
- project_dir: Path,
409
+ project: Project,
413
410
  overwrite: bool = False,
414
411
  ) -> ScaffoldResult:
415
412
  """Apply an approved PackProposal, writing a sparse ``faces/`` folder tree.
@@ -426,7 +423,7 @@ def apply_proposal(
426
423
 
427
424
  Args:
428
425
  proposal: An approved :class:`~dataface.core.pack.models.PackProposal`.
429
- project_dir: Dataface project root (the directory containing
426
+ project: Dataface project (root is the directory containing
430
427
  ``dataface.yml``).
431
428
  overwrite: When ``False`` (default), existing non-empty files are
432
429
  skipped and recorded in ``ScaffoldResult.skipped_files``. When
@@ -434,24 +431,23 @@ def apply_proposal(
434
431
 
435
432
  Returns:
436
433
  A :class:`ScaffoldResult` with ``created_files``, ``skipped_files``,
437
- and ``errors`` (relative to ``project_dir``).
434
+ and ``errors`` (relative to ``project.root``).
438
435
 
439
436
  Raises:
440
437
  ValueError: When any generated file fails ``validate_paths()``.
441
438
  """
442
439
  from dataface.agent_api.validate import validate_paths
443
440
 
444
- project_dir = project_dir.resolve()
445
441
  result = ScaffoldResult()
446
442
  validated_paths: list[Path] = []
447
443
  partial_targets, landing_targets, dashboard_targets = _validate_scaffold_targets(
448
444
  proposal,
449
- project_dir,
445
+ project.root,
450
446
  )
451
447
 
452
448
  # Ensure base dirs exist
453
- (project_dir / "faces").mkdir(exist_ok=True)
454
- (project_dir / "faces" / "partials").mkdir(exist_ok=True)
449
+ (project.root / "faces").mkdir(exist_ok=True)
450
+ (project.root / "faces" / "partials").mkdir(exist_ok=True)
455
451
 
456
452
  # Write partials — skipped by validate_paths (_*.yml convention)
457
453
  for partial_filename, partial_path, rel in partial_targets:
@@ -490,7 +486,7 @@ def apply_proposal(
490
486
 
491
487
  # Validate gate — validate all newly written face files
492
488
  if validated_paths:
493
- validate_results = validate_paths(validated_paths, project_dir=project_dir)
489
+ validate_results = validate_paths(validated_paths, project=project)
494
490
  for vr in validate_results:
495
491
  if not vr.success:
496
492
  for err in vr.errors:
@@ -144,6 +144,37 @@ class ProjectSession:
144
144
  session._resolver = resolver
145
145
  return session
146
146
 
147
+ @classmethod
148
+ def from_project(
149
+ cls,
150
+ project: Project,
151
+ *,
152
+ cache: DuckDBCache | None = None,
153
+ read_only: bool = True,
154
+ dbt_project_path: Path | None = None,
155
+ connection_string: str | None = None,
156
+ dialect: str = "duckdb",
157
+ target: str = "dev",
158
+ duckdb_config: dict[str, Any] | None = None,
159
+ allow_external_access_in_readonly: bool = False,
160
+ resolver: SourceResolver | None = None,
161
+ ) -> Self:
162
+ """Construct a ProjectSession from a pre-built Project.
163
+
164
+ Use this when the caller already holds a ``project: Project`` and does
165
+ not want to re-resolve the path. The caller owns the Project lifecycle;
166
+ this method stores it directly without re-wrapping.
167
+ """
168
+ session = cls(project=project, cache=cache, read_only=read_only)
169
+ session._dbt_project_path = dbt_project_path
170
+ session._connection_string = connection_string
171
+ session._dialect = dialect
172
+ session._target = target
173
+ session._duckdb_config = duckdb_config
174
+ session._allow_external_access_in_readonly = allow_external_access_in_readonly
175
+ session._resolver = resolver
176
+ return session
177
+
147
178
  @classmethod
148
179
  def from_face(
149
180
  cls,
@@ -177,8 +208,7 @@ class ProjectSession:
177
208
  When constructed with an injected registry, returns it directly.
178
209
  """
179
210
  return build_adapter_registry(
180
- self.project.root,
181
- project_sources=self.project.sources,
211
+ self.project,
182
212
  read_only=self._read_only,
183
213
  dbt_project_path=self._dbt_project_path,
184
214
  connection_string=self._connection_string,
@@ -246,12 +276,12 @@ class ProjectSession:
246
276
  # ── Verb forwarders ──────────────────────────────────────────────────────
247
277
 
248
278
  def validate(self, face_path: Path) -> ValidateResult:
249
- result = _validate.validate(face_path, project_dir=self.project.root)
279
+ result = _validate.validate(face_path, project=self.project)
250
280
  annotated = _validate.annotate_with_data_lint([result], project_session=self)
251
281
  return annotated[0]
252
282
 
253
283
  def validate_paths(self, paths: list[Path] | None) -> list[ValidateResult]:
254
- results = _validate.validate_paths(paths, project_dir=self.project.root)
284
+ results = _validate.validate_paths(paths, project=self.project)
255
285
  return _validate.annotate_with_data_lint(results, project_session=self)
256
286
 
257
287
  def _source_names(self) -> frozenset[str]:
@@ -313,10 +343,10 @@ class ProjectSession:
313
343
  )
314
344
 
315
345
  def describe_face(self, path: Path) -> _describe.DescribeFaceResult:
316
- return _describe.describe_face(path, project_dir=self.project.root)
346
+ return _describe.describe_face(path, project=self.project)
317
347
 
318
348
  def describe_paths(self, paths: list[Path]) -> list[_describe.DescribeFaceResult]:
319
- return _describe.describe_paths(paths, project_dir=self.project.root)
349
+ return _describe.describe_paths(paths, project=self.project)
320
350
 
321
351
  def list_dashboards(
322
352
  self,
@@ -335,7 +365,7 @@ class ProjectSession:
335
365
  include_raw: bool = False,
336
366
  ) -> _dashboards.CompiledDashboard:
337
367
  return _dashboards.get_dashboard(
338
- path, include_raw=include_raw, project_dir=self.project.root
368
+ path, include_raw=include_raw, project=self.project
339
369
  )
340
370
 
341
371
  def lookup_face_query_sql(
@@ -343,9 +373,7 @@ class ProjectSession:
343
373
  name: str,
344
374
  path: Path,
345
375
  ) -> _query.FaceQueryLookupResult:
346
- return _query.lookup_face_query_sql(
347
- name=name, path=path, project_dir=self.project.root
348
- )
376
+ return _query.lookup_face_query_sql(name=name, path=path, project=self.project)
349
377
 
350
378
  def query_face(
351
379
  self,
@@ -357,7 +385,7 @@ class ProjectSession:
357
385
  return _query.query_face(
358
386
  name,
359
387
  path,
360
- project_dir=self.project.root,
388
+ project=self.project,
361
389
  vars=vars,
362
390
  limit=limit,
363
391
  adapter_registry=self.adapter_registry,
@@ -412,7 +440,6 @@ class ProjectSession:
412
440
  yaml_content=yaml_content,
413
441
  variables=variables,
414
442
  adapter_registry=self.adapter_registry,
415
- project_dir=self.project.root,
416
443
  project=self.project,
417
444
  duckdb_cache=self.cache,
418
445
  format=format,
@@ -422,7 +449,6 @@ class ProjectSession:
422
449
  scale=scale,
423
450
  ignore_codes=ignore_codes,
424
451
  max_workers=max_workers,
425
- warnings_ignore=self.warnings_ignore,
426
452
  link_context=link_context,
427
453
  **render_options,
428
454
  )
@@ -10,6 +10,7 @@ from dataface.core.compile import compile_file
10
10
  from dataface.core.compile.models.query.normalized import SqlQuery
11
11
  from dataface.core.execute.adapters import AdapterRegistry
12
12
  from dataface.core.inspect.query_validator import validate_query
13
+ from dataface.core.project import Project
13
14
  from dataface.core.validate import normalize_data_for_json
14
15
 
15
16
  MAX_QUERY_LIMIT = 1000
@@ -29,7 +30,7 @@ def lookup_face_query_sql(
29
30
  name: str,
30
31
  path: Path,
31
32
  *,
32
- project_dir: Path,
33
+ project: Project,
33
34
  ) -> FaceQueryLookupResult:
34
35
  """Compile a face file and extract the SQL + source for one named SQL query.
35
36
 
@@ -37,7 +38,7 @@ def lookup_face_query_sql(
37
38
  executing it against a warehouse.
38
39
  """
39
40
  try:
40
- file_path = resolve_scoped_path(path, project_dir)
41
+ file_path = resolve_scoped_path(path, project.root)
41
42
  except ValueError as e:
42
43
  return FaceQueryLookupResult(success=False, errors=[str(e)])
43
44
 
@@ -46,7 +47,7 @@ def lookup_face_query_sql(
46
47
  success=False, errors=[f"File not found: {file_path}"]
47
48
  )
48
49
 
49
- compile_result = compile_file(file_path)
50
+ compile_result = compile_file(file_path, project=project)
50
51
  if not compile_result.success:
51
52
  return FaceQueryLookupResult(
52
53
  success=False, errors=[e.message for e in compile_result.errors]
@@ -248,7 +249,7 @@ def _fail(
248
249
  def query_face(
249
250
  name: str,
250
251
  path: Path,
251
- project_dir: Path | None = None,
252
+ project: Project,
252
253
  vars: dict[str, Any] | None = None,
253
254
  limit: int = 20,
254
255
  *,
@@ -258,17 +259,17 @@ def query_face(
258
259
  limit = min(limit, MAX_QUERY_LIMIT)
259
260
 
260
261
  try:
261
- file_path = resolve_scoped_path(path, project_dir)
262
+ file_path = resolve_scoped_path(path, project.root)
262
263
  except ValueError as e:
263
264
  return _fail(name, path, [str(e)])
264
265
 
265
266
  if not file_path.exists():
266
267
  from dataface.agent_api._paths import no_project_hint
267
268
 
268
- hint = no_project_hint(project_dir)
269
+ hint = no_project_hint(project.root)
269
270
  return _fail(name, file_path, [f"File not found: {file_path}{hint}"])
270
271
 
271
- compile_result = compile_file(file_path)
272
+ compile_result = compile_file(file_path, project=project)
272
273
  if not compile_result.success:
273
274
  return _fail(name, file_path, [e.message for e in compile_result.errors])
274
275