dataface 0.2.1.dev647__py3-none-any.whl → 0.2.1.dev691__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.
- dataface/_docs_site.py +1 -3
- dataface/_install_hint.py +2 -2
- dataface/agent_api/_paths.py +78 -27
- dataface/agent_api/cache.py +7 -18
- dataface/agent_api/data_paths.py +1 -3
- dataface/agent_api/describe.py +36 -22
- dataface/agent_api/docs/yaml-reference.md +0 -1
- dataface/agent_api/project_session.py +1 -1
- dataface/agent_api/query.py +10 -5
- dataface/agent_api/validate.py +46 -37
- dataface/ai/agent.py +13 -15
- dataface/ai/context.py +1 -1
- dataface/ai/llm.py +4 -2
- dataface/ai/prompts.py +2 -2
- dataface/ai/tool_schemas.py +6 -6
- dataface/ai/tools/__init__.py +1 -11
- dataface/cli/_extras.py +12 -20
- dataface/cli/commands/init.py +20 -17
- dataface/cli/commands/mcp.py +53 -0
- dataface/cli/commands/render.py +106 -75
- dataface/cli/filesystem_project.py +4 -0
- dataface/cli/main.py +33 -129
- dataface/core/compile/config.py +4 -0
- dataface/core/compile/models/cache.py +44 -3
- dataface/core/compile/models/face/authored.py +0 -8
- dataface/core/compile/normalize_queries.py +28 -10
- dataface/core/execute/_duckdb_cache_base.py +79 -19
- dataface/core/execute/adapters/adapter_registry.py +2 -2
- dataface/core/execute/cache_backend.py +28 -0
- dataface/core/execute/executor.py +74 -21
- dataface/core/execute/trivial_local_cache.py +2 -2
- dataface/core/project.py +13 -0
- dataface/core/project_roots.py +0 -5
- dataface/core/render/chart/auto_link.py +218 -63
- dataface/core/render/chart/emitters/_overlay.py +69 -28
- dataface/core/render/chart/rendering.py +2 -12
- dataface/core/render/chart/sql_grain.py +571 -0
- dataface/core/render/dir_context.py +3 -3
- dataface/core/render/faces.py +29 -10
- dataface/core/render/svg_utils.py +22 -0
- dataface/core/serve/embedded.py +14 -13
- dataface/core/serve/server.py +13 -19
- dataface/data/highlighting/face/v1.json +0 -1
- {dataface-0.2.1.dev647.dist-info → dataface-0.2.1.dev691.dist-info}/METADATA +1 -14
- {dataface-0.2.1.dev647.dist-info → dataface-0.2.1.dev691.dist-info}/RECORD +48 -53
- dataface/agent_api/_session_store.py +0 -489
- dataface/agent_api/_state.py +0 -28
- dataface/agent_api/chat.py +0 -224
- dataface/ai/external_mcp.py +0 -610
- dataface/cli/commands/_agent_input.py +0 -212
- dataface/cli/commands/_agent_server.py +0 -126
- dataface/cli/commands/chat.py +0 -774
- {dataface-0.2.1.dev647.dist-info → dataface-0.2.1.dev691.dist-info}/WHEEL +0 -0
- {dataface-0.2.1.dev647.dist-info → dataface-0.2.1.dev691.dist-info}/entry_points.txt +0 -0
- {dataface-0.2.1.dev647.dist-info → dataface-0.2.1.dev691.dist-info}/licenses/LICENSE +0 -0
dataface/_docs_site.py
CHANGED
|
@@ -12,9 +12,7 @@ def docs_site_url() -> str:
|
|
|
12
12
|
|
|
13
13
|
``DFT_DOCS_URL`` overrides the shipped default when set to a non-empty
|
|
14
14
|
value. Used by ``dft docs`` (web link on the topic index) and by structured
|
|
15
|
-
error ``doc_url``s.
|
|
16
|
-
and hides its ``docs:`` links when unset, so it does not inherit this
|
|
17
|
-
default — see ``dataface_playground.routes.get_runtime_config``.
|
|
15
|
+
error ``doc_url``s.
|
|
18
16
|
"""
|
|
19
17
|
raw = os.environ.get("DFT_DOCS_URL", DEFAULT_DOCS_SITE_URL).strip()
|
|
20
18
|
if not raw:
|
dataface/_install_hint.py
CHANGED
|
@@ -57,8 +57,8 @@ def install_hint(extra: str | None = None) -> str:
|
|
|
57
57
|
"""Return the install one-liner that matches how this Dataface got installed.
|
|
58
58
|
|
|
59
59
|
``extra`` is an optional optional-dependency group (``"mcp"``,
|
|
60
|
-
``"
|
|
61
|
-
|
|
60
|
+
``"playground"``, ``"bigquery"``); when set, the returned command
|
|
61
|
+
installs Dataface with that extras bracket.
|
|
62
62
|
"""
|
|
63
63
|
spec = f"dataface[{extra}]" if extra else "dataface"
|
|
64
64
|
if _is_uv_tool_install():
|
dataface/agent_api/_paths.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
8
|
|
|
9
9
|
# tach-ignore(agent_api->cli: runtime host-type guard in no_project_hint; the local-filesystem check needs a non-cli signal on Project — deferred)
|
|
10
10
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
@@ -75,40 +75,91 @@ def no_project_hint(project: Project) -> str:
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
"""
|
|
78
|
+
def resolve_face_relpath(relpath: PurePosixPath, project: Project) -> ProjectPath:
|
|
79
|
+
"""Project-relative face identity -> ProjectPath, with the faces-first retry.
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
``search_dashboards`` ``face_path`` of ``'looker/x.yaml'`` (``faces/``
|
|
89
|
-
prefix stripped at search time) resolve to ``faces/looker/x.yaml``
|
|
90
|
-
without producing a ``faces/faces/...`` double prefix when the caller
|
|
91
|
-
already supplies the full ``faces/looker/x.yaml``.
|
|
81
|
+
The relpath-native counterpart to ``resolve_face_path``: for callers that
|
|
82
|
+
already hold a project-relative face identity (Cloud's context strings)
|
|
83
|
+
rather than a real filesystem path, this is the single owner of the
|
|
84
|
+
face-relative vs root-relative reconciliation — a bare name like
|
|
85
|
+
``looker/x.yaml`` retries under ``FACES_SUBDIR`` when it doesn't exist at
|
|
86
|
+
root, without producing a ``faces/faces/...`` double prefix when the
|
|
87
|
+
caller already supplies the full ``faces/looker/x.yaml``.
|
|
92
88
|
|
|
93
89
|
Existence is checked via ``project.exists(...)`` so it works for all
|
|
94
90
|
``Project`` implementations (FilesystemProject, CloudManagedProject
|
|
95
91
|
git-blob store, etc.) — not just the local filesystem. Paths that exist
|
|
96
92
|
at root resolve from root — non-faces paths like ``models/schema.sql``
|
|
97
|
-
are not rewritten under faces/.
|
|
93
|
+
are not rewritten under faces/. ``str`` is used only at the identity seam
|
|
94
|
+
(``project.path`` / ``project.exists``); everything else is real
|
|
95
|
+
``PurePosixPath`` operations.
|
|
96
|
+
"""
|
|
97
|
+
relpath_str = str(relpath)
|
|
98
|
+
assert_relpath(relpath_str)
|
|
99
|
+
if project.exists(relpath_str):
|
|
100
|
+
return project.path(relpath_str)
|
|
101
|
+
if not relpath.parts or relpath.parts[0] != FACES_SUBDIR:
|
|
102
|
+
candidate = PurePosixPath(FACES_SUBDIR) / relpath
|
|
103
|
+
candidate_str = str(candidate)
|
|
104
|
+
if project.exists(candidate_str):
|
|
105
|
+
return project.path(candidate_str)
|
|
106
|
+
return project.path(relpath_str) # caller reports not-found
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _relpath_for_fs_location(path: Path, project: Project) -> str:
|
|
110
|
+
"""Relativize an absolute, on-disk face location against a local project root.
|
|
111
|
+
|
|
112
|
+
Absolute and ``..``-leading face paths are explicit filesystem locations —
|
|
113
|
+
the one place this boundary genuinely needs a real ``Path`` op. Only
|
|
114
|
+
``FilesystemProject`` ever receives them: Cloud/MCP always supply
|
|
115
|
+
project-relative identity strings, never a real filesystem path, so any
|
|
116
|
+
other ``Project`` implementation here is a caller bug, not a store to
|
|
117
|
+
fall back through.
|
|
118
|
+
|
|
119
|
+
Two-try containment check (mirrors the pre-refactor behavior): ``resolve()``
|
|
120
|
+
follows symlinks (handles e.g. ``/var`` -> ``/private/var`` on macOS); a
|
|
121
|
+
symlink-blind ``normpath`` fallback accepts paths through intentional
|
|
122
|
+
project-internal symlinks (e.g. ``faces/tasks/`` -> ``../../tasks/``).
|
|
123
|
+
Raises ``ValueError`` if neither check places *path* inside the root.
|
|
124
|
+
"""
|
|
125
|
+
if not isinstance(project, FilesystemProject):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"Absolute or '..'-relative face paths require a local filesystem "
|
|
128
|
+
f"project; got {type(project).__name__}."
|
|
129
|
+
)
|
|
130
|
+
root = project.root
|
|
131
|
+
resolved = path.resolve()
|
|
132
|
+
try:
|
|
133
|
+
return str(resolved.relative_to(root))
|
|
134
|
+
except ValueError:
|
|
135
|
+
pass
|
|
136
|
+
normalized = Path(os.path.normpath(path.absolute()))
|
|
137
|
+
try:
|
|
138
|
+
return str(normalized.relative_to(root))
|
|
139
|
+
except ValueError:
|
|
140
|
+
raise ValueError(f"Path {path!r} is outside project root {root!r}") from None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def resolve_face_path(path: Path, project: Project) -> ProjectPath:
|
|
144
|
+
"""User/agent-supplied filesystem-shaped face path -> ProjectPath.
|
|
145
|
+
|
|
146
|
+
Absolute and ``..``-leading paths are explicit locations: they relativize
|
|
147
|
+
against a concrete ``FilesystemProject`` root (``_relpath_for_fs_location``,
|
|
148
|
+
a leading ``..`` resolving against cwd for CLI shell-nav feel) and are
|
|
149
|
+
returned as-is — no ``faces/`` retry, even when the exact path doesn't
|
|
150
|
+
exist. A bare relative name (no explicit location) delegates to
|
|
151
|
+
``resolve_face_relpath`` for the faces-first retry, which works against
|
|
152
|
+
any ``Project`` implementation.
|
|
98
153
|
"""
|
|
99
154
|
if path.is_absolute():
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
candidate = f"{FACES_SUBDIR}/{relpath}"
|
|
109
|
-
if project.exists(candidate):
|
|
110
|
-
return project.path(candidate)
|
|
111
|
-
return project.path(relpath) # caller reports not-found
|
|
155
|
+
relpath = _relpath_for_fs_location(path, project)
|
|
156
|
+
elif ".." in path.parts:
|
|
157
|
+
relpath = _relpath_for_fs_location(
|
|
158
|
+
Path(os.path.normpath(Path.cwd() / path)), project
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
return resolve_face_relpath(PurePosixPath(path), project)
|
|
162
|
+
return project.path(relpath)
|
|
112
163
|
|
|
113
164
|
|
|
114
165
|
def resolve_face_or_error(path: Path, project: Project) -> FaceFile | Diagnostic:
|
dataface/agent_api/cache.py
CHANGED
|
@@ -21,7 +21,6 @@ if TYPE_CHECKING:
|
|
|
21
21
|
__all__ = [
|
|
22
22
|
"TrivialDuckDBCache",
|
|
23
23
|
"open_cache",
|
|
24
|
-
"open_project_cache",
|
|
25
24
|
"project_cache_ctx",
|
|
26
25
|
]
|
|
27
26
|
|
|
@@ -32,18 +31,19 @@ def open_cache(path: Path | None) -> TrivialDuckDBCache:
|
|
|
32
31
|
``path=None`` opens an in-memory cache — ephemeral, discarded when closed.
|
|
33
32
|
A path opens (or creates, if absent) a persistent DuckDB file, reused
|
|
34
33
|
across invocations. Callers own the returned cache's lifecycle (close it).
|
|
35
|
-
Does not consult project config — use ``
|
|
34
|
+
Does not consult project config — use ``project_cache_ctx`` for that.
|
|
36
35
|
"""
|
|
37
36
|
return TrivialDuckDBCache(db_path=path)
|
|
38
37
|
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
@contextmanager
|
|
40
|
+
def project_cache_ctx(
|
|
41
41
|
project: FilesystemProject,
|
|
42
42
|
*,
|
|
43
43
|
no_cache: bool = False,
|
|
44
44
|
cache_path: Path | None = None,
|
|
45
|
-
) -> TrivialDuckDBCache | None:
|
|
46
|
-
"""Open the query-result cache honoring project config,
|
|
45
|
+
) -> Generator[TrivialDuckDBCache | None]:
|
|
46
|
+
"""Open the query-result cache honoring project config, closing it on exit.
|
|
47
47
|
|
|
48
48
|
The caller owns the project instance. Pair this with
|
|
49
49
|
``ProjectSession.from_project(project, cache=...)`` rather than
|
|
@@ -59,23 +59,12 @@ def open_project_cache(
|
|
|
59
59
|
to off without closing the door on a nearer scope's opt-in, so it still
|
|
60
60
|
opens a store; the resolved per-query policy decides what gets written.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Yields:
|
|
63
63
|
``None`` only when ``no_cache=True`` — callers pass this straight
|
|
64
64
|
through as ``ProjectSession(cache=...)``.
|
|
65
65
|
"""
|
|
66
66
|
boot = resolve_cache_boot(project, no_cache=no_cache, cache_path=cache_path)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@contextmanager
|
|
71
|
-
def project_cache_ctx(
|
|
72
|
-
project: FilesystemProject,
|
|
73
|
-
*,
|
|
74
|
-
no_cache: bool = False,
|
|
75
|
-
cache_path: Path | None = None,
|
|
76
|
-
) -> Generator[TrivialDuckDBCache | None]:
|
|
77
|
-
"""Context-managed ``open_project_cache``; closes the cache on exit if opened."""
|
|
78
|
-
cache = open_project_cache(project, no_cache=no_cache, cache_path=cache_path)
|
|
67
|
+
cache = open_cache(boot.path) if boot.enabled else None
|
|
79
68
|
try:
|
|
80
69
|
yield cache
|
|
81
70
|
finally:
|
dataface/agent_api/data_paths.py
CHANGED
|
@@ -83,9 +83,7 @@ def data_alias_errors_for_file(
|
|
|
83
83
|
"""
|
|
84
84
|
from dataface.core.serve.alias_index import read_aliases_from_file
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
# that is a caller bug, not a parse error, and must NOT be swallowed.
|
|
88
|
-
project_path = project.path_for_fspath(face_file)
|
|
86
|
+
project_path = project.path(face_file.as_posix())
|
|
89
87
|
try:
|
|
90
88
|
aliases = read_aliases_from_file(project_path)
|
|
91
89
|
except ValueError:
|
dataface/agent_api/describe.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from pathlib import Path
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -22,7 +22,7 @@ from dataface.core.diagnostics import (
|
|
|
22
22
|
ERR_INTERNAL,
|
|
23
23
|
Diagnostic,
|
|
24
24
|
)
|
|
25
|
-
from dataface.core.project import Project
|
|
25
|
+
from dataface.core.project import Project, ProjectPath
|
|
26
26
|
|
|
27
27
|
# Chart-encoding fields surfaced to agents. Covers the stable channel fields
|
|
28
28
|
# across chart families. `label` is excluded — it's a KPI-specific render hint,
|
|
@@ -99,7 +99,7 @@ class DescribeFaceResult(BaseModel):
|
|
|
99
99
|
model_config = ConfigDict(frozen=True)
|
|
100
100
|
|
|
101
101
|
success: bool
|
|
102
|
-
path: Path
|
|
102
|
+
path: Path = Field(description="Project-relative path to the described face.")
|
|
103
103
|
title: str | None = None
|
|
104
104
|
description: str | None = None
|
|
105
105
|
queries: list[QueryDescription] = Field(
|
|
@@ -183,8 +183,7 @@ def _encoding_for_chart(chart: Chart) -> dict[str, Any]:
|
|
|
183
183
|
|
|
184
184
|
def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
185
185
|
"""Describe the structure of a face: queries, charts, variables, layout."""
|
|
186
|
-
from dataface.agent_api._paths import
|
|
187
|
-
from dataface.core.compile.compiler import compile_file
|
|
186
|
+
from dataface.agent_api._paths import resolve_face_path
|
|
188
187
|
from dataface.core.diagnostics.base import DatafaceError
|
|
189
188
|
|
|
190
189
|
try:
|
|
@@ -198,12 +197,22 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
198
197
|
],
|
|
199
198
|
)
|
|
200
199
|
|
|
201
|
-
|
|
200
|
+
return _describe_resolved(resolved, project=project)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _describe_resolved(
|
|
204
|
+
resolved: ProjectPath, *, project: Project
|
|
205
|
+
) -> DescribeFaceResult:
|
|
206
|
+
"""Describe an already-resolved face path — no re-resolution."""
|
|
207
|
+
from dataface.agent_api._paths import no_project_hint
|
|
208
|
+
from dataface.core.compile.compiler import compile_file
|
|
209
|
+
from dataface.core.diagnostics.base import DatafaceError
|
|
210
|
+
|
|
202
211
|
if not resolved.exists():
|
|
203
212
|
hint = no_project_hint(project)
|
|
204
213
|
return DescribeFaceResult(
|
|
205
214
|
success=False,
|
|
206
|
-
path=
|
|
215
|
+
path=Path(resolved.relpath),
|
|
207
216
|
errors=[
|
|
208
217
|
DatafaceError.from_code(
|
|
209
218
|
ERR_FILE_NOT_FOUND,
|
|
@@ -218,7 +227,7 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
218
227
|
except OSError as exc:
|
|
219
228
|
return DescribeFaceResult(
|
|
220
229
|
success=False,
|
|
221
|
-
path=
|
|
230
|
+
path=Path(resolved.relpath),
|
|
222
231
|
errors=[
|
|
223
232
|
DatafaceError.from_code(ERR_INTERNAL, message=str(exc)).to_diagnostic(
|
|
224
233
|
file=resolved.relpath
|
|
@@ -229,7 +238,7 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
229
238
|
if result.errors:
|
|
230
239
|
return DescribeFaceResult(
|
|
231
240
|
success=False,
|
|
232
|
-
path=
|
|
241
|
+
path=Path(resolved.relpath),
|
|
233
242
|
errors=list(result.errors),
|
|
234
243
|
)
|
|
235
244
|
|
|
@@ -237,7 +246,7 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
237
246
|
if face is None:
|
|
238
247
|
return DescribeFaceResult(
|
|
239
248
|
success=False,
|
|
240
|
-
path=
|
|
249
|
+
path=Path(resolved.relpath),
|
|
241
250
|
errors=[
|
|
242
251
|
DatafaceError.from_code(
|
|
243
252
|
ERR_INTERNAL, message="Compilation produced no face"
|
|
@@ -285,7 +294,7 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
285
294
|
|
|
286
295
|
return DescribeFaceResult(
|
|
287
296
|
success=True,
|
|
288
|
-
path=
|
|
297
|
+
path=Path(resolved.relpath),
|
|
289
298
|
title=face.title or None,
|
|
290
299
|
description=face.description or None,
|
|
291
300
|
queries=query_descs,
|
|
@@ -339,28 +348,33 @@ def _describe_one_path(
|
|
|
339
348
|
)
|
|
340
349
|
]
|
|
341
350
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return [describe_face(resolved_display, project=project)]
|
|
351
|
+
if resolved.is_yaml:
|
|
352
|
+
return [_describe_resolved(resolved, project=project)]
|
|
345
353
|
|
|
346
354
|
faces = sorted(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
355
|
+
(
|
|
356
|
+
pf
|
|
357
|
+
for pf in project.iter_faces(under=resolved.relpath, recursive=True)
|
|
358
|
+
if pf.is_yaml
|
|
359
|
+
and not pf.is_private
|
|
360
|
+
and not (pf.parent / INSPECT_TEMPLATE_MANIFEST).exists()
|
|
361
|
+
),
|
|
362
|
+
# Relpaths under `under` share a common prefix, so a PurePosixPath key
|
|
363
|
+
# reproduces the old absolute-Path sort order exactly on 3.10 and
|
|
364
|
+
# 3.13 — don't "simplify" this to a str/.parts key.
|
|
365
|
+
key=lambda pf: PurePosixPath(pf.relpath),
|
|
352
366
|
)
|
|
353
367
|
if not faces:
|
|
354
368
|
return [
|
|
355
369
|
DescribeFaceResult(
|
|
356
370
|
success=False,
|
|
357
|
-
path=
|
|
371
|
+
path=Path(resolved.relpath),
|
|
358
372
|
errors=[
|
|
359
373
|
DatafaceError.from_code(
|
|
360
374
|
ERR_INTERNAL,
|
|
361
|
-
message=f"No face files found in {
|
|
375
|
+
message=f"No face files found in {resolved.relpath}",
|
|
362
376
|
).to_diagnostic()
|
|
363
377
|
],
|
|
364
378
|
)
|
|
365
379
|
]
|
|
366
|
-
return [
|
|
380
|
+
return [_describe_resolved(pf, project=project) for pf in faces]
|
|
@@ -10,7 +10,6 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
10
10
|
| `description` | str | ✓ | Description text for the dashboard. |
|
|
11
11
|
| `tags` | list[str] | ✓ | Tags for categorization and search. |
|
|
12
12
|
| `aliases` | list[str] | ✓ | Additional URLs that redirect to this face's canonical file-path URL. Each entry must be absolute (leading /). Requests to these URLs are redirected (302) to the face's real path, query string preserved. Valid on .yml, .yaml, .md, and folder index.* faces. |
|
|
13
|
-
| `docs` | str | ✓ | Relative path under the docs site for the canonical doc page that explains this face. Surfaced as a 'Docs →' link in the playground gallery when DFT_DOCS_URL is set. |
|
|
14
13
|
| `text` | str | ✓ | Markdown text content for text-only sections. |
|
|
15
14
|
| `allow_html` | bool | ✓ | Render the face's body text as raw HTML via foreignObject instead of markdown. TRUSTED-CONTENT ONLY: the HTML (including any Jinja-interpolated values) is rendered as-authored — this is NOT a security sandbox. mdsvg strips <script>/event-handlers as a best-effort guard, not a guarantee. Enable only on first-party faces you fully control. |
|
|
16
15
|
| `source` | str | ✓ | Default source name for all queries in this face. Inheritable via meta.yaml cascade. |
|
|
@@ -120,7 +120,7 @@ class ProjectSession:
|
|
|
120
120
|
(or another cache backend) when caching is desired, otherwise omit and the
|
|
121
121
|
project will run uncached. ``close()`` does not touch the cache — whoever
|
|
122
122
|
opened it closes it. A caller deriving the cache from project config via
|
|
123
|
-
``
|
|
123
|
+
``project_cache_ctx(project)`` already holds a project — use
|
|
124
124
|
``from_project`` instead, or this builds a second one.
|
|
125
125
|
|
|
126
126
|
The adapter registry is built lazily on first access to ``adapter_registry``.
|
dataface/agent_api/query.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Typed query verbs for the agent API."""
|
|
2
2
|
|
|
3
|
-
from pathlib import Path
|
|
3
|
+
from pathlib import Path, PurePosixPath
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -233,7 +233,7 @@ class QueryFaceResult(BaseModel):
|
|
|
233
233
|
|
|
234
234
|
success: bool
|
|
235
235
|
name: str
|
|
236
|
-
path:
|
|
236
|
+
path: PurePosixPath
|
|
237
237
|
query_type: str | None = None
|
|
238
238
|
sql: str | None = None
|
|
239
239
|
columns: list[str] = []
|
|
@@ -245,7 +245,10 @@ class QueryFaceResult(BaseModel):
|
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
def _fail(
|
|
248
|
-
name: str,
|
|
248
|
+
name: str,
|
|
249
|
+
path: PurePosixPath,
|
|
250
|
+
errors: list[str],
|
|
251
|
+
available: list[str] | None = None,
|
|
249
252
|
) -> QueryFaceResult:
|
|
250
253
|
return QueryFaceResult(
|
|
251
254
|
success=False,
|
|
@@ -271,9 +274,11 @@ def query_face(
|
|
|
271
274
|
try:
|
|
272
275
|
file_path = resolve_face_path(path, project)
|
|
273
276
|
except ValueError as e:
|
|
274
|
-
|
|
277
|
+
# Raw-input echo — resolution failed, so this is the caller's rejected
|
|
278
|
+
# path, not a validated project-relative identity.
|
|
279
|
+
return _fail(name, PurePosixPath(path), [str(e)])
|
|
275
280
|
|
|
276
|
-
resolved_display =
|
|
281
|
+
resolved_display = PurePosixPath(file_path.relpath)
|
|
277
282
|
if not file_path.exists():
|
|
278
283
|
from dataface.agent_api._paths import no_project_hint
|
|
279
284
|
|
dataface/agent_api/validate.py
CHANGED
|
@@ -5,7 +5,7 @@ No warehouse connection, no query execution.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from pathlib import Path
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -21,7 +21,7 @@ from dataface.core.diagnostics import (
|
|
|
21
21
|
Diagnostic,
|
|
22
22
|
)
|
|
23
23
|
from dataface.core.diagnostics.base import DatafaceError
|
|
24
|
-
from dataface.core.project import FACES_SUBDIR, META_FILENAMES, Project
|
|
24
|
+
from dataface.core.project import FACES_SUBDIR, META_FILENAMES, Project, ProjectPath
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from dataface.agent_api.project_session import ProjectSession
|
|
@@ -49,7 +49,7 @@ class ValidateResult(BaseModel):
|
|
|
49
49
|
model_config = ConfigDict(frozen=True)
|
|
50
50
|
|
|
51
51
|
success: bool
|
|
52
|
-
path: Path
|
|
52
|
+
path: Path = Field(description="Project-relative path to the validated face.")
|
|
53
53
|
errors: list[Diagnostic] = Field(
|
|
54
54
|
default_factory=list, description="Validation errors found in the face file."
|
|
55
55
|
)
|
|
@@ -131,35 +131,40 @@ def _validate_one_path(
|
|
|
131
131
|
)
|
|
132
132
|
]
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return [validate(resolved_display, project=project)]
|
|
134
|
+
if resolved.is_yaml:
|
|
135
|
+
return [_validate_resolved(resolved, project=project)]
|
|
137
136
|
|
|
138
137
|
faces = sorted(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
(
|
|
139
|
+
pf
|
|
140
|
+
for pf in project.iter_faces(under=resolved.relpath, recursive=True)
|
|
141
|
+
if pf.is_yaml
|
|
142
|
+
and not pf.is_private
|
|
143
|
+
and PurePosixPath(pf.relpath).name not in META_FILENAMES
|
|
144
|
+
and not (pf.parent / INSPECT_TEMPLATE_MANIFEST).exists()
|
|
145
|
+
),
|
|
146
|
+
# Relpaths under `under` share a common prefix, so a PurePosixPath key
|
|
147
|
+
# reproduces the old absolute-Path sort order exactly on 3.10 and
|
|
148
|
+
# 3.13 — don't "simplify" this to a str/.parts key.
|
|
149
|
+
key=lambda pf: PurePosixPath(pf.relpath),
|
|
145
150
|
)
|
|
146
151
|
if not faces:
|
|
147
152
|
return [
|
|
148
153
|
ValidateResult(
|
|
149
154
|
success=False,
|
|
150
|
-
path=
|
|
155
|
+
path=Path(resolved.relpath),
|
|
151
156
|
errors=[
|
|
152
157
|
DatafaceError.from_code(
|
|
153
158
|
ERR_INTERNAL,
|
|
154
|
-
message=f"No face files found in {
|
|
159
|
+
message=f"No face files found in {resolved.relpath}",
|
|
155
160
|
).to_diagnostic()
|
|
156
161
|
],
|
|
157
162
|
)
|
|
158
163
|
]
|
|
159
|
-
return [
|
|
164
|
+
return [_validate_resolved(pf, project=project) for pf in faces]
|
|
160
165
|
|
|
161
166
|
|
|
162
|
-
def _validate_meta_file(
|
|
167
|
+
def _validate_meta_file(resolved: ProjectPath) -> ValidateResult:
|
|
163
168
|
"""Validate a meta.yaml as a FacePatch fragment — no layout required."""
|
|
164
169
|
from pydantic import ValidationError as PydanticValidationError
|
|
165
170
|
|
|
@@ -167,16 +172,17 @@ def _validate_meta_file(path: Path, *, project: Project) -> ValidateResult:
|
|
|
167
172
|
from dataface.core.compile.meta import load_meta_file
|
|
168
173
|
from dataface.core.compile.models.face.patch import FacePatch
|
|
169
174
|
|
|
175
|
+
relpath = resolved.relpath
|
|
170
176
|
try:
|
|
171
|
-
meta_data, _ = load_meta_file(
|
|
177
|
+
meta_data, _ = load_meta_file(resolved)
|
|
172
178
|
except CompilationError as exc:
|
|
173
179
|
return ValidateResult(
|
|
174
180
|
success=False,
|
|
175
|
-
path=
|
|
181
|
+
path=Path(relpath),
|
|
176
182
|
errors=[
|
|
177
183
|
DatafaceError.from_code(
|
|
178
184
|
ERR_META_SCHEMA, message=str(exc)
|
|
179
|
-
).to_diagnostic(file=
|
|
185
|
+
).to_diagnostic(file=relpath)
|
|
180
186
|
],
|
|
181
187
|
)
|
|
182
188
|
|
|
@@ -191,18 +197,16 @@ def _validate_meta_file(path: Path, *, project: Project) -> ValidateResult:
|
|
|
191
197
|
if e["loc"]
|
|
192
198
|
else e["msg"]
|
|
193
199
|
),
|
|
194
|
-
).to_diagnostic(file=
|
|
200
|
+
).to_diagnostic(file=relpath)
|
|
195
201
|
for e in exc.errors()
|
|
196
202
|
]
|
|
197
|
-
return ValidateResult(success=False, path=
|
|
203
|
+
return ValidateResult(success=False, path=Path(relpath), errors=errors)
|
|
198
204
|
|
|
199
|
-
return ValidateResult(success=True, path=
|
|
205
|
+
return ValidateResult(success=True, path=Path(relpath))
|
|
200
206
|
|
|
201
207
|
|
|
202
208
|
def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
203
209
|
"""Fast YAML schema + cross-reference validation. No warehouse, no execute."""
|
|
204
|
-
from dataface.core.compile.compiler import compile_file
|
|
205
|
-
|
|
206
210
|
try:
|
|
207
211
|
resolved = resolve_face_path(path, project)
|
|
208
212
|
except ValueError as exc:
|
|
@@ -214,11 +218,18 @@ def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
|
214
218
|
],
|
|
215
219
|
)
|
|
216
220
|
|
|
221
|
+
return _validate_resolved(resolved, project=project)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _validate_resolved(resolved: ProjectPath, *, project: Project) -> ValidateResult:
|
|
225
|
+
"""Validate an already-resolved face path — no re-resolution."""
|
|
226
|
+
from dataface.core.compile.compiler import compile_file
|
|
227
|
+
|
|
217
228
|
if not resolved.exists():
|
|
218
229
|
hint = no_project_hint(project)
|
|
219
230
|
return ValidateResult(
|
|
220
231
|
success=False,
|
|
221
|
-
path=
|
|
232
|
+
path=Path(resolved.relpath),
|
|
222
233
|
errors=[
|
|
223
234
|
DatafaceError.from_code(
|
|
224
235
|
ERR_FILE_NOT_FOUND,
|
|
@@ -228,16 +239,15 @@ def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
|
228
239
|
],
|
|
229
240
|
)
|
|
230
241
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return _validate_meta_file(resolved_display, project=project)
|
|
242
|
+
if PurePosixPath(resolved.relpath).name in META_FILENAMES:
|
|
243
|
+
return _validate_meta_file(resolved)
|
|
234
244
|
|
|
235
245
|
try:
|
|
236
246
|
result = compile_file(resolved.read_face())
|
|
237
247
|
except OSError as exc:
|
|
238
248
|
return ValidateResult(
|
|
239
249
|
success=False,
|
|
240
|
-
path=
|
|
250
|
+
path=Path(resolved.relpath),
|
|
241
251
|
errors=[
|
|
242
252
|
DatafaceError.from_code(ERR_INTERNAL, message=str(exc)).to_diagnostic(
|
|
243
253
|
file=resolved.relpath
|
|
@@ -248,7 +258,7 @@ def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
|
248
258
|
errors = list(result.errors)
|
|
249
259
|
return ValidateResult(
|
|
250
260
|
success=len(errors) == 0,
|
|
251
|
-
path=
|
|
261
|
+
path=Path(resolved.relpath),
|
|
252
262
|
errors=errors,
|
|
253
263
|
warnings=list(result.warnings),
|
|
254
264
|
)
|
|
@@ -297,18 +307,17 @@ def annotate_with_data_lint(
|
|
|
297
307
|
|
|
298
308
|
annotated: list[ValidateResult] = []
|
|
299
309
|
for result in results:
|
|
310
|
+
project_path = project_session.project.path(result.path.as_posix())
|
|
300
311
|
try:
|
|
301
|
-
|
|
312
|
+
path_exists = project_path.exists()
|
|
302
313
|
except ValueError:
|
|
303
314
|
# Fires for a genuine root-escape (an earlier resolve_face_path
|
|
304
|
-
# failure)
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# under project.root. Either way there's nothing to lint through
|
|
308
|
-
# the seam; keep the result unchanged.
|
|
315
|
+
# failure) or a not-found result echoing a raw caller-supplied
|
|
316
|
+
# path that isn't project-relative. Either way there's nothing to
|
|
317
|
+
# lint through the seam; keep the result unchanged.
|
|
309
318
|
annotated.append(result)
|
|
310
319
|
continue
|
|
311
|
-
if not
|
|
320
|
+
if not path_exists:
|
|
312
321
|
annotated.append(result)
|
|
313
322
|
continue
|
|
314
323
|
alias_msgs = data_alias_errors_for_file(
|