dataface 0.3.2.dev22__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/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +273 -239
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +113 -0
- dataface/agent_api/validate.py +8 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +26 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +25 -81
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +22 -33
- dataface/core/__init__.py +73 -28
- dataface/core/aliases.py +27 -0
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +161 -96
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/migrations.py +112 -9
- 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/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +78 -164
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +10 -154
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +24 -513
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +108 -3
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +27 -19
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +49 -40
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +9 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +24 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +12 -2
- dataface/core/diagnostics/codes_compile.py +17 -33
- dataface/core/diagnostics/codes_execute.py +149 -0
- dataface/core/diagnostics/codes_render.py +144 -31
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +94 -19
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -7
- dataface/core/project.py +19 -3
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +100 -46
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +97 -32
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +313 -238
- dataface/core/render/numeral_expr.py +80 -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/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +9 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
- 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.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""CSS/SVG color-string validation.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lives in core (below compile/execute/render) so both compile (pie-attachment
|
|
4
|
+
table style resolution) and render (KPI/table SVG emission) can validate a
|
|
5
|
+
foreign color string without either reaching into the other's layer.
|
|
4
6
|
"""
|
|
5
7
|
|
|
6
8
|
from __future__ import annotations
|
|
@@ -8,11 +10,24 @@ from __future__ import annotations
|
|
|
8
10
|
import re
|
|
9
11
|
from typing import overload
|
|
10
12
|
|
|
11
|
-
from dataface.core.
|
|
13
|
+
from dataface.core.diagnostics.base import DatafaceError
|
|
12
14
|
|
|
13
15
|
_CSS_HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
class InvalidColorError(DatafaceError):
|
|
19
|
+
"""A color string is neither a valid hex color nor a recognized CSS keyword."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str):
|
|
22
|
+
self.message = message
|
|
23
|
+
self.fields = {}
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
if self.code is None:
|
|
26
|
+
from dataface.core.diagnostics.codes_unknown import ERR_INTERNAL
|
|
27
|
+
|
|
28
|
+
self.code = ERR_INTERNAL
|
|
29
|
+
|
|
30
|
+
|
|
16
31
|
def is_sanitizable_color(color: str) -> bool:
|
|
17
32
|
"""Return True if ``color`` is accepted by sanitize_color (hex or transparent/none)."""
|
|
18
33
|
return bool(_CSS_HEX_COLOR_PATTERN.match(color)) or color.lower() in {
|
|
@@ -39,7 +54,7 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
|
|
|
39
54
|
inherited fill).
|
|
40
55
|
|
|
41
56
|
``fallback`` is returned when ``color`` is None or empty. A non-None
|
|
42
|
-
``color`` that is not a valid hex or keyword raises ``
|
|
57
|
+
``color`` that is not a valid hex or keyword raises ``InvalidColorError``
|
|
43
58
|
immediately — authored colors that are not valid are a configuration error,
|
|
44
59
|
not a case for silent defaulting.
|
|
45
60
|
"""
|
|
@@ -49,4 +64,4 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
|
|
|
49
64
|
return color
|
|
50
65
|
if color.lower() in {"transparent", "none"}:
|
|
51
66
|
return "transparent"
|
|
52
|
-
raise
|
|
67
|
+
raise InvalidColorError(f"Invalid color value: {color!r}")
|
|
@@ -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,10 +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
|
|
86
|
+
from dataface.core.compile.normalize_queries import split_external_query_ref
|
|
90
87
|
from dataface.core.compile.normalize_variables import (
|
|
91
88
|
VariableReferenceErrors,
|
|
92
89
|
synthetic_query_name,
|
|
@@ -100,6 +97,7 @@ from dataface.core.compile.parser import (
|
|
|
100
97
|
)
|
|
101
98
|
from dataface.core.compile.source_map import (
|
|
102
99
|
LiteralBlock,
|
|
100
|
+
build_container_paths,
|
|
103
101
|
build_literal_blocks,
|
|
104
102
|
build_source_map,
|
|
105
103
|
stamp_diagnostics,
|
|
@@ -163,6 +161,12 @@ class CompileResult:
|
|
|
163
161
|
diagnostics (chart-render errors, render warnings, query
|
|
164
162
|
diagnostics) reuse this same map via ``stamp_diagnostics`` instead
|
|
165
163
|
of each doing its own position resolution.
|
|
164
|
+
container_paths: Dotted paths addressing a mapping/sequence rather
|
|
165
|
+
than a scalar, built alongside ``source_map``. Lets the stamping
|
|
166
|
+
pass collapse a container's whole-body range to its key line, so a
|
|
167
|
+
diagnostic about a chart marks the line naming it instead of every
|
|
168
|
+
line inside. Carried on the result for the same reason as
|
|
169
|
+
``sql_blocks``: render-side stamping reuses it without recompiling.
|
|
166
170
|
sql_blocks: Dotted-path -> LiteralBlock for every literal (``|``)
|
|
167
171
|
block scalar, built alongside ``source_map``. Render-side
|
|
168
172
|
diagnostics pass this to ``stamp_diagnostics`` too, so a query
|
|
@@ -188,6 +192,7 @@ class CompileResult:
|
|
|
188
192
|
meta_lint: MetaLintConfig | None = None
|
|
189
193
|
source_map: dict[str, SourceRange] = field(default_factory=dict)
|
|
190
194
|
sql_blocks: dict[str, LiteralBlock] = field(default_factory=dict)
|
|
195
|
+
container_paths: frozenset[str] = frozenset()
|
|
191
196
|
|
|
192
197
|
@property
|
|
193
198
|
def success(self) -> bool:
|
|
@@ -211,11 +216,13 @@ def _stamp_compile_result(
|
|
|
211
216
|
return result
|
|
212
217
|
source_map = build_source_map(yaml_content, file)
|
|
213
218
|
sql_blocks = build_literal_blocks(yaml_content)
|
|
219
|
+
containers = build_container_paths(yaml_content)
|
|
214
220
|
result.source_map = source_map
|
|
215
221
|
result.sql_blocks = sql_blocks
|
|
216
|
-
|
|
217
|
-
stamp_diagnostics(result.
|
|
218
|
-
stamp_diagnostics(result.
|
|
222
|
+
result.container_paths = containers
|
|
223
|
+
stamp_diagnostics(result.errors, source_map, sql_blocks, containers)
|
|
224
|
+
stamp_diagnostics(result.warnings, source_map, sql_blocks, containers)
|
|
225
|
+
stamp_diagnostics(result.suppressed_warnings, source_map, sql_blocks, containers)
|
|
219
226
|
return result
|
|
220
227
|
|
|
221
228
|
|
|
@@ -315,7 +322,7 @@ def compile_authored_face(
|
|
|
315
322
|
# ════════════════════════════════════════════════════════════════════
|
|
316
323
|
# STEP 3b: Build Chart Registry (moved ahead of queries — Surface B's
|
|
317
324
|
# collect pass below needs charts to find dotted-ref channel bindings
|
|
318
|
-
# before
|
|
325
|
+
# before semantic queries lower to SQL).
|
|
319
326
|
# ════════════════════════════════════════════════════════════════════
|
|
320
327
|
# Pool that imported charts' own query chains resolve into, lexically
|
|
321
328
|
# scoped to each chart's source face (compile/AGENTS.md's "Cross-face
|
|
@@ -342,11 +349,10 @@ def compile_authored_face(
|
|
|
342
349
|
# ════════════════════════════════════════════════════════════════════
|
|
343
350
|
# STEP 3b.5: Desugar chart-level `model:` sugar into a synthesized named
|
|
344
351
|
# query, before Surface B channel collection (below) so the synthesized
|
|
345
|
-
# query is visible to that pass. `model:
|
|
346
|
-
# becomes a
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
# 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.
|
|
350
356
|
# ════════════════════════════════════════════════════════════════════
|
|
351
357
|
metricflow_model_by_query: dict[str, str] = {}
|
|
352
358
|
try:
|
|
@@ -375,33 +381,22 @@ def compile_authored_face(
|
|
|
375
381
|
if len(_parts) != 2 or not all(_parts):
|
|
376
382
|
raise CompilationError(
|
|
377
383
|
f"chart '{_chart_key}' has `model: {_model_value!r}` — expected "
|
|
378
|
-
"`
|
|
379
|
-
"joined by a dot)."
|
|
384
|
+
"`source.semantic_model` (a dbt_profile source name and "
|
|
385
|
+
"semantic model name joined by a dot)."
|
|
380
386
|
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
_lookml_source is None
|
|
386
|
-
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)
|
|
387
391
|
and _source_cfg.get("type") == "dbt_profile"
|
|
388
|
-
)
|
|
389
|
-
if _lookml_source is None and not _is_metricflow:
|
|
392
|
+
):
|
|
390
393
|
raise CompilationError(
|
|
391
394
|
f"chart '{_chart_key}' has `model: {_model_value}` naming "
|
|
392
|
-
f"unknown
|
|
393
|
-
"'models:' map, and it is not a dbt_profile source."
|
|
395
|
+
f"unknown source '{_source_name}' — it is not a dbt_profile source."
|
|
394
396
|
)
|
|
395
397
|
_synth_name = synthetic_query_name("model_query", _chart_key)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
source=_model_name
|
|
399
|
-
)
|
|
400
|
-
metricflow_model_by_query[_synth_name] = _explore_name
|
|
401
|
-
else:
|
|
402
|
-
authored.queries[_synth_name] = AuthoredLookmlQuery(
|
|
403
|
-
source=_model_name, explore=_explore_name
|
|
404
|
-
)
|
|
398
|
+
authored.queries[_synth_name] = AuthoredMetricflowQuery(source=_source_name)
|
|
399
|
+
metricflow_model_by_query[_synth_name] = _semantic_model_name
|
|
405
400
|
# Mutate in place: _BaseChartFields declares query + model; plain
|
|
406
401
|
# dicts use key assignment. Non-_BaseChartFields BaseModels (e.g.
|
|
407
402
|
# CalloutChart) have no `model:` field and are skipped above.
|
|
@@ -415,31 +410,21 @@ def compile_authored_face(
|
|
|
415
410
|
return CompileResult(errors=[e.to_diagnostic(file=file)])
|
|
416
411
|
|
|
417
412
|
# ════════════════════════════════════════════════════════════════════
|
|
418
|
-
# STEP 3c: Collect
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
# 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
|
|
422
416
|
# metrics/dimensions/time_grain.
|
|
423
417
|
# ════════════════════════════════════════════════════════════════════
|
|
424
|
-
|
|
425
|
-
for _cfg in sources_registry.values():
|
|
426
|
-
if isinstance(_cfg, dict) and isinstance(_cfg.get("models"), dict):
|
|
427
|
-
lookml_model_names.update(_cfg["models"].keys())
|
|
428
|
-
if lookml_model_names or metricflow_model_by_query:
|
|
418
|
+
if metricflow_model_by_query:
|
|
429
419
|
try:
|
|
430
420
|
raw_queries: dict[str, QueryOrRef] = {}
|
|
431
421
|
_collect_raw_queries(authored, raw_queries)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
collectible_charts,
|
|
439
|
-
raw_queries,
|
|
440
|
-
metricflow_model_by_query,
|
|
441
|
-
base_dir,
|
|
442
|
-
)
|
|
422
|
+
collect_metricflow_channel_refs(
|
|
423
|
+
collectible_charts,
|
|
424
|
+
raw_queries,
|
|
425
|
+
metricflow_model_by_query,
|
|
426
|
+
base_dir,
|
|
427
|
+
)
|
|
443
428
|
except CompilationError as e:
|
|
444
429
|
return CompileResult(errors=[e.to_diagnostic(file=file)])
|
|
445
430
|
|
|
@@ -480,7 +465,7 @@ def compile_authored_face(
|
|
|
480
465
|
# host-supplied fallback) so inline chart queries synthesized during
|
|
481
466
|
# normalization inherit it too — not just the named queries built above.
|
|
482
467
|
# `sources` (board-global named source configs) rides alongside so an
|
|
483
|
-
# inline
|
|
468
|
+
# inline metricflow query on a chart resolves its model just
|
|
484
469
|
# like an up-top query.
|
|
485
470
|
parent_context={
|
|
486
471
|
"default_source": default_source,
|
|
@@ -983,11 +968,6 @@ def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
|
|
|
983
968
|
tabs=item.tabs,
|
|
984
969
|
)
|
|
985
970
|
nested_faces.append(tab_face)
|
|
986
|
-
elif isinstance(item, ForeachItem):
|
|
987
|
-
# Recurse into foreach items to find any nested faces
|
|
988
|
-
for foreach_subitem in item.foreach.items:
|
|
989
|
-
if isinstance(foreach_subitem, AuthoredFace):
|
|
990
|
-
nested_faces.append(foreach_subitem)
|
|
991
971
|
|
|
992
972
|
return nested_faces
|
|
993
973
|
|
|
@@ -1057,7 +1037,7 @@ def _collect_raw_queries(face: AuthoredFace, registry: dict[str, QueryOrRef]) ->
|
|
|
1057
1037
|
|
|
1058
1038
|
Unlike `build_query_registry`, entries stay as authored AuthoredQuery/
|
|
1059
1039
|
QueryRef instances — used by the Surface B collect pass, which must
|
|
1060
|
-
mutate a
|
|
1040
|
+
mutate a semantic query's fields before `normalize_query` lowers it.
|
|
1061
1041
|
Cross-file QueryRef entries are included by reference but not resolved
|
|
1062
1042
|
(a ref's own file already ran its own collect pass at its own compile).
|
|
1063
1043
|
"""
|
|
@@ -1075,7 +1055,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
|
|
|
1075
1055
|
`build_chart_registry` only sees top-level named `charts:` — a chart authored
|
|
1076
1056
|
directly inline in `rows:`/`cols:`/`grid:`/`tabs:` (a bare AuthoredChart patch,
|
|
1077
1057
|
or `dict[str, AuthoredChart]`) never gets a registry entry. The Surface B
|
|
1078
|
-
collect pass (`
|
|
1058
|
+
collect pass (`collect_metricflow_channel_refs`) needs to see and mutate those
|
|
1079
1059
|
authored chart objects in place before normalization re-serializes them.
|
|
1080
1060
|
|
|
1081
1061
|
Keys are Python object ids (`id(chart_obj)`) — guaranteed unique per live
|
|
@@ -1086,7 +1066,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
|
|
|
1086
1066
|
def register(item: object) -> None:
|
|
1087
1067
|
# Handles both the bare-patch form (`- type: bar, ...`) and the named-dict
|
|
1088
1068
|
# form (`- my_chart: {type: bar, ...}`). Anything else (chart-name string,
|
|
1089
|
-
# nested face,
|
|
1069
|
+
# nested face, swatch strip) is not an inline chart and is skipped here.
|
|
1090
1070
|
# str(id(item)) is unique per live Python object — safer key than a
|
|
1091
1071
|
# position-only path, which collides when sibling nested faces share indices.
|
|
1092
1072
|
if isinstance(item, _BaseChartFields):
|
|
@@ -1135,7 +1115,7 @@ def _process_queries_for_registry(
|
|
|
1135
1115
|
registry: Registry to add queries to
|
|
1136
1116
|
base_dir: ProjectDirectory anchor for the face file (used to resolve sub-file refs).
|
|
1137
1117
|
default_source: Default source to apply to queries without explicit source
|
|
1138
|
-
sources: Named source config dicts. Consulted by
|
|
1118
|
+
sources: Named source config dicts. Consulted by metricflow queries at normalize time.
|
|
1139
1119
|
face_cache: The face-scope cache layer already folded down the nesting
|
|
1140
1120
|
chain by the caller.
|
|
1141
1121
|
|
|
@@ -1252,7 +1232,7 @@ def build_query_registry(
|
|
|
1252
1232
|
registry: Existing registry to add to
|
|
1253
1233
|
default_source: Default source to apply to queries without explicit source
|
|
1254
1234
|
sources: Named source config dicts (project + face level). Consulted by
|
|
1255
|
-
|
|
1235
|
+
metricflow queries, which resolve their source at normalize time.
|
|
1256
1236
|
face_cache: The cache layer this face inherits from the faces it is
|
|
1257
1237
|
nested in; its own `cache:` folds over it inside `_build_registry`.
|
|
1258
1238
|
|
|
@@ -1397,6 +1377,26 @@ def _resolve_ref_item(
|
|
|
1397
1377
|
return section[item_name], item_name, resolved_path, content
|
|
1398
1378
|
|
|
1399
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
|
+
|
|
1400
1400
|
def load_from_reference(
|
|
1401
1401
|
reference: VariableRef | QueryRef | ChartRef,
|
|
1402
1402
|
base_dir: ProjectDirectory | None = None,
|
|
@@ -1451,7 +1451,7 @@ def load_from_reference(
|
|
|
1451
1451
|
else:
|
|
1452
1452
|
section_name = "charts"
|
|
1453
1453
|
|
|
1454
|
-
item_def, item_name, resolved_path,
|
|
1454
|
+
item_def, item_name, resolved_path, source_file_content = _resolve_ref_item(
|
|
1455
1455
|
reference.ref, section_name, base_dir, yaml_path
|
|
1456
1456
|
)
|
|
1457
1457
|
|
|
@@ -1459,6 +1459,7 @@ def load_from_reference(
|
|
|
1459
1459
|
return normalize_query(
|
|
1460
1460
|
item_name,
|
|
1461
1461
|
item_def,
|
|
1462
|
+
_face_level_default_source(source_file_content, str(resolved_path.relpath)),
|
|
1462
1463
|
base_dir=base_dir,
|
|
1463
1464
|
sources=sources,
|
|
1464
1465
|
cache_root=cache_root,
|
|
@@ -1523,20 +1524,60 @@ def pull_cross_face_query_chain(
|
|
|
1523
1524
|
|
|
1524
1525
|
|
|
1525
1526
|
def _is_lexically_scoped_query_name(value: str) -> bool:
|
|
1526
|
-
"""True when a chart's `query:`/`layers[].query` string
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1527
|
+
"""True when a chart's `query:`/`layers[].query` string must resolve
|
|
1528
|
+
against the *source* face rather than the importer: a bare name declared
|
|
1529
|
+
in the source face's own `queries:` section, or a `<file>#<name>`
|
|
1530
|
+
external ref anchored at the source face's own directory. Excluded is
|
|
1531
|
+
inline SQL (self-contained) and a full `<file>.queries.<name>` cross-file
|
|
1532
|
+
ref written directly on the chart itself (an already-ambiguous, unrelated
|
|
1533
|
+
edge case for local charts too — left untouched).
|
|
1533
1534
|
"""
|
|
1534
1535
|
stripped = value.strip()
|
|
1535
|
-
return (
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1536
|
+
return ".queries." not in stripped and not looks_like_sql(stripped)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def _read_queries_section(
|
|
1540
|
+
file_dir: ProjectDirectory, target_file_str: str, ref: str, chart_ref: str
|
|
1541
|
+
) -> tuple[ProjectPath, dict[str, YamlValue], dict[str, YamlValue]]:
|
|
1542
|
+
"""The `queries:` section of `target_file_str`, resolved relative to
|
|
1543
|
+
`file_dir` — the *declaring* file's own directory, never the importer's.
|
|
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
|
+
|
|
1548
|
+
`ref` is the authored ref string this file part came from and `chart_ref`
|
|
1549
|
+
the chart import that pulled it in; both are named in every failure, since
|
|
1550
|
+
the author is looking at neither file when the resolution breaks.
|
|
1551
|
+
"""
|
|
1552
|
+
where = f"Chart ref '{chart_ref}' requires '{ref}'"
|
|
1553
|
+
try:
|
|
1554
|
+
target_path = file_dir / target_file_str
|
|
1555
|
+
except ValueError as e:
|
|
1556
|
+
raise CompilationError(f"{where}: {e}") from e
|
|
1557
|
+
if not target_path.exists():
|
|
1558
|
+
raise CompilationError(
|
|
1559
|
+
f"{where}, but {target_file_str} does not exist "
|
|
1560
|
+
f"(resolved relative to {file_dir.relpath})."
|
|
1561
|
+
)
|
|
1562
|
+
try:
|
|
1563
|
+
target_content = target_path.read_yaml()
|
|
1564
|
+
except (OSError, yaml.YAMLError) as e:
|
|
1565
|
+
raise CompilationError(f"{where}: failed to load {target_file_str}: {e}") from e
|
|
1566
|
+
# read_yaml is -> Any: a file whose top level is a list or scalar parses
|
|
1567
|
+
# fine and would only fail at the .get below, as an AttributeError escaping
|
|
1568
|
+
# compile() rather than a diagnostic.
|
|
1569
|
+
if target_content is not None and not isinstance(target_content, dict):
|
|
1570
|
+
raise CompilationError(f"{where}, but {target_file_str} is not a YAML mapping.")
|
|
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")
|
|
1576
|
+
if not isinstance(target_queries, dict):
|
|
1577
|
+
raise CompilationError(
|
|
1578
|
+
f"{where}, but {target_file_str} has no queries: section."
|
|
1579
|
+
)
|
|
1580
|
+
return target_path, target_queries, target_content
|
|
1540
1581
|
|
|
1541
1582
|
|
|
1542
1583
|
def _resolve_cross_face_query(
|
|
@@ -1549,6 +1590,7 @@ def _resolve_cross_face_query(
|
|
|
1549
1590
|
resolving: set[tuple[str, str]],
|
|
1550
1591
|
chart_ref: str,
|
|
1551
1592
|
*,
|
|
1593
|
+
default_source: str | None,
|
|
1552
1594
|
sources: dict[str, Any],
|
|
1553
1595
|
cache_root: CachePatch | None,
|
|
1554
1596
|
face_cache: CachePatch,
|
|
@@ -1593,19 +1635,9 @@ def _resolve_cross_face_query(
|
|
|
1593
1635
|
target_file_str, target_name = raw_def["ref"].rsplit(".queries.", 1)
|
|
1594
1636
|
if not (target_file_str.endswith(".yml") or target_file_str.endswith(".yaml")):
|
|
1595
1637
|
target_file_str += ".yml"
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
raise CompilationError(str(e)) from e
|
|
1600
|
-
if not target_path.exists():
|
|
1601
|
-
raise CompilationError(f"Referenced file not found: {target_file_str}")
|
|
1602
|
-
try:
|
|
1603
|
-
target_content = target_path.read_yaml()
|
|
1604
|
-
except (OSError, yaml.YAMLError) as e:
|
|
1605
|
-
raise CompilationError(f"Failed to load {target_file_str}: {e}") from e
|
|
1606
|
-
target_queries = target_content.get("queries") if target_content else None
|
|
1607
|
-
if not isinstance(target_queries, dict):
|
|
1608
|
-
raise CompilationError(f"No queries found in {target_file_str}")
|
|
1638
|
+
target_path, target_queries, target_content = _read_queries_section(
|
|
1639
|
+
file_dir, target_file_str, raw_def["ref"], chart_ref
|
|
1640
|
+
)
|
|
1609
1641
|
target_key = _resolve_cross_face_query(
|
|
1610
1642
|
target_name,
|
|
1611
1643
|
target_path.parent,
|
|
@@ -1615,6 +1647,7 @@ def _resolve_cross_face_query(
|
|
|
1615
1647
|
key_cache,
|
|
1616
1648
|
resolving,
|
|
1617
1649
|
chart_ref,
|
|
1650
|
+
default_source=_face_level_default_source(target_content, target_file_str),
|
|
1618
1651
|
sources=sources,
|
|
1619
1652
|
cache_root=cache_root,
|
|
1620
1653
|
face_cache=face_cache,
|
|
@@ -1640,6 +1673,7 @@ def _resolve_cross_face_query(
|
|
|
1640
1673
|
key_cache,
|
|
1641
1674
|
resolving,
|
|
1642
1675
|
chart_ref,
|
|
1676
|
+
default_source=default_source,
|
|
1643
1677
|
sources=sources,
|
|
1644
1678
|
cache_root=cache_root,
|
|
1645
1679
|
face_cache=face_cache,
|
|
@@ -1661,6 +1695,7 @@ def _resolve_cross_face_query(
|
|
|
1661
1695
|
pool[qualified_key] = normalize_query(
|
|
1662
1696
|
qualified_key,
|
|
1663
1697
|
raw_def,
|
|
1698
|
+
default_source,
|
|
1664
1699
|
base_dir=file_dir,
|
|
1665
1700
|
sources=sources,
|
|
1666
1701
|
cache_root=cache_root,
|
|
@@ -1710,19 +1745,49 @@ def _load_chart_ref_with_lexical_queries(
|
|
|
1710
1745
|
queries_section = {}
|
|
1711
1746
|
file_dir = resolved_path.parent
|
|
1712
1747
|
file_relpath = resolved_path.relpath
|
|
1748
|
+
source_default = _face_level_default_source(content, str(file_relpath))
|
|
1713
1749
|
key_cache: dict[tuple[str, str], str] = {}
|
|
1714
1750
|
resolving: set[tuple[str, str]] = set()
|
|
1715
1751
|
|
|
1716
|
-
def resolve_name(
|
|
1752
|
+
def resolve_name(name: str) -> str:
|
|
1753
|
+
"""A bare name resolves in the source face's own `queries:`; a
|
|
1754
|
+
`<file>#<name>` external ref resolves in that file's, reached relative
|
|
1755
|
+
to the source face's directory. Either way the resulting query lands
|
|
1756
|
+
in `cross_face_queries` under a key qualified by the file it was
|
|
1757
|
+
actually declared in — so an importer `#`-ref spelled identically to
|
|
1758
|
+
the imported chart's, but pointing at a different file, stays its own
|
|
1759
|
+
query.
|
|
1760
|
+
"""
|
|
1761
|
+
stripped = name.strip()
|
|
1762
|
+
if "#" not in stripped:
|
|
1763
|
+
return _resolve_cross_face_query(
|
|
1764
|
+
stripped,
|
|
1765
|
+
file_dir,
|
|
1766
|
+
file_relpath,
|
|
1767
|
+
queries_section,
|
|
1768
|
+
cross_face_queries,
|
|
1769
|
+
key_cache,
|
|
1770
|
+
resolving,
|
|
1771
|
+
chart_ref.ref,
|
|
1772
|
+
default_source=source_default,
|
|
1773
|
+
sources=sources,
|
|
1774
|
+
cache_root=cache_root,
|
|
1775
|
+
face_cache=face_cache,
|
|
1776
|
+
)
|
|
1777
|
+
target_file_str, target_name = split_external_query_ref(stripped)
|
|
1778
|
+
target_path, target_queries, target_content = _read_queries_section(
|
|
1779
|
+
file_dir, target_file_str, stripped, chart_ref.ref
|
|
1780
|
+
)
|
|
1717
1781
|
return _resolve_cross_face_query(
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1782
|
+
target_name,
|
|
1783
|
+
target_path.parent,
|
|
1784
|
+
target_path.relpath,
|
|
1785
|
+
target_queries,
|
|
1722
1786
|
cross_face_queries,
|
|
1723
1787
|
key_cache,
|
|
1724
1788
|
resolving,
|
|
1725
1789
|
chart_ref.ref,
|
|
1790
|
+
default_source=_face_level_default_source(target_content, target_file_str),
|
|
1726
1791
|
sources=sources,
|
|
1727
1792
|
cache_root=cache_root,
|
|
1728
1793
|
face_cache=face_cache,
|
|
@@ -53,11 +53,16 @@ def axis_offset(charts_style: ResolvedChartsStyle, style: DataTableStyle) -> flo
|
|
|
53
53
|
# Visibility is read from the fully-merged cascade (includes Layer-6 authored-
|
|
54
54
|
# label flips) so both label and title heights reflect the actual rendered state.
|
|
55
55
|
merged = resolved_axis_style(charts_style, "axis_x", "band")
|
|
56
|
+
# labels.padding is always required by build_resolved_axis (the shared
|
|
57
|
+
# ResolvedAxisElementStyle dataclass types it Optional only to let title
|
|
58
|
+
# leave it unset — see AxisTitleStyle).
|
|
59
|
+
assert merged.labels.padding is not None, "axis_x.labels.padding unset"
|
|
56
60
|
label_h = (
|
|
57
|
-
merged.
|
|
58
|
-
if merged.
|
|
61
|
+
merged.labels.padding + merged.labels.font.size * style.label_max_lines
|
|
62
|
+
if merged.labels.visible is not False and merged.labels.font.size > 0
|
|
59
63
|
else 0.0
|
|
60
64
|
)
|
|
65
|
+
assert merged.title.padding is not None, "axis_x.title.padding unset"
|
|
61
66
|
title_h = (
|
|
62
67
|
merged.title.padding + merged.title.font.size
|
|
63
68
|
if merged.title.visible is not False and merged.title.font.size > 0
|