dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__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/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +240 -106
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +115 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +20 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +26 -76
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +2 -19
- dataface/core/__init__.py +73 -28
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/compiler.py +97 -34
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/migrations.py +112 -9
- dataface/core/compile/models/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/source.py +5 -5
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +6 -2
- dataface/core/compile/normalize_queries.py +21 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/yaml_error_formatter.py +1 -1
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +24 -18
- dataface/core/defaults/themes/_base.yaml +49 -39
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +6 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +16 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +2 -2
- dataface/core/diagnostics/codes_compile.py +13 -31
- dataface/core/diagnostics/codes_execute.py +79 -0
- dataface/core/diagnostics/codes_render.py +87 -28
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +69 -16
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -6
- dataface/core/project.py +13 -1
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +3 -12
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/faces.py +3 -2
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +267 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +7 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +9487 -7978
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/METADATA +1 -1
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Typed startup composition for `dft serve`: validation, resolution, and app construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from dataface.core.diagnostics import Diagnostic
|
|
12
|
+
from dataface.core.diagnostics.base import DatafaceError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dataface.cli.filesystem_project import FilesystemProject
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServeSetup(BaseModel):
|
|
19
|
+
"""The resolved outcome of a successful `prepare_serve` call.
|
|
20
|
+
|
|
21
|
+
`app` is `Any` rather than `FastAPI`: `create_server`'s real return value is
|
|
22
|
+
a `FastAPI` instance, but the ASGI-app contract `uvicorn.run` actually needs
|
|
23
|
+
is duck-typed (any ASGI-callable) — pinning it to `FastAPI` would reject the
|
|
24
|
+
test doubles `dataface/tests/cli/test_serve_cli.py` legitimately passes in
|
|
25
|
+
place of a real server.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
29
|
+
|
|
30
|
+
app: Any
|
|
31
|
+
port: int
|
|
32
|
+
dialect: str
|
|
33
|
+
dialect_inferred: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def prepare_serve(
|
|
37
|
+
project: FilesystemProject,
|
|
38
|
+
*,
|
|
39
|
+
port: int | None,
|
|
40
|
+
host: str,
|
|
41
|
+
dialect: str | None,
|
|
42
|
+
target: str | None,
|
|
43
|
+
max_workers: int | None,
|
|
44
|
+
no_cache: bool,
|
|
45
|
+
cache_path: Path | None,
|
|
46
|
+
allow_external_access: bool,
|
|
47
|
+
) -> ServeSetup | Diagnostic:
|
|
48
|
+
"""Validate startup config, resolve port/dialect, and build the ASGI app.
|
|
49
|
+
|
|
50
|
+
Returns a `Diagnostic` (not a raise) when `DFT_DEFAULT_THEME` names an
|
|
51
|
+
unknown theme — the CLI formats and exits on either return shape. A
|
|
52
|
+
`dataface.yml` validation failure (`load_config`) raises `ValidationError`
|
|
53
|
+
uncaught; the CLI's own `try/except` around this call handles that,
|
|
54
|
+
unchanged from before this function existed.
|
|
55
|
+
"""
|
|
56
|
+
from dataface.core.compile.config import list_built_in_themes, load_config
|
|
57
|
+
from dataface.core.diagnostics import ERR_INVALID_DEFAULT_THEME
|
|
58
|
+
from dataface.core.project_roots import infer_dialect_from_dbt
|
|
59
|
+
from dataface.core.serve.port import resolve_port
|
|
60
|
+
from dataface.core.serve.server import create_server
|
|
61
|
+
|
|
62
|
+
env_theme = os.environ.get(
|
|
63
|
+
"DFT_DEFAULT_THEME"
|
|
64
|
+
) # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
65
|
+
# Empty string is treated as unset (matches get_default_theme_name, which
|
|
66
|
+
# falls back to the shipped default) — only a non-empty unknown name errors.
|
|
67
|
+
if env_theme:
|
|
68
|
+
available = list_built_in_themes()
|
|
69
|
+
if env_theme not in available:
|
|
70
|
+
return DatafaceError.from_code(
|
|
71
|
+
ERR_INVALID_DEFAULT_THEME,
|
|
72
|
+
source="DFT_DEFAULT_THEME env var",
|
|
73
|
+
theme=env_theme,
|
|
74
|
+
available=available,
|
|
75
|
+
).to_diagnostic()
|
|
76
|
+
|
|
77
|
+
# Validate dataface.yml wholesale: stray keys (style:, board:, theme:, etc.) are a
|
|
78
|
+
# hard error here at startup rather than silently ignored.
|
|
79
|
+
load_config(project)
|
|
80
|
+
|
|
81
|
+
project_dir = project.root
|
|
82
|
+
resolved_port = resolve_port(explicit_port=port, project_dir=project_dir, host=host)
|
|
83
|
+
|
|
84
|
+
effective_target = target or os.environ.get("DBT_TARGET") or None
|
|
85
|
+
|
|
86
|
+
dialect_inferred = False
|
|
87
|
+
effective_dialect = dialect
|
|
88
|
+
if effective_dialect is None:
|
|
89
|
+
inferred = infer_dialect_from_dbt(project_dir, effective_target)
|
|
90
|
+
effective_dialect = inferred or "duckdb"
|
|
91
|
+
dialect_inferred = bool(inferred)
|
|
92
|
+
|
|
93
|
+
app = create_server(
|
|
94
|
+
project,
|
|
95
|
+
dialect=effective_dialect,
|
|
96
|
+
target=effective_target,
|
|
97
|
+
max_workers=max_workers,
|
|
98
|
+
no_cache=no_cache,
|
|
99
|
+
cache_path=cache_path,
|
|
100
|
+
allow_external_access_in_readonly=allow_external_access,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return ServeSetup(
|
|
104
|
+
app=app,
|
|
105
|
+
port=resolved_port,
|
|
106
|
+
dialect=effective_dialect,
|
|
107
|
+
dialect_inferred=dialect_inferred,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_startup_failure(exc: Exception) -> Diagnostic:
|
|
112
|
+
"""Wrap an uncaught uvicorn startup exception in the ERR-STARTUP-FAILED envelope."""
|
|
113
|
+
from dataface.core.diagnostics import ERR_STARTUP_FAILED
|
|
114
|
+
|
|
115
|
+
return DatafaceError.from_code(ERR_STARTUP_FAILED, detail=str(exc)).to_diagnostic()
|
dataface/ai/agent.py
CHANGED
|
@@ -103,7 +103,7 @@ def schema_exploration_guidance(adapter_registry: AdapterRegistry) -> str:
|
|
|
103
103
|
Reads only registered connection config (source types) — never the
|
|
104
104
|
warehouse. Returns "" when no sources are configured (nothing to explore).
|
|
105
105
|
"""
|
|
106
|
-
types = {
|
|
106
|
+
types = {s["type"] for s in adapter_registry.list_sql_sources()}
|
|
107
107
|
if not types:
|
|
108
108
|
return ""
|
|
109
109
|
hints = [
|
|
@@ -125,7 +125,7 @@ def build_sources_context(adapter_registry: AdapterRegistry) -> str:
|
|
|
125
125
|
sources = adapter_registry.list_sql_sources()
|
|
126
126
|
if not sources:
|
|
127
127
|
return ""
|
|
128
|
-
lines = [f"- {s['name']} ({s
|
|
128
|
+
lines = [f"- {s['name']} ({s['type']})" for s in sources]
|
|
129
129
|
return (
|
|
130
130
|
"## Data sources (use these exact names in `source:` and queries)\n\n"
|
|
131
131
|
+ "\n".join(lines)
|
|
@@ -248,6 +248,7 @@ def run_agent(
|
|
|
248
248
|
context: DatafaceAIContext,
|
|
249
249
|
messages: list[AgentMessage] | None = None,
|
|
250
250
|
max_iterations: int = 25,
|
|
251
|
+
max_identical_calls: int = 3,
|
|
251
252
|
profile: AgentProfile | None = None,
|
|
252
253
|
tools: list[dict[str, Any]] | None = None,
|
|
253
254
|
extra_handlers: dict[str, ToolHandler] | None = None,
|
|
@@ -255,6 +256,9 @@ def run_agent(
|
|
|
255
256
|
"""Run the agent loop and stream typed events.
|
|
256
257
|
|
|
257
258
|
Args:
|
|
259
|
+
max_identical_calls: Consecutive iterations issuing an identical tool-call
|
|
260
|
+
batch (same names and arguments) before the loop errors instead of
|
|
261
|
+
dispatching again; any different call resets the count.
|
|
258
262
|
profile: An `AgentProfile` supplying the tool surface + system prompt. The
|
|
259
263
|
way to scope the agent (dashboard vs SQL-specialist vs an experiment).
|
|
260
264
|
Defaults to the dashboard analyst.
|
|
@@ -272,6 +276,9 @@ def run_agent(
|
|
|
272
276
|
conversation.append(UserMessage(content=prompt))
|
|
273
277
|
system_prompt = active.build_system_prompt(context)
|
|
274
278
|
|
|
279
|
+
last_call_key: tuple[tuple[str, str], ...] | None = None
|
|
280
|
+
identical_count = 0
|
|
281
|
+
|
|
275
282
|
for _ in range(max_iterations):
|
|
276
283
|
response_text = ""
|
|
277
284
|
tool_calls: list[ToolCallEvent] = []
|
|
@@ -306,6 +313,25 @@ def run_agent(
|
|
|
306
313
|
yield AgentDone(response=response_text)
|
|
307
314
|
return
|
|
308
315
|
|
|
316
|
+
call_key = tuple(
|
|
317
|
+
(tc.name, json.dumps(tc.arguments, sort_keys=True)) for tc in tool_calls
|
|
318
|
+
)
|
|
319
|
+
if call_key == last_call_key:
|
|
320
|
+
identical_count += 1
|
|
321
|
+
else:
|
|
322
|
+
identical_count = 1
|
|
323
|
+
last_call_key = call_key
|
|
324
|
+
|
|
325
|
+
if identical_count >= max_identical_calls:
|
|
326
|
+
names = ", ".join(dict.fromkeys(tc.name for tc in tool_calls))
|
|
327
|
+
yield AgentError(
|
|
328
|
+
message=(
|
|
329
|
+
f"Stopped: {names} called with identical arguments "
|
|
330
|
+
f"{identical_count} times in a row without making progress."
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
|
|
309
335
|
for tc in tool_calls:
|
|
310
336
|
result = dispatch_tool_call(
|
|
311
337
|
tc.name,
|
dataface/cli/_project.py
CHANGED
|
@@ -10,6 +10,7 @@ fallback. `with_project` is that one rule, injected once.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
13
14
|
from functools import wraps
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any, TypeVar
|
|
@@ -17,7 +18,11 @@ from typing import Any, TypeVar
|
|
|
17
18
|
import typer
|
|
18
19
|
|
|
19
20
|
from dataface.agent_api import Project
|
|
20
|
-
from dataface.agent_api._paths import
|
|
21
|
+
from dataface.agent_api._paths import (
|
|
22
|
+
DFT_ROOT_MARKERS as DFT_ROOT_MARKERS,
|
|
23
|
+
find_dft_root,
|
|
24
|
+
find_repo_root,
|
|
25
|
+
)
|
|
21
26
|
from dataface.cli._error_format import print_error
|
|
22
27
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
23
28
|
|
|
@@ -55,6 +60,100 @@ def resolve_project_dir(project_dir: Path | None) -> Path:
|
|
|
55
60
|
return found
|
|
56
61
|
|
|
57
62
|
|
|
63
|
+
def has_faces_marker(start: Path) -> bool:
|
|
64
|
+
"""True iff *start* sits inside a Dataface project that has a ``faces/`` directory.
|
|
65
|
+
|
|
66
|
+
Anchors on ``find_dft_root`` so the walk-up stops at the project root rather
|
|
67
|
+
than blindly hunting upward for stray ``faces/`` dirs elsewhere on the filesystem.
|
|
68
|
+
"""
|
|
69
|
+
if (start / "faces").is_dir():
|
|
70
|
+
return True
|
|
71
|
+
project = find_dft_root(start)
|
|
72
|
+
return project is not None and (project / "faces").is_dir()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class InitRootResolution:
|
|
77
|
+
"""Result of ``resolve_init_root``.
|
|
78
|
+
|
|
79
|
+
``git_root`` is set only for the ambiguous case (no dft project found, but a git
|
|
80
|
+
root diverges from cwd); ``None`` means unambiguous, use ``root`` as-is.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
root: Path
|
|
84
|
+
git_root: Path | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve_init_root(explicit: Path | None, cwd: Path) -> InitRootResolution:
|
|
88
|
+
"""Determine the init scaffold root without prompting the user.
|
|
89
|
+
|
|
90
|
+
Explicit dir wins (resolved, no git_root). Else walks up for a dft project —
|
|
91
|
+
if found, that's unambiguous. Else checks for a git repo: git root == cwd is
|
|
92
|
+
unambiguous; git root above cwd is ambiguous and the caller must prompt.
|
|
93
|
+
"""
|
|
94
|
+
if explicit is not None:
|
|
95
|
+
return InitRootResolution(root=explicit.resolve())
|
|
96
|
+
|
|
97
|
+
project_root = find_dft_root(cwd)
|
|
98
|
+
if project_root is not None:
|
|
99
|
+
return InitRootResolution(root=project_root)
|
|
100
|
+
|
|
101
|
+
git_root = find_repo_root(cwd)
|
|
102
|
+
if git_root is None or git_root == cwd.resolve():
|
|
103
|
+
return InitRootResolution(root=cwd.resolve())
|
|
104
|
+
|
|
105
|
+
return InitRootResolution(root=cwd.resolve(), git_root=git_root)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class McpProjectDirResolution:
|
|
110
|
+
"""Result of ``resolve_mcp_project_dir``.
|
|
111
|
+
|
|
112
|
+
``ai_config_root`` is always set (nearest dft project root, else git repo root,
|
|
113
|
+
else cwd) — where AI-client configs get written. ``project_dir`` is the project
|
|
114
|
+
the MCP server actually serves, ``None`` when unresolvable; ``nearest_root``
|
|
115
|
+
gives the caller a "did you mean" hint.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
ai_config_root: Path
|
|
119
|
+
project_dir: Path | None
|
|
120
|
+
nearest_root: Path | None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_mcp_project_dir(
|
|
124
|
+
explicit: Path | None, cwd: Path
|
|
125
|
+
) -> McpProjectDirResolution:
|
|
126
|
+
"""Determine both where AI configs are written and which project the MCP server serves."""
|
|
127
|
+
dft_root = find_dft_root(cwd)
|
|
128
|
+
ai_config_root = dft_root or find_repo_root(cwd) or cwd
|
|
129
|
+
|
|
130
|
+
if explicit is not None:
|
|
131
|
+
nearest = find_dft_root(explicit)
|
|
132
|
+
project_dir = explicit.resolve() if nearest == explicit.resolve() else None
|
|
133
|
+
return McpProjectDirResolution(
|
|
134
|
+
ai_config_root=ai_config_root,
|
|
135
|
+
project_dir=project_dir,
|
|
136
|
+
nearest_root=nearest,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return McpProjectDirResolution(
|
|
140
|
+
ai_config_root=ai_config_root,
|
|
141
|
+
project_dir=dft_root,
|
|
142
|
+
nearest_root=dft_root,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def resolve_skill_install_root(start: Path) -> Path:
|
|
147
|
+
"""Return the directory where skills should be installed.
|
|
148
|
+
|
|
149
|
+
Skills are agent tooling that belong at the git root so monorepos with nested
|
|
150
|
+
dft projects don't accumulate divergent skill copies.
|
|
151
|
+
"""
|
|
152
|
+
project_root = find_dft_root(start) or start.resolve()
|
|
153
|
+
git_root = find_repo_root(project_root)
|
|
154
|
+
return git_root if git_root is not None else project_root
|
|
155
|
+
|
|
156
|
+
|
|
58
157
|
def with_project(func: Callable[..., R]) -> Callable[..., R]:
|
|
59
158
|
"""Replace a command body's `project_dir: Path | None` with an injected `project`.
|
|
60
159
|
|
|
@@ -6,8 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from dataface.agent_api import Project
|
|
10
|
-
from dataface.agent_api.board_artifact import render_board_from_files
|
|
9
|
+
from dataface.agent_api import Project
|
|
11
10
|
from dataface.cli._console import dft_console
|
|
12
11
|
from dataface.cli._error_format import print_diagnostics
|
|
13
12
|
from dataface.cli._parsing import parse_kv_pairs
|
|
@@ -28,6 +27,8 @@ def emit_board_command(
|
|
|
28
27
|
json_output: bool = False,
|
|
29
28
|
) -> None:
|
|
30
29
|
"""Compile, resolve, and execute a face; write its board artifact + recording."""
|
|
30
|
+
from dataface.agent_api import ProjectSession
|
|
31
|
+
|
|
31
32
|
boards_dir = face_path.parent / "boards"
|
|
32
33
|
artifact_path = artifact or boards_dir / f"{face_path.stem}.board.json"
|
|
33
34
|
recording_path = recording or boards_dir / f"{face_path.stem}.recording.json"
|
|
@@ -66,6 +67,8 @@ def render_board_command(
|
|
|
66
67
|
json_output: bool = False,
|
|
67
68
|
) -> None:
|
|
68
69
|
"""Render a board from its artifact + recording — no compile, no warehouse."""
|
|
70
|
+
from dataface.agent_api.board_artifact import render_board_from_files
|
|
71
|
+
|
|
69
72
|
result = render_board_from_files(artifact_path, recording_path)
|
|
70
73
|
|
|
71
74
|
if json_output:
|
|
@@ -4,17 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
|
-
from dataface.agent_api import Project
|
|
13
|
-
from dataface.agent_api.describe import DescribeFaceResult
|
|
12
|
+
from dataface.agent_api import Project
|
|
14
13
|
from dataface.cli._console import dft_console
|
|
15
14
|
from dataface.cli._error_format import print_diagnostics
|
|
16
15
|
from dataface.cli._project import with_project
|
|
17
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from dataface.agent_api.describe import DescribeFaceResult
|
|
19
|
+
|
|
18
20
|
console = dft_console()
|
|
19
21
|
|
|
20
22
|
|
|
@@ -23,6 +25,8 @@ def describe_command(
|
|
|
23
25
|
paths: list[Path], *, project: Project, json_output: bool = False
|
|
24
26
|
) -> None:
|
|
25
27
|
"""Describe the structure of one or more face files."""
|
|
28
|
+
from dataface.agent_api import ProjectSession
|
|
29
|
+
|
|
26
30
|
with ProjectSession.from_project(project) as project_session:
|
|
27
31
|
results = project_session.describe_paths(paths)
|
|
28
32
|
_emit(results, json_output=json_output)
|
dataface/cli/commands/docs.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""CLI command for `dft docs`."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
4
6
|
|
|
5
7
|
import typer
|
|
6
8
|
from rich.markdown import Markdown
|
|
@@ -8,11 +10,12 @@ from rich.padding import Padding
|
|
|
8
10
|
from rich.table import Table
|
|
9
11
|
|
|
10
12
|
from dataface._docs_site import docs_site_url
|
|
11
|
-
from dataface.agent_api.diagnostics import get_diagnostic_code, list_diagnostic_codes
|
|
12
|
-
from dataface.agent_api.docs import TopicEntry, docs as _docs
|
|
13
13
|
from dataface.cli._console import dft_console, is_plain_output
|
|
14
14
|
from dataface.cli._json_output import print_json_result
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from dataface.agent_api.docs import TopicEntry
|
|
18
|
+
|
|
16
19
|
console = dft_console()
|
|
17
20
|
|
|
18
21
|
# The two user-facing CLI verbs over the one diagnostic registry.
|
|
@@ -26,6 +29,8 @@ def docs_diagnostics_list(
|
|
|
26
29
|
level: Literal["error", "warning"], json_output: bool
|
|
27
30
|
) -> None:
|
|
28
31
|
"""Print all registered codes at `level` with one-line descriptions, sorted."""
|
|
32
|
+
from dataface.agent_api.diagnostics import list_diagnostic_codes
|
|
33
|
+
|
|
29
34
|
result = list_diagnostic_codes(level=level)
|
|
30
35
|
|
|
31
36
|
if json_output:
|
|
@@ -61,6 +66,8 @@ def docs_diagnostic_detail(
|
|
|
61
66
|
code" for a code we ship — with a note pointing at the verb that matches
|
|
62
67
|
its actual level.
|
|
63
68
|
"""
|
|
69
|
+
from dataface.agent_api.diagnostics import get_diagnostic_code
|
|
70
|
+
|
|
64
71
|
result = get_diagnostic_code(code)
|
|
65
72
|
|
|
66
73
|
if json_output:
|
|
@@ -113,6 +120,8 @@ def docs_command(
|
|
|
113
120
|
docs_diagnostics_list(level, json_output)
|
|
114
121
|
return
|
|
115
122
|
|
|
123
|
+
from dataface.agent_api.docs import docs as _docs
|
|
124
|
+
|
|
116
125
|
result = _docs(topic=topic, search=search, limit=limit)
|
|
117
126
|
|
|
118
127
|
if json_output:
|
dataface/cli/commands/init.py
CHANGED
|
@@ -8,10 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
-
from dataface.
|
|
12
|
-
|
|
13
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
14
|
-
from dataface.core.project_roots import find_dft_root, find_repo_root
|
|
11
|
+
from dataface.cli._project import resolve_init_root
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
def _resolve(
|
|
@@ -30,17 +27,11 @@ def _resolve(
|
|
|
30
27
|
|
|
31
28
|
def _resolve_init_root(project_dir: Path | None, *, yes: bool) -> Path:
|
|
32
29
|
"""Choose the init root for the wizard without changing lower-level API defaults."""
|
|
33
|
-
if project_dir is not None:
|
|
34
|
-
return project_dir.resolve()
|
|
35
|
-
|
|
36
30
|
cwd = Path.cwd().resolve()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
git_root = find_repo_root(cwd)
|
|
42
|
-
if git_root is None or git_root == cwd:
|
|
43
|
-
return cwd
|
|
31
|
+
resolution = resolve_init_root(project_dir, cwd)
|
|
32
|
+
git_root = resolution.git_root
|
|
33
|
+
if git_root is None:
|
|
34
|
+
return resolution.root
|
|
44
35
|
|
|
45
36
|
typer.echo(f" No Dataface or dbt project found above {cwd}.")
|
|
46
37
|
typer.echo(f" git root: {git_root}")
|
|
@@ -70,6 +61,8 @@ def run_wizard(
|
|
|
70
61
|
cursor: bool | None,
|
|
71
62
|
) -> None:
|
|
72
63
|
"""Run the dft init wizard: prompt, scaffold, dispatch opt-ins."""
|
|
64
|
+
from dataface.agent_api.init import init_project
|
|
65
|
+
|
|
73
66
|
root = _resolve_init_root(project_dir, yes=yes)
|
|
74
67
|
|
|
75
68
|
if skills is not None:
|
dataface/cli/commands/inspect.py
CHANGED
|
@@ -12,7 +12,6 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
import typer
|
|
14
14
|
|
|
15
|
-
from dataface.agent_api import inspect as _api
|
|
16
15
|
from dataface.cli._console import dft_console
|
|
17
16
|
|
|
18
17
|
console = dft_console()
|
|
@@ -24,6 +23,8 @@ def eject_command(
|
|
|
24
23
|
force: bool = False,
|
|
25
24
|
output_dir: Path | None = None,
|
|
26
25
|
) -> None:
|
|
26
|
+
from dataface.agent_api import inspect as _api
|
|
27
|
+
|
|
27
28
|
target_dir = output_dir or (Path.cwd() / "faces" / "inspect")
|
|
28
29
|
to_eject: list[str] | None = None if all_templates else (templates or None)
|
|
29
30
|
if not all_templates and not templates:
|
|
@@ -55,6 +56,8 @@ def eject_command(
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def validate_ejected_templates_command(output_dir: Path | None = None) -> None:
|
|
59
|
+
from dataface.agent_api import inspect as _api
|
|
60
|
+
|
|
58
61
|
target = output_dir or (Path.cwd() / "faces" / "inspect")
|
|
59
62
|
try:
|
|
60
63
|
result = _api.validate_ejected_templates(target_dir=target)
|
|
@@ -85,6 +88,8 @@ def validate_ejected_templates_command(output_dir: Path | None = None) -> None:
|
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def templates_command() -> None:
|
|
91
|
+
from dataface.agent_api import inspect as _api
|
|
92
|
+
|
|
88
93
|
console.print("\n[bold]Available Inspect Templates[/bold]\n")
|
|
89
94
|
for t in _api.list_templates():
|
|
90
95
|
console.print(f" • {t.name}")
|
|
@@ -8,43 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
10
|
from dataface.agent_api import mcp_install
|
|
11
|
-
|
|
12
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
13
|
-
from dataface.core.project_roots import (
|
|
14
|
-
DFT_ROOT_MARKERS,
|
|
15
|
-
find_dft_root,
|
|
16
|
-
find_repo_root,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _resolve_project_dir(explicit: Path | None, discovered: Path | None) -> Path:
|
|
21
|
-
"""The dft project the MCP server points at: an explicit --project-dir (validated)
|
|
22
|
-
or the project discovered from cwd. Exits with an error when neither resolves."""
|
|
23
|
-
markers_str = ", ".join(DFT_ROOT_MARKERS)
|
|
24
|
-
if explicit is not None:
|
|
25
|
-
nearest = find_dft_root(explicit)
|
|
26
|
-
if nearest != explicit:
|
|
27
|
-
msg = (
|
|
28
|
-
f"Error: --project-dir {explicit} does not contain a Dataface "
|
|
29
|
-
f"or dbt project.\n"
|
|
30
|
-
f"Looked for: {markers_str}."
|
|
31
|
-
)
|
|
32
|
-
if nearest is not None:
|
|
33
|
-
msg += f"\nTip: did you mean {nearest}?"
|
|
34
|
-
typer.echo(msg, err=True)
|
|
35
|
-
raise typer.Exit(1)
|
|
36
|
-
return explicit
|
|
37
|
-
|
|
38
|
-
if discovered is None:
|
|
39
|
-
typer.echo(
|
|
40
|
-
f"Error: No Dataface or dbt project found at or above {Path.cwd()}.\n"
|
|
41
|
-
f"Looked for: {markers_str}.\n"
|
|
42
|
-
f"Re-run from inside your Dataface project, "
|
|
43
|
-
f"or pass --project-dir <path>.",
|
|
44
|
-
err=True,
|
|
45
|
-
)
|
|
46
|
-
raise typer.Exit(1)
|
|
47
|
-
return discovered
|
|
11
|
+
from dataface.cli._project import DFT_ROOT_MARKERS, resolve_mcp_project_dir
|
|
48
12
|
|
|
49
13
|
|
|
50
14
|
def run_init(
|
|
@@ -54,13 +18,29 @@ def run_init(
|
|
|
54
18
|
project_dir: Path | None = None,
|
|
55
19
|
) -> None:
|
|
56
20
|
"""Shared implementation for `dft init mcp`."""
|
|
57
|
-
# Where the AI-client configs get written (.cursor/mcp.json, .vscode/mcp.json,
|
|
58
|
-
# .mcp.json, …): the dft project root if we're in one, else the repo root, else
|
|
59
|
-
# cwd. Distinct from project_dir below — the project the server actually serves.
|
|
60
21
|
cwd = Path.cwd()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
22
|
+
resolution = resolve_mcp_project_dir(project_dir, cwd)
|
|
23
|
+
if resolution.project_dir is None:
|
|
24
|
+
markers_str = ", ".join(DFT_ROOT_MARKERS)
|
|
25
|
+
if project_dir is not None:
|
|
26
|
+
msg = (
|
|
27
|
+
f"Error: --project-dir {project_dir} does not contain a Dataface "
|
|
28
|
+
f"or dbt project.\n"
|
|
29
|
+
f"Looked for: {markers_str}."
|
|
30
|
+
)
|
|
31
|
+
if resolution.nearest_root is not None:
|
|
32
|
+
msg += f"\nTip: did you mean {resolution.nearest_root}?"
|
|
33
|
+
else:
|
|
34
|
+
msg = (
|
|
35
|
+
f"Error: No Dataface or dbt project found at or above {cwd}.\n"
|
|
36
|
+
f"Looked for: {markers_str}.\n"
|
|
37
|
+
f"Re-run from inside your Dataface project, "
|
|
38
|
+
f"or pass --project-dir <path>."
|
|
39
|
+
)
|
|
40
|
+
typer.echo(msg, err=True)
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
ai_config_root = resolution.ai_config_root
|
|
43
|
+
project_dir = resolution.project_dir
|
|
64
44
|
|
|
65
45
|
dft_executable = mcp_install.resolve_dft_executable(ai_config_root)
|
|
66
46
|
server_args: list[str] = ["mcp", "serve"]
|
dataface/cli/commands/migrate.py
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path, PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
|
|
9
|
-
from dataface.agent_api import Project
|
|
10
|
-
from dataface.agent_api._paths import resolve_face_path
|
|
11
|
-
from dataface.agent_api.migrate import MigrateSummary
|
|
10
|
+
from dataface.agent_api import Project
|
|
12
11
|
from dataface.cli._console import dft_console
|
|
13
12
|
from dataface.cli._project import with_project
|
|
14
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dataface.agent_api.migrate import MigrateSummary
|
|
16
|
+
|
|
15
17
|
_console = dft_console()
|
|
16
18
|
|
|
17
19
|
|
|
@@ -23,6 +25,9 @@ def migrate_command(
|
|
|
23
25
|
dry_run: bool,
|
|
24
26
|
) -> None:
|
|
25
27
|
"""Rewrite supported retired face syntax in the selected paths."""
|
|
28
|
+
from dataface.agent_api import ProjectSession
|
|
29
|
+
from dataface.agent_api._paths import resolve_face_path
|
|
30
|
+
|
|
26
31
|
relpaths = (
|
|
27
32
|
[PurePosixPath(resolve_face_path(path, project).relpath) for path in paths]
|
|
28
33
|
if paths
|
dataface/cli/commands/query.py
CHANGED
|
@@ -14,14 +14,15 @@ from rich.table import Table
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
|
|
17
|
-
from dataface.agent_api import
|
|
18
|
-
from dataface.agent_api.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
from dataface.agent_api.describe_query import DescribeQueryResult
|
|
18
|
+
from dataface.agent_api.query import (
|
|
19
|
+
ExecuteQueryResult,
|
|
20
|
+
FaceQueryLookupResult,
|
|
21
|
+
QueryFaceResult,
|
|
22
|
+
)
|
|
23
|
+
from dataface.agent_api.validate_query import QueryDiagnostic
|
|
24
|
+
|
|
25
|
+
from dataface.agent_api import Project
|
|
25
26
|
from dataface.cli._console import dft_console
|
|
26
27
|
from dataface.cli._json_output import print_json_result
|
|
27
28
|
from dataface.cli._project import with_project
|
|
@@ -278,6 +279,8 @@ def query_command(
|
|
|
278
279
|
project: Project,
|
|
279
280
|
) -> None:
|
|
280
281
|
"""Run a named face query, raw SQL, or static SQL lint/describe — selected from the input args."""
|
|
282
|
+
from dataface.agent_api import ProjectSession
|
|
283
|
+
|
|
281
284
|
# --- 1. Resolve input shape ---
|
|
282
285
|
sql_text, mode_is_named, query_name, face, source = _resolve_query_input(
|
|
283
286
|
context, query_text, file
|