dataface 0.1.5.dev322__py3-none-any.whl → 0.1.5.dev384__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.
- d3_format/format.py +9 -0
- dataface/DATAFACE_SYNTAX.md +3 -0
- dataface/agent_api/docs/_loader.py +1 -1
- dataface/agent_api/docs/yaml-reference.md +16 -16
- dataface/agent_api/mcp_install.py +2 -3
- dataface/ai/mcp/server.py +5 -5
- dataface/ai/tool_schemas.py +2 -28
- dataface/ai/tools/__init__.py +6 -4
- dataface/cli/commands/serve.py +7 -4
- dataface/cli/main.py +13 -1
- dataface/core/compile/colors.py +39 -23
- dataface/core/compile/compiler.py +2 -4
- dataface/core/compile/config.py +71 -8
- dataface/core/compile/data_table_attachment.py +26 -24
- dataface/core/compile/inherit_graph.py +47 -21
- dataface/core/compile/inherit_resolver.py +86 -0
- dataface/core/compile/introspection.py +1 -1
- dataface/core/compile/jinja.py +8 -50
- dataface/core/compile/models/chart/authored.py +24 -24
- dataface/core/compile/models/chart/normalized.py +14 -6
- dataface/core/compile/models/config.py +12 -7
- dataface/core/compile/models/markers.py +7 -7
- dataface/core/compile/models/query/normalized.py +19 -12
- dataface/core/compile/models/source.py +33 -81
- dataface/core/compile/models/style/resolved.py +22 -18
- dataface/core/compile/models/style/theme.py +52 -38
- dataface/core/compile/normalize_charts.py +6 -3
- dataface/core/compile/normalize_queries.py +3 -0
- dataface/core/compile/normalize_variables.py +102 -14
- dataface/core/compile/normalizer.py +15 -0
- dataface/core/compile/schema_renderers/prompt.py +4 -14
- dataface/core/compile/schema_renderers/vscode_schema.py +4 -15
- dataface/core/compile/sizing.py +21 -14
- dataface/core/compile/style_cascade.py +9 -14
- dataface/core/compile/yaml_error_formatter.py +46 -11
- dataface/core/dashboard.py +19 -6
- dataface/core/defaults/default_config.yml +12 -0
- dataface/core/defaults/themes/_base.yaml +5 -0
- dataface/core/defaults/themes/editorial.yaml +3 -0
- dataface/core/errors/__init__.py +2 -0
- dataface/core/errors/codes_render.py +16 -0
- dataface/core/errors/codes_serve.py +2 -3
- dataface/core/errors/structured.py +2 -2
- dataface/core/execute/adapters/adapter_registry.py +6 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +14 -2
- dataface/core/execute/adapters/http_adapter.py +12 -15
- dataface/core/execute/adapters/sql_adapter.py +175 -17
- dataface/core/execute/batch.py +45 -0
- dataface/core/execute/duckdb_cache.py +84 -8
- dataface/core/execute/executor.py +132 -133
- dataface/core/inspect/query_validator.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +0 -2
- dataface/core/inspect/templates/charts.yml +0 -2
- dataface/core/inspect/templates/date_column.yml +0 -2
- dataface/core/inspect/templates/model.yml +0 -2
- dataface/core/inspect/templates/numeric_column.yml +0 -2
- dataface/core/inspect/templates/quality.yml +0 -2
- dataface/core/inspect/templates/string_column.yml +0 -2
- dataface/core/project_roots.py +1 -1
- dataface/core/registered_views/query_runner.py +29 -1
- dataface/core/registered_views/render_pipeline.py +1 -1
- dataface/core/render/chart/geo.py +91 -17
- dataface/core/render/chart/pipeline.py +3 -0
- dataface/core/render/chart/profile.py +47 -55
- dataface/core/render/chart/serialization.py +2 -1
- dataface/core/render/chart/standard_renderer.py +48 -0
- dataface/core/render/chart/table.py +46 -42
- dataface/core/render/chart/table_support.py +5 -7
- dataface/core/render/chart/vl_field_maps.py +11 -0
- dataface/core/render/control_registry.py +1 -3
- dataface/core/render/errors.py +11 -5
- dataface/core/render/faces.py +4 -4
- dataface/core/render/geo_defaults.yml +16 -0
- dataface/core/render/nav.py +14 -32
- dataface/core/render/renderer.py +23 -13
- dataface/core/render/templates/controls/_styles.css +15 -12
- dataface/core/render/templates/controls/select.html +0 -1
- dataface/core/render/templates/nav/nav.css +11 -11
- dataface/core/render/templates/nav/nav.html +56 -1
- dataface/core/render/terminal.py +7 -3
- dataface/core/render/utils.py +19 -12
- dataface/core/render/variable_controls.py +19 -71
- dataface/core/serve/bootstrap.py +37 -22
- dataface/core/serve/port.py +16 -6
- dataface/core/serve/server.py +295 -218
- dataface/core/serve/templates/error.html.j2 +8 -7
- {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/METADATA +1 -1
- {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/RECORD +93 -94
- mdsvg/fonts.py +4 -1
- mdsvg/renderer.py +13 -6
- dataface/core/render/markdown_defaults.yml +0 -16
- dataface/core/render/missing_vars_prompt.py +0 -79
- {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/WHEEL +0 -0
- {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/licenses/LICENSE +0 -0
d3_format/format.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import math
|
|
15
15
|
from collections.abc import Callable
|
|
16
16
|
from decimal import ROUND_HALF_UP, Decimal
|
|
17
|
+
from typing import overload
|
|
17
18
|
|
|
18
19
|
from d3_format.spec import FormatSpec, parse
|
|
19
20
|
|
|
@@ -520,6 +521,14 @@ def _apply_spec(spec: FormatSpec, v: float | int | Decimal) -> str:
|
|
|
520
521
|
return _assemble(spec, sign_char, body, suffix)
|
|
521
522
|
|
|
522
523
|
|
|
524
|
+
@overload
|
|
525
|
+
def format(spec_str: str) -> Callable[[float | int | Decimal], str]: ... # noqa: A001
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@overload
|
|
529
|
+
def format(spec_str: str, value: float | int | Decimal) -> str: ... # noqa: A001
|
|
530
|
+
|
|
531
|
+
|
|
523
532
|
def format( # noqa: A001
|
|
524
533
|
spec_str: str,
|
|
525
534
|
value: float | int | Decimal | None = None,
|
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -427,6 +427,8 @@ WHERE {{ filter('plan', plans) }}
|
|
|
427
427
|
|
|
428
428
|
The `filter()` macro handles `select` (single value → `=`) and `multiselect` (list → `IN (...)`) automatically and quotes all string literals correctly.
|
|
429
429
|
|
|
430
|
+
Select and multiselect controls render only the options the face author provides. Dataface does not add an "All" option; author a real sentinel option and matching SQL/Jinja explicitly if a dashboard needs one.
|
|
431
|
+
|
|
430
432
|
### Inline query in a chart
|
|
431
433
|
|
|
432
434
|
A chart can carry its own one-off query instead of referencing a named one. Three equivalent forms:
|
|
@@ -672,6 +674,7 @@ lookup: state
|
|
|
672
674
|
value: revenue
|
|
673
675
|
|
|
674
676
|
# map — generic choropleth (alias for geoshape with named source)
|
|
677
|
+
# world-countries/world-50m join on numeric TopoJSON ids such as 840, not ISO alpha codes.
|
|
675
678
|
type: map
|
|
676
679
|
geo_source: us-states
|
|
677
680
|
lookup: state
|
|
@@ -58,7 +58,7 @@ class DocsSearchHit(BaseModel):
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
class DocsArgs(BaseModel):
|
|
61
|
-
"""
|
|
61
|
+
"""Browse the Dataface YAML reference offline. Modes: no args = topic index (slug + one-line description per H2), topic='<slug>' = one section, topic='all' = whole reference unsliced, search='<query>' = substring search across topics. Use this before writing YAML to learn field names, valid values, and examples. Call with no args first to see the available topics."""
|
|
62
62
|
|
|
63
63
|
topic: str | None = Field(
|
|
64
64
|
None,
|
|
@@ -1457,8 +1457,8 @@ Authored overlay for BarChartMarksStyle. Bar-family mark overrides. Only bar and
|
|
|
1457
1457
|
|
|
1458
1458
|
| Field | Type | Optional | Description |
|
|
1459
1459
|
|-------|------|:--------:|-------------|
|
|
1460
|
-
| `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides for bar charts;
|
|
1461
|
-
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides for bar endpoint labels;
|
|
1460
|
+
| `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides for bar charts; inherits from global. |
|
|
1461
|
+
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides for bar endpoint labels; inherits from global. |
|
|
1462
1462
|
|
|
1463
1463
|
<a id="linechartmarksstyle"></a>
|
|
1464
1464
|
## LineChartMarksStyle
|
|
@@ -1466,9 +1466,9 @@ Authored overlay for LineChartMarksStyle. Line-family mark overrides.
|
|
|
1466
1466
|
|
|
1467
1467
|
| Field | Type | Optional | Description |
|
|
1468
1468
|
|-------|------|:--------:|-------------|
|
|
1469
|
-
| `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Line mark overrides;
|
|
1470
|
-
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides;
|
|
1471
|
-
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides;
|
|
1469
|
+
| `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Line mark overrides; inherits from global. |
|
|
1470
|
+
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
|
|
1471
|
+
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
|
|
1472
1472
|
| `rule` | [RuleMarkStyle](#rulemarkstyle) | ✓ | Rule mark overrides; None inherits global. |
|
|
1473
1473
|
|
|
1474
1474
|
<a id="areachartmarksstyle"></a>
|
|
@@ -1477,10 +1477,10 @@ Authored overlay for AreaChartMarksStyle. Area-family mark overrides.
|
|
|
1477
1477
|
|
|
1478
1478
|
| Field | Type | Optional | Description |
|
|
1479
1479
|
|-------|------|:--------:|-------------|
|
|
1480
|
-
| `area` | [AreaMarkStyle](#areamarkstyle) | ✓ | Area mark overrides;
|
|
1481
|
-
| `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Top-edge line mark overrides;
|
|
1482
|
-
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides;
|
|
1483
|
-
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides;
|
|
1480
|
+
| `area` | [AreaMarkStyle](#areamarkstyle) | ✓ | Area mark overrides; inherits from global. |
|
|
1481
|
+
| `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Top-edge line mark overrides; inherits from global. |
|
|
1482
|
+
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
|
|
1483
|
+
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
|
|
1484
1484
|
|
|
1485
1485
|
<a id="scatterchartmarksstyle"></a>
|
|
1486
1486
|
## ScatterChartMarksStyle
|
|
@@ -1488,8 +1488,8 @@ Authored overlay for ScatterChartMarksStyle. Scatter-family mark overrides.
|
|
|
1488
1488
|
|
|
1489
1489
|
| Field | Type | Optional | Description |
|
|
1490
1490
|
|-------|------|:--------:|-------------|
|
|
1491
|
-
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides;
|
|
1492
|
-
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides;
|
|
1491
|
+
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
|
|
1492
|
+
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
|
|
1493
1493
|
|
|
1494
1494
|
<a id="heatmapchartmarksstyle"></a>
|
|
1495
1495
|
## HeatmapChartMarksStyle
|
|
@@ -1497,7 +1497,7 @@ Authored overlay for HeatmapChartMarksStyle. Heatmap-family mark overrides.
|
|
|
1497
1497
|
|
|
1498
1498
|
| Field | Type | Optional | Description |
|
|
1499
1499
|
|-------|------|:--------:|-------------|
|
|
1500
|
-
| `rect` | [RectMarkStyle](#rectmarkstyle) | ✓ | Rect mark overrides;
|
|
1500
|
+
| `rect` | [RectMarkStyle](#rectmarkstyle) | ✓ | Rect mark overrides; inherits from global. |
|
|
1501
1501
|
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
|
|
1502
1502
|
|
|
1503
1503
|
<a id="totalstyle"></a>
|
|
@@ -1515,7 +1515,7 @@ Authored overlay for PieChartMarksStyle. Pie/donut-family mark overrides.
|
|
|
1515
1515
|
|
|
1516
1516
|
| Field | Type | Optional | Description |
|
|
1517
1517
|
|-------|------|:--------:|-------------|
|
|
1518
|
-
| `slice` | [SliceMarkStyle](#slicemarkstyle) | ✓ | Slice mark overrides;
|
|
1518
|
+
| `slice` | [SliceMarkStyle](#slicemarkstyle) | ✓ | Slice mark overrides; inherits from global. |
|
|
1519
1519
|
| `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
|
|
1520
1520
|
|
|
1521
1521
|
<a id="kpivaluestyle"></a>
|
|
@@ -1720,7 +1720,7 @@ Authored overlay for PointMapChartMarksStyle. Point map-family mark overrides.
|
|
|
1720
1720
|
|
|
1721
1721
|
| Field | Type | Optional | Description |
|
|
1722
1722
|
|-------|------|:--------:|-------------|
|
|
1723
|
-
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides;
|
|
1723
|
+
| `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
|
|
1724
1724
|
|
|
1725
1725
|
<a id="geoshapechartmarksstyle"></a>
|
|
1726
1726
|
## GeoshapeChartMarksStyle
|
|
@@ -1728,7 +1728,7 @@ Authored overlay for GeoshapeChartMarksStyle. Geoshape-family mark overrides.
|
|
|
1728
1728
|
|
|
1729
1729
|
| Field | Type | Optional | Description |
|
|
1730
1730
|
|-------|------|:--------:|-------------|
|
|
1731
|
-
| `geoshape` | [GeoshapeMarkStyle](#geoshapemarkstyle) | ✓ | Geoshape mark overrides;
|
|
1731
|
+
| `geoshape` | [GeoshapeMarkStyle](#geoshapemarkstyle) | ✓ | Geoshape mark overrides; inherits from global. |
|
|
1732
1732
|
|
|
1733
1733
|
<a id="layeraxisystyle"></a>
|
|
1734
1734
|
## LayerAxisYStyle
|
|
@@ -2441,7 +2441,7 @@ Authored overlay for HistogramChartMarksStyle. Histogram-family mark overrides.
|
|
|
2441
2441
|
|
|
2442
2442
|
| Field | Type | Optional | Description |
|
|
2443
2443
|
|-------|------|:--------:|-------------|
|
|
2444
|
-
| `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides;
|
|
2444
|
+
| `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides; inherits from global. |
|
|
2445
2445
|
| `rule` | [RuleMarkStyle](#rulemarkstyle) | ✓ | Rule mark overrides; None inherits global. |
|
|
2446
2446
|
|
|
2447
2447
|
<a id="detailsarrowstyle"></a>
|
|
@@ -126,9 +126,8 @@ def _upsert_mcp_config(
|
|
|
126
126
|
existing: dict[str, Any] = {}
|
|
127
127
|
if config_path.exists():
|
|
128
128
|
try:
|
|
129
|
-
|
|
130
|
-
if
|
|
131
|
-
existing = {}
|
|
129
|
+
loaded = json.loads(config_path.read_text(encoding="utf-8"))
|
|
130
|
+
existing = loaded if isinstance(loaded, dict) else {}
|
|
132
131
|
except (json.JSONDecodeError, OSError):
|
|
133
132
|
existing = {}
|
|
134
133
|
|
dataface/ai/mcp/server.py
CHANGED
|
@@ -178,14 +178,14 @@ def create_server(context: DatafaceAIContext) -> Any:
|
|
|
178
178
|
def _uri(value: str) -> AnyUrl:
|
|
179
179
|
return TypeAdapter(AnyUrl).validate_python(value)
|
|
180
180
|
|
|
181
|
-
@server.list_resources()
|
|
181
|
+
@server.list_resources() # type: ignore[no-untyped-call]
|
|
182
182
|
async def handle_list_resources() -> list[Resource]:
|
|
183
183
|
return [
|
|
184
184
|
Resource(uri=_uri(u), mimeType=m, name=n, description=d)
|
|
185
185
|
for u, m, n, d in (*_BASE_RESOURCES, *_docs_topic_resources())
|
|
186
186
|
]
|
|
187
187
|
|
|
188
|
-
@server.list_resource_templates()
|
|
188
|
+
@server.list_resource_templates() # type: ignore[no-untyped-call]
|
|
189
189
|
async def handle_list_resource_templates() -> list[ResourceTemplate]:
|
|
190
190
|
return [
|
|
191
191
|
ResourceTemplate(
|
|
@@ -202,11 +202,11 @@ def create_server(context: DatafaceAIContext) -> Any:
|
|
|
202
202
|
),
|
|
203
203
|
]
|
|
204
204
|
|
|
205
|
-
@server.read_resource()
|
|
206
|
-
async def handle_read_resource(uri:
|
|
205
|
+
@server.read_resource() # type: ignore[no-untyped-call]
|
|
206
|
+
async def handle_read_resource(uri: AnyUrl) -> str:
|
|
207
207
|
return _read_resource_content(str(uri), context=context)
|
|
208
208
|
|
|
209
|
-
@server.list_tools()
|
|
209
|
+
@server.list_tools() # type: ignore[no-untyped-call]
|
|
210
210
|
async def handle_list_tools() -> list[Tool]:
|
|
211
211
|
return [
|
|
212
212
|
Tool(
|
dataface/ai/tool_schemas.py
CHANGED
|
@@ -14,6 +14,7 @@ from pydantic import BaseModel
|
|
|
14
14
|
from dataface.agent_api.dashboards import RenderDashboardArgs
|
|
15
15
|
from dataface.agent_api.describe import DescribeFaceArgs
|
|
16
16
|
from dataface.agent_api.describe_query import DescribeQueryArgs
|
|
17
|
+
from dataface.agent_api.docs import DocsArgs
|
|
17
18
|
from dataface.agent_api.docs.warnings import GetWarningCodeArgs
|
|
18
19
|
from dataface.agent_api.files import (
|
|
19
20
|
EditFileArgs,
|
|
@@ -47,34 +48,7 @@ SCHEMA = _ai_tool("schema", SchemaArgs)
|
|
|
47
48
|
SCHEMA_SEARCH = _ai_tool("schema_search", SchemaSearchArgs)
|
|
48
49
|
SEARCH_DASHBOARDS = _ai_tool("search_dashboards", SearchDashboardsArgs)
|
|
49
50
|
|
|
50
|
-
DOCS =
|
|
51
|
-
"name": "docs",
|
|
52
|
-
"description": (
|
|
53
|
-
"Browse the Dataface YAML reference offline. "
|
|
54
|
-
"Modes: no args = topic index (slug + one-line description per H2), "
|
|
55
|
-
"topic='<slug>' = one section, topic='all' = whole reference unsliced, "
|
|
56
|
-
"search='<query>' = substring search across topics. "
|
|
57
|
-
"Use this before writing YAML to learn field names, valid values, "
|
|
58
|
-
"and examples. Call with no args first to see the available topics."
|
|
59
|
-
),
|
|
60
|
-
"input_schema": {
|
|
61
|
-
"type": "object",
|
|
62
|
-
"properties": {
|
|
63
|
-
"topic": {
|
|
64
|
-
"type": "string",
|
|
65
|
-
"description": "Topic slug (e.g. 'face', 'charts', 'cheatsheet') or 'all' for the whole file. Omit for the topic index.",
|
|
66
|
-
},
|
|
67
|
-
"search": {
|
|
68
|
-
"type": "string",
|
|
69
|
-
"description": "Substring query — scans all H2 sections and returns ranked hits",
|
|
70
|
-
},
|
|
71
|
-
"limit": {
|
|
72
|
-
"type": "integer",
|
|
73
|
-
"description": "Max search hits to return (default 5, max 50)",
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
}
|
|
51
|
+
DOCS = _ai_tool("docs", DocsArgs)
|
|
78
52
|
|
|
79
53
|
LIST_WARNING_CODES = {
|
|
80
54
|
"name": "list_warning_codes",
|
dataface/ai/tools/__init__.py
CHANGED
|
@@ -20,6 +20,7 @@ from dataface.agent_api import (
|
|
|
20
20
|
search as _search,
|
|
21
21
|
skills as _skills,
|
|
22
22
|
)
|
|
23
|
+
from dataface.agent_api.query import ExecuteQueryArgs as _ExecuteQueryArgs
|
|
23
24
|
from dataface.agent_api.validate import (
|
|
24
25
|
ValidateDashboardArgs as _ValidateDashboardArgs,
|
|
25
26
|
annotate_with_data_lint as _annotate_with_data_lint,
|
|
@@ -120,11 +121,12 @@ def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, An
|
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
def _handle_query(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
124
|
+
parsed = _ExecuteQueryArgs.model_validate(args)
|
|
123
125
|
return _query.execute_query(
|
|
124
|
-
sql=
|
|
125
|
-
variables=
|
|
126
|
-
source=
|
|
127
|
-
limit=
|
|
126
|
+
sql=parsed.sql,
|
|
127
|
+
variables=parsed.variables,
|
|
128
|
+
source=parsed.source,
|
|
129
|
+
limit=parsed.limit or 50,
|
|
128
130
|
adapter_registry=ctx.project.adapter_registry,
|
|
129
131
|
).model_dump(mode="json", exclude_none=True)
|
|
130
132
|
|
dataface/cli/commands/serve.py
CHANGED
|
@@ -20,6 +20,7 @@ def serve_command(
|
|
|
20
20
|
dialect: str | None = None,
|
|
21
21
|
target: str | None = None,
|
|
22
22
|
max_workers: int | None = None,
|
|
23
|
+
no_cache: bool = False,
|
|
23
24
|
) -> None:
|
|
24
25
|
"""Start unified Dataface server.
|
|
25
26
|
|
|
@@ -46,21 +47,22 @@ def serve_command(
|
|
|
46
47
|
None (exits with code 0 on success, 1 on errors)
|
|
47
48
|
"""
|
|
48
49
|
from dataface.core.project_roots import find_dft_root, infer_dialect_from_dbt
|
|
49
|
-
from dataface.core.serve.bootstrap import
|
|
50
|
+
from dataface.core.serve.bootstrap import apply_default_theme
|
|
50
51
|
from dataface.core.serve.port import resolve_port
|
|
51
52
|
from dataface.core.serve.server import create_server
|
|
52
53
|
|
|
53
54
|
# Resolve the project root: --project-dir if given, else discover from cwd.
|
|
54
55
|
project_dir = project_dir or find_dft_root() or Path.cwd()
|
|
55
56
|
|
|
56
|
-
#
|
|
57
|
+
# Resolve default theme (env var > dataface.yml theme: > built-in default).
|
|
58
|
+
# Fails fast on invalid values.
|
|
57
59
|
try:
|
|
58
|
-
|
|
60
|
+
apply_default_theme(project_dir)
|
|
59
61
|
except DatafaceError as e:
|
|
60
62
|
print_structured_errors([e.to_structured()])
|
|
61
63
|
raise typer.Exit(1) from None
|
|
62
64
|
|
|
63
|
-
# Resolve port: --port > DFT_PORT > dataface.yml > hash(project_dir)
|
|
65
|
+
# Resolve port: --port > DFT_PORT > dataface.yml server.port > hash(project_dir)
|
|
64
66
|
port = resolve_port(
|
|
65
67
|
explicit_port=port,
|
|
66
68
|
project_dir=project_dir,
|
|
@@ -85,6 +87,7 @@ def serve_command(
|
|
|
85
87
|
dialect=effective_dialect,
|
|
86
88
|
target=effective_target,
|
|
87
89
|
max_workers=max_workers,
|
|
90
|
+
no_cache=no_cache,
|
|
88
91
|
)
|
|
89
92
|
|
|
90
93
|
# Start server
|
dataface/cli/main.py
CHANGED
|
@@ -911,6 +911,17 @@ def serve(
|
|
|
911
911
|
),
|
|
912
912
|
),
|
|
913
913
|
] = None,
|
|
914
|
+
no_cache: Annotated[
|
|
915
|
+
bool,
|
|
916
|
+
typer.Option(
|
|
917
|
+
"--no-cache",
|
|
918
|
+
help=(
|
|
919
|
+
"Disable the persistent query cache. By default dft serve caches "
|
|
920
|
+
"results in <project>/.dft/cache.duckdb across page loads. "
|
|
921
|
+
"Use --no-cache to disable (matches today's behavior)."
|
|
922
|
+
),
|
|
923
|
+
),
|
|
924
|
+
] = False,
|
|
914
925
|
) -> None:
|
|
915
926
|
"""Start the dashboard server.
|
|
916
927
|
|
|
@@ -929,7 +940,7 @@ def serve(
|
|
|
929
940
|
4. ~/.dbt/profiles.yml
|
|
930
941
|
|
|
931
942
|
Port is auto-resolved: --port flag > DFT_PORT env var > dataface.yml
|
|
932
|
-
port
|
|
943
|
+
server.port > deterministic hash of project directory. If the chosen port
|
|
933
944
|
is occupied, the next available port is used automatically.
|
|
934
945
|
|
|
935
946
|
\b
|
|
@@ -948,6 +959,7 @@ def serve(
|
|
|
948
959
|
dialect=dialect,
|
|
949
960
|
target=target,
|
|
950
961
|
max_workers=max_workers,
|
|
962
|
+
no_cache=no_cache,
|
|
951
963
|
)
|
|
952
964
|
|
|
953
965
|
|
dataface/core/compile/colors.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Color utility functions for Dataface renderers.
|
|
2
2
|
|
|
3
|
-
Provides
|
|
4
|
-
compile and render layers.
|
|
3
|
+
Provides color helpers for use across compile and render layers.
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -9,9 +8,16 @@ from __future__ import annotations
|
|
|
9
8
|
import re
|
|
10
9
|
from typing import overload
|
|
11
10
|
|
|
11
|
+
from PIL import ImageColor
|
|
12
|
+
|
|
12
13
|
from dataface.core.compile.errors import CompilationError
|
|
13
14
|
|
|
14
15
|
_CSS_HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
16
|
+
_CSS_RGBA_COLOR_PATTERN = re.compile(
|
|
17
|
+
r"^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*"
|
|
18
|
+
r"((?:0(?:\.\d+)?)|(?:1(?:\.0+)?)|(?:\.\d+))\s*\)$",
|
|
19
|
+
re.IGNORECASE,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
def is_sanitizable_color(color: str) -> bool:
|
|
@@ -22,31 +28,41 @@ def is_sanitizable_color(color: str) -> bool:
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
|
|
25
|
-
def
|
|
26
|
-
|
|
31
|
+
def _color_channels(color: str) -> tuple[int, int, int]:
|
|
32
|
+
rgba_match = _CSS_RGBA_COLOR_PATTERN.match(color)
|
|
33
|
+
if rgba_match:
|
|
34
|
+
r, g, b = (int(value) for value in rgba_match.groups()[:3])
|
|
35
|
+
if all(0 <= value <= 255 for value in (r, g, b)):
|
|
36
|
+
return (r, g, b)
|
|
37
|
+
raise CompilationError(f"mix_colors expects CSS colors, got {color!r}")
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return False
|
|
39
|
+
try:
|
|
40
|
+
parsed = ImageColor.getrgb(color)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
raise CompilationError(f"mix_colors expects CSS colors, got {color!r}") from exc
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
if
|
|
36
|
-
|
|
44
|
+
r, g, b = int(parsed[0]), int(parsed[1]), int(parsed[2])
|
|
45
|
+
if not all(0 <= value <= 255 for value in (r, g, b)):
|
|
46
|
+
raise CompilationError(f"mix_colors expects CSS colors, got {color!r}")
|
|
47
|
+
return (r, g, b)
|
|
37
48
|
|
|
38
|
-
if len(hex_color) != 6:
|
|
39
|
-
return False
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
def mix_colors(base: str, overlay: str, fraction: float) -> str:
|
|
51
|
+
"""Blend ``fraction`` of ``overlay`` into ``base``. Returns 6-digit hex.
|
|
52
|
+
|
|
53
|
+
Used to derive subtle surface/secondary tiers from theme tokens — e.g. a code
|
|
54
|
+
background that is the page background nudged toward the text color — so the
|
|
55
|
+
result tracks the theme on both light and dark backgrounds instead of pinning a
|
|
56
|
+
constant. ``fraction`` is clamped to [0, 1]; 0 returns ``base``, 1 returns ``overlay``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
t = min(1.0, max(0.0, fraction))
|
|
60
|
+
br, bg, bb = _color_channels(base)
|
|
61
|
+
o_r, o_g, o_b = _color_channels(overlay)
|
|
62
|
+
r = round(br + (o_r - br) * t)
|
|
63
|
+
g = round(bg + (o_g - bg) * t)
|
|
64
|
+
b = round(bb + (o_b - bb) * t)
|
|
65
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
50
66
|
|
|
51
67
|
|
|
52
68
|
@overload
|
|
@@ -437,7 +437,7 @@ def validate_compiled_queries(
|
|
|
437
437
|
|
|
438
438
|
|
|
439
439
|
def compile_file(
|
|
440
|
-
file_path: Path,
|
|
440
|
+
file_path: str | Path,
|
|
441
441
|
options: dict[str, Any] | None = None,
|
|
442
442
|
apply_meta: bool = True,
|
|
443
443
|
root_path: Path | None = None,
|
|
@@ -468,9 +468,7 @@ def compile_file(
|
|
|
468
468
|
>>> if result.success:
|
|
469
469
|
... print(result.face.title)
|
|
470
470
|
"""
|
|
471
|
-
|
|
472
|
-
if isinstance(file_path, str):
|
|
473
|
-
file_path = Path(file_path)
|
|
471
|
+
file_path = Path(file_path)
|
|
474
472
|
|
|
475
473
|
if not file_path.exists():
|
|
476
474
|
return CompileResult(
|
dataface/core/compile/config.py
CHANGED
|
@@ -23,6 +23,7 @@ YAML is the single source of truth - no Python dataclass defaults.
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
import os
|
|
26
27
|
from collections.abc import Mapping
|
|
27
28
|
from copy import deepcopy
|
|
28
29
|
from functools import cache
|
|
@@ -36,8 +37,8 @@ from dataface.core.compile.models.config import (
|
|
|
36
37
|
ConfigNode,
|
|
37
38
|
ConfigPatch,
|
|
38
39
|
ExecutionConfig,
|
|
39
|
-
MarkdownConfig,
|
|
40
40
|
RenderingConfig,
|
|
41
|
+
ServerConfig,
|
|
41
42
|
VegaRuntimeConfig,
|
|
42
43
|
as_plain_mapping,
|
|
43
44
|
is_mapping_like,
|
|
@@ -61,7 +62,6 @@ _core_dir: Path = Path(__file__).parent.parent
|
|
|
61
62
|
_MODULE_DEFAULT_PATHS: list[Path] = [
|
|
62
63
|
_core_dir / "inspect" / "defaults.yml",
|
|
63
64
|
_core_dir / "render" / "geo_defaults.yml",
|
|
64
|
-
_core_dir / "render" / "markdown_defaults.yml",
|
|
65
65
|
_core_dir / "render" / "terminal_defaults.yml",
|
|
66
66
|
]
|
|
67
67
|
|
|
@@ -122,11 +122,6 @@ def get_chart_rendering() -> ConfigNode:
|
|
|
122
122
|
return get_config().chart_rendering
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def get_markdown_config() -> MarkdownConfig:
|
|
126
|
-
"""Narrow getter — markdown rendering color config."""
|
|
127
|
-
return get_config().markdown
|
|
128
|
-
|
|
129
|
-
|
|
130
125
|
def get_terminal_config() -> ConfigNode:
|
|
131
126
|
"""Narrow getter — terminal rendering defaults."""
|
|
132
127
|
return get_config().terminal
|
|
@@ -147,6 +142,42 @@ def get_execution_config() -> ExecutionConfig:
|
|
|
147
142
|
return get_config().execution
|
|
148
143
|
|
|
149
144
|
|
|
145
|
+
def resolve_max_workers(explicit: int | None) -> int:
|
|
146
|
+
"""Resolve the query-parallelism width: explicit arg → DFT_MAX_WORKERS → config.
|
|
147
|
+
|
|
148
|
+
Single source of truth for both the render-time worker pool and the
|
|
149
|
+
per-source connection pool, so the two never disagree on width.
|
|
150
|
+
"""
|
|
151
|
+
if explicit is not None:
|
|
152
|
+
return explicit
|
|
153
|
+
env_val = os.getenv("DFT_MAX_WORKERS") # noqa: TID251 — DFT_MAX_WORKERS knob
|
|
154
|
+
if env_val is not None:
|
|
155
|
+
return int(env_val)
|
|
156
|
+
return get_execution_config().max_workers
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_project_server_config(project_dir: Path) -> ServerConfig:
|
|
160
|
+
"""Read project-level ``server:`` settings from dataface.yml/yaml.
|
|
161
|
+
|
|
162
|
+
Only the ``server:`` section is validated here. dataface.yml is also used
|
|
163
|
+
as a lightweight project marker in tests and examples, so unrelated top-level
|
|
164
|
+
keys must not make serve startup fail.
|
|
165
|
+
"""
|
|
166
|
+
base = get_config().server.to_plain_dict(exclude_none=False)
|
|
167
|
+
resolved_dir = project_dir.resolve()
|
|
168
|
+
for candidate in (resolved_dir / "dataface.yml", resolved_dir / "dataface.yaml"):
|
|
169
|
+
if candidate.exists():
|
|
170
|
+
file_data = _as_mapping(_load_yaml_data(candidate), candidate.name)
|
|
171
|
+
server_section = file_data.get("server")
|
|
172
|
+
if server_section is None:
|
|
173
|
+
break
|
|
174
|
+
server_data = _as_mapping(
|
|
175
|
+
server_section, f"{candidate.name} server section"
|
|
176
|
+
)
|
|
177
|
+
return ServerConfig.model_validate(_deep_merge(base, server_data))
|
|
178
|
+
return ServerConfig.model_validate(base)
|
|
179
|
+
|
|
180
|
+
|
|
150
181
|
def load_config(path: Path) -> Config:
|
|
151
182
|
"""Load configuration from a custom YAML file.
|
|
152
183
|
|
|
@@ -444,7 +475,7 @@ def set_default_theme_name(theme_name: str) -> None:
|
|
|
444
475
|
"""Override the in-process default theme name.
|
|
445
476
|
|
|
446
477
|
Validates the theme exists before patching. Raises ValueError on unknown names.
|
|
447
|
-
Called at serve startup by
|
|
478
|
+
Called at serve startup by apply_default_theme(); not for general use.
|
|
448
479
|
|
|
449
480
|
Args:
|
|
450
481
|
theme_name: A built-in theme stem (e.g. "carbong100", "light").
|
|
@@ -628,6 +659,38 @@ def get_project_warnings_ignore(project_dir: Path) -> frozenset[str]:
|
|
|
628
659
|
return frozenset()
|
|
629
660
|
|
|
630
661
|
|
|
662
|
+
# ============================================================================
|
|
663
|
+
# PROJECT DEFAULT THEME
|
|
664
|
+
# ============================================================================
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def get_project_default_theme(project_dir: Path) -> str | None:
|
|
668
|
+
"""Return the project-declared default theme from ``dataface.yml``.
|
|
669
|
+
|
|
670
|
+
Reads the top-level ``theme:`` key. Returns ``None`` when the file is
|
|
671
|
+
absent or the key is unauthored. Raises ``TypeError`` for any non-string
|
|
672
|
+
value so misauthored configs fail loud at startup.
|
|
673
|
+
|
|
674
|
+
Validation of the theme name (must be a built-in theme) lives in
|
|
675
|
+
``apply_default_theme`` so that env-var and config-file inputs share one
|
|
676
|
+
structured-error path.
|
|
677
|
+
"""
|
|
678
|
+
resolved_dir = project_dir.resolve()
|
|
679
|
+
for candidate in (resolved_dir / "dataface.yml", resolved_dir / "dataface.yaml"):
|
|
680
|
+
if candidate.exists():
|
|
681
|
+
file_data = _as_mapping(_load_yaml_data(candidate), candidate.name)
|
|
682
|
+
if "theme" not in file_data:
|
|
683
|
+
return None
|
|
684
|
+
value = file_data["theme"]
|
|
685
|
+
if not isinstance(value, str):
|
|
686
|
+
raise TypeError(
|
|
687
|
+
f"{candidate.name}: theme must be a string, "
|
|
688
|
+
f"got {type(value).__name__}: {value!r}"
|
|
689
|
+
)
|
|
690
|
+
return value
|
|
691
|
+
return None
|
|
692
|
+
|
|
693
|
+
|
|
631
694
|
# ============================================================================
|
|
632
695
|
# META CONFIG CLASSES
|
|
633
696
|
# ============================================================================
|