dataface 0.1.6.dev321__py3-none-any.whl → 0.1.6.dev360__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/agent_api/describe_query.py +3 -1
- dataface/agent_api/docs/yaml-reference.md +4 -2
- dataface/agent_api/inspect.py +2 -1
- dataface/agent_api/pack.py +7 -4
- dataface/agent_api/query.py +2 -2
- dataface/agent_api/render_face.py +5 -7
- dataface/ai/prompts/sql-guidance.md +2 -2
- dataface/cli/commands/_agent_input.py +1 -1
- dataface/cli/commands/docs.py +5 -5
- dataface/core/compile/compiler.py +47 -35
- dataface/core/compile/errors.py +2 -2
- dataface/core/compile/filter_injection.py +1 -1
- dataface/core/compile/jinja.py +3 -3
- dataface/core/compile/markdown.py +7 -6
- dataface/core/compile/models/chart/authored.py +1 -1
- dataface/core/compile/models/face/authored.py +13 -0
- dataface/core/compile/models/face/normalized.py +9 -0
- dataface/core/compile/models/source.py +7 -0
- dataface/core/compile/models/style/resolved.py +1 -1
- dataface/core/compile/models/style/theme.py +2 -2
- dataface/core/compile/models/vega_lite/config.py +4 -2
- dataface/core/compile/normalize_charts.py +10 -6
- dataface/core/compile/normalize_layout.py +58 -46
- dataface/core/compile/normalize_queries.py +28 -19
- dataface/core/compile/normalize_variables.py +10 -10
- dataface/core/compile/normalizer.py +16 -11
- dataface/core/compile/parameterized.py +2 -2
- dataface/core/compile/sizing.py +3 -3
- dataface/core/compile/yaml_error_formatter.py +1 -1
- dataface/core/dashboard.py +1 -2
- dataface/core/execute/adapters/base.py +1 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +1 -1
- dataface/core/execute/dialects/base.py +3 -2
- dataface/core/execute/duckdb_cache.py +1 -1
- dataface/core/execute/executor.py +5 -3
- dataface/core/execute/parallel.py +2 -2
- dataface/core/inspect/manifest_utils.py +3 -3
- dataface/core/project.py +93 -0
- dataface/core/registered_views/expander.py +0 -38
- dataface/core/registered_views/link_keys.py +42 -0
- dataface/core/registered_views/render_pipeline.py +3 -1
- dataface/core/registered_views/templates/data/table-index.yaml +4 -11
- dataface/core/render/board_links.py +35 -13
- dataface/core/render/chart/auto_link.py +187 -0
- dataface/core/render/chart/decisions.py +2 -2
- dataface/core/render/chart/pipeline.py +2 -2
- dataface/core/render/chart/render_single.py +32 -0
- dataface/core/render/chart/table.py +2 -2
- dataface/core/render/chart_interactivity.py +2 -2
- dataface/core/render/face_api.py +6 -4
- dataface/core/render/renderer.py +5 -0
- dataface/core/render/terminal_charts.py +1 -1
- dataface/core/render/text_format.py +1 -1
- dataface/core/utils.py +1 -1
- dataface/core/validate.py +9 -6
- dataface/integrations/markdown.py +5 -2
- {dataface-0.1.6.dev321.dist-info → dataface-0.1.6.dev360.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev321.dist-info → dataface-0.1.6.dev360.dist-info}/RECORD +61 -59
- {dataface-0.1.6.dev321.dist-info → dataface-0.1.6.dev360.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev321.dist-info → dataface-0.1.6.dev360.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev321.dist-info → dataface-0.1.6.dev360.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
5
7
|
from pydantic import BaseModel, Field
|
|
6
8
|
|
|
7
9
|
from dataface.core.execute.adapters import AdapterRegistry
|
|
@@ -38,7 +40,7 @@ class DescribeQueryColumn(BaseModel):
|
|
|
38
40
|
class DescribeQueryResult(BaseModel):
|
|
39
41
|
success: bool
|
|
40
42
|
columns: list[DescribeQueryColumn] | None = None
|
|
41
|
-
diagnostics: list[dict] = Field(
|
|
43
|
+
diagnostics: list[dict[str, Any]] = Field(
|
|
42
44
|
default_factory=list,
|
|
43
45
|
description="Dialect-specific diagnostic messages from the query runner.",
|
|
44
46
|
)
|
|
@@ -31,6 +31,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
31
31
|
| `height` | str \| int | ✓ | Height when nested (e.g., '300px' or an integer in pixels). |
|
|
32
32
|
| `visible` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Controls whether this layout item is rendered. Accepts a bool, variable name, Jinja expression, or {query, column} probe. |
|
|
33
33
|
| `theme` | str | ✓ | Theme name (e.g., 'editorial', 'cream', 'stark'). |
|
|
34
|
+
| `auto_link` | bool | ✓ | When True, table charts with no explicit link: automatically link each row to its canonical /data/<source>/<schema>/<table>/detail/ page. Default off. Explicit link: always wins; set link: ~ to suppress per chart. |
|
|
34
35
|
|
|
35
36
|
<a id="sourcessection"></a>
|
|
36
37
|
## SourcesSection
|
|
@@ -1390,7 +1391,7 @@ Authored overlay for AxisStyle.
|
|
|
1390
1391
|
| `band_position` | float | ✓ | Band position within the step (0–1); None uses Vega-Lite's default. |
|
|
1391
1392
|
| `offset` | float | ✓ | Pixel offset of the axis from its default position; None means no offset. |
|
|
1392
1393
|
| `format` | str | ✓ | Tick value format string; None uses auto-format. |
|
|
1393
|
-
| `values` | list | ✓ | Explicit tick values; None uses Vega-Lite's auto tick values. |
|
|
1394
|
+
| `values` | list[Any] | ✓ | Explicit tick values; None uses Vega-Lite's auto tick values. |
|
|
1394
1395
|
| `time_unit` | enum: "auto", "year", "yearquarter", "yearmonth", "yearweek", "yearmonthdate", "monthofyear", "dayofweek", "dayofmonth", "dayofyear", "hourofday", "none" | ✓ | Time-unit bucketing for temporal x-axes; None or 'auto' auto-detects from data. |
|
|
1395
1396
|
| `scale` | [ScaleStyle](#scalestyle) | ✓ | Per-axis scale overrides; None means no override. |
|
|
1396
1397
|
| `type` | enum: "auto", "ordinal", "temporal" | ✓ | Scale type for bucketed-time x-axes; None/'auto' infers from time_unit grain. |
|
|
@@ -1416,7 +1417,7 @@ Authored overlay for ScaleStyle. Scale configuration primitive.
|
|
|
1416
1417
|
| `use_unaggregated_domain` | bool | ✓ | Use unaggregated domain for scale extent; None uses Vega-Lite's default. |
|
|
1417
1418
|
| `x_reverse` | bool | ✓ | Reverse the x-axis scale direction; None means no reversal. |
|
|
1418
1419
|
| `type` | str | ✓ | Scale type override; None lets Vega-Lite infer from field type. |
|
|
1419
|
-
| `domain` | list | ✓ | Explicit scale domain values; None lets Vega-Lite auto-determine from data. |
|
|
1420
|
+
| `domain` | list[Any] | ✓ | Explicit scale domain values; None lets Vega-Lite auto-determine from data. |
|
|
1420
1421
|
| `nice` | bool | ✓ | Round scale domain to nice values; None uses Vega-Lite's default. |
|
|
1421
1422
|
| `padding` | float | ✓ | Unified scale padding shortcut; dispatches to band, point, or continuous padding per scale type. |
|
|
1422
1423
|
|
|
@@ -2608,6 +2609,7 @@ Postgres source configuration.
|
|
|
2608
2609
|
| `password` | str | | Database password. |
|
|
2609
2610
|
| `port` | int | ✓ | Database port number. |
|
|
2610
2611
|
| `schema` | str | ✓ | Default schema for queries. |
|
|
2612
|
+
| `sslmode` | enum: "disable", "allow", "prefer", "require", "verify-ca", "verify-full" | ✓ | libpq SSL mode forwarded to psycopg2. None lets libpq decide its default. |
|
|
2611
2613
|
|
|
2612
2614
|
<a id="snowflakesourceconfig"></a>
|
|
2613
2615
|
## SnowflakeSourceConfig
|
dataface/agent_api/inspect.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
from hashlib import sha256
|
|
13
13
|
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
14
15
|
|
|
15
16
|
from pydantic import BaseModel
|
|
16
17
|
|
|
@@ -109,7 +110,7 @@ def validate_ejected_templates(target_dir: Path) -> ValidateTemplatesResult:
|
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
manifest = load_manifest(target_dir, dataface_version=__version__)
|
|
112
|
-
manifest_templates: dict[str, dict] = manifest.get("templates", {})
|
|
113
|
+
manifest_templates: dict[str, dict[str, Any]] = manifest.get("templates", {})
|
|
113
114
|
templates_pkg = _pkg_files("dataface.core.inspect.templates")
|
|
114
115
|
|
|
115
116
|
missing, upstream_changed, custom_safe, unchanged = compare_templates(
|
dataface/agent_api/pack.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
17
18
|
|
|
18
19
|
import yaml
|
|
19
20
|
from pydantic import BaseModel
|
|
@@ -254,7 +255,7 @@ def propose_pack(
|
|
|
254
255
|
return proposal, out_path
|
|
255
256
|
|
|
256
257
|
|
|
257
|
-
def _build_face_dict(dashboard: ProposedDashboard) -> dict:
|
|
258
|
+
def _build_face_dict(dashboard: ProposedDashboard) -> dict[str, Any]:
|
|
258
259
|
"""Build a minimal valid face dict from a ProposedDashboard.
|
|
259
260
|
|
|
260
261
|
Sparse + valid + no TODO placeholders. Emits title + description + text only.
|
|
@@ -266,7 +267,7 @@ def _build_face_dict(dashboard: ProposedDashboard) -> dict:
|
|
|
266
267
|
is included so the data URL redirects to this authored face. The alias
|
|
267
268
|
lives on the face file — no central mapping needed.
|
|
268
269
|
"""
|
|
269
|
-
face: dict = {
|
|
270
|
+
face: dict[str, Any] = {
|
|
270
271
|
"title": dashboard.title,
|
|
271
272
|
"description": dashboard.purpose,
|
|
272
273
|
"text": (
|
|
@@ -278,7 +279,9 @@ def _build_face_dict(dashboard: ProposedDashboard) -> dict:
|
|
|
278
279
|
return face
|
|
279
280
|
|
|
280
281
|
|
|
281
|
-
def _build_index_dict(
|
|
282
|
+
def _build_index_dict(
|
|
283
|
+
folder_path: str, dashboards: list[ProposedDashboard]
|
|
284
|
+
) -> dict[str, Any]:
|
|
282
285
|
"""Build the index.yml landing face for a folder.
|
|
283
286
|
|
|
284
287
|
Generates a simple text-only landing that lists the dashboards in the folder.
|
|
@@ -302,7 +305,7 @@ def _build_index_dict(folder_path: str, dashboards: list[ProposedDashboard]) ->
|
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
|
|
305
|
-
def _write_face(path: Path, face_dict: dict) -> None:
|
|
308
|
+
def _write_face(path: Path, face_dict: dict[str, Any]) -> None:
|
|
306
309
|
"""Write a face dict to a YAML file in block style."""
|
|
307
310
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
308
311
|
path.write_text(
|
dataface/agent_api/query.py
CHANGED
|
@@ -102,7 +102,7 @@ class ExecuteQueryResult(BaseModel):
|
|
|
102
102
|
errors: list[str]
|
|
103
103
|
row_count: int
|
|
104
104
|
truncated: bool
|
|
105
|
-
diagnostics: list[dict] = Field(
|
|
105
|
+
diagnostics: list[dict[str, Any]] = Field(
|
|
106
106
|
default_factory=list,
|
|
107
107
|
description=(
|
|
108
108
|
"Deterministic validate_query findings (fanout_risk, "
|
|
@@ -114,7 +114,7 @@ class ExecuteQueryResult(BaseModel):
|
|
|
114
114
|
|
|
115
115
|
def _query_diagnostics(
|
|
116
116
|
sql: str, source: str | None, adapter_registry: AdapterRegistry
|
|
117
|
-
) -> list[dict]:
|
|
117
|
+
) -> list[dict[str, Any]]:
|
|
118
118
|
"""Run the structural validator over *sql*; never raise on a real query.
|
|
119
119
|
|
|
120
120
|
Resolves the source dialect when possible so dialect-specific parsing is
|
|
@@ -24,7 +24,6 @@ def render_face(
|
|
|
24
24
|
warnings_ignore: frozenset[str] | None = None,
|
|
25
25
|
format: str = "svg",
|
|
26
26
|
project: Project | None = None,
|
|
27
|
-
base_dir: str | Path | None = None,
|
|
28
27
|
variables: dict[str, str] | None = None,
|
|
29
28
|
use_cache: bool = True,
|
|
30
29
|
cache: DuckDBCache | None = None,
|
|
@@ -40,8 +39,6 @@ def render_face(
|
|
|
40
39
|
format: Output format — "svg", "html", "png", "pdf", etc.
|
|
41
40
|
project: Explicit project. When omitted, auto-discovered upward from
|
|
42
41
|
the face file's directory via ``ProjectSession.from_face``.
|
|
43
|
-
base_dir: Base directory for resolving relative paths in the face.
|
|
44
|
-
Defaults to the face file's parent directory.
|
|
45
42
|
variables: Runtime variable overrides.
|
|
46
43
|
use_cache: Whether to use the in-memory Executor result cache.
|
|
47
44
|
cache: Optional DuckDB query-result cache. When provided, the caller
|
|
@@ -63,8 +60,6 @@ def render_face(
|
|
|
63
60
|
else:
|
|
64
61
|
yaml_content = raw
|
|
65
62
|
|
|
66
|
-
resolved_base_dir = Path(base_dir).resolve() if base_dir else face_file.parent
|
|
67
|
-
|
|
68
63
|
if project is not None:
|
|
69
64
|
if warnings_ignore is None:
|
|
70
65
|
warnings_ignore = project.warnings_ignore
|
|
@@ -74,13 +69,16 @@ def render_face(
|
|
|
74
69
|
warnings_ignore = frozenset()
|
|
75
70
|
project_session = ProjectSession.from_face(face_file, cache=cache)
|
|
76
71
|
try:
|
|
72
|
+
p = project_session.project
|
|
73
|
+
base_file = p.file_for_path(face_file)
|
|
77
74
|
return compile_and_render(
|
|
78
75
|
yaml_content,
|
|
79
|
-
|
|
76
|
+
p,
|
|
80
77
|
adapter_registry=project_session.adapter_registry,
|
|
81
78
|
cache=cache,
|
|
82
79
|
warnings_ignore=warnings_ignore,
|
|
83
|
-
|
|
80
|
+
base_file=base_file,
|
|
81
|
+
face_dir=face_file.parent,
|
|
84
82
|
format=format,
|
|
85
83
|
variables=variables,
|
|
86
84
|
use_cache=use_cache,
|
|
@@ -4,11 +4,11 @@ Before writing SQL, write a short query plan:
|
|
|
4
4
|
|
|
5
5
|
- **OUTPUT**: the columns to return, with clear aliases that describe the value
|
|
6
6
|
- **FROM/JOIN**: the tables and the join keys between them
|
|
7
|
-
- **GRAIN**: what one row represents after the joins; guard against fan-out / double-counting on the non-unique side of a join — if a join multiplies rows, aggregate before joining or add a GROUP BY
|
|
7
|
+
- **GRAIN**: what one row represents after the joins; guard against fan-out / double-counting on the non-unique side of a join — if a join multiplies rows, aggregate before joining or add a GROUP BY. If the schema context already states a join's multiplicity (e.g. one-to-many) or a column's uniqueness, use that to decide grain — don't spend a query probing for what the schema already tells you
|
|
8
8
|
- **FILTERS**: each WHERE condition; verify the exact stored value before filtering (stored values may differ from display labels — e.g. `'LA'` not `'Los Angeles'`)
|
|
9
9
|
- **SORT/LIMIT**: include if the question asks to rank, sort, or return a top-N
|
|
10
10
|
|
|
11
|
-
Then translate the plan faithfully into SQL. Verify by executing the query and checking that shape and values match the plan.
|
|
11
|
+
Then translate the plan faithfully into SQL. Verify by executing the query and checking that shape and values match the plan. When you run a query, heed any diagnostics it returns — a `fanout_risk`, `missing_join_predicate`, or `reaggregation` warning means the join or aggregate is probably double-counting even though the query ran; fix the grain before trusting the numbers.
|
|
12
12
|
|
|
13
13
|
Additional rules:
|
|
14
14
|
|
|
@@ -147,7 +147,7 @@ class PromptToolkitInput:
|
|
|
147
147
|
def _newline_meta(event: Any) -> None: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
148
148
|
event.current_buffer.insert_text("\n")
|
|
149
149
|
|
|
150
|
-
self._session: PromptSession = PromptSession(
|
|
150
|
+
self._session: PromptSession[str] = PromptSession(
|
|
151
151
|
history=FileHistory(str(hist_path)),
|
|
152
152
|
# DftCompleter is duck-typed against the Completer protocol; we
|
|
153
153
|
# don't inherit because prompt_toolkit imports are deferred to
|
dataface/cli/commands/docs.py
CHANGED
|
@@ -6,7 +6,7 @@ from rich.padding import Padding
|
|
|
6
6
|
from rich.table import Table
|
|
7
7
|
|
|
8
8
|
from dataface._docs_site import docs_site_url
|
|
9
|
-
from dataface.agent_api.docs import docs as _docs
|
|
9
|
+
from dataface.agent_api.docs import TopicEntry, docs as _docs
|
|
10
10
|
from dataface.agent_api.docs.warnings import get_warning_code, list_warning_codes
|
|
11
11
|
from dataface.cli._console import dft_console, is_plain_output
|
|
12
12
|
from dataface.cli._json_output import print_json_result
|
|
@@ -119,7 +119,7 @@ def docs_command(
|
|
|
119
119
|
_emit_markdown(result.topic.content)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
def _emit_topic_index(topics: list) -> None:
|
|
122
|
+
def _emit_topic_index(topics: list[TopicEntry]) -> None:
|
|
123
123
|
if not topics:
|
|
124
124
|
typer.echo("No topics found.")
|
|
125
125
|
return
|
|
@@ -131,7 +131,7 @@ def _emit_topic_index(topics: list) -> None:
|
|
|
131
131
|
_emit_topic_index_plain(topics, web_docs)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def _emit_topic_index_rich(topics: list, web_docs: str) -> None:
|
|
134
|
+
def _emit_topic_index_rich(topics: list[TopicEntry], web_docs: str) -> None:
|
|
135
135
|
typer.echo(
|
|
136
136
|
"Dataface (`dft`) is a dbt-native dashboard layer. You author dashboards as "
|
|
137
137
|
'YAML "faces" — queries, charts, variables, and layout — and `dft` compiles '
|
|
@@ -158,7 +158,7 @@ def _emit_topic_index_rich(topics: list, web_docs: str) -> None:
|
|
|
158
158
|
typer.echo(" dft skills Agent workflows and layout patterns")
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def _emit_topic_index_plain(topics: list, web_docs: str) -> None:
|
|
161
|
+
def _emit_topic_index_plain(topics: list[TopicEntry], web_docs: str) -> None:
|
|
162
162
|
typer.echo(
|
|
163
163
|
"Dataface (`dft`) is a dbt-native dashboard layer. You author dashboards as "
|
|
164
164
|
'YAML "faces" — queries, charts, variables, and layout — and `dft` compiles '
|
|
@@ -185,7 +185,7 @@ def _emit_topic_index_plain(topics: list, web_docs: str) -> None:
|
|
|
185
185
|
typer.echo(" dft skills Agent workflows and layout patterns")
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
def _topic_table(topics: list) -> Table:
|
|
188
|
+
def _topic_table(topics: list[TopicEntry]) -> Table:
|
|
189
189
|
name_width = max(len(entry.id) for entry in topics)
|
|
190
190
|
table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
|
|
191
191
|
table.add_column("Topic", style="bold", no_wrap=True, width=name_width)
|
|
@@ -68,7 +68,7 @@ from dataface.core.compile.yaml_error_formatter import (
|
|
|
68
68
|
format_validation_errors_structured,
|
|
69
69
|
)
|
|
70
70
|
from dataface.core.errors.structured import StructuredError
|
|
71
|
-
from dataface.core.project import Project
|
|
71
|
+
from dataface.core.project import Project, ProjectFile
|
|
72
72
|
from dataface.core.render.warnings import unreferenced_chart
|
|
73
73
|
from dataface.core.render.warnings.base import RenderWarning
|
|
74
74
|
from dataface.core.render.warnings.from_query_diagnostic import from_query_diagnostic
|
|
@@ -140,7 +140,7 @@ class CompileResult:
|
|
|
140
140
|
|
|
141
141
|
def compile_authored_face(
|
|
142
142
|
authored: AuthoredFace,
|
|
143
|
-
|
|
143
|
+
base_file: ProjectFile | None = None,
|
|
144
144
|
project_sources: ProjectSourcesConfig | None = None,
|
|
145
145
|
_yaml_content: str = "",
|
|
146
146
|
) -> CompileResult:
|
|
@@ -152,7 +152,7 @@ def compile_authored_face(
|
|
|
152
152
|
|
|
153
153
|
Args:
|
|
154
154
|
authored: Parsed face from the registered-view expander.
|
|
155
|
-
|
|
155
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
156
156
|
project_sources: Project-level sources config.
|
|
157
157
|
_yaml_content: Original YAML text, used only to produce richer
|
|
158
158
|
``format_validation_errors_structured`` output on PydanticValidationError.
|
|
@@ -188,7 +188,7 @@ def compile_authored_face(
|
|
|
188
188
|
# ════════════════════════════════════════════════════════════════════
|
|
189
189
|
try:
|
|
190
190
|
query_registry = build_query_registry(
|
|
191
|
-
authored,
|
|
191
|
+
authored, base_file, default_source=default_source
|
|
192
192
|
)
|
|
193
193
|
except CompilationError as e:
|
|
194
194
|
return CompileResult(errors=[e.to_structured()])
|
|
@@ -206,7 +206,7 @@ def compile_authored_face(
|
|
|
206
206
|
# STEP 5b: Build Chart Registry
|
|
207
207
|
# ════════════════════════════════════════════════════════════════════
|
|
208
208
|
try:
|
|
209
|
-
chart_registry = build_chart_registry(authored,
|
|
209
|
+
chart_registry = build_chart_registry(authored, base_file=base_file)
|
|
210
210
|
except CompilationError as e:
|
|
211
211
|
return CompileResult(errors=[e.to_structured()])
|
|
212
212
|
|
|
@@ -218,7 +218,7 @@ def compile_authored_face(
|
|
|
218
218
|
authored,
|
|
219
219
|
query_registry=query_registry,
|
|
220
220
|
chart_registry=chart_registry,
|
|
221
|
-
|
|
221
|
+
base_file=base_file,
|
|
222
222
|
)
|
|
223
223
|
except ReferenceError as e:
|
|
224
224
|
return CompileResult(errors=[e.to_structured()])
|
|
@@ -262,7 +262,7 @@ def compile_authored_face(
|
|
|
262
262
|
def compile(
|
|
263
263
|
yaml_content: str,
|
|
264
264
|
options: dict[str, Any] | None = None,
|
|
265
|
-
|
|
265
|
+
base_file: ProjectFile | None = None,
|
|
266
266
|
project_sources: ProjectSourcesConfig | None = None,
|
|
267
267
|
) -> CompileResult:
|
|
268
268
|
"""Compile YAML content to a Face.
|
|
@@ -278,7 +278,7 @@ def compile(
|
|
|
278
278
|
Args:
|
|
279
279
|
yaml_content: YAML string to compile
|
|
280
280
|
options: Optional compilation options
|
|
281
|
-
|
|
281
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
282
282
|
project_sources: Project-level sources.
|
|
283
283
|
|
|
284
284
|
Returns:
|
|
@@ -319,7 +319,7 @@ def compile(
|
|
|
319
319
|
return _compile_with_text(
|
|
320
320
|
face,
|
|
321
321
|
yaml_content,
|
|
322
|
-
|
|
322
|
+
base_file=base_file,
|
|
323
323
|
project_sources=project_sources,
|
|
324
324
|
meta_lint=options.get("meta_lint"),
|
|
325
325
|
)
|
|
@@ -342,7 +342,7 @@ def _parse_error_to_structured(
|
|
|
342
342
|
def _compile_with_text(
|
|
343
343
|
face: AuthoredFace,
|
|
344
344
|
yaml_content: str,
|
|
345
|
-
|
|
345
|
+
base_file: ProjectFile | None,
|
|
346
346
|
project_sources: ProjectSourcesConfig | None,
|
|
347
347
|
meta_lint: MetaLintConfig | None,
|
|
348
348
|
) -> CompileResult:
|
|
@@ -355,7 +355,7 @@ def _compile_with_text(
|
|
|
355
355
|
authoring_warnings = detect_authoring_warnings(yaml_content)
|
|
356
356
|
result = compile_authored_face(
|
|
357
357
|
face,
|
|
358
|
-
|
|
358
|
+
base_file=base_file,
|
|
359
359
|
project_sources=project_sources,
|
|
360
360
|
_yaml_content=yaml_content,
|
|
361
361
|
)
|
|
@@ -498,7 +498,10 @@ def compile_file(
|
|
|
498
498
|
errors=[CompilationError(f"File not found: {file_path}").to_structured()]
|
|
499
499
|
)
|
|
500
500
|
|
|
501
|
-
|
|
501
|
+
face_file = project.file_for_path(file_path)
|
|
502
|
+
|
|
503
|
+
# Markdown report files: translate to YAML before compiling.
|
|
504
|
+
# is_markdown_face stays path-based (upward directory walk, out of scope).
|
|
502
505
|
from dataface.core.compile.markdown import (
|
|
503
506
|
MARKDOWN_NOT_FACE_MESSAGE,
|
|
504
507
|
MARKDOWN_SUFFIXES,
|
|
@@ -523,7 +526,7 @@ def compile_file(
|
|
|
523
526
|
)
|
|
524
527
|
else:
|
|
525
528
|
try:
|
|
526
|
-
yaml_content =
|
|
529
|
+
yaml_content = face_file.read_text()
|
|
527
530
|
except OSError as e:
|
|
528
531
|
return CompileResult(
|
|
529
532
|
errors=[CompilationError(f"Failed to read file: {e}").to_structured()]
|
|
@@ -556,7 +559,7 @@ def compile_file(
|
|
|
556
559
|
result = _compile_with_text(
|
|
557
560
|
face,
|
|
558
561
|
yaml_content,
|
|
559
|
-
|
|
562
|
+
base_file=face_file,
|
|
560
563
|
project_sources=project.sources,
|
|
561
564
|
meta_lint=meta_lint,
|
|
562
565
|
)
|
|
@@ -654,8 +657,8 @@ def _build_registry(
|
|
|
654
657
|
registry_type: Type of registry to build ("queries" or "charts")
|
|
655
658
|
registry: Existing registry to add to (will be created if None)
|
|
656
659
|
**kwargs: Additional arguments specific to registry type:
|
|
657
|
-
- For "queries":
|
|
658
|
-
- For "charts":
|
|
660
|
+
- For "queries": base_file, default_source
|
|
661
|
+
- For "charts": base_file
|
|
659
662
|
|
|
660
663
|
Returns:
|
|
661
664
|
Complete registry dictionary
|
|
@@ -685,7 +688,7 @@ def _build_registry(
|
|
|
685
688
|
def _process_queries_for_registry(
|
|
686
689
|
face: AuthoredFace,
|
|
687
690
|
registry: dict[str, AnyQuery],
|
|
688
|
-
|
|
691
|
+
base_file: ProjectFile | None = None,
|
|
689
692
|
default_source: str | None = None,
|
|
690
693
|
) -> None:
|
|
691
694
|
"""Process queries from a face and add to registry.
|
|
@@ -693,7 +696,7 @@ def _process_queries_for_registry(
|
|
|
693
696
|
Args:
|
|
694
697
|
face: AuthoredFace to process queries from
|
|
695
698
|
registry: Registry to add queries to
|
|
696
|
-
|
|
699
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
697
700
|
default_source: Default source to apply to queries without explicit source
|
|
698
701
|
|
|
699
702
|
Raises:
|
|
@@ -713,7 +716,7 @@ def _process_queries_for_registry(
|
|
|
713
716
|
|
|
714
717
|
# Handle cross-file references
|
|
715
718
|
if isinstance(query_def, QueryRef):
|
|
716
|
-
registry[name] = load_from_reference(query_def,
|
|
719
|
+
registry[name] = load_from_reference(query_def, base_file=base_file)
|
|
717
720
|
else:
|
|
718
721
|
registry[name] = normalize_query(
|
|
719
722
|
name, query_def, default_source=effective_default_source
|
|
@@ -723,7 +726,7 @@ def _process_queries_for_registry(
|
|
|
723
726
|
def _process_charts_for_registry(
|
|
724
727
|
face: AuthoredFace,
|
|
725
728
|
registry: dict[str, Any],
|
|
726
|
-
|
|
729
|
+
base_file: ProjectFile | None = None,
|
|
727
730
|
**kwargs: Any,
|
|
728
731
|
) -> None:
|
|
729
732
|
"""Process charts from a face and add to registry.
|
|
@@ -731,7 +734,7 @@ def _process_charts_for_registry(
|
|
|
731
734
|
Args:
|
|
732
735
|
face: AuthoredFace to process charts from
|
|
733
736
|
registry: Registry to add charts to
|
|
734
|
-
|
|
737
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
735
738
|
**kwargs: Additional arguments (e.g., query_registry for future use)
|
|
736
739
|
|
|
737
740
|
Raises:
|
|
@@ -747,14 +750,14 @@ def _process_charts_for_registry(
|
|
|
747
750
|
|
|
748
751
|
# Handle cross-file references
|
|
749
752
|
if isinstance(chart_def, ChartRef):
|
|
750
|
-
registry[name] = load_from_reference(chart_def,
|
|
753
|
+
registry[name] = load_from_reference(chart_def, base_file=base_file)
|
|
751
754
|
else:
|
|
752
755
|
registry[name] = chart_def
|
|
753
756
|
|
|
754
757
|
|
|
755
758
|
def build_query_registry(
|
|
756
759
|
face: AuthoredFace,
|
|
757
|
-
|
|
760
|
+
base_file: ProjectFile | None = None,
|
|
758
761
|
registry: dict[str, AnyQuery] | None = None,
|
|
759
762
|
default_source: str | None = None,
|
|
760
763
|
) -> dict[str, AnyQuery]:
|
|
@@ -765,7 +768,7 @@ def build_query_registry(
|
|
|
765
768
|
|
|
766
769
|
Args:
|
|
767
770
|
face: AuthoredFace to process
|
|
768
|
-
|
|
771
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
769
772
|
registry: Existing registry to add to
|
|
770
773
|
default_source: Default source to apply to queries without explicit source
|
|
771
774
|
|
|
@@ -779,14 +782,14 @@ def build_query_registry(
|
|
|
779
782
|
face,
|
|
780
783
|
"queries",
|
|
781
784
|
registry=registry,
|
|
782
|
-
|
|
785
|
+
base_file=base_file,
|
|
783
786
|
default_source=default_source,
|
|
784
787
|
)
|
|
785
788
|
|
|
786
789
|
|
|
787
790
|
def build_chart_registry(
|
|
788
791
|
face: AuthoredFace,
|
|
789
|
-
|
|
792
|
+
base_file: ProjectFile | None = None,
|
|
790
793
|
registry: dict[str, Any] | None = None,
|
|
791
794
|
) -> dict[str, Any]:
|
|
792
795
|
"""Build complete chart registry from face and nested faces.
|
|
@@ -796,7 +799,7 @@ def build_chart_registry(
|
|
|
796
799
|
|
|
797
800
|
Args:
|
|
798
801
|
face: AuthoredFace to process
|
|
799
|
-
|
|
802
|
+
base_file: ProjectFile handle for the face file (used to resolve sub-file refs).
|
|
800
803
|
registry: Existing registry to add to
|
|
801
804
|
|
|
802
805
|
Returns:
|
|
@@ -805,19 +808,20 @@ def build_chart_registry(
|
|
|
805
808
|
Raises:
|
|
806
809
|
CompilationError: If duplicate chart names found
|
|
807
810
|
"""
|
|
808
|
-
return _build_registry(face, "charts", registry=registry,
|
|
811
|
+
return _build_registry(face, "charts", registry=registry, base_file=base_file)
|
|
809
812
|
|
|
810
813
|
|
|
811
814
|
def load_from_reference(
|
|
812
815
|
reference: VariableRef | QueryRef | ChartRef,
|
|
813
|
-
|
|
816
|
+
base_file: ProjectFile | None = None,
|
|
814
817
|
) -> Any:
|
|
815
818
|
"""Load an item from a typed cross-file reference.
|
|
816
819
|
|
|
817
820
|
Args:
|
|
818
821
|
reference: A typed ref model (VariableRef, QueryRef, or ChartRef).
|
|
819
822
|
The grammar has already been validated by the Pydantic model.
|
|
820
|
-
|
|
823
|
+
base_file: ProjectFile handle for the face file (used to resolve paths).
|
|
824
|
+
If None and a sub-file ref is encountered, raises CompilationError.
|
|
821
825
|
|
|
822
826
|
Returns:
|
|
823
827
|
- QueryRef → AnyQuery
|
|
@@ -841,14 +845,22 @@ def load_from_reference(
|
|
|
841
845
|
if not (file_path_str.endswith(".yml") or file_path_str.endswith(".yaml")):
|
|
842
846
|
file_path_str += ".yml"
|
|
843
847
|
|
|
844
|
-
|
|
848
|
+
if base_file is None:
|
|
849
|
+
raise CompilationError(
|
|
850
|
+
f"Cannot resolve cross-file reference '{reference.ref}': "
|
|
851
|
+
"no base file context (face was compiled without a ProjectFile)"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
try:
|
|
855
|
+
ref_file = base_file.sibling(file_path_str)
|
|
856
|
+
except ValueError as e:
|
|
857
|
+
raise CompilationError(str(e)) from e
|
|
845
858
|
|
|
846
|
-
if not
|
|
859
|
+
if not ref_file.exists():
|
|
847
860
|
raise CompilationError(f"Referenced file not found: {file_path_str}")
|
|
848
861
|
|
|
849
862
|
try:
|
|
850
|
-
|
|
851
|
-
content = yaml.safe_load(f)
|
|
863
|
+
content = ref_file.read_yaml()
|
|
852
864
|
except (OSError, yaml.YAMLError) as e:
|
|
853
865
|
raise CompilationError(f"Failed to load {file_path_str}: {e}") from e
|
|
854
866
|
|
|
@@ -881,7 +893,7 @@ def load_from_reference(
|
|
|
881
893
|
)
|
|
882
894
|
|
|
883
895
|
|
|
884
|
-
def _extract_sources(face: AuthoredFace) -> dict[str, dict]:
|
|
896
|
+
def _extract_sources(face: AuthoredFace) -> dict[str, dict[str, Any]]:
|
|
885
897
|
"""Extract named source configurations from AuthoredFace.
|
|
886
898
|
|
|
887
899
|
Extracts named source definitions (excluding 'default') into a dict.
|
dataface/core/compile/errors.py
CHANGED
|
@@ -213,7 +213,7 @@ class ValidationError(CompilationError):
|
|
|
213
213
|
|
|
214
214
|
field_path: str | None = None
|
|
215
215
|
invalid_value: str | None = None
|
|
216
|
-
valid_values: list | None = None
|
|
216
|
+
valid_values: list[str] | None = None
|
|
217
217
|
|
|
218
218
|
def __init__(
|
|
219
219
|
self,
|
|
@@ -224,7 +224,7 @@ class ValidationError(CompilationError):
|
|
|
224
224
|
suggestion: str | None = None,
|
|
225
225
|
field_path: str | None = None,
|
|
226
226
|
invalid_value: str | None = None,
|
|
227
|
-
valid_values: list | None = None,
|
|
227
|
+
valid_values: list[str] | None = None,
|
|
228
228
|
):
|
|
229
229
|
self.field_path = field_path
|
|
230
230
|
self.invalid_value = invalid_value
|
|
@@ -178,7 +178,7 @@ def inject_filters(
|
|
|
178
178
|
return tree.sql(dialect=sg_dialect), all_params
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
def _is_operator_dict(d: dict) -> bool:
|
|
181
|
+
def _is_operator_dict(d: dict[str, Any]) -> bool:
|
|
182
182
|
"""Check if a dict's keys are all recognized filter operators."""
|
|
183
183
|
operators = set(_OP_MAP) | {"in", "not_in", "between"}
|
|
184
184
|
return all(k in operators for k in d)
|
dataface/core/compile/jinja.py
CHANGED
|
@@ -267,8 +267,8 @@ def _detect_circular(dependencies: dict[str, list[str]]) -> None:
|
|
|
267
267
|
Raises:
|
|
268
268
|
JinjaError: If circular dependency found
|
|
269
269
|
"""
|
|
270
|
-
visited: set = set()
|
|
271
|
-
rec_stack: set = set()
|
|
270
|
+
visited: set[str] = set()
|
|
271
|
+
rec_stack: set[str] = set()
|
|
272
272
|
|
|
273
273
|
def dfs(node: str, path: list[str]) -> None:
|
|
274
274
|
if node in rec_stack:
|
|
@@ -340,7 +340,7 @@ def _substitute_query_refs(sql: str, resolved: dict[str, str]) -> str:
|
|
|
340
340
|
"""
|
|
341
341
|
_QUERY_REF = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
|
|
342
342
|
|
|
343
|
-
def _replacer(m: re.Match) -> str:
|
|
343
|
+
def _replacer(m: re.Match[str]) -> str:
|
|
344
344
|
name = m.group(1)
|
|
345
345
|
if name in resolved:
|
|
346
346
|
return resolved[name]
|
|
@@ -19,6 +19,7 @@ validator.
|
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
22
23
|
|
|
23
24
|
import yaml
|
|
24
25
|
|
|
@@ -66,7 +67,7 @@ def parse_chart_embeds(body: str) -> list[tuple[str, str]]:
|
|
|
66
67
|
return blocks
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
def _extract_frontmatter(md_text: str) -> tuple[dict, str]:
|
|
70
|
+
def _extract_frontmatter(md_text: str) -> tuple[dict[str, Any], str]:
|
|
70
71
|
"""Extract optional YAML frontmatter and markdown body from text."""
|
|
71
72
|
lines = md_text.splitlines(keepends=True)
|
|
72
73
|
if not lines or lines[0].strip() != "---":
|
|
@@ -95,7 +96,7 @@ def _extract_frontmatter(md_text: str) -> tuple[dict, str]:
|
|
|
95
96
|
return fm, body
|
|
96
97
|
|
|
97
98
|
|
|
98
|
-
def _metadata_to_markdown_table(metadata: dict) -> str:
|
|
99
|
+
def _metadata_to_markdown_table(metadata: dict[str, Any]) -> str:
|
|
99
100
|
"""Render a flat metadata dict as a two-column markdown table.
|
|
100
101
|
|
|
101
102
|
Produces:
|
|
@@ -116,7 +117,7 @@ def _metadata_to_markdown_table(metadata: dict) -> str:
|
|
|
116
117
|
return "\n".join(lines)
|
|
117
118
|
|
|
118
119
|
|
|
119
|
-
def parse_markdown_face(md_text: str) -> tuple[str, dict]:
|
|
120
|
+
def parse_markdown_face(md_text: str) -> tuple[str, dict[str, Any]]:
|
|
120
121
|
"""Parse a markdown face file and return (yaml_str, metadata_dict).
|
|
121
122
|
|
|
122
123
|
``yaml_str`` is the YAML string ready to feed to compile().
|
|
@@ -131,8 +132,8 @@ def parse_markdown_face(md_text: str) -> tuple[str, dict]:
|
|
|
131
132
|
"""
|
|
132
133
|
fm, body = _extract_frontmatter(md_text)
|
|
133
134
|
|
|
134
|
-
face_config: dict = {}
|
|
135
|
-
metadata: dict = {}
|
|
135
|
+
face_config: dict[str, Any] = {}
|
|
136
|
+
metadata: dict[str, Any] = {}
|
|
136
137
|
|
|
137
138
|
if "face" in fm:
|
|
138
139
|
face_cfg = fm["face"]
|
|
@@ -146,7 +147,7 @@ def parse_markdown_face(md_text: str) -> tuple[str, dict]:
|
|
|
146
147
|
|
|
147
148
|
# Build rows from markdown body
|
|
148
149
|
blocks = parse_chart_embeds(body)
|
|
149
|
-
rows: list = []
|
|
150
|
+
rows: list[dict[str, Any] | str] = []
|
|
150
151
|
for block_type, value in blocks:
|
|
151
152
|
if block_type == "text":
|
|
152
153
|
rows.append({"text": value})
|
|
@@ -252,7 +252,7 @@ CHART_TYPE_DISPLAY: dict[ChartType, dict[str, str]] = {
|
|
|
252
252
|
_INTERNAL_CHART_TYPES: set[ChartType] = {ChartType.DONUT}
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
def get_chart_type_options(include_internal: bool = False) -> list[dict]:
|
|
255
|
+
def get_chart_type_options(include_internal: bool = False) -> list[dict[str, Any]]:
|
|
256
256
|
"""Get chart types for UI dropdowns. Single source of truth.
|
|
257
257
|
|
|
258
258
|
Args:
|