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.
Files changed (227) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/__init__.py +70 -23
  3. dataface/agent_api/__init__.py +162 -68
  4. dataface/agent_api/_paths.py +9 -3
  5. dataface/agent_api/docs/yaml-reference.md +273 -239
  6. dataface/agent_api/init.py +1 -1
  7. dataface/agent_api/project_session.py +2 -1
  8. dataface/agent_api/serve.py +113 -0
  9. dataface/agent_api/validate.py +8 -0
  10. dataface/ai/agent.py +28 -2
  11. dataface/cli/_project.py +100 -1
  12. dataface/cli/commands/board_artifact.py +5 -2
  13. dataface/cli/commands/describe.py +7 -3
  14. dataface/cli/commands/docs.py +12 -3
  15. dataface/cli/commands/init.py +7 -14
  16. dataface/cli/commands/inspect.py +6 -1
  17. dataface/cli/commands/mcp_init.py +23 -43
  18. dataface/cli/commands/migrate.py +8 -3
  19. dataface/cli/commands/query.py +11 -8
  20. dataface/cli/commands/render.py +26 -9
  21. dataface/cli/commands/search.py +8 -2
  22. dataface/cli/commands/serve.py +25 -81
  23. dataface/cli/commands/skills_init.py +2 -19
  24. dataface/cli/commands/validate.py +7 -2
  25. dataface/cli/filesystem_project.py +5 -4
  26. dataface/cli/main.py +22 -33
  27. dataface/core/__init__.py +73 -28
  28. dataface/core/aliases.py +27 -0
  29. dataface/core/{compile/colors.py → colors.py} +20 -5
  30. dataface/core/compile/__init__.py +0 -8
  31. dataface/core/compile/channel.py +0 -13
  32. dataface/core/compile/compiler.py +161 -96
  33. dataface/core/compile/data_table.py +7 -2
  34. dataface/core/compile/enrich.py +18 -44
  35. dataface/core/compile/face_warnings.py +46 -8
  36. dataface/core/compile/format.py +47 -1
  37. dataface/core/compile/inherit_graph.py +3 -3
  38. dataface/core/compile/inherit_registry.yaml +1 -1
  39. dataface/core/compile/introspection.py +2 -13
  40. dataface/core/compile/migrations.py +112 -9
  41. dataface/core/compile/models/chart/authored/_base.py +1 -1
  42. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  43. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  44. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  45. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  46. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  47. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  48. dataface/core/compile/models/chart/resolved/table.py +16 -2
  49. dataface/core/compile/models/config.py +5 -0
  50. dataface/core/compile/models/face/authored.py +24 -89
  51. dataface/core/compile/models/markers.py +1 -1
  52. dataface/core/compile/models/primitives.py +175 -2
  53. dataface/core/compile/models/query/authored.py +1 -119
  54. dataface/core/compile/models/refs.py +0 -2
  55. dataface/core/compile/models/source.py +78 -164
  56. dataface/core/compile/models/style/authored/__init__.py +32 -8
  57. dataface/core/compile/models/style/authored/_base.py +126 -69
  58. dataface/core/compile/models/style/authored/table.py +61 -3
  59. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  60. dataface/core/compile/models/style/resolved/_base.py +142 -57
  61. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  62. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  63. dataface/core/compile/models/style/resolved/area.py +1 -1
  64. dataface/core/compile/models/style/resolved/bar.py +1 -1
  65. dataface/core/compile/models/style/resolved/line.py +1 -1
  66. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  67. dataface/core/compile/models/style/theme/__init__.py +36 -14
  68. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  69. dataface/core/compile/models/style/theme/axis.py +432 -312
  70. dataface/core/compile/models/style/theme/charts.py +10 -6
  71. dataface/core/compile/models/style/theme/kpi.py +2 -1
  72. dataface/core/compile/models/style/theme/page.py +5 -2
  73. dataface/core/compile/models/style/theme/table.py +2 -2
  74. dataface/core/compile/models/style/theme/variables.py +0 -3
  75. dataface/core/compile/models/vega_lite/config.py +1 -1
  76. dataface/core/compile/normalize_charts.py +10 -154
  77. dataface/core/compile/normalize_layout.py +20 -284
  78. dataface/core/compile/normalize_queries.py +24 -513
  79. dataface/core/compile/normalizer.py +4 -10
  80. dataface/core/compile/palette.py +19 -7
  81. dataface/core/compile/resolve/__init__.py +2 -0
  82. dataface/core/compile/resolve/_axes.py +147 -44
  83. dataface/core/compile/resolve/_channels.py +1 -1
  84. dataface/core/compile/resolve/_dispatch.py +1 -1
  85. dataface/core/compile/resolve/_domain.py +49 -32
  86. dataface/core/compile/resolve/_kwargs.py +39 -12
  87. dataface/core/compile/resolve/_marks.py +8 -3
  88. dataface/core/compile/resolve/_palette.py +7 -3
  89. dataface/core/compile/resolve/_table.py +124 -33
  90. dataface/core/compile/resolve/area.py +48 -18
  91. dataface/core/compile/resolve/bar.py +245 -64
  92. dataface/core/compile/resolve/geo.py +10 -3
  93. dataface/core/compile/resolve/heatmap.py +16 -8
  94. dataface/core/compile/resolve/line.py +35 -12
  95. dataface/core/compile/resolve/pie.py +4 -1
  96. dataface/core/compile/resolve/pie_attachment.py +1 -1
  97. dataface/core/compile/resolve/scatter.py +25 -13
  98. dataface/core/compile/resolve/simple.py +25 -2
  99. dataface/core/compile/schema_renderers/prompt.py +1 -1
  100. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  101. dataface/core/compile/source_map.py +211 -28
  102. dataface/core/compile/style_cascade.py +126 -55
  103. dataface/core/compile/validator.py +121 -44
  104. dataface/core/compile/yaml_error_formatter.py +108 -3
  105. dataface/core/connections.py +10 -3
  106. dataface/core/dashboard.py +27 -19
  107. dataface/core/defaults/default_config.yml +4 -0
  108. dataface/core/defaults/themes/_base.yaml +49 -40
  109. dataface/core/defaults/themes/cream.yaml +4 -4
  110. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  111. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  112. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  113. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  114. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  115. dataface/core/defaults/themes/editorial.yaml +9 -6
  116. dataface/core/defaults/themes/neon.yaml +15 -8
  117. dataface/core/defaults/themes/plain.yaml +6 -4
  118. dataface/core/defaults/themes/stark.yaml +24 -7
  119. dataface/core/defaults/themes/vivid.yaml +15 -8
  120. dataface/core/diagnostics/__init__.py +12 -2
  121. dataface/core/diagnostics/codes_compile.py +17 -33
  122. dataface/core/diagnostics/codes_execute.py +149 -0
  123. dataface/core/diagnostics/codes_render.py +144 -31
  124. dataface/core/diagnostics/diagnostic.py +30 -0
  125. dataface/core/diagnostics/hints.py +19 -0
  126. dataface/core/dialects/base.py +17 -0
  127. dataface/core/dialects/postgres.py +14 -0
  128. dataface/core/dialects/redshift.py +5 -0
  129. dataface/core/execute/__init__.py +59 -14
  130. dataface/core/execute/adapters/__init__.py +1 -1
  131. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  132. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  133. dataface/core/execute/adapters/dbt_utils.py +84 -20
  134. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  135. dataface/core/execute/adapters/sql_adapter.py +72 -4
  136. dataface/core/execute/cache_keys.py +4 -9
  137. dataface/core/execute/dbt_jinja.py +65 -10
  138. dataface/core/execute/executor.py +94 -19
  139. dataface/core/execute/file_source_materializer.py +185 -32
  140. dataface/core/execute/trivial_local_cache.py +5 -2
  141. dataface/core/font_measure.py +28 -0
  142. dataface/core/fonts.py +8 -5
  143. dataface/core/inspect/__init__.py +46 -7
  144. dataface/core/project.py +19 -3
  145. dataface/core/render/chart/data_table_attachment.py +14 -12
  146. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  147. dataface/core/render/chart/emitters/_channels.py +56 -8
  148. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  149. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  150. dataface/core/render/chart/emitters/area.py +3 -3
  151. dataface/core/render/chart/emitters/bar.py +60 -35
  152. dataface/core/render/chart/emitters/geo.py +8 -13
  153. dataface/core/render/chart/emitters/heatmap.py +20 -26
  154. dataface/core/render/chart/emitters/line.py +2 -2
  155. dataface/core/render/chart/emitters/scatter.py +7 -30
  156. dataface/core/render/chart/features/baseline.py +22 -9
  157. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  158. dataface/core/render/chart/features/mirror_axis.py +12 -9
  159. dataface/core/render/chart/features/value_labels.py +1 -1
  160. dataface/core/render/chart/kpi.py +11 -90
  161. dataface/core/render/chart/mark_extents.py +339 -0
  162. dataface/core/render/chart/presentation.py +6 -8
  163. dataface/core/render/chart/rendering.py +100 -46
  164. dataface/core/render/chart/session.py +10 -0
  165. dataface/core/render/chart/spec.py +3 -0
  166. dataface/core/render/chart/spec_builders.py +18 -18
  167. dataface/core/render/chart/table.py +81 -36
  168. dataface/core/render/chart/table_support.py +2 -53
  169. dataface/core/render/chart/time_unit_detect.py +3 -2
  170. dataface/core/render/chart/translate.py +82 -44
  171. dataface/core/render/chart/type_inference.py +45 -25
  172. dataface/core/render/chart/validation.py +91 -0
  173. dataface/core/render/chart/vega_lite.py +30 -11
  174. dataface/core/render/chart/vl_field_maps.py +111 -76
  175. dataface/core/render/conditions.py +122 -0
  176. dataface/core/render/controls.py +60 -187
  177. dataface/core/render/converters/chart.py +16 -6
  178. dataface/core/render/data_format.py +1 -1
  179. dataface/core/render/face_resolve.py +7 -0
  180. dataface/core/render/faces.py +97 -32
  181. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  182. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  183. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  184. dataface/core/render/fonts/_font_face.css +10 -0
  185. dataface/core/render/layout_sizing.py +313 -238
  186. dataface/core/render/numeral_expr.py +80 -0
  187. dataface/core/render/renderer.py +14 -1
  188. dataface/core/render/sizing.py +42 -105
  189. dataface/core/render/svg_utils.py +60 -14
  190. dataface/core/render/terminal.py +1 -1
  191. dataface/core/render/variables_layout.py +211 -0
  192. dataface/core/render/variables_resolve.py +376 -0
  193. dataface/core/render/variables_strip.py +291 -368
  194. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  195. dataface/core/render/warnings/base.py +18 -0
  196. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  197. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  198. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  199. dataface/core/render/warnings/palette_unsupported.py +6 -0
  200. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  201. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  202. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  203. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  204. dataface/core/render/warnings/redundant_encoding.py +20 -1
  205. dataface/core/render/warnings/registry.py +9 -3
  206. dataface/core/render/warnings/temporal_single_point.py +1 -0
  207. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  208. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  209. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  210. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  211. dataface/core/render_format.py +18 -0
  212. dataface/core/serve/alias_index.py +16 -22
  213. dataface/core/text/format_d3.py +56 -0
  214. dataface/core/text/numeral_scale.py +218 -0
  215. dataface/core/utils.py +103 -10
  216. dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
  217. dataface/data/schemas/yaml/0.3.2.json +1 -0
  218. dataface/data/schemas/yaml/manifest.json +7 -0
  219. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  220. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
  221. dataface/core/inspect/partials/categorical.yml +0 -38
  222. dataface/core/inspect/partials/date.yml +0 -38
  223. dataface/core/inspect/partials/numeric.yml +0 -53
  224. dataface/core/inspect/templates/charts.yml +0 -94
  225. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  226. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  227. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
