dataface 0.1.4.dev6__py3-none-any.whl → 0.1.5.dev48__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 +14 -1
- dataface/agent_api/_init_templates/{index.md → README.md} +2 -2
- dataface/agent_api/_project_agents_md.py +1 -1
- dataface/agent_api/_session_store.py +3 -3
- dataface/agent_api/dashboards.py +2 -2
- dataface/agent_api/docs/yaml-reference.md +40 -3
- dataface/agent_api/init.py +24 -11
- dataface/agent_api/inspect.py +2 -2
- dataface/agent_api/mcp_install.py +4 -4
- dataface/agent_api/pack.py +4 -2
- dataface/agent_api/schema.py +1 -1
- dataface/agent_api/search.py +1 -1
- dataface/agent_api/surface_aliases.yaml +1 -1
- dataface/ai/external_mcp.py +2 -2
- dataface/ai/memories.py +1 -1
- dataface/ai/prompts.py +3 -3
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +4 -1
- dataface/cli/commands/chat.py +1 -1
- dataface/cli/commands/init.py +14 -4
- dataface/cli/commands/query.py +1 -1
- dataface/cli/main.py +10 -3
- dataface/core/compile/compiler.py +8 -9
- dataface/core/compile/markdown.py +29 -35
- dataface/core/compile/models/face/authored.py +6 -2
- dataface/core/compile/models/face/compiled.py +18 -0
- dataface/core/compile/models/primitives.py +7 -7
- dataface/core/compile/models/source.py +10 -2
- dataface/core/compile/models/style/compiled.py +218 -116
- dataface/core/compile/models/style/merged.py +85 -16
- dataface/core/compile/normalize_layout.py +22 -3
- dataface/core/compile/normalize_queries.py +6 -5
- dataface/core/compile/normalizer.py +9 -5
- dataface/core/compile/sources.py +35 -26
- dataface/core/dashboard.py +3 -0
- dataface/core/defaults/themes/_base.yaml +26 -1
- dataface/core/defaults/themes/cream.yaml +11 -0
- dataface/core/defaults/themes/editorial.yaml +11 -0
- dataface/core/defaults/themes/stark.yaml +12 -0
- dataface/core/errors/codes_compile.py +16 -0
- dataface/core/execute/adapters/dbt_adapter.py +39 -22
- dataface/core/execute/adapters/sql_adapter.py +62 -9
- dataface/core/execute/duckdb_cache.py +32 -68
- dataface/core/inspect/dbt_schema.py +1 -1
- dataface/core/inspect/manifest_utils.py +4 -2
- dataface/core/pack/proposal_store.py +6 -3
- dataface/core/project_roots.py +86 -12
- dataface/core/render/chart_interactivity.py +39 -1
- dataface/core/render/control_registry.py +6 -4
- dataface/core/render/converters/html.py +1 -1
- dataface/core/render/dir_context.py +267 -0
- dataface/core/render/face_api.py +8 -5
- dataface/core/render/renderer.py +22 -4
- dataface/core/render/template_loader.py +1 -1
- dataface/core/render/templates/controls/_styles.css +4 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +15 -16
- dataface/core/render/variable_controls.py +1 -1
- dataface/core/resolve_face.py +10 -10
- dataface/core/serve/port.py +1 -1
- dataface/core/serve/server.py +26 -16
- {dataface-0.1.4.dev6.dist-info → dataface-0.1.5.dev48.dist-info}/METADATA +50 -18
- {dataface-0.1.4.dev6.dist-info → dataface-0.1.5.dev48.dist-info}/RECORD +65 -64
- {dataface-0.1.4.dev6.dist-info → dataface-0.1.5.dev48.dist-info}/WHEEL +1 -1
- /dataface/agent_api/_init_templates/{faces-dataface.yml → guide.yml} +0 -0
- {dataface-0.1.4.dev6.dist-info → dataface-0.1.5.dev48.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.4.dev6.dist-info → dataface-0.1.5.dev48.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -1113,17 +1113,30 @@ Active code categories (see `dataface/core/errors/codes_*.py` for the authoritat
|
|
|
1113
1113
|
|
|
1114
1114
|
| Prefix | Meaning |
|
|
1115
1115
|
|--------|---------|
|
|
1116
|
+
| `DF-COMPILE-*` | Compile-time errors (missing fields, unknown references, malformed YAML) |
|
|
1116
1117
|
| `DF-RENDER-*` | Chart rendering errors (wrong row counts, unknown chart types, format issues) |
|
|
1117
1118
|
| `DF-EXECUTE-*` | Query execution errors (missing sources, inline-source policy, source typing) |
|
|
1118
1119
|
| `DF-UNKNOWN-*` | Fallback codes for legacy string-message errors not yet migrated |
|
|
1119
1120
|
|
|
1120
|
-
The registry is being filled out incrementally —
|
|
1121
|
+
The registry is being filled out incrementally — some compile-time and variable-resolution errors still raise as plain `DatafaceError` without a structured code. Today's typed codes:
|
|
1122
|
+
|
|
1123
|
+
### Compile errors
|
|
1124
|
+
|
|
1125
|
+
- **`DF-COMPILE-SOURCE-REQUIRED`** — a SQL query has no `source:` and no default is configured. Set the source on the query (`source: my_db`), at the face level (`source:` or `sources.default`), or project-wide via `sources.default` in `dataface.yml`.
|
|
1126
|
+
- **`DF-COMPILE-UNKNOWN-QUERY`** — a chart's `query:` references a name that does not appear under `queries:`. Check for a typo or add the missing query.
|
|
1127
|
+
- **`DF-COMPILE-EXTRA-FIELD`** — the face YAML contains a field not recognised by the schema. Remove it or check the schema for supported keys.
|
|
1128
|
+
- **`DF-COMPILE-SQL-LITERAL-NEWLINES`** — a SQL field contains a literal `\n` (backslash + n) from single-quoted YAML. Use a YAML block scalar (`sql: |`) to write multiline SQL.
|
|
1129
|
+
|
|
1130
|
+
### Render errors
|
|
1121
1131
|
|
|
1122
1132
|
- **`DF-RENDER-KPI-MULTIROW`** — a KPI chart's query returned more than one row but `value` is a column reference. Add `LIMIT 1` or aggregate down to one row.
|
|
1123
1133
|
- **`DF-RENDER-UNKNOWN-CHART-TYPE`** — `type:` is not a recognized chart type. See the [Charts](#charts) section for the list.
|
|
1124
1134
|
- **`DF-RENDER-FORMAT-UNSUPPORTED`** — the render `format` argument is not one of `svg`/`html`/`png`.
|
|
1125
1135
|
- **`DF-RENDER-NO-LAYOUT`** — the face has no `rows`/`cols`/`grid`/`tabs` and nothing to render.
|
|
1126
1136
|
- **`DF-RENDER-INPUT-INVALID`** — the render call received structurally invalid input (e.g. both `path` and `yaml_content`).
|
|
1137
|
+
|
|
1138
|
+
### Execute errors
|
|
1139
|
+
|
|
1127
1140
|
- **`DF-EXECUTE-SOURCE-NOT-FOUND`** / **`-NOT-FOUND-EMPTY`** — the query's `source:` (or default source) is not configured in `dataface.yml`.
|
|
1128
1141
|
- **`DF-EXECUTE-NO-DEFAULT-SOURCE`** — multiple sources are defined and no `default_source` was set.
|
|
1129
1142
|
- **`DF-EXECUTE-SOURCE-INLINE-FORBIDDEN`** / **`-CROSS-FILE-FORBIDDEN`** — inline source definitions are restricted by project policy.
|
|
@@ -11,14 +11,14 @@ for your data project.
|
|
|
11
11
|
## Authoring modes
|
|
12
12
|
|
|
13
13
|
**YAML (`.yml`)** — structured dashboards with queries, charts, and layout.
|
|
14
|
-
See the [dataface guide](
|
|
14
|
+
See the [dataface guide](guide) for a working example covering queries, charts, and variables.
|
|
15
15
|
|
|
16
16
|
**Markdown (`.md`)** — prose pages like this one. Add YAML frontmatter for
|
|
17
17
|
queries and charts, then embed them inline with `{% raw %}{{ chart my_chart }}{% endraw %}`.
|
|
18
18
|
|
|
19
19
|
## Next steps
|
|
20
20
|
|
|
21
|
-
1. Open `faces/
|
|
21
|
+
1. Open `faces/guide.yml` and tweak the content.
|
|
22
22
|
2. Run `dft serve` and open the URL it prints.
|
|
23
23
|
3. Add new `.yml` or `.md` files under `faces/` — they appear automatically.
|
|
24
24
|
4. Run `dft validate` to validate your face YAML for errors.
|
|
@@ -15,7 +15,7 @@ _TEMPLATES = importlib.resources.files("dataface.agent_api._init_templates")
|
|
|
15
15
|
|
|
16
16
|
def load_agents_snippet() -> str:
|
|
17
17
|
"""Return the packaged markdown blurb (includes marker comments)."""
|
|
18
|
-
return _TEMPLATES.joinpath("agents_dft_snippet.md").read_text()
|
|
18
|
+
return _TEMPLATES.joinpath("agents_dft_snippet.md").read_text(encoding="utf-8")
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def has_dataface_markers(text: str) -> bool:
|
|
@@ -198,7 +198,7 @@ class SessionIndex:
|
|
|
198
198
|
def _load(self) -> dict[str, list[SessionMeta]]:
|
|
199
199
|
if self._path.exists():
|
|
200
200
|
try:
|
|
201
|
-
raw = json.loads(self._path.read_text())
|
|
201
|
+
raw = json.loads(self._path.read_text(encoding="utf-8"))
|
|
202
202
|
if isinstance(raw, dict):
|
|
203
203
|
return raw
|
|
204
204
|
logger.warning("sessions/index.json has unexpected shape; rebuilding")
|
|
@@ -208,7 +208,7 @@ class SessionIndex:
|
|
|
208
208
|
|
|
209
209
|
def _save(self) -> None:
|
|
210
210
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
-
self._path.write_text(json.dumps(self._data, indent=2))
|
|
211
|
+
self._path.write_text(json.dumps(self._data, indent=2), encoding="utf-8")
|
|
212
212
|
|
|
213
213
|
def _rebuild(self) -> dict[str, list[SessionMeta]]:
|
|
214
214
|
"""Scan all JSONL files under ~/.dft/sessions/ and reconstruct the index."""
|
|
@@ -234,7 +234,7 @@ class SessionIndex:
|
|
|
234
234
|
data.setdefault(cwd, []).append(entry)
|
|
235
235
|
# Persist the rebuilt index
|
|
236
236
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
-
self._path.write_text(json.dumps(data, indent=2))
|
|
237
|
+
self._path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
238
238
|
return data
|
|
239
239
|
|
|
240
240
|
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -148,7 +148,7 @@ def list_dashboards(
|
|
|
148
148
|
if yaml_file.name.startswith("_"):
|
|
149
149
|
continue
|
|
150
150
|
try:
|
|
151
|
-
content = yaml.safe_load(yaml_file.read_text())
|
|
151
|
+
content = yaml.safe_load(yaml_file.read_text(encoding="utf-8"))
|
|
152
152
|
if not isinstance(content, dict):
|
|
153
153
|
skipped.append(
|
|
154
154
|
SkippedFile(
|
|
@@ -235,7 +235,7 @@ def get_dashboard(
|
|
|
235
235
|
)
|
|
236
236
|
|
|
237
237
|
try:
|
|
238
|
-
raw_yaml = file_path.read_text()
|
|
238
|
+
raw_yaml = file_path.read_text(encoding="utf-8")
|
|
239
239
|
except OSError as e:
|
|
240
240
|
return CompiledDashboard(
|
|
241
241
|
success=False,
|
|
@@ -15,7 +15,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
15
15
|
| `source` | str | ✓ | Default source name shorthand (equivalent to sources.default). Sets the connection for all queries. |
|
|
16
16
|
| `variables` | dict[str, [Variable](#variable) \| VariableRef] | ✓ | Variable definitions for dynamic filtering and UI controls. |
|
|
17
17
|
| `queries` | dict[str, [Query](#query) \| QueryRef] | ✓ | Named query definitions (SQL, CSV, MetricFlow, HTTP, etc.). |
|
|
18
|
-
| `charts` | dict[str, [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart) \| ChartRef] | ✓ | Named chart definitions
|
|
18
|
+
| `charts` | dict[str, [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart) \| ChartRef] | ✓ | Named chart definitions. When no explicit layout is present, charts render as an implicit row layout in authored order. |
|
|
19
19
|
| `rows` | list[str \| [Face](#face) \| [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart) \| [ForeachItem](#foreachitem) \| dict[str, [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart)]] | ✓ | Vertical stack layout: list of chart names or inline chart/face definitions. |
|
|
20
20
|
| `cols` | list[str \| [Face](#face) \| [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart) \| [ForeachItem](#foreachitem) \| dict[str, [BarChart](#barchart) \| [LineChart](#linechart) \| [AreaChart](#areachart) \| [ScatterChart](#scatterchart) \| [HeatmapChart](#heatmapchart) \| [PieChart](#piechart) \| [KpiChart](#kpichart) \| [TableChart](#tablechart) \| [PointMapChart](#pointmapchart) \| [GeoshapeChart](#geoshapechart) \| [LayeredChart](#layeredchart) \| [CalloutChart](#calloutchart) \| [SparkBarChart](#sparkbarchart)]] | ✓ | Horizontal layout: list of chart names or inline chart/face definitions. |
|
|
21
21
|
| `grid` | [GridLayout](#gridlayout) | ✓ | CSS-grid style layout with explicit row/column placement. |
|
|
@@ -646,13 +646,14 @@ HTTP/REST API source configuration.
|
|
|
646
646
|
|
|
647
647
|
<a id="dbtprofilesourceconfig"></a>
|
|
648
648
|
## DbtProfileSourceConfig
|
|
649
|
-
Reference to a dbt profile
|
|
649
|
+
Reference to a dbt profile.
|
|
650
650
|
|
|
651
651
|
| Field | Type | Optional | Description |
|
|
652
652
|
|-------|------|:--------:|-------------|
|
|
653
653
|
| `type` | enum: "dbt_profile" | | Source type identifier. |
|
|
654
654
|
| `profile` | str | | dbt profile name from profiles.yml. |
|
|
655
655
|
| `target` | str | ✓ | dbt target to use; defaults to the profile's default target. |
|
|
656
|
+
| `profiles_dir` | str | ✓ | Directory containing profiles.yml, relative to the dataface project root. Use when profiles.yml is in a subdirectory (e.g. services/dbt). Resolution order: profiles_dir → $DBT_PROFILES_DIR → project root → ~/.dbt. |
|
|
656
657
|
|
|
657
658
|
<a id="variableoptions"></a>
|
|
658
659
|
## VariableOptions
|
|
@@ -1485,11 +1486,21 @@ Authored overlay for LegendStyle.
|
|
|
1485
1486
|
|
|
1486
1487
|
<a id="tooltipstyle"></a>
|
|
1487
1488
|
## TooltipStyle
|
|
1488
|
-
Authored overlay for TooltipStyle. Tooltip
|
|
1489
|
+
Authored overlay for TooltipStyle. Tooltip box style — all cascade keys for the hover bubble.
|
|
1489
1490
|
|
|
1490
1491
|
| Field | Type | Optional | Description |
|
|
1491
1492
|
|-------|------|:--------:|-------------|
|
|
1492
1493
|
| `format` | str | ✓ | Default tooltip value format string; theme always provides this. |
|
|
1494
|
+
| `background` | str | ✓ | Tooltip background color (CSS color string); theme always provides this. |
|
|
1495
|
+
| `line_height` | float | ✓ | Tooltip line-height multiplier; theme always provides this. |
|
|
1496
|
+
| `max_width` | float | ✓ | Maximum tooltip width in pixels; theme always provides this. |
|
|
1497
|
+
| `gap` | float | ✓ | Gap in pixels between label and value columns; theme always provides this. |
|
|
1498
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Tooltip font overrides (size etc.); cascade fills missing fields. |
|
|
1499
|
+
| `padding` | [PaddingStyle](#paddingstyle) | ✓ | Tooltip inner padding (4 sides in pixels); theme always provides this. |
|
|
1500
|
+
| `label` | [TooltipSlotStyle](#tooltipslotstyle) | ✓ | Label-column font overrides (color, weight). |
|
|
1501
|
+
| `value` | [TooltipSlotStyle](#tooltipslotstyle) | ✓ | Value-column font overrides (color, weight). |
|
|
1502
|
+
| `border` | [TooltipBorderStyle](#tooltipborderstyle) | ✓ | Tooltip border style; theme always provides this. |
|
|
1503
|
+
| `shadow` | [TooltipShadowStyle](#tooltipshadowstyle) | ✓ | Tooltip drop-shadow config; theme always provides this. |
|
|
1493
1504
|
|
|
1494
1505
|
<a id="stylecolorconfig"></a>
|
|
1495
1506
|
## StyleColorConfig
|
|
@@ -2266,6 +2277,32 @@ Authored overlay for LegendElementStyle.
|
|
|
2266
2277
|
| `font` | [FontStyle](#fontstyle) | ✓ | Legend element font style overrides. |
|
|
2267
2278
|
| `padding` | float | ✓ | Padding between legend symbol and element text in pixels. |
|
|
2268
2279
|
|
|
2280
|
+
<a id="tooltipslotstyle"></a>
|
|
2281
|
+
## TooltipSlotStyle
|
|
2282
|
+
Authored overlay for TooltipSlotStyle. Typography for a single tooltip slot (label or value).
|
|
2283
|
+
|
|
2284
|
+
| Field | Type | Optional | Description |
|
|
2285
|
+
|-------|------|:--------:|-------------|
|
|
2286
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Font overrides for this tooltip slot (color, weight). |
|
|
2287
|
+
|
|
2288
|
+
<a id="tooltipborderstyle"></a>
|
|
2289
|
+
## TooltipBorderStyle
|
|
2290
|
+
Authored overlay for TooltipBorderStyle. Tooltip box border — all fields required; theme YAML supplies defaults.
|
|
2291
|
+
|
|
2292
|
+
| Field | Type | Optional | Description |
|
|
2293
|
+
|-------|------|:--------:|-------------|
|
|
2294
|
+
| `color` | str | ✓ | Border color as a CSS color string. |
|
|
2295
|
+
| `width` | float | ✓ | Border width in pixels. |
|
|
2296
|
+
| `radius` | float | ✓ | Border corner radius in pixels. |
|
|
2297
|
+
|
|
2298
|
+
<a id="tooltipshadowstyle"></a>
|
|
2299
|
+
## TooltipShadowStyle
|
|
2300
|
+
Authored overlay for TooltipShadowStyle. Tooltip drop-shadow toggle. JS applies the shadow expression when visible=true.
|
|
2301
|
+
|
|
2302
|
+
| Field | Type | Optional | Description |
|
|
2303
|
+
|-------|------|:--------:|-------------|
|
|
2304
|
+
| `visible` | bool | ✓ | Show a drop-shadow on the tooltip box; theme always provides this. |
|
|
2305
|
+
|
|
2269
2306
|
<a id="scaletargetconfig"></a>
|
|
2270
2307
|
## ScaleTargetConfig
|
|
2271
2308
|
Scale configuration for a single style target (background or color).
|
dataface/agent_api/init.py
CHANGED
|
@@ -51,9 +51,18 @@ def init_project(
|
|
|
51
51
|
(root / "faces" / "partials").mkdir(exist_ok=True)
|
|
52
52
|
|
|
53
53
|
scaffolds: list[tuple[str, str]] = [
|
|
54
|
-
(
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
(
|
|
55
|
+
"dataface.yml",
|
|
56
|
+
_TEMPLATES.joinpath("dataface.yml").read_text(encoding="utf-8"),
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
"faces/README.md",
|
|
60
|
+
_TEMPLATES.joinpath("README.md").read_text(encoding="utf-8"),
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
"faces/guide.yml",
|
|
64
|
+
_TEMPLATES.joinpath("guide.yml").read_text(encoding="utf-8"),
|
|
65
|
+
),
|
|
57
66
|
("faces/partials/.gitkeep", ""),
|
|
58
67
|
]
|
|
59
68
|
|
|
@@ -62,10 +71,10 @@ def init_project(
|
|
|
62
71
|
if target.exists() and not force:
|
|
63
72
|
result.skipped_files.append(Path(rel))
|
|
64
73
|
elif target.exists():
|
|
65
|
-
target.write_text(content)
|
|
74
|
+
target.write_text(content, encoding="utf-8")
|
|
66
75
|
result.refreshed_files.append(Path(rel))
|
|
67
76
|
else:
|
|
68
|
-
target.write_text(content)
|
|
77
|
+
target.write_text(content, encoding="utf-8")
|
|
69
78
|
result.created_files.append(Path(rel))
|
|
70
79
|
|
|
71
80
|
_ensure_gitignore_entries(root, result)
|
|
@@ -74,10 +83,12 @@ def init_project(
|
|
|
74
83
|
snippet = load_agents_snippet()
|
|
75
84
|
agents_md_path = root / "AGENTS.md"
|
|
76
85
|
existing: str | None = (
|
|
77
|
-
agents_md_path.read_text()
|
|
86
|
+
agents_md_path.read_text(encoding="utf-8")
|
|
87
|
+
if agents_md_path.exists()
|
|
88
|
+
else None
|
|
78
89
|
)
|
|
79
90
|
final_text, action = merge_agents_snippet(existing, snippet)
|
|
80
|
-
agents_md_path.write_text(final_text)
|
|
91
|
+
agents_md_path.write_text(final_text, encoding="utf-8")
|
|
81
92
|
if action == "created":
|
|
82
93
|
result.created_files.append(Path("AGENTS.md"))
|
|
83
94
|
elif action == "refreshed":
|
|
@@ -88,7 +99,7 @@ def init_project(
|
|
|
88
99
|
if write_claude_md:
|
|
89
100
|
claude_md_path = root / "CLAUDE.md"
|
|
90
101
|
if not claude_md_path.exists():
|
|
91
|
-
claude_md_path.write_text("@AGENTS.md\n")
|
|
102
|
+
claude_md_path.write_text("@AGENTS.md\n", encoding="utf-8")
|
|
92
103
|
result.created_files.append(Path("CLAUDE.md"))
|
|
93
104
|
|
|
94
105
|
if eject_inspect:
|
|
@@ -111,16 +122,18 @@ def init_project(
|
|
|
111
122
|
def _ensure_gitignore_entries(root: Path, result: InitResult) -> None:
|
|
112
123
|
gitignore = root / ".gitignore"
|
|
113
124
|
if not gitignore.exists():
|
|
114
|
-
gitignore.write_text("\n".join(GITIGNORE_ENTRIES) + "\n")
|
|
125
|
+
gitignore.write_text("\n".join(GITIGNORE_ENTRIES) + "\n", encoding="utf-8")
|
|
115
126
|
result.created_files.append(Path(".gitignore"))
|
|
116
127
|
return
|
|
117
128
|
|
|
118
|
-
text = gitignore.read_text()
|
|
129
|
+
text = gitignore.read_text(encoding="utf-8")
|
|
119
130
|
existing = set(text.splitlines())
|
|
120
131
|
missing = [e for e in GITIGNORE_ENTRIES if e not in existing]
|
|
121
132
|
if not missing:
|
|
122
133
|
return
|
|
123
134
|
|
|
124
135
|
separator = "" if not text or text.endswith("\n") else "\n"
|
|
125
|
-
gitignore.write_text(
|
|
136
|
+
gitignore.write_text(
|
|
137
|
+
f"{text}{separator}" + "\n".join(missing) + "\n", encoding="utf-8"
|
|
138
|
+
)
|
|
126
139
|
result.refreshed_files.append(Path(".gitignore"))
|
dataface/agent_api/inspect.py
CHANGED
|
@@ -78,9 +78,9 @@ def eject_templates(
|
|
|
78
78
|
if target_file.exists() and not force:
|
|
79
79
|
continue
|
|
80
80
|
|
|
81
|
-
content = source_file.read_text()
|
|
81
|
+
content = source_file.read_text(encoding="utf-8")
|
|
82
82
|
source_hash = sha256(content.encode("utf-8")).hexdigest()
|
|
83
|
-
target_file.write_text(content)
|
|
83
|
+
target_file.write_text(content, encoding="utf-8")
|
|
84
84
|
|
|
85
85
|
manifest_templates[name] = {
|
|
86
86
|
"filename": f"{name}.yml",
|
|
@@ -126,7 +126,7 @@ def _upsert_mcp_config(
|
|
|
126
126
|
existing: dict[str, Any] = {}
|
|
127
127
|
if config_path.exists():
|
|
128
128
|
try:
|
|
129
|
-
existing = json.loads(config_path.read_text())
|
|
129
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
130
130
|
if not isinstance(existing, dict):
|
|
131
131
|
existing = {}
|
|
132
132
|
except (json.JSONDecodeError, OSError):
|
|
@@ -140,7 +140,7 @@ def _upsert_mcp_config(
|
|
|
140
140
|
|
|
141
141
|
existing.setdefault(servers_key, {})
|
|
142
142
|
existing[servers_key]["dataface"] = server_entry
|
|
143
|
-
config_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
143
|
+
config_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
144
144
|
return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"
|
|
145
145
|
|
|
146
146
|
|
|
@@ -158,7 +158,7 @@ def _upsert_toml_mcp_config(
|
|
|
158
158
|
|
|
159
159
|
existing: dict[str, Any] = {}
|
|
160
160
|
if config_path.exists():
|
|
161
|
-
existing = tomllib.loads(config_path.read_text())
|
|
161
|
+
existing = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
162
162
|
|
|
163
163
|
already_has = "dataface" in existing.get(servers_key, {})
|
|
164
164
|
if already_has and not force:
|
|
@@ -166,5 +166,5 @@ def _upsert_toml_mcp_config(
|
|
|
166
166
|
|
|
167
167
|
existing.setdefault(servers_key, {})
|
|
168
168
|
existing[servers_key]["dataface"] = server_entry
|
|
169
|
-
config_path.write_text(tomli_w.dumps(existing))
|
|
169
|
+
config_path.write_text(tomli_w.dumps(existing), encoding="utf-8")
|
|
170
170
|
return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"
|
dataface/agent_api/pack.py
CHANGED
|
@@ -291,7 +291,8 @@ def _write_face(path: Path, face_dict: dict) -> None:
|
|
|
291
291
|
path.write_text(
|
|
292
292
|
yaml.dump(
|
|
293
293
|
face_dict, default_flow_style=False, sort_keys=False, allow_unicode=True
|
|
294
|
-
)
|
|
294
|
+
),
|
|
295
|
+
encoding="utf-8",
|
|
295
296
|
)
|
|
296
297
|
|
|
297
298
|
|
|
@@ -417,7 +418,8 @@ def apply_proposal(
|
|
|
417
418
|
# Write a minimal partial YAML comment block
|
|
418
419
|
partial_path.write_text(
|
|
419
420
|
f"# Shared partial: {partial_filename}\n"
|
|
420
|
-
f"# Add shared query fragments, variables, or chart definitions here.\n"
|
|
421
|
+
f"# Add shared query fragments, variables, or chart definitions here.\n",
|
|
422
|
+
encoding="utf-8",
|
|
421
423
|
)
|
|
422
424
|
result.created_files.append(rel)
|
|
423
425
|
|
dataface/agent_api/schema.py
CHANGED
|
@@ -646,7 +646,7 @@ def _project_uses_direct_file_queries(project_root: Path) -> bool:
|
|
|
646
646
|
|
|
647
647
|
for dashboard in list_dashboards(directory=project_root, recursive=True).dashboards:
|
|
648
648
|
try:
|
|
649
|
-
content = dashboard.absolute_path.read_text()
|
|
649
|
+
content = dashboard.absolute_path.read_text(encoding="utf-8")
|
|
650
650
|
except OSError:
|
|
651
651
|
continue
|
|
652
652
|
if (
|
dataface/agent_api/search.py
CHANGED
|
@@ -124,7 +124,7 @@ def _build_index(directory: Path) -> list[dict[str, Any]]:
|
|
|
124
124
|
for dash in listing.dashboards:
|
|
125
125
|
abs_path = dash.absolute_path
|
|
126
126
|
try:
|
|
127
|
-
content = yaml.safe_load(abs_path.read_text())
|
|
127
|
+
content = yaml.safe_load(abs_path.read_text(encoding="utf-8"))
|
|
128
128
|
except (yaml.YAMLError, OSError):
|
|
129
129
|
continue
|
|
130
130
|
|
dataface/ai/external_mcp.py
CHANGED
|
@@ -234,7 +234,7 @@ def _parse_json_config(
|
|
|
234
234
|
if not path.exists():
|
|
235
235
|
return
|
|
236
236
|
try:
|
|
237
|
-
data = json.loads(path.read_text())
|
|
237
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
238
238
|
except json.JSONDecodeError:
|
|
239
239
|
_console.print(f"[yellow]MCP: {path} has invalid JSON; skipping[/yellow]")
|
|
240
240
|
return
|
|
@@ -271,7 +271,7 @@ def _parse_toml_config(
|
|
|
271
271
|
if not path.exists():
|
|
272
272
|
return
|
|
273
273
|
try:
|
|
274
|
-
data = tomllib.loads(path.read_text())
|
|
274
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
275
275
|
except (tomllib.TOMLDecodeError, OSError) as exc:
|
|
276
276
|
_console.print(
|
|
277
277
|
f"[yellow]MCP: {path} has invalid TOML: {exc}; skipping[/yellow]"
|
dataface/ai/memories.py
CHANGED
|
@@ -17,7 +17,7 @@ def load_memories(base_dir: Path | None = None) -> list[dict[str, Any]]:
|
|
|
17
17
|
if not path.exists():
|
|
18
18
|
return []
|
|
19
19
|
|
|
20
|
-
payload = yaml.safe_load(path.read_text()) or {}
|
|
20
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
21
21
|
rows = payload.get("memories") if isinstance(payload, dict) else None
|
|
22
22
|
if not isinstance(rows, list):
|
|
23
23
|
return []
|
dataface/ai/prompts.py
CHANGED
|
@@ -41,7 +41,7 @@ def load_prompt(prompt_name: str, prompts_dir: Path) -> str:
|
|
|
41
41
|
"""
|
|
42
42
|
prompt_file = prompts_dir / f"{prompt_name}.md"
|
|
43
43
|
if prompt_file.exists():
|
|
44
|
-
return prompt_file.read_text()
|
|
44
|
+
return prompt_file.read_text(encoding="utf-8")
|
|
45
45
|
return ""
|
|
46
46
|
|
|
47
47
|
|
|
@@ -94,7 +94,7 @@ class PromptLoader:
|
|
|
94
94
|
prompt_file = self.prompts_dir / f"{prompt_name}.md"
|
|
95
95
|
if not prompt_file.exists():
|
|
96
96
|
raise FileNotFoundError(f"Prompt file not found: {prompt_file}")
|
|
97
|
-
return prompt_file.read_text()
|
|
97
|
+
return prompt_file.read_text(encoding="utf-8")
|
|
98
98
|
|
|
99
99
|
def exists(self, prompt_name: str) -> bool:
|
|
100
100
|
"""Check if a prompt file exists.
|
|
@@ -129,7 +129,7 @@ def load_shared_prompt(skill_name: str, *, surface: SkillSurface = "tool") -> st
|
|
|
129
129
|
"""
|
|
130
130
|
skill_file = SKILLS_DIR / skill_name / "SKILL.md"
|
|
131
131
|
if skill_file.exists():
|
|
132
|
-
body = _strip_frontmatter(skill_file.read_text())
|
|
132
|
+
body = _strip_frontmatter(skill_file.read_text(encoding="utf-8"))
|
|
133
133
|
return render_skill_body(body, surface=surface)
|
|
134
134
|
return ""
|
|
135
135
|
|
|
@@ -38,7 +38,10 @@ These come from `{{ s_validate_dashboard }}`. The error message tells you exactl
|
|
|
38
38
|
|
|
39
39
|
### "must have at least one layout"
|
|
40
40
|
|
|
41
|
-
Every face needs
|
|
41
|
+
Every face needs visible content. Add a chart under `charts:`, an explicit
|
|
42
|
+
layout (`rows:`, `cols:`, `grid:`, or `tabs:`), or text/title content. If
|
|
43
|
+
`charts:` is present and no layout key is present, Dataface renders those
|
|
44
|
+
charts as implicit rows.
|
|
42
45
|
|
|
43
46
|
### "references unknown chart 'X'"
|
|
44
47
|
|
dataface/cli/commands/chat.py
CHANGED
|
@@ -236,7 +236,7 @@ def _render_tool_result(name: str, result: Any, console: Console, tmpdir: Path)
|
|
|
236
236
|
# Sanitize name to avoid path separators from LLM-provided tool names.
|
|
237
237
|
safe_name = re.sub(r"[^\w-]", "_", name)
|
|
238
238
|
tmp_file = tmpdir / f"dft_{safe_name}_{int(time.monotonic() * 1000)}.json"
|
|
239
|
-
tmp_file.write_text(body)
|
|
239
|
+
tmp_file.write_text(body, encoding="utf-8")
|
|
240
240
|
summary = f"[dim]... ({extra} more lines, full result in {tmp_file})[/dim]"
|
|
241
241
|
console.print(
|
|
242
242
|
Panel(
|
dataface/cli/commands/init.py
CHANGED
|
@@ -80,7 +80,7 @@ def run_wizard(
|
|
|
80
80
|
agents_md_prompt = (
|
|
81
81
|
"Create AGENTS.md at the project root with Dataface instructions?"
|
|
82
82
|
)
|
|
83
|
-
elif has_dataface_markers(agents_md_path.read_text()):
|
|
83
|
+
elif has_dataface_markers(agents_md_path.read_text(encoding="utf-8")):
|
|
84
84
|
agents_md_prompt = "Refresh the Dataface section in AGENTS.md?"
|
|
85
85
|
else:
|
|
86
86
|
agents_md_prompt = "Append Dataface instructions to your existing AGENTS.md?"
|
|
@@ -137,7 +137,10 @@ def run_wizard(
|
|
|
137
137
|
default=True,
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
-
# Playground extra — skip prompt if already installed
|
|
140
|
+
# Playground extra — skip prompt if already installed.
|
|
141
|
+
# Default is False: dataface-playground is not on public PyPI yet, so an
|
|
142
|
+
# unprompted install attempt would always fail for OSS users. Explicit
|
|
143
|
+
# --with-playground still works; failure degrades to a warning (see below).
|
|
141
144
|
if not _missing_packages("playground"):
|
|
142
145
|
do_with_playground = False
|
|
143
146
|
else:
|
|
@@ -145,7 +148,7 @@ def run_wizard(
|
|
|
145
148
|
with_playground,
|
|
146
149
|
yes=yes,
|
|
147
150
|
prompt="Install the dataface[playground] extra now (so 'dft playground' works without prompting later)?",
|
|
148
|
-
default=
|
|
151
|
+
default=False,
|
|
149
152
|
)
|
|
150
153
|
|
|
151
154
|
# IDE detection — only prompt for IDEs found on PATH
|
|
@@ -238,7 +241,14 @@ def run_wizard(
|
|
|
238
241
|
if do_with_playground:
|
|
239
242
|
from dataface.cli._extras import install_extras
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
try:
|
|
245
|
+
install_extras("playground", interactive=False)
|
|
246
|
+
except typer.Exit:
|
|
247
|
+
install_warnings.append(
|
|
248
|
+
"playground skipped — dataface-playground is not on public PyPI yet. "
|
|
249
|
+
"Install it from the private registry or run `dft playground` later "
|
|
250
|
+
"to install interactively."
|
|
251
|
+
)
|
|
242
252
|
|
|
243
253
|
if do_vscode:
|
|
244
254
|
from dataface.cli.commands import extension as extension_cmd
|
dataface/cli/commands/query.py
CHANGED
|
@@ -186,7 +186,7 @@ def _resolve_sql_text(
|
|
|
186
186
|
if not file.exists():
|
|
187
187
|
typer.echo(f"Error: file not found: {file}", err=True)
|
|
188
188
|
raise typer.Exit(1)
|
|
189
|
-
return file.read_text(), False, None
|
|
189
|
+
return file.read_text(encoding="utf-8"), False, None
|
|
190
190
|
|
|
191
191
|
if not target:
|
|
192
192
|
typer.echo(
|
dataface/cli/main.py
CHANGED
|
@@ -903,9 +903,16 @@ def serve(
|
|
|
903
903
|
Renders your dashboards in the browser. Face file paths map
|
|
904
904
|
to URLs (faces/sales.yml → /sales/). Query params become variables.
|
|
905
905
|
|
|
906
|
-
Auto-discovers the project root by walking
|
|
907
|
-
to find dataface.yml or dbt_project.yml.
|
|
908
|
-
the SQL dialect is inferred from the profile
|
|
906
|
+
Auto-discovers the project root by walking UP from the current directory
|
|
907
|
+
to find dataface.yml or dbt_project.yml. Subdirectories are not searched.
|
|
908
|
+
When a dbt project is found, the SQL dialect is inferred from the profile
|
|
909
|
+
target.
|
|
910
|
+
|
|
911
|
+
dbt profile location is resolved in this order:
|
|
912
|
+
1. profiles_dir field in the dbt_profile source config (dataface.yml)
|
|
913
|
+
2. DBT_PROFILES_DIR environment variable
|
|
914
|
+
3. Project root (next to dataface.yml / dbt_project.yml)
|
|
915
|
+
4. ~/.dbt/profiles.yml
|
|
909
916
|
|
|
910
917
|
Port is auto-resolved: --port flag > DFT_PORT env var > dataface.yml
|
|
911
918
|
port field > deterministic hash of project directory. If the chosen port
|
|
@@ -478,20 +478,19 @@ def compile_file(
|
|
|
478
478
|
|
|
479
479
|
# Markdown report files: translate to YAML before compiling
|
|
480
480
|
if file_path.suffix.lower() == ".md":
|
|
481
|
-
from dataface.core.compile.markdown import
|
|
481
|
+
from dataface.core.compile.markdown import (
|
|
482
|
+
MARKDOWN_NOT_FACE_MESSAGE,
|
|
483
|
+
is_markdown_face,
|
|
484
|
+
markdown_to_yaml,
|
|
485
|
+
)
|
|
482
486
|
|
|
483
487
|
if not is_markdown_face(file_path):
|
|
484
488
|
return CompileResult(
|
|
485
|
-
errors=[
|
|
486
|
-
CompilationError(
|
|
487
|
-
"Not a Dataface face file: .md files require YAML frontmatter "
|
|
488
|
-
"with at least one of: queries, charts, variables, source, sources"
|
|
489
|
-
).to_structured()
|
|
490
|
-
]
|
|
489
|
+
errors=[CompilationError(MARKDOWN_NOT_FACE_MESSAGE).to_structured()]
|
|
491
490
|
)
|
|
492
491
|
|
|
493
492
|
try:
|
|
494
|
-
raw_text = file_path.read_text()
|
|
493
|
+
raw_text = file_path.read_text(encoding="utf-8")
|
|
495
494
|
yaml_content = markdown_to_yaml(raw_text)
|
|
496
495
|
except (OSError, ValueError) as e:
|
|
497
496
|
return CompileResult(
|
|
@@ -499,7 +498,7 @@ def compile_file(
|
|
|
499
498
|
)
|
|
500
499
|
else:
|
|
501
500
|
try:
|
|
502
|
-
yaml_content = file_path.read_text()
|
|
501
|
+
yaml_content = file_path.read_text(encoding="utf-8")
|
|
503
502
|
except OSError as e:
|
|
504
503
|
return CompileResult(
|
|
505
504
|
errors=[CompilationError(f"Failed to read file: {e}").to_structured()]
|