dataface 0.1.5.dev206__py3-none-any.whl → 0.1.5.dev215__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/DATAFACE_SYNTAX.md +1 -1
- dataface/agent_api/docs/yaml-reference.md +4 -4
- dataface/agent_api/project.py +40 -44
- dataface/ai/tools/__init__.py +1 -1
- dataface/cli/commands/serve.py +9 -4
- dataface/core/compile/__init__.py +5 -1
- dataface/core/compile/compiler.py +25 -49
- dataface/core/compile/config.py +32 -3
- dataface/core/compile/models/query/authored.py +6 -8
- dataface/core/compile/models/query/normalized.py +13 -13
- dataface/core/compile/normalize_queries.py +6 -6
- dataface/core/dashboard.py +13 -4
- dataface/core/errors/__init__.py +6 -0
- dataface/core/errors/codes_serve.py +36 -0
- dataface/core/errors/hints.py +13 -0
- dataface/core/execute/adapters/__init__.py +2 -2
- dataface/core/execute/adapters/adapter_registry.py +9 -11
- dataface/core/execute/adapters/{schema_resolver_adapter.py → schema_adapter.py} +8 -8
- dataface/core/inspect/renderer.py +23 -16
- dataface/core/inspect/templates/categorical_column.yml +1 -1
- dataface/core/inspect/templates/date_column.yml +1 -1
- dataface/core/inspect/templates/model.yml +1 -1
- dataface/core/inspect/templates/numeric_column.yml +1 -1
- dataface/core/inspect/templates/quality.yml +1 -1
- dataface/core/inspect/templates/string_column.yml +1 -1
- dataface/core/pack/models.py +1 -1
- dataface/core/render/face_api.py +2 -2
- dataface/core/render/nav.py +9 -5
- dataface/core/render/renderer.py +1 -1
- dataface/core/serve/bootstrap.py +22 -8
- dataface/core/serve/server.py +6 -2
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev215.dist-info}/METADATA +1 -1
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev215.dist-info}/RECORD +36 -35
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev215.dist-info}/WHEEL +0 -0
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev215.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev215.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -346,7 +346,7 @@ queries:
|
|
|
346
346
|
- [Bob, 87.1]
|
|
347
347
|
```
|
|
348
348
|
|
|
349
|
-
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `
|
|
349
|
+
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `schema` is internal-only and not part of the authored surface.
|
|
350
350
|
|
|
351
351
|
Common fields (all query types):
|
|
352
352
|
|
|
@@ -72,7 +72,7 @@ AuthoredQuery definition from YAML.
|
|
|
72
72
|
|
|
73
73
|
| Field | Type | Optional | Description |
|
|
74
74
|
|-------|------|:--------:|-------------|
|
|
75
|
-
| `type` | enum: "sql", "metricflow", "dbt_model", "http", "csv", "values", "
|
|
75
|
+
| `type` | enum: "sql", "metricflow", "dbt_model", "http", "csv", "values", "schema" | ✓ | Query adapter type. Defaults to 'sql'. Options: sql, metricflow, dbt_model, http, csv, values, schema. |
|
|
76
76
|
| `source` | str \| dict[str, Any] | ✓ | Database source reference (name string) or inline source config dict. |
|
|
77
77
|
| `target` | str | ✓ | dbt target name for dbt_model queries (defaults to 'dev'). |
|
|
78
78
|
| `sql` | str | ✓ | SQL query string. Supports Jinja2 templates referencing variables. |
|
|
@@ -92,9 +92,9 @@ AuthoredQuery definition from YAML.
|
|
|
92
92
|
| `filter` | dict[str, Any] | ✓ | Row-level filter applied to CSV or values data. |
|
|
93
93
|
| `delimiter` | str | ✓ | Column delimiter for CSV queries. Default: comma (','). |
|
|
94
94
|
| `encoding` | str | ✓ | File encoding for CSV queries. Default: UTF-8. |
|
|
95
|
-
| `schema` | str | ✓ | Schema name for
|
|
96
|
-
| `table` | str | ✓ | Table name for
|
|
97
|
-
| `column` | str | ✓ | Column name for
|
|
95
|
+
| `schema` | str | ✓ | Schema name for schema queries (YAML key: schema). |
|
|
96
|
+
| `table` | str | ✓ | Table name for schema queries. |
|
|
97
|
+
| `column` | str | ✓ | Column name for schema queries. |
|
|
98
98
|
| `rows` | list[dict[str, Any]] | ✓ | Inline data rows for values-type queries (list of row dicts). |
|
|
99
99
|
| `values` | list[list[Any]] | ✓ | Inline column-oriented data for values-type queries (list of lists). |
|
|
100
100
|
| `description` | str | ✓ | Human-readable description of the query. Used by AI search and tooling. |
|
dataface/agent_api/project.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING, Any
|
|
9
10
|
|
|
@@ -55,7 +56,6 @@ class Project:
|
|
|
55
56
|
project_root: Path
|
|
56
57
|
cache: DuckDBCache | None
|
|
57
58
|
_owns_registry: bool
|
|
58
|
-
_adapter_registry: AdapterRegistry | None
|
|
59
59
|
_read_only: bool
|
|
60
60
|
_dbt_project_path: Path | None
|
|
61
61
|
_connection_string: str | None
|
|
@@ -64,8 +64,8 @@ class Project:
|
|
|
64
64
|
_duckdb_config: dict[str, Any] | None
|
|
65
65
|
_allow_external_access_in_readonly: bool
|
|
66
66
|
_resolver: SourceResolver | None
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
_sources: ProjectSourcesConfig
|
|
68
|
+
_warnings_ignore: frozenset[str]
|
|
69
69
|
|
|
70
70
|
def __init__(
|
|
71
71
|
self,
|
|
@@ -78,7 +78,10 @@ class Project:
|
|
|
78
78
|
# We own the registry only when we'll lazy-build it ourselves. An injected
|
|
79
79
|
# registry belongs to the caller; we use it but don't close it on their behalf.
|
|
80
80
|
self._owns_registry = adapter_registry is None
|
|
81
|
-
|
|
81
|
+
if adapter_registry is not None:
|
|
82
|
+
# cached_property stores in __dict__; pre-populating makes the descriptor
|
|
83
|
+
# short-circuit the build on first access.
|
|
84
|
+
self.__dict__["adapter_registry"] = adapter_registry
|
|
82
85
|
self._read_only = True
|
|
83
86
|
self._dbt_project_path = None
|
|
84
87
|
self._connection_string = None
|
|
@@ -87,8 +90,8 @@ class Project:
|
|
|
87
90
|
self._duckdb_config = None
|
|
88
91
|
self._allow_external_access_in_readonly = False
|
|
89
92
|
self._resolver = None
|
|
90
|
-
self.
|
|
91
|
-
self.
|
|
93
|
+
self._sources = get_project_sources(self.project_root)
|
|
94
|
+
self._warnings_ignore = get_project_warnings_ignore(self.project_root)
|
|
92
95
|
|
|
93
96
|
@classmethod
|
|
94
97
|
def open(
|
|
@@ -133,7 +136,7 @@ class Project:
|
|
|
133
136
|
def __exit__(self, *exc_info: object) -> None:
|
|
134
137
|
self.close()
|
|
135
138
|
|
|
136
|
-
@
|
|
139
|
+
@cached_property
|
|
137
140
|
def adapter_registry(self) -> AdapterRegistry:
|
|
138
141
|
"""Lazily build the registry on first access; cached thereafter.
|
|
139
142
|
|
|
@@ -141,57 +144,49 @@ class Project:
|
|
|
141
144
|
own it (i.e. it was not injected at construction time).
|
|
142
145
|
When constructed with an injected registry, returns it directly.
|
|
143
146
|
"""
|
|
144
|
-
|
|
145
|
-
self.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
156
|
-
return self._adapter_registry
|
|
147
|
+
return build_adapter_registry(
|
|
148
|
+
self.project_root,
|
|
149
|
+
read_only=self._read_only,
|
|
150
|
+
dbt_project_path=self._dbt_project_path,
|
|
151
|
+
connection_string=self._connection_string,
|
|
152
|
+
profile_type=self._dialect,
|
|
153
|
+
target=self._target,
|
|
154
|
+
duckdb_config=self._duckdb_config,
|
|
155
|
+
allow_external_access_in_readonly=self._allow_external_access_in_readonly,
|
|
156
|
+
resolver=self._resolver,
|
|
157
|
+
)
|
|
157
158
|
|
|
158
159
|
def refresh(self) -> None:
|
|
159
160
|
"""Policy-free rebuild primitive.
|
|
160
161
|
|
|
161
162
|
For projects opened via ``open()``: closes the current registry (if built)
|
|
162
|
-
and clears it so the next access rebuilds from disk. Also
|
|
163
|
-
sources and warnings_ignore
|
|
163
|
+
and clears it so the next access rebuilds from disk. Also re-reads
|
|
164
|
+
sources and warnings_ignore from disk.
|
|
164
165
|
|
|
165
166
|
For projects constructed with an injected ``adapter_registry``: skips the
|
|
166
|
-
registry rebuild (build arguments are not available), but still
|
|
167
|
-
config
|
|
167
|
+
registry rebuild (build arguments are not available), but still re-reads
|
|
168
|
+
the config fields.
|
|
168
169
|
"""
|
|
169
|
-
if self._owns_registry:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.
|
|
174
|
-
self._warnings_ignore_cache = None
|
|
170
|
+
if self._owns_registry and "adapter_registry" in self.__dict__:
|
|
171
|
+
self.adapter_registry.close()
|
|
172
|
+
del self.__dict__["adapter_registry"]
|
|
173
|
+
self._sources = get_project_sources(self.project_root)
|
|
174
|
+
self._warnings_ignore = get_project_warnings_ignore(self.project_root)
|
|
175
175
|
|
|
176
176
|
def close(self) -> None:
|
|
177
177
|
"""Close the adapter registry iff we own it. The cache is the caller's to close."""
|
|
178
|
-
if self._owns_registry and self.
|
|
179
|
-
self.
|
|
180
|
-
self.
|
|
181
|
-
|
|
182
|
-
# ── Config accessors ─────────────────────────────────────────────────────
|
|
178
|
+
if self._owns_registry and "adapter_registry" in self.__dict__:
|
|
179
|
+
self.adapter_registry.close()
|
|
180
|
+
del self.__dict__["adapter_registry"]
|
|
183
181
|
|
|
182
|
+
# Read-only views: project lifecycle owns these; external writers go through refresh().
|
|
183
|
+
@property
|
|
184
184
|
def sources(self) -> ProjectSourcesConfig:
|
|
185
|
-
|
|
186
|
-
if self._sources_cache is None:
|
|
187
|
-
self._sources_cache = get_project_sources(self.project_root)
|
|
188
|
-
return self._sources_cache
|
|
185
|
+
return self._sources
|
|
189
186
|
|
|
187
|
+
@property
|
|
190
188
|
def warnings_ignore(self) -> frozenset[str]:
|
|
191
|
-
|
|
192
|
-
if self._warnings_ignore_cache is None:
|
|
193
|
-
self._warnings_ignore_cache = get_project_warnings_ignore(self.project_root)
|
|
194
|
-
return self._warnings_ignore_cache
|
|
189
|
+
return self._warnings_ignore
|
|
195
190
|
|
|
196
191
|
# ── Verb forwarders ──────────────────────────────────────────────────────
|
|
197
192
|
|
|
@@ -300,6 +295,7 @@ class Project:
|
|
|
300
295
|
variables=variables,
|
|
301
296
|
adapter_registry=self.adapter_registry,
|
|
302
297
|
project_dir=self.project_root,
|
|
298
|
+
project_sources=self.sources,
|
|
303
299
|
duckdb_cache=self.cache,
|
|
304
300
|
format=format,
|
|
305
301
|
use_cache=use_cache,
|
|
@@ -308,5 +304,5 @@ class Project:
|
|
|
308
304
|
scale=scale,
|
|
309
305
|
ignore_codes=ignore_codes,
|
|
310
306
|
max_workers=max_workers,
|
|
311
|
-
warnings_ignore=self.warnings_ignore
|
|
307
|
+
warnings_ignore=self.warnings_ignore,
|
|
312
308
|
)
|
dataface/ai/tools/__init__.py
CHANGED
|
@@ -113,7 +113,7 @@ def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, An
|
|
|
113
113
|
adapter_registry=ctx.project.adapter_registry,
|
|
114
114
|
duckdb_cache=ctx.project.cache,
|
|
115
115
|
server_port=ctx.server_port,
|
|
116
|
-
warnings_ignore=ctx.project.warnings_ignore
|
|
116
|
+
warnings_ignore=ctx.project.warnings_ignore,
|
|
117
117
|
).model_dump(mode="json", exclude_none=True)
|
|
118
118
|
|
|
119
119
|
|
dataface/cli/commands/serve.py
CHANGED
|
@@ -7,6 +7,10 @@ from pathlib import Path
|
|
|
7
7
|
import typer
|
|
8
8
|
import uvicorn
|
|
9
9
|
|
|
10
|
+
from dataface.cli._error_format import print_structured_errors
|
|
11
|
+
from dataface.core.compile.errors import DatafaceError
|
|
12
|
+
from dataface.core.errors import DF_SERVE_STARTUP_FAILED
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
def serve_command(
|
|
12
16
|
port: int | None = None,
|
|
@@ -52,8 +56,8 @@ def serve_command(
|
|
|
52
56
|
# Apply DFT_DEFAULT_THEME before anything else — fails fast on invalid names.
|
|
53
57
|
try:
|
|
54
58
|
apply_default_theme_from_env()
|
|
55
|
-
except
|
|
56
|
-
|
|
59
|
+
except DatafaceError as e:
|
|
60
|
+
print_structured_errors([e.to_structured()])
|
|
57
61
|
raise typer.Exit(1) from None
|
|
58
62
|
|
|
59
63
|
# Resolve port: --port > DFT_PORT > dataface.yml > hash(project_dir)
|
|
@@ -108,6 +112,7 @@ def serve_command(
|
|
|
108
112
|
except KeyboardInterrupt:
|
|
109
113
|
typer.echo("\n👋 Server stopped")
|
|
110
114
|
sys.exit(0)
|
|
111
|
-
except Exception as e: # noqa: BLE001
|
|
112
|
-
|
|
115
|
+
except Exception as e: # noqa: BLE001 — uvicorn surface
|
|
116
|
+
err = DatafaceError.from_code(DF_SERVE_STARTUP_FAILED, detail=str(e))
|
|
117
|
+
print_structured_errors([err.to_structured()])
|
|
113
118
|
raise typer.Exit(1) from None
|
|
@@ -20,7 +20,11 @@ This module is pure - it does NOT import from execute/ or render/.
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from dataface.core.compile.chart_focus import focus_on_chart
|
|
23
|
-
from dataface.core.compile.compiler import
|
|
23
|
+
from dataface.core.compile.compiler import (
|
|
24
|
+
CompileResult,
|
|
25
|
+
compile,
|
|
26
|
+
compile_file,
|
|
27
|
+
)
|
|
24
28
|
from dataface.core.compile.config import (
|
|
25
29
|
AccessConfig,
|
|
26
30
|
MetaConfig,
|
|
@@ -38,6 +38,10 @@ import yaml
|
|
|
38
38
|
from pydantic import ValidationError as PydanticValidationError
|
|
39
39
|
|
|
40
40
|
from dataface.core.compile.authoring_warnings import detect_authoring_warnings
|
|
41
|
+
from dataface.core.compile.config import (
|
|
42
|
+
ProjectSourcesConfig,
|
|
43
|
+
get_project_sources,
|
|
44
|
+
)
|
|
41
45
|
from dataface.core.compile.errors import (
|
|
42
46
|
CompilationError,
|
|
43
47
|
JinjaError,
|
|
@@ -80,35 +84,6 @@ def _to_plain_dict(obj: Any) -> Any:
|
|
|
80
84
|
return obj
|
|
81
85
|
|
|
82
86
|
|
|
83
|
-
def resolve_project_source_paths(
|
|
84
|
-
sources: dict[str, dict[str, Any]],
|
|
85
|
-
project_dir: Path,
|
|
86
|
-
) -> dict[str, dict[str, Any]]:
|
|
87
|
-
"""Resolve relative file/database paths in project-level source configs."""
|
|
88
|
-
resolved_sources: dict[str, dict[str, Any]] = {}
|
|
89
|
-
|
|
90
|
-
for name, config in sources.items():
|
|
91
|
-
resolved_config = dict(config)
|
|
92
|
-
source_type = resolved_config.get("type")
|
|
93
|
-
|
|
94
|
-
if source_type == "duckdb":
|
|
95
|
-
path_value = resolved_config.get("path")
|
|
96
|
-
if (
|
|
97
|
-
isinstance(path_value, str)
|
|
98
|
-
and path_value != ":memory:"
|
|
99
|
-
and not Path(path_value).is_absolute()
|
|
100
|
-
):
|
|
101
|
-
resolved_config["path"] = str((project_dir / path_value).resolve())
|
|
102
|
-
elif source_type in {"csv", "parquet", "json"}:
|
|
103
|
-
file_value = resolved_config.get("file")
|
|
104
|
-
if isinstance(file_value, str) and not Path(file_value).is_absolute():
|
|
105
|
-
resolved_config["file"] = str((project_dir / file_value).resolve())
|
|
106
|
-
|
|
107
|
-
resolved_sources[name] = resolved_config
|
|
108
|
-
|
|
109
|
-
return resolved_sources
|
|
110
|
-
|
|
111
|
-
|
|
112
87
|
@dataclass
|
|
113
88
|
class CompileResult:
|
|
114
89
|
"""Result of compilation.
|
|
@@ -160,7 +135,7 @@ def compile(
|
|
|
160
135
|
yaml_content: str,
|
|
161
136
|
options: dict[str, Any] | None = None,
|
|
162
137
|
base_dir: Path | None = None,
|
|
163
|
-
|
|
138
|
+
project_sources: ProjectSourcesConfig | None = None,
|
|
164
139
|
) -> CompileResult:
|
|
165
140
|
"""Compile YAML content to a Face.
|
|
166
141
|
|
|
@@ -177,7 +152,9 @@ def compile(
|
|
|
177
152
|
yaml_content: YAML string to compile
|
|
178
153
|
options: Optional compilation options
|
|
179
154
|
base_dir: Base directory for resolving file references
|
|
180
|
-
|
|
155
|
+
project_sources: Project-level sources, ready to use. Callers that
|
|
156
|
+
hold a ``Project`` thread ``project.sources``; one-shot callers
|
|
157
|
+
pre-load via ``get_project_sources(project_dir)``.
|
|
181
158
|
|
|
182
159
|
Returns:
|
|
183
160
|
CompileResult with compiled face or errors
|
|
@@ -234,13 +211,8 @@ def compile(
|
|
|
234
211
|
# ════════════════════════════════════════════════════════════════════
|
|
235
212
|
# STEP 3: Get Default Source and Extract Named Sources
|
|
236
213
|
# ════════════════════════════════════════════════════════════════════
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
project_sources = None
|
|
240
|
-
if project_dir is not None:
|
|
241
|
-
from dataface.core.compile.config import get_project_sources
|
|
242
|
-
|
|
243
|
-
project_sources = get_project_sources(project_dir)
|
|
214
|
+
# project_sources is supplied by the caller (Project verb or file-based
|
|
215
|
+
# entry point); its sources dict is already path-resolved.
|
|
244
216
|
|
|
245
217
|
# Get the face's default source to pass to query normalization.
|
|
246
218
|
default_source = face.get_default_source()
|
|
@@ -249,12 +221,9 @@ def compile(
|
|
|
249
221
|
|
|
250
222
|
# Extract named source configurations from the sources section
|
|
251
223
|
# These are used to resolve source references in queries (e.g., source: profiles)
|
|
252
|
-
sources_registry = {}
|
|
224
|
+
sources_registry: dict[str, dict[str, Any]] = {}
|
|
253
225
|
if project_sources is not None:
|
|
254
|
-
|
|
255
|
-
sources_registry.update(
|
|
256
|
-
resolve_project_source_paths(project_sources.sources, project_dir)
|
|
257
|
-
)
|
|
226
|
+
sources_registry.update(project_sources.sources)
|
|
258
227
|
sources_registry.update(_extract_sources(face))
|
|
259
228
|
|
|
260
229
|
# ════════════════════════════════════════════════════════════════════
|
|
@@ -446,6 +415,7 @@ def compile_file(
|
|
|
446
415
|
options: dict[str, Any] | None = None,
|
|
447
416
|
apply_meta: bool = True,
|
|
448
417
|
root_path: Path | None = None,
|
|
418
|
+
project_sources: ProjectSourcesConfig | None = None,
|
|
449
419
|
) -> CompileResult:
|
|
450
420
|
"""Compile a YAML file to a Face.
|
|
451
421
|
|
|
@@ -457,6 +427,9 @@ def compile_file(
|
|
|
457
427
|
options: Optional compilation options
|
|
458
428
|
apply_meta: If True, resolve and apply meta.yaml chain (default: True)
|
|
459
429
|
root_path: Project root for meta resolution (default: auto-detect)
|
|
430
|
+
project_sources: Pre-loaded project sources. When None, ``compile_file``
|
|
431
|
+
loads once from the derived project root. ``Project.render_dashboard``
|
|
432
|
+
threads its cached ``self.sources`` here to avoid the disk read.
|
|
460
433
|
|
|
461
434
|
Returns:
|
|
462
435
|
CompileResult with compiled face or errors
|
|
@@ -541,12 +514,15 @@ def compile_file(
|
|
|
541
514
|
e,
|
|
542
515
|
)
|
|
543
516
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
517
|
+
if project_sources is None:
|
|
518
|
+
project_dir = root_path
|
|
519
|
+
if project_dir is None:
|
|
520
|
+
from dataface.core.project_roots import discover_render_context
|
|
521
|
+
|
|
522
|
+
compile_boundary = Path(file_path.anchor) if file_path.anchor else None
|
|
523
|
+
project_dir, _ = discover_render_context(file_path.parent, compile_boundary)
|
|
547
524
|
|
|
548
|
-
|
|
549
|
-
project_dir, _ = discover_render_context(file_path.parent, compile_boundary)
|
|
525
|
+
project_sources = get_project_sources(project_dir)
|
|
550
526
|
|
|
551
527
|
# Thread meta lint config through to compile for query diagnostics
|
|
552
528
|
compile_options = dict(options) if options else {}
|
|
@@ -557,7 +533,7 @@ def compile_file(
|
|
|
557
533
|
yaml_content,
|
|
558
534
|
options=compile_options,
|
|
559
535
|
base_dir=file_path.parent,
|
|
560
|
-
|
|
536
|
+
project_sources=project_sources,
|
|
561
537
|
)
|
|
562
538
|
# Stamp file path and add validate next_command on each compile error so
|
|
563
539
|
# UI surfaces and CLI can surface "dft validate <file>" without re-deriving the path.
|
dataface/core/compile/config.py
CHANGED
|
@@ -484,10 +484,12 @@ def get_palette(name: str = "default") -> list[str]:
|
|
|
484
484
|
|
|
485
485
|
|
|
486
486
|
def get_project_sources(project_dir: Path) -> ProjectSourcesConfig:
|
|
487
|
-
"""Get project-level sources configuration.
|
|
487
|
+
"""Get project-level sources configuration with file paths absolutized.
|
|
488
488
|
|
|
489
489
|
Reads disk every call — no cache. _sources.yaml / dataface.yml edits are
|
|
490
|
-
visible to the next call.
|
|
490
|
+
visible to the next call. Relative ``path:`` (duckdb) and ``file:`` (csv /
|
|
491
|
+
parquet / json) values resolve against ``project_dir`` so callers can pass
|
|
492
|
+
the returned config straight into ``compile()`` without further work.
|
|
491
493
|
"""
|
|
492
494
|
project_dir = project_dir.resolve()
|
|
493
495
|
config = get_config()
|
|
@@ -542,10 +544,37 @@ def get_project_sources(project_dir: Path) -> ProjectSourcesConfig:
|
|
|
542
544
|
|
|
543
545
|
return ProjectSourcesConfig(
|
|
544
546
|
default=default_source,
|
|
545
|
-
sources=
|
|
547
|
+
sources=_absolutize_source_paths(
|
|
548
|
+
_normalize_project_sources(all_sources), project_dir
|
|
549
|
+
),
|
|
546
550
|
)
|
|
547
551
|
|
|
548
552
|
|
|
553
|
+
def _absolutize_source_paths(
|
|
554
|
+
sources: dict[str, dict[str, Any]],
|
|
555
|
+
project_dir: Path,
|
|
556
|
+
) -> dict[str, dict[str, Any]]:
|
|
557
|
+
"""Absolutize relative file/database paths against ``project_dir``."""
|
|
558
|
+
resolved: dict[str, dict[str, Any]] = {}
|
|
559
|
+
for name, config in sources.items():
|
|
560
|
+
cfg = dict(config)
|
|
561
|
+
source_type = cfg.get("type")
|
|
562
|
+
if source_type == "duckdb":
|
|
563
|
+
path_value = cfg.get("path")
|
|
564
|
+
if (
|
|
565
|
+
isinstance(path_value, str)
|
|
566
|
+
and path_value != ":memory:"
|
|
567
|
+
and not Path(path_value).is_absolute()
|
|
568
|
+
):
|
|
569
|
+
cfg["path"] = str((project_dir / path_value).resolve())
|
|
570
|
+
elif source_type in {"csv", "parquet", "json"}:
|
|
571
|
+
file_value = cfg.get("file")
|
|
572
|
+
if isinstance(file_value, str) and not Path(file_value).is_absolute():
|
|
573
|
+
cfg["file"] = str((project_dir / file_value).resolve())
|
|
574
|
+
resolved[name] = cfg
|
|
575
|
+
return resolved
|
|
576
|
+
|
|
577
|
+
|
|
549
578
|
# ============================================================================
|
|
550
579
|
# PROJECT WARNINGS IGNORE
|
|
551
580
|
# ============================================================================
|
|
@@ -97,13 +97,11 @@ class AuthoredQuery(BaseModel):
|
|
|
97
97
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
98
98
|
|
|
99
99
|
type: (
|
|
100
|
-
Literal[
|
|
101
|
-
"sql", "metricflow", "dbt_model", "http", "csv", "values", "schema_resolver"
|
|
102
|
-
]
|
|
100
|
+
Literal["sql", "metricflow", "dbt_model", "http", "csv", "values", "schema"]
|
|
103
101
|
| None
|
|
104
102
|
) = Field(
|
|
105
103
|
default=None,
|
|
106
|
-
description="Query adapter type. Defaults to 'sql'. Options: sql, metricflow, dbt_model, http, csv, values,
|
|
104
|
+
description="Query adapter type. Defaults to 'sql'. Options: sql, metricflow, dbt_model, http, csv, values, schema.",
|
|
107
105
|
)
|
|
108
106
|
|
|
109
107
|
# Source field - can be a string reference or inline source config
|
|
@@ -194,21 +192,21 @@ class AuthoredQuery(BaseModel):
|
|
|
194
192
|
description="File encoding for CSV queries. Default: UTF-8.",
|
|
195
193
|
)
|
|
196
194
|
|
|
197
|
-
#
|
|
195
|
+
# schema fields
|
|
198
196
|
# alias="schema" so YAML authors write `schema: analytics`; Python code uses
|
|
199
197
|
# schema_name to avoid shadowing BaseModel.schema (a Pydantic v2 legacy classmethod).
|
|
200
198
|
schema_name: str | None = Field(
|
|
201
199
|
default=None,
|
|
202
200
|
alias="schema",
|
|
203
|
-
description="Schema name for
|
|
201
|
+
description="Schema name for schema queries (YAML key: schema).",
|
|
204
202
|
)
|
|
205
203
|
table: str | None = Field(
|
|
206
204
|
default=None,
|
|
207
|
-
description="Table name for
|
|
205
|
+
description="Table name for schema queries.",
|
|
208
206
|
)
|
|
209
207
|
column: str | None = Field(
|
|
210
208
|
default=None,
|
|
211
|
-
description="Column name for
|
|
209
|
+
description="Column name for schema queries.",
|
|
212
210
|
)
|
|
213
211
|
|
|
214
212
|
# Values fields (inline data)
|
|
@@ -494,7 +494,7 @@ class DbtModelQuery(Query):
|
|
|
494
494
|
return f"dbt Model: {self.model}"
|
|
495
495
|
|
|
496
496
|
|
|
497
|
-
class
|
|
497
|
+
class SchemaQuery(Query):
|
|
498
498
|
"""Query the LayeredSchemaResolver in-process.
|
|
499
499
|
|
|
500
500
|
Dispatches to list_schemas / list_tables / profile_table / profile_column
|
|
@@ -537,24 +537,24 @@ class SchemaResolverQuery(Query):
|
|
|
537
537
|
def _no_jinja(cls, v: Any) -> Any:
|
|
538
538
|
if isinstance(v, str) and any(tok in v for tok in ("{{", "{%", "{#")):
|
|
539
539
|
raise ValueError(
|
|
540
|
-
"
|
|
540
|
+
"schema fields must be literal strings; "
|
|
541
541
|
"Jinja templates ({{ }}, {% %}, {# #}) are not supported."
|
|
542
542
|
)
|
|
543
543
|
return v
|
|
544
544
|
|
|
545
545
|
@model_validator(mode="after")
|
|
546
|
-
def _field_prerequisites(self) -> "
|
|
546
|
+
def _field_prerequisites(self) -> "SchemaQuery":
|
|
547
547
|
if self.table is not None and self.schema_name is None:
|
|
548
|
-
raise ValueError("
|
|
548
|
+
raise ValueError("schema: 'table' requires 'schema' to be set")
|
|
549
549
|
if self.column is not None and self.table is None:
|
|
550
550
|
raise ValueError(
|
|
551
|
-
"
|
|
551
|
+
"schema: 'column' requires 'table' (and 'schema') to be set"
|
|
552
552
|
)
|
|
553
553
|
return self
|
|
554
554
|
|
|
555
555
|
@property
|
|
556
556
|
def query_type(self) -> str:
|
|
557
|
-
return "
|
|
557
|
+
return "schema"
|
|
558
558
|
|
|
559
559
|
@property
|
|
560
560
|
def source_description(self) -> str:
|
|
@@ -565,7 +565,7 @@ class SchemaResolverQuery(Query):
|
|
|
565
565
|
parts.append(self.table)
|
|
566
566
|
if self.column:
|
|
567
567
|
parts.append(self.column)
|
|
568
|
-
return f"
|
|
568
|
+
return f"schema: {'.'.join(parts)}"
|
|
569
569
|
|
|
570
570
|
|
|
571
571
|
# ============================================================================
|
|
@@ -582,7 +582,7 @@ AnyQuery = (
|
|
|
582
582
|
| HttpQuery
|
|
583
583
|
| DbtModelQuery
|
|
584
584
|
| ValuesQuery
|
|
585
|
-
|
|
|
585
|
+
| SchemaQuery
|
|
586
586
|
)
|
|
587
587
|
|
|
588
588
|
|
|
@@ -594,7 +594,7 @@ VALID_QUERY_TYPES: set[str] = {
|
|
|
594
594
|
"http",
|
|
595
595
|
"dbt_model",
|
|
596
596
|
"values",
|
|
597
|
-
"
|
|
597
|
+
"schema",
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
|
|
@@ -715,13 +715,13 @@ def is_values_query(query: AnyQuery) -> TypeGuard[ValuesQuery]:
|
|
|
715
715
|
return query.query_type == "values"
|
|
716
716
|
|
|
717
717
|
|
|
718
|
-
def
|
|
719
|
-
"""Type guard for schema
|
|
718
|
+
def is_schema_query(query: AnyQuery) -> TypeGuard[SchemaQuery]:
|
|
719
|
+
"""Type guard for schema queries.
|
|
720
720
|
|
|
721
721
|
Args:
|
|
722
722
|
query: Any compiled query
|
|
723
723
|
|
|
724
724
|
Returns:
|
|
725
|
-
True if query is a
|
|
725
|
+
True if query is a SchemaQuery
|
|
726
726
|
"""
|
|
727
|
-
return query.query_type == "
|
|
727
|
+
return query.query_type == "schema"
|
|
@@ -17,7 +17,7 @@ from dataface.core.compile.models.query.normalized import (
|
|
|
17
17
|
DbtModelQuery,
|
|
18
18
|
HttpQuery,
|
|
19
19
|
MetricFlowQuery,
|
|
20
|
-
|
|
20
|
+
SchemaQuery,
|
|
21
21
|
SqlQuery,
|
|
22
22
|
ValuesQuery,
|
|
23
23
|
is_sql_query,
|
|
@@ -62,7 +62,7 @@ def normalize_query(
|
|
|
62
62
|
HttpQuery,
|
|
63
63
|
DbtModelQuery,
|
|
64
64
|
ValuesQuery,
|
|
65
|
-
|
|
65
|
+
SchemaQuery,
|
|
66
66
|
),
|
|
67
67
|
):
|
|
68
68
|
return query_def
|
|
@@ -90,13 +90,13 @@ def normalize_query(
|
|
|
90
90
|
query_type = query_dict.pop("type")
|
|
91
91
|
|
|
92
92
|
# Apply default source if query doesn't have one.
|
|
93
|
-
# ValuesQuery is inline data;
|
|
93
|
+
# ValuesQuery is inline data; SchemaQuery's source is a dbt source name,
|
|
94
94
|
# not a connection reference; HttpQuery uses url, not source — all three are excluded.
|
|
95
95
|
has_source = query_dict.get("source") is not None
|
|
96
96
|
if (
|
|
97
97
|
not has_source
|
|
98
98
|
and default_source
|
|
99
|
-
and query_type not in {"values", "dbt_model", "
|
|
99
|
+
and query_type not in {"values", "dbt_model", "schema", "http"}
|
|
100
100
|
):
|
|
101
101
|
query_dict["source"] = default_source
|
|
102
102
|
|
|
@@ -140,8 +140,8 @@ def normalize_query(
|
|
|
140
140
|
query = DbtModelQuery(**query_dict)
|
|
141
141
|
elif query_type == "values":
|
|
142
142
|
query = ValuesQuery(**query_dict)
|
|
143
|
-
elif query_type == "
|
|
144
|
-
query =
|
|
143
|
+
elif query_type == "schema":
|
|
144
|
+
query = SchemaQuery(**query_dict)
|
|
145
145
|
else:
|
|
146
146
|
# Default to SQL
|
|
147
147
|
query = SqlQuery(**query_dict)
|