dataface 0.1.5.dev206__py3-none-any.whl → 0.1.5.dev237__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 +28 -2
- dataface/agent_api/__init__.py +20 -0
- dataface/agent_api/docs/yaml-reference.md +5 -4
- dataface/agent_api/entity_paths.py +221 -0
- dataface/agent_api/project.py +61 -50
- dataface/agent_api/schema_hints.py +49 -0
- dataface/agent_api/validate.py +48 -0
- dataface/agent_api/validate_query.py +40 -62
- dataface/ai/mcp/server.py +1 -1
- dataface/ai/tools/__init__.py +5 -5
- dataface/cli/commands/_agent_server.py +10 -4
- dataface/cli/commands/query.py +54 -36
- dataface/cli/commands/schema.py +29 -0
- dataface/cli/commands/serve.py +9 -4
- dataface/cli/main.py +13 -0
- dataface/core/compile/__init__.py +11 -3
- dataface/core/compile/compiler.py +128 -126
- dataface/core/compile/config.py +86 -34
- dataface/core/compile/models/face/authored.py +9 -0
- dataface/core/compile/models/face/normalized.py +8 -0
- 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/compile/normalizer.py +1 -0
- 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/alias_index.py +284 -0
- dataface/core/serve/bootstrap.py +22 -8
- dataface/core/serve/server.py +211 -20
- dataface/core/system_views/__init__.py +0 -0
- dataface/core/system_views/expander.py +383 -0
- dataface/core/system_views/loader.py +105 -0
- dataface/core/system_views/models.py +67 -0
- dataface/core/system_views/query_runner.py +248 -0
- dataface/core/system_views/registry.yaml +78 -0
- dataface/core/system_views/router.py +127 -0
- dataface/core/system_views/templates/entity/schema-index.yaml +21 -0
- dataface/core/system_views/templates/entity/source-index.yaml +20 -0
- dataface/core/system_views/templates/entity/table-detail.yaml +30 -0
- dataface/core/system_views/templates/entity/table-index.yaml +41 -0
- dataface/core/system_views/templates/inspector/column.yaml +24 -0
- dataface/core/system_views/templates/inspector/schema.yaml +20 -0
- dataface/core/system_views/templates/inspector/source.yaml +19 -0
- dataface/core/system_views/templates/inspector/table.yaml +23 -0
- dataface/core/system_views/variable_planner.py +327 -0
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev237.dist-info}/METADATA +1 -1
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev237.dist-info}/RECORD +66 -46
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev237.dist-info}/WHEEL +0 -0
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev237.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.5.dev206.dist-info → dataface-0.1.5.dev237.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -206,15 +206,20 @@ chart_focus: revenue_trend # Render only one chart with its dependent variab
|
|
|
206
206
|
details: "Click to expand" # Collapsible section
|
|
207
207
|
expanded_title: "Hide details"
|
|
208
208
|
expanded: false
|
|
209
|
+
|
|
210
|
+
aliases: # Additional URLs that 302-redirect to this face
|
|
211
|
+
- /old-reports/
|
|
212
|
+
- /legacy/sales/
|
|
209
213
|
```
|
|
210
214
|
|
|
211
|
-
Top-level fields (
|
|
215
|
+
Top-level fields (24 total):
|
|
212
216
|
|
|
213
217
|
| Field | Type | Notes |
|
|
214
218
|
|-------|------|-------|
|
|
215
219
|
| `title` | string | Display title |
|
|
216
220
|
| `description` | string | Description text |
|
|
217
221
|
| `tags` | list[string] | Tags for categorization/search |
|
|
222
|
+
| `aliases` | list[string] | See [Aliases](#aliases) below |
|
|
218
223
|
| `text` | string | Markdown body for text-only faces |
|
|
219
224
|
| `source` | string | Default source shorthand (equivalent to `sources.default`) |
|
|
220
225
|
| `sources` | object | `{default: name, <name>: {type: ...}}` |
|
|
@@ -238,6 +243,27 @@ Top-level fields (23 total):
|
|
|
238
243
|
|
|
239
244
|
`face:` as a top-level key is rejected. Put face properties (title, rows, queries, …) directly at the YAML root.
|
|
240
245
|
|
|
246
|
+
### Aliases
|
|
247
|
+
|
|
248
|
+
`aliases:` is a list of absolute URL paths that 302-redirect to this face's canonical file-path URL. Each entry must start with `/`; trailing slashes are normalized automatically.
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
aliases:
|
|
252
|
+
- /old-reports/
|
|
253
|
+
- /legacy/sales/
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Rules:
|
|
257
|
+
- An alias must not collide with a real face file path — the server raises an error at startup if it does.
|
|
258
|
+
- Each alias must be unique across the project — two faces claiming the same alias is a startup error.
|
|
259
|
+
- Aliasing a generated system route (entity or inspector view) is allowed: the redirect takes precedence, letting you override what that URL serves.
|
|
260
|
+
|
|
261
|
+
There are two ways to override an entity/inspector route for a given path:
|
|
262
|
+
- **Face file at that path** — create `faces/entity/warehouse/schema/table.yml` and the server renders it directly (no redirect).
|
|
263
|
+
- **`aliases:` entry** — any face can declare `/entity/…/` as an alias; the server issues a 302 to the declaring face's canonical URL.
|
|
264
|
+
|
|
265
|
+
Both approaches work. A face file wins without a redirect round-trip; an alias lets a face live anywhere and still capture a system-view URL.
|
|
266
|
+
|
|
241
267
|
### Board style
|
|
242
268
|
|
|
243
269
|
`style:` on a face, nested face, or layout section accepts a fixed set of keys — not arbitrary CSS:
|
|
@@ -346,7 +372,7 @@ queries:
|
|
|
346
372
|
- [Bob, 87.1]
|
|
347
373
|
```
|
|
348
374
|
|
|
349
|
-
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `
|
|
375
|
+
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `schema` is internal-only and not part of the authored surface.
|
|
350
376
|
|
|
351
377
|
Common fields (all query types):
|
|
352
378
|
|
dataface/agent_api/__init__.py
CHANGED
|
@@ -53,8 +53,20 @@ from dataface.agent_api.init import (
|
|
|
53
53
|
)
|
|
54
54
|
from dataface.agent_api.project import Project as Project
|
|
55
55
|
from dataface.agent_api.query import QueryFaceResult, query_face
|
|
56
|
+
from dataface.agent_api.schema_hints import (
|
|
57
|
+
SchemaHints as SchemaHints,
|
|
58
|
+
schema_hints as schema_hints,
|
|
59
|
+
)
|
|
56
60
|
from dataface.agent_api.validate import ValidateDashboardArgs, ValidateResult
|
|
61
|
+
from dataface.agent_api.validate_query import (
|
|
62
|
+
QueryDiagnostic as QueryDiagnostic,
|
|
63
|
+
validate_query as validate_query,
|
|
64
|
+
)
|
|
57
65
|
from dataface.core.compile.config import ProjectSourcesConfig as ProjectSourcesConfig
|
|
66
|
+
from dataface.core.errors.structured import StructuredError as StructuredError
|
|
67
|
+
from dataface.core.fonts import get_inter_font_path as get_inter_font_path
|
|
68
|
+
from dataface.core.render.board_links import LinkContext as LinkContext
|
|
69
|
+
from dataface.core.render.errors import RenderError as RenderError
|
|
58
70
|
|
|
59
71
|
__all__ = [
|
|
60
72
|
"DescribeFaceArgs",
|
|
@@ -63,15 +75,23 @@ __all__ = [
|
|
|
63
75
|
"DocsResult",
|
|
64
76
|
"DocsSearchHit",
|
|
65
77
|
"InitResult",
|
|
78
|
+
"LinkContext",
|
|
66
79
|
"Project",
|
|
67
80
|
"ProjectSourcesConfig",
|
|
81
|
+
"QueryDiagnostic",
|
|
68
82
|
"QueryFaceResult",
|
|
69
83
|
"RenderedDashboard",
|
|
84
|
+
"RenderError",
|
|
85
|
+
"SchemaHints",
|
|
86
|
+
"StructuredError",
|
|
70
87
|
"Topic",
|
|
71
88
|
"ValidateDashboardArgs",
|
|
72
89
|
"ValidateResult",
|
|
73
90
|
"describe_face",
|
|
91
|
+
"get_inter_font_path",
|
|
74
92
|
"init_project",
|
|
75
93
|
"query_face",
|
|
76
94
|
"render_dashboard",
|
|
95
|
+
"schema_hints",
|
|
96
|
+
"validate_query",
|
|
77
97
|
]
|
|
@@ -9,6 +9,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
9
9
|
| `title` | str | ✓ | Dashboard title displayed at the top. |
|
|
10
10
|
| `description` | str | ✓ | Description text for the dashboard. |
|
|
11
11
|
| `tags` | list[str] | ✓ | Tags for categorization and search. |
|
|
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. |
|
|
12
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. |
|
|
13
14
|
| `text` | str | ✓ | Markdown text content for text-only sections. |
|
|
14
15
|
| `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. |
|
|
@@ -72,7 +73,7 @@ AuthoredQuery definition from YAML.
|
|
|
72
73
|
|
|
73
74
|
| Field | Type | Optional | Description |
|
|
74
75
|
|-------|------|:--------:|-------------|
|
|
75
|
-
| `type` | enum: "sql", "metricflow", "dbt_model", "http", "csv", "values", "
|
|
76
|
+
| `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
77
|
| `source` | str \| dict[str, Any] | ✓ | Database source reference (name string) or inline source config dict. |
|
|
77
78
|
| `target` | str | ✓ | dbt target name for dbt_model queries (defaults to 'dev'). |
|
|
78
79
|
| `sql` | str | ✓ | SQL query string. Supports Jinja2 templates referencing variables. |
|
|
@@ -92,9 +93,9 @@ AuthoredQuery definition from YAML.
|
|
|
92
93
|
| `filter` | dict[str, Any] | ✓ | Row-level filter applied to CSV or values data. |
|
|
93
94
|
| `delimiter` | str | ✓ | Column delimiter for CSV queries. Default: comma (','). |
|
|
94
95
|
| `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
|
|
96
|
+
| `schema` | str | ✓ | Schema name for schema queries (YAML key: schema). |
|
|
97
|
+
| `table` | str | ✓ | Table name for schema queries. |
|
|
98
|
+
| `column` | str | ✓ | Column name for schema queries. |
|
|
98
99
|
| `rows` | list[dict[str, Any]] | ✓ | Inline data rows for values-type queries (list of row dicts). |
|
|
99
100
|
| `values` | list[list[Any]] | ✓ | Inline column-oriented data for values-type queries (list of lists). |
|
|
100
101
|
| `description` | str | ✓ | Human-readable description of the query. Used by AI search and tooling. |
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Entity URL surfacing and alias typo lint.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- EntityPathInfo: the agent-facing wire shape for a canonical entity URL
|
|
5
|
+
(url, resolves_to: "generic" | "authored", face)
|
|
6
|
+
- entity_paths_for_source/schema/table: compute entity URLs and check the
|
|
7
|
+
alias index to determine whether a face overrides the generic system view
|
|
8
|
+
- entity_paths_list: flat list of EntityPathInfo (source + schema + table level)
|
|
9
|
+
from a SchemaResponse — used for --entity-paths CLI flag and agent discovery
|
|
10
|
+
- validate_entity_aliases: typo lint for /entity/ prefixed aliases — checks
|
|
11
|
+
source names only (config only, no DB connection required).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from dataface.agent_api.schema import SchemaResponse
|
|
22
|
+
from dataface.core.serve.alias_index import AliasIndex
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class EntityPathInfo:
|
|
27
|
+
"""Canonical entity URL with resolution status.
|
|
28
|
+
|
|
29
|
+
Wire shape (pinned by contract test — any change is a breaking API change):
|
|
30
|
+
{"url": "/entity/src/schema/table/", "resolves_to": "generic"}
|
|
31
|
+
{"url": "...", "resolves_to": "authored", "face": "/sales/"}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
url: str
|
|
35
|
+
resolves_to: Literal["generic", "authored"]
|
|
36
|
+
face: str | None = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""Serialize to the agent wire shape, omitting face when generic."""
|
|
40
|
+
d: dict[str, Any] = {"url": self.url, "resolves_to": self.resolves_to}
|
|
41
|
+
if self.face is not None:
|
|
42
|
+
d["face"] = self.face
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def entity_paths_for_source(
|
|
47
|
+
source: str,
|
|
48
|
+
*,
|
|
49
|
+
alias_index: AliasIndex,
|
|
50
|
+
) -> EntityPathInfo:
|
|
51
|
+
"""Return the entity URL info for a source.
|
|
52
|
+
|
|
53
|
+
The canonical URL is /entity/<source>/. When a face aliases it the
|
|
54
|
+
resolves_to is 'authored' and face is the canonical face URL.
|
|
55
|
+
"""
|
|
56
|
+
url = f"/entity/{source}/"
|
|
57
|
+
face = alias_index.lookup(url)
|
|
58
|
+
if face is not None:
|
|
59
|
+
return EntityPathInfo(url=url, resolves_to="authored", face=face)
|
|
60
|
+
return EntityPathInfo(url=url, resolves_to="generic")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def entity_paths_for_schema(
|
|
64
|
+
source: str,
|
|
65
|
+
schema: str,
|
|
66
|
+
*,
|
|
67
|
+
alias_index: AliasIndex,
|
|
68
|
+
) -> EntityPathInfo:
|
|
69
|
+
"""Return the entity URL info for a schema.
|
|
70
|
+
|
|
71
|
+
The canonical URL is /entity/<source>/<schema>/. When a face aliases it
|
|
72
|
+
the resolves_to is 'authored' and face is the canonical face URL.
|
|
73
|
+
"""
|
|
74
|
+
url = f"/entity/{source}/{schema}/"
|
|
75
|
+
face = alias_index.lookup(url)
|
|
76
|
+
if face is not None:
|
|
77
|
+
return EntityPathInfo(url=url, resolves_to="authored", face=face)
|
|
78
|
+
return EntityPathInfo(url=url, resolves_to="generic")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def entity_paths_for_table(
|
|
82
|
+
source: str,
|
|
83
|
+
schema: str,
|
|
84
|
+
table: str,
|
|
85
|
+
*,
|
|
86
|
+
alias_index: AliasIndex,
|
|
87
|
+
) -> EntityPathInfo:
|
|
88
|
+
"""Return the entity URL info for a table.
|
|
89
|
+
|
|
90
|
+
The canonical URL is /entity/<source>/<schema>/<table>/. When a face
|
|
91
|
+
aliases it the resolves_to is 'authored' and face is the canonical face URL.
|
|
92
|
+
"""
|
|
93
|
+
url = f"/entity/{source}/{schema}/{table}/"
|
|
94
|
+
face = alias_index.lookup(url)
|
|
95
|
+
if face is not None:
|
|
96
|
+
return EntityPathInfo(url=url, resolves_to="authored", face=face)
|
|
97
|
+
return EntityPathInfo(url=url, resolves_to="generic")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_entity_aliases(
|
|
101
|
+
aliases: list[str],
|
|
102
|
+
*,
|
|
103
|
+
source_names: frozenset[str],
|
|
104
|
+
) -> list[str]:
|
|
105
|
+
"""Lint /entity/ prefixed aliases against configured source names.
|
|
106
|
+
|
|
107
|
+
Called at dft-validate time (connection-free). Checks that every
|
|
108
|
+
/entity/ URL references a known source, and errors with available
|
|
109
|
+
sources as a hint when not.
|
|
110
|
+
|
|
111
|
+
Returns a list of error message strings (empty = all valid).
|
|
112
|
+
Aliases not prefixed with /entity/ are silently skipped.
|
|
113
|
+
"""
|
|
114
|
+
errors: list[str] = []
|
|
115
|
+
for raw_alias in aliases:
|
|
116
|
+
# Normalize: trailing-slash canonical, strip percent-encoding
|
|
117
|
+
from urllib.parse import unquote
|
|
118
|
+
|
|
119
|
+
alias = unquote(raw_alias)
|
|
120
|
+
if not alias.endswith("/"):
|
|
121
|
+
alias = alias + "/"
|
|
122
|
+
|
|
123
|
+
if not alias.startswith("/entity/"):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Strip leading "/entity/" and trailing "/" to get segments
|
|
127
|
+
inner = alias[len("/entity/") :]
|
|
128
|
+
if inner.endswith("/"):
|
|
129
|
+
inner = inner[:-1]
|
|
130
|
+
segments = [s for s in inner.split("/") if s]
|
|
131
|
+
|
|
132
|
+
if not segments:
|
|
133
|
+
# Bare /entity/ — no validation needed
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Segment 0: source
|
|
137
|
+
source = segments[0]
|
|
138
|
+
if source not in source_names:
|
|
139
|
+
available = sorted(source_names)
|
|
140
|
+
hint = (
|
|
141
|
+
f"available sources: {', '.join(available)}"
|
|
142
|
+
if available
|
|
143
|
+
else "no sources configured"
|
|
144
|
+
)
|
|
145
|
+
errors.append(
|
|
146
|
+
f"Entity alias {raw_alias!r} references unknown source {source!r}. "
|
|
147
|
+
f"Did you mean one of: {hint}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return errors
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def entity_paths_list(
|
|
154
|
+
schema_response: SchemaResponse,
|
|
155
|
+
*,
|
|
156
|
+
alias_index: AliasIndex,
|
|
157
|
+
) -> list[EntityPathInfo]:
|
|
158
|
+
"""Build a flat list of EntityPathInfo from a SchemaResponse.
|
|
159
|
+
|
|
160
|
+
Iterates all sources → schemas → tables in the response and returns one
|
|
161
|
+
EntityPathInfo per source, per schema, and per table, checking the alias
|
|
162
|
+
index for author overrides at each level.
|
|
163
|
+
"""
|
|
164
|
+
result: list[EntityPathInfo] = []
|
|
165
|
+
for source_name, source_data in schema_response.sources.items():
|
|
166
|
+
result.append(entity_paths_for_source(source_name, alias_index=alias_index))
|
|
167
|
+
for schema_name, schema_data in (source_data.get("schemas") or {}).items():
|
|
168
|
+
result.append(
|
|
169
|
+
entity_paths_for_schema(
|
|
170
|
+
source_name, schema_name, alias_index=alias_index
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
for table_name in schema_data.get("tables") or {}:
|
|
174
|
+
result.append(
|
|
175
|
+
entity_paths_for_table(
|
|
176
|
+
source_name,
|
|
177
|
+
schema_name,
|
|
178
|
+
table_name,
|
|
179
|
+
alias_index=alias_index,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_alias_index_for_project(project_dir: Path) -> AliasIndex:
|
|
186
|
+
"""Build an AliasIndex from a project directory.
|
|
187
|
+
|
|
188
|
+
Uses the same faces_at_root detection logic as the server. Intended for
|
|
189
|
+
CLI commands (dft schema --entity-paths) that need alias lookup without
|
|
190
|
+
starting a server.
|
|
191
|
+
"""
|
|
192
|
+
from dataface.core.serve.alias_index import AliasIndex
|
|
193
|
+
|
|
194
|
+
faces_dir = project_dir / "faces"
|
|
195
|
+
faces_at_root = faces_dir.is_dir()
|
|
196
|
+
return AliasIndex.build(
|
|
197
|
+
project_dir=project_dir,
|
|
198
|
+
faces_dir=faces_dir,
|
|
199
|
+
faces_at_root=faces_at_root,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def entity_alias_errors_for_file(
|
|
204
|
+
face_file: Path,
|
|
205
|
+
source_names: frozenset[str],
|
|
206
|
+
) -> list[str]:
|
|
207
|
+
"""Read aliases from a face file and lint any /entity/ prefixed ones.
|
|
208
|
+
|
|
209
|
+
Source-name check only — no DB connection, no resolver. Called from the
|
|
210
|
+
validate path which must stay connection-free.
|
|
211
|
+
|
|
212
|
+
Returns a list of error message strings; empty means no entity alias issues.
|
|
213
|
+
Silently returns [] when the file cannot be parsed (compile catches that).
|
|
214
|
+
"""
|
|
215
|
+
from dataface.core.serve.alias_index import _read_aliases_from_file
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
aliases = _read_aliases_from_file(face_file)
|
|
219
|
+
except ValueError:
|
|
220
|
+
return [] # compile path reports alias parse errors with better context
|
|
221
|
+
return validate_entity_aliases(aliases, source_names=source_names)
|
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
|
|
|
@@ -27,8 +28,8 @@ from dataface.agent_api.validate import (
|
|
|
27
28
|
)
|
|
28
29
|
from dataface.core.compile.config import (
|
|
29
30
|
ProjectSourcesConfig,
|
|
30
|
-
get_project_sources,
|
|
31
31
|
get_project_warnings_ignore,
|
|
32
|
+
load_project_sources,
|
|
32
33
|
)
|
|
33
34
|
from dataface.core.execute.adapters.adapter_registry import (
|
|
34
35
|
AdapterRegistry,
|
|
@@ -39,6 +40,7 @@ from dataface.core.execute.duckdb_cache import DuckDBCache
|
|
|
39
40
|
if TYPE_CHECKING:
|
|
40
41
|
from dataface.core.dashboard import RenderedDashboard, RenderFormat
|
|
41
42
|
from dataface.core.execute.source_resolver import SourceResolver
|
|
43
|
+
from dataface.core.render.board_links import LinkContext
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
@dataclass(init=False)
|
|
@@ -55,7 +57,6 @@ class Project:
|
|
|
55
57
|
project_root: Path
|
|
56
58
|
cache: DuckDBCache | None
|
|
57
59
|
_owns_registry: bool
|
|
58
|
-
_adapter_registry: AdapterRegistry | None
|
|
59
60
|
_read_only: bool
|
|
60
61
|
_dbt_project_path: Path | None
|
|
61
62
|
_connection_string: str | None
|
|
@@ -64,22 +65,26 @@ class Project:
|
|
|
64
65
|
_duckdb_config: dict[str, Any] | None
|
|
65
66
|
_allow_external_access_in_readonly: bool
|
|
66
67
|
_resolver: SourceResolver | None
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
_sources: ProjectSourcesConfig
|
|
69
|
+
_warnings_ignore: frozenset[str]
|
|
69
70
|
|
|
70
71
|
def __init__(
|
|
71
72
|
self,
|
|
72
73
|
project_root: Path,
|
|
73
74
|
cache: DuckDBCache | None = None,
|
|
74
75
|
adapter_registry: AdapterRegistry | None = None,
|
|
76
|
+
read_only: bool = True,
|
|
75
77
|
) -> None:
|
|
76
78
|
self.project_root = project_root
|
|
77
79
|
self.cache = cache
|
|
78
80
|
# We own the registry only when we'll lazy-build it ourselves. An injected
|
|
79
81
|
# registry belongs to the caller; we use it but don't close it on their behalf.
|
|
80
82
|
self._owns_registry = adapter_registry is None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
if adapter_registry is not None:
|
|
84
|
+
# cached_property stores in __dict__; pre-populating makes the descriptor
|
|
85
|
+
# short-circuit the build on first access.
|
|
86
|
+
self.__dict__["adapter_registry"] = adapter_registry
|
|
87
|
+
self._read_only = read_only
|
|
83
88
|
self._dbt_project_path = None
|
|
84
89
|
self._connection_string = None
|
|
85
90
|
self._dialect = "duckdb"
|
|
@@ -87,8 +92,8 @@ class Project:
|
|
|
87
92
|
self._duckdb_config = None
|
|
88
93
|
self._allow_external_access_in_readonly = False
|
|
89
94
|
self._resolver = None
|
|
90
|
-
self.
|
|
91
|
-
self.
|
|
95
|
+
self._sources = load_project_sources(self.project_root)
|
|
96
|
+
self._warnings_ignore = get_project_warnings_ignore(self.project_root)
|
|
92
97
|
|
|
93
98
|
@classmethod
|
|
94
99
|
def open(
|
|
@@ -116,8 +121,9 @@ class Project:
|
|
|
116
121
|
Call ``refresh()`` to close the current registry and force a rebuild on the
|
|
117
122
|
next access.
|
|
118
123
|
"""
|
|
119
|
-
project = cls(
|
|
120
|
-
|
|
124
|
+
project = cls(
|
|
125
|
+
project_root=Path(project_dir).resolve(), cache=cache, read_only=read_only
|
|
126
|
+
)
|
|
121
127
|
project._dbt_project_path = dbt_project_path
|
|
122
128
|
project._connection_string = connection_string
|
|
123
129
|
project._dialect = dialect
|
|
@@ -133,7 +139,7 @@ class Project:
|
|
|
133
139
|
def __exit__(self, *exc_info: object) -> None:
|
|
134
140
|
self.close()
|
|
135
141
|
|
|
136
|
-
@
|
|
142
|
+
@cached_property
|
|
137
143
|
def adapter_registry(self) -> AdapterRegistry:
|
|
138
144
|
"""Lazily build the registry on first access; cached thereafter.
|
|
139
145
|
|
|
@@ -141,65 +147,67 @@ class Project:
|
|
|
141
147
|
own it (i.e. it was not injected at construction time).
|
|
142
148
|
When constructed with an injected registry, returns it directly.
|
|
143
149
|
"""
|
|
144
|
-
|
|
145
|
-
self.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
156
|
-
return self._adapter_registry
|
|
150
|
+
return build_adapter_registry(
|
|
151
|
+
self.project_root,
|
|
152
|
+
read_only=self._read_only,
|
|
153
|
+
dbt_project_path=self._dbt_project_path,
|
|
154
|
+
connection_string=self._connection_string,
|
|
155
|
+
profile_type=self._dialect,
|
|
156
|
+
target=self._target,
|
|
157
|
+
duckdb_config=self._duckdb_config,
|
|
158
|
+
allow_external_access_in_readonly=self._allow_external_access_in_readonly,
|
|
159
|
+
resolver=self._resolver,
|
|
160
|
+
)
|
|
157
161
|
|
|
158
162
|
def refresh(self) -> None:
|
|
159
163
|
"""Policy-free rebuild primitive.
|
|
160
164
|
|
|
161
165
|
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
|
|
166
|
+
and clears it so the next access rebuilds from disk. Also re-reads
|
|
167
|
+
sources and warnings_ignore from disk.
|
|
164
168
|
|
|
165
169
|
For projects constructed with an injected ``adapter_registry``: skips the
|
|
166
|
-
registry rebuild (build arguments are not available), but still
|
|
167
|
-
config
|
|
170
|
+
registry rebuild (build arguments are not available), but still re-reads
|
|
171
|
+
the config fields.
|
|
168
172
|
"""
|
|
169
|
-
if self._owns_registry:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.
|
|
174
|
-
self._warnings_ignore_cache = None
|
|
173
|
+
if self._owns_registry and "adapter_registry" in self.__dict__:
|
|
174
|
+
self.adapter_registry.close()
|
|
175
|
+
del self.__dict__["adapter_registry"]
|
|
176
|
+
self._sources = load_project_sources(self.project_root)
|
|
177
|
+
self._warnings_ignore = get_project_warnings_ignore(self.project_root)
|
|
175
178
|
|
|
176
179
|
def close(self) -> None:
|
|
177
180
|
"""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 ─────────────────────────────────────────────────────
|
|
181
|
+
if self._owns_registry and "adapter_registry" in self.__dict__:
|
|
182
|
+
self.adapter_registry.close()
|
|
183
|
+
del self.__dict__["adapter_registry"]
|
|
183
184
|
|
|
185
|
+
# Read-only views: project lifecycle owns these; external writers go through refresh().
|
|
186
|
+
@property
|
|
184
187
|
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
|
|
188
|
+
return self._sources
|
|
189
189
|
|
|
190
|
+
@property
|
|
190
191
|
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
|
|
192
|
+
return self._warnings_ignore
|
|
195
193
|
|
|
196
194
|
# ── Verb forwarders ──────────────────────────────────────────────────────
|
|
197
195
|
|
|
198
196
|
def validate(self, face_path: Path) -> ValidateResult:
|
|
199
|
-
|
|
197
|
+
result = _validate.validate(face_path, project_dir=self.project_root)
|
|
198
|
+
annotated = _validate.annotate_with_entity_lint([result], project=self)
|
|
199
|
+
return annotated[0]
|
|
200
200
|
|
|
201
201
|
def validate_paths(self, paths: list[Path] | None) -> list[ValidateResult]:
|
|
202
|
-
|
|
202
|
+
results = _validate.validate_paths(paths, project_dir=self.project_root)
|
|
203
|
+
return _validate.annotate_with_entity_lint(results, project=self)
|
|
204
|
+
|
|
205
|
+
def _source_names(self) -> frozenset[str]:
|
|
206
|
+
"""Return the configured source names for entity alias validation.
|
|
207
|
+
|
|
208
|
+
Reads from the project config file — no adapter registry or DB connection needed.
|
|
209
|
+
"""
|
|
210
|
+
return frozenset(self._sources.sources.keys())
|
|
203
211
|
|
|
204
212
|
def describe_face(self, path: Path) -> _describe.DescribeFaceResult:
|
|
205
213
|
return _describe.describe_face(path, project_dir=self.project_root)
|
|
@@ -293,6 +301,7 @@ class Project:
|
|
|
293
301
|
scale: float | None = None,
|
|
294
302
|
ignore_codes: set[str] | None = None,
|
|
295
303
|
max_workers: int | None = None,
|
|
304
|
+
link_context: LinkContext | None = None,
|
|
296
305
|
) -> RenderedDashboard:
|
|
297
306
|
return _dashboards.render_dashboard(
|
|
298
307
|
path=path,
|
|
@@ -300,6 +309,7 @@ class Project:
|
|
|
300
309
|
variables=variables,
|
|
301
310
|
adapter_registry=self.adapter_registry,
|
|
302
311
|
project_dir=self.project_root,
|
|
312
|
+
project_sources=self.sources,
|
|
303
313
|
duckdb_cache=self.cache,
|
|
304
314
|
format=format,
|
|
305
315
|
use_cache=use_cache,
|
|
@@ -308,5 +318,6 @@ class Project:
|
|
|
308
318
|
scale=scale,
|
|
309
319
|
ignore_codes=ignore_codes,
|
|
310
320
|
max_workers=max_workers,
|
|
311
|
-
warnings_ignore=self.warnings_ignore
|
|
321
|
+
warnings_ignore=self.warnings_ignore,
|
|
322
|
+
link_context=link_context,
|
|
312
323
|
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Stateless view of compile-time schema constants for authoring agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from dataface.core.compile.config import list_built_in_themes
|
|
10
|
+
from dataface.core.compile.models.chart.authored import (
|
|
11
|
+
_INTERNAL_CHART_TYPES,
|
|
12
|
+
CHART_TYPE_DISPLAY,
|
|
13
|
+
ChartType,
|
|
14
|
+
)
|
|
15
|
+
from dataface.core.compile.models.variable.authored import VariableInputType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChartTypeDisplay(BaseModel):
|
|
19
|
+
label: str
|
|
20
|
+
icon: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SchemaHints(BaseModel):
|
|
24
|
+
chart_types: list[str]
|
|
25
|
+
input_types: list[str]
|
|
26
|
+
theme_names: list[str]
|
|
27
|
+
chart_type_display: dict[str, ChartTypeDisplay]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def schema_hints() -> SchemaHints:
|
|
31
|
+
"""Return compile-time schema constants for Dataface authoring agents.
|
|
32
|
+
|
|
33
|
+
Takes no arguments and reads no process state. Two consecutive calls
|
|
34
|
+
return equal results.
|
|
35
|
+
"""
|
|
36
|
+
public_chart_types = [t for t in ChartType if t not in _INTERNAL_CHART_TYPES]
|
|
37
|
+
return SchemaHints(
|
|
38
|
+
chart_types=[t.value for t in public_chart_types],
|
|
39
|
+
input_types=list(typing.get_args(VariableInputType)),
|
|
40
|
+
theme_names=list_built_in_themes(),
|
|
41
|
+
chart_type_display={
|
|
42
|
+
t.value: ChartTypeDisplay(
|
|
43
|
+
label=CHART_TYPE_DISPLAY[t]["label"],
|
|
44
|
+
icon=CHART_TYPE_DISPLAY[t]["icon"],
|
|
45
|
+
)
|
|
46
|
+
for t in public_chart_types
|
|
47
|
+
if t in CHART_TYPE_DISPLAY
|
|
48
|
+
},
|
|
49
|
+
)
|