dataface 0.2.1.dev516__py3-none-any.whl → 0.2.1.dev647__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 +21 -26
- dataface/_install_hint.py +51 -11
- dataface/agent_api/__init__.py +9 -3
- dataface/agent_api/_paths.py +82 -5
- dataface/agent_api/cache.py +55 -10
- dataface/agent_api/dashboards.py +23 -67
- dataface/agent_api/describe.py +33 -34
- dataface/agent_api/describe_query.py +2 -2
- dataface/agent_api/diagnostics.py +217 -0
- dataface/agent_api/docs/_loader.py +37 -38
- dataface/agent_api/docs/yaml-reference.md +49 -27
- dataface/agent_api/files.py +1 -1
- dataface/agent_api/mcp_install.py +142 -41
- dataface/agent_api/project_session.py +7 -14
- dataface/agent_api/query.py +20 -18
- dataface/agent_api/schema_hints.py +11 -10
- dataface/agent_api/search.py +2 -4
- dataface/agent_api/surface_aliases.yaml +2 -2
- dataface/agent_api/validate.py +49 -48
- dataface/ai/agent.py +16 -17
- dataface/ai/llm.py +0 -17
- dataface/ai/mcp/server.py +30 -23
- dataface/ai/prompts/sql-guidance.md +1 -1
- dataface/ai/skills/dashboard-build/SKILL.md +1 -1
- dataface/ai/skills/dashboard-replicate/SKILL.md +38 -6
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +2 -3
- dataface/ai/skills/drill-down-link/SKILL.md +11 -5
- dataface/ai/tool_schemas.py +8 -14
- dataface/ai/tools/__init__.py +41 -16
- dataface/cli/_error_format.py +41 -53
- dataface/cli/commands/chat.py +1 -1
- dataface/cli/commands/describe.py +2 -2
- dataface/cli/commands/docs.py +48 -21
- dataface/cli/commands/mcp_init.py +13 -29
- dataface/cli/commands/render.py +80 -63
- dataface/cli/commands/search.py +2 -2
- dataface/cli/commands/serve.py +9 -12
- dataface/cli/commands/validate.py +3 -3
- dataface/cli/filesystem_project.py +40 -1
- dataface/cli/main.py +33 -17
- dataface/core/compile/__init__.py +1 -1
- dataface/core/compile/authoring_warnings.py +46 -58
- dataface/core/compile/compiler.py +620 -164
- dataface/core/compile/config.py +93 -2
- dataface/core/compile/errors.py +16 -17
- dataface/core/compile/introspection.py +67 -55
- dataface/core/compile/merge.py +4 -2
- dataface/core/compile/meta.py +12 -0
- dataface/core/compile/models/cache.py +390 -0
- dataface/core/compile/models/chart/authored/__init__.py +16 -0
- dataface/core/compile/models/chart/authored/_base.py +8 -0
- dataface/core/compile/models/chart/resolved/_base.py +8 -0
- dataface/core/compile/models/chart/resolved/callout.py +12 -0
- dataface/core/compile/models/config.py +52 -0
- dataface/core/compile/models/face/authored.py +20 -0
- dataface/core/compile/models/factories.py +4 -2
- dataface/core/compile/models/primitives.py +38 -8
- dataface/core/compile/models/query/authored.py +22 -6
- dataface/core/compile/models/query/normalized.py +24 -8
- dataface/core/compile/models/refs.py +20 -9
- dataface/core/compile/models/source.py +35 -3
- dataface/core/compile/models/style/resolved/_base.py +25 -3
- dataface/core/compile/models/style/resolved/_cascade.py +46 -4
- dataface/core/compile/models/style/theme/__init__.py +4 -0
- dataface/core/compile/models/style/theme/axis.py +29 -2
- dataface/core/compile/models/style/theme/board.py +8 -1
- dataface/core/compile/models/style/theme/legend.py +19 -5
- dataface/core/compile/normalize_charts.py +55 -11
- dataface/core/compile/normalize_layout.py +71 -7
- dataface/core/compile/normalize_queries.py +92 -26
- dataface/core/compile/normalize_variables.py +92 -25
- dataface/core/compile/normalizer.py +99 -15
- dataface/core/compile/palette.py +42 -17
- dataface/core/compile/parser.py +13 -20
- dataface/core/compile/resolve/_resolve.py +432 -121
- dataface/core/compile/schema_renderers/completion_catalog.py +0 -2
- dataface/core/compile/schema_renderers/highlight_manifest.py +11 -4
- dataface/core/compile/schema_renderers/json_schema.py +120 -12
- dataface/core/compile/schema_renderers/prompt.py +38 -4
- dataface/core/compile/schema_renderers/vscode_schema.py +42 -22
- dataface/core/compile/schema_renderers/yaml_schema.py +26 -6
- dataface/core/compile/source_map.py +211 -0
- dataface/core/compile/sql_guard.py +1 -1
- dataface/core/compile/style_cascade.py +88 -43
- dataface/core/compile/tick_values.py +15 -0
- dataface/core/compile/typography.py +13 -10
- dataface/core/compile/validator.py +3 -3
- dataface/core/compile/variables.py +1 -1
- dataface/core/compile/yaml_error_formatter.py +40 -123
- dataface/core/dashboard.py +168 -187
- dataface/core/defaults/default_config.yml +26 -0
- dataface/core/defaults/themes/_base.yaml +7 -1
- dataface/core/defaults/themes/editorial.yaml +13 -0
- dataface/core/defaults/themes/neon.yaml +6 -0
- dataface/core/defaults/themes/vivid.yaml +7 -0
- dataface/core/diagnostics/__init__.py +227 -0
- dataface/core/diagnostics/base.py +91 -0
- dataface/core/{errors → diagnostics}/chart_data.py +3 -3
- dataface/core/diagnostics/codes_compile.py +601 -0
- dataface/core/diagnostics/codes_execute.py +238 -0
- dataface/core/diagnostics/codes_query.py +98 -0
- dataface/core/diagnostics/codes_render.py +727 -0
- dataface/core/diagnostics/codes_serve.py +44 -0
- dataface/core/diagnostics/codes_unknown.py +20 -0
- dataface/core/diagnostics/diagnostic.py +214 -0
- dataface/core/{errors → diagnostics}/execution.py +16 -13
- dataface/core/diagnostics/from_query_diagnostic.py +54 -0
- dataface/core/diagnostics/hints.py +47 -0
- dataface/core/diagnostics/registry.py +152 -0
- dataface/core/diagnostics/render_reference.py +128 -0
- dataface/core/diagnostics/suppression.py +85 -0
- dataface/core/dialects/athena.py +3 -42
- dataface/core/dialects/base.py +0 -54
- dataface/core/dialects/bigquery.py +1 -2
- dataface/core/dialects/databricks.py +0 -31
- dataface/core/dialects/duckdb.py +0 -1
- dataface/core/dialects/mysql.py +2 -20
- dataface/core/dialects/postgres.py +0 -1
- dataface/core/dialects/snowflake.py +2 -17
- dataface/core/dialects/sqlite.py +0 -1
- dataface/core/dialects/sqlserver.py +1 -41
- dataface/core/execute/__init__.py +1 -30
- dataface/core/execute/_duckdb_cache_base.py +180 -70
- dataface/core/execute/adapters/adapter_registry.py +69 -52
- dataface/core/execute/adapters/base.py +65 -3
- dataface/core/execute/adapters/dbt_adapter.py +19 -10
- dataface/core/execute/adapters/duckdb_adapter.py +47 -31
- dataface/core/execute/adapters/schema_adapter.py +1 -1
- dataface/core/execute/adapters/sql_adapter.py +109 -72
- dataface/core/execute/adapters/sqlite_adapter.py +26 -17
- dataface/core/execute/cache_backend.py +97 -28
- dataface/core/execute/cache_keys.py +15 -3
- dataface/core/execute/collect.py +83 -0
- dataface/core/execute/executor.py +176 -352
- dataface/core/execute/file_source_materializer.py +4 -3
- dataface/core/execute/source_resolver.py +58 -31
- dataface/core/execute/trivial_local_cache.py +108 -74
- dataface/core/inspect/__init__.py +2 -2
- dataface/core/inspect/bulk_schema.py +1 -1
- dataface/core/inspect/partials/categorical.yml +2 -4
- dataface/core/inspect/partials/date.yml +2 -4
- dataface/core/inspect/partials/numeric.yml +2 -4
- dataface/core/inspect/query_validator.py +63 -28
- dataface/core/inspect/renderer.py +11 -70
- dataface/core/inspect/resolver.py +17 -13
- dataface/core/inspect/templates/categorical_column.yml +1 -8
- dataface/core/inspect/templates/charts.yml +1 -8
- dataface/core/inspect/templates/date_column.yml +1 -8
- dataface/core/inspect/templates/model.yml +1 -8
- dataface/core/inspect/templates/numeric_column.yml +1 -8
- dataface/core/inspect/templates/quality.yml +1 -5
- dataface/core/inspect/templates/string_column.yml +1 -8
- dataface/core/project.py +134 -1
- dataface/core/registered_views/query_runner.py +19 -7
- dataface/core/registered_views/render_pipeline.py +14 -32
- dataface/core/render/__init__.py +1 -1
- dataface/core/render/board_links.py +7 -7
- dataface/core/render/chart/auto_link.py +6 -3
- dataface/core/render/chart/callout.py +2 -2
- dataface/core/render/chart/data_table_attachment.py +16 -16
- dataface/core/render/chart/emitters/__init__.py +2 -2
- dataface/core/render/chart/emitters/_cartesian.py +29 -5
- dataface/core/render/chart/emitters/_overlay.py +120 -28
- dataface/core/render/chart/emitters/area.py +3 -5
- dataface/core/render/chart/emitters/bar.py +46 -41
- dataface/core/render/chart/emitters/heatmap.py +20 -1
- dataface/core/render/chart/emitters/line.py +3 -5
- dataface/core/render/chart/emitters/scatter.py +8 -1
- dataface/core/render/chart/features/endpoint_labels.py +47 -21
- dataface/core/render/chart/features/facet.py +1 -1
- dataface/core/render/chart/features/mirror_axis.py +1 -1
- dataface/core/render/chart/features/value_labels.py +3 -3
- dataface/core/render/chart/geo_tooltip.py +3 -3
- dataface/core/render/chart/kpi.py +3 -3
- dataface/core/render/chart/rendering.py +35 -27
- dataface/core/render/chart/step_band.py +1 -1
- dataface/core/render/chart/table.py +367 -160
- dataface/core/render/chart/time_unit_detect.py +6 -4
- dataface/core/render/chart/type_inference.py +14 -9
- dataface/core/render/chart/validation.py +3 -3
- dataface/core/render/chart/vega_lite.py +5 -5
- dataface/core/render/chart/vl_field_maps.py +49 -60
- dataface/core/render/converters/chart.py +5 -3
- dataface/core/render/converters/html.py +7 -3
- dataface/core/render/dir_context.py +80 -143
- dataface/core/render/errors.py +6 -20
- dataface/core/render/face_to_dict.py +18 -15
- dataface/core/render/faces.py +18 -4
- dataface/core/render/format_utils.py +2 -2
- dataface/core/render/json_format.py +2 -2
- dataface/core/render/layout_sizing.py +47 -18
- dataface/core/render/layouts.py +5 -5
- dataface/core/render/nav.py +4 -3
- dataface/core/render/render_result.py +5 -6
- dataface/core/render/renderer.py +28 -29
- dataface/core/render/stable_domain.py +4 -4
- dataface/core/render/templates/page.css +7 -2
- dataface/core/render/templates/page.html +3 -2
- dataface/core/render/templates/scripts/chart_interactivity.js +18 -8
- dataface/core/render/terminal_charts.py +1 -1
- dataface/core/render/text_format.py +2 -2
- dataface/core/render/utils.py +20 -14
- dataface/core/render/variable_controls.py +1 -1
- dataface/core/render/warnings/__init__.py +2 -2
- dataface/core/render/warnings/bar_band_width_too_narrow.py +15 -25
- dataface/core/render/warnings/base.py +3 -3
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +15 -22
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +16 -24
- dataface/core/render/warnings/palette_unsupported.py +43 -0
- dataface/core/render/warnings/pie_dominant_segment.py +11 -17
- dataface/core/render/warnings/pie_too_many_segments.py +13 -17
- dataface/core/render/warnings/point_map_out_of_projection.py +13 -19
- dataface/core/render/warnings/query_returned_zero_rows.py +12 -15
- dataface/core/render/warnings/redundant_encoding.py +11 -20
- dataface/core/render/warnings/registry.py +12 -5
- dataface/core/render/warnings/table_columns_overflow.py +12 -24
- dataface/core/render/warnings/temporal_single_point.py +11 -21
- dataface/core/render/warnings/too_many_color_categories.py +14 -18
- dataface/core/render/warnings/too_many_x_categories.py +14 -18
- dataface/core/render/warnings/value_labels_crowd_width.py +12 -25
- dataface/core/render/warnings/y_encoding_mostly_null.py +14 -23
- dataface/core/render/yaml_format.py +2 -2
- dataface/core/serve/server.py +125 -135
- dataface/core/serve/templates/error.html.j2 +7 -72
- dataface/core/validate.py +1 -1
- dataface/data/highlighting/face/v1.json +2 -12
- dataface/integrations/markdown.py +13 -5
- {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/METADATA +3 -2
- {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/RECORD +232 -237
- dataface/agent_api/docs/warnings.py +0 -171
- dataface/ai/memories.py +0 -85
- dataface/core/errors/__init__.py +0 -91
- dataface/core/errors/base.py +0 -57
- dataface/core/errors/codes_compile.py +0 -372
- dataface/core/errors/codes_execute.py +0 -254
- dataface/core/errors/codes_render.py +0 -324
- dataface/core/errors/codes_serve.py +0 -35
- dataface/core/errors/codes_unknown.py +0 -15
- dataface/core/errors/hints.py +0 -87
- dataface/core/errors/registry.py +0 -48
- dataface/core/errors/render_error_reference.py +0 -42
- dataface/core/errors/structured.py +0 -88
- dataface/core/execute/batch.py +0 -790
- dataface/core/scoped_paths.py +0 -114
- dataface/core/warnings/__init__.py +0 -16
- dataface/core/warnings/base.py +0 -20
- dataface/core/warnings/fanout_risk.py +0 -15
- dataface/core/warnings/from_query_diagnostic.py +0 -56
- dataface/core/warnings/missing_join_predicate.py +0 -13
- dataface/core/warnings/query_parse_error.py +0 -14
- dataface/core/warnings/reaggregation.py +0 -14
- dataface/core/warnings/suppression.py +0 -46
- dataface/core/warnings/unreferenced_chart.py +0 -15
- {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/WHEEL +0 -0
- {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/entry_points.txt +0 -0
- {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -155,7 +155,7 @@ Reference variables inside queries with bare `{{ region }}` — no `variables.`
|
|
|
155
155
|
charts:
|
|
156
156
|
revenue_trend:
|
|
157
157
|
query: revenue # Named query reference
|
|
158
|
-
type: line # See `dft docs charts` for all
|
|
158
|
+
type: line # See `dft docs charts` for all 16 authorable chart types
|
|
159
159
|
x: month
|
|
160
160
|
y: total
|
|
161
161
|
color: segment
|
|
@@ -413,7 +413,7 @@ Common fields (all query types):
|
|
|
413
413
|
| `filters` | Post-execution result filters |
|
|
414
414
|
| `limit` | Maximum rows returned |
|
|
415
415
|
| `pivot` | Table-rendering cross-tab hint: `{column, value}` |
|
|
416
|
-
| `ignore` | Diagnostic codes to suppress (e.g. `["
|
|
416
|
+
| `ignore` | Diagnostic codes to suppress (e.g. `["WARN-FANOUT-RISK"]`) |
|
|
417
417
|
|
|
418
418
|
SQL fields: `sql`, `setup_sql`.
|
|
419
419
|
MetricFlow fields: `metrics`, `dimensions`, `time_grain`.
|
|
@@ -531,25 +531,25 @@ charts:
|
|
|
531
531
|
# bar/area families also accept style.orientation and style.stack
|
|
532
532
|
```
|
|
533
533
|
|
|
534
|
-
### Chart types (
|
|
534
|
+
### Chart types (16 authorable)
|
|
535
535
|
|
|
536
536
|
Set `type:` to one of:
|
|
537
537
|
|
|
538
|
-
**Basic** (one mark per chart)
|
|
538
|
+
**Basic** (one mark per chart) -- `bar`, `line`, `area`, `scatter`, `pie`, `donut`, `kpi`, `table`.
|
|
539
539
|
|
|
540
|
-
**Statistical**
|
|
540
|
+
**Statistical** -- `histogram`, `heatmap`.
|
|
541
541
|
|
|
542
|
-
**Geographic**
|
|
542
|
+
**Geographic** -- `geoshape`, `map`, `point_map`, `bubble_map`.
|
|
543
543
|
|
|
544
|
-
**
|
|
544
|
+
**Overlays** -- `bar`, `line`, `area`, and `scatter` accept a `layers:` field for mixed-mark or dual-axis charts (see [Combo charts](#combo-charts-barlinearea-with-layers) and [Composition](#composition)).
|
|
545
545
|
|
|
546
|
-
**
|
|
546
|
+
**Sparklines** -- `spark_bar` (compact horizontal bars used in profiler cards).
|
|
547
547
|
|
|
548
|
-
**
|
|
548
|
+
**Auxiliary** -- `callout` (message card with a `style.tone:` field; `message:` required).
|
|
549
549
|
|
|
550
|
-
|
|
550
|
+
Note: `donut` is an internal alias for `pie` -- `donut` is accepted but normalizes to `pie` internally. `auto` is an internal sentinel (not authored). `boxplot`, `errorbar`, and `errorband` are Vega-Lite mark types that are not in the authorable surface.
|
|
551
551
|
|
|
552
|
-
|
|
552
|
+
Type aliases: `scatter` uses a circle mark, `heatmap` uses rect, `pie`/`donut` use arc, `histogram` uses bar with binning, `map` maps to geoshape.
|
|
553
553
|
|
|
554
554
|
### Shared chart fields
|
|
555
555
|
|
|
@@ -709,11 +709,6 @@ color: event_count
|
|
|
709
709
|
type: histogram
|
|
710
710
|
x: amount
|
|
711
711
|
|
|
712
|
-
# boxplot / errorbar / errorband — statistical primitives
|
|
713
|
-
type: boxplot
|
|
714
|
-
x: region
|
|
715
|
-
y: latency
|
|
716
|
-
|
|
717
712
|
# geoshape — choropleth via GeoJSON
|
|
718
713
|
type: geoshape
|
|
719
714
|
geo_source: us-states
|
|
@@ -1115,26 +1110,26 @@ rows:
|
|
|
1115
1110
|
|
|
1116
1111
|
## Errors
|
|
1117
1112
|
|
|
1118
|
-
Dataface errors carry a machine-readable code in the form `
|
|
1113
|
+
Dataface errors carry a machine-readable code in the form `ERR-<SLUG>`. The error message includes the code and a pointer to docs; a separate `domain` field on the error records where it originated (the code string itself has no domain segment).
|
|
1119
1114
|
|
|
1120
1115
|
```
|
|
1121
|
-
DatafaceError [
|
|
1116
|
+
DatafaceError [ERR-KPI-MULTIROW]: KPI chart 'revenue' returned 12 rows but `value` is a column reference.
|
|
1122
1117
|
Add LIMIT 1 or aggregate down to one row.
|
|
1123
1118
|
```
|
|
1124
1119
|
|
|
1125
|
-
Active
|
|
1120
|
+
Active domains (see `dataface/core/diagnostics/codes_*.py` for the authoritative registry):
|
|
1126
1121
|
|
|
1127
|
-
|
|
|
1122
|
+
| Domain | Meaning |
|
|
1128
1123
|
|--------|---------|
|
|
1129
|
-
| `
|
|
1130
|
-
| `
|
|
1131
|
-
| `
|
|
1132
|
-
| `
|
|
1133
|
-
| `
|
|
1124
|
+
| `compile` | Compile-time errors (missing fields, unknown references, malformed YAML) |
|
|
1125
|
+
| `render` | Chart rendering errors (wrong row counts, unknown chart types, format issues) |
|
|
1126
|
+
| `execute` | Query execution errors (missing sources, inline-source policy, source typing) |
|
|
1127
|
+
| `serve` | Server startup errors (invalid theme, launch failures) |
|
|
1128
|
+
| `unknown` | Fallback codes for legacy string-message errors not yet migrated |
|
|
1134
1129
|
|
|
1135
1130
|
The registry is being filled out incrementally — some compile-time and variable-resolution errors still raise as plain `DatafaceError` without a structured code.
|
|
1136
1131
|
|
|
1137
|
-
Run `dft docs error-reference` for the full
|
|
1132
|
+
Run `dft docs errors` to list every registered code with a one-line summary, straight from `dataface.core.diagnostics.REGISTRY`. Run `dft docs error-reference` for the full reference — message template and docs pointer included. (`dft docs warnings` / `dft docs warning-reference` are the equivalents for `WARN-*` codes.) Run `dft validate <face>` to validate a face and see structured error details.
|
|
1138
1133
|
|
|
1139
1134
|
**See also:** `dft docs queries`, `dft docs charts`, `dft docs variables` (where most errors originate),
|
|
1140
1135
|
`dft docs all` (whole reference for context).
|
dataface/_install_hint.py
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Install-command hints surfaced from runtime error messages.
|
|
2
2
|
|
|
3
3
|
Centralized here so every CLI/runtime hint stays in lock-step with
|
|
4
|
-
``dataface/README.md`` and the wheel-shipped MCP-setup skill.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
``
|
|
4
|
+
``dataface/README.md`` and the wheel-shipped MCP-setup skill.
|
|
5
|
+
|
|
6
|
+
This module only ever runs from an install that already exists (a
|
|
7
|
+
missing extra, a missing optional dependency like ``uvicorn``) — so the
|
|
8
|
+
right question is "how did *this* install get here", not "is `uv` on
|
|
9
|
+
PATH". A pip/venv install with `uv` merely available on PATH must keep
|
|
10
|
+
recommending `pip`; recommending `uv tool install` there would create a
|
|
11
|
+
second, parallel installation alongside the one already running.
|
|
12
|
+
``cli/_extras.py`` separately emits ``uv pip install --python <path>``
|
|
13
|
+
when uv is the active installer for *adding an extra to the current
|
|
14
|
+
interpreter* (a different question); this helper is the canonical
|
|
15
|
+
reinstall hint.
|
|
16
|
+
|
|
17
|
+
The TypeScript mirror at
|
|
18
|
+
``apps/ide/vscode-extension/src/utils/install-hint.ts`` uses a
|
|
19
|
+
*different* signal (is `uv` on PATH) because it runs before anything is
|
|
20
|
+
installed — there is no existing install to inspect there. If you
|
|
21
|
+
change one file, check whether the other's signal still applies before
|
|
22
|
+
mirroring the change.
|
|
13
23
|
"""
|
|
14
24
|
|
|
15
25
|
from __future__ import annotations
|
|
16
26
|
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_uv_tool_install() -> bool:
|
|
33
|
+
"""True if this process is running from a `uv tool install` environment.
|
|
34
|
+
|
|
35
|
+
`uv tool install` creates an isolated virtualenv under uv's tool
|
|
36
|
+
directory (`uv tool dir`; e.g. `~/.local/share/uv/tools` on Linux,
|
|
37
|
+
the platform's per-user data dir on macOS/Windows, overridable via
|
|
38
|
+
`UV_TOOL_DIR`) with the tool name as an immediate child directory.
|
|
39
|
+
`sys.prefix` is that tool venv's own directory — it's the value uv
|
|
40
|
+
set up when creating the venv, not a filesystem fact to be
|
|
41
|
+
recomputed. Deliberately NOT `Path(sys.executable).resolve()`: a
|
|
42
|
+
uv-tool venv's `bin/python` is a symlink back into uv's *shared*
|
|
43
|
+
interpreter directory (`uv/python/...`), so resolving it walks
|
|
44
|
+
straight out of `uv/tools/` and defeats this check.
|
|
45
|
+
"""
|
|
46
|
+
prefix = Path(sys.prefix)
|
|
47
|
+
tool_dir = os.environ.get("UV_TOOL_DIR")
|
|
48
|
+
if tool_dir and prefix.parent == Path(tool_dir):
|
|
49
|
+
return True
|
|
50
|
+
parts = prefix.parts
|
|
51
|
+
return any(
|
|
52
|
+
a == "uv" and b == "tools" for a, b in zip(parts, parts[1:], strict=False)
|
|
53
|
+
)
|
|
54
|
+
|
|
17
55
|
|
|
18
56
|
def install_hint(extra: str | None = None) -> str:
|
|
19
|
-
"""Return the
|
|
57
|
+
"""Return the install one-liner that matches how this Dataface got installed.
|
|
20
58
|
|
|
21
59
|
``extra`` is an optional optional-dependency group (``"mcp"``,
|
|
22
60
|
``"chat"``, ``"playground"``, ``"bigquery"``); when set, the
|
|
23
61
|
returned command installs Dataface with that extras bracket.
|
|
24
62
|
"""
|
|
25
63
|
spec = f"dataface[{extra}]" if extra else "dataface"
|
|
64
|
+
if _is_uv_tool_install():
|
|
65
|
+
return f'uv tool install "{spec}"'
|
|
26
66
|
return f'pip install "{spec}"'
|
dataface/agent_api/__init__.py
CHANGED
|
@@ -70,7 +70,7 @@ from dataface.core.dashboard import (
|
|
|
70
70
|
RenderedDashboard as RenderedDashboard,
|
|
71
71
|
render_dashboard as render_dashboard,
|
|
72
72
|
)
|
|
73
|
-
from dataface.core.
|
|
73
|
+
from dataface.core.diagnostics import Diagnostic as Diagnostic
|
|
74
74
|
|
|
75
75
|
# Execution extension seams: the protocols a composition root injects, plus the
|
|
76
76
|
# resolver base impls it subclasses. Re-exported so roots (CLI, MCP, serve,
|
|
@@ -85,7 +85,11 @@ from dataface.core.execute.source_resolver import (
|
|
|
85
85
|
SourceResolver as SourceResolver,
|
|
86
86
|
)
|
|
87
87
|
from dataface.core.fonts import get_inter_font_path as get_inter_font_path
|
|
88
|
-
from dataface.core.project import
|
|
88
|
+
from dataface.core.project import (
|
|
89
|
+
FaceFile as FaceFile,
|
|
90
|
+
InMemoryFace as InMemoryFace,
|
|
91
|
+
Project as Project,
|
|
92
|
+
)
|
|
89
93
|
from dataface.core.render.board_links import LinkContext as LinkContext
|
|
90
94
|
from dataface.core.render.errors import RenderError as RenderError
|
|
91
95
|
|
|
@@ -94,9 +98,12 @@ __all__ = [
|
|
|
94
98
|
"CompileResult",
|
|
95
99
|
"DescribeFaceArgs",
|
|
96
100
|
"DescribeFaceResult",
|
|
101
|
+
"Diagnostic",
|
|
97
102
|
"DocsArgs",
|
|
98
103
|
"DocsResult",
|
|
99
104
|
"DocsSearchHit",
|
|
105
|
+
"FaceFile",
|
|
106
|
+
"InMemoryFace",
|
|
100
107
|
"InitResult",
|
|
101
108
|
"LinkContext",
|
|
102
109
|
"Project",
|
|
@@ -107,7 +114,6 @@ __all__ = [
|
|
|
107
114
|
"RenderedDashboard",
|
|
108
115
|
"RenderError",
|
|
109
116
|
"SchemaHints",
|
|
110
|
-
"StructuredError",
|
|
111
117
|
"Topic",
|
|
112
118
|
"ValidateDashboardArgs",
|
|
113
119
|
"ValidateResult",
|
dataface/agent_api/_paths.py
CHANGED
|
@@ -2,20 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
# tach-ignore(agent_api->cli: runtime host-type guard in no_project_hint; the local-filesystem check needs a non-cli signal on Project — deferred)
|
|
9
10
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
10
|
-
from dataface.core.
|
|
11
|
+
from dataface.core.diagnostics import (
|
|
12
|
+
ERR_FILE_NOT_FOUND,
|
|
13
|
+
ERR_INTERNAL,
|
|
14
|
+
Diagnostic,
|
|
15
|
+
)
|
|
16
|
+
from dataface.core.diagnostics.base import DatafaceError
|
|
17
|
+
from dataface.core.project import (
|
|
18
|
+
FACES_SUBDIR,
|
|
19
|
+
FaceFile,
|
|
20
|
+
Project,
|
|
21
|
+
ProjectPath,
|
|
22
|
+
assert_relpath,
|
|
23
|
+
)
|
|
11
24
|
from dataface.core.project_roots import (
|
|
12
25
|
find_dft_root as find_dft_root,
|
|
13
26
|
find_project_root as find_project_root,
|
|
14
27
|
)
|
|
15
|
-
from dataface.core.scoped_paths import (
|
|
16
|
-
resolve_face_path as resolve_face_path,
|
|
17
|
-
resolve_scoped_path as resolve_scoped_path,
|
|
18
|
-
)
|
|
19
28
|
|
|
20
29
|
|
|
21
30
|
def resolve_project_dir(project_dir: Path | None) -> Path:
|
|
@@ -66,6 +75,74 @@ def no_project_hint(project: Project) -> str:
|
|
|
66
75
|
)
|
|
67
76
|
|
|
68
77
|
|
|
78
|
+
def resolve_face_path(path: Path, project: Project) -> ProjectPath:
|
|
79
|
+
"""User/agent-supplied face path -> ProjectPath.
|
|
80
|
+
|
|
81
|
+
Absolute and ``..``-leading paths are explicit locations: they relativize
|
|
82
|
+
through the containment seam (``project.path_for_fspath``, a leading
|
|
83
|
+
``..`` resolving against cwd for CLI shell-nav feel) and are returned
|
|
84
|
+
as-is — no ``faces/`` retry, even when the exact path doesn't exist. A
|
|
85
|
+
bare relative name (no explicit location) gets the faces-first retry: for
|
|
86
|
+
a relpath that doesn't exist in the project store and has no explicit
|
|
87
|
+
``faces/`` prefix, retries under ``FACES_SUBDIR``. This lets a
|
|
88
|
+
``search_dashboards`` ``face_path`` of ``'looker/x.yaml'`` (``faces/``
|
|
89
|
+
prefix stripped at search time) resolve to ``faces/looker/x.yaml``
|
|
90
|
+
without producing a ``faces/faces/...`` double prefix when the caller
|
|
91
|
+
already supplies the full ``faces/looker/x.yaml``.
|
|
92
|
+
|
|
93
|
+
Existence is checked via ``project.exists(...)`` so it works for all
|
|
94
|
+
``Project`` implementations (FilesystemProject, CloudManagedProject
|
|
95
|
+
git-blob store, etc.) — not just the local filesystem. Paths that exist
|
|
96
|
+
at root resolve from root — non-faces paths like ``models/schema.sql``
|
|
97
|
+
are not rewritten under faces/.
|
|
98
|
+
"""
|
|
99
|
+
if path.is_absolute():
|
|
100
|
+
return project.path_for_fspath(path)
|
|
101
|
+
if ".." in path.parts:
|
|
102
|
+
return project.path_for_fspath(Path(os.path.normpath(Path.cwd() / path)))
|
|
103
|
+
relpath = str(path)
|
|
104
|
+
assert_relpath(relpath)
|
|
105
|
+
if project.exists(relpath):
|
|
106
|
+
return project.path(relpath)
|
|
107
|
+
if not relpath.startswith(f"{FACES_SUBDIR}/"):
|
|
108
|
+
candidate = f"{FACES_SUBDIR}/{relpath}"
|
|
109
|
+
if project.exists(candidate):
|
|
110
|
+
return project.path(candidate)
|
|
111
|
+
return project.path(relpath) # caller reports not-found
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def resolve_face_or_error(path: Path, project: Project) -> FaceFile | Diagnostic:
|
|
115
|
+
"""Faces-first resolve a path to a stored ``FaceFile``, or a structured error.
|
|
116
|
+
|
|
117
|
+
The one agent_api seam turning a user-supplied path into the compiler's
|
|
118
|
+
located input — shared by the CLI render verb and the AI render tool so the
|
|
119
|
+
faces-first retry and the ``ERR-FILE-NOT-FOUND`` envelope never drift
|
|
120
|
+
between surfaces. Returns a ``Diagnostic`` for an escaping path
|
|
121
|
+
(``ERR-INTERNAL``) or a missing face (``ERR-FILE-NOT-FOUND``).
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
resolved = resolve_face_path(path, project)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
return DatafaceError.from_code(ERR_INTERNAL, message=str(exc)).to_diagnostic()
|
|
127
|
+
if not resolved.exists():
|
|
128
|
+
return DatafaceError.from_code(
|
|
129
|
+
ERR_FILE_NOT_FOUND,
|
|
130
|
+
path=resolved.relpath,
|
|
131
|
+
hint=no_project_hint(project),
|
|
132
|
+
).to_diagnostic(file=resolved.relpath)
|
|
133
|
+
if (
|
|
134
|
+
isinstance(project, FilesystemProject)
|
|
135
|
+
and not (project.root / resolved.relpath).is_file()
|
|
136
|
+
):
|
|
137
|
+
# A directory (or other non-file) resolves + exists on disk but read_face
|
|
138
|
+
# would raise past the structured envelope. This is a filesystem-only
|
|
139
|
+
# concern — a non-filesystem store's `exists()` already gates real faces.
|
|
140
|
+
return DatafaceError.from_code(
|
|
141
|
+
ERR_INTERNAL, message=f"Not a file: {resolved.relpath}"
|
|
142
|
+
).to_diagnostic(file=resolved.relpath)
|
|
143
|
+
return resolved.read_face()
|
|
144
|
+
|
|
145
|
+
|
|
69
146
|
@dataclass(frozen=True)
|
|
70
147
|
class FaceRenderContext:
|
|
71
148
|
"""Path resolution result from a face path + project root.
|
dataface/agent_api/cache.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""Cache constructors at the agent_api boundary."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from collections.abc import Generator
|
|
4
6
|
from contextlib import contextmanager
|
|
5
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from dataface.core.compile.config import resolve_cache_boot
|
|
6
11
|
|
|
7
12
|
# Re-exported: TrivialDuckDBCache is the declared return type of open_cache,
|
|
8
13
|
# so embedders annotating a held cache reach it here without a core import.
|
|
@@ -10,29 +15,69 @@ from dataface.core.execute.trivial_local_cache import (
|
|
|
10
15
|
TrivialDuckDBCache as TrivialDuckDBCache,
|
|
11
16
|
)
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from dataface.cli.filesystem_project import FilesystemProject
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"TrivialDuckDBCache",
|
|
23
|
+
"open_cache",
|
|
24
|
+
"open_project_cache",
|
|
25
|
+
"project_cache_ctx",
|
|
26
|
+
]
|
|
14
27
|
|
|
15
28
|
|
|
16
29
|
def open_cache(path: Path | None) -> TrivialDuckDBCache:
|
|
17
|
-
"""Open the query-result cache.
|
|
30
|
+
"""Open the query-result cache at an already-resolved path.
|
|
18
31
|
|
|
19
32
|
``path=None`` opens an in-memory cache — ephemeral, discarded when closed.
|
|
20
33
|
A path opens (or creates, if absent) a persistent DuckDB file, reused
|
|
21
34
|
across invocations. Callers own the returned cache's lifecycle (close it).
|
|
35
|
+
Does not consult project config — use ``open_project_cache`` for that.
|
|
22
36
|
"""
|
|
23
37
|
return TrivialDuckDBCache(db_path=path)
|
|
24
38
|
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
def open_project_cache(
|
|
41
|
+
project: FilesystemProject,
|
|
42
|
+
*,
|
|
43
|
+
no_cache: bool = False,
|
|
44
|
+
cache_path: Path | None = None,
|
|
45
|
+
) -> TrivialDuckDBCache | None:
|
|
46
|
+
"""Open the query-result cache honoring project config, with overrides.
|
|
47
|
+
|
|
48
|
+
The caller owns the project instance. Pair this with
|
|
49
|
+
``ProjectSession.from_project(project, cache=...)`` rather than
|
|
50
|
+
``ProjectSession.open(project_dir, cache=...)`` — ``open()`` builds its own
|
|
51
|
+
project from the path, so composing it with this function constructs two.
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
Precedence: ``no_cache=True`` (the CLI's ``--no-cache``) skips the cache
|
|
54
|
+
entirely; ``cache_path`` (``--cache PATH``, or its ``DFT_CACHE_PATH`` env
|
|
55
|
+
backing) opens that persistent file. Otherwise the project's
|
|
56
|
+
``dataface.yml`` ``cache:`` block picks the location — a configured ``path``
|
|
57
|
+
opens that file (created if absent); no path opens the zero-config
|
|
58
|
+
in-memory default. A project-level ``cache: false`` defaults the *cascade*
|
|
59
|
+
to off without closing the door on a nearer scope's opt-in, so it still
|
|
60
|
+
opens a store; the resolved per-query policy decides what gets written.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
``None`` only when ``no_cache=True`` — callers pass this straight
|
|
64
|
+
through as ``ProjectSession(cache=...)``.
|
|
33
65
|
"""
|
|
34
|
-
|
|
66
|
+
boot = resolve_cache_boot(project, no_cache=no_cache, cache_path=cache_path)
|
|
67
|
+
return open_cache(boot.path) if boot.enabled else None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@contextmanager
|
|
71
|
+
def project_cache_ctx(
|
|
72
|
+
project: FilesystemProject,
|
|
73
|
+
*,
|
|
74
|
+
no_cache: bool = False,
|
|
75
|
+
cache_path: Path | None = None,
|
|
76
|
+
) -> Generator[TrivialDuckDBCache | None]:
|
|
77
|
+
"""Context-managed ``open_project_cache``; closes the cache on exit if opened."""
|
|
78
|
+
cache = open_project_cache(project, no_cache=no_cache, cache_path=cache_path)
|
|
35
79
|
try:
|
|
36
80
|
yield cache
|
|
37
81
|
finally:
|
|
38
|
-
cache
|
|
82
|
+
if cache is not None:
|
|
83
|
+
cache.close()
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -13,17 +13,16 @@ from typing import Any, Literal
|
|
|
13
13
|
import yaml
|
|
14
14
|
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
15
15
|
|
|
16
|
-
from dataface.agent_api._paths import
|
|
16
|
+
from dataface.agent_api._paths import resolve_face_or_error
|
|
17
17
|
from dataface.core.compile import Face, compile_file
|
|
18
18
|
from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
|
|
19
|
-
from dataface.core.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
from dataface.core.diagnostics import Diagnostic
|
|
20
|
+
from dataface.core.project import (
|
|
21
|
+
FACES_SUBDIR,
|
|
22
|
+
Project,
|
|
23
|
+
ProjectDirectory,
|
|
24
|
+
ProjectPath,
|
|
23
25
|
)
|
|
24
|
-
from dataface.core.errors.base import DatafaceError
|
|
25
|
-
from dataface.core.project import FACES_SUBDIR, Project, ProjectPath
|
|
26
|
-
from dataface.core.warnings.base import RenderWarning
|
|
27
26
|
|
|
28
27
|
# Top-level keys whose presence classifies a YAML mapping as a dashboard.
|
|
29
28
|
DASHBOARD_KEYS = frozenset({"queries", "charts", "rows", "cols", "grid", "tabs"})
|
|
@@ -64,23 +63,27 @@ class SkippedFile(BaseModel):
|
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
class ListDashboardsResult(BaseModel):
|
|
67
|
-
model_config = ConfigDict(frozen=True)
|
|
66
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
|
|
68
67
|
|
|
69
68
|
success: bool
|
|
70
|
-
errors: list[
|
|
69
|
+
errors: list[Diagnostic] = []
|
|
71
70
|
dashboards: list[DashboardSummary] = []
|
|
72
71
|
count: int = 0
|
|
73
|
-
directory:
|
|
72
|
+
directory: ProjectDirectory
|
|
74
73
|
skipped_files: list[SkippedFile] = []
|
|
75
74
|
|
|
75
|
+
@field_serializer("directory")
|
|
76
|
+
def _serialize_directory(self, d: ProjectDirectory) -> str:
|
|
77
|
+
return d.relpath
|
|
78
|
+
|
|
76
79
|
|
|
77
80
|
class CompiledDashboard(BaseModel):
|
|
78
81
|
model_config = ConfigDict(frozen=True)
|
|
79
82
|
|
|
80
83
|
success: bool
|
|
81
84
|
dashboard: Face | None = None
|
|
82
|
-
errors: list[
|
|
83
|
-
warnings: list[
|
|
85
|
+
errors: list[Diagnostic] = []
|
|
86
|
+
warnings: list[Diagnostic] = []
|
|
84
87
|
raw_yaml: str | None = None
|
|
85
88
|
|
|
86
89
|
|
|
@@ -182,7 +185,7 @@ def list_dashboards(
|
|
|
182
185
|
success=True,
|
|
183
186
|
dashboards=dashboards,
|
|
184
187
|
count=len(dashboards),
|
|
185
|
-
directory=project.
|
|
188
|
+
directory=project.directory(under),
|
|
186
189
|
skipped_files=skipped,
|
|
187
190
|
)
|
|
188
191
|
|
|
@@ -194,63 +197,16 @@ def get_dashboard(
|
|
|
194
197
|
include_raw: bool = False,
|
|
195
198
|
) -> CompiledDashboard:
|
|
196
199
|
"""Get the compiled structure of a dashboard."""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return CompiledDashboard(
|
|
201
|
-
|
|
202
|
-
errors=[
|
|
203
|
-
DatafaceError.from_code(
|
|
204
|
-
DF_UNKNOWN_INTERNAL, message=str(exc)
|
|
205
|
-
).to_structured()
|
|
206
|
-
],
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if not file_path.exists():
|
|
210
|
-
hint = no_project_hint(project)
|
|
211
|
-
return CompiledDashboard(
|
|
212
|
-
success=False,
|
|
213
|
-
errors=[
|
|
214
|
-
DatafaceError.from_code(
|
|
215
|
-
DF_COMPILE_FILE_NOT_FOUND,
|
|
216
|
-
path=str(file_path),
|
|
217
|
-
hint=hint,
|
|
218
|
-
).to_structured(file=str(file_path))
|
|
219
|
-
],
|
|
220
|
-
)
|
|
221
|
-
if not file_path.is_file():
|
|
222
|
-
return CompiledDashboard(
|
|
223
|
-
success=False,
|
|
224
|
-
errors=[
|
|
225
|
-
DatafaceError.from_code(
|
|
226
|
-
DF_UNKNOWN_INTERNAL,
|
|
227
|
-
message=f"Not a file: {path}",
|
|
228
|
-
).to_structured(file=str(file_path))
|
|
229
|
-
],
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
face_pf = project.path_for_fspath(file_path)
|
|
233
|
-
result = compile_file(face_pf, project=project)
|
|
234
|
-
|
|
235
|
-
raw_yaml: str | None = None
|
|
236
|
-
if include_raw:
|
|
237
|
-
try:
|
|
238
|
-
raw_yaml = face_pf.read_text()
|
|
239
|
-
except OSError as e:
|
|
240
|
-
return CompiledDashboard(
|
|
241
|
-
success=False,
|
|
242
|
-
errors=[
|
|
243
|
-
DatafaceError.from_code(
|
|
244
|
-
DF_UNKNOWN_INTERNAL,
|
|
245
|
-
message=f"Failed to read file: {e}",
|
|
246
|
-
).to_structured(file=str(file_path))
|
|
247
|
-
],
|
|
248
|
-
)
|
|
200
|
+
# One faces-first resolve/exists/is-file seam, shared with the render path.
|
|
201
|
+
face = resolve_face_or_error(path, project)
|
|
202
|
+
if isinstance(face, Diagnostic):
|
|
203
|
+
return CompiledDashboard(success=False, errors=[face])
|
|
204
|
+
result = compile_file(face)
|
|
249
205
|
|
|
250
206
|
return CompiledDashboard(
|
|
251
207
|
success=result.success,
|
|
252
208
|
dashboard=result.face if result.success else None,
|
|
253
209
|
errors=list(result.errors),
|
|
254
210
|
warnings=result.warnings,
|
|
255
|
-
raw_yaml=
|
|
211
|
+
raw_yaml=face.content if include_raw else None,
|
|
256
212
|
)
|