dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/DATAFACE_SYNTAX.md +15 -4
- dataface/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +273 -239
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +113 -0
- dataface/agent_api/validate.py +8 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +26 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +25 -81
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +22 -33
- dataface/core/__init__.py +73 -28
- dataface/core/aliases.py +27 -0
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +161 -96
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/migrations.py +112 -9
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +78 -164
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +10 -154
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +24 -513
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +108 -3
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +27 -19
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +49 -40
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +9 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +24 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +12 -2
- dataface/core/diagnostics/codes_compile.py +17 -33
- dataface/core/diagnostics/codes_execute.py +149 -0
- dataface/core/diagnostics/codes_render.py +144 -31
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +94 -19
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -7
- dataface/core/project.py +19 -3
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +100 -46
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +97 -32
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +313 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +9 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
dataface/cli/commands/render.py
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
"""Render command implementation."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import json as _json
|
|
4
6
|
import sys
|
|
5
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
6
9
|
|
|
7
10
|
import typer
|
|
8
11
|
|
|
9
|
-
from dataface.agent_api import Diagnostic,
|
|
10
|
-
from dataface.agent_api._paths import (
|
|
11
|
-
build_face_render_context,
|
|
12
|
-
build_yaml_render_context,
|
|
13
|
-
resolve_face_or_error,
|
|
14
|
-
)
|
|
15
|
-
from dataface.agent_api.cache import project_cache_ctx
|
|
16
|
-
from dataface.agent_api.dashboards import RenderedDashboard
|
|
17
|
-
from dataface.agent_api.diagnostics import REGISTRY as _diag_registry
|
|
12
|
+
from dataface.agent_api import Diagnostic, RenderFormat
|
|
18
13
|
from dataface.cli._error_format import print_diagnostics
|
|
19
14
|
from dataface.cli._project import with_project
|
|
20
15
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
21
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from dataface.agent_api.dashboards import RenderedDashboard
|
|
19
|
+
|
|
22
20
|
# Maps common file extensions to render formats for format inference.
|
|
23
21
|
_EXT_TO_FORMAT: dict[str, RenderFormat] = {
|
|
24
22
|
"svg": "svg",
|
|
@@ -64,6 +62,8 @@ def _validate_ignore_codes(ignore_codes: set[str]) -> None:
|
|
|
64
62
|
at error level (suppressing an error is never valid) versus a code that
|
|
65
63
|
isn't registered at all (a typo or stale suppression stub).
|
|
66
64
|
"""
|
|
65
|
+
from dataface.agent_api.diagnostics import REGISTRY as _diag_registry
|
|
66
|
+
|
|
67
67
|
warning_codes = _diag_registry.codes(level="warning")
|
|
68
68
|
error_codes = _diag_registry.codes(level="error")
|
|
69
69
|
for code in sorted(ignore_codes):
|
|
@@ -172,6 +172,7 @@ def render_command(
|
|
|
172
172
|
fail_on_chart_errors: bool = True,
|
|
173
173
|
max_workers: int | None = None,
|
|
174
174
|
print0: bool = False,
|
|
175
|
+
chart: str | None = None,
|
|
175
176
|
*,
|
|
176
177
|
project: FilesystemProject,
|
|
177
178
|
) -> str | None:
|
|
@@ -204,6 +205,13 @@ def render_command(
|
|
|
204
205
|
Returns:
|
|
205
206
|
Output file path on success, None for terminal/stdout output
|
|
206
207
|
"""
|
|
208
|
+
from dataface.agent_api import ProjectSession
|
|
209
|
+
from dataface.agent_api._paths import (
|
|
210
|
+
build_face_render_context,
|
|
211
|
+
resolve_face_or_error,
|
|
212
|
+
)
|
|
213
|
+
from dataface.agent_api.cache import project_cache_ctx
|
|
214
|
+
|
|
207
215
|
if ignore_codes:
|
|
208
216
|
_validate_ignore_codes(ignore_codes)
|
|
209
217
|
|
|
@@ -230,6 +238,7 @@ def render_command(
|
|
|
230
238
|
raise RenderFailed([resolved])
|
|
231
239
|
result = project_session.render_dashboard(
|
|
232
240
|
face=resolved,
|
|
241
|
+
chart=chart,
|
|
233
242
|
format=format,
|
|
234
243
|
variables=variables,
|
|
235
244
|
use_cache=use_cache,
|
|
@@ -266,6 +275,7 @@ def render_command_from_yaml(
|
|
|
266
275
|
fail_on_chart_errors: bool = True,
|
|
267
276
|
max_workers: int | None = None,
|
|
268
277
|
print0: bool = False,
|
|
278
|
+
chart: str | None = None,
|
|
269
279
|
*,
|
|
270
280
|
project: FilesystemProject,
|
|
271
281
|
) -> str | None:
|
|
@@ -296,6 +306,10 @@ def render_command_from_yaml(
|
|
|
296
306
|
Output file path on success, None for terminal/stdout output. Exits 1
|
|
297
307
|
(typer.Exit) after reporting on a validation/chart failure.
|
|
298
308
|
"""
|
|
309
|
+
from dataface.agent_api import InMemoryFace, ProjectSession
|
|
310
|
+
from dataface.agent_api._paths import build_yaml_render_context
|
|
311
|
+
from dataface.agent_api.cache import project_cache_ctx
|
|
312
|
+
|
|
299
313
|
if ignore_codes:
|
|
300
314
|
_validate_ignore_codes(ignore_codes)
|
|
301
315
|
|
|
@@ -318,6 +332,7 @@ def render_command_from_yaml(
|
|
|
318
332
|
face = InMemoryFace(yaml_content, path=None)
|
|
319
333
|
result = project_session.render_dashboard(
|
|
320
334
|
face=face,
|
|
335
|
+
chart=chart,
|
|
321
336
|
format=format,
|
|
322
337
|
variables=variables,
|
|
323
338
|
use_cache=use_cache,
|
|
@@ -428,6 +443,7 @@ def render_commands(
|
|
|
428
443
|
max_workers: int | None = None,
|
|
429
444
|
print0: bool = False,
|
|
430
445
|
fail_fast: bool = False,
|
|
446
|
+
chart: str | None = None,
|
|
431
447
|
) -> None:
|
|
432
448
|
"""Render one or more dashboards, printing output paths to stdout.
|
|
433
449
|
|
|
@@ -474,6 +490,7 @@ def render_commands(
|
|
|
474
490
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
475
491
|
max_workers=max_workers,
|
|
476
492
|
print0=print0,
|
|
493
|
+
chart=chart,
|
|
477
494
|
)
|
|
478
495
|
except RenderFailed as exc:
|
|
479
496
|
print_diagnostics(exc.errors, json_output=json_errors)
|
dataface/cli/commands/search.py
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
5
7
|
import typer
|
|
6
8
|
|
|
7
|
-
from dataface.agent_api import Project
|
|
8
|
-
from dataface.agent_api.search import SearchResult
|
|
9
|
+
from dataface.agent_api import Project
|
|
9
10
|
from dataface.cli._error_format import print_diagnostics
|
|
10
11
|
from dataface.cli._project import with_project
|
|
11
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from dataface.agent_api.search import SearchResult
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
@with_project
|
|
14
18
|
def search_command(
|
|
@@ -19,6 +23,8 @@ def search_command(
|
|
|
19
23
|
limit: int = 10,
|
|
20
24
|
) -> None:
|
|
21
25
|
"""Search dashboards by keyword with ranked results."""
|
|
26
|
+
from dataface.agent_api import ProjectSession
|
|
27
|
+
|
|
22
28
|
with ProjectSession.from_project(project) as project_session:
|
|
23
29
|
result = project_session.search_dashboards(query, limit=limit)
|
|
24
30
|
|
dataface/cli/commands/serve.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
5
|
import sys
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from typing import TYPE_CHECKING
|
|
@@ -10,15 +9,10 @@ from typing import TYPE_CHECKING
|
|
|
10
9
|
import typer
|
|
11
10
|
import uvicorn
|
|
12
11
|
|
|
12
|
+
from dataface.agent_api import Diagnostic
|
|
13
13
|
from dataface.cli._error_format import print_diagnostics
|
|
14
14
|
from dataface.cli._project import with_project
|
|
15
15
|
|
|
16
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
17
|
-
from dataface.core.diagnostics import ERR_STARTUP_FAILED
|
|
18
|
-
|
|
19
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
20
|
-
from dataface.core.diagnostics.base import DatafaceError
|
|
21
|
-
|
|
22
16
|
if TYPE_CHECKING:
|
|
23
17
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
24
18
|
|
|
@@ -34,7 +28,6 @@ def serve_command(
|
|
|
34
28
|
max_workers: int | None = None,
|
|
35
29
|
no_cache: bool = False,
|
|
36
30
|
cache_path: Path | None = None,
|
|
37
|
-
allow_external_access: bool = False,
|
|
38
31
|
) -> None:
|
|
39
32
|
"""Start unified Dataface server.
|
|
40
33
|
|
|
@@ -55,10 +48,6 @@ def serve_command(
|
|
|
55
48
|
project: The resolved Dataface project (injected by @with_project)
|
|
56
49
|
dialect: SQL dialect (auto-detected from dbt profile, or duckdb)
|
|
57
50
|
target: dbt target name (defaults to DBT_TARGET env var, then profile default)
|
|
58
|
-
allow_external_access: Allow author SQL to reach the filesystem/network via
|
|
59
|
-
DuckDB (read_csv, read_json_auto, httpfs). Off by default — serve is
|
|
60
|
-
strict read-only. Opt in only for local dashboards that must read files
|
|
61
|
-
directly in SQL (e.g. glob-based analytics that cannot be file sources).
|
|
62
51
|
no_cache: Skip the query-result cache entirely.
|
|
63
52
|
cache_path: Persist the query-result cache to this DuckDB file (created
|
|
64
53
|
if absent). None (the default) opens an in-memory cache — ephemeral,
|
|
@@ -68,86 +57,42 @@ def serve_command(
|
|
|
68
57
|
None (exits with code 0 on success, 1 on errors)
|
|
69
58
|
"""
|
|
70
59
|
from dataface.agent_api import set_surface
|
|
71
|
-
|
|
72
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
73
|
-
from dataface.core.compile.config import list_built_in_themes, load_config
|
|
74
|
-
|
|
75
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
76
|
-
from dataface.core.diagnostics import ERR_INVALID_DEFAULT_THEME
|
|
77
|
-
|
|
78
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
79
|
-
from dataface.core.project_roots import infer_dialect_from_dbt
|
|
80
|
-
|
|
81
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
82
|
-
from dataface.core.serve.port import resolve_port
|
|
83
|
-
|
|
84
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
85
|
-
from dataface.core.serve.server import create_server
|
|
60
|
+
from dataface.agent_api.serve import format_startup_failure, prepare_serve
|
|
86
61
|
|
|
87
62
|
# Narrows the root callback's "cli": a served dashboard's queries are a
|
|
88
63
|
# different cost story from a one-shot `dft render`.
|
|
89
64
|
set_surface("serve")
|
|
90
65
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# here ensures the user sees a clear error at startup rather than on first render.
|
|
96
|
-
env_theme = os.environ.get(
|
|
97
|
-
"DFT_DEFAULT_THEME"
|
|
98
|
-
) # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
99
|
-
# Empty string is treated as unset (matches get_default_theme_name, which
|
|
100
|
-
# falls back to the shipped default) — only a non-empty unknown name errors.
|
|
101
|
-
if env_theme:
|
|
102
|
-
available = list_built_in_themes()
|
|
103
|
-
if env_theme not in available:
|
|
104
|
-
err = DatafaceError.from_code(
|
|
105
|
-
ERR_INVALID_DEFAULT_THEME,
|
|
106
|
-
source="DFT_DEFAULT_THEME env var",
|
|
107
|
-
theme=env_theme,
|
|
108
|
-
available=available,
|
|
109
|
-
)
|
|
110
|
-
print_diagnostics([err.to_diagnostic()])
|
|
111
|
-
raise typer.Exit(1) from None
|
|
112
|
-
|
|
113
|
-
# Validate dataface.yml wholesale: stray keys (style:, board:, theme:, etc.) are a
|
|
114
|
-
# hard error here at startup rather than silently ignored.
|
|
66
|
+
# Validate DFT_DEFAULT_THEME/dataface.yml, resolve port/dialect, and build the
|
|
67
|
+
# ASGI app — a dataface.yml validation failure raises pydantic's ValidationError
|
|
68
|
+
# uncaught (matches pre-existing behavior: a plain echo, not the Diagnostic
|
|
69
|
+
# envelope used everywhere else in this command).
|
|
115
70
|
from pydantic import ValidationError as _PydanticError
|
|
116
71
|
|
|
117
72
|
try:
|
|
118
|
-
|
|
73
|
+
setup = prepare_serve(
|
|
74
|
+
project,
|
|
75
|
+
port=port,
|
|
76
|
+
host=host,
|
|
77
|
+
dialect=dialect,
|
|
78
|
+
target=target,
|
|
79
|
+
max_workers=max_workers,
|
|
80
|
+
no_cache=no_cache,
|
|
81
|
+
cache_path=cache_path,
|
|
82
|
+
)
|
|
119
83
|
except _PydanticError as e:
|
|
120
84
|
typer.echo(f"dataface.yml validation error: {e}", err=True)
|
|
121
85
|
raise typer.Exit(1) from None
|
|
122
86
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
project_dir=project_dir,
|
|
127
|
-
host=host,
|
|
128
|
-
)
|
|
87
|
+
if isinstance(setup, Diagnostic):
|
|
88
|
+
print_diagnostics([setup])
|
|
89
|
+
raise typer.Exit(1) from None
|
|
129
90
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if effective_dialect is None:
|
|
136
|
-
inferred = infer_dialect_from_dbt(project_dir, effective_target)
|
|
137
|
-
if inferred:
|
|
138
|
-
typer.echo(f" Auto-detected dialect: {inferred} (from dbt profile)")
|
|
139
|
-
effective_dialect = inferred or "duckdb"
|
|
140
|
-
|
|
141
|
-
# Create FastAPI app
|
|
142
|
-
app = create_server(
|
|
143
|
-
project,
|
|
144
|
-
dialect=effective_dialect,
|
|
145
|
-
target=effective_target,
|
|
146
|
-
max_workers=max_workers,
|
|
147
|
-
no_cache=no_cache,
|
|
148
|
-
cache_path=cache_path,
|
|
149
|
-
allow_external_access_in_readonly=allow_external_access,
|
|
150
|
-
)
|
|
91
|
+
if setup.dialect_inferred:
|
|
92
|
+
typer.echo(f" Auto-detected dialect: {setup.dialect} (from dbt profile)")
|
|
93
|
+
|
|
94
|
+
port = setup.port
|
|
95
|
+
app = setup.app
|
|
151
96
|
|
|
152
97
|
# Start server
|
|
153
98
|
typer.echo("🚀 Starting dashboard server")
|
|
@@ -175,6 +120,5 @@ def serve_command(
|
|
|
175
120
|
typer.echo("\n👋 Server stopped")
|
|
176
121
|
sys.exit(0)
|
|
177
122
|
except Exception as e: # noqa: BLE001 — uvicorn surface
|
|
178
|
-
|
|
179
|
-
print_diagnostics([err.to_diagnostic()])
|
|
123
|
+
print_diagnostics([format_startup_failure(e)])
|
|
180
124
|
raise typer.Exit(1) from None
|
|
@@ -7,28 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from dataface.agent_api import skill_install
|
|
10
|
-
|
|
11
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
12
|
-
from dataface.core.project_roots import find_dft_root, find_repo_root
|
|
10
|
+
from dataface.cli._project import resolve_skill_install_root
|
|
13
11
|
|
|
14
12
|
_VALID_TARGETS = skill_install.SKILL_INSTALL_TARGETS
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
def _resolve_project_dir(start: Path) -> Path:
|
|
18
|
-
"""Return the directory where skills should be installed.
|
|
19
|
-
|
|
20
|
-
Skills are agent tooling that belong at the git root, not the operational
|
|
21
|
-
project root. When a .git directory exists above the dataface.yml, install
|
|
22
|
-
at the git root so monorepos with nested dataface projects don't accumulate
|
|
23
|
-
divergent skill copies.
|
|
24
|
-
"""
|
|
25
|
-
project_root = find_dft_root(start) or start.resolve()
|
|
26
|
-
git_root = find_repo_root(project_root)
|
|
27
|
-
if git_root is not None:
|
|
28
|
-
return git_root
|
|
29
|
-
return project_root
|
|
30
|
-
|
|
31
|
-
|
|
32
15
|
def _resolve_targets(
|
|
33
16
|
target: str | None,
|
|
34
17
|
*,
|
|
@@ -110,7 +93,7 @@ def run_init_skills(
|
|
|
110
93
|
typer.echo("Specify either --all or --dir, not both.", err=True)
|
|
111
94
|
raise typer.Exit(1)
|
|
112
95
|
|
|
113
|
-
project_root =
|
|
96
|
+
project_root = resolve_skill_install_root(
|
|
114
97
|
project_dir if project_dir is not None else Path.cwd()
|
|
115
98
|
)
|
|
116
99
|
target_dirs = _resolve_targets(
|
|
@@ -4,15 +4,18 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
|
|
10
|
-
from dataface.agent_api import Project
|
|
11
|
-
from dataface.agent_api.validate import ValidateResult
|
|
11
|
+
from dataface.agent_api import Project
|
|
12
12
|
from dataface.cli._console import dft_console
|
|
13
13
|
from dataface.cli._error_format import print_diagnostics
|
|
14
14
|
from dataface.cli._project import with_project
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from dataface.agent_api.validate import ValidateResult
|
|
18
|
+
|
|
16
19
|
_console = dft_console()
|
|
17
20
|
|
|
18
21
|
|
|
@@ -24,6 +27,8 @@ def validate_command(
|
|
|
24
27
|
json_output: bool = False,
|
|
25
28
|
strict: bool = False,
|
|
26
29
|
) -> None:
|
|
30
|
+
from dataface.agent_api import ProjectSession
|
|
31
|
+
|
|
27
32
|
with ProjectSession.from_project(project) as project_session:
|
|
28
33
|
results = project_session.validate_paths(paths)
|
|
29
34
|
_emit(results, json_output=json_output, strict=strict)
|
|
@@ -30,6 +30,7 @@ from dataface.core.project import (
|
|
|
30
30
|
assert_relpath,
|
|
31
31
|
glob_literal_prefix,
|
|
32
32
|
glob_to_regex,
|
|
33
|
+
posix_relpath,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
@@ -143,7 +144,7 @@ class FilesystemProject(Project):
|
|
|
143
144
|
# First try: fully resolved path. Handles OS-level symlinks (/var on macOS).
|
|
144
145
|
resolved = path.resolve()
|
|
145
146
|
try:
|
|
146
|
-
return
|
|
147
|
+
return posix_relpath(resolved, self.root)
|
|
147
148
|
except ValueError:
|
|
148
149
|
pass
|
|
149
150
|
|
|
@@ -151,7 +152,7 @@ class FilesystemProject(Project):
|
|
|
151
152
|
# paths through project-internal symlinks (e.g. faces/tasks/ -> ../../tasks/).
|
|
152
153
|
normalized = Path(os.path.normpath(path.absolute()))
|
|
153
154
|
try:
|
|
154
|
-
return
|
|
155
|
+
return posix_relpath(normalized, self.root)
|
|
155
156
|
except ValueError:
|
|
156
157
|
raise ValueError(
|
|
157
158
|
f"Path {path!r} is outside project root {self.root!r}"
|
|
@@ -218,12 +219,12 @@ class FilesystemProject(Project):
|
|
|
218
219
|
dirs[:] = sorted(d for d in dirs if d not in SKIP_SCAN_DIRS)
|
|
219
220
|
root_path = Path(root_str)
|
|
220
221
|
for name in files:
|
|
221
|
-
candidates.append(
|
|
222
|
+
candidates.append(posix_relpath(root_path / name, self.root))
|
|
222
223
|
yield from sorted(candidates)
|
|
223
224
|
else:
|
|
224
225
|
for p in sorted(subtree.iterdir()):
|
|
225
226
|
if p.is_file():
|
|
226
|
-
yield
|
|
227
|
+
yield posix_relpath(p, self.root)
|
|
227
228
|
|
|
228
229
|
def iter_dir(self, under: str) -> Iterator[ProjectPath | ProjectDirectory]:
|
|
229
230
|
"""``os.scandir`` over *under*, filtered/sorted per ``Project.iter_dir``.
|
dataface/cli/main.py
CHANGED
|
@@ -16,6 +16,7 @@ from dataface.agent_api import RenderFormat, set_surface
|
|
|
16
16
|
from dataface.cli._console import is_plain_output
|
|
17
17
|
from dataface.cli._extras import require_extras
|
|
18
18
|
from dataface.cli._parsing import parse_kv_pairs
|
|
19
|
+
from dataface.cli._project import has_faces_marker
|
|
19
20
|
from dataface.cli.commands import (
|
|
20
21
|
board_artifact as board_artifact_cmd,
|
|
21
22
|
describe as describe_cmd,
|
|
@@ -35,9 +36,6 @@ from dataface.cli.commands import (
|
|
|
35
36
|
from dataface.cli.commands.mcp_init import run_init as _mcp_run_init
|
|
36
37
|
from dataface.cli.commands.skills_init import run_init_skills as _skills_run_init
|
|
37
38
|
|
|
38
|
-
# tach-ignore(cli->core debt: route via agent_api; burn-down task)
|
|
39
|
-
from dataface.core.project_roots import find_dft_root
|
|
40
|
-
|
|
41
39
|
# Evaluated once at process startup — mid-session env changes won't take effect.
|
|
42
40
|
_RICH_MARKUP_MODE: Literal["rich"] | None = None if is_plain_output() else "rich"
|
|
43
41
|
# Probe for private package once at startup; avoids repeated find_spec calls.
|
|
@@ -107,21 +105,6 @@ if not logging.getLogger().handlers:
|
|
|
107
105
|
)
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
def _has_faces_marker(start: Path) -> bool:
|
|
111
|
-
"""True iff *start* sits inside a Dataface project that has a ``faces/`` directory.
|
|
112
|
-
|
|
113
|
-
Anchors on :func:`find_dft_root` so the walk-up stops at the
|
|
114
|
-
project root (any of ``dataface.yml``/``dataface.yaml``/``dbt_project.yml``)
|
|
115
|
-
rather than blindly hunting upward for stray ``faces/`` directories
|
|
116
|
-
elsewhere on the filesystem. A bare-dbt repo with no scaffolded
|
|
117
|
-
``faces/`` returns False — that's the case the banner exists for.
|
|
118
|
-
"""
|
|
119
|
-
if (start / "faces").is_dir():
|
|
120
|
-
return True
|
|
121
|
-
project = find_dft_root(start)
|
|
122
|
-
return project is not None and (project / "faces").is_dir()
|
|
123
|
-
|
|
124
|
-
|
|
125
108
|
def _render_init_banner() -> None:
|
|
126
109
|
"""Print a nudge to run ``dft init``.
|
|
127
110
|
|
|
@@ -203,7 +186,7 @@ class _RootHelpGroup(TyperGroup):
|
|
|
203
186
|
)
|
|
204
187
|
|
|
205
188
|
def format_help(self, ctx: Any, formatter: Any) -> None:
|
|
206
|
-
if not
|
|
189
|
+
if not has_faces_marker(Path.cwd()):
|
|
207
190
|
_render_init_banner()
|
|
208
191
|
super().format_help(ctx, formatter)
|
|
209
192
|
|
|
@@ -813,6 +796,16 @@ def render(
|
|
|
813
796
|
),
|
|
814
797
|
),
|
|
815
798
|
] = False,
|
|
799
|
+
chart: Annotated[
|
|
800
|
+
str | None,
|
|
801
|
+
typer.Option(
|
|
802
|
+
"--chart",
|
|
803
|
+
help=(
|
|
804
|
+
"Render only this chart (by chart id). "
|
|
805
|
+
"Valid only with a single face argument or stdin."
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
] = None,
|
|
816
809
|
) -> None:
|
|
817
810
|
"""Render one or more dashboards to SVG, HTML, PNG, PDF, or terminal.
|
|
818
811
|
|
|
@@ -876,6 +869,14 @@ def render(
|
|
|
876
869
|
param_hint="--output",
|
|
877
870
|
)
|
|
878
871
|
|
|
872
|
+
# --chart is ambiguous with multiple faces: no mapping syntax, no silent fanout.
|
|
873
|
+
if chart is not None and len(faces) > 1:
|
|
874
|
+
raise typer.BadParameter(
|
|
875
|
+
f"--chart is only valid with a single face argument; "
|
|
876
|
+
f"got {len(faces)} face arguments",
|
|
877
|
+
param_hint="--chart",
|
|
878
|
+
)
|
|
879
|
+
|
|
879
880
|
# Stdin path (single face "-")
|
|
880
881
|
if has_stdin:
|
|
881
882
|
yaml_content = sys.stdin.read()
|
|
@@ -896,6 +897,7 @@ def render(
|
|
|
896
897
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
897
898
|
max_workers=max_workers,
|
|
898
899
|
print0=print0,
|
|
900
|
+
chart=chart,
|
|
899
901
|
)
|
|
900
902
|
return
|
|
901
903
|
|
|
@@ -914,6 +916,7 @@ def render(
|
|
|
914
916
|
max_workers=max_workers,
|
|
915
917
|
print0=print0,
|
|
916
918
|
fail_fast=fail_fast,
|
|
919
|
+
chart=chart,
|
|
917
920
|
)
|
|
918
921
|
|
|
919
922
|
|
|
@@ -1104,19 +1107,6 @@ def serve(
|
|
|
1104
1107
|
),
|
|
1105
1108
|
),
|
|
1106
1109
|
] = None,
|
|
1107
|
-
allow_external_access: Annotated[
|
|
1108
|
-
bool,
|
|
1109
|
-
typer.Option(
|
|
1110
|
-
"--allow-external-access",
|
|
1111
|
-
help=(
|
|
1112
|
-
"Allow author SQL to read local files / URLs through DuckDB "
|
|
1113
|
-
"(read_csv, read_json_auto, httpfs). Off by default — serve runs "
|
|
1114
|
-
"strict read-only. Opt in only for local dashboards that must read "
|
|
1115
|
-
"files directly in SQL (e.g. glob analytics not expressible as file "
|
|
1116
|
-
"sources); prefer type: csv/json/parquet sources otherwise."
|
|
1117
|
-
),
|
|
1118
|
-
),
|
|
1119
|
-
] = False,
|
|
1120
1110
|
) -> None:
|
|
1121
1111
|
"""Start the dashboard server.
|
|
1122
1112
|
|
|
@@ -1166,7 +1156,6 @@ def serve(
|
|
|
1166
1156
|
max_workers=max_workers,
|
|
1167
1157
|
no_cache=no_cache,
|
|
1168
1158
|
cache_path=cache,
|
|
1169
|
-
allow_external_access=allow_external_access,
|
|
1170
1159
|
)
|
|
1171
1160
|
|
|
1172
1161
|
|
dataface/core/__init__.py
CHANGED
|
@@ -13,8 +13,10 @@ Submodules:
|
|
|
13
13
|
|
|
14
14
|
High-Level API:
|
|
15
15
|
>>> from pathlib import Path
|
|
16
|
-
>>> from dataface.core import compile
|
|
16
|
+
>>> from dataface.core.compile import compile
|
|
17
|
+
>>> from dataface.core.execute import Executor
|
|
17
18
|
>>> from dataface.core.execute.adapters import build_adapter_registry
|
|
19
|
+
>>> from dataface.core.render import render
|
|
18
20
|
>>> result = compile(yaml_content)
|
|
19
21
|
>>> from dataface.cli.filesystem_project import FilesystemProject
|
|
20
22
|
>>> registry = build_adapter_registry(FilesystemProject(Path.cwd()))
|
|
@@ -22,37 +24,45 @@ High-Level API:
|
|
|
22
24
|
>>> html = render(result.face, executor, format="html")
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
# Re-export validate module
|
|
27
|
-
from dataface.core import validate
|
|
28
|
-
from dataface.core.compile import (
|
|
29
|
-
AnyQuery,
|
|
30
|
-
AuthoredFace,
|
|
31
|
-
CompilationError,
|
|
32
|
-
CompileResult,
|
|
33
|
-
Face,
|
|
34
|
-
Layout,
|
|
35
|
-
LayoutItem,
|
|
36
|
-
Variable,
|
|
37
|
-
compile,
|
|
38
|
-
compile_file,
|
|
39
|
-
)
|
|
27
|
+
from __future__ import annotations
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
from
|
|
43
|
-
ExecutionError,
|
|
44
|
-
Executor,
|
|
45
|
-
)
|
|
29
|
+
import importlib
|
|
30
|
+
from typing import TYPE_CHECKING, Any
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
render
|
|
51
|
-
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
# Import-time-only: PEP 562 __getattr__ below resolves these lazily at
|
|
34
|
+
# runtime, so `import dataface.core.<anything>` (this file runs first,
|
|
35
|
+
# always) doesn't eagerly pull in the whole compile/execute/render stack
|
|
36
|
+
# just for this convenience re-export.
|
|
37
|
+
from dataface.core import validate as validate
|
|
38
|
+
from dataface.core.compile import (
|
|
39
|
+
AnyQuery as AnyQuery,
|
|
40
|
+
AuthoredFace as AuthoredFace,
|
|
41
|
+
CompilationError as CompilationError,
|
|
42
|
+
CompileResult as CompileResult,
|
|
43
|
+
Face as Face,
|
|
44
|
+
Layout as Layout,
|
|
45
|
+
LayoutItem as LayoutItem,
|
|
46
|
+
Variable as Variable,
|
|
47
|
+
compile_file as compile_file,
|
|
48
|
+
)
|
|
49
|
+
from dataface.core.execute import (
|
|
50
|
+
ExecutionError as ExecutionError,
|
|
51
|
+
Executor as Executor,
|
|
52
|
+
)
|
|
53
|
+
from dataface.core.render import RenderError as RenderError
|
|
52
54
|
|
|
55
|
+
# `compile` and `render` are deliberately NOT re-exported here: both names
|
|
56
|
+
# collide with a same-named submodule one level down (`dataface.core.compile`,
|
|
57
|
+
# `dataface.core.render`). Importing that submodule for *any* reason binds it
|
|
58
|
+
# onto this package under that exact name as a side effect of Python's own
|
|
59
|
+
# import machinery — a rebind PEP 562 __getattr__ cannot see or prevent — so a
|
|
60
|
+
# lazy alias sharing a submodule's leaf name silently flips from function to
|
|
61
|
+
# module depending on import order. Import the function directly from its
|
|
62
|
+
# submodule instead: `from dataface.core.compile import compile`,
|
|
63
|
+
# `from dataface.core.render import render`.
|
|
53
64
|
__all__ = [
|
|
54
65
|
# Compile
|
|
55
|
-
"compile",
|
|
56
66
|
"compile_file",
|
|
57
67
|
"CompileResult",
|
|
58
68
|
"Face",
|
|
@@ -66,8 +76,43 @@ __all__ = [
|
|
|
66
76
|
"Executor",
|
|
67
77
|
"ExecutionError",
|
|
68
78
|
# Render
|
|
69
|
-
"render",
|
|
70
79
|
"RenderError",
|
|
71
80
|
# Validate
|
|
72
81
|
"validate",
|
|
73
82
|
]
|
|
83
|
+
|
|
84
|
+
_LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
85
|
+
"validate": ("dataface.core", "validate"),
|
|
86
|
+
"AnyQuery": ("dataface.core.compile", "AnyQuery"),
|
|
87
|
+
"AuthoredFace": ("dataface.core.compile", "AuthoredFace"),
|
|
88
|
+
"CompilationError": ("dataface.core.compile", "CompilationError"),
|
|
89
|
+
"CompileResult": ("dataface.core.compile", "CompileResult"),
|
|
90
|
+
"Face": ("dataface.core.compile", "Face"),
|
|
91
|
+
"Layout": ("dataface.core.compile", "Layout"),
|
|
92
|
+
"LayoutItem": ("dataface.core.compile", "LayoutItem"),
|
|
93
|
+
"Variable": ("dataface.core.compile", "Variable"),
|
|
94
|
+
"compile_file": ("dataface.core.compile", "compile_file"),
|
|
95
|
+
"ExecutionError": ("dataface.core.execute", "ExecutionError"),
|
|
96
|
+
"Executor": ("dataface.core.execute", "Executor"),
|
|
97
|
+
"RenderError": ("dataface.core.render", "RenderError"),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def __getattr__(name: str) -> Any:
|
|
102
|
+
try:
|
|
103
|
+
module_path, attr_name = _LAZY_ATTRS[name]
|
|
104
|
+
except KeyError:
|
|
105
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
106
|
+
if module_path == __name__ and attr_name == name:
|
|
107
|
+
# "validate" is a submodule of this very package, not another
|
|
108
|
+
# module's attribute — import it directly to avoid recursing back
|
|
109
|
+
# into this __getattr__.
|
|
110
|
+
module = importlib.import_module(f"{__name__}.{attr_name}")
|
|
111
|
+
else:
|
|
112
|
+
module = getattr(importlib.import_module(module_path), attr_name)
|
|
113
|
+
globals()[name] = module # cache: repeated access is O(1) and `is` identity holds
|
|
114
|
+
return module
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def __dir__() -> list[str]:
|
|
118
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS))
|
dataface/core/aliases.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Alias-URL semantics, shared by every host that resolves a face's ``aliases:``.
|
|
2
|
+
|
|
3
|
+
A leaf module on purpose. An alias is claimed in a face and matched against an
|
|
4
|
+
incoming request path as a *string*, so the two sides must normalize it
|
|
5
|
+
identically or a claim resolves on one host and silently does nothing on the
|
|
6
|
+
other. Hosts that keep their own board index (rather than building an
|
|
7
|
+
``AliasIndex``) need that rule without importing the ``dft serve`` HTTP
|
|
8
|
+
application to get it — ``core/serve/`` sits at the top of the dependency stack.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from urllib.parse import unquote
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_alias_url(url: str) -> str:
|
|
17
|
+
"""Normalize a URL key: percent-decode, ensure leading /, ensure trailing /."""
|
|
18
|
+
decoded = unquote(url)
|
|
19
|
+
if not decoded.startswith("/"):
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"Alias {url!r} is not absolute: aliases must start with a leading /. "
|
|
22
|
+
"Example: /old-reports/"
|
|
23
|
+
)
|
|
24
|
+
# Trailing-slash canonical: add if missing
|
|
25
|
+
if not decoded.endswith("/"):
|
|
26
|
+
decoded = decoded + "/"
|
|
27
|
+
return decoded
|