dataface 0.1.5.dev215__py3-none-any.whl → 0.1.5.dev278__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 +38 -20
- dataface/__init__.py +2 -1
- dataface/agent_api/__init__.py +24 -4
- dataface/agent_api/_init_templates/guide.yaml +6 -2
- dataface/agent_api/dashboards.py +1 -8
- dataface/agent_api/data_paths.py +225 -0
- dataface/agent_api/docs/yaml-reference.md +42 -156
- dataface/agent_api/pack.py +45 -3
- dataface/agent_api/project.py +112 -10
- dataface/agent_api/render_face.py +17 -18
- dataface/agent_api/schema_hints.py +59 -0
- dataface/agent_api/search.py +32 -1
- dataface/agent_api/validate.py +46 -0
- dataface/agent_api/validate_query.py +40 -62
- dataface/ai/mcp/server.py +1 -1
- dataface/ai/skills/dashboard-pack-scaffolding/SKILL.md +98 -10
- dataface/ai/skills/kpi-row/SKILL.md +3 -1
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +6 -2
- dataface/ai/skills/single-metric-bignum/SKILL.md +3 -1
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +3 -1
- dataface/ai/tools/__init__.py +6 -5
- dataface/cli/commands/_agent_server.py +10 -4
- dataface/cli/commands/query.py +54 -36
- dataface/cli/commands/schema.py +73 -42
- dataface/cli/commands/search.py +3 -3
- dataface/cli/main.py +13 -0
- dataface/core/__init__.py +2 -1
- dataface/core/compile/__init__.py +6 -2
- dataface/core/compile/compiler.py +111 -85
- dataface/core/compile/config.py +62 -39
- dataface/core/compile/meta.py +3 -3
- dataface/core/compile/models/chart/authored.py +47 -83
- dataface/core/compile/models/chart/resolved.py +1 -1
- dataface/core/compile/models/face/authored.py +9 -0
- dataface/core/compile/models/face/normalized.py +8 -0
- dataface/core/compile/models/query/normalized.py +13 -5
- dataface/core/compile/models/style/authored.py +1 -7
- dataface/core/compile/models/style/resolved.py +13 -24
- dataface/core/compile/models/style/theme.py +13 -72
- dataface/core/compile/models/variable/authored.py +7 -7
- dataface/core/compile/normalize_layout.py +2 -2
- dataface/core/compile/normalize_variables.py +5 -5
- dataface/core/compile/normalizer.py +1 -0
- dataface/core/compile/sizing.py +42 -16
- dataface/core/compile/style_cascade.py +7 -7
- dataface/core/compile/typography.py +18 -5
- dataface/core/compile/validator.py +3 -3
- dataface/core/compile/yaml_error_formatter.py +0 -13
- dataface/core/dashboard.py +3 -3
- dataface/core/defaults/themes/_base.yaml +2 -7
- dataface/core/defaults/themes/editorial.yaml +3 -3
- dataface/core/errors/__init__.py +6 -0
- dataface/core/errors/codes_search.py +36 -0
- dataface/core/execute/adapters/adapter_registry.py +19 -12
- dataface/core/execute/adapters/dbt_adapter.py +5 -6
- dataface/core/execute/adapters/schema_adapter.py +15 -5
- dataface/core/execute/duckdb_cache.py +10 -5
- dataface/core/execute/executor.py +2 -1
- dataface/core/inspect/db_types.py +29 -18
- dataface/core/inspect/renderer.py +8 -3
- dataface/core/inspect/templates/categorical_column.yml +9 -3
- dataface/core/inspect/templates/date_column.yml +9 -3
- dataface/core/inspect/templates/numeric_column.yml +18 -6
- dataface/core/inspect/templates/string_column.yml +18 -6
- dataface/core/pack/models.py +14 -0
- dataface/core/pack/planner.py +43 -13
- dataface/core/registered_views/__init__.py +0 -0
- dataface/core/registered_views/data_urls.py +31 -0
- dataface/core/registered_views/expander.py +392 -0
- dataface/core/registered_views/loader.py +104 -0
- dataface/core/registered_views/models.py +51 -0
- dataface/core/registered_views/query_runner.py +247 -0
- dataface/core/registered_views/registry.yaml +72 -0
- dataface/core/registered_views/render_pipeline.py +221 -0
- dataface/core/registered_views/router.py +126 -0
- dataface/core/registered_views/templates/data/root.yaml +19 -0
- dataface/core/registered_views/templates/data/schema-index.yaml +21 -0
- dataface/core/registered_views/templates/data/source-index.yaml +20 -0
- dataface/core/registered_views/templates/data/table-detail.yaml +30 -0
- dataface/core/registered_views/templates/data/table-index.yaml +41 -0
- dataface/core/registered_views/templates/inspector/column.yaml +24 -0
- dataface/core/registered_views/templates/inspector/schema.yaml +20 -0
- dataface/core/registered_views/templates/inspector/source.yaml +19 -0
- dataface/core/registered_views/templates/inspector/table.yaml +23 -0
- dataface/core/registered_views/variable_planner.py +243 -0
- dataface/core/render/chart/decisions.py +11 -1
- dataface/core/render/chart/kpi.py +2 -1
- dataface/core/render/chart/pipeline.py +11 -5
- dataface/core/render/chart/profile.py +18 -18
- dataface/core/render/chart/standard_renderer.py +5 -5
- dataface/core/render/chart/vl_field_maps.py +4 -4
- dataface/core/render/face_api.py +2 -2
- dataface/core/render/face_to_dict.py +109 -0
- dataface/core/render/faces.py +9 -3
- dataface/core/render/json_format.py +3 -98
- dataface/core/render/layout_sizing.py +4 -1
- dataface/core/render/nav.py +13 -4
- dataface/core/render/svg_utils.py +1 -1
- dataface/core/render/text_format.py +2 -3
- dataface/core/render/variable_controls.py +12 -12
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +29 -2
- dataface/core/render/yaml_format.py +2 -3
- dataface/core/serve/alias_index.py +284 -0
- dataface/core/serve/server.py +155 -21
- dataface/integrations/markdown.py +26 -47
- {dataface-0.1.5.dev215.dist-info → dataface-0.1.5.dev278.dist-info}/METADATA +1 -1
- {dataface-0.1.5.dev215.dist-info → dataface-0.1.5.dev278.dist-info}/RECORD +112 -88
- mdsvg/renderer.py +6 -1
- mdsvg/style.py +3 -0
- {dataface-0.1.5.dev215.dist-info → dataface-0.1.5.dev278.dist-info}/WHEEL +0 -0
- {dataface-0.1.5.dev215.dist-info → dataface-0.1.5.dev278.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.5.dev215.dist-info → dataface-0.1.5.dev278.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -19,7 +19,7 @@ dft validate faces/my_dashboard.yml
|
|
|
19
19
|
To verify that your database connection works, run a simple query:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
dft query
|
|
22
|
+
dft query 'SELECT 1' --source <your_source>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
A successful result means your data source is reachable.
|
|
@@ -138,7 +138,7 @@ variables:
|
|
|
138
138
|
region:
|
|
139
139
|
input: select # See `dft docs variables` for all 14 input types
|
|
140
140
|
options: { static: [US, EU, APAC] }
|
|
141
|
-
|
|
141
|
+
default: US
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
Reference variables inside queries with bare `{{ region }}` — no `variables.` prefix.
|
|
@@ -238,6 +238,27 @@ Top-level fields (23 total):
|
|
|
238
238
|
|
|
239
239
|
`face:` as a top-level key is rejected. Put face properties (title, rows, queries, …) directly at the YAML root.
|
|
240
240
|
|
|
241
|
+
### Aliases
|
|
242
|
+
|
|
243
|
+
`aliases:` is a list of absolute URL paths that 302-redirect to this face's canonical file-path URL. Each entry must start with `/`; trailing slashes are normalized automatically.
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
aliases:
|
|
247
|
+
- /old-reports/
|
|
248
|
+
- /legacy/sales/
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Rules:
|
|
252
|
+
- An alias must not collide with a real face file path — the server raises an error at startup if it does.
|
|
253
|
+
- Each alias must be unique across the project — two faces claiming the same alias is a startup error.
|
|
254
|
+
- Aliasing a generated system route (data or inspector view) is allowed: the redirect takes precedence, letting you override what that URL serves.
|
|
255
|
+
|
|
256
|
+
There are two ways to override a data/inspector route for a given path:
|
|
257
|
+
- **Face file at that path** — create `faces/data/warehouse/schema/table.yml` and the server renders it directly (no redirect).
|
|
258
|
+
- **`aliases:` entry** — any face can declare `/data/…/` as an alias; the server issues a 302 to the declaring face's canonical URL.
|
|
259
|
+
|
|
260
|
+
Both approaches work. A face file wins without a redirect round-trip; an alias lets a face live anywhere and still capture a system-view URL.
|
|
261
|
+
|
|
241
262
|
### Board style
|
|
242
263
|
|
|
243
264
|
`style:` on a face, nested face, or layout section accepts a fixed set of keys — not arbitrary CSS:
|
|
@@ -346,7 +367,7 @@ queries:
|
|
|
346
367
|
- [Bob, 87.1]
|
|
347
368
|
```
|
|
348
369
|
|
|
349
|
-
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `
|
|
370
|
+
Query types (`type:` literals): `sql`, `csv`, `http`, `dbt_model`, `metricflow`, `values`. `schema_resolver` is internal-only and not part of the authored surface.
|
|
350
371
|
|
|
351
372
|
Common fields (all query types):
|
|
352
373
|
|
|
@@ -461,7 +482,6 @@ charts:
|
|
|
461
482
|
|
|
462
483
|
# Style + behavior
|
|
463
484
|
sort: { by: total, order: desc }
|
|
464
|
-
format: integer # named alias or raw D3 spec (e.g. ",.0f")
|
|
465
485
|
x_label: "Month"
|
|
466
486
|
y_label: "Revenue (USD)"
|
|
467
487
|
link: "/orders?month={{ month }}" # Click-through URL template (drill-down)
|
|
@@ -469,6 +489,7 @@ charts:
|
|
|
469
489
|
|
|
470
490
|
style: # Chart-local style patch (typed; not raw CSS) — paint only
|
|
471
491
|
orientation: vertical # style: does NOT accept height or aspect_ratio
|
|
492
|
+
number_format: ",.0f" # D3 format string or named alias for axis/tooltip format
|
|
472
493
|
stack: zero # false | true | zero | normalize | center
|
|
473
494
|
```
|
|
474
495
|
|
|
@@ -597,14 +618,12 @@ support: # Optional support line (same shape: value/label/format/
|
|
|
597
618
|
glyph: "▲"
|
|
598
619
|
tone: positive
|
|
599
620
|
|
|
600
|
-
# table — renders all query columns unless `style.columns` selects a subset
|
|
601
|
-
# `columns` is a MAPPING keyed by column name (NOT a list). Omit it to show
|
|
602
|
-
# every query column; include it to choose a subset and/or style columns.
|
|
621
|
+
# table — renders all query columns unless `style.columns` selects a subset
|
|
603
622
|
type: table
|
|
604
623
|
style:
|
|
605
624
|
columns:
|
|
606
|
-
|
|
607
|
-
|
|
625
|
+
- column: order_id
|
|
626
|
+
- column: amount
|
|
608
627
|
label: Amount
|
|
609
628
|
format: currency_whole
|
|
610
629
|
align: right # left | center | right
|
|
@@ -700,7 +719,7 @@ layers:
|
|
|
700
719
|
y: target
|
|
701
720
|
label: Target
|
|
702
721
|
axis_y:
|
|
703
|
-
|
|
722
|
+
position: right # left | right
|
|
704
723
|
title: "Target"
|
|
705
724
|
```
|
|
706
725
|
|
|
@@ -846,7 +865,7 @@ variables:
|
|
|
846
865
|
description: "Restrict every query to one region."
|
|
847
866
|
options:
|
|
848
867
|
static: [US, EU, APAC]
|
|
849
|
-
|
|
868
|
+
default: US
|
|
850
869
|
```
|
|
851
870
|
|
|
852
871
|
Input types (14 total):
|
|
@@ -878,6 +897,7 @@ Common variable fields:
|
|
|
878
897
|
| `default` | any | Default value when no URL param is set |
|
|
879
898
|
| `placeholder` | string | Placeholder text |
|
|
880
899
|
| `required` | bool | Block rendering until a value exists |
|
|
900
|
+
| `allow_null` | bool | `null` is a valid selection |
|
|
881
901
|
| `visible` | bool | Hidden when `false`; still settable via URL param |
|
|
882
902
|
| `disabled` | bool \| string \| `{query, column}` | Static, Jinja expr, or query-backed disable |
|
|
883
903
|
| `data_type` | string | Upstream type hint (informational; preserved through migrations) |
|
|
@@ -893,7 +913,7 @@ variables:
|
|
|
893
913
|
product:
|
|
894
914
|
input: select
|
|
895
915
|
options:
|
|
896
|
-
static: [Electronics, Clothing]
|
|
916
|
+
static: [All, Electronics, Clothing] # Hardcoded list
|
|
897
917
|
# OR
|
|
898
918
|
query: products_list # Query whose first column is the option list
|
|
899
919
|
column: product_name # Optional: which column in that query
|
|
@@ -906,25 +926,23 @@ variables:
|
|
|
906
926
|
|
|
907
927
|
Top-level option-source binding (alternative to `options:`): `column`, `query`, `dimension` (MetricFlow), `measure` (MetricFlow), `model` (dbt).
|
|
908
928
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
Disabled forms:
|
|
929
|
+
Enabled/disabled forms (`enabled: false` means the control is inactive):
|
|
912
930
|
|
|
913
931
|
```yaml
|
|
914
932
|
variables:
|
|
915
|
-
# Static bool
|
|
916
|
-
closed: { input: checkbox,
|
|
933
|
+
# Static bool — enabled: false disables the control
|
|
934
|
+
closed: { input: checkbox, enabled: false }
|
|
917
935
|
|
|
918
936
|
# Jinja expression against current variable values
|
|
919
|
-
q4_only: { input: select, options: { static: [Q4] },
|
|
937
|
+
q4_only: { input: select, options: { static: [Q4] }, enabled: "{{ year >= 2024 }}" }
|
|
920
938
|
|
|
921
939
|
# Query-backed (must return exactly 1 row with the named boolean column)
|
|
922
940
|
territory:
|
|
923
941
|
input: select
|
|
924
942
|
options: { query: territory_options }
|
|
925
|
-
|
|
943
|
+
enabled:
|
|
926
944
|
query: control_state
|
|
927
|
-
column:
|
|
945
|
+
column: territory_enabled
|
|
928
946
|
```
|
|
929
947
|
|
|
930
948
|
**Multiselect SQL antipattern** — the first instinct for filtering a multiselect variable in SQL is `IN ({{ plans | map('tojson') | join(', ') }})`. This is wrong: `tojson` produces double-quoted strings (`"trial"`), which most SQL dialects (including DuckDB) treat as *column references*, not string literals. The query silently returns an empty result and `dft render` exits 0.
|
dataface/__init__.py
CHANGED
|
@@ -22,7 +22,8 @@ Quick Start:
|
|
|
22
22
|
... face = result.face
|
|
23
23
|
...
|
|
24
24
|
... # Create executor and render
|
|
25
|
-
...
|
|
25
|
+
... from dataface.core.compile.config import load_project_sources
|
|
26
|
+
... registry = build_adapter_registry(Path.cwd(), project_sources=load_project_sources(Path.cwd()))
|
|
26
27
|
... executor = Executor(face, registry, query_registry=result.query_registry)
|
|
27
28
|
... svg = render(face, executor, format="svg")
|
|
28
29
|
"""
|
dataface/agent_api/__init__.py
CHANGED
|
@@ -37,10 +37,6 @@ CLI/MCP files that contain logic beyond parse-and-dispatch.
|
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
39
|
from dataface.agent_api import skills as skills
|
|
40
|
-
from dataface.agent_api.dashboards import (
|
|
41
|
-
RenderedDashboard as RenderedDashboard,
|
|
42
|
-
render_dashboard as render_dashboard,
|
|
43
|
-
)
|
|
44
40
|
from dataface.agent_api.describe import (
|
|
45
41
|
DescribeFaceArgs,
|
|
46
42
|
DescribeFaceResult,
|
|
@@ -53,8 +49,24 @@ from dataface.agent_api.init import (
|
|
|
53
49
|
)
|
|
54
50
|
from dataface.agent_api.project import Project as Project
|
|
55
51
|
from dataface.agent_api.query import QueryFaceResult, query_face
|
|
52
|
+
from dataface.agent_api.schema_hints import (
|
|
53
|
+
SchemaHints as SchemaHints,
|
|
54
|
+
schema_hints as schema_hints,
|
|
55
|
+
)
|
|
56
56
|
from dataface.agent_api.validate import ValidateDashboardArgs, ValidateResult
|
|
57
|
+
from dataface.agent_api.validate_query import (
|
|
58
|
+
QueryDiagnostic as QueryDiagnostic,
|
|
59
|
+
validate_query as validate_query,
|
|
60
|
+
)
|
|
57
61
|
from dataface.core.compile.config import ProjectSourcesConfig as ProjectSourcesConfig
|
|
62
|
+
from dataface.core.dashboard import (
|
|
63
|
+
RenderedDashboard as RenderedDashboard,
|
|
64
|
+
render_dashboard as render_dashboard,
|
|
65
|
+
)
|
|
66
|
+
from dataface.core.errors.structured import StructuredError as StructuredError
|
|
67
|
+
from dataface.core.fonts import get_inter_font_path as get_inter_font_path
|
|
68
|
+
from dataface.core.render.board_links import LinkContext as LinkContext
|
|
69
|
+
from dataface.core.render.errors import RenderError as RenderError
|
|
58
70
|
|
|
59
71
|
__all__ = [
|
|
60
72
|
"DescribeFaceArgs",
|
|
@@ -63,15 +75,23 @@ __all__ = [
|
|
|
63
75
|
"DocsResult",
|
|
64
76
|
"DocsSearchHit",
|
|
65
77
|
"InitResult",
|
|
78
|
+
"LinkContext",
|
|
66
79
|
"Project",
|
|
67
80
|
"ProjectSourcesConfig",
|
|
81
|
+
"QueryDiagnostic",
|
|
68
82
|
"QueryFaceResult",
|
|
69
83
|
"RenderedDashboard",
|
|
84
|
+
"RenderError",
|
|
85
|
+
"SchemaHints",
|
|
86
|
+
"StructuredError",
|
|
70
87
|
"Topic",
|
|
71
88
|
"ValidateDashboardArgs",
|
|
72
89
|
"ValidateResult",
|
|
73
90
|
"describe_face",
|
|
91
|
+
"get_inter_font_path",
|
|
74
92
|
"init_project",
|
|
75
93
|
"query_face",
|
|
76
94
|
"render_dashboard",
|
|
95
|
+
"schema_hints",
|
|
96
|
+
"validate_query",
|
|
77
97
|
]
|
|
@@ -82,7 +82,9 @@ charts:
|
|
|
82
82
|
query: kpi_summary
|
|
83
83
|
label: Total Revenue (H1)
|
|
84
84
|
value: total_revenue
|
|
85
|
-
|
|
85
|
+
style:
|
|
86
|
+
value:
|
|
87
|
+
format: currency_compact
|
|
86
88
|
support:
|
|
87
89
|
value: revenue_delta
|
|
88
90
|
label: vs H1 last year
|
|
@@ -95,7 +97,9 @@ charts:
|
|
|
95
97
|
query: kpi_summary
|
|
96
98
|
label: Avg Deal Size
|
|
97
99
|
value: avg_deal
|
|
98
|
-
|
|
100
|
+
style:
|
|
101
|
+
value:
|
|
102
|
+
format: currency_compact
|
|
99
103
|
|
|
100
104
|
data_table:
|
|
101
105
|
type: table
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
All public functions have concrete typed arguments and typed Pydantic return
|
|
4
4
|
values. No bare dict[str, Any] in any return position. Tool/CLI callers use
|
|
5
5
|
.model_dump() at the wire boundary.
|
|
6
|
-
|
|
7
|
-
`RenderedDashboard` and `render_dashboard` live in `dataface.core.dashboard`
|
|
8
|
-
so the embedded HTTP server can call them without inverting the layer stack; they
|
|
9
|
-
are re-exported here so the agent_api thin-wrapper rule still holds.
|
|
10
6
|
"""
|
|
11
7
|
|
|
12
8
|
from __future__ import annotations
|
|
@@ -20,10 +16,7 @@ from pydantic import BaseModel, Field
|
|
|
20
16
|
from dataface.agent_api._paths import resolve_scoped_path
|
|
21
17
|
from dataface.core.compile import Face, compile_file
|
|
22
18
|
from dataface.core.compile.errors import DatafaceError
|
|
23
|
-
from dataface.core.dashboard import
|
|
24
|
-
RenderedDashboard as RenderedDashboard,
|
|
25
|
-
render_dashboard as render_dashboard,
|
|
26
|
-
)
|
|
19
|
+
from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
|
|
27
20
|
from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
|
|
28
21
|
from dataface.core.render.warnings.base import RenderWarning
|
|
29
22
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Data URL surfacing and alias typo lint.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- DataPathInfo: the agent-facing wire shape for a canonical data URL
|
|
5
|
+
(url, resolves_to: "generic" | "authored", face)
|
|
6
|
+
- data_paths_for_source/schema/table: compute data URLs and check the
|
|
7
|
+
alias index to determine whether a face overrides the generic system view
|
|
8
|
+
- data_paths_list: flat list of DataPathInfo (source + schema + table level)
|
|
9
|
+
from a SchemaResponse — used for --data-paths CLI flag and agent discovery
|
|
10
|
+
- validate_data_aliases: typo lint for /data/ prefixed aliases — checks
|
|
11
|
+
source names only (config only, no DB connection required).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
19
|
+
|
|
20
|
+
from dataface.core.registered_views.data_urls import (
|
|
21
|
+
data_schema_url,
|
|
22
|
+
data_source_url,
|
|
23
|
+
data_table_url,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from dataface.agent_api.schema import SchemaResponse
|
|
28
|
+
from dataface.core.serve.alias_index import AliasIndex
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DataPathInfo:
|
|
33
|
+
"""Canonical data URL with resolution status.
|
|
34
|
+
|
|
35
|
+
Wire shape (pinned by contract test — any change is a breaking API change):
|
|
36
|
+
{"url": "/data/src/schema/table/", "resolves_to": "generic"}
|
|
37
|
+
{"url": "...", "resolves_to": "authored", "face": "/sales/"}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
url: str
|
|
41
|
+
resolves_to: Literal["generic", "authored"]
|
|
42
|
+
face: str | None = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, Any]:
|
|
45
|
+
"""Serialize to the agent wire shape, omitting face when generic."""
|
|
46
|
+
d: dict[str, Any] = {"url": self.url, "resolves_to": self.resolves_to}
|
|
47
|
+
if self.face is not None:
|
|
48
|
+
d["face"] = self.face
|
|
49
|
+
return d
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def data_paths_for_source(
|
|
53
|
+
source: str,
|
|
54
|
+
*,
|
|
55
|
+
alias_index: AliasIndex,
|
|
56
|
+
) -> DataPathInfo:
|
|
57
|
+
"""Return the data URL info for a source.
|
|
58
|
+
|
|
59
|
+
The canonical URL is /data/<source>/. When a face aliases it the
|
|
60
|
+
resolves_to is 'authored' and face is the canonical face URL.
|
|
61
|
+
"""
|
|
62
|
+
url = data_source_url(source)
|
|
63
|
+
face = alias_index.lookup(url)
|
|
64
|
+
if face is not None:
|
|
65
|
+
return DataPathInfo(url=url, resolves_to="authored", face=face)
|
|
66
|
+
return DataPathInfo(url=url, resolves_to="generic")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def data_paths_for_schema(
|
|
70
|
+
source: str,
|
|
71
|
+
schema: str,
|
|
72
|
+
*,
|
|
73
|
+
alias_index: AliasIndex,
|
|
74
|
+
) -> DataPathInfo:
|
|
75
|
+
"""Return the data URL info for a schema.
|
|
76
|
+
|
|
77
|
+
The canonical URL is /data/<source>/<schema>/. When a face aliases it
|
|
78
|
+
the resolves_to is 'authored' and face is the canonical face URL.
|
|
79
|
+
"""
|
|
80
|
+
url = data_schema_url(source, schema)
|
|
81
|
+
face = alias_index.lookup(url)
|
|
82
|
+
if face is not None:
|
|
83
|
+
return DataPathInfo(url=url, resolves_to="authored", face=face)
|
|
84
|
+
return DataPathInfo(url=url, resolves_to="generic")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def data_paths_for_table(
|
|
88
|
+
source: str,
|
|
89
|
+
schema: str,
|
|
90
|
+
table: str,
|
|
91
|
+
*,
|
|
92
|
+
alias_index: AliasIndex,
|
|
93
|
+
) -> DataPathInfo:
|
|
94
|
+
"""Return the data URL info for a table.
|
|
95
|
+
|
|
96
|
+
The canonical URL is /data/<source>/<schema>/<table>/. When a face
|
|
97
|
+
aliases it the resolves_to is 'authored' and face is the canonical face URL.
|
|
98
|
+
"""
|
|
99
|
+
url = data_table_url(source, schema, table)
|
|
100
|
+
face = alias_index.lookup(url)
|
|
101
|
+
if face is not None:
|
|
102
|
+
return DataPathInfo(url=url, resolves_to="authored", face=face)
|
|
103
|
+
return DataPathInfo(url=url, resolves_to="generic")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_data_aliases(
|
|
107
|
+
aliases: list[str],
|
|
108
|
+
*,
|
|
109
|
+
source_names: frozenset[str],
|
|
110
|
+
) -> list[str]:
|
|
111
|
+
"""Lint /data/ prefixed aliases against configured source names.
|
|
112
|
+
|
|
113
|
+
Called at dft-validate time (connection-free). Checks that every
|
|
114
|
+
/data/ URL references a known source, and errors with available
|
|
115
|
+
sources as a hint when not.
|
|
116
|
+
|
|
117
|
+
Returns a list of error message strings (empty = all valid).
|
|
118
|
+
Aliases not prefixed with /data/ are silently skipped.
|
|
119
|
+
"""
|
|
120
|
+
errors: list[str] = []
|
|
121
|
+
for raw_alias in aliases:
|
|
122
|
+
# Normalize: trailing-slash canonical, strip percent-encoding
|
|
123
|
+
from urllib.parse import unquote
|
|
124
|
+
|
|
125
|
+
alias = unquote(raw_alias)
|
|
126
|
+
if not alias.endswith("/"):
|
|
127
|
+
alias = alias + "/"
|
|
128
|
+
|
|
129
|
+
if not alias.startswith("/data/"):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Strip leading "/data/" and trailing "/" to get segments
|
|
133
|
+
inner = alias[len("/data/") :]
|
|
134
|
+
if inner.endswith("/"):
|
|
135
|
+
inner = inner[:-1]
|
|
136
|
+
segments = [s for s in inner.split("/") if s]
|
|
137
|
+
|
|
138
|
+
if not segments:
|
|
139
|
+
# Bare /data/ — no validation needed
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
# Segment 0: source
|
|
143
|
+
source = segments[0]
|
|
144
|
+
if source not in source_names:
|
|
145
|
+
available = sorted(source_names)
|
|
146
|
+
hint = (
|
|
147
|
+
f"available sources: {', '.join(available)}"
|
|
148
|
+
if available
|
|
149
|
+
else "no sources configured"
|
|
150
|
+
)
|
|
151
|
+
errors.append(
|
|
152
|
+
f"Data alias {raw_alias!r} references unknown source {source!r}. "
|
|
153
|
+
f"Did you mean one of: {hint}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return errors
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def data_paths_list(
|
|
160
|
+
schema_response: SchemaResponse,
|
|
161
|
+
*,
|
|
162
|
+
alias_index: AliasIndex,
|
|
163
|
+
) -> list[DataPathInfo]:
|
|
164
|
+
"""Build a flat list of DataPathInfo from a SchemaResponse.
|
|
165
|
+
|
|
166
|
+
Iterates all sources → schemas → tables in the response and returns one
|
|
167
|
+
DataPathInfo per source, per schema, and per table, checking the alias
|
|
168
|
+
index for author overrides at each level.
|
|
169
|
+
"""
|
|
170
|
+
result: list[DataPathInfo] = []
|
|
171
|
+
for source_name, source_data in schema_response.sources.items():
|
|
172
|
+
result.append(data_paths_for_source(source_name, alias_index=alias_index))
|
|
173
|
+
for schema_name, schema_data in (source_data.get("schemas") or {}).items():
|
|
174
|
+
result.append(
|
|
175
|
+
data_paths_for_schema(source_name, schema_name, alias_index=alias_index)
|
|
176
|
+
)
|
|
177
|
+
for table_name in schema_data.get("tables") or {}:
|
|
178
|
+
result.append(
|
|
179
|
+
data_paths_for_table(
|
|
180
|
+
source_name,
|
|
181
|
+
schema_name,
|
|
182
|
+
table_name,
|
|
183
|
+
alias_index=alias_index,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_alias_index_for_project(project_dir: Path) -> AliasIndex:
|
|
190
|
+
"""Build an AliasIndex from a project directory.
|
|
191
|
+
|
|
192
|
+
Uses the same faces_at_root detection logic as the server. Intended for
|
|
193
|
+
CLI commands (dft schema --data-paths) that need alias lookup without
|
|
194
|
+
starting a server.
|
|
195
|
+
"""
|
|
196
|
+
from dataface.core.serve.alias_index import AliasIndex
|
|
197
|
+
|
|
198
|
+
faces_dir = project_dir / "faces"
|
|
199
|
+
faces_at_root = faces_dir.is_dir()
|
|
200
|
+
return AliasIndex.build(
|
|
201
|
+
project_dir=project_dir,
|
|
202
|
+
faces_dir=faces_dir,
|
|
203
|
+
faces_at_root=faces_at_root,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def data_alias_errors_for_file(
|
|
208
|
+
face_file: Path,
|
|
209
|
+
source_names: frozenset[str],
|
|
210
|
+
) -> list[str]:
|
|
211
|
+
"""Read aliases from a face file and lint any /data/ prefixed ones.
|
|
212
|
+
|
|
213
|
+
Source-name check only — no DB connection, no resolver. Called from the
|
|
214
|
+
validate path which must stay connection-free.
|
|
215
|
+
|
|
216
|
+
Returns a list of error message strings; empty means no data alias issues.
|
|
217
|
+
Silently returns [] when the file cannot be parsed (compile catches that).
|
|
218
|
+
"""
|
|
219
|
+
from dataface.core.serve.alias_index import read_aliases_from_file
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
aliases = read_aliases_from_file(face_file)
|
|
223
|
+
except ValueError:
|
|
224
|
+
return [] # compile path reports alias parse errors with better context
|
|
225
|
+
return validate_data_aliases(aliases, source_names=source_names)
|