@@ -29,7 +29,7 @@ from dataface.core.compile.models.query.normalized import AnyQuery
29
29
  from dataface.core.compile.models.style.resolved import (
30
30
  ResolvedLegendStyle,
31
31
  )
32
- from dataface.core.compile.models.style.theme.board import PaddingStyle
32
+ from dataface.core.compile.models.style.theme.board import PaddingStyle, TitleStyle
33
33
  from dataface.core.compile.models.vega_lite.contracts import Projection
34
34
 
35
35
 
@@ -94,23 +94,41 @@ class _BaseResolvedChartFields(BaseModel):
94
94
  legend: ResolvedLegendStyle = Field(
95
95
  description="Baked board-level legend style from the style cascade.",
96
96
  )
97
- layout_padding: PaddingStyle | None = Field(
98
- default=None,
97
+ layout_padding: PaddingStyle = Field(
99
98
  description=(
100
- "Effective per-chart layout padding baked in at resolve time. "
101
- "Combines board.charts.padding with any chart-local style.<family>.padding "
102
- "override. None only for charts resolved outside the full pipeline "
103
- "(e.g. unit tests). render_layout_item falls back to board-level padding "
104
- "when None."
99
+ "Effective per-chart layout padding, baked in at construction time by "
100
+ "every family resolver. Combines board.charts.padding with any "
101
+ "chart-local style.<family>.padding override."
105
102
  ),
106
103
  )
107
104
 
108
105
 
109
106
  class _SharedResolvedChartFields(_BaseResolvedChartFields):
110
- """Adds title/subtitle for all families except KPI and Callout."""
107
+ """Adds title/subtitle/background for all families except KPI and Callout."""
111
108
 
112
- title: str | None = Field(default=None, description="Chart title.")
109
+ title: str | None = Field(
110
+ default=None,
111
+ description=(
112
+ "Jinja-resolved chart title, raw (no title-case transform). Also "
113
+ "backs non-VL consumers (e.g. the data-chart-title wire attribute) "
114
+ "that have always emitted the authored text as-is."
115
+ ),
116
+ )
113
117
  subtitle: str | None = Field(default=None, description="Chart subtitle.")
118
+ background: str = Field(
119
+ description=(
120
+ "Effective chart background: chart-local style.<family>.background "
121
+ "when authored, otherwise the board/face background."
122
+ ),
123
+ )
124
+ title_style: TitleStyle = Field(
125
+ description=(
126
+ "Effective title style — chart-local style.<family>.title merged "
127
+ "onto the board title style. Render-time VL assembly reads "
128
+ "title_style.font.case to case-transform `title` (VL presentation "
129
+ "only), and title_style.overflow for title-wrap mode."
130
+ ),
131
+ )
114
132
 
115
133
 
116
134
  class _CartesianResolvedChartFields(_SharedResolvedChartFields):
@@ -74,10 +74,9 @@ class ResolvedCalloutChart(BaseModel):
74
74
  default_factory=dict,
75
75
  description="Always empty — callout has no data channels.",
76
76
  )
