dataface 0.1.5.dev177__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 +6 -4
- dataface/agent_api/cache.py +21 -2
- dataface/agent_api/dashboards.py +4 -2
- dataface/agent_api/docs/yaml-reference.md +26 -6
- dataface/agent_api/files.py +262 -0
- dataface/agent_api/project.py +46 -39
- dataface/agent_api/render_face.py +7 -0
- dataface/agent_api/surface_aliases.yaml +4 -0
- dataface/ai/__init__.py +1 -2
- dataface/ai/agent.py +30 -10
- dataface/ai/generate_sql.py +9 -24
- dataface/ai/prompts/sql-guidance.md +8 -0
- dataface/ai/prompts/sql-system.md +13 -0
- dataface/ai/prompts/system.md +19 -0
- dataface/ai/prompts.py +77 -64
- dataface/ai/schema_context.py +39 -14
- dataface/ai/skills/dashboard-build/SKILL.md +5 -0
- dataface/ai/skills/data-exploration/SKILL.md +75 -0
- dataface/ai/tool_schemas.py +28 -0
- dataface/ai/tools/__init__.py +48 -0
- dataface/cli/commands/chat.py +144 -3
- dataface/cli/commands/query.py +92 -100
- dataface/cli/commands/render.py +30 -34
- 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 +39 -92
- dataface/core/compile/models/config.py +2 -17
- dataface/core/compile/models/face/authored.py +10 -0
- dataface/core/compile/models/face/normalized.py +10 -0
- dataface/core/compile/models/face/resolved.py +1 -0
- dataface/core/compile/models/query/authored.py +6 -8
- dataface/core/compile/models/query/normalized.py +13 -13
- dataface/core/compile/models/style/resolved.py +4 -0
- dataface/core/compile/models/style/theme.py +45 -2
- dataface/core/compile/normalize_queries.py +6 -6
- dataface/core/compile/normalizer.py +1 -0
- dataface/core/compile/sizing.py +10 -12
- dataface/core/dashboard.py +18 -16
- dataface/core/defaults/default_config.yml +2 -7
- dataface/core/defaults/themes/_base.yaml +10 -1
- dataface/core/defaults/themes/cream.yaml +3 -1
- dataface/core/defaults/themes/editorial.yaml +4 -1
- 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 +23 -6
- dataface/core/execute/adapters/{schema_resolver_adapter.py → schema_adapter.py} +8 -8
- dataface/core/execute/source_registry.py +17 -43
- dataface/core/inspect/renderer.py +28 -21
- dataface/core/inspect/resolver.py +28 -1
- 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 +4 -2
- dataface/core/render/faces.py +17 -14
- dataface/core/render/nav.py +185 -0
- dataface/core/render/renderer.py +7 -4
- dataface/core/resolve_face.py +7 -2
- dataface/core/serve/bootstrap.py +22 -8
- dataface/core/serve/server.py +213 -10
- dataface/core/serve/templates/nav.yml +12 -0
- dataface/integrations/markdown.py +31 -9
- {dataface-0.1.5.dev177.dist-info → dataface-0.1.5.dev215.dist-info}/METADATA +1 -1
- {dataface-0.1.5.dev177.dist-info → dataface-0.1.5.dev215.dist-info}/RECORD +73 -65
- {dataface-0.1.5.dev177.dist-info → dataface-0.1.5.dev215.dist-info}/WHEEL +0 -0
- {dataface-0.1.5.dev177.dist-info → dataface-0.1.5.dev215.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.5.dev177.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
|
|
|
@@ -597,12 +597,14 @@ support: # Optional support line (same shape: value/label/format/
|
|
|
597
597
|
glyph: "▲"
|
|
598
598
|
tone: positive
|
|
599
599
|
|
|
600
|
-
# table — renders all query columns unless `style.columns` selects a subset
|
|
600
|
+
# table — renders all query columns unless `style.columns` selects a subset.
|
|
601
|
+
# `columns` is a MAPPING keyed by column name (NOT a list). Omit it to show
|
|
602
|
+
# every query column; include it to choose a subset and/or style columns.
|
|
601
603
|
type: table
|
|
602
604
|
style:
|
|
603
605
|
columns:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
+
order_id: {} # include with default styling
|
|
607
|
+
amount: # the key is the column name
|
|
606
608
|
label: Amount
|
|
607
609
|
format: currency_whole
|
|
608
610
|
align: right # left | center | right
|
dataface/agent_api/cache.py
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
"""Cache constructors at the agent_api boundary."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from contextlib import contextmanager
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
from dataface.core.execute.duckdb_cache import DuckDBCache, open_cache_from_env
|
|
7
|
+
|
|
8
|
+
__all__ = ["cache_from_env", "open_cache_from_env"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def cache_from_env() -> Generator[DuckDBCache | None]:
|
|
13
|
+
"""Open DFT_CACHE_PATH-backed cache (or None) and close it on exit.
|
|
14
|
+
|
|
15
|
+
Use for CLI verbs and composition roots that need a cache scoped to
|
|
16
|
+
a single command invocation. Long-lived processes (server boot, MCP
|
|
17
|
+
server) use ``open_cache_from_env()`` directly and own the lifecycle.
|
|
18
|
+
"""
|
|
19
|
+
cache = open_cache_from_env()
|
|
20
|
+
try:
|
|
21
|
+
yield cache
|
|
22
|
+
finally:
|
|
23
|
+
if cache is not None:
|
|
24
|
+
cache.close()
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -79,7 +79,7 @@ class RenderDashboardArgs(BaseModel):
|
|
|
79
79
|
variables: dict[str, Any] | None = Field(
|
|
80
80
|
None, description="Variable values to apply to the dashboard"
|
|
81
81
|
)
|
|
82
|
-
format: Literal["json", "text", "yaml", "svg"] | None = Field(
|
|
82
|
+
format: Literal["json", "text", "yaml", "svg", "terminal"] | None = Field(
|
|
83
83
|
None,
|
|
84
84
|
description=(
|
|
85
85
|
"Output format. 'json' (default) returns resolved chart "
|
|
@@ -89,7 +89,9 @@ class RenderDashboardArgs(BaseModel):
|
|
|
89
89
|
"YAML with inline data — valid input for re-compilation, "
|
|
90
90
|
"ideal for round-trip editing. 'svg' returns the rendered "
|
|
91
91
|
"dashboard as inline SVG (under result['data']) for hosts "
|
|
92
|
-
"that embed the rendered output directly."
|
|
92
|
+
"that embed the rendered output directly. 'terminal' returns "
|
|
93
|
+
"the charts as ANSI text (under result['data']) for display "
|
|
94
|
+
"in a terminal."
|
|
93
95
|
),
|
|
94
96
|
)
|
|
95
97
|
as_link: bool = Field(
|
|
@@ -11,6 +11,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
11
11
|
| `tags` | list[str] | ✓ | Tags for categorization and search. |
|
|
12
12
|
| `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
13
|
| `text` | str | ✓ | Markdown text content for text-only sections. |
|
|
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. |
|
|
14
15
|
| `sources` | [SourcesSection](#sourcessection) | ✓ | Database source configuration. Use 'default:' to set the default source for all queries. |
|
|
15
16
|
| `source` | str | ✓ | Default source name shorthand (equivalent to sources.default). Sets the connection for all queries. |
|
|
16
17
|
| `variables` | dict[str, [Variable](#variable) \| VariableRef] | ✓ | Variable definitions for dynamic filtering and UI controls. |
|
|
@@ -71,7 +72,7 @@ AuthoredQuery definition from YAML.
|
|
|
71
72
|
|
|
72
73
|
| Field | Type | Optional | Description |
|
|
73
74
|
|-------|------|:--------:|-------------|
|
|
74
|
-
| `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. |
|
|
75
76
|
| `source` | str \| dict[str, Any] | ✓ | Database source reference (name string) or inline source config dict. |
|
|
76
77
|
| `target` | str | ✓ | dbt target name for dbt_model queries (defaults to 'dev'). |
|
|
77
78
|
| `sql` | str | ✓ | SQL query string. Supports Jinja2 templates referencing variables. |
|
|
@@ -91,9 +92,9 @@ AuthoredQuery definition from YAML.
|
|
|
91
92
|
| `filter` | dict[str, Any] | ✓ | Row-level filter applied to CSV or values data. |
|
|
92
93
|
| `delimiter` | str | ✓ | Column delimiter for CSV queries. Default: comma (','). |
|
|
93
94
|
| `encoding` | str | ✓ | File encoding for CSV queries. Default: UTF-8. |
|
|
94
|
-
| `schema` | str | ✓ | Schema name for
|
|
95
|
-
| `table` | str | ✓ | Table name for
|
|
96
|
-
| `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. |
|
|
97
98
|
| `rows` | list[dict[str, Any]] | ✓ | Inline data rows for values-type queries (list of row dicts). |
|
|
98
99
|
| `values` | list[list[Any]] | ✓ | Inline column-oriented data for values-type queries (list of lists). |
|
|
99
100
|
| `description` | str | ✓ | Human-readable description of the query. Used by AI search and tooling. |
|
|
@@ -490,6 +491,8 @@ Authored overlay for Style — all fields optional. Adds CSS shorthand coercers.
|
|
|
490
491
|
| `layout` | [LayoutStyle](#layoutstyle) | ✓ | Layout container styles (rows, cols, grid, tabs, details). |
|
|
491
492
|
| `variables` | [VariablesStyle](#variablesstyle) | ✓ | Variable controls chrome style. |
|
|
492
493
|
| `page` | [PageStyle](#pagestyle) | ✓ | Page-level canvas style (behind the board). |
|
|
494
|
+
| `footer` | [FooterStyle](#footerstyle) | ✓ | Page footer chrome visibility. |
|
|
495
|
+
| `timestamp` | [TimestampStyle](#timestampstyle) | ✓ | Render-timestamp chrome: visibility, format, and font. |
|
|
493
496
|
| `formats` | dict[str, str] | ✓ | Format alias map; None means no aliases at this cascade level. |
|
|
494
497
|
| `palettes` | dict[str, str] | ✓ | Theme palette role assignments: open dict mapping role name to palette file name. Default seed: chrome, negative, positive, warning, category, sequence, diverge. |
|
|
495
498
|
| `roles` | dict[str, str] | ✓ | Optional top-level theme role aliases: bare name → role.alias. e.g. ink: chrome.heading |
|
|
@@ -1063,8 +1066,7 @@ Authored overlay for BoardStyle. Face-level structural dimensions. Do NOT cascad
|
|
|
1063
1066
|
| Field | Type | Optional | Description |
|
|
1064
1067
|
|-------|------|:--------:|-------------|
|
|
1065
1068
|
| `width` | float | ✓ | Board width in pixels. |
|
|
1066
|
-
| `
|
|
1067
|
-
| `min_height` | float | ✓ | Minimum row height in pixels. |
|
|
1069
|
+
| `min_height` | float | ✓ | Minimum board height in pixels. |
|
|
1068
1070
|
| `margin` | float | ✓ | Board outer margin in pixels. |
|
|
1069
1071
|
| `card_padding` | float | ✓ | Padding added to each card side in pixels. |
|
|
1070
1072
|
| `card_gap` | float | ✓ | Gap between cards in pixels. |
|
|
@@ -1221,6 +1223,24 @@ Authored overlay for PageStyle. Page-level (outer HTML canvas) styling.
|
|
|
1221
1223
|
|-------|------|:--------:|-------------|
|
|
1222
1224
|
| `background` | str | ✓ | Page canvas background color (behind the face board). |
|
|
1223
1225
|
|
|
1226
|
+
<a id="footerstyle"></a>
|
|
1227
|
+
## FooterStyle
|
|
1228
|
+
Authored overlay for FooterStyle. Authored visibility toggle for the page footer chrome.
|
|
1229
|
+
|
|
1230
|
+
| Field | Type | Optional | Description |
|
|
1231
|
+
|-------|------|:--------:|-------------|
|
|
1232
|
+
| `visible` | bool | ✓ | Show the footer attribution line. |
|
|
1233
|
+
|
|
1234
|
+
<a id="timestampstyle"></a>
|
|
1235
|
+
## TimestampStyle
|
|
1236
|
+
Authored overlay for TimestampStyle. Authored timestamp chrome: visibility, strftime format, and font.
|
|
1237
|
+
|
|
1238
|
+
| Field | Type | Optional | Description |
|
|
1239
|
+
|-------|------|:--------:|-------------|
|
|
1240
|
+
| `visible` | bool | ✓ | Show the render-timestamp line. |
|
|
1241
|
+
| `format` | str | ✓ | strftime format string for the render timestamp. |
|
|
1242
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Timestamp font style overrides (size, color, weight, ...). |
|
|
1243
|
+
|
|
1224
1244
|
<a id="spacingvalues"></a>
|
|
1225
1245
|
## SpacingValues
|
|
1226
1246
|
Pre-parsed CSS spacing (margin/padding).
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""General project-file tools for the chat agent.
|
|
2
|
+
|
|
3
|
+
Claude-Code-style primitives — read, write, edit, glob, grep — that let an
|
|
4
|
+
agent author and save faces directly, rather than via a domain-specific
|
|
5
|
+
"write a dashboard" verb. The agent composes these with the typed dft tools
|
|
6
|
+
(``validate_dashboard``, ``render_dashboard``, ``schema``, ``execute_query``).
|
|
7
|
+
|
|
8
|
+
Every path is resolved against and confined to the project root. A path that
|
|
9
|
+
escapes the root (``..`` traversal, absolute path outside, symlink out) is
|
|
10
|
+
refused — these tools never touch the wider filesystem.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
# Cap grep/glob payloads — read_file returns full content (model must handle size).
|
|
20
|
+
_MAX_GREP_MATCHES = 200
|
|
21
|
+
_MAX_GLOB_MATCHES = 500
|
|
22
|
+
|
|
23
|
+
# Directory names to skip on whole-tree scans — avoids traversing .venv,
|
|
24
|
+
# .git, node_modules, target, etc. which can be huge and contain no user code.
|
|
25
|
+
_SKIP_DIRS = frozenset(
|
|
26
|
+
{".git", ".venv", "venv", "node_modules", "target", "__pycache__", ".tox"}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReadFileArgs(BaseModel):
|
|
31
|
+
"""Read a UTF-8 text file from the project. Path is relative to the project root."""
|
|
32
|
+
|
|
33
|
+
path: str = Field(description="File path relative to the project root.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WriteFileArgs(BaseModel):
|
|
37
|
+
"""Write a UTF-8 text file in the project, creating parent directories.
|
|
38
|
+
|
|
39
|
+
Overwrites an existing file. To change part of an existing file, prefer
|
|
40
|
+
edit_file so you don't clobber content you haven't read. Path is relative
|
|
41
|
+
to the project root and may not escape it.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
path: str = Field(description="File path relative to the project root.")
|
|
45
|
+
content: str = Field(description="Full file contents to write.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EditFileArgs(BaseModel):
|
|
49
|
+
"""Replace an exact string in a project file. old_string must occur exactly once.
|
|
50
|
+
|
|
51
|
+
Fails (writing nothing) if old_string is absent or appears more than once —
|
|
52
|
+
include enough surrounding context to make it unique. Path is relative to
|
|
53
|
+
the project root and may not escape it.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
path: str = Field(description="File path relative to the project root.")
|
|
57
|
+
old_string: str = Field(description="Exact text to replace (must be unique).")
|
|
58
|
+
new_string: str = Field(description="Replacement text.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GlobFilesArgs(BaseModel):
|
|
62
|
+
"""List files matching a glob pattern under the project root (e.g. 'faces/*.yml')."""
|
|
63
|
+
|
|
64
|
+
pattern: str = Field(description="Glob pattern relative to the project root.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GrepFilesArgs(BaseModel):
|
|
68
|
+
"""Search file contents for a substring under the project root.
|
|
69
|
+
|
|
70
|
+
Optionally restrict to files matching a glob (e.g. only 'faces/**/*.yml').
|
|
71
|
+
Skips .git, .venv, node_modules, target, and other large non-user directories.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
pattern: str = Field(description="Substring to search for (case-sensitive).")
|
|
75
|
+
glob: str | None = Field(
|
|
76
|
+
default=None, description="Optional glob to restrict which files are searched."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ReadFileResult(BaseModel):
|
|
81
|
+
success: bool
|
|
82
|
+
path: str
|
|
83
|
+
content: str | None = None
|
|
84
|
+
error: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class WriteFileResult(BaseModel):
|
|
88
|
+
success: bool
|
|
89
|
+
path: str
|
|
90
|
+
bytes_written: int = 0
|
|
91
|
+
error: str | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class EditFileResult(BaseModel):
|
|
95
|
+
success: bool
|
|
96
|
+
path: str
|
|
97
|
+
replacements: int = 0
|
|
98
|
+
error: str | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class GlobResult(BaseModel):
|
|
102
|
+
success: bool
|
|
103
|
+
matches: list[str] = Field(default_factory=list)
|
|
104
|
+
truncated: bool = False
|
|
105
|
+
error: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class GrepMatch(BaseModel):
|
|
109
|
+
path: str
|
|
110
|
+
line_number: int
|
|
111
|
+
line: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class GrepResult(BaseModel):
|
|
115
|
+
success: bool
|
|
116
|
+
matches: list[GrepMatch] = Field(default_factory=list)
|
|
117
|
+
truncated: bool = False
|
|
118
|
+
error: str | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_within(project_dir: Path, raw: str) -> Path:
|
|
122
|
+
"""Resolve ``raw`` against the project root, refusing anything that escapes it."""
|
|
123
|
+
root = project_dir.resolve()
|
|
124
|
+
candidate = Path(raw)
|
|
125
|
+
candidate = candidate if candidate.is_absolute() else root / candidate
|
|
126
|
+
resolved = candidate.resolve()
|
|
127
|
+
if resolved != root and root not in resolved.parents:
|
|
128
|
+
raise ValueError(f"path {raw!r} escapes the project root")
|
|
129
|
+
return resolved
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _rel(project_dir: Path, p: Path) -> str:
|
|
133
|
+
return str(p.resolve().relative_to(project_dir.resolve()))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _walk_project(root: Path) -> list[Path]:
|
|
137
|
+
"""Return files under root, skipping known large/irrelevant directories."""
|
|
138
|
+
result: list[Path] = []
|
|
139
|
+
for item in sorted(root.iterdir()):
|
|
140
|
+
if item.is_dir():
|
|
141
|
+
if item.name in _SKIP_DIRS or item.name.startswith("."):
|
|
142
|
+
continue
|
|
143
|
+
result.extend(_walk_project(item))
|
|
144
|
+
elif item.is_file():
|
|
145
|
+
result.append(item)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_file(path: str, project_dir: Path) -> ReadFileResult:
|
|
150
|
+
try:
|
|
151
|
+
target = _resolve_within(project_dir, path)
|
|
152
|
+
except ValueError as exc:
|
|
153
|
+
return ReadFileResult(success=False, path=path, error=str(exc))
|
|
154
|
+
if not target.is_file():
|
|
155
|
+
return ReadFileResult(success=False, path=path, error=f"file not found: {path}")
|
|
156
|
+
try:
|
|
157
|
+
content = target.read_text(encoding="utf-8")
|
|
158
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
159
|
+
return ReadFileResult(success=False, path=path, error=str(exc))
|
|
160
|
+
return ReadFileResult(success=True, path=_rel(project_dir, target), content=content)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def write_file(path: str, content: str, project_dir: Path) -> WriteFileResult:
|
|
164
|
+
try:
|
|
165
|
+
target = _resolve_within(project_dir, path)
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
return WriteFileResult(success=False, path=path, error=str(exc))
|
|
168
|
+
try:
|
|
169
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
target.write_text(content, encoding="utf-8")
|
|
171
|
+
except OSError as exc:
|
|
172
|
+
return WriteFileResult(success=False, path=path, error=str(exc))
|
|
173
|
+
return WriteFileResult(
|
|
174
|
+
success=True,
|
|
175
|
+
path=_rel(project_dir, target),
|
|
176
|
+
bytes_written=len(content.encode("utf-8")),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def edit_file(
|
|
181
|
+
path: str, old_string: str, new_string: str, project_dir: Path
|
|
182
|
+
) -> EditFileResult:
|
|
183
|
+
try:
|
|
184
|
+
target = _resolve_within(project_dir, path)
|
|
185
|
+
except ValueError as exc:
|
|
186
|
+
return EditFileResult(success=False, path=path, error=str(exc))
|
|
187
|
+
if not target.is_file():
|
|
188
|
+
return EditFileResult(success=False, path=path, error=f"file not found: {path}")
|
|
189
|
+
try:
|
|
190
|
+
original = target.read_text(encoding="utf-8")
|
|
191
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
192
|
+
return EditFileResult(success=False, path=path, error=str(exc))
|
|
193
|
+
count = original.count(old_string)
|
|
194
|
+
if count == 0:
|
|
195
|
+
return EditFileResult(
|
|
196
|
+
success=False, path=path, error="old_string not found in file"
|
|
197
|
+
)
|
|
198
|
+
if count > 1:
|
|
199
|
+
return EditFileResult(
|
|
200
|
+
success=False,
|
|
201
|
+
path=path,
|
|
202
|
+
error=f"old_string is not unique ({count} occurrences) — add surrounding context",
|
|
203
|
+
)
|
|
204
|
+
try:
|
|
205
|
+
target.write_text(original.replace(old_string, new_string), encoding="utf-8")
|
|
206
|
+
except OSError as exc:
|
|
207
|
+
return EditFileResult(success=False, path=path, error=str(exc))
|
|
208
|
+
return EditFileResult(success=True, path=_rel(project_dir, target), replacements=1)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def glob_files(pattern: str, project_dir: Path) -> GlobResult:
|
|
212
|
+
root = project_dir.resolve()
|
|
213
|
+
matches: list[str] = []
|
|
214
|
+
try:
|
|
215
|
+
candidates = root.glob(pattern)
|
|
216
|
+
except (ValueError, OSError, NotImplementedError) as exc:
|
|
217
|
+
return GlobResult(success=False, error=str(exc))
|
|
218
|
+
for p in sorted(candidates):
|
|
219
|
+
if not p.is_file():
|
|
220
|
+
continue
|
|
221
|
+
try:
|
|
222
|
+
rel = p.resolve().relative_to(root)
|
|
223
|
+
except ValueError:
|
|
224
|
+
continue # symlink escaping the root
|
|
225
|
+
matches.append(str(rel))
|
|
226
|
+
if len(matches) >= _MAX_GLOB_MATCHES:
|
|
227
|
+
return GlobResult(success=True, matches=matches, truncated=True)
|
|
228
|
+
return GlobResult(success=True, matches=matches)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def grep_files(pattern: str, project_dir: Path, glob: str | None = None) -> GrepResult:
|
|
232
|
+
root = project_dir.resolve()
|
|
233
|
+
matches: list[GrepMatch] = []
|
|
234
|
+
try:
|
|
235
|
+
candidates: list[Path] = (
|
|
236
|
+
sorted(root.glob(glob)) if glob else _walk_project(root)
|
|
237
|
+
)
|
|
238
|
+
except (ValueError, OSError, NotImplementedError) as exc:
|
|
239
|
+
return GrepResult(success=False, error=str(exc))
|
|
240
|
+
for p in candidates:
|
|
241
|
+
if not p.is_file():
|
|
242
|
+
continue
|
|
243
|
+
try:
|
|
244
|
+
rel = p.resolve().relative_to(root)
|
|
245
|
+
except ValueError:
|
|
246
|
+
continue
|
|
247
|
+
try:
|
|
248
|
+
with p.open(encoding="utf-8") as fh:
|
|
249
|
+
for i, line in enumerate(fh, start=1):
|
|
250
|
+
if pattern in line:
|
|
251
|
+
matches.append(
|
|
252
|
+
GrepMatch(
|
|
253
|
+
path=str(rel), line_number=i, line=line.rstrip("\n")
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
if len(matches) >= _MAX_GREP_MATCHES:
|
|
257
|
+
return GrepResult(
|
|
258
|
+
success=True, matches=matches, truncated=True
|
|
259
|
+
)
|
|
260
|
+
except (OSError, UnicodeDecodeError):
|
|
261
|
+
continue # skip binary / unreadable files
|
|
262
|
+
return GrepResult(success=True, matches=matches)
|
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
|
|
|
@@ -38,6 +39,7 @@ from dataface.core.execute.duckdb_cache import DuckDBCache
|
|
|
38
39
|
|
|
39
40
|
if TYPE_CHECKING:
|
|
40
41
|
from dataface.core.dashboard import RenderedDashboard, RenderFormat
|
|
42
|
+
from dataface.core.execute.source_resolver import SourceResolver
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
@dataclass(init=False)
|
|
@@ -54,7 +56,6 @@ class Project:
|
|
|
54
56
|
project_root: Path
|
|
55
57
|
cache: DuckDBCache | None
|
|
56
58
|
_owns_registry: bool
|
|
57
|
-
_adapter_registry: AdapterRegistry | None
|
|
58
59
|
_read_only: bool
|
|
59
60
|
_dbt_project_path: Path | None
|
|
60
61
|
_connection_string: str | None
|
|
@@ -62,6 +63,9 @@ class Project:
|
|
|
62
63
|
_target: str
|
|
63
64
|
_duckdb_config: dict[str, Any] | None
|
|
64
65
|
_allow_external_access_in_readonly: bool
|
|
66
|
+
_resolver: SourceResolver | None
|
|
67
|
+
_sources: ProjectSourcesConfig
|
|
68
|
+
_warnings_ignore: frozenset[str]
|
|
65
69
|
|
|
66
70
|
def __init__(
|
|
67
71
|
self,
|
|
@@ -74,7 +78,10 @@ class Project:
|
|
|
74
78
|
# We own the registry only when we'll lazy-build it ourselves. An injected
|
|
75
79
|
# registry belongs to the caller; we use it but don't close it on their behalf.
|
|
76
80
|
self._owns_registry = adapter_registry is None
|
|
77
|
-
|
|
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
|
|
78
85
|
self._read_only = True
|
|
79
86
|
self._dbt_project_path = None
|
|
80
87
|
self._connection_string = None
|
|
@@ -82,6 +89,9 @@ class Project:
|
|
|
82
89
|
self._target = "dev"
|
|
83
90
|
self._duckdb_config = None
|
|
84
91
|
self._allow_external_access_in_readonly = False
|
|
92
|
+
self._resolver = None
|
|
93
|
+
self._sources = get_project_sources(self.project_root)
|
|
94
|
+
self._warnings_ignore = get_project_warnings_ignore(self.project_root)
|
|
85
95
|
|
|
86
96
|
@classmethod
|
|
87
97
|
def open(
|
|
@@ -96,6 +106,7 @@ class Project:
|
|
|
96
106
|
target: str = "dev",
|
|
97
107
|
duckdb_config: dict[str, Any] | None = None,
|
|
98
108
|
allow_external_access_in_readonly: bool = False,
|
|
109
|
+
resolver: SourceResolver | None = None,
|
|
99
110
|
) -> Self:
|
|
100
111
|
"""Construct a Project rooted at *project_dir*.
|
|
101
112
|
|
|
@@ -116,6 +127,7 @@ class Project:
|
|
|
116
127
|
project._target = target
|
|
117
128
|
project._duckdb_config = duckdb_config
|
|
118
129
|
project._allow_external_access_in_readonly = allow_external_access_in_readonly
|
|
130
|
+
project._resolver = resolver
|
|
119
131
|
return project
|
|
120
132
|
|
|
121
133
|
def __enter__(self) -> Self:
|
|
@@ -124,7 +136,7 @@ class Project:
|
|
|
124
136
|
def __exit__(self, *exc_info: object) -> None:
|
|
125
137
|
self.close()
|
|
126
138
|
|
|
127
|
-
@
|
|
139
|
+
@cached_property
|
|
128
140
|
def adapter_registry(self) -> AdapterRegistry:
|
|
129
141
|
"""Lazily build the registry on first access; cached thereafter.
|
|
130
142
|
|
|
@@ -132,56 +144,49 @@ class Project:
|
|
|
132
144
|
own it (i.e. it was not injected at construction time).
|
|
133
145
|
When constructed with an injected registry, returns it directly.
|
|
134
146
|
"""
|
|
135
|
-
|
|
136
|
-
self.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
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
|
+
)
|
|
147
158
|
|
|
148
159
|
def refresh(self) -> None:
|
|
149
160
|
"""Policy-free rebuild primitive.
|
|
150
161
|
|
|
151
162
|
For projects opened via ``open()``: closes the current registry (if built)
|
|
152
|
-
and clears it so the next access rebuilds from disk.
|
|
163
|
+
and clears it so the next access rebuilds from disk. Also re-reads
|
|
164
|
+
sources and warnings_ignore from disk.
|
|
153
165
|
|
|
154
|
-
For projects constructed with an injected ``adapter_registry``:
|
|
155
|
-
|
|
166
|
+
For projects constructed with an injected ``adapter_registry``: skips the
|
|
167
|
+
registry rebuild (build arguments are not available), but still re-reads
|
|
168
|
+
the config fields.
|
|
156
169
|
"""
|
|
157
|
-
if
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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)
|
|
162
175
|
|
|
163
176
|
def close(self) -> None:
|
|
164
177
|
"""Close the adapter registry iff we own it. The cache is the caller's to close."""
|
|
165
|
-
if self._owns_registry and self.
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
# ── Config accessors ─────────────────────────────────────────────────────
|
|
178
|
+
if self._owns_registry and "adapter_registry" in self.__dict__:
|
|
179
|
+
self.adapter_registry.close()
|
|
180
|
+
del self.__dict__["adapter_registry"]
|
|
170
181
|
|
|
182
|
+
# Read-only views: project lifecycle owns these; external writers go through refresh().
|
|
183
|
+
@property
|
|
171
184
|
def sources(self) -> ProjectSourcesConfig:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
No instance-level cache: every call routes through the loader. The underlying
|
|
175
|
-
loader in `core/compile/config.py` currently warms a module-level cache keyed
|
|
176
|
-
by `project_dir`, so end-to-end disk freshness is gated on that cache being
|
|
177
|
-
deleted in a follow-up; this method's contract (fresh-per-call invocation
|
|
178
|
-
of the loader) is independent of that work.
|
|
179
|
-
"""
|
|
180
|
-
return get_project_sources(self.project_root)
|
|
185
|
+
return self._sources
|
|
181
186
|
|
|
187
|
+
@property
|
|
182
188
|
def warnings_ignore(self) -> frozenset[str]:
|
|
183
|
-
|
|
184
|
-
return get_project_warnings_ignore(self.project_root)
|
|
189
|
+
return self._warnings_ignore
|
|
185
190
|
|
|
186
191
|
# ── Verb forwarders ──────────────────────────────────────────────────────
|
|
187
192
|
|
|
@@ -290,6 +295,7 @@ class Project:
|
|
|
290
295
|
variables=variables,
|
|
291
296
|
adapter_registry=self.adapter_registry,
|
|
292
297
|
project_dir=self.project_root,
|
|
298
|
+
project_sources=self.sources,
|
|
293
299
|
duckdb_cache=self.cache,
|
|
294
300
|
format=format,
|
|
295
301
|
use_cache=use_cache,
|
|
@@ -298,4 +304,5 @@ class Project:
|
|
|
298
304
|
scale=scale,
|
|
299
305
|
ignore_codes=ignore_codes,
|
|
300
306
|
max_workers=max_workers,
|
|
307
|
+
warnings_ignore=self.warnings_ignore,
|
|
301
308
|
)
|
|
@@ -23,6 +23,8 @@ from dataface.core.render.face_api import compile_and_render
|
|
|
23
23
|
|
|
24
24
|
def render_face(
|
|
25
25
|
face_path: str | Path,
|
|
26
|
+
*,
|
|
27
|
+
warnings_ignore: frozenset[str],
|
|
26
28
|
format: str = "svg",
|
|
27
29
|
project_dir: str | Path | None = None,
|
|
28
30
|
base_dir: str | Path | None = None,
|
|
@@ -35,6 +37,10 @@ def render_face(
|
|
|
35
37
|
|
|
36
38
|
Args:
|
|
37
39
|
face_path: Path to the .yml or .md face file to render.
|
|
40
|
+
warnings_ignore: Project-level warning codes to suppress. Caller
|
|
41
|
+
resolves this (typically via
|
|
42
|
+
``get_project_warnings_ignore(project_root)``); render_face
|
|
43
|
+
does no disk I/O for warnings config.
|
|
38
44
|
format: Output format — "svg", "html", "png", "pdf", etc.
|
|
39
45
|
project_dir: Explicit project root. When omitted, auto-discovered
|
|
40
46
|
upward from the face file's directory.
|
|
@@ -83,6 +89,7 @@ def render_face(
|
|
|
83
89
|
project_root=project_root,
|
|
84
90
|
adapter_registry=adapter_registry,
|
|
85
91
|
cache=cache,
|
|
92
|
+
warnings_ignore=warnings_ignore,
|
|
86
93
|
base_dir=resolved_base_dir,
|
|
87
94
|
format=format,
|
|
88
95
|
variables=variables,
|
dataface/ai/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ Modules:
|
|
|
9
9
|
yaml_utils: YAML extraction from AI responses
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from dataface.ai.prompts import
|
|
12
|
+
from dataface.ai.prompts import load_prompt
|
|
13
13
|
from dataface.ai.schema_context import get_schema_context
|
|
14
14
|
from dataface.ai.tools import (
|
|
15
15
|
TOOL_EXECUTE_QUERY,
|
|
@@ -24,7 +24,6 @@ from dataface.ai.yaml_utils import extract_yaml
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
# Prompts
|
|
26
26
|
"load_prompt",
|
|
27
|
-
"PromptLoader",
|
|
28
27
|
# Tools
|
|
29
28
|
"get_tools",
|
|
30
29
|
"handle_tool_call",
|