dataface 0.4.1.dev134__py3-none-any.whl → 0.4.1.dev224__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 +2 -0
- dataface/agent_api/_init_templates/meta.yaml +3 -3
- dataface/agent_api/dashboards.py +21 -4
- dataface/agent_api/docs/yaml-reference.md +8 -8
- dataface/agent_api/pack.py +1 -1
- dataface/agent_api/query.py +59 -5
- dataface/agent_api/serve.py +1 -3
- dataface/ai/llm.py +59 -13
- dataface/ai/tools/__init__.py +11 -4
- dataface/cli/commands/board_artifact.py +1 -1
- dataface/cli/commands/extension.py +1 -0
- dataface/cli/commands/serve.py +5 -14
- dataface/cli/commands/skills_init.py +1 -2
- dataface/cli/main.py +29 -13
- dataface/core/compile/compiler.py +51 -27
- dataface/core/compile/config.py +71 -5
- dataface/core/compile/inherit_resolver.py +3 -3
- dataface/core/compile/label_data.py +3 -3
- dataface/core/compile/labels_env.py +14 -2
- dataface/core/compile/migrations/migrations.py +3 -1
- dataface/core/compile/migrations/versions/__init__.py +16 -0
- dataface/core/compile/models/chart/resolved/pie.py +10 -0
- dataface/core/compile/models/config.py +20 -2
- dataface/core/compile/models/face/authored.py +3 -4
- dataface/core/compile/models/face/normalized.py +2 -1
- dataface/core/compile/models/face/resolved.py +3 -3
- dataface/core/compile/models/primitives.py +7 -0
- dataface/core/compile/models/style/resolved/_base.py +68 -5
- dataface/core/compile/models/style/resolved/_cascade.py +341 -55
- dataface/core/compile/models/style/theme/__init__.py +3 -3
- dataface/core/compile/models/style/theme/board.py +3 -3
- dataface/core/compile/models/style/theme/style.py +2 -2
- dataface/core/compile/models/style/theme/table.py +7 -1
- dataface/core/compile/models/variable/authored.py +5 -1
- dataface/core/compile/normalize_layout.py +1 -1
- dataface/core/compile/normalize_queries.py +1 -3
- dataface/core/compile/normalize_variables.py +10 -3
- dataface/core/compile/normalizer.py +93 -19
- dataface/core/compile/parameterized.py +9 -10
- dataface/core/compile/resolve/_axes.py +71 -24
- dataface/core/compile/resolve/_domain.py +34 -1
- dataface/core/compile/resolve/adaptive_stroke.py +7 -3
- dataface/core/compile/resolve/area.py +32 -6
- dataface/core/compile/resolve/bar.py +167 -17
- dataface/core/compile/resolve/heatmap.py +10 -2
- dataface/core/compile/resolve/line.py +30 -6
- dataface/core/compile/resolve/pie.py +23 -6
- dataface/core/compile/resolve/pie_attachment.py +25 -2
- dataface/core/compile/resolve/scatter.py +16 -2
- dataface/core/compile/schema_renderers/yaml_schema_catalog.py +4 -2
- dataface/core/compile/sizing.py +8 -8
- dataface/core/compile/source_map.py +56 -76
- dataface/core/compile/sources.py +2 -4
- dataface/core/compile/style_cascade.py +79 -13
- dataface/core/compile/typography.py +9 -9
- dataface/core/compile/validate_formats.py +33 -1
- dataface/core/compile/yaml_error_formatter.py +1 -2
- dataface/core/compile/yaml_patch.py +1 -1
- dataface/core/defaults/default_config.yml +42 -3
- dataface/core/defaults/themes/_base.yaml +1 -1
- dataface/core/defaults/themes/stark.yaml +6 -4
- dataface/core/diagnostics/__init__.py +18 -0
- dataface/core/diagnostics/codes_compile.py +34 -0
- dataface/core/diagnostics/codes_render.py +199 -9
- dataface/core/execute/_duckdb_cache_base.py +1 -3
- dataface/core/execute/adapters/sql_adapter.py +1 -3
- dataface/core/execute/cache_composition.py +1 -3
- dataface/core/execute/sql_literals.py +1 -2
- dataface/core/execute/trivial_local_cache.py +3 -1
- dataface/core/font_measure.py +155 -1
- dataface/core/fonts.py +72 -0
- dataface/core/inspect/resolver.py +3 -1
- dataface/core/numeric.py +15 -0
- dataface/core/registered_views/expander.py +95 -24
- dataface/core/registered_views/render_pipeline.py +36 -1
- dataface/core/render/chart/callout.py +44 -30
- dataface/core/render/chart/data_table_attachment.py +11 -13
- dataface/core/render/chart/emitters/_cartesian.py +48 -20
- dataface/core/render/chart/emitters/_channels.py +3 -3
- dataface/core/render/chart/emitters/_label_overlap.py +2 -2
- dataface/core/render/chart/emitters/_measured_label_padding.py +26 -11
- dataface/core/render/chart/emitters/_overlay.py +33 -6
- dataface/core/render/chart/emitters/_tooltip.py +6 -6
- dataface/core/render/chart/emitters/area.py +8 -5
- dataface/core/render/chart/emitters/bar.py +42 -13
- dataface/core/render/chart/emitters/line.py +5 -7
- dataface/core/render/chart/emitters/pie.py +1 -2
- dataface/core/render/chart/emitters/scatter.py +10 -3
- dataface/core/render/chart/features/baseline.py +9 -9
- dataface/core/render/chart/features/endpoint_labels.py +36 -14
- dataface/core/render/chart/features/mirror_axis.py +39 -5
- dataface/core/render/chart/features/structured_tooltip.py +5 -3
- dataface/core/render/chart/kpi.py +49 -46
- dataface/core/render/chart/rendering.py +40 -6
- dataface/core/render/chart/series_label_truncation.py +81 -0
- dataface/core/render/chart/spark.py +1 -1
- dataface/core/render/chart/spark_bar.py +15 -10
- dataface/core/render/chart/spec.py +2 -2
- dataface/core/render/chart/table.py +83 -65
- dataface/core/render/chart/table_support.py +8 -5
- dataface/core/render/chart/text_truncation.py +96 -0
- dataface/core/render/chart/time_unit_detect.py +118 -36
- dataface/core/render/chart/title_overflow.py +39 -62
- dataface/core/render/chart/translate.py +4 -10
- dataface/core/render/chart/vega_lite.py +14 -5
- dataface/core/render/chart/vl_field_maps.py +173 -37
- dataface/core/render/converters/chart.py +29 -18
- dataface/core/render/face_resolve.py +10 -10
- dataface/core/render/faces.py +45 -19
- dataface/core/render/font_support.py +71 -13
- dataface/core/render/fonts/DFTSansTabular-Medium.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-SemiBold.ttf +0 -0
- dataface/core/render/fonts/InterVariable-Medium.ttf +0 -0
- dataface/core/render/fonts/InterVariable-SemiBold.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Medium.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-SemiBold.ttf +0 -0
- dataface/core/render/layout_sizing.py +33 -19
- dataface/core/render/renderer.py +109 -69
- dataface/core/render/sizing.py +170 -132
- dataface/core/render/svg_utils.py +7 -10
- dataface/core/render/variables_layout.py +5 -3
- dataface/core/render/variables_resolve.py +4 -7
- dataface/core/render/variables_strip.py +9 -2
- dataface/core/render/warnings/axis_title_truncated.py +43 -0
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/callout_text_truncated.py +41 -0
- dataface/core/render/warnings/chart_title_truncated.py +36 -0
- dataface/core/render/warnings/facet_panel_width_below_minimum.py +71 -0
- dataface/core/render/warnings/kpi_label_truncated.py +34 -0
- dataface/core/render/warnings/registry.py +16 -0
- dataface/core/render/warnings/series_label_truncated.py +64 -0
- dataface/core/render/warnings/spark_label_truncated.py +37 -0
- dataface/core/render/warnings/table_text_truncated.py +42 -0
- dataface/core/render/yaml_format.py +15 -0
- dataface/core/serve/server.py +3 -5
- dataface/core/text/format_d3.py +126 -19
- dataface/core/text/numeral_scale.py +39 -0
- dataface/core/validate.py +0 -1
- dataface/data/schemas/board-resolved/board-resolved.schema.json +118 -46
- dataface/schema_release.py +4 -2
- {dataface-0.4.1.dev134.dist-info → dataface-0.4.1.dev224.dist-info}/METADATA +1 -1
- {dataface-0.4.1.dev134.dist-info → dataface-0.4.1.dev224.dist-info}/RECORD +146 -130
- mdsvg/fonts.py +13 -6
- {dataface-0.4.1.dev134.dist-info → dataface-0.4.1.dev224.dist-info}/WHEEL +0 -0
- {dataface-0.4.1.dev134.dist-info → dataface-0.4.1.dev224.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev134.dist-info → dataface-0.4.1.dev224.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -928,6 +928,8 @@ variables:
|
|
|
928
928
|
|
|
929
929
|
Top-level option-source binding (alternative to `options:`): `column`, `query`, `dimension` (MetricFlow), `measure` (MetricFlow), `model` (dbt).
|
|
930
930
|
|
|
931
|
+
Top-level `column` is `table.column`, and the table may be schema-qualified when it is not in the connection's default schema — `column: gis.fact_sales.property_type`. `filter()` accepts the same qualified form.
|
|
932
|
+
|
|
931
933
|
Enabled/disabled forms (`enabled: false` means the control is inactive):
|
|
932
934
|
|
|
933
935
|
```yaml
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# directory and its subdirectories; each face overrides what it sets.
|
|
7
7
|
#
|
|
8
8
|
# A meta.yaml deeper in the tree layers on top of the ones above it, and a face
|
|
9
|
-
# always wins over meta. Nested style keys merge (a face setting style.
|
|
10
|
-
# keeps a meta-supplied style.
|
|
9
|
+
# always wins over meta. Nested style keys merge (a face setting style.frame.margin
|
|
10
|
+
# keeps a meta-supplied style.frame.width).
|
|
11
11
|
#
|
|
12
12
|
# Uncomment and edit to set project-wide defaults:
|
|
13
13
|
#
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
# source: my_warehouse # default data source for faces that don't name one
|
|
16
16
|
#
|
|
17
17
|
# style:
|
|
18
|
-
#
|
|
18
|
+
# frame:
|
|
19
19
|
# width: 1000
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -7,13 +7,20 @@ values. No bare dict[str, Any] in any return position. Tool/CLI callers use
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import sys
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import Literal
|
|
12
13
|
|
|
13
14
|
import yaml
|
|
14
|
-
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_validator
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
from typing import Self
|
|
19
|
+
else:
|
|
20
|
+
from typing_extensions import Self
|
|
15
21
|
|
|
16
22
|
from dataface.agent_api._paths import resolve_face_or_error
|
|
23
|
+
from dataface.agent_api.query import VariableBinding, variables_to_dict as _vars_to_dict
|
|
17
24
|
from dataface.core.compile import Face, compile_file
|
|
18
25
|
from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
|
|
19
26
|
from dataface.core.diagnostics import Diagnostic
|
|
@@ -112,8 +119,12 @@ class RenderDashboardArgs(BaseModel):
|
|
|
112
119
|
"the list of available chart ids."
|
|
113
120
|
),
|
|
114
121
|
)
|
|
115
|
-
variables:
|
|
116
|
-
None,
|
|
122
|
+
variables: list[VariableBinding] | None = Field(
|
|
123
|
+
None,
|
|
124
|
+
description=(
|
|
125
|
+
"Variable values to apply to the dashboard, as an array of {name, value} pairs. "
|
|
126
|
+
'Example: [{"name": "region", "value": "US"}, {"name": "year", "value": 2024}]'
|
|
127
|
+
),
|
|
117
128
|
)
|
|
118
129
|
format: Literal["json", "text", "yaml", "svg", "terminal", "data"] | None = Field(
|
|
119
130
|
None,
|
|
@@ -148,6 +159,12 @@ class RenderDashboardArgs(BaseModel):
|
|
|
148
159
|
),
|
|
149
160
|
)
|
|
150
161
|
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
164
|
+
if self.variables is not None:
|
|
165
|
+
_vars_to_dict(self.variables)
|
|
166
|
+
return self
|
|
167
|
+
|
|
151
168
|
|
|
152
169
|
# ---------------------------------------------------------------------------
|
|
153
170
|
# Public verbs
|
|
@@ -26,7 +26,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
26
26
|
| `details` | [FaceDetails](#facedetails) | ✓ | Collapsible section metadata. String shorthand: details: 'text' → FaceDetails(summary='text'). Block form: details: {summary: ..., expanded_title: ..., expanded: false}. |
|
|
27
27
|
| `id` | str | ✓ | Explicit ID for this face. Auto-generated from filename if omitted. |
|
|
28
28
|
| `style` | [Style](#style) | ✓ | Style patch (background, padding, border, etc.). Background and semantic color tokens (accent, muted) cascade to nested child faces. |
|
|
29
|
-
| `width` | str \| int | ✓ | Width when nested (e.g., '50%', '400px', or an integer in pixels). On the root face there is no parent to place it into, so it instead sets the board's own width (equivalent to 'style.
|
|
29
|
+
| `width` | str \| int | ✓ | Width when nested (e.g., '50%', '400px', or an integer in pixels). On the root face there is no parent to place it into, so it instead sets the board's own width (equivalent to 'style.frame.width') — percentages are rejected there since there's nothing to size relative to. |
|
|
30
30
|
| `height` | str \| int | ✓ | Height when nested (e.g., '300px' or an integer in pixels). |
|
|
31
31
|
| `visible` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Controls whether this layout item is rendered. Accepts a bool, variable name, Jinja expression, or {query, column} probe. |
|
|
32
32
|
| `extends` | str \| list[str] | ✓ | Face name(s) or relative path(s) this face inherits from, low to high priority. |
|
|
@@ -54,7 +54,7 @@ Variable definition from YAML.
|
|
|
54
54
|
| `required` | bool | ✓ | When True, a value must be provided before queries execute. |
|
|
55
55
|
| `visible` | bool | ✓ | When False, the variable is not rendered in the UI but can still be set via URL params. |
|
|
56
56
|
| `enabled` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Enable this control. Accepts: static bool; a variable name or Jinja boolean expression string (no {{ }} required — bare names auto-wrap); or a {query, column} form that reads a single boolean cell from a named query. None = enabled. Absent variable in a string expression raises (use a default). |
|
|
57
|
-
| `column` | str | ✓ |
|
|
57
|
+
| `column` | str | ✓ | Table column to draw option values from, as 'table.column'. The table may be schema-qualified ('schema.table.column') when it is not in the connection's default schema. |
|
|
58
58
|
| `query` | str \| [SqlQuery](#sqlquery) \| [MetricflowQuery](#metricflowquery) \| [HttpQuery](#httpquery) \| [ValuesQuery](#valuesquery) \| [CompactValuesQuery](#compactvaluesquery) \| [SchemaQuery](#schemaquery) | ✓ | Query name or inline query definition for populating options. |
|
|
59
59
|
| `dimension` | str | ✓ | MetricFlow dimension name for populating options. |
|
|
60
60
|
| `measure` | str | ✓ | MetricFlow measure name for populating options. |
|
|
@@ -496,7 +496,7 @@ Authored overlay for Style — all fields optional. Adds CSS shorthand coercers.
|
|
|
496
496
|
|
|
497
497
|
| Field | Type | Optional | Description |
|
|
498
498
|
|-------|------|:--------:|-------------|
|
|
499
|
-
| `
|
|
499
|
+
| `frame` | [FrameStyle](#framestyle) | ✓ | Face-level structural frame dimensions. |
|
|
500
500
|
| `background` | str | ✓ | Working-surface background color (board and card fills). |
|
|
501
501
|
| `accent` | str | ✓ | Accent color token cascaded to sparklines, bars, and focus rings. |
|
|
502
502
|
| `muted` | str | ✓ | Muted secondary-text color token (KPI support rows, table and spark subtitles). |
|
|
@@ -1023,13 +1023,13 @@ Tab layout item.
|
|
|
1023
1023
|
| `grid` | [GridLayout](#gridlayout) | ✓ | CSS-grid layout for this tab's content. |
|
|
1024
1024
|
| `tabs` | [TabLayout](#tablayout) | ✓ | Nested tab layout (tabs within tabs). |
|
|
1025
1025
|
|
|
1026
|
-
<a id="
|
|
1027
|
-
##
|
|
1028
|
-
Authored overlay for
|
|
1026
|
+
<a id="framestyle"></a>
|
|
1027
|
+
## FrameStyle
|
|
1028
|
+
Authored overlay for FrameStyle. Face-level structural dimensions. Do NOT cascade to child faces.
|
|
1029
1029
|
|
|
1030
1030
|
| Field | Type | Optional | Description |
|
|
1031
1031
|
|-------|------|:--------:|-------------|
|
|
1032
|
-
| `width` | float | ✓ | Maximum
|
|
1032
|
+
| `width` | float | ✓ | Maximum board width in pixels. Caps the measured layout width, setting the board's proportions. Rendered as an on-screen pixel maximum everywhere except the dft HTML page (dft serve, dft render --format html), which scales the board to its container. |
|
|
1033
1033
|
| `min_height` | float | ✓ | Minimum board height in pixels. |
|
|
1034
1034
|
| `margin` | float | ✓ | Board outer margin in pixels. |
|
|
1035
1035
|
| `card_padding` | float | ✓ | Padding added to each card side in pixels. |
|
|
@@ -1689,7 +1689,7 @@ Authored overlay for TableTitleStyle.
|
|
|
1689
1689
|
|
|
1690
1690
|
| Field | Type | Optional | Description |
|
|
1691
1691
|
|-------|------|:--------:|-------------|
|
|
1692
|
-
| `height` | float | ✓ |
|
|
1692
|
+
| `height` | float | ✓ | Minimum title-block height in pixels. Applies as a floor only when a subtitle is present; a title-only block sizes to its natural content height instead. |
|
|
1693
1693
|
| `font` | [FontStyle](#fontstyle) | ✓ | Table title font style overrides. Unset fields fall back to [`style.charts.table.font`](#tablechartstyle). |
|
|
1694
1694
|
|
|
1695
1695
|
<a id="paginationconfig"></a>
|
dataface/agent_api/pack.py
CHANGED
|
@@ -274,7 +274,7 @@ def _build_face_dict(dashboard: ProposedDashboard) -> dict[str, Any]:
|
|
|
274
274
|
"title": dashboard.title,
|
|
275
275
|
"description": dashboard.purpose,
|
|
276
276
|
"text": (
|
|
277
|
-
f"Dashboard for {dashboard.primary_entity}.
|
|
277
|
+
f"Dashboard for {dashboard.primary_entity}. Add queries and charts here."
|
|
278
278
|
),
|
|
279
279
|
}
|
|
280
280
|
if dashboard.canonical_data_url:
|
dataface/agent_api/query.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Typed query verbs for the agent API."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from pathlib import Path, PurePosixPath
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
from typing import Self
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import Self
|
|
7
13
|
|
|
8
14
|
from dataface.agent_api._paths import resolve_face_path
|
|
9
15
|
from dataface.core.compile import compile_file
|
|
@@ -16,6 +22,34 @@ from dataface.core.validate import normalize_data_for_json
|
|
|
16
22
|
MAX_QUERY_LIMIT = 1000
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
class VariableBinding(BaseModel):
|
|
26
|
+
"""One variable binding in a tool call: a name–value pair.
|
|
27
|
+
|
|
28
|
+
Used in the ``variables`` / ``vars`` fields of ``render_dashboard``,
|
|
29
|
+
``execute_query``, and ``query_face`` instead of an open-keyed dict, so
|
|
30
|
+
that OpenAI strict mode can validate the full tool-call structure.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
34
|
+
|
|
35
|
+
name: str = Field(..., description="Variable name, e.g. 'region'")
|
|
36
|
+
value: str | int | float | bool = Field(
|
|
37
|
+
..., description="Scalar value for the variable"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def variables_to_dict(
|
|
42
|
+
bindings: list[VariableBinding],
|
|
43
|
+
) -> dict[str, str | int | float | bool]:
|
|
44
|
+
"""Convert a VariableBinding list to a dict, raising on duplicate names."""
|
|
45
|
+
result: dict[str, str | int | float | bool] = {}
|
|
46
|
+
for binding in bindings:
|
|
47
|
+
if binding.name in result:
|
|
48
|
+
raise ValueError(f"Duplicate variable name: {binding.name!r}")
|
|
49
|
+
result[binding.name] = binding.value
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
19
53
|
class FaceQueryLookupResult(BaseModel):
|
|
20
54
|
"""Result of extracting SQL from a named face query for offline use (validate/describe)."""
|
|
21
55
|
|
|
@@ -100,8 +134,12 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
100
134
|
...,
|
|
101
135
|
description="SQL query to execute. Use {{ variable_name }} for parameterized values — these work identically in dashboard YAML, so queries you test here will be cached when reused in dashboards.",
|
|
102
136
|
)
|
|
103
|
-
variables:
|
|
104
|
-
None,
|
|
137
|
+
variables: list[VariableBinding] | None = Field(
|
|
138
|
+
None,
|
|
139
|
+
description=(
|
|
140
|
+
"Variable values to substitute, as an array of {name, value} pairs. "
|
|
141
|
+
'Example: [{"name": "region", "value": "US"}, {"name": "year", "value": 2024}]'
|
|
142
|
+
),
|
|
105
143
|
)
|
|
106
144
|
source: str | None = Field(None, description="Data source name to execute against")
|
|
107
145
|
limit: int | None = Field(None, description="Maximum rows to return (default 50)")
|
|
@@ -118,6 +156,12 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
118
156
|
description="Short phrase naming what this query is for (e.g. 'Leads by industry'), shown to the user in place of the raw SQL.",
|
|
119
157
|
)
|
|
120
158
|
|
|
159
|
+
@model_validator(mode="after")
|
|
160
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
161
|
+
if self.variables is not None:
|
|
162
|
+
variables_to_dict(self.variables)
|
|
163
|
+
return self
|
|
164
|
+
|
|
121
165
|
|
|
122
166
|
class ExecuteQueryResult(BaseModel):
|
|
123
167
|
model_config = ConfigDict(frozen=True)
|
|
@@ -242,8 +286,12 @@ class QueryFaceArgs(BaseModel):
|
|
|
242
286
|
..., description="Name of the query inside the face's `queries:` block"
|
|
243
287
|
)
|
|
244
288
|
path: Path = Field(..., description="Path to the face YAML file")
|
|
245
|
-
vars:
|
|
246
|
-
None,
|
|
289
|
+
vars: list[VariableBinding] | None = Field(
|
|
290
|
+
None,
|
|
291
|
+
description=(
|
|
292
|
+
"Variable overrides as an array of {name, value} pairs. "
|
|
293
|
+
'Example: [{"name": "country", "value": "FR"}]'
|
|
294
|
+
),
|
|
247
295
|
)
|
|
248
296
|
limit: int = Field(
|
|
249
297
|
20, ge=1, le=1000, description="Max rows to return (default 20, max 1000)"
|
|
@@ -251,6 +299,12 @@ class QueryFaceArgs(BaseModel):
|
|
|
251
299
|
|
|
252
300
|
model_config = ConfigDict(extra="forbid")
|
|
253
301
|
|
|
302
|
+
@model_validator(mode="after")
|
|
303
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
304
|
+
if self.vars is not None:
|
|
305
|
+
variables_to_dict(self.vars)
|
|
306
|
+
return self
|
|
307
|
+
|
|
254
308
|
|
|
255
309
|
class QueryFaceResult(BaseModel):
|
|
256
310
|
model_config = ConfigDict(frozen=True)
|
dataface/agent_api/serve.py
CHANGED
|
@@ -58,9 +58,7 @@ def prepare_serve(
|
|
|
58
58
|
from dataface.core.serve.port import resolve_port
|
|
59
59
|
from dataface.core.serve.server import create_server
|
|
60
60
|
|
|
61
|
-
env_theme = os.environ.get(
|
|
62
|
-
"DFT_DEFAULT_THEME"
|
|
63
|
-
) # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
61
|
+
env_theme = os.environ.get("DFT_DEFAULT_THEME") # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
64
62
|
# Empty string is treated as unset (matches get_default_theme_name, which
|
|
65
63
|
# falls back to the shipped default) — only a non-empty unknown name errors.
|
|
66
64
|
if env_theme:
|
dataface/ai/llm.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
@@ -93,24 +94,69 @@ def _resolve_model(
|
|
|
93
94
|
return explicit_model or os.getenv(env_var) or default_model
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
def _to_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
"""Transform a JSON Schema dict in-place to conform to OpenAI strict-mode rules.
|
|
99
|
+
|
|
100
|
+
Mirrors openai/lib/_pydantic.py::_ensure_strict_json_schema:
|
|
101
|
+
- Every object gets additionalProperties: false.
|
|
102
|
+
- required covers all property keys whenever properties is present.
|
|
103
|
+
- 'default: null' entries are stripped (non-null defaults are left intact).
|
|
104
|
+
Recurses into $defs, definitions, properties, items, anyOf, allOf, oneOf.
|
|
105
|
+
"""
|
|
106
|
+
if schema.get("type") == "object":
|
|
107
|
+
schema.setdefault("additionalProperties", False)
|
|
108
|
+
props = schema.get("properties", {})
|
|
109
|
+
if props:
|
|
110
|
+
schema["required"] = list(props.keys())
|
|
111
|
+
|
|
112
|
+
if schema.get("default") is None and "default" in schema:
|
|
113
|
+
del schema["default"]
|
|
114
|
+
|
|
115
|
+
for defs_key in ("$defs", "definitions"):
|
|
116
|
+
for sub in schema.get(defs_key, {}).values():
|
|
117
|
+
_to_strict_json_schema(sub)
|
|
118
|
+
|
|
119
|
+
for sub in schema.get("properties", {}).values():
|
|
120
|
+
_to_strict_json_schema(sub)
|
|
121
|
+
|
|
122
|
+
for key in ("anyOf", "allOf", "oneOf"):
|
|
123
|
+
for sub in schema.get(key, []):
|
|
124
|
+
_to_strict_json_schema(sub)
|
|
125
|
+
|
|
126
|
+
if "items" in schema:
|
|
127
|
+
_to_strict_json_schema(schema["items"])
|
|
128
|
+
|
|
129
|
+
return schema
|
|
130
|
+
|
|
131
|
+
|
|
96
132
|
def _normalize_openai_tools(
|
|
97
133
|
tools: list[dict[str, Any]] | None,
|
|
98
134
|
) -> list[dict[str, Any]]:
|
|
99
135
|
# None means "use default agent tools"; [] means "no tools, text-only completion".
|
|
100
136
|
source_tools = ALL_TOOLS if tools is None else tools
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
result: list[dict[str, Any]] = []
|
|
138
|
+
for tool in source_tools:
|
|
139
|
+
if "input_schema" in tool:
|
|
140
|
+
# First-party tool in MCP/input_schema format — strict-transform and
|
|
141
|
+
# convert to OpenAI Responses API format. Strict mode is a property of
|
|
142
|
+
# schemas we authored, not of the transport; MCP passthrough schemas
|
|
143
|
+
# (already in OpenAI function format) are appended below unchanged.
|
|
144
|
+
result.append(
|
|
145
|
+
{
|
|
146
|
+
"type": "function",
|
|
147
|
+
"strict": True,
|
|
148
|
+
"name": tool["name"],
|
|
149
|
+
"description": tool["description"],
|
|
150
|
+
"parameters": _to_strict_json_schema(
|
|
151
|
+
copy.deepcopy(tool["input_schema"])
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
# Already in OpenAI function format (e.g. MCP passthrough) — pass through
|
|
157
|
+
# untouched. Caller-supplied schemas must not be strict-marked or rewritten.
|
|
158
|
+
result.append(tool)
|
|
159
|
+
return result
|
|
114
160
|
|
|
115
161
|
|
|
116
162
|
class LLMClientError(RuntimeError):
|
dataface/ai/tools/__init__.py
CHANGED
|
@@ -18,7 +18,10 @@ from dataface.agent_api import (
|
|
|
18
18
|
skills as _skills,
|
|
19
19
|
)
|
|
20
20
|
from dataface.agent_api._paths import resolve_face_or_error
|
|
21
|
-
from dataface.agent_api.query import
|
|
21
|
+
from dataface.agent_api.query import (
|
|
22
|
+
ExecuteQueryArgs as _ExecuteQueryArgs,
|
|
23
|
+
variables_to_dict as _vars_to_dict,
|
|
24
|
+
)
|
|
22
25
|
from dataface.agent_api.validate import (
|
|
23
26
|
ValidateDashboardArgs as _ValidateDashboardArgs,
|
|
24
27
|
validate_dashboard as _validate_dashboard_func,
|
|
@@ -105,7 +108,9 @@ def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, An
|
|
|
105
108
|
return ctx.project_session.render_dashboard(
|
|
106
109
|
face=face,
|
|
107
110
|
chart=parsed.chart,
|
|
108
|
-
variables=
|
|
111
|
+
variables=(
|
|
112
|
+
_vars_to_dict(parsed.variables) if parsed.variables is not None else None
|
|
113
|
+
),
|
|
109
114
|
format=parsed.format or "json",
|
|
110
115
|
as_link=parsed.as_link,
|
|
111
116
|
server_port=ctx.server_port,
|
|
@@ -117,7 +122,9 @@ def _handle_query(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any
|
|
|
117
122
|
parsed = _ExecuteQueryArgs.model_validate(args)
|
|
118
123
|
return _query.execute_query(
|
|
119
124
|
sql=parsed.sql,
|
|
120
|
-
variables=
|
|
125
|
+
variables=(
|
|
126
|
+
_vars_to_dict(parsed.variables) if parsed.variables is not None else None
|
|
127
|
+
),
|
|
121
128
|
source=parsed.source or ctx.default_source,
|
|
122
129
|
limit=parsed.limit or 50,
|
|
123
130
|
adapter_registry=ctx.project_session.adapter_registry,
|
|
@@ -141,7 +148,7 @@ def _handle_query_face(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str
|
|
|
141
148
|
name=parsed.name,
|
|
142
149
|
path=parsed.path,
|
|
143
150
|
project=ctx.project_session.project,
|
|
144
|
-
vars=parsed.vars,
|
|
151
|
+
vars=_vars_to_dict(parsed.vars) if parsed.vars is not None else None,
|
|
145
152
|
limit=parsed.limit,
|
|
146
153
|
adapter_registry=ctx.project_session.adapter_registry,
|
|
147
154
|
).model_dump(mode="json", exclude_none=True)
|
dataface/cli/commands/serve.py
CHANGED
|
@@ -32,14 +32,11 @@ def serve_command(
|
|
|
32
32
|
"""Start unified Dataface server.
|
|
33
33
|
|
|
34
34
|
This command starts a FastAPI server that serves:
|
|
35
|
-
- Profile templates at /profile/{template}/?model=X&column=Y
|
|
36
35
|
- Any face file at /{path}/?var=value (path.yml → /path/)
|
|
37
36
|
- Health check at /health
|
|
38
37
|
|
|
39
38
|
Example URLs:
|
|
40
|
-
http://localhost:9876/
|
|
41
|
-
http://localhost:9876/profile/model/?model=fct_orders&theme=dark
|
|
42
|
-
http://localhost:9876/profile/numeric_column/?model=fct_orders&column=revenue
|
|
39
|
+
http://localhost:9876/
|
|
43
40
|
http://localhost:9876/sales/?region=West
|
|
44
41
|
|
|
45
42
|
Args:
|
|
@@ -94,17 +91,11 @@ def serve_command(
|
|
|
94
91
|
port = setup.port
|
|
95
92
|
app = setup.app
|
|
96
93
|
|
|
97
|
-
# Start server
|
|
98
|
-
|
|
94
|
+
# Start server. The URL is the whole banner: it lists every face in the
|
|
95
|
+
# project, so any other route printed here is a second way to say the same
|
|
96
|
+
# thing — or, once a route is renamed, a link that 404s.
|
|
97
|
+
typer.echo("📈 Starting dashboard server")
|
|
99
98
|
typer.echo(f" URL: http://{host}:{port}")
|
|
100
|
-
typer.echo(f" Health: http://{host}:{port}/health")
|
|
101
|
-
typer.echo("")
|
|
102
|
-
typer.echo(" Routes:")
|
|
103
|
-
typer.echo(f" http://{host}:{port}/profile/model/?model=<table>")
|
|
104
|
-
typer.echo(
|
|
105
|
-
f" http://{host}:{port}/profile/numeric_column/?model=<table>&column=<col>"
|
|
106
|
-
)
|
|
107
|
-
typer.echo(f" http://{host}:{port}/<name>/?var=value")
|
|
108
99
|
typer.echo("")
|
|
109
100
|
typer.echo(" Press CTRL+C to stop")
|
|
110
101
|
|
|
@@ -122,8 +122,7 @@ def run_init_skills(
|
|
|
122
122
|
typer.echo(f" ✓ {name}")
|
|
123
123
|
elif result.skipped_existing:
|
|
124
124
|
typer.echo(
|
|
125
|
-
f" Workflow skills unchanged ({len(result.skipped_existing)})"
|
|
126
|
-
f" → {rel}/"
|
|
125
|
+
f" Workflow skills unchanged ({len(result.skipped_existing)}) → {rel}/"
|
|
127
126
|
)
|
|
128
127
|
if result.retired_removed:
|
|
129
128
|
typer.echo(f" Removed retired: {', '.join(result.retired_removed)}")
|
dataface/cli/main.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Main CLI entry point."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import io
|
|
4
5
|
import logging
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
@@ -71,9 +72,9 @@ inspect_app = typer.Typer(
|
|
|
71
72
|
rich_markup_mode=_RICH_MARKUP_MODE,
|
|
72
73
|
)
|
|
73
74
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
name="
|
|
75
|
+
# Artifact subcommand app — `dft artifact emit` / `dft artifact render`
|
|
76
|
+
artifact_app = typer.Typer(
|
|
77
|
+
name="artifact",
|
|
77
78
|
help="Emit a resolved-board artifact + recording, and render one back.",
|
|
78
79
|
no_args_is_help=True,
|
|
79
80
|
pretty_exceptions_show_locals=False,
|
|
@@ -632,7 +633,7 @@ def mcp_serve(
|
|
|
632
633
|
# --- Dashboards -------------------------------------------------------------
|
|
633
634
|
|
|
634
635
|
|
|
635
|
-
app.add_typer(
|
|
636
|
+
app.add_typer(artifact_app, name="artifact", rich_help_panel="Dashboards")
|
|
636
637
|
|
|
637
638
|
|
|
638
639
|
@app.command("describe", rich_help_panel="Dashboards")
|
|
@@ -920,7 +921,7 @@ def render(
|
|
|
920
921
|
)
|
|
921
922
|
|
|
922
923
|
|
|
923
|
-
@
|
|
924
|
+
@artifact_app.command("emit")
|
|
924
925
|
def emit_board(
|
|
925
926
|
face: Annotated[
|
|
926
927
|
Path,
|
|
@@ -965,14 +966,14 @@ def emit_board(
|
|
|
965
966
|
|
|
966
967
|
The artifact is the resolved board's published, data-free contract; the
|
|
967
968
|
recording is a separate sidecar carrying the rows it was resolved
|
|
968
|
-
against. Render the pair back with `dft
|
|
969
|
+
against. Render the pair back with `dft artifact render` — no compile step,
|
|
969
970
|
no warehouse connection.
|
|
970
971
|
|
|
971
972
|
\b
|
|
972
973
|
Examples:
|
|
973
|
-
dft
|
|
974
|
-
dft
|
|
975
|
-
dft
|
|
974
|
+
dft artifact emit faces/sales.yml
|
|
975
|
+
dft artifact emit faces/sales.yml --artifact out/sales.board.json --recording out/sales.recording.json
|
|
976
|
+
dft artifact emit faces/sales.yml --var region=West
|
|
976
977
|
"""
|
|
977
978
|
board_artifact_cmd.emit_board_command(
|
|
978
979
|
face,
|
|
@@ -985,7 +986,7 @@ def emit_board(
|
|
|
985
986
|
)
|
|
986
987
|
|
|
987
988
|
|
|
988
|
-
@
|
|
989
|
+
@artifact_app.command("render")
|
|
989
990
|
def render_board(
|
|
990
991
|
artifact: Annotated[Path, typer.Argument(help="Path to a board artifact file.")],
|
|
991
992
|
recording: Annotated[
|
|
@@ -1001,14 +1002,14 @@ def render_board(
|
|
|
1001
1002
|
) -> None:
|
|
1002
1003
|
"""Render a board from its artifact + recording — no compile, no warehouse.
|
|
1003
1004
|
|
|
1004
|
-
Both files come from `dft
|
|
1005
|
+
Both files come from `dft artifact emit`. A recording missing a query, or
|
|
1005
1006
|
captured under different variable values than the artifact expects,
|
|
1006
1007
|
fails loudly rather than rendering an empty or wrong chart.
|
|
1007
1008
|
|
|
1008
1009
|
\b
|
|
1009
1010
|
Examples:
|
|
1010
|
-
dft
|
|
1011
|
-
dft
|
|
1011
|
+
dft artifact render out/sales.board.json out/sales.recording.json
|
|
1012
|
+
dft artifact render out/sales.board.json out/sales.recording.json --output sales.svg
|
|
1012
1013
|
"""
|
|
1013
1014
|
board_artifact_cmd.render_board_command(
|
|
1014
1015
|
artifact,
|
|
@@ -1464,6 +1465,20 @@ for _ep in _entry_points(group="dataface.cli_plugins"):
|
|
|
1464
1465
|
_plugin()
|
|
1465
1466
|
|
|
1466
1467
|
|
|
1468
|
+
def _reconfigure_console_encoding() -> None:
|
|
1469
|
+
"""Force UTF-8 on stdout/stderr so non-ASCII diagnostics never crash.
|
|
1470
|
+
|
|
1471
|
+
Windows consoles default to the legacy cp1252 codepage; printing
|
|
1472
|
+
non-ASCII text (e.g. an arrow in an error message) would otherwise raise
|
|
1473
|
+
UnicodeEncodeError while reporting the original error. No-op on
|
|
1474
|
+
Linux/macOS, where the streams are already UTF-8. Streams without
|
|
1475
|
+
reconfigure() — e.g. one swapped in under test capture — are left alone.
|
|
1476
|
+
"""
|
|
1477
|
+
for stream in (sys.stdout, sys.stderr):
|
|
1478
|
+
if isinstance(stream, io.TextIOWrapper):
|
|
1479
|
+
stream.reconfigure(encoding="utf-8")
|
|
1480
|
+
|
|
1481
|
+
|
|
1467
1482
|
def version_callback(value: bool) -> None:
|
|
1468
1483
|
"""Print version and exit."""
|
|
1469
1484
|
if value:
|
|
@@ -1486,6 +1501,7 @@ def main(
|
|
|
1486
1501
|
] = None,
|
|
1487
1502
|
) -> None:
|
|
1488
1503
|
"""dft - Declarative, dbt-native dashboards in YAML."""
|
|
1504
|
+
_reconfigure_console_encoding()
|
|
1489
1505
|
# Runs before any subcommand, so every warehouse query this process sends is
|
|
1490
1506
|
# attributed to the CLI. `dft serve` narrows it to "serve" in its own command.
|
|
1491
1507
|
set_surface("cli")
|