77
- layout_padding: PaddingStyle | None = Field(
78
- default=None,
77
+ layout_padding: PaddingStyle = Field(
79
78
  description=(
80
- "Effective per-chart layout padding baked in at resolve time. "
79
+ "Effective per-chart layout padding, baked in at construction time. "
81
80
  "Structural parity with _BaseResolvedChartFields so render_layout_item "
82
81
  "can access chart.layout_padding without isinstance guards."
83
82
  ),
@@ -7,10 +7,10 @@ from typing import Literal
7
7
  from pydantic import Field
8
8
 
9
9
  from dataface.core.compile.models.chart.authored import KpiSupportConfig
10
+ from dataface.core.compile.models.primitives import FormatConfig
10
11
  from dataface.core.compile.models.style.resolved import ResolvedKpiStyle
11
12
 
12
13
  from ._base import _BaseResolvedChartFields
13
- from ._layer import FormatState
14
14
 
15
15
 
16
16
  class ResolvedKpiChart(_BaseResolvedChartFields):
@@ -50,9 +50,13 @@ class ResolvedKpiChart(_BaseResolvedChartFields):
50
50
  default="stacked",
51
51
  description="Layout variant — dispatches to a distinct SVG emit path.",
52
52
  )
53
- format: FormatState = Field(
53
+ format: FormatConfig | None = Field(
54
54
  default=None,
55
- description="D3-format string, FormatConfig, or None for the headline value.",
55
+ description=(
56
+ "Final headline value format — resolve() has already applied the "
57
+ "narrative-notation and SI-compaction defaults against this "
58
+ "chart's row; render consumes it as-is."
59
+ ),
56
60
  )
57
61
  style: ResolvedKpiStyle = Field(
58
62
  description="KPI family style slice.",
@@ -39,7 +39,15 @@ class ResolvedTableChart(_SharedResolvedChartFields):
39
39
  # not from source_chart.style (which no longer exists on Resolved*Chart).
40
40
  columns: dict[str, TableColumnConfig] | None = Field(
41
41
  default=None,
42
- description="Per-column display config (width, format, label, …).",
42
+ description=(
43
+ "Final per-column display config (width, format, label, …), keyed by "
44
+ "column name. Complete for every column resolve can see: defaults, "
45
+ "explicit overrides, and runtime FK links are already merged. Excludes "
46
+ "pivot measure columns (see `column_defaults`) — their real key space "
47
+ "only exists after render's pivot transform runs. None means no "
48
+ "column-level config or runtime link facts apply — render falls back "
49
+ "to plain query columns."
50
+ ),
43
51
  )
44
52
  header_overflow: Literal["clip", "truncate", "wrap-two", "wrap"] | None = Field(
45
53
  default=None,
@@ -47,7 +55,13 @@ class ResolvedTableChart(_SharedResolvedChartFields):
47
55
  )
48
56
  column_defaults: TableColumnDefaultsConfig | None = Field(
49
57
  default=None,
50
- description="Defaults applied to query-inferred columns.",
58
+ description=(
59
+ "Table-level column defaults, kept only for render's two render-native "
60
+ "column key spaces — pivot leaf/measure columns and transpose's "
61
+ "__metric__/__value__ pair — neither of which exists until a "
62
+ "render-time data transform runs. Every other column (explicit or "
63
+ "query-inferred) already has defaults merged into `columns`."
64
+ ),
51
65
  )
52
66
  style: ResolvedTableStyle = Field(
53
67
  description="Table family style slice.",
@@ -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]
@@ -22,6 +22,7 @@ from pydantic import (
22
22
  model_validator,
23
23
  )
24
24
 
25
+ from dataface.core.aliases import normalize_alias_url
25
26
  from dataface.core.compile.models.cache import CachePatch
26
27
  from dataface.core.compile.models.chart.authored import AuthoredChart
27
28
  from dataface.core.compile.models.markers import Merge, Strategy
@@ -119,74 +120,6 @@ def _check_layout_item(v: Any) -> Any:
119
120
  return _reject_chartref_dict(v)
120
121
 
121
122
 
122
- class ForeachQuery(BaseModel):
123
- """Inline data source for compile-time foreach loops."""
124
-
125
- model_config = ConfigDict(extra="forbid")
126
-
127
- data: list[dict[str, Any]] | None = Field(
128
- default=None,
129
- description="Inline row data for compile-time iteration.",
130
- )
131
- static_data: list[dict[str, Any]] | None = Field(
132
- default=None,
133
- description="Alias for data; inline row data for compile-time iteration.",
134
- )
135
- palette: str | None = Field(
136
- default=None,
137
- description=(
138
- "Palette name to iterate its live stops from the palette YAML source "
139
- "of truth (one row per stop: {n: 1-indexed position, hex: sRGB hex}). "
140
- "Accepts the same 'name:steps' / 'name_r' shorthand as palette()."
141
- ),
142
- )
143
-
144
- @model_validator(mode="after")
145
- def _reject_palette_with_inline_data(self) -> ForeachQuery:
146
- """palette: is a distinct data source; combining it with data:/static_data:
147
- is ambiguous about which one drives the loop."""
148
- if self.palette is not None and (
149
- self.data is not None or self.static_data is not None
150
- ):
151
- raise ValueError(
152
- "foreach.query.palette is mutually exclusive with data/static_data"
153
- )
154
- return self
155
-
156
-
157
- class ForeachConfig(BaseModel):
158
- """foreach loop configuration.
159
-
160
- `as` is a Python keyword so the field is declared as `as_` with an alias.
161
- """
162
-
163
- model_config = ConfigDict(extra="forbid", populate_by_name=True)
164
-
165
- query: ForeachQuery = Field(
166
- description="Inline data source providing the rows to iterate over."
167
- )
168
- as_: str = Field(
169
- alias="as",
170
- description="Variable name bound to each row (accessible as {{ as.field }}).",
171
- )
172
- items: Annotated[
173
- list[
174
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart]
175
- ],
176
- BeforeValidator(_check_layout_list),
177
- ] = Field(description="Layout item templates resolved once per data row.")
178
-
179
-
180
- class ForeachItem(BaseModel):
181
- """Top-level foreach construct in a layout list."""
182
-
183
- model_config = ConfigDict(extra="forbid")
184
-
185
- foreach: ForeachConfig = Field(
186
- description="foreach loop configuration with query, as, and items."
187
- )
188
-
189
-
190
123
  class GridItem(BaseModel):
191
124
  """Grid layout item with position and span.
192
125
 
@@ -209,7 +142,7 @@ class GridItem(BaseModel):
209
142
  model_config = ConfigDict(extra="forbid")
210
143
 
211
144
  item: Annotated[
212
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart],
145
+ str | AuthoredFace | AuthoredChart | dict[str, AuthoredChart],
213
146
  BeforeValidator(_check_layout_item),
214
147
  ] = Field(
215
148
  description="Chart name or inline chart/face definition to place in this grid cell."
@@ -288,17 +221,11 @@ class TabItem(BaseModel):
288
221
 
289
222
  # Layout fields (tab can contain nested layouts)
290
223
  rows: Annotated[
291
- list[
292
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart]
293
- ]
294
- | None,
224
+ list[str | AuthoredFace | AuthoredChart | dict[str, AuthoredChart]] | None,
295
225
  BeforeValidator(_check_layout_list),
296
226
  ] = Field(default=None, description="Vertical stack layout for this tab's content.")
297
227
  cols: Annotated[
298
- list[
299
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart]
300
- ]
301
- | None,
228
+ list[str | AuthoredFace | AuthoredChart | dict[str, AuthoredChart]] | None,
302
229
  BeforeValidator(_check_layout_list),
303
230
  ] = Field(default=None, description="Horizontal layout for this tab's content.")
304
231
  grid: GridLayout | None = Field(
@@ -510,6 +437,7 @@ class AuthoredFace(_FaceDesugarMixin):
510
437
  ),
511
438
  )
512
439
  )
440
+
513
441
  text: Annotated[str | None, Merge(Strategy.OVERRIDE)] = Field(
514
442
  default=None, description="Markdown text content for text-only sections."
515
443
  )
@@ -573,10 +501,7 @@ class AuthoredFace(_FaceDesugarMixin):
573
501
 
574
502
  # Layout (exactly one should be present)
575
503
  rows: Annotated[
576
- list[
577
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart]
578
- ]
579
- | None,
504
+ list[str | AuthoredFace | AuthoredChart | dict[str, AuthoredChart]] | None,
580
505
  BeforeValidator(_check_layout_list),
581
506
  Merge(Strategy.APPEND, nested=Strategy.CHILD),
582
507
  ] = Field(
@@ -584,10 +509,7 @@ class AuthoredFace(_FaceDesugarMixin):
584
509
  description="Vertical stack layout: list of chart names or inline chart/face definitions.",
585
510
  )
586
511
  cols: Annotated[
587
- list[
588
- str | AuthoredFace | AuthoredChart | ForeachItem | dict[str, AuthoredChart]
589
- ]
590
- | None,
512
+ list[str | AuthoredFace | AuthoredChart | dict[str, AuthoredChart]] | None,
591
513
  BeforeValidator(_check_layout_list),
592
514
  Merge(Strategy.APPEND, nested=Strategy.CHILD),
593
515
  ] = Field(
@@ -697,6 +619,21 @@ class AuthoredFace(_FaceDesugarMixin):
697
619
  )
698
620
  return data
699
621
 
622
+ @model_validator(mode="after")
623
+ def validate_aliases(self) -> AuthoredFace:
624
+ """Every alias must be absolute, per ``aliases``' own description.
625
+
626
+ Checked through ``normalize_alias_url`` so the authored surface and the
627
+ redirect index cannot disagree about what a well-formed alias is; the
628
+ normalized value is discarded, since the face stores what the author
629
+ wrote.
630
+ """
631
+ if self.aliases is None:
632
+ return self
633
+ for alias in self.aliases:
634
+ normalize_alias_url(alias)
635
+ return self
636
+
700
637
  @model_validator(mode="after")
701
638
  def validate_layout(self) -> AuthoredFace:
702
639
  """Ensure at least one layout type or content is defined."""
@@ -723,10 +660,8 @@ class AuthoredFace(_FaceDesugarMixin):
723
660
  return self.source
724
661
 
725
662
 
726
- # Resolve forward references (ForeachConfig.items references ForeachItem and
727
- # AuthoredFace; GridItem.item and TabItem.rows/cols also reference types defined
728
- # later in the file — rebuild after all classes are in scope)
729
- ForeachConfig.model_rebuild()
663
+ # Resolve forward references (GridItem.item and TabItem.rows/cols reference types
664
+ # defined later in the file — rebuild after all classes are in scope)
730
665
  GridItem.model_rebuild()
731
666
  TabItem.model_rebuild()
732
667
  AuthoredFace.model_rebuild()
@@ -104,7 +104,7 @@ class SkipInheritSlots:
104
104
  ``_ChartStyleBase`` must not inherit from the root cascade.
105
105
 
106
106
  **On a leaf scalar field**: prevents the leaf from being added to the graph
107
- via slot expansion. Use on ``AxisStyle`` fields that the style resolver does
107
+ via slot expansion. Use on axis style fields that the style resolver does
108
108
  not cascade — e.g. ``grid.dash``, ``ticks.count``, ``scale``, ``position`` —
109
109
  so the inherit graph only declares what the resolver actually does.
110
110
 
@@ -8,12 +8,13 @@ cannot live in either without creating a circular import.
8
8
  from __future__ import annotations
9
9
 
10
10
  import re
11
- from typing import Annotated, Any, Literal
11
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
12
12
 
13
13
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
14
14
 
15
15
  from dataface.core.compile.models.factories import (
16
16
  _PatchBase,
17
+ build_patch_model_ext,
17
18
  register_as_own_patch,
18
19
  register_patch,
19
20
  )
@@ -410,7 +411,158 @@ class FontColorStrokeStyle(StrokeStyle):
410
411
  )
411
412
 
412
413
 
413
- class ScaleTargetConfig(BaseModel):
414
+ # Vega/Vega-Lite's built-in named color schemes (https://vega.github.io/vega/docs/schemes/),
415
+ # the only strings ``ScaleTargetConfig.palette`` forwards straight through to VL's
416
+ # ``scale.scheme``. Sampled-discrete shorthand like "blues-9" is not a real scheme
417
+ # name — VL fails deep inside rendering ("Cannot read properties of undefined")
418
+ # rather than rejecting it up front, so this is validated at compile time instead.
419
+ #
420
+ # Empirically verified against this repo's pinned vl-convert (not hand-copied from
421
+ # docs alone): every name below was round-tripped through
422
+ # ``vlc.vegalite_to_svg`` with a quantitative ``scale.scheme`` encoding and
423
+ # confirmed to render — see ``test_vega_scheme_names_parity.py``, which re-runs
424
+ # that sweep and fails if this set and the bundled renderer ever diverge.
425
+ VEGA_SCHEME_NAMES: frozenset[str] = frozenset(
426
+ {
427
+ # Categorical
428
+ "accent",
429
+ "category10",
430
+ "category20",
431
+ "category20b",
432
+ "category20c",
433
+ "dark2",
434
+ "observable10",
435
+ "paired",
436
+ "pastel1",
437
+ "pastel2",
438
+ "set1",
439
+ "set2",
440
+ "set3",
441
+ "tableau10",
442
+ "tableau20",
443
+ # Sequential (single-hue)
444
+ "blues",
445
+ "tealblues",
446
+ "teals",
447
+ "greens",
448
+ "browns",
449
+ "greys",
450
+ "oranges",
451
+ "purples",
452
+ "reds",
453
+ "warmgreys",
454
+ # Sequential (multi-hue)
455
+ "viridis",
456
+ "magma",
457
+ "inferno",
458
+ "plasma",
459
+ "cividis",
460
+ "turbo",
461
+ "bluegreen",
462
+ "bluepurple",
463
+ "goldgreen",
464
+ "goldorange",
465
+ "goldred",
466
+ "greenblue",
467
+ "orangered",
468
+ "purplebluegreen",
469
+ "purpleblue",
470
+ "purplered",
471
+ "redpurple",
472
+ "yellowgreenblue",
473
+ "yellowgreen",
474
+ "yelloworangebrown",
475
+ "yelloworangered",
476
+ # Diverging
477
+ "blueorange",
478
+ "brownbluegreen",
479
+ "purplegreen",
480
+ "pinkyellowgreen",
481
+ "purpleorange",
482
+ "redblue",
483
+ "redgrey",
484
+ "redyellowblue",
485
+ "redyellowgreen",
486
+ "spectral",
487
+ # Cyclical
488
+ "rainbow",
489
+ "sinebow",
490
+ # For dark backgrounds
491
+ "darkblue",
492
+ "darkgold",
493
+ "darkgreen",
494
+ "darkmulti",
495
+ "darkred",
496
+ # For light backgrounds
497
+ "lightgreyred",
498
+ "lightgreyteal",
499
+ "lightmulti",
500
+ "lightorange",
501
+ "lighttealblue",
502
+ }
503
+ )
504
+
505
+
506
+ class ScaleTargetPaletteValidationMixin(BaseModel):
507
+ """Shared authored-input validator for ``ScaleTargetConfig.palette``.
508
+
509
+ Inherited by both ``ScaleTargetConfig`` and ``ScaleTargetConfigPatch`` so
510
+ chart-local authored patches reject a bad palette at the compile boundary
511
+ (where ``normalize_face()`` wraps the resulting ``pydantic.ValidationError``
512
+ into a proper ``ERR-VALIDATION-FIELD`` diagnostic) instead of only at
513
+ resolve time, where the raw pydantic error leaks past Dataface's error
514
+ formatting — ``build_patch_model_ext`` only carries over validators that
515
+ live on the patch model's base class (mirrors ``ScaleDomainValidationMixin``).
516
+ """
517
+
518
+ model_config = ConfigDict(extra="forbid")
519
+
520
+ @model_validator(mode="before")
521
+ @classmethod
522
+ def _validate_named_palette(cls, data: object) -> object:
523
+ """A string palette is either a literal Vega scheme name (forwarded
524
+ as-is to VL's ``scale.scheme`` by geo/heatmap gradient rendering) or a
525
+ Dataface named palette (resolved to hex stops downstream by
526
+ ``resolve_palette_stops``). Reject anything that resolves as neither.
527
+
528
+ A dash-suffixed name whose prefix is a real scheme (``"blues-9"``,
529
+ VL's sampled-discrete shorthand, which Dataface does not forward) gets
530
+ a specific hint naming the continuous scheme instead of the generic
531
+ unknown-palette message — that shape is the exact mistake this
532
+ validator exists to catch: an author reaching for N discrete buckets
533
+ that Dataface doesn't support yet, not a random typo.
534
+
535
+ ``mode="before"`` on the raw dict (not ``field_validator``) so this
536
+ runs unchanged on both ``ScaleTargetConfig`` and the generated
537
+ ``ScaleTargetConfigPatch`` — a field-scoped validator would need the
538
+ field to exist on this mixin itself, which a patch base never does.
539
+ """
540
+ if not isinstance(data, dict):
541
+ return data
542
+ value = data.get("palette")
543
+ if not isinstance(value, str) or value in VEGA_SCHEME_NAMES:
544
+ return data
545
+ from dataface.core.compile.palette import ( # noqa: PLC0415
546
+ UnknownPaletteError,
547
+ palette as resolve_palette,
548
+ )
549
+
550
+ try:
551
+ resolve_palette(value)
552
+ except UnknownPaletteError as exc:
553
+ base, _, suffix = value.rpartition("-")
554
+ if base in VEGA_SCHEME_NAMES and suffix.isdigit():
555
+ raise ValueError(
556
+ f"palette {value!r} requests a {suffix}-step discrete "
557
+ f"variant of the '{base}' scheme. Dataface does not support "
558
+ "bucketed/quantized color scales yet — use the continuous "
559
+ f"scheme '{base}' instead."
560
+ ) from None
561
+ raise ValueError(str(exc)) from None
562
+ return data
563
+
564
+
565
+ class ScaleTargetConfig(ScaleTargetPaletteValidationMixin):
414
566
  """Scale configuration for a single style target (background or color).
415
567
 
416
568
  Used as a chart channel annotation (``ColumnScaleConfig``) and as the
@@ -447,6 +599,27 @@ class ScaleTargetConfig(BaseModel):
447
599
  )
448
600
 
449
601
 
602
+ # Patch type for ScaleTargetConfig — generated (and registered) here, right after
603
+ # the compiled class, so every later recursive build_patch_model_ext call that
604
+ # encounters ScaleTargetConfig as a nested field (StaticGradientColorStyle.gradient,
605
+ # ColumnScaleConfig.color/background) reuses this exact registered class instead of
606
+ # a vanilla auto-generated one with no palette validation.
607
+ if TYPE_CHECKING:
608
+
609
+ class ScaleTargetConfigPatch(ScaleTargetConfig):
610
+ pass
611
+
612
+ else:
613
+
614
+ class _ScaleTargetConfigPatchBase(_PatchBase, ScaleTargetPaletteValidationMixin):
615
+ """Base for ScaleTargetConfigPatch: extra="forbid" from _PatchBase plus
616
+ the palette validation from ScaleTargetPaletteValidationMixin."""
617
+
618
+ ScaleTargetConfigPatch = register_patch(ScaleTargetConfig)(
619
+ build_patch_model_ext(ScaleTargetConfig, base_cls=_ScaleTargetConfigPatchBase)
620
+ )
621
+
622
+
450
623
  class CategoricalColorStyle(BaseModel):
451
624
  """Categorical palette config: per-series colors and single-series ink list."""
452
625
 
@@ -287,114 +287,6 @@ class AuthoredSchemaQuery(_BaseQueryFields):
287
287
  )
288
288
 
289
289
 
290
- class _AuthoredLookmlQueryFields(_BaseQueryFields):
291
- """Fields shared by both LookML authoring shapes.
292
-
293
- ``source:`` names the LookML *model* (declared under a warehouse source's
294
- ``models:`` map), not a connection. Optional install:
295
- ``pip install dataface[lookml]``.
296
- """
297
-
298
- type: Literal["lookml"] = Field(
299
- default="lookml",
300
- description="Query adapter type.",
301
- )
302
- fields: list[str] | None = Field(
303
- default=None,
304
- description=(
305
- "Qualified 'view.field' refs to select. "
306
- "Optional at authoring time: collected from chart channels when absent."
307
- ),
308
- )
309
- sorts: list[str] | None = Field(
310
- default=None,
311
- description=(
312
- "Looker sort clauses, e.g. "
313
- "['view.field', 'view.other desc']. Lowered to ORDER BY via likeml."
314
- ),
315
- )
316
- pivots: list[str] | None = Field(
317
- default=None,
318
- description="Qualified 'view.field' refs to pivot on.",
319
- )
320
- pivots_unwrapped: bool | None = Field(
321
- default=None,
322
- description=(
323
- "Which of Looker's two pivot SQL shapes to emit: "
324
- "False/None = wrapped (5-layer DENSE_RANK), True = plain GROUP BY."
325
- ),
326
- )
327
- total: bool | None = Field(
328
- default=None,
329
- description="Looker grand-totals flag.",
330
- )
331
- subtotals: list[str] | None = Field(
332
- default=None,
333
- description="Dimension refs to subtotal via GROUP BY ROLLUP.",
334
- )
335
- filter_expression: str | None = Field(
336
- default=None,
337
- description=(
338
- "Looker query-level free-form boolean filter over ${view.field} refs."
339
- ),
340
- )
341
- dynamic_fields: list[dict[str, Any]] | None = Field(
342
- default=None,
343
- description=(
344
- "Looker dynamic fields: custom measures / custom "
345
- "dimensions synthesized into SQL. Table calculations compose as a derived "
346
- "wrapper query, not here."
347
- ),
348
- )
349
- limit: int | None = Field(
350
- default=None,
351
- description="Maximum number of rows returned.",
352
- )
353
- column_limit: int | None = Field(
354
- default=None,
355
- description="Looker column_limit — caps wrapped-pivot columns.",
356
- )
357
- filters: dict[str, Any] | None = Field(
358
- default=None,
359
- description=(
360
- "RAW Looker filter expressions keyed by 'view.field' "
361
- '(e.g. "13 months", "gold") — translated to SQL predicates by likeml.'
362
- ),
363
- )
364
-
365
-
366
- class AuthoredLookmlQuery(_AuthoredLookmlQueryFields):
367
- """LookML query rooted at one explore."""
368
-
369
- explore: Annotated[
370
- str,
371
- Field(
372
- description=(
373
- "LookML explore name. Presence of this key infers type: lookml. "
374
- "Use a merge query instead to combine multiple sources."
375
- )
376
- ),
377
- ]
378
-
379
-
380
- class AuthoredLookmlMergeQuery(_AuthoredLookmlQueryFields):
381
- """LookML query that merges multiple sources."""
382
-
383
- merge: Annotated[
384
- list[dict[str, Any]],
385
- Field(
386
- description=(
387
- "LookML merge sources — a list of source specs. "
388
- "Each is either an explore spec ({explore, fields, filters?, merge_on?}) "
389
- "or a query reference ({query, columns, merge_on?}) for a derived source "
390
- "such as a table-calc wrapper. First is the primary; each secondary's "
391
- "`merge_on` maps its key to the primary's. Presence of this key infers "
392
- "type: lookml. Compiled to a FULL OUTER JOIN via likeml."
393
- )
394
- ),
395
- ]
396
-
397
-
398
290
  def _discriminate_authored_query(v: Any) -> str | None:
399
291
  """Custom discriminator: return the type tag for the AuthoredQuery union.
400
292
 
@@ -419,14 +311,6 @@ def _discriminate_authored_query(v: Any) -> str | None:
419
311
  return (
420
312
  "values_rows" if isinstance(v, dict) and "rows" in v else "values_compact"
421
313
  )
422
- if t == "lookml":
423
- if isinstance(v, AuthoredLookmlMergeQuery):
424
- return "lookml_merge"
425
- if isinstance(v, AuthoredLookmlQuery):
426
- return "lookml_explore"
427
- return (
428
- "lookml_merge" if isinstance(v, dict) and "merge" in v else "lookml_explore"
429
- )
430
314
  return t
431
315
 
432
316
 
@@ -436,9 +320,7 @@ AuthoredQuery = Annotated[
436
320
  | Annotated[AuthoredHttpQuery, Tag("http")]
437
321
  | Annotated[AuthoredValuesQuery, Tag("values_rows")]
438
322
  | Annotated[AuthoredCompactValuesQuery, Tag("values_compact")]
439
- | Annotated[AuthoredSchemaQuery, Tag("schema")]
440
- | Annotated[AuthoredLookmlQuery, Tag("lookml_explore")]
441
- | Annotated[AuthoredLookmlMergeQuery, Tag("lookml_merge")],
323
+ | Annotated[AuthoredSchemaQuery, Tag("schema")],
442
324
  Discriminator(_discriminate_authored_query),
443
325
  ]
444
326
  """Discriminated union over the authored query types.