dataface 0.1.6.dev622__py3-none-any.whl → 0.1.6.dev644__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 +5 -8
- dataface/agent_api/cache.py +3 -2
- dataface/agent_api/data_paths.py +10 -4
- dataface/agent_api/docs/yaml-reference.md +4 -33
- dataface/agent_api/project_session.py +1 -1
- dataface/agent_api/render_face.py +1 -1
- dataface/agent_api/validate.py +3 -1
- dataface/core/compile/detect.py +8 -1
- dataface/core/compile/models/chart/authored.py +51 -134
- dataface/core/compile/models/chart/normalized.py +53 -12
- dataface/core/compile/models/query/authored.py +2 -44
- dataface/core/compile/models/query/normalized.py +3 -13
- dataface/core/compile/normalize_charts.py +0 -14
- dataface/core/compile/normalize_variables.py +1 -7
- dataface/core/compile/schema_renderers/prompt.py +2 -2
- dataface/core/compile/schema_renderers/vscode_schema.py +0 -27
- dataface/core/execute/__init__.py +0 -2
- dataface/core/execute/_duckdb_cache_base.py +8 -13
- dataface/core/execute/cache_backend.py +7 -6
- dataface/core/execute/duckdb_cache.py +11 -285
- dataface/core/execute/executor.py +2 -218
- dataface/core/file_plugin.py +118 -0
- dataface/core/project.py +17 -39
- dataface/core/project_roots.py +10 -1
- dataface/core/render/chart/pipeline.py +14 -25
- dataface/core/render/chart/serialization.py +0 -5
- dataface/core/render/chart/table.py +352 -16
- dataface/core/render/chart/table_support.py +85 -31
- dataface/core/render/layout_sizing.py +10 -3
- dataface/core/serve/alias_index.py +16 -11
- dataface/core/serve/port.py +6 -1
- dataface/core/serve/server.py +6 -1
- {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/RECORD +37 -36
- {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -516,22 +516,20 @@ charts:
|
|
|
516
516
|
y: total # column name OR [col, col, ...] for multi-series
|
|
517
517
|
color: segment # column | {value: "#ccc"} | {field, scale} | {field, when}
|
|
518
518
|
|
|
519
|
-
# Sizing —
|
|
519
|
+
# Sizing — height lives at chart root; aspect_ratio is a style field
|
|
520
520
|
height: 400 # exact px; bypasses aspect_ratio and min/max clamps
|
|
521
|
-
|
|
522
|
-
# height and aspect_ratio are ignored on kpi, table, callout, spark_bar
|
|
521
|
+
# height/aspect_ratio are ignored on kpi, table, callout, spark_bar
|
|
523
522
|
|
|
524
523
|
# Style + behavior
|
|
525
524
|
sort: { by: total, order: desc }
|
|
526
525
|
x_label: "Month"
|
|
527
526
|
y_label: "Revenue (USD)"
|
|
528
527
|
link: "/orders?month={{ month }}" # Click-through URL template (drill-down)
|
|
529
|
-
filters: { ... } # Result filters
|
|
530
528
|
|
|
531
529
|
style: # Chart-local style patch (typed; not raw CSS) — paint only
|
|
532
|
-
|
|
530
|
+
aspect_ratio: 2.0 # shape without a fixed size; height = width / aspect_ratio
|
|
533
531
|
number_format: ",.0f" # D3 format string or named alias for axis/tooltip format
|
|
534
|
-
|
|
532
|
+
# bar/area families also accept style.orientation and style.stack
|
|
535
533
|
```
|
|
536
534
|
|
|
537
535
|
### Chart types (29 total)
|
|
@@ -585,7 +583,6 @@ All chart types accept the channels and style fields below — but each type rej
|
|
|
585
583
|
| `background` | string \| object | Background channel — color, `{value}`, `{field, scale/when}`, or map layer |
|
|
586
584
|
| `sort` | object | `{by, order}` — categorical sort. Horizontal bar charts default to value-descending order when omitted. |
|
|
587
585
|
| `link` | string | Click-through URL template for drill-down links |
|
|
588
|
-
| `filters` | object | Post-execution row filters: `{col: var_name}` (implicit `eq`) or `{col: {op: var}}` where op is one of `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`, `not_in`, `between`. Implicit-eq values may also be Jinja templates (e.g. `"{{ region }}"`) resolved at execute time. A filter is skipped when the variable is `None`, `""`, or renders to `"none"`. |
|
|
589
586
|
| `layers` | list | Layer definitions for `type: layered` (see [Composition](#composition)) |
|
|
590
587
|
| `conditional_formatting` | object | Discrete style rules by column (see [Conditional formatting](#conditional-formatting)) |
|
|
591
588
|
| `data_table` | list | Attached mini-table beneath bar/line/area/layered (see [Composition](#composition)) |
|
|
@@ -598,7 +595,7 @@ All chart types accept the channels and style fields below — but each type rej
|
|
|
598
595
|
|
|
599
596
|
KPI-only fields: `value`, `label`, `support`. KPI uses `label:` for the header text — `title:` is rejected on KPI charts. `glyph` and `tone` moved into the style namespace: use `style.glyph.character` and `style.tone`. To override glyph/value color, use `style.kpi.glyph.font.color` / `style.kpi.value.font.color`.
|
|
600
597
|
|
|
601
|
-
Top-level chart fields shared by all types: `id`, `query`, `type`, `title`, `subtitle`, `description`, `height`, `aspect_ratio`, `style`, `link`, `
|
|
598
|
+
Top-level chart fields shared by all types: `id`, `query`, `type`, `title`, `subtitle`, `description`, `height`, `aspect_ratio`, `style`, `link`, `conditional_formatting`.
|
|
602
599
|
|
|
603
600
|
### Chart-type cheatsheet
|
|
604
601
|
|
dataface/agent_api/cache.py
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
from collections.abc import Generator
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
|
|
6
|
-
from dataface.core.execute.duckdb_cache import
|
|
6
|
+
from dataface.core.execute.duckdb_cache import open_cache_from_env
|
|
7
|
+
from dataface.core.execute.trivial_local_cache import TrivialDuckDBCache
|
|
7
8
|
|
|
8
9
|
__all__ = ["cache_from_env", "open_cache_from_env"]
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@contextmanager
|
|
12
|
-
def cache_from_env() -> Generator[
|
|
13
|
+
def cache_from_env() -> Generator[TrivialDuckDBCache | None]:
|
|
13
14
|
"""Open DFT_CACHE_PATH-backed cache (or None) and close it on exit.
|
|
14
15
|
|
|
15
16
|
Use for CLI verbs and composition roots that need a cache scoped to
|
dataface/agent_api/data_paths.py
CHANGED
|
@@ -201,19 +201,25 @@ def build_alias_index_for_project(project: Project) -> AliasIndex:
|
|
|
201
201
|
def data_alias_errors_for_file(
|
|
202
202
|
face_file: Path,
|
|
203
203
|
source_names: frozenset[str],
|
|
204
|
+
project: Project,
|
|
204
205
|
) -> list[str]:
|
|
205
206
|
"""Read aliases from a face file and lint any /data/ prefixed ones.
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
Reads through *project* (the FilePlugin seam), so it works against a
|
|
209
|
+
git-blob store as well as the filesystem. Source-name check only — no DB
|
|
210
|
+
connection, no resolver. Called from the validate path which must stay
|
|
211
|
+
connection-free.
|
|
209
212
|
|
|
210
213
|
Returns a list of error message strings; empty means no data alias issues.
|
|
211
214
|
Silently returns [] when the file cannot be parsed (compile catches that).
|
|
212
215
|
"""
|
|
213
216
|
from dataface.core.serve.alias_index import read_aliases_from_file
|
|
214
217
|
|
|
218
|
+
# file_for_path raises ValueError when face_file is outside project.root —
|
|
219
|
+
# that is a caller bug, not a parse error, and must NOT be swallowed.
|
|
220
|
+
project_file = project.file_for_path(face_file)
|
|
215
221
|
try:
|
|
216
|
-
aliases = read_aliases_from_file(
|
|
222
|
+
aliases = read_aliases_from_file(project_file)
|
|
217
223
|
except ValueError:
|
|
218
|
-
return [] # compile path reports alias parse errors with
|
|
224
|
+
return [] # compile path reports alias-shape parse errors with context
|
|
219
225
|
return validate_data_aliases(aliases, source_names=source_names)
|
|
@@ -100,9 +100,8 @@ AuthoredQuery definition from YAML.
|
|
|
100
100
|
| `rows` | list[dict[str, Any]] | ✓ | Inline data rows for values-type queries (list of row dicts). |
|
|
101
101
|
| `values` | list[list[Any]] | ✓ | Inline column-oriented data for values-type queries (list of lists). |
|
|
102
102
|
| `description` | str | ✓ | Human-readable description of the query. Used by AI search and tooling. |
|
|
103
|
-
| `filters` | dict[str, Any] | ✓ |
|
|
103
|
+
| `filters` | dict[str, Any] | ✓ | Declarative column filters injected into the query's WHERE clause (SQL/DuckDB sources). |
|
|
104
104
|
| `limit` | int | ✓ | Maximum number of rows returned. |
|
|
105
|
-
| `pivot` | [Pivot](#pivot) | ✓ | Cross-tab pivot hint: transforms long-form SQL output into a wide table at render time (table consumers only). Does not affect SQL execution. |
|
|
106
105
|
| `ignore` | list[str] | ✓ | Diagnostic codes to suppress for this query (e.g., ['fanout_risk', 'reaggregation']). |
|
|
107
106
|
|
|
108
107
|
<a id="barchart"></a>
|
|
@@ -116,7 +115,6 @@ Authored patch for bar and histogram charts.
|
|
|
116
115
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
117
116
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
118
117
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
119
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
120
118
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
121
119
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
122
120
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -144,7 +142,6 @@ Authored patch for line charts.
|
|
|
144
142
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
145
143
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
146
144
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
147
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
148
145
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
149
146
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
150
147
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -172,7 +169,6 @@ Authored patch for area charts.
|
|
|
172
169
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
173
170
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
174
171
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
175
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
176
172
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
177
173
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
178
174
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -200,7 +196,6 @@ Authored patch for scatter charts.
|
|
|
200
196
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
201
197
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
202
198
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
203
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
204
199
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
205
200
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
206
201
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -230,7 +225,6 @@ Authored patch for heatmap charts.
|
|
|
230
225
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
231
226
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
232
227
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
233
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
234
228
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
235
229
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
236
230
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -259,7 +253,6 @@ Authored patch for pie and donut charts.
|
|
|
259
253
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
260
254
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
261
255
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
262
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
263
256
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
264
257
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
265
258
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -281,7 +274,6 @@ Authored patch for KPI (key performance indicator) charts.
|
|
|
281
274
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
282
275
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
283
276
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
284
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
285
277
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
286
278
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
287
279
|
| `label` | str | ✓ | KPI label rendered above the headline value. |
|
|
@@ -300,12 +292,14 @@ Authored patch for table charts.
|
|
|
300
292
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
301
293
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
302
294
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
303
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
304
295
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
305
296
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
306
297
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
307
298
|
| `subtitle` | str | ✓ | Chart subtitle displayed below the title. |
|
|
308
299
|
| `style` | [TableChartStyle](#tablechartstyle) | ✓ | Chart-local style overrides. |
|
|
300
|
+
| `rows` | list[str] | ✓ | Fields whose distinct values form the row dimension of a pivot cross-tab. Each string is a column name from the query result. Omit for flat (non-pivot) tables. |
|
|
301
|
+
| `columns` | list[str] | ✓ | Fields whose distinct values become column headers in a pivot cross-tab. Multiple fields create a nested multi-dimension pivot (outer → inner). Omit for flat tables. |
|
|
302
|
+
| `values` | list[str] | ✓ | Measure fields that fill pivot cells. Each string is a column name from the query result. When omitted, all query columns not claimed by rows or columns are used. |
|
|
309
303
|
|
|
310
304
|
<a id="pointmapchart"></a>
|
|
311
305
|
## PointMapChart
|
|
@@ -318,7 +312,6 @@ Authored patch for point_map and bubble_map charts.
|
|
|
318
312
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
319
313
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
320
314
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
321
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
322
315
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
323
316
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
324
317
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -346,7 +339,6 @@ Authored patch for map and geoshape charts.
|
|
|
346
339
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
347
340
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
348
341
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
349
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
350
342
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
351
343
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
352
344
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -373,7 +365,6 @@ Authored patch for layered multi-mark charts.
|
|
|
373
365
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
374
366
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
375
367
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
376
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
377
368
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
378
369
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
379
370
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -408,7 +399,6 @@ Authored patch for spark_bar charts (compact horizontal bars).
|
|
|
408
399
|
| `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
|
|
409
400
|
| `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
|
|
410
401
|
| `link` | str | ✓ | Click-through URL template for drill-down links. |
|
|
411
|
-
| `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
|
|
412
402
|
| `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
|
|
413
403
|
| `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
|
|
414
404
|
| `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
|
|
@@ -510,25 +500,6 @@ Options configuration for variable inputs.
|
|
|
510
500
|
| `column` | str | ✓ | Column in the query result to use as option values. |
|
|
511
501
|
| `label_column` | str | ✓ | Column in the query result to use as display labels (separate from values). |
|
|
512
502
|
|
|
513
|
-
<a id="pivot"></a>
|
|
514
|
-
## Pivot
|
|
515
|
-
Query-level pivot rendering hint for cross-tab table layout.
|
|
516
|
-
|
|
517
|
-
| Field | Type | Description |
|
|
518
|
-
|-------|------|-------------|
|
|
519
|
-
| `column` | str | Dimension whose distinct values become column headers in the cross-tab layout. |
|
|
520
|
-
| `value` | str | Measure that fills each cell in the cross-tab layout. |
|
|
521
|
-
|
|
522
|
-
<a id="filterdef"></a>
|
|
523
|
-
## FilterDef
|
|
524
|
-
One column→variable binding in chart.filters.
|
|
525
|
-
|
|
526
|
-
| Field | Type | Optional | Description |
|
|
527
|
-
|-------|------|:--------:|-------------|
|
|
528
|
-
| `op` | enum: "eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "in", "not_in", "between" | ✓ | Comparison operator. Defaults to 'eq'. |
|
|
529
|
-
| `var` | str | ✓ | Plain variable name (no Jinja). Set when the filter value is a dashboard variable reference. |
|
|
530
|
-
| `template` | str | ✓ | Jinja template resolved at execute time. Only valid with op='eq'. Set when conditional-disable or complex expressions are needed. |
|
|
531
|
-
|
|
532
503
|
<a id="fieldconditionalformatting"></a>
|
|
533
504
|
## FieldConditionalFormatting
|
|
534
505
|
Conditional formatting rules scoped to a single column.
|
|
@@ -124,7 +124,7 @@ class ProjectSession:
|
|
|
124
124
|
"""Construct a ProjectSession rooted at *project_dir*.
|
|
125
125
|
|
|
126
126
|
Cache lifecycle belongs to the caller: pass ``cache=open_cache_from_env()``
|
|
127
|
-
(or another
|
|
127
|
+
(or another cache backend) when persistent caching is desired, otherwise omit
|
|
128
128
|
and the project will run uncached. ``close()`` does not touch the cache —
|
|
129
129
|
whoever opened it closes it.
|
|
130
130
|
|
|
@@ -45,7 +45,7 @@ def render_face(
|
|
|
45
45
|
use_cache: Whether to use the in-memory Executor result cache.
|
|
46
46
|
cache: Optional DuckDB query-result cache. When provided, the caller
|
|
47
47
|
owns the cache lifecycle and must close it. The DuckDB cache is
|
|
48
|
-
*not* read from DFT_CACHE_PATH — pass an open
|
|
48
|
+
*not* read from DFT_CACHE_PATH — pass an open cache backend to
|
|
49
49
|
enable caching (see ``open_cache_from_env`` in
|
|
50
50
|
``dataface.core.execute.duckdb_cache``).
|
|
51
51
|
**options: Additional render options (e.g. ``scale`` for PNG).
|
dataface/agent_api/validate.py
CHANGED
|
@@ -258,7 +258,9 @@ def annotate_with_data_lint(
|
|
|
258
258
|
if not result.path.exists() or not result.path.is_file():
|
|
259
259
|
annotated.append(result)
|
|
260
260
|
continue
|
|
261
|
-
alias_msgs = data_alias_errors_for_file(
|
|
261
|
+
alias_msgs = data_alias_errors_for_file(
|
|
262
|
+
result.path, source_names=source_names, project=project_session.project
|
|
263
|
+
)
|
|
262
264
|
if not alias_msgs:
|
|
263
265
|
annotated.append(result)
|
|
264
266
|
continue
|
dataface/core/compile/detect.py
CHANGED
|
@@ -60,7 +60,14 @@ def is_dataface_file(path: Path | str) -> bool:
|
|
|
60
60
|
if "faces" in path.parts:
|
|
61
61
|
return True
|
|
62
62
|
|
|
63
|
-
# Content detection: check for Dataface-specific keys
|
|
63
|
+
# Content detection: check for Dataface-specific keys.
|
|
64
|
+
# TODO(fileplugin): this content branch (and is_markdown_face) reads the path
|
|
65
|
+
# off disk, bypassing the Project/FilePlugin seam. Enumeration already routes
|
|
66
|
+
# through project.iter_faces, but routing content detection through the seam
|
|
67
|
+
# means threading a ProjectFile here and into the path-based is_markdown_face
|
|
68
|
+
# / resolve_meta_chain helpers in compiler.py, plus keeping the VS Code
|
|
69
|
+
# extension's duplicated detection in sync. Deferred as a follow-up; faces
|
|
70
|
+
# discovered under faces/ or named *.dataface.yml never reach this branch.
|
|
64
71
|
if path.exists():
|
|
65
72
|
try:
|
|
66
73
|
content = path.read_text(encoding="utf-8")
|
|
@@ -861,133 +861,6 @@ class Layer(BaseModel):
|
|
|
861
861
|
return data
|
|
862
862
|
|
|
863
863
|
|
|
864
|
-
# ============================================================================
|
|
865
|
-
# FILTER BINDING
|
|
866
|
-
# ============================================================================
|
|
867
|
-
|
|
868
|
-
# Canonical set of recognized filter operator keys.
|
|
869
|
-
# filter_injection._OP_MAP holds the sqlglot-class mapping for SQL injection;
|
|
870
|
-
# this set is the authoritative list for compile-time validation.
|
|
871
|
-
FILTER_OPS: frozenset[str] = frozenset(
|
|
872
|
-
{"eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "in", "not_in", "between"}
|
|
873
|
-
)
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
class FilterDef(BaseModel):
|
|
877
|
-
"""One column→variable binding in chart.filters.
|
|
878
|
-
|
|
879
|
-
Authored in YAML as a plain variable name (implicit eq), a Jinja template
|
|
880
|
-
(implicit eq, resolved at execute time), or a single-key operator dict:
|
|
881
|
-
|
|
882
|
-
filters:
|
|
883
|
-
region: selected_region → FilterDef(op="eq", var="selected_region")
|
|
884
|
-
region: "{{ selected_region }}" → FilterDef(op="eq", template="{{ selected_region }}")
|
|
885
|
-
revenue:
|
|
886
|
-
gte: min_revenue → FilterDef(op="gte", var="min_revenue")
|
|
887
|
-
|
|
888
|
-
Exactly one of ``var`` or ``template`` must be set. Operator dicts require
|
|
889
|
-
plain variable names; Jinja templates are not accepted in operator-dict values.
|
|
890
|
-
"""
|
|
891
|
-
|
|
892
|
-
model_config = ConfigDict(extra="forbid")
|
|
893
|
-
|
|
894
|
-
op: Literal[
|
|
895
|
-
"eq",
|
|
896
|
-
"neq",
|
|
897
|
-
"gt",
|
|
898
|
-
"gte",
|
|
899
|
-
"lt",
|
|
900
|
-
"lte",
|
|
901
|
-
"like",
|
|
902
|
-
"ilike",
|
|
903
|
-
"in",
|
|
904
|
-
"not_in",
|
|
905
|
-
"between",
|
|
906
|
-
] = Field(default="eq", description="Comparison operator. Defaults to 'eq'.")
|
|
907
|
-
var: str | None = Field(
|
|
908
|
-
default=None,
|
|
909
|
-
description="Plain variable name (no Jinja). Set when the filter value is a dashboard variable reference.",
|
|
910
|
-
)
|
|
911
|
-
template: str | None = Field(
|
|
912
|
-
default=None,
|
|
913
|
-
description="Jinja template resolved at execute time. Only valid with op='eq'. Set when conditional-disable or complex expressions are needed.",
|
|
914
|
-
)
|
|
915
|
-
|
|
916
|
-
@model_validator(mode="before")
|
|
917
|
-
@classmethod
|
|
918
|
-
def _from_yaml(cls, v: Any) -> dict[str, Any]:
|
|
919
|
-
"""Coerce authored YAML shapes into normalized dict form.
|
|
920
|
-
|
|
921
|
-
Accepts four input forms:
|
|
922
|
-
- plain variable name: "region_var" → {op: "eq", var: "region_var"}
|
|
923
|
-
- Jinja template: "{{ region_var }}" → {op: "eq", template: "{{ region_var }}"}
|
|
924
|
-
- single-key op dict: {"gte": "min_rev"} → {op: "gte", var: "min_rev"}
|
|
925
|
-
- already-normalized: {"op": X, "var": Y} — passed through as-is
|
|
926
|
-
{"op": X, "template": Y} — passed through as-is
|
|
927
|
-
"""
|
|
928
|
-
if isinstance(v, str):
|
|
929
|
-
if not v:
|
|
930
|
-
raise ValueError("filter value must be a non-empty string")
|
|
931
|
-
if "{{" in v or "}}" in v:
|
|
932
|
-
return {"op": "eq", "template": v}
|
|
933
|
-
return {"op": "eq", "var": v}
|
|
934
|
-
if isinstance(v, dict):
|
|
935
|
-
# Already-normalized form from programmatic construction — pass through.
|
|
936
|
-
if set(v.keys()) <= {"op", "var", "template"}:
|
|
937
|
-
return v
|
|
938
|
-
# YAML authored shape: single-key {operator: var_name} — plain var only.
|
|
939
|
-
if len(v) != 1:
|
|
940
|
-
raise ValueError(
|
|
941
|
-
f"operator dict must have exactly one key, got {sorted(v.keys())!r}"
|
|
942
|
-
)
|
|
943
|
-
op, var = next(iter(v.items()))
|
|
944
|
-
if op not in FILTER_OPS:
|
|
945
|
-
raise ValueError(
|
|
946
|
-
f"unknown operator {op!r}; must be one of {sorted(FILTER_OPS)}"
|
|
947
|
-
)
|
|
948
|
-
if not isinstance(var, str) or not var:
|
|
949
|
-
raise ValueError(
|
|
950
|
-
f"filters.{op}: variable name must be a non-empty string"
|
|
951
|
-
)
|
|
952
|
-
if "{{" in var or "}}" in var:
|
|
953
|
-
raise ValueError(
|
|
954
|
-
f"filters.{op}: Jinja templates are not allowed in operator-dict "
|
|
955
|
-
f"filter values; use a plain variable name instead of {var!r}"
|
|
956
|
-
)
|
|
957
|
-
return {"op": op, "var": var}
|
|
958
|
-
raise ValueError(
|
|
959
|
-
f"filter binding must be a variable name string or single-key operator dict, "
|
|
960
|
-
f"got {type(v).__name__}"
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
@model_validator(mode="after")
|
|
964
|
-
def _exactly_one(self) -> FilterDef:
|
|
965
|
-
"""Exactly one of var or template must be set; template requires op='eq'."""
|
|
966
|
-
if (self.var is None) == (self.template is None):
|
|
967
|
-
raise ValueError(
|
|
968
|
-
"FilterDef must set exactly one of var or template, "
|
|
969
|
-
f"got var={self.var!r}, template={self.template!r}"
|
|
970
|
-
)
|
|
971
|
-
if self.template is not None and self.op != "eq":
|
|
972
|
-
raise ValueError(
|
|
973
|
-
f"FilterDef.template is only valid with op='eq', got op={self.op!r}"
|
|
974
|
-
)
|
|
975
|
-
return self
|
|
976
|
-
|
|
977
|
-
def to_yaml_form(self) -> str | dict[str, str]:
|
|
978
|
-
"""Return the canonical YAML-authored representation.
|
|
979
|
-
|
|
980
|
-
Template bindings and implicit-eq bindings round-trip as a plain string;
|
|
981
|
-
all other operators round-trip as a single-key operator dict.
|
|
982
|
-
"""
|
|
983
|
-
if self.template is not None:
|
|
984
|
-
return self.template
|
|
985
|
-
assert self.var is not None # enforced by _exactly_one
|
|
986
|
-
if self.op == "eq":
|
|
987
|
-
return self.var
|
|
988
|
-
return {self.op: self.var}
|
|
989
|
-
|
|
990
|
-
|
|
991
864
|
# ============================================================================
|
|
992
865
|
# CHART FIELD BASES
|
|
993
866
|
# ============================================================================
|
|
@@ -1076,13 +949,6 @@ class _BaseChartFields(BaseModel):
|
|
|
1076
949
|
)
|
|
1077
950
|
return data
|
|
1078
951
|
|
|
1079
|
-
filters: Annotated[
|
|
1080
|
-
dict[str, FilterDef] | None,
|
|
1081
|
-
Field(
|
|
1082
|
-
default=None,
|
|
1083
|
-
description="Declarative column filters applied to chart data after query execution.",
|
|
1084
|
-
),
|
|
1085
|
-
]
|
|
1086
952
|
conditional_formatting: Annotated[
|
|
1087
953
|
dict[str, FieldConditionalFormatting] | None,
|
|
1088
954
|
Field(
|
|
@@ -1619,6 +1485,57 @@ class TableChart(_SharedChartFields):
|
|
|
1619
1485
|
TableChartStylePatch | None,
|
|
1620
1486
|
Field(default=None, description="Chart-local style overrides."),
|
|
1621
1487
|
]
|
|
1488
|
+
rows: Annotated[
|
|
1489
|
+
list[str] | None,
|
|
1490
|
+
Field(
|
|
1491
|
+
default=None,
|
|
1492
|
+
description=(
|
|
1493
|
+
"Fields whose distinct values form the row dimension of a pivot cross-tab. "
|
|
1494
|
+
"Each string is a column name from the query result. "
|
|
1495
|
+
"Omit for flat (non-pivot) tables."
|
|
1496
|
+
),
|
|
1497
|
+
),
|
|
1498
|
+
]
|
|
1499
|
+
columns: Annotated[
|
|
1500
|
+
list[str] | None,
|
|
1501
|
+
Field(
|
|
1502
|
+
default=None,
|
|
1503
|
+
description=(
|
|
1504
|
+
"Fields whose distinct values become column headers in a pivot cross-tab. "
|
|
1505
|
+
"Multiple fields create a nested multi-dimension pivot (outer → inner). "
|
|
1506
|
+
"Omit for flat tables."
|
|
1507
|
+
),
|
|
1508
|
+
),
|
|
1509
|
+
]
|
|
1510
|
+
values: Annotated[
|
|
1511
|
+
list[str] | None,
|
|
1512
|
+
Field(
|
|
1513
|
+
default=None,
|
|
1514
|
+
description=(
|
|
1515
|
+
"Measure fields that fill pivot cells. "
|
|
1516
|
+
"Each string is a column name from the query result. "
|
|
1517
|
+
"When omitted, all query columns not claimed by rows or columns are used."
|
|
1518
|
+
),
|
|
1519
|
+
),
|
|
1520
|
+
]
|
|
1521
|
+
|
|
1522
|
+
@model_validator(mode="after")
|
|
1523
|
+
def _validate_pivot_channels(self) -> TableChart:
|
|
1524
|
+
# A field may appear on at most one channel.
|
|
1525
|
+
seen: dict[str, str] = {}
|
|
1526
|
+
for channel, fields in (
|
|
1527
|
+
("rows", self.rows or []),
|
|
1528
|
+
("columns", self.columns or []),
|
|
1529
|
+
("values", self.values or []),
|
|
1530
|
+
):
|
|
1531
|
+
for f in fields:
|
|
1532
|
+
if f in seen:
|
|
1533
|
+
raise ValueError(
|
|
1534
|
+
f"Field {f!r} appears on both '{seen[f]}' and '{channel}' channels. "
|
|
1535
|
+
"A field may appear on at most one of rows/columns/values."
|
|
1536
|
+
)
|
|
1537
|
+
seen[f] = channel
|
|
1538
|
+
return self
|
|
1622
1539
|
|
|
1623
1540
|
|
|
1624
1541
|
# --- Geo family ---
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any, Literal, TypeGuard
|
|
12
12
|
|
|
13
|
-
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
14
14
|
|
|
15
15
|
from dataface.core.compile.models.chart.authored import (
|
|
16
16
|
BUILTIN_CHART_TYPE_VALUES,
|
|
@@ -19,7 +19,6 @@ from dataface.core.compile.models.chart.authored import (
|
|
|
19
19
|
ChartSort,
|
|
20
20
|
ChartTotal,
|
|
21
21
|
FieldConditionalFormatting,
|
|
22
|
-
FilterDef,
|
|
23
22
|
KpiSupportConfig,
|
|
24
23
|
Layer,
|
|
25
24
|
)
|
|
@@ -125,7 +124,7 @@ class Chart(BaseModel):
|
|
|
125
124
|
# Variable dependencies - computed during normalization
|
|
126
125
|
variable_dependencies: set[str] = Field(
|
|
127
126
|
default_factory=set,
|
|
128
|
-
description="Variable names this chart depends on (from title,
|
|
127
|
+
description="Variable names this chart depends on (from title, subtitle, query SQL).",
|
|
129
128
|
)
|
|
130
129
|
|
|
131
130
|
# Source location in YAML for edit-back support
|
|
@@ -233,10 +232,6 @@ class Chart(BaseModel):
|
|
|
233
232
|
link: str | None = Field(
|
|
234
233
|
default=None, description="Click-through URL template for drill-down links."
|
|
235
234
|
)
|
|
236
|
-
filters: dict[str, FilterDef] | None = Field(
|
|
237
|
-
default=None,
|
|
238
|
-
description="Declarative column filters applied to chart data after query execution.",
|
|
239
|
-
)
|
|
240
235
|
layers: list[Layer] | None = Field(
|
|
241
236
|
default=None, description="Layers for multi-mark charts (layered type)."
|
|
242
237
|
)
|
|
@@ -284,6 +279,57 @@ class Chart(BaseModel):
|
|
|
284
279
|
"Has no effect when chart.height is set. Promoted from per-chart style at compile time."
|
|
285
280
|
),
|
|
286
281
|
)
|
|
282
|
+
# --- Pivot encoding channels (table charts only) ---
|
|
283
|
+
rows: list[str] | None = Field(
|
|
284
|
+
default=None,
|
|
285
|
+
description=(
|
|
286
|
+
"Fields whose distinct values form the row dimension of a pivot cross-tab. "
|
|
287
|
+
"None = flat table (no pivot)."
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
# Justification for T | None: None means 'no pivot dimension' (flat table),
|
|
291
|
+
# which is a genuinely distinguishable authored state from an empty list.
|
|
292
|
+
columns: list[str] | None = Field(
|
|
293
|
+
default=None,
|
|
294
|
+
description=(
|
|
295
|
+
"Fields whose distinct values become column headers in a pivot cross-tab. "
|
|
296
|
+
"Multiple fields create a nested multi-dimension pivot (outer → inner). "
|
|
297
|
+
"None = flat table."
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
# Justification for T | None: None means 'infer values:* at render time',
|
|
301
|
+
# which is semantically distinct from an explicit empty list.
|
|
302
|
+
values: list[str] | None = Field(
|
|
303
|
+
default=None,
|
|
304
|
+
description=(
|
|
305
|
+
"Measure fields that fill pivot cells. "
|
|
306
|
+
"None = infer as all query columns not claimed by rows or columns."
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@model_validator(mode="after")
|
|
311
|
+
def _validate_pivot_channels(self) -> Chart:
|
|
312
|
+
"""Validate pivot channel invariants at the trust boundary.
|
|
313
|
+
|
|
314
|
+
Mirrors the authored-stage TableChart validator so the invariant also
|
|
315
|
+
holds for Chart objects built directly (tests, internal callers), not
|
|
316
|
+
only those that flow through AuthoredChart. Render trusts this.
|
|
317
|
+
"""
|
|
318
|
+
seen: dict[str, str] = {}
|
|
319
|
+
for channel, fields in (
|
|
320
|
+
("rows", self.rows or []),
|
|
321
|
+
("columns", self.columns or []),
|
|
322
|
+
("values", self.values or []),
|
|
323
|
+
):
|
|
324
|
+
for f in fields:
|
|
325
|
+
if f in seen:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
f"Field {f!r} appears on both {seen[f]!r} and {channel!r} "
|
|
328
|
+
"channels. A field may appear on at most one of rows/columns/values."
|
|
329
|
+
)
|
|
330
|
+
seen[f] = channel
|
|
331
|
+
return self
|
|
332
|
+
|
|
287
333
|
warnings_ignore: list[str] = Field(
|
|
288
334
|
default_factory=list,
|
|
289
335
|
description="Codes of render warnings to suppress for this chart.",
|
|
@@ -380,11 +426,6 @@ class Chart(BaseModel):
|
|
|
380
426
|
if self.link:
|
|
381
427
|
result["link"] = self.link
|
|
382
428
|
|
|
383
|
-
if self.filters:
|
|
384
|
-
result["filters"] = {
|
|
385
|
-
col: fd.to_yaml_form() for col, fd in self.filters.items()
|
|
386
|
-
}
|
|
387
|
-
|
|
388
429
|
return result
|
|
389
430
|
|
|
390
431
|
def get_dependencies(self) -> ChartDependencies:
|