dataface 0.1.6.dev129__py3-none-any.whl → 0.1.6.dev183__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/docs/yaml-reference.md +2 -2
- dataface/agent_api/project_session.py +2 -0
- dataface/agent_api/query.py +6 -0
- dataface/ai/mcp/server.py +5 -5
- dataface/ai/skills/dashboard-build/SKILL.md +7 -0
- dataface/cli/commands/_agent_input.py +3 -3
- dataface/core/compile/__init__.py +0 -2
- dataface/core/compile/channel.py +1 -6
- dataface/core/compile/colors.py +1 -1
- dataface/core/compile/compiler.py +1 -3
- dataface/core/compile/config.py +3 -3
- dataface/core/compile/dbt_jinja.py +66 -0
- dataface/core/compile/inherit_graph.py +31 -4
- dataface/core/compile/inherit_resolver.py +13 -4
- dataface/core/compile/jinja.py +2 -2
- dataface/core/compile/models/chart/authored.py +8 -8
- dataface/core/compile/models/face/resolved.py +3 -3
- dataface/core/compile/models/markers.py +14 -3
- dataface/core/compile/models/query/normalized.py +4 -0
- dataface/core/compile/models/source.py +4 -86
- dataface/core/compile/models/style/authored.py +2 -2
- dataface/core/compile/models/style/resolved.py +15 -518
- dataface/core/compile/models/style/theme.py +61 -57
- dataface/core/compile/normalize_charts.py +3 -3
- dataface/core/compile/normalize_layout.py +6 -33
- dataface/core/compile/normalize_variables.py +10 -10
- dataface/core/compile/normalizer.py +24 -24
- dataface/core/compile/parameterized.py +1 -6
- dataface/core/compile/sizing.py +30 -8
- dataface/core/compile/sources.py +19 -6
- dataface/core/compile/style_cascade.py +202 -60
- dataface/core/compile/typography.py +92 -89
- dataface/core/defaults/themes/_base.yaml +2 -2
- dataface/core/execute/adapters/dbt_adapter_factory.py +7 -7
- dataface/core/execute/adapters/sql_adapter.py +10 -6
- dataface/core/execute/batch.py +4 -2
- dataface/core/execute/executor.py +3 -6
- dataface/core/inspect/query_validator.py +1 -1
- dataface/core/inspect/renderer.py +0 -1
- dataface/core/render/__init__.py +0 -3
- dataface/core/render/chart/callout.py +5 -9
- dataface/core/render/chart/geo.py +9 -30
- dataface/core/render/chart/kpi.py +18 -11
- dataface/core/render/chart/pipeline.py +15 -98
- dataface/core/render/chart/profile.py +25 -61
- dataface/core/render/chart/render_single.py +116 -67
- dataface/core/render/chart/renderers.py +45 -95
- dataface/core/render/chart/rendering.py +67 -64
- dataface/core/render/chart/spark.py +1 -1
- dataface/core/render/chart/spark_bar.py +34 -20
- dataface/core/render/chart/standard_renderer.py +102 -228
- dataface/core/render/chart/table.py +54 -50
- dataface/core/render/chart/table_support.py +2 -5
- dataface/core/render/chart/vega_lite.py +13 -23
- dataface/core/render/chart/vega_lite_types.py +5 -5
- dataface/core/render/converters/chart.py +5 -6
- dataface/core/render/dir_context.py +37 -35
- dataface/core/render/faces.py +34 -25
- dataface/core/render/font_support.py +8 -8
- dataface/core/render/layout_sizing.py +44 -53
- dataface/core/render/layouts.py +9 -17
- dataface/core/render/renderer.py +21 -18
- dataface/core/render/svg_utils.py +11 -8
- dataface/core/render/terminal_charts.py +2 -2
- dataface/core/render/utils.py +1 -1
- dataface/core/render/variable_controls.py +20 -22
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +4 -3
- dataface/core/render/warnings/base.py +2 -2
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +2 -2
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +2 -2
- dataface/core/render/warnings/redundant_encoding.py +10 -6
- dataface/core/render/warnings/temporal_single_point.py +3 -3
- dataface/core/resolve_face.py +21 -6
- dataface/core/serve/bootstrap.py +2 -2
- dataface/core/serve/server.py +9 -9
- dataface/core/validate.py +2 -2
- dataface/integrations/highlighting.py +4 -3
- dataface/integrations/markdown.py +2 -2
- {dataface-0.1.6.dev129.dist-info → dataface-0.1.6.dev183.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev129.dist-info → dataface-0.1.6.dev183.dist-info}/RECORD +85 -92
- mdsvg/renderer.py +76 -16
- mdsvg/style.py +16 -4
- dataface/core/render/control_registry.py +0 -287
- dataface/core/render/templates/controls/checkbox.html +0 -16
- dataface/core/render/templates/controls/date.html +0 -16
- dataface/core/render/templates/controls/number.html +0 -19
- dataface/core/render/templates/controls/readonly.html +0 -9
- dataface/core/render/templates/controls/select.html +0 -20
- dataface/core/render/templates/controls/slider.html +0 -22
- dataface/core/render/templates/controls/text.html +0 -16
- {dataface-0.1.6.dev129.dist-info → dataface-0.1.6.dev183.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev129.dist-info → dataface-0.1.6.dev183.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev129.dist-info → dataface-0.1.6.dev183.dist-info}/licenses/LICENSE +0 -0
|
@@ -1082,8 +1082,8 @@ Authored overlay for TitleStyle. Board and face titles.
|
|
|
1082
1082
|
|-------|------|:--------:|-------------|
|
|
1083
1083
|
| `font` | [FontStyle](#fontstyle) | ✓ | Title font style overrides. |
|
|
1084
1084
|
| `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose. |
|
|
1085
|
-
| `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``.
|
|
1086
|
-
| `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide).
|
|
1085
|
+
| `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Face/prose titles index this ramp directly (no width input). Object titles (chart/table/spark) combine ``width_offsets`` with a fixed object-title anchor to pick a slot. |
|
|
1086
|
+
| `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Consumed by ``chart_title_spec`` only — object titles add the tier offset to a fixed anchor to pick an H slot from ``sizes``. Face/prose titles are level-only and do not consult these. |
|
|
1087
1087
|
| `min_height` | float | ✓ | Minimum title row height in pixels. |
|
|
1088
1088
|
| `overflow` | str | ✓ | Text overflow mode (clip, truncate, wrap-two, wrap). |
|
|
1089
1089
|
| `position` | [TitlePositionStyle](#titlepositionstyle) | ✓ | Vega-Lite title positioning: anchor, angle, offset, baseline. |
|
|
@@ -360,6 +360,7 @@ class ProjectSession:
|
|
|
360
360
|
variables: dict[str, Any] | None = None,
|
|
361
361
|
source: str | None = None,
|
|
362
362
|
limit: int = 50,
|
|
363
|
+
lenient_variables: bool = False,
|
|
363
364
|
) -> _query.ExecuteQueryResult:
|
|
364
365
|
return _query.execute_query(
|
|
365
366
|
sql,
|
|
@@ -367,6 +368,7 @@ class ProjectSession:
|
|
|
367
368
|
source=source,
|
|
368
369
|
limit=limit,
|
|
369
370
|
adapter_registry=self.adapter_registry,
|
|
371
|
+
lenient_variables=lenient_variables,
|
|
370
372
|
)
|
|
371
373
|
|
|
372
374
|
def describe_query(
|
dataface/agent_api/query.py
CHANGED
|
@@ -88,6 +88,10 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
88
88
|
)
|
|
89
89
|
source: str | None = Field(None, description="Data source name to execute against")
|
|
90
90
|
limit: int | None = Field(None, description="Maximum rows to return (default 50)")
|
|
91
|
+
lenient_variables: bool = Field(
|
|
92
|
+
False,
|
|
93
|
+
description="When True, undefined Jinja variables degrade gracefully (useful for iterative chart editing where not all variables are set).",
|
|
94
|
+
)
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
class ExecuteQueryResult(BaseModel):
|
|
@@ -135,6 +139,7 @@ def execute_query(
|
|
|
135
139
|
limit: int = 50,
|
|
136
140
|
*,
|
|
137
141
|
adapter_registry: AdapterRegistry,
|
|
142
|
+
lenient_variables: bool = False,
|
|
138
143
|
) -> ExecuteQueryResult:
|
|
139
144
|
"""Execute a SQL query and return the results.
|
|
140
145
|
|
|
@@ -155,6 +160,7 @@ def execute_query(
|
|
|
155
160
|
sql=sql,
|
|
156
161
|
source=source,
|
|
157
162
|
limit=fetch_limit,
|
|
163
|
+
lenient_variables=lenient_variables,
|
|
158
164
|
)
|
|
159
165
|
|
|
160
166
|
result = adapter_registry.execute(query, variables=variables)
|
dataface/ai/mcp/server.py
CHANGED
|
@@ -180,14 +180,14 @@ def create_server(context: DatafaceAIContext) -> Any:
|
|
|
180
180
|
return TypeAdapter(AnyUrl).validate_python(value)
|
|
181
181
|
|
|
182
182
|
@server.list_resources() # type: ignore[no-untyped-call]
|
|
183
|
-
async def handle_list_resources() -> list[Resource]:
|
|
183
|
+
async def handle_list_resources() -> list[Resource]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
184
184
|
return [
|
|
185
185
|
Resource(uri=_uri(u), mimeType=m, name=n, description=d)
|
|
186
186
|
for u, m, n, d in (*_BASE_RESOURCES, *_docs_topic_resources())
|
|
187
187
|
]
|
|
188
188
|
|
|
189
189
|
@server.list_resource_templates() # type: ignore[no-untyped-call]
|
|
190
|
-
async def handle_list_resource_templates() -> list[ResourceTemplate]:
|
|
190
|
+
async def handle_list_resource_templates() -> list[ResourceTemplate]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
191
191
|
return [
|
|
192
192
|
ResourceTemplate(
|
|
193
193
|
uriTemplate="dataface://dashboard/{path}",
|
|
@@ -204,11 +204,11 @@ def create_server(context: DatafaceAIContext) -> Any:
|
|
|
204
204
|
]
|
|
205
205
|
|
|
206
206
|
@server.read_resource() # type: ignore[no-untyped-call]
|
|
207
|
-
async def handle_read_resource(uri: AnyUrl) -> str:
|
|
207
|
+
async def handle_read_resource(uri: AnyUrl) -> str: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
208
208
|
return _read_resource_content(str(uri), context=context)
|
|
209
209
|
|
|
210
210
|
@server.list_tools() # type: ignore[no-untyped-call]
|
|
211
|
-
async def handle_list_tools() -> list[Tool]:
|
|
211
|
+
async def handle_list_tools() -> list[Tool]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
212
212
|
return [
|
|
213
213
|
Tool(
|
|
214
214
|
name=cast(str, t["name"]),
|
|
@@ -234,7 +234,7 @@ def create_server(context: DatafaceAIContext) -> Any:
|
|
|
234
234
|
]
|
|
235
235
|
|
|
236
236
|
@server.call_tool()
|
|
237
|
-
async def handle_call_tool(
|
|
237
|
+
async def handle_call_tool( # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration
|
|
238
238
|
name: str, arguments: dict[str, Any] | None
|
|
239
239
|
) -> CallToolResult:
|
|
240
240
|
arguments = arguments or {}
|
|
@@ -19,6 +19,13 @@ metadata:
|
|
|
19
19
|
|
|
20
20
|
**`{{ s_render_dashboard }}` is how you deliver a dashboard** — call it and return its output. Do not skip it.
|
|
21
21
|
|
|
22
|
+
## Delivery discipline
|
|
23
|
+
|
|
24
|
+
- `{{ s_render_dashboard }}` delivers the dashboard — call it and let the result stand. Do not also paste the rendered output or the full YAML back as prose; that just duplicates the deliverable as an unreadable dump.
|
|
25
|
+
- Act, don't ask. Apply edits and render directly — do not ask for permission or offer optional follow-ups ("if you want, I can save this", "would you like me to…").
|
|
26
|
+
- Fail loud. If a tool returns an error, report it and either fix the input and call the tool again, or stop and say what's blocking. Never route around a failed tool by fabricating output, dumping query rows as a text table in place of a chart, or improvising a different format. A failed render is a failure to report, not to paper over.
|
|
27
|
+
- Never claim a render, query, or save succeeded unless the tool returned success. Never invent columns, tables, or data — if a schema or query call fails, fix it against the real schema.
|
|
28
|
+
|
|
22
29
|
Build dashboards and reports **incrementally** — one chart at a time, validating at every step. Never one-shot an entire dashboard.
|
|
23
30
|
|
|
24
31
|
## Companion Skills
|
|
@@ -140,11 +140,11 @@ class PromptToolkitInput:
|
|
|
140
140
|
kb = KeyBindings()
|
|
141
141
|
|
|
142
142
|
@kb.add("enter")
|
|
143
|
-
def _submit(event: Any) -> None:
|
|
143
|
+
def _submit(event: Any) -> None: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
|
|
144
144
|
event.current_buffer.validate_and_handle()
|
|
145
145
|
|
|
146
146
|
@kb.add("escape", "enter")
|
|
147
|
-
def _newline_meta(event: Any) -> None:
|
|
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
150
|
self._session: PromptSession = PromptSession(
|
|
@@ -205,7 +205,7 @@ def select_input_layer(
|
|
|
205
205
|
return StdlibInput(hist_path)
|
|
206
206
|
|
|
207
207
|
try:
|
|
208
|
-
import prompt_toolkit # noqa: PLC0415, F401
|
|
208
|
+
import prompt_toolkit # noqa: PLC0415, F401 # pyright: ignore[reportUnusedImport] — availability probe; ImportError selects StdlibInput
|
|
209
209
|
except ImportError:
|
|
210
210
|
return StdlibInput(hist_path)
|
|
211
211
|
|
|
@@ -120,7 +120,6 @@ from dataface.core.compile.models.source import (
|
|
|
120
120
|
is_database_source,
|
|
121
121
|
is_file_source,
|
|
122
122
|
parse_source_config,
|
|
123
|
-
resolve_env_var,
|
|
124
123
|
)
|
|
125
124
|
from dataface.core.compile.models.variable.authored import (
|
|
126
125
|
Variable,
|
|
@@ -201,7 +200,6 @@ __all__ = [
|
|
|
201
200
|
"is_database_source",
|
|
202
201
|
"is_file_source",
|
|
203
202
|
"is_api_source",
|
|
204
|
-
"resolve_env_var",
|
|
205
203
|
# Config
|
|
206
204
|
"ProjectSourcesConfig",
|
|
207
205
|
"get_config",
|
dataface/core/compile/channel.py
CHANGED
|
@@ -52,11 +52,6 @@ def parse_style_channel(
|
|
|
52
52
|
if isinstance(raw, str):
|
|
53
53
|
return ResolvedStyleChannel(channel=channel_name, mode="series", data_field=raw)
|
|
54
54
|
|
|
55
|
-
if not isinstance(raw, dict):
|
|
56
|
-
raise ValueError(
|
|
57
|
-
f"Channel '{channel_name}' must be a string or dict, got {type(raw).__name__}"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
55
|
has_column = "column" in raw
|
|
61
56
|
has_value = "value" in raw
|
|
62
57
|
has_scale = "scale" in raw
|
|
@@ -188,7 +183,7 @@ def normalize_chart_channels(
|
|
|
188
183
|
channels["color"] = parse_style_channel(color_raw, "color")
|
|
189
184
|
|
|
190
185
|
# Upgrade series channel to gradient when style.color carries a scale.
|
|
191
|
-
if isinstance(style_color, StyleColorConfig)
|
|
186
|
+
if isinstance(style_color, StyleColorConfig):
|
|
192
187
|
if "color" not in channels:
|
|
193
188
|
raise ValueError(
|
|
194
189
|
"style.color: {scale: ...} requires chart.color to name a data field.\n"
|
dataface/core/compile/colors.py
CHANGED
|
@@ -43,7 +43,7 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
|
|
|
43
43
|
immediately — authored colors that are not valid are a configuration error,
|
|
44
44
|
not a case for silent defaulting.
|
|
45
45
|
"""
|
|
46
|
-
if not color
|
|
46
|
+
if not color:
|
|
47
47
|
return fallback
|
|
48
48
|
if _CSS_HEX_COLOR_PATTERN.match(color):
|
|
49
49
|
return color
|
|
@@ -824,10 +824,8 @@ def load_from_reference(
|
|
|
824
824
|
section_name = "variables"
|
|
825
825
|
elif isinstance(reference, QueryRef):
|
|
826
826
|
section_name = "queries"
|
|
827
|
-
elif isinstance(reference, ChartRef):
|
|
828
|
-
section_name = "charts"
|
|
829
827
|
else:
|
|
830
|
-
|
|
828
|
+
section_name = "charts"
|
|
831
829
|
|
|
832
830
|
# Grammar already validated by the typed model; split is deterministic.
|
|
833
831
|
file_path_str, item_name = reference.ref.rsplit(f".{section_name}.", 1)
|
dataface/core/compile/config.py
CHANGED
|
@@ -32,6 +32,7 @@ from typing import Any
|
|
|
32
32
|
|
|
33
33
|
import yaml
|
|
34
34
|
|
|
35
|
+
from dataface.core.compile.dbt_jinja import render_dbt_jinja_in_dict
|
|
35
36
|
from dataface.core.compile.models.config import (
|
|
36
37
|
Config,
|
|
37
38
|
ConfigNode,
|
|
@@ -43,7 +44,6 @@ from dataface.core.compile.models.config import (
|
|
|
43
44
|
as_plain_mapping,
|
|
44
45
|
is_mapping_like,
|
|
45
46
|
)
|
|
46
|
-
from dataface.core.compile.models.source import resolve_env_vars_in_dict
|
|
47
47
|
from dataface.core.project import Project
|
|
48
48
|
|
|
49
49
|
# ============================================================================
|
|
@@ -101,7 +101,7 @@ def _dataface_yml_mapping_section(
|
|
|
101
101
|
return filename, _as_mapping(value, f"{filename} {key} section")
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def
|
|
104
|
+
def dataface_yml_string_value(project: Project, key: str) -> str | None:
|
|
105
105
|
filename, file_data = _load_dataface_yml(project)
|
|
106
106
|
if key not in file_data:
|
|
107
107
|
return None
|
|
@@ -781,4 +781,4 @@ class ProjectSourcesConfig:
|
|
|
781
781
|
def _normalize_project_sources(
|
|
782
782
|
sources: dict[str, dict[str, Any]],
|
|
783
783
|
) -> dict[str, dict[str, Any]]:
|
|
784
|
-
return {name:
|
|
784
|
+
return {name: render_dbt_jinja_in_dict(config) for name, config in sources.items()}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Render dbt Jinja in source/profile config values.
|
|
2
|
+
|
|
3
|
+
dataface resolves `{{ env_var(...) }}` — and the rest of dbt's profile-rendering
|
|
4
|
+
Jinja — through dbt's own ``ProfileRenderer`` rather than a hand-rolled regex, so
|
|
5
|
+
these values resolve *exactly* as dbt would: env_var defaults, the
|
|
6
|
+
``DBT_ENV_SECRET_``-prefixed secret form, and non-string value preservation
|
|
7
|
+
(`port: 5432` stays an int) all come for free.
|
|
8
|
+
|
|
9
|
+
Behaviour matches dbt's, deliberately:
|
|
10
|
+
- A missing ``env_var()`` with no default raises (surfaced as ``ValueError`` —
|
|
11
|
+
see :func:`_render`) for normal fields.
|
|
12
|
+
- dbt's ``SecretRenderer`` *defers* rendering for ``password`` keypaths (real
|
|
13
|
+
passwords may contain ``{{``/``%`` characters), so a missing password env_var
|
|
14
|
+
is left as a literal and fails at connect time rather than at parse. We accept
|
|
15
|
+
dbt's contract here rather than re-deriving a stricter one.
|
|
16
|
+
|
|
17
|
+
dbt reads env vars from a process-global *invocation context* (a ``ContextVar``
|
|
18
|
+
it sets at CLI startup). dataface embeds dbt as a library, so we establish that
|
|
19
|
+
context from the live environment per render.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _refresh_invocation_context() -> None:
|
|
29
|
+
"""(Re)build dbt's invocation context from the live ``os.environ``.
|
|
30
|
+
|
|
31
|
+
dbt's ``env_var()`` reads a process-global ``ContextVar``, not ``os.environ``
|
|
32
|
+
directly. The context holds nothing but env-derived state (public/private env
|
|
33
|
+
split + a secret cache), so rebuilding it per render is cheap, loses nothing,
|
|
34
|
+
and keeps ``env_var()`` reading the *current* environment — matching a direct
|
|
35
|
+
``os.environ.get`` and keeping renders independent across env changes.
|
|
36
|
+
|
|
37
|
+
This is a compile-stage, single-threaded path. If dataface ever rendered
|
|
38
|
+
source configs concurrently with an active *embedded* dbt invocation, this
|
|
39
|
+
overwrite would clobber that invocation's context — not a concern today.
|
|
40
|
+
"""
|
|
41
|
+
from dbt_common.context import set_invocation_context
|
|
42
|
+
|
|
43
|
+
# noqa rationale: dbt env_var() resolves YAML macros from the live process
|
|
44
|
+
# environment by contract — this is the composition root for that read.
|
|
45
|
+
set_invocation_context(dict(os.environ)) # noqa: TID251 — env_var() YAML macro
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_dbt_jinja_in_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
"""Render dbt Jinja in every (recursively nested) string value of a dict.
|
|
50
|
+
|
|
51
|
+
Non-string values pass through unchanged. ``password`` keypaths follow dbt's
|
|
52
|
+
secret-deferral (see module docstring); other fields raise ``ValueError`` on
|
|
53
|
+
an unset env var with no default or on malformed/unresolvable Jinja.
|
|
54
|
+
|
|
55
|
+
dbt raises its own hierarchy (``EnvVarMissingError`` / ``CompilationError``);
|
|
56
|
+
we adapt it to ``ValueError`` at this boundary so Pydantic field/model
|
|
57
|
+
validators surface a ``ValidationError``. The dbt message is preserved.
|
|
58
|
+
"""
|
|
59
|
+
from dbt.config.renderer import ProfileRenderer
|
|
60
|
+
from dbt_common.exceptions.base import DbtBaseException
|
|
61
|
+
|
|
62
|
+
_refresh_invocation_context()
|
|
63
|
+
try:
|
|
64
|
+
return ProfileRenderer({}).render_data(data)
|
|
65
|
+
except DbtBaseException as e:
|
|
66
|
+
raise ValueError(str(e)) from e
|
|
@@ -125,14 +125,41 @@ def _build_graph(
|
|
|
125
125
|
|
|
126
126
|
if nested is not None:
|
|
127
127
|
if skip_marker is not None:
|
|
128
|
-
#
|
|
128
|
+
# SkipInheritSlots suppresses slot expansion for this field.
|
|
129
|
+
# cascade=True: emit a container-level link so apply_inherit can
|
|
130
|
+
# copy the whole object when the child is None. Leaf writes
|
|
131
|
+
# through a None intermediate are not viable (_set skips them).
|
|
132
|
+
if (
|
|
133
|
+
skip_marker.cascade
|
|
134
|
+
and slot_at is not None
|
|
135
|
+
and slot_from is not None
|
|
136
|
+
and not skip_slots
|
|
137
|
+
):
|
|
138
|
+
suffix: StylePath = current[len(slot_at) :]
|
|
139
|
+
fallback_path = _path_str(slot_from + suffix)
|
|
140
|
+
graph[_path_str(current)] = fallback_path
|
|
141
|
+
# Recurse into subtree.
|
|
142
|
+
# cascade=True inside a slot: keep the outer slot context so
|
|
143
|
+
# that individual leaf fields inside the subtree also get
|
|
144
|
+
# slot-derived links (e.g. charts.line.marks.line.stroke.width
|
|
145
|
+
# → charts.marks.line.stroke.width). The container link above
|
|
146
|
+
# handles the stroke=None case; leaf links handle the case where
|
|
147
|
+
# the container is set but individual fields are None. Inner
|
|
148
|
+
# InheritSlot markers with an absolute from_path still fire
|
|
149
|
+
# because skip_slots=False.
|
|
150
|
+
# cascade=True outside a slot (canonical path): both slot args
|
|
151
|
+
# are None already, so this is equivalent to clearing them.
|
|
152
|
+
# cascade=False: set skip_slots=True to fully suppress inner
|
|
153
|
+
# slot expansion (original SkipInheritSlots semantics).
|
|
129
154
|
_build_graph(
|
|
130
155
|
nested,
|
|
131
156
|
current,
|
|
132
157
|
seen,
|
|
133
158
|
valid_paths,
|
|
134
159
|
graph,
|
|
135
|
-
|
|
160
|
+
slot_at=slot_at if skip_marker.cascade else None,
|
|
161
|
+
slot_from=slot_from if skip_marker.cascade else None,
|
|
162
|
+
skip_slots=not skip_marker.cascade,
|
|
136
163
|
)
|
|
137
164
|
elif slot_marker is not None and not skip_slots:
|
|
138
165
|
if slot_at is not None:
|
|
@@ -197,8 +224,8 @@ def _build_graph(
|
|
|
197
224
|
else:
|
|
198
225
|
graph[current_path] = p
|
|
199
226
|
elif slot_at is not None and slot_from is not None and skip_marker is None:
|
|
200
|
-
|
|
201
|
-
fallback_path = _path_str(slot_from +
|
|
227
|
+
leaf_suffix: StylePath = current[len(slot_at) :]
|
|
228
|
+
fallback_path = _path_str(slot_from + leaf_suffix)
|
|
202
229
|
if fallback_path not in valid_paths:
|
|
203
230
|
raise ValueError(
|
|
204
231
|
f"InheritSlot expansion {fallback_path!r} not found in model paths"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Inherit resolver: fill style leaf fields from single-link InheritGraph chains.
|
|
2
2
|
|
|
3
|
-
apply_inherit
|
|
4
|
-
It runs after deep_merge and fills None-sentinel leaves by walking the single-link
|
|
3
|
+
apply_inherit fills None-sentinel leaves by walking the single-link
|
|
5
4
|
parent chains declared via Inherit/InheritSlot markers on the compiled Style.
|
|
5
|
+
It runs after deep_merge, replacing the former hard-coded push loop approach.
|
|
6
6
|
|
|
7
7
|
Not in scope: _apply_token_cascade, _build_resolved_style, resolved_axis_style.
|
|
8
8
|
"""
|
|
@@ -68,8 +68,17 @@ def apply_inherit(merged: Style, links: InheritGraph) -> Style:
|
|
|
68
68
|
if not updates:
|
|
69
69
|
return merged
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
# Write shorter paths before longer ones so container-level updates (from
|
|
72
|
+
# cascade=True SkipInheritSlots links) land before leaf writes into those
|
|
73
|
+
# containers. _set skips writes through a None intermediate, so the
|
|
74
|
+
# container dict must exist first.
|
|
75
|
+
# Re-read from d before each write: a container update may have already
|
|
76
|
+
# populated a field that a leaf link also targets (e.g. container copies
|
|
77
|
+
# marks.slice.labels.font.size=14 from the canonical path, while a separate
|
|
78
|
+
# leaf link would overwrite with the fallback charts.font.size=11).
|
|
79
|
+
for path in sorted(updates, key=lambda p: p.count(".")):
|
|
80
|
+
if _get(d, _obj_path(path)) is None:
|
|
81
|
+
_set(d, _obj_path(path), updates[path])
|
|
73
82
|
|
|
74
83
|
return Style.model_validate(d)
|
|
75
84
|
|
dataface/core/compile/jinja.py
CHANGED
|
@@ -87,7 +87,7 @@ def extract_variable_dependencies(template_str: str) -> set[str]:
|
|
|
87
87
|
... )
|
|
88
88
|
{'region'}
|
|
89
89
|
"""
|
|
90
|
-
if not template_str
|
|
90
|
+
if not template_str:
|
|
91
91
|
return set()
|
|
92
92
|
|
|
93
93
|
# Quick check - no Jinja syntax
|
|
@@ -142,7 +142,7 @@ def resolve_jinja_template(
|
|
|
142
142
|
... )
|
|
143
143
|
"SELECT * FROM users WHERE status = 'active'"
|
|
144
144
|
"""
|
|
145
|
-
if not template
|
|
145
|
+
if not template:
|
|
146
146
|
return template
|
|
147
147
|
|
|
148
148
|
# Quick check for Jinja syntax
|
|
@@ -29,16 +29,16 @@ from pydantic import (
|
|
|
29
29
|
from dataface.core.compile.models.primitives import (
|
|
30
30
|
FontStyle,
|
|
31
31
|
FormatConfig,
|
|
32
|
-
ScaleTargetConfig, #
|
|
32
|
+
ScaleTargetConfig as ScaleTargetConfig, # re-exported for external callers
|
|
33
33
|
)
|
|
34
34
|
from dataface.core.compile.models.query.authored import AuthoredQuery
|
|
35
35
|
from dataface.core.compile.models.refs import QueryRef, normalize_query_value
|
|
36
36
|
from dataface.core.compile.models.style.authored import (
|
|
37
|
-
_SPARK_RENAMED_AWAY, #
|
|
37
|
+
_SPARK_RENAMED_AWAY as _SPARK_RENAMED_AWAY, # re-exported for external callers
|
|
38
38
|
AreaChartStylePatch,
|
|
39
39
|
BarChartStylePatch,
|
|
40
40
|
CalloutChartStylePatch,
|
|
41
|
-
ColumnScaleConfig, #
|
|
41
|
+
ColumnScaleConfig as ColumnScaleConfig, # re-exported for external callers
|
|
42
42
|
GeoshapeChartStylePatch,
|
|
43
43
|
HeatmapChartStylePatch,
|
|
44
44
|
KpiChartStylePatch,
|
|
@@ -48,10 +48,10 @@ from dataface.core.compile.models.style.authored import (
|
|
|
48
48
|
PointMapChartStylePatch,
|
|
49
49
|
ScatterChartStylePatch,
|
|
50
50
|
SparkBarChartStylePatch,
|
|
51
|
-
SparkConfig, #
|
|
52
|
-
SparkTypeLiteral, #
|
|
51
|
+
SparkConfig as SparkConfig, # re-exported for external callers
|
|
52
|
+
SparkTypeLiteral as SparkTypeLiteral, # re-exported for external callers
|
|
53
53
|
TableChartStylePatch,
|
|
54
|
-
TableColumnConfig, #
|
|
54
|
+
TableColumnConfig as TableColumnConfig, # re-exported for external callers
|
|
55
55
|
_validate_glyph_pair,
|
|
56
56
|
)
|
|
57
57
|
from dataface.core.compile.models.style.theme import (
|
|
@@ -1554,10 +1554,10 @@ class KpiChart(_BaseChartFields):
|
|
|
1554
1554
|
)
|
|
1555
1555
|
if data.get("subtitle"):
|
|
1556
1556
|
from dataface.core.compile.normalize_charts import ( # noqa: PLC0415
|
|
1557
|
-
|
|
1557
|
+
kpi_subtitle_error,
|
|
1558
1558
|
)
|
|
1559
1559
|
|
|
1560
|
-
raise ValueError(
|
|
1560
|
+
raise ValueError(kpi_subtitle_error(data.get("id")))
|
|
1561
1561
|
return data
|
|
1562
1562
|
|
|
1563
1563
|
@model_validator(mode="before")
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from dataface.core.compile.models.chart.
|
|
9
|
+
from dataface.core.compile.models.chart.resolved import ResolvedChart
|
|
10
10
|
from dataface.core.compile.models.face.normalized import VariableValues
|
|
11
11
|
from dataface.core.compile.models.query.normalized import AnyQuery
|
|
12
12
|
from dataface.core.compile.models.style.resolved import ResolvedStyle
|
|
@@ -18,7 +18,7 @@ class ResolvedLayoutItem:
|
|
|
18
18
|
"""Layout item with static dimension estimates — no executor, no data access."""
|
|
19
19
|
|
|
20
20
|
type: str # "chart" | "face" | "text" | "markdown" | ...
|
|
21
|
-
chart:
|
|
21
|
+
chart: ResolvedChart | None
|
|
22
22
|
# nested face is a forward reference — resolved recursively
|
|
23
23
|
face: "ResolvedFace | None"
|
|
24
24
|
x: float
|
|
@@ -101,7 +101,7 @@ class ResolvedFace:
|
|
|
101
101
|
layout: ResolvedLayout
|
|
102
102
|
|
|
103
103
|
# Data (unchanged from Face)
|
|
104
|
-
charts: dict[str,
|
|
104
|
+
charts: dict[str, ResolvedChart]
|
|
105
105
|
variables: dict[str, Variable]
|
|
106
106
|
queries: dict[str, AnyQuery]
|
|
107
107
|
variable_defaults: VariableValues
|
|
@@ -92,7 +92,18 @@ class SkipInheritSlots:
|
|
|
92
92
|
``_ChartStyleBase`` must not inherit from the root cascade.
|
|
93
93
|
|
|
94
94
|
**On a leaf scalar field**: prevents the leaf from being added to the graph
|
|
95
|
-
via slot expansion. Use on ``AxisStyle`` fields that
|
|
96
|
-
cascade — e.g. ``grid.dash``, ``ticks.count``, ``scale``, ``position`` —
|
|
97
|
-
the inherit graph only declares what
|
|
95
|
+
via slot expansion. Use on ``AxisStyle`` fields that the style resolver does
|
|
96
|
+
not cascade — e.g. ``grid.dash``, ``ticks.count``, ``scale``, ``position`` —
|
|
97
|
+
so the inherit graph only declares what the resolver actually does.
|
|
98
|
+
|
|
99
|
+
``cascade=True`` — for optional nested-model fields that the resolver
|
|
100
|
+
cascades at container granularity (copies the whole object when the child is
|
|
101
|
+
None, rather than filling individual leaf fields). When set, the graph
|
|
102
|
+
emits a *container-level* link ``child_path → parent_path`` instead of
|
|
103
|
+
suppressing the field entirely. Leaf writes through a ``None`` intermediate
|
|
104
|
+
are not viable (``_set`` skips them), so container links are the only
|
|
105
|
+
mechanism. Only applicable on ``T | None`` nested-model fields inside an
|
|
106
|
+
active slot context.
|
|
98
107
|
"""
|
|
108
|
+
|
|
109
|
+
cascade: bool = False
|
|
@@ -188,6 +188,10 @@ class SqlQuery(Query):
|
|
|
188
188
|
default=None,
|
|
189
189
|
description="dbt target name (defaults to 'dev' if not specified).",
|
|
190
190
|
)
|
|
191
|
+
lenient_variables: bool = Field(
|
|
192
|
+
default=False,
|
|
193
|
+
description="When True, undefined Jinja variables degrade gracefully (empty string) instead of raising. Intended for the iterative chart-editor flow where not all variables are set yet.",
|
|
194
|
+
)
|
|
191
195
|
|
|
192
196
|
@field_validator("source", mode="before")
|
|
193
197
|
@classmethod
|
|
@@ -44,12 +44,13 @@ See also:
|
|
|
44
44
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
|
-
import os
|
|
48
47
|
import re
|
|
49
48
|
from typing import Any, ClassVar, Literal, get_args, get_type_hints
|
|
50
49
|
|
|
51
50
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
52
51
|
|
|
52
|
+
from dataface.core.compile.dbt_jinja import render_dbt_jinja_in_dict
|
|
53
|
+
|
|
53
54
|
# ============================================================================
|
|
54
55
|
# BIGQUERY AUTH HELPERS
|
|
55
56
|
# ============================================================================
|
|
@@ -66,65 +67,6 @@ def infer_bq_method(
|
|
|
66
67
|
return "oauth"
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
# ============================================================================
|
|
70
|
-
# ENVIRONMENT VARIABLE RESOLUTION
|
|
71
|
-
# ============================================================================
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def resolve_env_var(value: str) -> str:
|
|
75
|
-
"""Resolve {{ env_var('VAR') }} or {{ env_var('VAR', 'default') }} syntax.
|
|
76
|
-
|
|
77
|
-
Matches dbt's env_var() function behavior.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
value: String that may contain env_var() expressions
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
String with env_var() resolved to actual values
|
|
84
|
-
|
|
85
|
-
Raises:
|
|
86
|
-
ValueError: If required env var is not set
|
|
87
|
-
"""
|
|
88
|
-
# Pattern: {{ env_var('VAR') }} or {{ env_var('VAR', 'default') }}
|
|
89
|
-
pattern = r"\{\{\s*env_var\(\s*['\"]([^'\"]+)['\"]\s*(?:,\s*['\"]([^'\"]*)['\"])?\s*\)\s*\}\}"
|
|
90
|
-
|
|
91
|
-
def replace_env_var(match: re.Match[str]) -> str:
|
|
92
|
-
var_name = match.group(1)
|
|
93
|
-
default_value = match.group(2)
|
|
94
|
-
|
|
95
|
-
env_value = os.environ.get(var_name) # noqa: TID251 — env_var() YAML macro
|
|
96
|
-
if env_value is not None:
|
|
97
|
-
return env_value
|
|
98
|
-
elif default_value is not None:
|
|
99
|
-
return default_value
|
|
100
|
-
else:
|
|
101
|
-
raise ValueError(
|
|
102
|
-
f"Environment variable '{var_name}' is not set and no default provided"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
return re.sub(pattern, replace_env_var, value)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def resolve_env_vars_in_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
109
|
-
"""Recursively resolve env_var() expressions in a dictionary.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
data: Dictionary that may contain env_var() expressions
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
Dictionary with all env_var() expressions resolved
|
|
116
|
-
"""
|
|
117
|
-
result: dict[str, Any] = {}
|
|
118
|
-
for key, value in data.items():
|
|
119
|
-
if isinstance(value, str):
|
|
120
|
-
result[key] = resolve_env_var(value)
|
|
121
|
-
elif isinstance(value, dict):
|
|
122
|
-
result[key] = resolve_env_vars_in_dict(value)
|
|
123
|
-
else:
|
|
124
|
-
result[key] = value
|
|
125
|
-
return result
|
|
126
|
-
|
|
127
|
-
|
|
128
70
|
# ============================================================================
|
|
129
71
|
# BASE SOURCE CONFIG
|
|
130
72
|
# ============================================================================
|
|
@@ -143,9 +85,9 @@ class BaseSourceConfig(BaseModel):
|
|
|
143
85
|
@model_validator(mode="before")
|
|
144
86
|
@classmethod
|
|
145
87
|
def _resolve_all_env_vars(cls, data: Any) -> Any:
|
|
146
|
-
"""
|
|
88
|
+
"""Render dbt Jinja (env_var etc.) in all fields before Pydantic validates."""
|
|
147
89
|
if isinstance(data, dict):
|
|
148
|
-
return
|
|
90
|
+
return render_dbt_jinja_in_dict(data)
|
|
149
91
|
return data
|
|
150
92
|
|
|
151
93
|
|
|
@@ -406,14 +348,6 @@ class CsvSourceConfig(BaseSourceConfig):
|
|
|
406
348
|
raise ValueError("CsvSourceConfig requires either 'file' or 'url'")
|
|
407
349
|
return self
|
|
408
350
|
|
|
409
|
-
@field_validator("file", "url", mode="before")
|
|
410
|
-
@classmethod
|
|
411
|
-
def resolve_env_vars(cls, v: Any) -> Any:
|
|
412
|
-
"""Resolve env_var() syntax in string fields."""
|
|
413
|
-
if isinstance(v, str):
|
|
414
|
-
return resolve_env_var(v)
|
|
415
|
-
return v
|
|
416
|
-
|
|
417
351
|
|
|
418
352
|
class ParquetSourceConfig(BaseSourceConfig):
|
|
419
353
|
"""Parquet file source configuration."""
|
|
@@ -473,22 +407,6 @@ class HttpSourceConfig(BaseSourceConfig):
|
|
|
473
407
|
default=None, description="Default HTTP headers (e.g. Authorization)."
|
|
474
408
|
)
|
|
475
409
|
|
|
476
|
-
@field_validator("url", mode="before")
|
|
477
|
-
@classmethod
|
|
478
|
-
def resolve_url_env_var(cls, v: Any) -> Any:
|
|
479
|
-
"""Resolve env_var() syntax in URL."""
|
|
480
|
-
if isinstance(v, str):
|
|
481
|
-
return resolve_env_var(v)
|
|
482
|
-
return v
|
|
483
|
-
|
|
484
|
-
@field_validator("headers", mode="before")
|
|
485
|
-
@classmethod
|
|
486
|
-
def resolve_headers_env_vars(cls, v: Any) -> Any:
|
|
487
|
-
"""Resolve env_var() syntax in headers."""
|
|
488
|
-
if isinstance(v, dict):
|
|
489
|
-
return resolve_env_vars_in_dict(v)
|
|
490
|
-
return v
|
|
491
|
-
|
|
492
410
|
|
|
493
411
|
# ============================================================================
|
|
494
412
|
# DBT PROFILE SOURCE CONFIG
|