dataface 0.3.2.dev111__py3-none-any.whl → 0.3.2.dev150__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 +15 -4
- dataface/agent_api/docs/yaml-reference.md +33 -133
- dataface/agent_api/serve.py +0 -2
- dataface/agent_api/validate.py +8 -0
- dataface/cli/commands/render.py +6 -0
- dataface/cli/commands/serve.py +0 -6
- dataface/cli/main.py +20 -14
- dataface/core/aliases.py +27 -0
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +73 -71
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +73 -159
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/normalize_charts.py +4 -152
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +3 -506
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +107 -2
- dataface/core/dashboard.py +3 -1
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +0 -1
- dataface/core/defaults/themes/editorial.yaml +3 -0
- dataface/core/defaults/themes/stark.yaml +8 -0
- dataface/core/diagnostics/__init__.py +10 -0
- dataface/core/diagnostics/codes_compile.py +4 -2
- dataface/core/diagnostics/codes_execute.py +70 -0
- dataface/core/diagnostics/codes_render.py +58 -4
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/executor.py +25 -3
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/inspect/__init__.py +0 -1
- dataface/core/project.py +6 -2
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/rendering.py +97 -34
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +94 -30
- dataface/core/render/layout_sizing.py +46 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/registry.py +2 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -499
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +69 -67
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
dataface/cli/main.py
CHANGED
|
@@ -796,6 +796,16 @@ def render(
|
|
|
796
796
|
),
|
|
797
797
|
),
|
|
798
798
|
] = False,
|
|
799
|
+
chart: Annotated[
|
|
800
|
+
str | None,
|
|
801
|
+
typer.Option(
|
|
802
|
+
"--chart",
|
|
803
|
+
help=(
|
|
804
|
+
"Render only this chart (by chart id). "
|
|
805
|
+
"Valid only with a single face argument or stdin."
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
] = None,
|
|
799
809
|
) -> None:
|
|
800
810
|
"""Render one or more dashboards to SVG, HTML, PNG, PDF, or terminal.
|
|
801
811
|
|
|
@@ -859,6 +869,14 @@ def render(
|
|
|
859
869
|
param_hint="--output",
|
|
860
870
|
)
|
|
861
871
|
|
|
872
|
+
# --chart is ambiguous with multiple faces: no mapping syntax, no silent fanout.
|
|
873
|
+
if chart is not None and len(faces) > 1:
|
|
874
|
+
raise typer.BadParameter(
|
|
875
|
+
f"--chart is only valid with a single face argument; "
|
|
876
|
+
f"got {len(faces)} face arguments",
|
|
877
|
+
param_hint="--chart",
|
|
878
|
+
)
|
|
879
|
+
|
|
862
880
|
# Stdin path (single face "-")
|
|
863
881
|
if has_stdin:
|
|
864
882
|
yaml_content = sys.stdin.read()
|
|
@@ -879,6 +897,7 @@ def render(
|
|
|
879
897
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
880
898
|
max_workers=max_workers,
|
|
881
899
|
print0=print0,
|
|
900
|
+
chart=chart,
|
|
882
901
|
)
|
|
883
902
|
return
|
|
884
903
|
|
|
@@ -897,6 +916,7 @@ def render(
|
|
|
897
916
|
max_workers=max_workers,
|
|
898
917
|
print0=print0,
|
|
899
918
|
fail_fast=fail_fast,
|
|
919
|
+
chart=chart,
|
|
900
920
|
)
|
|
901
921
|
|
|
902
922
|
|
|
@@ -1087,19 +1107,6 @@ def serve(
|
|
|
1087
1107
|
),
|
|
1088
1108
|
),
|
|
1089
1109
|
] = None,
|
|
1090
|
-
allow_external_access: Annotated[
|
|
1091
|
-
bool,
|
|
1092
|
-
typer.Option(
|
|
1093
|
-
"--allow-external-access",
|
|
1094
|
-
help=(
|
|
1095
|
-
"Allow author SQL to read local files / URLs through DuckDB "
|
|
1096
|
-
"(read_csv, read_json_auto, httpfs). Off by default — serve runs "
|
|
1097
|
-
"strict read-only. Opt in only for local dashboards that must read "
|
|
1098
|
-
"files directly in SQL (e.g. glob analytics not expressible as file "
|
|
1099
|
-
"sources); prefer type: csv/json/parquet sources otherwise."
|
|
1100
|
-
),
|
|
1101
|
-
),
|
|
1102
|
-
] = False,
|
|
1103
1110
|
) -> None:
|
|
1104
1111
|
"""Start the dashboard server.
|
|
1105
1112
|
|
|
@@ -1149,7 +1156,6 @@ def serve(
|
|
|
1149
1156
|
max_workers=max_workers,
|
|
1150
1157
|
no_cache=no_cache,
|
|
1151
1158
|
cache_path=cache,
|
|
1152
|
-
allow_external_access=allow_external_access,
|
|
1153
1159
|
)
|
|
1154
1160
|
|
|
1155
1161
|
|
dataface/core/aliases.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Alias-URL semantics, shared by every host that resolves a face's ``aliases:``.
|
|
2
|
+
|
|
3
|
+
A leaf module on purpose. An alias is claimed in a face and matched against an
|
|
4
|
+
incoming request path as a *string*, so the two sides must normalize it
|
|
5
|
+
identically or a claim resolves on one host and silently does nothing on the
|
|
6
|
+
other. Hosts that keep their own board index (rather than building an
|
|
7
|
+
``AliasIndex``) need that rule without importing the ``dft serve`` HTTP
|
|
8
|
+
application to get it — ``core/serve/`` sits at the top of the dependency stack.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from urllib.parse import unquote
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_alias_url(url: str) -> str:
|
|
17
|
+
"""Normalize a URL key: percent-decode, ensure leading /, ensure trailing /."""
|
|
18
|
+
decoded = unquote(url)
|
|
19
|
+
if not decoded.startswith("/"):
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"Alias {url!r} is not absolute: aliases must start with a leading /. "
|
|
22
|
+
"Example: /old-reports/"
|
|
23
|
+
)
|
|
24
|
+
# Trailing-slash canonical: add if missing
|
|
25
|
+
if not decoded.endswith("/"):
|
|
26
|
+
decoded = decoded + "/"
|
|
27
|
+
return decoded
|
|
@@ -80,8 +80,6 @@ from dataface.core.compile.models.face.normalized import (
|
|
|
80
80
|
from dataface.core.compile.models.query.authored import (
|
|
81
81
|
AuthoredCompactValuesQuery,
|
|
82
82
|
AuthoredHttpQuery,
|
|
83
|
-
AuthoredLookmlMergeQuery,
|
|
84
|
-
AuthoredLookmlQuery,
|
|
85
83
|
AuthoredMetricflowQuery,
|
|
86
84
|
AuthoredQuery,
|
|
87
85
|
AuthoredSchemaQuery,
|
|
@@ -104,8 +102,6 @@ from dataface.core.compile.models.source import (
|
|
|
104
102
|
DuckDBSourceConfig,
|
|
105
103
|
HttpSourceConfig,
|
|
106
104
|
JsonSourceConfig,
|
|
107
|
-
LookMLModelConfig,
|
|
108
|
-
LookMLModelsMixin,
|
|
109
105
|
MySQLSourceConfig,
|
|
110
106
|
ParquetSourceConfig,
|
|
111
107
|
PostgresSourceConfig,
|
|
@@ -162,8 +158,6 @@ __all__ = [
|
|
|
162
158
|
"SparkBarChart",
|
|
163
159
|
"AuthoredCompactValuesQuery",
|
|
164
160
|
"AuthoredHttpQuery",
|
|
165
|
-
"AuthoredLookmlQuery",
|
|
166
|
-
"AuthoredLookmlMergeQuery",
|
|
167
161
|
"AuthoredMetricflowQuery",
|
|
168
162
|
"AuthoredQuery",
|
|
169
163
|
"AuthoredSchemaQuery",
|
|
@@ -196,8 +190,6 @@ __all__ = [
|
|
|
196
190
|
"JsonSourceConfig",
|
|
197
191
|
"HttpSourceConfig",
|
|
198
192
|
"DbtProfileSourceConfig",
|
|
199
|
-
"LookMLModelConfig",
|
|
200
|
-
"LookMLModelsMixin",
|
|
201
193
|
"VALID_SOURCE_TYPES",
|
|
202
194
|
"parse_source_config",
|
|
203
195
|
"is_database_source",
|
dataface/core/compile/channel.py
CHANGED
|
@@ -10,7 +10,6 @@ block (indexed by column name), not per-channel. See ``types.py`` for
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import re
|
|
14
13
|
from collections.abc import Callable, Iterable
|
|
15
14
|
from dataclasses import dataclass
|
|
16
15
|
from typing import Any, Protocol, runtime_checkable
|
|
@@ -72,8 +71,6 @@ class ConditionalFormattingChart(Protocol):
|
|
|
72
71
|
...
|
|
73
72
|
|
|
74
73
|
|
|
75
|
-
_DOTTED_LOOKML_REF = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$")
|
|
76
|
-
|
|
77
74
|
# Chart types whose color channel is a paintable mark fill — conditional_
|
|
78
75
|
# formatting rules targeting `background` lower into their `color` channel.
|
|
79
76
|
_MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
|
|
@@ -91,16 +88,6 @@ _MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
|
|
|
91
88
|
)
|
|
92
89
|
|
|
93
90
|
|
|
94
|
-
def is_dotted_lookml_ref(value: str) -> bool:
|
|
95
|
-
"""True if `value` has the shape of a LookML `view.field` ref.
|
|
96
|
-
|
|
97
|
-
Exactly one dot, both sides valid identifiers, no spaces. Used by the
|
|
98
|
-
Surface B collect pass to find channel values that name LookML fields
|
|
99
|
-
rather than already-resolved column names.
|
|
100
|
-
"""
|
|
101
|
-
return bool(_DOTTED_LOOKML_REF.match(value))
|
|
102
|
-
|
|
103
|
-
|
|
104
91
|
def parse_style_channel(
|
|
105
92
|
raw: _RawStyleChannel,
|
|
106
93
|
channel_name: str,
|
|
@@ -66,12 +66,10 @@ from dataface.core.compile.models.chart.authored import _BaseChartFields
|
|
|
66
66
|
from dataface.core.compile.models.config import ProjectCacheConfig
|
|
67
67
|
from dataface.core.compile.models.face.authored import (
|
|
68
68
|
AuthoredFace,
|
|
69
|
-
ForeachItem,
|
|
70
69
|
QueryOrRef,
|
|
71
70
|
)
|
|
72
71
|
from dataface.core.compile.models.face.normalized import Face
|
|
73
72
|
from dataface.core.compile.models.query.authored import (
|
|
74
|
-
AuthoredLookmlQuery,
|
|
75
73
|
AuthoredMetricflowQuery,
|
|
76
74
|
)
|
|
77
75
|
from dataface.core.compile.models.query.normalized import AnyQuery, is_sql_query
|
|
@@ -83,13 +81,9 @@ from dataface.core.compile.models.refs import (
|
|
|
83
81
|
)
|
|
84
82
|
from dataface.core.compile.models.variable.authored import Variable
|
|
85
83
|
from dataface.core.compile.normalize_charts import (
|
|
86
|
-
collect_lookml_channel_refs,
|
|
87
84
|
collect_metricflow_channel_refs,
|
|
88
85
|
)
|
|
89
|
-
from dataface.core.compile.normalize_queries import
|
|
90
|
-
find_lookml_model_source,
|
|
91
|
-
split_external_query_ref,
|
|
92
|
-
)
|
|
86
|
+
from dataface.core.compile.normalize_queries import split_external_query_ref
|
|
93
87
|
from dataface.core.compile.normalize_variables import (
|
|
94
88
|
VariableReferenceErrors,
|
|
95
89
|
synthetic_query_name,
|
|
@@ -328,7 +322,7 @@ def compile_authored_face(
|
|
|
328
322
|
# ════════════════════════════════════════════════════════════════════
|
|
329
323
|
# STEP 3b: Build Chart Registry (moved ahead of queries — Surface B's
|
|
330
324
|
# collect pass below needs charts to find dotted-ref channel bindings
|
|
331
|
-
# before
|
|
325
|
+
# before semantic queries lower to SQL).
|
|
332
326
|
# ════════════════════════════════════════════════════════════════════
|
|
333
327
|
# Pool that imported charts' own query chains resolve into, lexically
|
|
334
328
|
# scoped to each chart's source face (compile/AGENTS.md's "Cross-face
|
|
@@ -355,11 +349,10 @@ def compile_authored_face(
|
|
|
355
349
|
# ════════════════════════════════════════════════════════════════════
|
|
356
350
|
# STEP 3b.5: Desugar chart-level `model:` sugar into a synthesized named
|
|
357
351
|
# query, before Surface B channel collection (below) so the synthesized
|
|
358
|
-
# query is visible to that pass. `model:
|
|
359
|
-
# becomes a
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
# raises a clear error.
|
|
352
|
+
# query is visible to that pass. `model: source.semantic_model` on a
|
|
353
|
+
# dbt_profile source becomes a metricflow query (roles inferred from the
|
|
354
|
+
# manifest in STEP 3c). Runs unconditionally so an unknown/ambiguous
|
|
355
|
+
# `model:` name still raises a clear error.
|
|
363
356
|
# ════════════════════════════════════════════════════════════════════
|
|
364
357
|
metricflow_model_by_query: dict[str, str] = {}
|
|
365
358
|
try:
|
|
@@ -388,33 +381,22 @@ def compile_authored_face(
|
|
|
388
381
|
if len(_parts) != 2 or not all(_parts):
|
|
389
382
|
raise CompilationError(
|
|
390
383
|
f"chart '{_chart_key}' has `model: {_model_value!r}` — expected "
|
|
391
|
-
"`
|
|
392
|
-
"joined by a dot)."
|
|
384
|
+
"`source.semantic_model` (a dbt_profile source name and "
|
|
385
|
+
"semantic model name joined by a dot)."
|
|
393
386
|
)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
_lookml_source is None
|
|
399
|
-
and isinstance(_source_cfg, dict)
|
|
387
|
+
_source_name, _semantic_model_name = _parts
|
|
388
|
+
_source_cfg = sources_registry.get(_source_name)
|
|
389
|
+
if not (
|
|
390
|
+
isinstance(_source_cfg, dict)
|
|
400
391
|
and _source_cfg.get("type") == "dbt_profile"
|
|
401
|
-
)
|
|
402
|
-
if _lookml_source is None and not _is_metricflow:
|
|
392
|
+
):
|
|
403
393
|
raise CompilationError(
|
|
404
394
|
f"chart '{_chart_key}' has `model: {_model_value}` naming "
|
|
405
|
-
f"unknown
|
|
406
|
-
"'models:' map, and it is not a dbt_profile source."
|
|
395
|
+
f"unknown source '{_source_name}' — it is not a dbt_profile source."
|
|
407
396
|
)
|
|
408
397
|
_synth_name = synthetic_query_name("model_query", _chart_key)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
source=_model_name
|
|
412
|
-
)
|
|
413
|
-
metricflow_model_by_query[_synth_name] = _explore_name
|
|
414
|
-
else:
|
|
415
|
-
authored.queries[_synth_name] = AuthoredLookmlQuery(
|
|
416
|
-
source=_model_name, explore=_explore_name
|
|
417
|
-
)
|
|
398
|
+
authored.queries[_synth_name] = AuthoredMetricflowQuery(source=_source_name)
|
|
399
|
+
metricflow_model_by_query[_synth_name] = _semantic_model_name
|
|
418
400
|
# Mutate in place: _BaseChartFields declares query + model; plain
|
|
419
401
|
# dicts use key assignment. Non-_BaseChartFields BaseModels (e.g.
|
|
420
402
|
# CalloutChart) have no `model:` field and are skipped above.
|
|
@@ -428,31 +410,21 @@ def compile_authored_face(
|
|
|
428
410
|
return CompileResult(errors=[e.to_diagnostic(file=file)])
|
|
429
411
|
|
|
430
412
|
# ════════════════════════════════════════════════════════════════════
|
|
431
|
-
# STEP 3c: Collect
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
# MetricFlow classifies bare fields against the manifest into
|
|
413
|
+
# STEP 3c: Collect bare-field (metricflow) chart channels into their
|
|
414
|
+
# synthesized queries, before query normalization lowers them. MetricFlow
|
|
415
|
+
# classifies bare fields against the manifest into
|
|
435
416
|
# metrics/dimensions/time_grain.
|
|
436
417
|
# ════════════════════════════════════════════════════════════════════
|
|
437
|
-
|
|
438
|
-
for _cfg in sources_registry.values():
|
|
439
|
-
if isinstance(_cfg, dict) and isinstance(_cfg.get("models"), dict):
|
|
440
|
-
lookml_model_names.update(_cfg["models"].keys())
|
|
441
|
-
if lookml_model_names or metricflow_model_by_query:
|
|
418
|
+
if metricflow_model_by_query:
|
|
442
419
|
try:
|
|
443
420
|
raw_queries: dict[str, QueryOrRef] = {}
|
|
444
421
|
_collect_raw_queries(authored, raw_queries)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
collectible_charts,
|
|
452
|
-
raw_queries,
|
|
453
|
-
metricflow_model_by_query,
|
|
454
|
-
base_dir,
|
|
455
|
-
)
|
|
422
|
+
collect_metricflow_channel_refs(
|
|
423
|
+
collectible_charts,
|
|
424
|
+
raw_queries,
|
|
425
|
+
metricflow_model_by_query,
|
|
426
|
+
base_dir,
|
|
427
|
+
)
|
|
456
428
|
except CompilationError as e:
|
|
457
429
|
return CompileResult(errors=[e.to_diagnostic(file=file)])
|
|
458
430
|
|
|
@@ -493,7 +465,7 @@ def compile_authored_face(
|
|
|
493
465
|
# host-supplied fallback) so inline chart queries synthesized during
|
|
494
466
|
# normalization inherit it too — not just the named queries built above.
|
|
495
467
|
# `sources` (board-global named source configs) rides alongside so an
|
|
496
|
-
# inline
|
|
468
|
+
# inline metricflow query on a chart resolves its model just
|
|
497
469
|
# like an up-top query.
|
|
498
470
|
parent_context={
|
|
499
471
|
"default_source": default_source,
|
|
@@ -996,11 +968,6 @@ def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
|
|
|
996
968
|
tabs=item.tabs,
|
|
997
969
|
)
|
|
998
970
|
nested_faces.append(tab_face)
|
|
999
|
-
elif isinstance(item, ForeachItem):
|
|
1000
|
-
# Recurse into foreach items to find any nested faces
|
|
1001
|
-
for foreach_subitem in item.foreach.items:
|
|
1002
|
-
if isinstance(foreach_subitem, AuthoredFace):
|
|
1003
|
-
nested_faces.append(foreach_subitem)
|
|
1004
971
|
|
|
1005
972
|
return nested_faces
|
|
1006
973
|
|
|
@@ -1070,7 +1037,7 @@ def _collect_raw_queries(face: AuthoredFace, registry: dict[str, QueryOrRef]) ->
|
|
|
1070
1037
|
|
|
1071
1038
|
Unlike `build_query_registry`, entries stay as authored AuthoredQuery/
|
|
1072
1039
|
QueryRef instances — used by the Surface B collect pass, which must
|
|
1073
|
-
mutate a
|
|
1040
|
+
mutate a semantic query's fields before `normalize_query` lowers it.
|
|
1074
1041
|
Cross-file QueryRef entries are included by reference but not resolved
|
|
1075
1042
|
(a ref's own file already ran its own collect pass at its own compile).
|
|
1076
1043
|
"""
|
|
@@ -1088,7 +1055,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
|
|
|
1088
1055
|
`build_chart_registry` only sees top-level named `charts:` — a chart authored
|
|
1089
1056
|
directly inline in `rows:`/`cols:`/`grid:`/`tabs:` (a bare AuthoredChart patch,
|
|
1090
1057
|
or `dict[str, AuthoredChart]`) never gets a registry entry. The Surface B
|
|
1091
|
-
collect pass (`
|
|
1058
|
+
collect pass (`collect_metricflow_channel_refs`) needs to see and mutate those
|
|
1092
1059
|
authored chart objects in place before normalization re-serializes them.
|
|
1093
1060
|
|
|
1094
1061
|
Keys are Python object ids (`id(chart_obj)`) — guaranteed unique per live
|
|
@@ -1099,7 +1066,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
|
|
|
1099
1066
|
def register(item: object) -> None:
|
|
1100
1067
|
# Handles both the bare-patch form (`- type: bar, ...`) and the named-dict
|
|
1101
1068
|
# form (`- my_chart: {type: bar, ...}`). Anything else (chart-name string,
|
|
1102
|
-
# nested face,
|
|
1069
|
+
# nested face, swatch strip) is not an inline chart and is skipped here.
|
|
1103
1070
|
# str(id(item)) is unique per live Python object — safer key than a
|
|
1104
1071
|
# position-only path, which collides when sibling nested faces share indices.
|
|
1105
1072
|
if isinstance(item, _BaseChartFields):
|
|
@@ -1148,7 +1115,7 @@ def _process_queries_for_registry(
|
|
|
1148
1115
|
registry: Registry to add queries to
|
|
1149
1116
|
base_dir: ProjectDirectory anchor for the face file (used to resolve sub-file refs).
|
|
1150
1117
|
default_source: Default source to apply to queries without explicit source
|
|
1151
|
-
sources: Named source config dicts. Consulted by
|
|
1118
|
+
sources: Named source config dicts. Consulted by metricflow queries at normalize time.
|
|
1152
1119
|
face_cache: The face-scope cache layer already folded down the nesting
|
|
1153
1120
|
chain by the caller.
|
|
1154
1121
|
|
|
@@ -1265,7 +1232,7 @@ def build_query_registry(
|
|
|
1265
1232
|
registry: Existing registry to add to
|
|
1266
1233
|
default_source: Default source to apply to queries without explicit source
|
|
1267
1234
|
sources: Named source config dicts (project + face level). Consulted by
|
|
1268
|
-
|
|
1235
|
+
metricflow queries, which resolve their source at normalize time.
|
|
1269
1236
|
face_cache: The cache layer this face inherits from the faces it is
|
|
1270
1237
|
nested in; its own `cache:` folds over it inside `_build_registry`.
|
|
1271
1238
|
|
|
@@ -1410,6 +1377,26 @@ def _resolve_ref_item(
|
|
|
1410
1377
|
return section[item_name], item_name, resolved_path, content
|
|
1411
1378
|
|
|
1412
1379
|
|
|
1380
|
+
def _face_level_default_source(
|
|
1381
|
+
content: dict[str, YamlValue], file_label: str
|
|
1382
|
+
) -> str | None:
|
|
1383
|
+
"""Validated face-level `source:` of a raw source-file mapping.
|
|
1384
|
+
|
|
1385
|
+
Cross-face-pulled queries inherit the declaring file's default source. The
|
|
1386
|
+
source file's content is a raw mapping here (never parsed into
|
|
1387
|
+
AuthoredFace), so the string-ness the model would enforce is checked here.
|
|
1388
|
+
"""
|
|
1389
|
+
source = content.get("source")
|
|
1390
|
+
if source is None:
|
|
1391
|
+
return None
|
|
1392
|
+
if not isinstance(source, str):
|
|
1393
|
+
raise CompilationError(
|
|
1394
|
+
f"Invalid face-level `source:` in {file_label}: "
|
|
1395
|
+
f"expected a source name string, got {type(source).__name__}"
|
|
1396
|
+
)
|
|
1397
|
+
return source
|
|
1398
|
+
|
|
1399
|
+
|
|
1413
1400
|
def load_from_reference(
|
|
1414
1401
|
reference: VariableRef | QueryRef | ChartRef,
|
|
1415
1402
|
base_dir: ProjectDirectory | None = None,
|
|
@@ -1464,7 +1451,7 @@ def load_from_reference(
|
|
|
1464
1451
|
else:
|
|
1465
1452
|
section_name = "charts"
|
|
1466
1453
|
|
|
1467
|
-
item_def, item_name, resolved_path,
|
|
1454
|
+
item_def, item_name, resolved_path, source_file_content = _resolve_ref_item(
|
|
1468
1455
|
reference.ref, section_name, base_dir, yaml_path
|
|
1469
1456
|
)
|
|
1470
1457
|
|
|
@@ -1472,6 +1459,7 @@ def load_from_reference(
|
|
|
1472
1459
|
return normalize_query(
|
|
1473
1460
|
item_name,
|
|
1474
1461
|
item_def,
|
|
1462
|
+
_face_level_default_source(source_file_content, str(resolved_path.relpath)),
|
|
1475
1463
|
base_dir=base_dir,
|
|
1476
1464
|
sources=sources,
|
|
1477
1465
|
cache_root=cache_root,
|
|
@@ -1550,10 +1538,13 @@ def _is_lexically_scoped_query_name(value: str) -> bool:
|
|
|
1550
1538
|
|
|
1551
1539
|
def _read_queries_section(
|
|
1552
1540
|
file_dir: ProjectDirectory, target_file_str: str, ref: str, chart_ref: str
|
|
1553
|
-
) -> tuple[ProjectPath, dict[str, YamlValue]]:
|
|
1541
|
+
) -> tuple[ProjectPath, dict[str, YamlValue], dict[str, YamlValue]]:
|
|
1554
1542
|
"""The `queries:` section of `target_file_str`, resolved relative to
|
|
1555
1543
|
`file_dir` — the *declaring* file's own directory, never the importer's.
|
|
1556
1544
|
|
|
1545
|
+
Also returns the file's full parsed mapping, so callers can read its
|
|
1546
|
+
face-level `source:` default for the queries declared in it.
|
|
1547
|
+
|
|
1557
1548
|
`ref` is the authored ref string this file part came from and `chart_ref`
|
|
1558
1549
|
the chart import that pulled it in; both are named in every failure, since
|
|
1559
1550
|
the author is looking at neither file when the resolution breaks.
|
|
@@ -1577,12 +1568,16 @@ def _read_queries_section(
|
|
|
1577
1568
|
# compile() rather than a diagnostic.
|
|
1578
1569
|
if target_content is not None and not isinstance(target_content, dict):
|
|
1579
1570
|
raise CompilationError(f"{where}, but {target_file_str} is not a YAML mapping.")
|
|
1580
|
-
|
|
1571
|
+
if target_content is None:
|
|
1572
|
+
raise CompilationError(
|
|
1573
|
+
f"{where}, but {target_file_str} has no queries: section."
|
|
1574
|
+
)
|
|
1575
|
+
target_queries = target_content.get("queries")
|
|
1581
1576
|
if not isinstance(target_queries, dict):
|
|
1582
1577
|
raise CompilationError(
|
|
1583
1578
|
f"{where}, but {target_file_str} has no queries: section."
|
|
1584
1579
|
)
|
|
1585
|
-
return target_path, target_queries
|
|
1580
|
+
return target_path, target_queries, target_content
|
|
1586
1581
|
|
|
1587
1582
|
|
|
1588
1583
|
def _resolve_cross_face_query(
|
|
@@ -1595,6 +1590,7 @@ def _resolve_cross_face_query(
|
|
|
1595
1590
|
resolving: set[tuple[str, str]],
|
|
1596
1591
|
chart_ref: str,
|
|
1597
1592
|
*,
|
|
1593
|
+
default_source: str | None,
|
|
1598
1594
|
sources: dict[str, Any],
|
|
1599
1595
|
cache_root: CachePatch | None,
|
|
1600
1596
|
face_cache: CachePatch,
|
|
@@ -1639,7 +1635,7 @@ def _resolve_cross_face_query(
|
|
|
1639
1635
|
target_file_str, target_name = raw_def["ref"].rsplit(".queries.", 1)
|
|
1640
1636
|
if not (target_file_str.endswith(".yml") or target_file_str.endswith(".yaml")):
|
|
1641
1637
|
target_file_str += ".yml"
|
|
1642
|
-
target_path, target_queries = _read_queries_section(
|
|
1638
|
+
target_path, target_queries, target_content = _read_queries_section(
|
|
1643
1639
|
file_dir, target_file_str, raw_def["ref"], chart_ref
|
|
1644
1640
|
)
|
|
1645
1641
|
target_key = _resolve_cross_face_query(
|
|
@@ -1651,6 +1647,7 @@ def _resolve_cross_face_query(
|
|
|
1651
1647
|
key_cache,
|
|
1652
1648
|
resolving,
|
|
1653
1649
|
chart_ref,
|
|
1650
|
+
default_source=_face_level_default_source(target_content, target_file_str),
|
|
1654
1651
|
sources=sources,
|
|
1655
1652
|
cache_root=cache_root,
|
|
1656
1653
|
face_cache=face_cache,
|
|
@@ -1676,6 +1673,7 @@ def _resolve_cross_face_query(
|
|
|
1676
1673
|
key_cache,
|
|
1677
1674
|
resolving,
|
|
1678
1675
|
chart_ref,
|
|
1676
|
+
default_source=default_source,
|
|
1679
1677
|
sources=sources,
|
|
1680
1678
|
cache_root=cache_root,
|
|
1681
1679
|
face_cache=face_cache,
|
|
@@ -1697,6 +1695,7 @@ def _resolve_cross_face_query(
|
|
|
1697
1695
|
pool[qualified_key] = normalize_query(
|
|
1698
1696
|
qualified_key,
|
|
1699
1697
|
raw_def,
|
|
1698
|
+
default_source,
|
|
1700
1699
|
base_dir=file_dir,
|
|
1701
1700
|
sources=sources,
|
|
1702
1701
|
cache_root=cache_root,
|
|
@@ -1746,6 +1745,7 @@ def _load_chart_ref_with_lexical_queries(
|
|
|
1746
1745
|
queries_section = {}
|
|
1747
1746
|
file_dir = resolved_path.parent
|
|
1748
1747
|
file_relpath = resolved_path.relpath
|
|
1748
|
+
source_default = _face_level_default_source(content, str(file_relpath))
|
|
1749
1749
|
key_cache: dict[tuple[str, str], str] = {}
|
|
1750
1750
|
resolving: set[tuple[str, str]] = set()
|
|
1751
1751
|
|
|
@@ -1769,12 +1769,13 @@ def _load_chart_ref_with_lexical_queries(
|
|
|
1769
1769
|
key_cache,
|
|
1770
1770
|
resolving,
|
|
1771
1771
|
chart_ref.ref,
|
|
1772
|
+
default_source=source_default,
|
|
1772
1773
|
sources=sources,
|
|
1773
1774
|
cache_root=cache_root,
|
|
1774
1775
|
face_cache=face_cache,
|
|
1775
1776
|
)
|
|
1776
1777
|
target_file_str, target_name = split_external_query_ref(stripped)
|
|
1777
|
-
target_path, target_queries = _read_queries_section(
|
|
1778
|
+
target_path, target_queries, target_content = _read_queries_section(
|
|
1778
1779
|
file_dir, target_file_str, stripped, chart_ref.ref
|
|
1779
1780
|
)
|
|
1780
1781
|
return _resolve_cross_face_query(
|
|
@@ -1786,6 +1787,7 @@ def _load_chart_ref_with_lexical_queries(
|
|
|
1786
1787
|
key_cache,
|
|
1787
1788
|
resolving,
|
|
1788
1789
|
chart_ref.ref,
|
|
1790
|
+
default_source=_face_level_default_source(target_content, target_file_str),
|
|
1789
1791
|
sources=sources,
|
|
1790
1792
|
cache_root=cache_root,
|
|
1791
1793
|
face_cache=face_cache,
|
|
@@ -27,8 +27,6 @@ from dataface.core.compile.models.factories import _PatchBase
|
|
|
27
27
|
from dataface.core.compile.models.query.authored import (
|
|
28
28
|
AuthoredCompactValuesQuery,
|
|
29
29
|
AuthoredHttpQuery,
|
|
30
|
-
AuthoredLookmlMergeQuery,
|
|
31
|
-
AuthoredLookmlQuery,
|
|
32
30
|
AuthoredMetricflowQuery,
|
|
33
31
|
AuthoredQuery,
|
|
34
32
|
AuthoredSchemaQuery,
|
|
@@ -42,7 +40,6 @@ from dataface.core.compile.models.source import (
|
|
|
42
40
|
DuckDBSourceConfig,
|
|
43
41
|
HttpSourceConfig,
|
|
44
42
|
JsonSourceConfig,
|
|
45
|
-
LookMLModelConfig,
|
|
46
43
|
MySQLSourceConfig,
|
|
47
44
|
ParquetSourceConfig,
|
|
48
45
|
PostgresSourceConfig,
|
|
@@ -133,9 +130,7 @@ def _is_authored_model(cls: Any) -> bool:
|
|
|
133
130
|
"""Return True if cls should be included in the IR.
|
|
134
131
|
|
|
135
132
|
Includes authored.* models, primitives, _PatchBase subclasses, compiled
|
|
136
|
-
style models from style/theme/,
|
|
137
|
-
dict[str, LookMLModelConfig] fields emit additionalProperties refs in JSON
|
|
138
|
-
Schema, and *Patch models from models.cache (CachePatch is the authored
|
|
133
|
+
style models from style/theme/, and *Patch models from models.cache (CachePatch is the authored
|
|
139
134
|
overlay for the cache block — same role as *StylePatch, different module).
|
|
140
135
|
"""
|
|
141
136
|
if not (isinstance(cls, type) and issubclass(cls, BaseModel)):
|
|
@@ -148,7 +143,6 @@ def _is_authored_model(cls: Any) -> bool:
|
|
|
148
143
|
or module.endswith("primitives")
|
|
149
144
|
or module.endswith("style.theme")
|
|
150
145
|
or ".style.theme." in module
|
|
151
|
-
or issubclass(cls, LookMLModelConfig)
|
|
152
146
|
or (module.endswith(".models.cache") and cls.__name__.endswith("Patch"))
|
|
153
147
|
)
|
|
154
148
|
|
|
@@ -576,8 +570,6 @@ def introspect() -> AuthorableSchema:
|
|
|
576
570
|
AuthoredValuesQuery,
|
|
577
571
|
AuthoredCompactValuesQuery,
|
|
578
572
|
AuthoredSchemaQuery,
|
|
579
|
-
AuthoredLookmlQuery,
|
|
580
|
-
AuthoredLookmlMergeQuery,
|
|
581
573
|
PaginationConfig,
|
|
582
574
|
PostgresSourceConfig,
|
|
583
575
|
SnowflakeSourceConfig,
|
|
@@ -591,7 +583,6 @@ def introspect() -> AuthorableSchema:
|
|
|
591
583
|
JsonSourceConfig,
|
|
592
584
|
HttpSourceConfig,
|
|
593
585
|
DbtProfileSourceConfig,
|
|
594
|
-
LookMLModelConfig,
|
|
595
586
|
# Compiled style root — walks style/theme/ types so inherit-chain metadata
|
|
596
587
|
# (Inherit/InheritSlot markers) is visible in the schema IR.
|
|
597
588
|
Style,
|
|
@@ -623,8 +614,6 @@ def introspect() -> AuthorableSchema:
|
|
|
623
614
|
"AuthoredValuesQuery",
|
|
624
615
|
"AuthoredCompactValuesQuery",
|
|
625
616
|
"AuthoredSchemaQuery",
|
|
626
|
-
"AuthoredLookmlQuery",
|
|
627
|
-
"AuthoredLookmlMergeQuery",
|
|
628
617
|
)
|
|
629
618
|
if all(name in collected for name in query_variants):
|
|
630
619
|
collected["AuthoredQuery"] = AuthorableModel(
|
|
@@ -632,7 +621,7 @@ def introspect() -> AuthorableSchema:
|
|
|
632
621
|
doc=(
|
|
633
622
|
"Union of per-type authored query shapes. "
|
|
634
623
|
"type: is inferred from key presence when omitted (bare SQL string, "
|
|
635
|
-
"sql: -> sql; metrics: -> metricflow;
|
|
624
|
+
"sql: -> sql; metrics: -> metricflow; "
|
|
636
625
|
"url: -> http; rows:/values: -> values)."
|
|
637
626
|
),
|
|
638
627
|
fields=[],
|
|
@@ -168,7 +168,7 @@ class _BaseChartFields(BaseModel):
|
|
|
168
168
|
str | None,
|
|
169
169
|
Field(
|
|
170
170
|
default=None,
|
|
171
|
-
description="Semantic-layer entry point, two dot-joined names: '
|
|
171
|
+
description="Semantic-layer entry point, two dot-joined names: 'source.semantic_model' for a dbt_profile (MetricFlow) source. Channel fields name the model's fields directly; roles are inferred from the dbt semantic manifest. Mutually exclusive with `query:`.",
|
|
172
172
|
),
|
|
173
173
|
]
|
|
174
174
|
|
|
@@ -30,6 +30,7 @@ from pydantic import Discriminator
|
|
|
30
30
|
|
|
31
31
|
from ._base import (
|
|
32
32
|
NON_ASPECT_RATIO_TYPES,
|
|
33
|
+
PAINTS_MARKS,
|
|
33
34
|
SVG_LAYOUT_PADDED_TYPES,
|
|
34
35
|
ChartDependencies,
|
|
35
36
|
_BaseChartFields,
|
|
@@ -75,6 +76,7 @@ model. Each variant's ``type:`` Literal is the discriminator key.
|
|
|
75
76
|
__all__ = [
|
|
76
77
|
# constants and helpers
|
|
77
78
|
"NON_ASPECT_RATIO_TYPES",
|
|
79
|
+
"PAINTS_MARKS",
|
|
78
80
|
"SVG_LAYOUT_PADDED_TYPES",
|
|
79
81
|
"ChartDependencies",
|
|
80
82
|
# discriminated union
|
|
@@ -189,6 +189,32 @@ __all__ = [
|
|
|
189
189
|
NON_ASPECT_RATIO_TYPES = frozenset({"table", "kpi", "spark_bar", "callout"})
|
|
190
190
|
SVG_LAYOUT_PADDED_TYPES = frozenset({"table", "kpi", "spark_bar"})
|
|
191
191
|
|
|
192
|
+
# Chart-type strings dispatched through get_emitter() (render/chart/emitters/__init__.py)
|
|
193
|
+
# — the families that emit Vega-Lite marks. The complement (table, kpi, spark_bar,
|
|
194
|
+
# callout) never paints a mark; it is drawn by a hand-crafted SVG renderer instead.
|
|
195
|
+
# This is a different fact from SVG_LAYOUT_PADDED_TYPES above (that one is about
|
|
196
|
+
# layout padding, not mark-painting) even though their complements happen to
|
|
197
|
+
# coincide today — don't fold them into one set.
|
|
198
|
+
#
|
|
199
|
+
# These are RESOLVED-stage chart_type strings (ResolvedChart.chart_type), not
|
|
200
|
+
# normalized-stage ones. The normalized `.type` vocabulary also contains
|
|
201
|
+
# `donut`, `map`, and `bubble_map` — they collapse to `pie`/`geoshape`/
|
|
202
|
+
# `point_map` only at resolve. Applying this set to a normalized `.type` would
|
|
203
|
+
# silently misclassify those three as non-painting.
|
|
204
|
+
PAINTS_MARKS = frozenset(
|
|
205
|
+
{
|
|
206
|
+
"bar",
|
|
207
|
+
"histogram",
|
|
208
|
+
"line",
|
|
209
|
+
"area",
|
|
210
|
+
"scatter",
|
|
211
|
+
"heatmap",
|
|
212
|
+
"pie",
|
|
213
|
+
"geoshape",
|
|
214
|
+
"point_map",
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
192
218
|
|
|
193
219
|
@dataclass(frozen=True)
|
|
194
220
|
class ChartDependencies:
|
|
@@ -217,6 +217,11 @@ class ExecutionConfig(ConfigNode):
|
|
|
217
217
|
max_query_duration_seconds: int = Field(
|
|
218
218
|
gt=0, description="Maximum seconds a single query may run (must be > 0)."
|
|
219
219
|
)
|
|
220
|
+
# Hard cap on files matched per glob pattern in file sources. Error is raised
|
|
221
|
+
# before any file is read so a runaway glob fails fast. Override in dataface.yml.
|
|
222
|
+
max_glob_file_count: int = Field(
|
|
223
|
+
gt=0, description="Maximum files a single glob may match (must be > 0)."
|
|
224
|
+
)
|
|
220
225
|
# sqlglot uses different dialect names than Dataface's public-facing dialect strings.
|
|
221
226
|
# This mapping normalizes Dataface names to sqlglot equivalents before parsing.
|
|
222
227
|
dialect_aliases: dict[str, str]
|