dataface 0.4.1.dev5__py3-none-any.whl → 0.4.1.dev32__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 +12 -0
- dataface/agent_api/docs/yaml-reference.md +0 -4
- dataface/ai/skills/dashboard-pack-scaffolding/SKILL.md +1 -1
- dataface/cli/_error_format.py +13 -9
- dataface/cli/commands/render.py +25 -7
- dataface/cli/main.py +8 -8
- dataface/core/compile/chart_focus.py +13 -2
- dataface/core/compile/jinja.py +19 -3
- dataface/core/compile/models/chart/authored/__init__.py +0 -2
- dataface/core/compile/models/chart/authored/_conditional_formatting.py +2 -18
- dataface/core/compile/models/style/theme/variables.py +0 -19
- dataface/core/compile/parameterized.py +59 -1
- dataface/core/compile/sizing.py +239 -5
- dataface/core/dashboard.py +1 -6
- dataface/core/defaults/themes/_base.yaml +0 -7
- dataface/core/defaults/themes/cream.yaml +0 -3
- dataface/core/defaults/themes/neon.yaml +0 -1
- dataface/core/diagnostics/codes_compile.py +5 -0
- dataface/core/diagnostics/codes_render.py +1 -0
- dataface/core/diagnostics/registry.py +6 -0
- dataface/core/render/__init__.py +0 -2
- dataface/core/render/chart/kpi.py +17 -13
- dataface/core/render/chart/rendering.py +7 -7
- dataface/core/render/chart/table.py +4 -2
- dataface/core/render/chart/table_support.py +3 -3
- dataface/core/render/controls.py +26 -476
- dataface/core/render/converters/chart.py +20 -0
- dataface/core/render/converters/html.py +5 -8
- dataface/core/render/faces.py +45 -35
- dataface/core/render/layout_sizing.py +2 -19
- dataface/core/render/render_result.py +0 -3
- dataface/core/render/renderer.py +7 -25
- dataface/core/render/sizing.py +21 -152
- dataface/core/render/svg_utils.py +19 -6
- dataface/core/render/templates/controls/_styles.css +40 -658
- dataface/core/render/templates/page.html +1 -2
- dataface/core/render/templates/scripts/variables.js +582 -1129
- dataface/core/render/templates/svg/styles.css +29 -6
- dataface/core/render/variables_resolve.py +59 -6
- dataface/core/render/variables_strip.py +113 -9
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +2 -1
- dataface/core/utils.py +7 -3
- dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -31
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev32.dist-info}/METADATA +1 -2
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev32.dist-info}/RECORD +48 -58
- dataface/core/render/templates/variable_controls/checkbox.html +0 -6
- dataface/core/render/templates/variable_controls/date.html +0 -6
- dataface/core/render/templates/variable_controls/daterange.html +0 -16
- dataface/core/render/templates/variable_controls/layer.html +0 -28
- dataface/core/render/templates/variable_controls/multiselect.html +0 -19
- dataface/core/render/templates/variable_controls/number.html +0 -6
- dataface/core/render/templates/variable_controls/readonly.html +0 -4
- dataface/core/render/templates/variable_controls/select.html +0 -13
- dataface/core/render/templates/variable_controls/slider.html +0 -9
- dataface/core/render/templates/variable_controls/text.html +0 -6
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev32.dist-info}/WHEEL +0 -0
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev32.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev32.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -957,6 +957,18 @@ WHERE plan IN ({{ plans | map('tojson') | join(', ') }})
|
|
|
957
957
|
WHERE {{ filter('plan', plans) }}
|
|
958
958
|
```
|
|
959
959
|
|
|
960
|
+
**Numeric arithmetic antipattern** — `{{ n | int }}` and `{{ n | float }}` raise an error on parameterized variables. Jinja's `| int` filter calls `int(value)` internally; on a parameterized variable this would silently return 0, so Dataface rejects it instead. Write SQL arithmetic directly on the variable:
|
|
961
|
+
|
|
962
|
+
```sql
|
|
963
|
+
-- WRONG: raises ERR-JINJA-ERROR at render time
|
|
964
|
+
INTERVAL -({{ months | int - 1 }}) MONTH
|
|
965
|
+
|
|
966
|
+
-- CORRECT: {{ months }} emits the bound parameter; the database handles arithmetic
|
|
967
|
+
INTERVAL -({{ months }} - 1) MONTH
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
In a direct query (no `{{ queries.X }}` references), `{{ n }}` emits a bound parameter (`$1`, `%s`, `?` depending on dialect). In a composed query (one using `{{ queries.X }}`), variables are resolved as literals before the parameterization pass — `{{ n }}` with `n: 3` becomes the literal `3`. In both cases, `{{ n | int }}` and `{{ n | float }}` raise — so adding or removing a `{{ queries.X }}` reference cannot change the value a filter expression produces. Arithmetic, casting, and type coercion belong in the SQL expression around the variable, not in a Jinja filter.
|
|
971
|
+
|
|
960
972
|
Common mistakes (the validator rejects these — listed here so authors don't hit them):
|
|
961
973
|
|
|
962
974
|
- `options.values: [...]` → use `options.static: [...]`.
|
|
@@ -1177,13 +1177,10 @@ Authored overlay for VariablesStyle. Variable controls chrome styling.
|
|
|
1177
1177
|
| `label` | [VariablesLabelStyle](#variableslabelstyle) | ✓ | Variable label typography. |
|
|
1178
1178
|
| `value` | [VariablesValueStyle](#variablesvaluestyle) | ✓ | Variable value typography. |
|
|
1179
1179
|
| `placeholder` | [VariablesPlaceholderStyle](#variablesplaceholderstyle) | ✓ | Style for unselected/hint text in variable inputs. |
|
|
1180
|
-
| `padding` | float | ✓ | Outer padding around the variables panel in pixels. |
|
|
1181
1180
|
| `container_height` | float | ✓ | Height of the variables panel container in pixels. |
|
|
1182
|
-
| `container_padding` | float | ✓ | Inner padding of the variables panel container in pixels. |
|
|
1183
1181
|
| `border` | [BorderStyle](#borderstyle) | ✓ | Variables panel border style. |
|
|
1184
1182
|
| `control_gap` | float | ✓ | Gap between label and input within a single control in pixels. |
|
|
1185
1183
|
| `input` | [InputStyle](#inputstyle) | ✓ | Input control style. |
|
|
1186
|
-
| `popover_rail_background` | str | ✓ | Background color for the preset rail (left column) inside the daterange chip's popover. Typically the theme's `surface-subtle` step — gray-50 on white-canvas themes, a cream-toned light on warm-canvas themes. Distinct from the popover card itself, which is always white-equivalent. |
|
|
1187
1184
|
|
|
1188
1185
|
<a id="pagestyle"></a>
|
|
1189
1186
|
## PageStyle
|
|
@@ -2076,7 +2073,6 @@ Authored overlay for VariablesValueStyle. Per-value font substyle for variable c
|
|
|
2076
2073
|
| Field | Type | Optional | Description |
|
|
2077
2074
|
|-------|------|:--------:|-------------|
|
|
2078
2075
|
| `font` | [FontStyle](#fontstyle) | ✓ | Variable value font style overrides. Unset fields fall back to [`style.variables.font`](#variablesstyle). |
|
|
2079
|
-
| `numeric_variant` | enum: "normal", "tabular-nums" | ✓ | OpenType numeric feature for value displays ('normal' or 'tabular-nums'). |
|
|
2080
2076
|
|
|
2081
2077
|
<a id="variablesplaceholderstyle"></a>
|
|
2082
2078
|
## VariablesPlaceholderStyle
|
|
@@ -153,7 +153,7 @@ in queries.
|
|
|
153
153
|
dft serve
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
Visit a landing page (e.g. `http://localhost:
|
|
156
|
+
Visit a landing page (e.g. `http://localhost:8001/zendesk/`) and click one of
|
|
157
157
|
the data links. It should redirect to the authored face. If it serves the
|
|
158
158
|
generic system view instead, the alias is not set — check the face file.
|
|
159
159
|
|
dataface/cli/_error_format.py
CHANGED
|
@@ -15,6 +15,7 @@ not from a caller-supplied flag.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import sys
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from rich.markup import escape
|
|
@@ -50,25 +51,28 @@ def print_error(message: str) -> None:
|
|
|
50
51
|
console.print(f"[bold red]Error:[/] {message}")
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def emit_diagnostics_jsonl(diags: list[Diagnostic]) -> None:
|
|
55
|
+
"""Write diagnostics as JSON Lines to stderr — one compact JSON object per line.
|
|
56
|
+
|
|
57
|
+
Callers use this for the ``--diagnostics-json`` path: every diagnostic
|
|
58
|
+
(error or warning) is emitted to stderr so stdout stays the render payload,
|
|
59
|
+
and line-delimited JSON survives a truncated read with whole objects intact.
|
|
60
|
+
"""
|
|
61
|
+
for d in diags:
|
|
62
|
+
print(json.dumps(d.model_dump(exclude_none=True)), file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
|
|
53
65
|
def print_diagnostics(
|
|
54
66
|
diags: list[Diagnostic],
|
|
55
67
|
*,
|
|
56
|
-
json_output: bool = False,
|
|
57
68
|
path: Path | None = None,
|
|
58
69
|
) -> None:
|
|
59
|
-
"""Print Diagnostics as Rich panels
|
|
70
|
+
"""Print Diagnostics as Rich panels to stderr.
|
|
60
71
|
|
|
61
72
|
`path` is a fallback "At:" location used only when a diagnostic has no
|
|
62
73
|
`range` of its own (e.g. compile-time warnings that don't carry position
|
|
63
74
|
data yet).
|
|
64
75
|
"""
|
|
65
|
-
if json_output:
|
|
66
|
-
print(
|
|
67
|
-
json.dumps(
|
|
68
|
-
{"errors": [d.model_dump(exclude_none=True) for d in diags]}, indent=2
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
return
|
|
72
76
|
console = dft_console(stderr=True)
|
|
73
77
|
for d in diags:
|
|
74
78
|
style = "red" if d.level == "error" else "yellow"
|
dataface/cli/commands/render.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from dataface.agent_api import Diagnostic, RenderFormat
|
|
13
|
-
from dataface.cli._error_format import print_diagnostics
|
|
13
|
+
from dataface.cli._error_format import emit_diagnostics_jsonl, print_diagnostics
|
|
14
14
|
from dataface.cli._project import with_project
|
|
15
15
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
16
16
|
|
|
@@ -119,6 +119,7 @@ def _emit_result(
|
|
|
119
119
|
no_warnings: bool,
|
|
120
120
|
fail_on_chart_errors: bool,
|
|
121
121
|
print0: bool,
|
|
122
|
+
diagnostics_json: bool = False,
|
|
122
123
|
) -> str | None:
|
|
123
124
|
"""Turn a completed render into terminal/file output, or raise RenderFailed.
|
|
124
125
|
|
|
@@ -136,7 +137,11 @@ def _emit_result(
|
|
|
136
137
|
if fail_on_chart_errors and result.chart_errors:
|
|
137
138
|
raise RenderFailed(result.chart_errors)
|
|
138
139
|
|
|
139
|
-
if
|
|
140
|
+
if diagnostics_json:
|
|
141
|
+
all_diags = list(result.warnings) + list(result.chart_errors)
|
|
142
|
+
if all_diags:
|
|
143
|
+
emit_diagnostics_jsonl(all_diags)
|
|
144
|
+
elif not no_warnings:
|
|
140
145
|
_print_warnings(result.warnings)
|
|
141
146
|
|
|
142
147
|
if format in ("json", "data"):
|
|
@@ -173,6 +178,7 @@ def render_command(
|
|
|
173
178
|
max_workers: int | None = None,
|
|
174
179
|
print0: bool = False,
|
|
175
180
|
chart: str | None = None,
|
|
181
|
+
diagnostics_json: bool = False,
|
|
176
182
|
*,
|
|
177
183
|
project: FilesystemProject,
|
|
178
184
|
) -> str | None:
|
|
@@ -258,6 +264,7 @@ def render_command(
|
|
|
258
264
|
no_warnings=no_warnings,
|
|
259
265
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
260
266
|
print0=print0,
|
|
267
|
+
diagnostics_json=diagnostics_json,
|
|
261
268
|
)
|
|
262
269
|
|
|
263
270
|
|
|
@@ -269,7 +276,7 @@ def render_command_from_yaml(
|
|
|
269
276
|
variables: dict[str, str] | None = None,
|
|
270
277
|
use_cache: bool = True,
|
|
271
278
|
cache_path: Path | None = None,
|
|
272
|
-
|
|
279
|
+
diagnostics_json: bool = False,
|
|
273
280
|
no_warnings: bool = False,
|
|
274
281
|
ignore_codes: set[str] | None = None,
|
|
275
282
|
fail_on_chart_errors: bool = True,
|
|
@@ -296,7 +303,8 @@ def render_command_from_yaml(
|
|
|
296
303
|
if absent), overriding project config. None (the default) falls
|
|
297
304
|
through to the project's dataface.yml cache: block (path: null
|
|
298
305
|
there means the zero-config in-memory default).
|
|
299
|
-
|
|
306
|
+
diagnostics_json: Emit all diagnostics (errors and warnings) as JSON Lines
|
|
307
|
+
to stderr. Stdout stays the render payload on success.
|
|
300
308
|
no_warnings: Suppress stderr warning output (warnings still appear in JSON)
|
|
301
309
|
ignore_codes: Warning codes to suppress via the partition seam
|
|
302
310
|
fail_on_chart_errors: Exit 1 when any per-chart runtime error exists
|
|
@@ -354,9 +362,14 @@ def render_command_from_yaml(
|
|
|
354
362
|
no_warnings=no_warnings,
|
|
355
363
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
356
364
|
print0=print0,
|
|
365
|
+
diagnostics_json=diagnostics_json,
|
|
357
366
|
)
|
|
358
367
|
except RenderFailed as exc:
|
|
359
|
-
|
|
368
|
+
if exc.errors:
|
|
369
|
+
if diagnostics_json:
|
|
370
|
+
emit_diagnostics_jsonl(exc.errors)
|
|
371
|
+
else:
|
|
372
|
+
print_diagnostics(exc.errors)
|
|
360
373
|
raise typer.Exit(1) from exc
|
|
361
374
|
|
|
362
375
|
|
|
@@ -436,7 +449,7 @@ def render_commands(
|
|
|
436
449
|
variables: dict[str, str] | None = None,
|
|
437
450
|
use_cache: bool = True,
|
|
438
451
|
cache_path: Path | None = None,
|
|
439
|
-
|
|
452
|
+
diagnostics_json: bool = False,
|
|
440
453
|
no_warnings: bool = False,
|
|
441
454
|
ignore_codes: set[str] | None = None,
|
|
442
455
|
fail_on_chart_errors: bool = True,
|
|
@@ -491,9 +504,14 @@ def render_commands(
|
|
|
491
504
|
max_workers=max_workers,
|
|
492
505
|
print0=print0,
|
|
493
506
|
chart=chart,
|
|
507
|
+
diagnostics_json=diagnostics_json,
|
|
494
508
|
)
|
|
495
509
|
except RenderFailed as exc:
|
|
496
|
-
|
|
510
|
+
if exc.errors:
|
|
511
|
+
if diagnostics_json:
|
|
512
|
+
emit_diagnostics_jsonl(exc.errors)
|
|
513
|
+
else:
|
|
514
|
+
print_diagnostics(exc.errors)
|
|
497
515
|
has_failure = True
|
|
498
516
|
if fail_fast:
|
|
499
517
|
raise typer.Exit(1) from exc
|
dataface/cli/main.py
CHANGED
|
@@ -718,14 +718,14 @@ def render(
|
|
|
718
718
|
),
|
|
719
719
|
),
|
|
720
720
|
] = None,
|
|
721
|
-
|
|
721
|
+
diagnostics_json: Annotated[
|
|
722
722
|
bool,
|
|
723
723
|
typer.Option(
|
|
724
|
-
"--json
|
|
724
|
+
"--diagnostics-json",
|
|
725
725
|
help=(
|
|
726
|
-
"Emit errors as JSON to
|
|
727
|
-
"
|
|
728
|
-
"
|
|
726
|
+
"Emit all diagnostics (errors and warnings) as JSON Lines to stderr. "
|
|
727
|
+
"Stdout stays the render payload on success; stderr carries "
|
|
728
|
+
"one compact JSON object per diagnostic, line-delimited."
|
|
729
729
|
),
|
|
730
730
|
),
|
|
731
731
|
] = False,
|
|
@@ -830,7 +830,7 @@ def render(
|
|
|
830
830
|
dft render faces/sales.yml --format terminal
|
|
831
831
|
dft render faces/sales.yml --no-cache
|
|
832
832
|
dft render faces/sales.yml --cache renders.duckdb
|
|
833
|
-
dft render faces/sales.yml --json
|
|
833
|
+
dft render faces/sales.yml --diagnostics-json
|
|
834
834
|
dft render faces/sales.yml --no-warnings
|
|
835
835
|
dft render faces/sales.yml --ignore-warning WARN-REDUNDANT-ENCODING
|
|
836
836
|
dft render faces/sales.yml --allow-chart-errors
|
|
@@ -891,7 +891,7 @@ def render(
|
|
|
891
891
|
variables=variables or None,
|
|
892
892
|
use_cache=use_cache,
|
|
893
893
|
cache_path=cache,
|
|
894
|
-
|
|
894
|
+
diagnostics_json=diagnostics_json,
|
|
895
895
|
no_warnings=no_warnings,
|
|
896
896
|
ignore_codes=ignore_codes,
|
|
897
897
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
@@ -909,7 +909,7 @@ def render(
|
|
|
909
909
|
variables=variables or None,
|
|
910
910
|
use_cache=use_cache,
|
|
911
911
|
cache_path=cache,
|
|
912
|
-
|
|
912
|
+
diagnostics_json=diagnostics_json,
|
|
913
913
|
no_warnings=no_warnings,
|
|
914
914
|
ignore_codes=ignore_codes,
|
|
915
915
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
@@ -42,6 +42,7 @@ from dataface.core.compile.models.face.normalized import (
|
|
|
42
42
|
Layout,
|
|
43
43
|
LayoutItem,
|
|
44
44
|
)
|
|
45
|
+
from dataface.core.compile.sizing import chart_slot_width
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
@@ -50,6 +51,9 @@ def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
|
50
51
|
Creates a new face with:
|
|
51
52
|
- Only the specified chart in the layout
|
|
52
53
|
- Only the variables that chart depends on
|
|
54
|
+
- The chart pinned at the slot width the dashboard layout assigned it,
|
|
55
|
+
so the focused render is geometry-identical to the dashboard (WYSIWYG);
|
|
56
|
+
a chart not placed in the layout keeps its preferred width
|
|
53
57
|
- All sources and queries unchanged (they're lazy-loaded anyway)
|
|
54
58
|
|
|
55
59
|
This is called automatically when `chart_focus` is set in the YAML.
|
|
@@ -88,8 +92,15 @@ def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
|
88
92
|
if name in chart.variable_dependencies
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
#
|
|
92
|
-
|
|
95
|
+
# Pin the dashboard slot width so the focused chart keeps the exact
|
|
96
|
+
# geometry it had on the board (band widths, axis-label fit). The board
|
|
97
|
+
# container hugs the pinned width the same way it hugs preferred widths.
|
|
98
|
+
slot_width = chart_slot_width(face, chart_id)
|
|
99
|
+
chart_item = LayoutItem(
|
|
100
|
+
type="chart",
|
|
101
|
+
chart=chart,
|
|
102
|
+
user_width=None if slot_width is None else f"{slot_width}px",
|
|
103
|
+
)
|
|
93
104
|
simple_layout = Layout(type=LayoutType.ROWS, items=[chart_item])
|
|
94
105
|
|
|
95
106
|
# Create new face with focused content
|
dataface/core/compile/jinja.py
CHANGED
|
@@ -48,6 +48,7 @@ from jinja2 import (
|
|
|
48
48
|
|
|
49
49
|
from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
|
|
50
50
|
from dataface.core.compile.errors import CompilationError, JinjaError
|
|
51
|
+
from dataface.core.compile.parameterized import _LiteralCollector, _ParameterizedValue
|
|
51
52
|
|
|
52
53
|
logger = logging.getLogger(__name__)
|
|
53
54
|
|
|
@@ -554,10 +555,25 @@ def resolve_query_sql_with_dependencies(
|
|
|
554
555
|
# Phase 2: resolve variables via Jinja. filter()/filter_date_range() calls
|
|
555
556
|
# are deferred (left verbatim) so they reach the SQL adapter's parameterized
|
|
556
557
|
# pass instead of the raising compile-time stub — see resolve_jinja_template.
|
|
558
|
+
#
|
|
559
|
+
# Variables are wrapped in _ParameterizedValue with a _LiteralCollector so
|
|
560
|
+
# that {{ n }} still inlines as a literal (the reference-path contract) while
|
|
561
|
+
# {{ n | int }} / {{ n | float }} raise — matching the direct-path behavior
|
|
562
|
+
# and preventing silent value changes when {{ queries.X }} refs are added or
|
|
563
|
+
# removed. Using the same wrapper class as the direct path ensures equality,
|
|
564
|
+
# membership, and iteration operators delegate correctly to the raw value.
|
|
557
565
|
if target_sql and ("{{" in target_sql or "{%" in target_sql):
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
566
|
+
_collector = _LiteralCollector()
|
|
567
|
+
guarded_vars = {
|
|
568
|
+
name: _ParameterizedValue(name, val, _collector)
|
|
569
|
+
for name, val in (variables.items() if variables else ())
|
|
570
|
+
}
|
|
571
|
+
try:
|
|
572
|
+
target_sql = resolve_jinja_template(
|
|
573
|
+
target_sql, variables=guarded_vars, defer_filters=True
|
|
574
|
+
)
|
|
575
|
+
except (TypeError, KeyError) as e:
|
|
576
|
+
raise JinjaError(f"Template error: {e}", target_sql) from e
|
|
561
577
|
|
|
562
578
|
return target_sql
|
|
563
579
|
|
|
@@ -64,7 +64,6 @@ from ._conditional_formatting import (
|
|
|
64
64
|
ConditionalRule,
|
|
65
65
|
FieldConditionalFormatting,
|
|
66
66
|
_PredicateBase,
|
|
67
|
-
coerce_numeric,
|
|
68
67
|
match_predicate,
|
|
69
68
|
)
|
|
70
69
|
from ._data_table import (
|
|
@@ -211,7 +210,6 @@ __all__ = [
|
|
|
211
210
|
"FieldConditionalFormatting",
|
|
212
211
|
"_PREDICATE_OPS",
|
|
213
212
|
"_PredicateBase",
|
|
214
|
-
"coerce_numeric",
|
|
215
213
|
"match_predicate",
|
|
216
214
|
# data_table
|
|
217
215
|
"CHART_DATA_TABLE_SUPPORTED_TYPES",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import math
|
|
6
5
|
from typing import Any, Literal
|
|
7
6
|
|
|
8
7
|
from pydantic import (
|
|
@@ -20,6 +19,7 @@ from dataface.core.compile.models.style.theme import (
|
|
|
20
19
|
VALID_FONT_WEIGHTS,
|
|
21
20
|
font_weight_as_css,
|
|
22
21
|
)
|
|
22
|
+
from dataface.core.utils import coerce_numeric_cell
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class _PredicateBase(BaseModel):
|
|
@@ -120,22 +120,6 @@ def _validate_exactly_one_predicate(obj: Any) -> None:
|
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def coerce_numeric(v: Any) -> float | None:
|
|
124
|
-
"""Return float(v) for finite numeric-compatible values; None otherwise."""
|
|
125
|
-
if v is None or isinstance(v, bool):
|
|
126
|
-
return None
|
|
127
|
-
if isinstance(v, (int, float)):
|
|
128
|
-
result = float(v)
|
|
129
|
-
return result if math.isfinite(result) else None
|
|
130
|
-
if isinstance(v, str):
|
|
131
|
-
try:
|
|
132
|
-
result = float(v)
|
|
133
|
-
return result if math.isfinite(result) else None
|
|
134
|
-
except (ValueError, TypeError):
|
|
135
|
-
return None
|
|
136
|
-
return None
|
|
137
|
-
|
|
138
|
-
|
|
139
123
|
def _in_value_matches(candidate: Any, value: Any) -> bool:
|
|
140
124
|
"""Equality check for a single ``in`` candidate, with the bool/int guard."""
|
|
141
125
|
if isinstance(value, bool) != isinstance(candidate, bool):
|
|
@@ -159,7 +143,7 @@ def match_predicate(rule: _PredicateBase, value: Any) -> bool:
|
|
|
159
143
|
return value != rule.ne
|
|
160
144
|
if rule.in_ is not None:
|
|
161
145
|
return any(_in_value_matches(candidate, value) for candidate in rule.in_)
|
|
162
|
-
v_coerced =
|
|
146
|
+
v_coerced = coerce_numeric_cell(value)
|
|
163
147
|
if v_coerced is None:
|
|
164
148
|
return False
|
|
165
149
|
v = v_coerced
|
|
@@ -40,10 +40,6 @@ class VariablesValueStyle(BaseModel):
|
|
|
40
40
|
default_factory=FontStyle,
|
|
41
41
|
description="Variable value font style overrides.",
|
|
42
42
|
)
|
|
43
|
-
# OpenType numeric feature for slider, number, and date-range value displays.
|
|
44
|
-
numeric_variant: Literal["normal", "tabular-nums"] = Field(
|
|
45
|
-
description="OpenType numeric feature for value displays ('normal' or 'tabular-nums')."
|
|
46
|
-
)
|
|
47
43
|
|
|
48
44
|
|
|
49
45
|
class VariablesPlaceholderStyle(BaseModel):
|
|
@@ -107,26 +103,11 @@ class VariablesStyle(BaseModel):
|
|
|
107
103
|
placeholder: VariablesPlaceholderStyle = Field(
|
|
108
104
|
description="Style for unselected/hint text in variable inputs."
|
|
109
105
|
)
|
|
110
|
-
padding: float = Field(
|
|
111
|
-
description="Outer padding around the variables panel in pixels."
|
|
112
|
-
)
|
|
113
106
|
container_height: float = Field(
|
|
114
107
|
description="Height of the variables panel container in pixels."
|
|
115
108
|
)
|
|
116
|
-
container_padding: float = Field(
|
|
117
|
-
description="Inner padding of the variables panel container in pixels."
|
|
118
|
-
)
|
|
119
109
|
border: BorderStyle = Field(description="Variables panel border style.")
|
|
120
110
|
control_gap: float = Field(
|
|
121
111
|
description="Gap between label and input within a single control in pixels."
|
|
122
112
|
)
|
|
123
113
|
input: InputStyle = Field(description="Input control style.")
|
|
124
|
-
popover_rail_background: str = Field(
|
|
125
|
-
description=(
|
|
126
|
-
"Background color for the preset rail (left column) inside the "
|
|
127
|
-
"daterange chip's popover. Typically the theme's `surface-subtle` "
|
|
128
|
-
"step — gray-50 on white-canvas themes, a cream-toned light on "
|
|
129
|
-
"warm-canvas themes. Distinct from the popover card itself, which "
|
|
130
|
-
"is always white-equivalent."
|
|
131
|
-
)
|
|
132
|
-
)
|
|
@@ -209,13 +209,27 @@ class _NullValue:
|
|
|
209
209
|
return not self.__eq__(other)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
class _LiteralCollector:
|
|
213
|
+
"""Passthrough collector for _ParameterizedValue on the Phase-2 composed-query path.
|
|
214
|
+
|
|
215
|
+
Variables on this path inline as literal SQL (the reference-path contract): {{ n }}
|
|
216
|
+
becomes the raw value, not a $N parameter. Returning "NULL" for None matches
|
|
217
|
+
the _NullValue behavior on the direct parameterized path.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def get_param(self, name: str, value: Any) -> str:
|
|
221
|
+
return "NULL" if value is None else str(value)
|
|
222
|
+
|
|
223
|
+
|
|
212
224
|
class _ParameterizedValue:
|
|
213
225
|
"""Wrapper for parameterized variable values.
|
|
214
226
|
|
|
215
227
|
When rendered by Jinja, returns the parameter placeholder.
|
|
216
228
|
"""
|
|
217
229
|
|
|
218
|
-
def __init__(
|
|
230
|
+
def __init__(
|
|
231
|
+
self, name: str, value: Any, collector: _ParameterCollector | _LiteralCollector
|
|
232
|
+
):
|
|
219
233
|
self._name = name
|
|
220
234
|
self._value = value
|
|
221
235
|
self._collector = collector
|
|
@@ -247,6 +261,50 @@ class _ParameterizedValue:
|
|
|
247
261
|
return self._value[key]
|
|
248
262
|
raise TypeError(f"'{type(self._value).__name__}' is not subscriptable")
|
|
249
263
|
|
|
264
|
+
def __int__(self) -> int:
|
|
265
|
+
# Jinja's | int filter calls int(value) and catches TypeError, returning
|
|
266
|
+
# 0 on failure — so without this override the filter silently emits 0.
|
|
267
|
+
# Raising here bypasses that swallowing and surfaces the error.
|
|
268
|
+
if isinstance(self._collector, _LiteralCollector):
|
|
269
|
+
raise JinjaError(
|
|
270
|
+
f"Variable '{self._name}': the '| int' filter cannot coerce a face "
|
|
271
|
+
f"variable in a composed query — it would silently return 0. "
|
|
272
|
+
f"Use SQL arithmetic directly: {{{{ {self._name} }}}} - 1 inlines "
|
|
273
|
+
f"the value minus 1."
|
|
274
|
+
)
|
|
275
|
+
raise JinjaError(
|
|
276
|
+
f"Variable '{self._name}': the '| int' filter cannot coerce a parameterized "
|
|
277
|
+
f"variable — it would silently return 0. Use SQL arithmetic on the variable "
|
|
278
|
+
f"directly instead: {{{{ {self._name} }}}} - 1 emits $N - 1 where $N is "
|
|
279
|
+
f"the bound parameter (e.g. INTERVAL -({{{{ {self._name} }}}} - 1) MONTH)."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def __index__(self) -> int:
|
|
283
|
+
if isinstance(self._collector, _LiteralCollector):
|
|
284
|
+
raise JinjaError(
|
|
285
|
+
f"Variable '{self._name}': integer index conversion is not supported "
|
|
286
|
+
f"on face variables in a composed query."
|
|
287
|
+
)
|
|
288
|
+
raise JinjaError(
|
|
289
|
+
f"Variable '{self._name}': integer index conversion is not supported "
|
|
290
|
+
f"on parameterized variables. Use SQL arithmetic on the variable "
|
|
291
|
+
f"directly instead: {{{{ {self._name} }}}} - 1 emits the bound parameter "
|
|
292
|
+
f"minus 1."
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def __float__(self) -> float:
|
|
296
|
+
if isinstance(self._collector, _LiteralCollector):
|
|
297
|
+
raise JinjaError(
|
|
298
|
+
f"Variable '{self._name}': the '| float' filter cannot coerce a face "
|
|
299
|
+
f"variable in a composed query — it would silently return 0.0. "
|
|
300
|
+
f"Use SQL arithmetic directly: {{{{ {self._name} }}}} * 1.0 or cast in SQL."
|
|
301
|
+
)
|
|
302
|
+
raise JinjaError(
|
|
303
|
+
f"Variable '{self._name}': the '| float' filter cannot coerce a parameterized "
|
|
304
|
+
f"variable — it would silently return 0.0. Use SQL arithmetic on the variable "
|
|
305
|
+
f"directly instead: {{{{ {self._name} }}}} * 1.0 or cast in SQL."
|
|
306
|
+
)
|
|
307
|
+
|
|
250
308
|
|
|
251
309
|
def _compute_template_hash(template: str) -> str:
|
|
252
310
|
"""Compute hash of template for caching.
|