bideux 0.1.2__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.
- bideux/__init__.py +157 -0
- bideux/_guard.py +62 -0
- bideux/authoring.py +12 -0
- bideux/cli.py +118 -0
- bideux/core.py +12 -0
- bideux/corpus.py +19 -0
- bideux/demo.py +200 -0
- bideux/doctor.py +249 -0
- bideux/engine.py +14 -0
- bideux/fastapi/__init__.py +62 -0
- bideux/fastapi/authoring.py +472 -0
- bideux/fastapi/dashboards.py +381 -0
- bideux/fastapi/mount.py +354 -0
- bideux/fastapi/reader.py +227 -0
- bideux/fastapi/reports.py +95 -0
- bideux/hosting.py +387 -0
- bideux/init.py +109 -0
- bideux/py.typed +0 -0
- bideux/quickstart.py +154 -0
- bideux/report.py +12 -0
- bideux/server.py +12 -0
- bideux/spec.py +13 -0
- bideux/store.py +12 -0
- bideux-0.1.2.dist-info/METADATA +157 -0
- bideux-0.1.2.dist-info/RECORD +122 -0
- bideux-0.1.2.dist-info/WHEEL +4 -0
- bideux-0.1.2.dist-info/entry_points.txt +2 -0
- bideux-0.1.2.dist-info/licenses/LICENSE +202 -0
- dashboard_authoring/__init__.py +223 -0
- dashboard_authoring/checks.py +604 -0
- dashboard_authoring/drafts.py +621 -0
- dashboard_authoring/grade.py +427 -0
- dashboard_authoring/grounding.py +257 -0
- dashboard_authoring/history.py +233 -0
- dashboard_authoring/parsing.py +48 -0
- dashboard_authoring/ports.py +412 -0
- dashboard_authoring/prompts.py +313 -0
- dashboard_authoring/py.typed +0 -0
- dashboard_authoring/render.py +809 -0
- dashboard_authoring/results.py +129 -0
- dashboard_authoring/service.py +1510 -0
- dashboard_authoring/suite.py +394 -0
- dashboard_authoring/tools.py +252 -0
- dashboard_authoring/warehouse.py +208 -0
- dashboard_core/__init__.py +128 -0
- dashboard_core/cron.py +127 -0
- dashboard_core/pivot.py +1259 -0
- dashboard_core/py.typed +0 -0
- dashboard_core/schedule_filters.py +205 -0
- dashboard_core/schedule_timing.py +184 -0
- dashboard_core/tokens.py +76 -0
- dashboard_engine/__init__.py +198 -0
- dashboard_engine/diagnostics.py +664 -0
- dashboard_engine/errors.py +48 -0
- dashboard_engine/execution.py +1146 -0
- dashboard_engine/expressions.py +725 -0
- dashboard_engine/periods.py +320 -0
- dashboard_engine/planning.py +665 -0
- dashboard_engine/ports.py +161 -0
- dashboard_engine/py.typed +0 -0
- dashboard_engine/reference.py +634 -0
- dashboard_engine/results.py +38 -0
- dashboard_engine/sql.py +490 -0
- dashboard_engine/tables.py +322 -0
- dashboard_engine/testing.py +245 -0
- dashboard_engine/widgets.py +717 -0
- dashboard_report/__init__.py +91 -0
- dashboard_report/bands.py +474 -0
- dashboard_report/charts.py +568 -0
- dashboard_report/constants.py +328 -0
- dashboard_report/csv_report.py +110 -0
- dashboard_report/deck.py +2554 -0
- dashboard_report/logos.py +104 -0
- dashboard_report/pages.py +1279 -0
- dashboard_report/pictures.py +55 -0
- dashboard_report/pivots.py +223 -0
- dashboard_report/plots.py +277 -0
- dashboard_report/ports.py +170 -0
- dashboard_report/py.typed +0 -0
- dashboard_report/report.py +464 -0
- dashboard_report/sections.py +516 -0
- dashboard_report/service.py +165 -0
- dashboard_report/theme.py +142 -0
- dashboard_report/values.py +235 -0
- dashboard_report/workbook.py +1087 -0
- dashboard_server/__init__.py +187 -0
- dashboard_server/access.py +201 -0
- dashboard_server/documents.py +373 -0
- dashboard_server/errors.py +104 -0
- dashboard_server/memory.py +660 -0
- dashboard_server/ports.py +264 -0
- dashboard_server/principals.py +93 -0
- dashboard_server/py.typed +0 -0
- dashboard_server/records.py +193 -0
- dashboard_server/service.py +1441 -0
- dashboard_spec/__init__.py +72 -0
- dashboard_spec/diagnostics.py +22 -0
- dashboard_spec/migrations.py +59 -0
- dashboard_spec/model_schema.py +44 -0
- dashboard_spec/models.py +988 -0
- dashboard_spec/parsing.py +185 -0
- dashboard_spec/py.typed +0 -0
- dashboard_spec/round_trip.py +37 -0
- dashboard_spec/schema.py +60 -0
- dashboard_spec/schema_aliases.py +43 -0
- dashboard_spec/slugs.py +43 -0
- dashboard_spec/widget_schema.py +570 -0
- dashboard_store/__init__.py +133 -0
- dashboard_store/authoring/__init__.py +1 -0
- dashboard_store/authoring/history.py +314 -0
- dashboard_store/binding.py +217 -0
- dashboard_store/config.py +404 -0
- dashboard_store/creation.py +145 -0
- dashboard_store/diagnostics.py +318 -0
- dashboard_store/inspection.py +599 -0
- dashboard_store/manifest.py +416 -0
- dashboard_store/model_repository.py +226 -0
- dashboard_store/py.typed +0 -0
- dashboard_store/repository.py +585 -0
- dashboard_store/rows.py +453 -0
- dashboard_store/schedule_repository.py +310 -0
- dashboard_store/share_repository.py +200 -0
bideux/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Bideux dashboards in one install.
|
|
2
|
+
|
|
3
|
+
This distribution carries ``dashboard_spec``, ``dashboard_core``,
|
|
4
|
+
``dashboard_engine``, ``dashboard_store``, ``dashboard_server``,
|
|
5
|
+
``dashboard_report`` and ``dashboard_authoring`` under their own names, so
|
|
6
|
+
every import that works against the granular ``bideux-dashboard-*`` wheels
|
|
7
|
+
works unchanged here. This module is the short path: the objects a host
|
|
8
|
+
touches when it wires a dashboard service, runs the engine, or renders a
|
|
9
|
+
report, in one namespace.
|
|
10
|
+
|
|
11
|
+
Scheduling is not here on purpose. A host owns its schedules, cron and
|
|
12
|
+
delivery and calls :class:`DashboardReportService` for the bytes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dashboard_authoring import DashboardAuthoringService
|
|
18
|
+
from dashboard_core import slugify
|
|
19
|
+
from dashboard_engine import (
|
|
20
|
+
DashboardEngine,
|
|
21
|
+
DashboardEngineError,
|
|
22
|
+
EngineOptions,
|
|
23
|
+
ScopeContext,
|
|
24
|
+
WarehousePort,
|
|
25
|
+
)
|
|
26
|
+
from dashboard_report import (
|
|
27
|
+
BrandingPort,
|
|
28
|
+
BuiltReport,
|
|
29
|
+
CapturePort,
|
|
30
|
+
DashboardLinkPort,
|
|
31
|
+
DashboardReportPort,
|
|
32
|
+
DashboardReportRequest,
|
|
33
|
+
DashboardReportService,
|
|
34
|
+
ReportBranding,
|
|
35
|
+
ReportFormat,
|
|
36
|
+
)
|
|
37
|
+
from dashboard_server import (
|
|
38
|
+
AccessPolicy,
|
|
39
|
+
AuditEvent,
|
|
40
|
+
AuditPort,
|
|
41
|
+
Caller,
|
|
42
|
+
DashboardAccessError,
|
|
43
|
+
DashboardLayoutConflictError,
|
|
44
|
+
DashboardListing,
|
|
45
|
+
DashboardNotFoundError,
|
|
46
|
+
DashboardPermission,
|
|
47
|
+
DashboardRef,
|
|
48
|
+
DashboardRender,
|
|
49
|
+
DashboardRepositoryPort,
|
|
50
|
+
DashboardService,
|
|
51
|
+
DashboardServiceError,
|
|
52
|
+
DefaultAccessPolicy,
|
|
53
|
+
EngineFactoryPort,
|
|
54
|
+
InvalidDocumentError,
|
|
55
|
+
ModelRepositoryPort,
|
|
56
|
+
Principal,
|
|
57
|
+
PrincipalDirectoryPort,
|
|
58
|
+
RenderCachePort,
|
|
59
|
+
Repositories,
|
|
60
|
+
ServiceOptions,
|
|
61
|
+
ShareRepositoryPort,
|
|
62
|
+
SharingDisabledError,
|
|
63
|
+
SlugTakenError,
|
|
64
|
+
SlugUnavailableError,
|
|
65
|
+
UnitOfWorkPort,
|
|
66
|
+
WidgetNotFoundError,
|
|
67
|
+
WidgetRepositoryPort,
|
|
68
|
+
)
|
|
69
|
+
from dashboard_spec import (
|
|
70
|
+
DashboardSpec,
|
|
71
|
+
SemanticModel,
|
|
72
|
+
WidgetSpec,
|
|
73
|
+
parse_dashboard_yaml,
|
|
74
|
+
parse_semantic_model_yaml,
|
|
75
|
+
parse_widget_yaml,
|
|
76
|
+
)
|
|
77
|
+
from dashboard_store import (
|
|
78
|
+
DashboardModelRepository,
|
|
79
|
+
ModelRepository,
|
|
80
|
+
ShareRepository,
|
|
81
|
+
WidgetRepository,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
from bideux._guard import (
|
|
85
|
+
GRANULAR_DISTRIBUTIONS,
|
|
86
|
+
BideuxInstallConflict,
|
|
87
|
+
installed_granular_distributions,
|
|
88
|
+
warn_on_conflict,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
__version__ = "0.1.2"
|
|
92
|
+
|
|
93
|
+
warn_on_conflict()
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"GRANULAR_DISTRIBUTIONS",
|
|
97
|
+
"AccessPolicy",
|
|
98
|
+
"AuditEvent",
|
|
99
|
+
"AuditPort",
|
|
100
|
+
"BideuxInstallConflict",
|
|
101
|
+
"BrandingPort",
|
|
102
|
+
"BuiltReport",
|
|
103
|
+
"Caller",
|
|
104
|
+
"CapturePort",
|
|
105
|
+
"DashboardAccessError",
|
|
106
|
+
"DashboardAuthoringService",
|
|
107
|
+
"DashboardEngine",
|
|
108
|
+
"DashboardEngineError",
|
|
109
|
+
"DashboardLayoutConflictError",
|
|
110
|
+
"DashboardLinkPort",
|
|
111
|
+
"DashboardListing",
|
|
112
|
+
"DashboardModelRepository",
|
|
113
|
+
"DashboardNotFoundError",
|
|
114
|
+
"DashboardPermission",
|
|
115
|
+
"DashboardRef",
|
|
116
|
+
"DashboardRender",
|
|
117
|
+
"DashboardReportPort",
|
|
118
|
+
"DashboardReportRequest",
|
|
119
|
+
"DashboardReportService",
|
|
120
|
+
"DashboardRepositoryPort",
|
|
121
|
+
"DashboardService",
|
|
122
|
+
"DashboardServiceError",
|
|
123
|
+
"DashboardSpec",
|
|
124
|
+
"DefaultAccessPolicy",
|
|
125
|
+
"EngineFactoryPort",
|
|
126
|
+
"EngineOptions",
|
|
127
|
+
"InvalidDocumentError",
|
|
128
|
+
"ModelRepository",
|
|
129
|
+
"ModelRepositoryPort",
|
|
130
|
+
"Principal",
|
|
131
|
+
"PrincipalDirectoryPort",
|
|
132
|
+
"RenderCachePort",
|
|
133
|
+
"ReportBranding",
|
|
134
|
+
"ReportFormat",
|
|
135
|
+
"Repositories",
|
|
136
|
+
"ScopeContext",
|
|
137
|
+
"SemanticModel",
|
|
138
|
+
"ServiceOptions",
|
|
139
|
+
"ShareRepository",
|
|
140
|
+
"ShareRepositoryPort",
|
|
141
|
+
"SharingDisabledError",
|
|
142
|
+
"SlugTakenError",
|
|
143
|
+
"SlugUnavailableError",
|
|
144
|
+
"UnitOfWorkPort",
|
|
145
|
+
"WarehousePort",
|
|
146
|
+
"WidgetNotFoundError",
|
|
147
|
+
"WidgetRepository",
|
|
148
|
+
"WidgetRepositoryPort",
|
|
149
|
+
"WidgetSpec",
|
|
150
|
+
"__version__",
|
|
151
|
+
"installed_granular_distributions",
|
|
152
|
+
"parse_dashboard_yaml",
|
|
153
|
+
"parse_semantic_model_yaml",
|
|
154
|
+
"parse_widget_yaml",
|
|
155
|
+
"slugify",
|
|
156
|
+
"warn_on_conflict",
|
|
157
|
+
]
|
bideux/_guard.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Refuse to run next to the granular distributions this wheel already contains.
|
|
2
|
+
|
|
3
|
+
``bideux`` and the ``bideux-dashboard-*`` wheels install the same import
|
|
4
|
+
packages (``dashboard_spec``, ``dashboard_server`` and the rest), so having
|
|
5
|
+
both means whichever was installed last silently wins. Python metadata has no
|
|
6
|
+
conflicts field, so the check is done here, once, at import.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
|
|
14
|
+
GRANULAR_DISTRIBUTIONS: tuple[str, ...] = (
|
|
15
|
+
"bideux-dashboard-spec",
|
|
16
|
+
"bideux-dashboard-core",
|
|
17
|
+
"bideux-dashboard-engine",
|
|
18
|
+
"bideux-dashboard-store",
|
|
19
|
+
"bideux-dashboard-server",
|
|
20
|
+
"bideux-dashboard-report",
|
|
21
|
+
"bideux-dashboard-authoring",
|
|
22
|
+
"bideux-dashboard-schedule",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BideuxInstallConflict(RuntimeWarning):
|
|
27
|
+
"""Both the bundle and one or more granular distributions are installed."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def installed_granular_distributions() -> tuple[str, ...]:
|
|
31
|
+
"""Return the ``bideux-dashboard-*`` distributions present in this environment."""
|
|
32
|
+
found: list[str] = []
|
|
33
|
+
for name in GRANULAR_DISTRIBUTIONS:
|
|
34
|
+
try:
|
|
35
|
+
metadata.version(name)
|
|
36
|
+
except metadata.PackageNotFoundError:
|
|
37
|
+
continue
|
|
38
|
+
found.append(name)
|
|
39
|
+
return tuple(found)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def warn_on_conflict() -> tuple[str, ...]:
|
|
43
|
+
"""Warn once when granular distributions share the environment with ``bideux``."""
|
|
44
|
+
found = installed_granular_distributions()
|
|
45
|
+
if found:
|
|
46
|
+
warnings.warn(
|
|
47
|
+
"bideux is installed alongside "
|
|
48
|
+
+ ", ".join(found)
|
|
49
|
+
+ ". Both provide the same dashboard_* modules; uninstall one family. "
|
|
50
|
+
"Hosts pick either `bideux` or the `bideux-dashboard-*` packages, never both.",
|
|
51
|
+
BideuxInstallConflict,
|
|
52
|
+
stacklevel=3,
|
|
53
|
+
)
|
|
54
|
+
return found
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"GRANULAR_DISTRIBUTIONS",
|
|
59
|
+
"BideuxInstallConflict",
|
|
60
|
+
"installed_granular_distributions",
|
|
61
|
+
"warn_on_conflict",
|
|
62
|
+
]
|
bideux/authoring.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""AI assisted authoring of models, widgets and dashboards over host provided ports.
|
|
2
|
+
|
|
3
|
+
Every public name of ``dashboard_authoring`` re-exported under the ``bideux`` namespace, so a
|
|
4
|
+
host that installs the single ``bideux`` wheel never imports a granular module name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dashboard_authoring import * # noqa: F403
|
|
10
|
+
from dashboard_authoring import __all__ as _base
|
|
11
|
+
|
|
12
|
+
__all__ = [*_base]
|
bideux/cli.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""The ``bideux`` command: ``demo``, ``init`` and ``doctor``.
|
|
2
|
+
|
|
3
|
+
``demo`` and ``init`` need the ``fastapi`` extra (and ``demo`` the ``demo``
|
|
4
|
+
extra for uvicorn); ``doctor`` needs nothing beyond the base install, which is
|
|
5
|
+
why the demo module is loaded only when that subcommand runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from importlib import import_module
|
|
13
|
+
from importlib.util import find_spec
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from bideux import doctor, init
|
|
17
|
+
from bideux.corpus import find_fixtures
|
|
18
|
+
|
|
19
|
+
DEMO_PORT = 5477
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _demo(args: argparse.Namespace) -> int:
|
|
23
|
+
missing = [name for name in ("fastapi", "aiosqlite", "uvicorn") if find_spec(name) is None]
|
|
24
|
+
if missing:
|
|
25
|
+
print(
|
|
26
|
+
"bideux demo needs " + ", ".join(missing) + ": uv add 'bideux[demo]'",
|
|
27
|
+
file=sys.stderr,
|
|
28
|
+
)
|
|
29
|
+
return 2
|
|
30
|
+
fixtures = Path(args.fixtures) if args.fixtures else find_fixtures(Path.cwd())
|
|
31
|
+
if fixtures is None:
|
|
32
|
+
print(
|
|
33
|
+
"no conformance/fixtures directory at or above the working directory; "
|
|
34
|
+
"pass --fixtures PATH (the corpus lives in the bideux repository)",
|
|
35
|
+
file=sys.stderr,
|
|
36
|
+
)
|
|
37
|
+
return 2
|
|
38
|
+
element_dir = Path(args.element_dir) if args.element_dir else None
|
|
39
|
+
demo = import_module("bideux.demo")
|
|
40
|
+
uvicorn = import_module("uvicorn")
|
|
41
|
+
app = demo.create_demo_app(
|
|
42
|
+
fixtures=fixtures,
|
|
43
|
+
database_url=args.database_url,
|
|
44
|
+
prefix=args.prefix,
|
|
45
|
+
element_dir=element_dir,
|
|
46
|
+
)
|
|
47
|
+
print(f"Bideux demo at http://{args.host}:{args.port}/ (API under {args.prefix})")
|
|
48
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _init(args: argparse.Namespace) -> int:
|
|
53
|
+
for written in init.scaffold(
|
|
54
|
+
Path(args.target), prefix=args.prefix, port=args.port, force=args.force
|
|
55
|
+
):
|
|
56
|
+
verb = "wrote" if written.created else "kept"
|
|
57
|
+
print(f"{verb} {written.path}")
|
|
58
|
+
print(
|
|
59
|
+
"next: uv add 'bideux[fastapi,sqlite]' uvicorn, fill in MyHost, then "
|
|
60
|
+
f"uvicorn bideux_host:app --port {args.port} and open dashboard.html"
|
|
61
|
+
)
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _doctor(args: argparse.Namespace) -> int:
|
|
66
|
+
findings = doctor.run(Path(args.project))
|
|
67
|
+
for finding in findings:
|
|
68
|
+
print(finding.line())
|
|
69
|
+
return 1 if doctor.worst(findings) == "fail" else 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
73
|
+
parser = argparse.ArgumentParser(prog="bideux", description="Bideux dashboards in one install.")
|
|
74
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
75
|
+
|
|
76
|
+
demo = commands.add_parser("demo", help="run a seeded dashboard API and page on one port")
|
|
77
|
+
demo.add_argument("--host", default="127.0.0.1")
|
|
78
|
+
demo.add_argument("--port", type=int, default=DEMO_PORT)
|
|
79
|
+
demo.add_argument("--prefix", default="/api/dashboards")
|
|
80
|
+
demo.add_argument(
|
|
81
|
+
"--database-url",
|
|
82
|
+
default="sqlite+aiosqlite:///bideux-demo.sqlite3",
|
|
83
|
+
help="where the store lives; delete the file to reseed",
|
|
84
|
+
)
|
|
85
|
+
demo.add_argument(
|
|
86
|
+
"--fixtures", help="path to conformance/fixtures; found automatically in the repo"
|
|
87
|
+
)
|
|
88
|
+
demo.add_argument(
|
|
89
|
+
"--element-dir",
|
|
90
|
+
help="a built packages/bideux/dist to serve instead of loading @dlh.io/bideux from unpkg",
|
|
91
|
+
)
|
|
92
|
+
demo.set_defaults(run=_demo)
|
|
93
|
+
|
|
94
|
+
scaffold = commands.add_parser(
|
|
95
|
+
"init", help="write bideux_host.py and dashboard.html to start from"
|
|
96
|
+
)
|
|
97
|
+
scaffold.add_argument("target", nargs="?", default=".")
|
|
98
|
+
scaffold.add_argument("--prefix", default="/api/dashboards")
|
|
99
|
+
scaffold.add_argument("--port", type=int, default=DEMO_PORT)
|
|
100
|
+
scaffold.add_argument("--force", action="store_true", help="overwrite files that exist")
|
|
101
|
+
scaffold.set_defaults(run=_init)
|
|
102
|
+
|
|
103
|
+
check = commands.add_parser(
|
|
104
|
+
"doctor", help="check the Python and Node install for known problems"
|
|
105
|
+
)
|
|
106
|
+
check.add_argument("project", nargs="?", default=".", help="directory holding package.json")
|
|
107
|
+
check.set_defaults(run=_doctor)
|
|
108
|
+
return parser
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv: list[str] | None = None) -> int:
|
|
112
|
+
args = build_parser().parse_args(argv)
|
|
113
|
+
result = args.run(args)
|
|
114
|
+
return result if isinstance(result, int) else 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
raise SystemExit(main())
|
bideux/core.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Headless dashboard logic: layout, filters, pivots and tokens.
|
|
2
|
+
|
|
3
|
+
Every public name of ``dashboard_core`` re-exported under the ``bideux`` namespace, so a
|
|
4
|
+
host that installs the single ``bideux`` wheel never imports a granular module name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dashboard_core import * # noqa: F403
|
|
10
|
+
from dashboard_core import __all__ as _base
|
|
11
|
+
|
|
12
|
+
__all__ = [*_base]
|
bideux/corpus.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Where the conformance corpus is, for the commands that read it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
FIXTURE_DIRECTORY = Path("conformance") / "fixtures"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def find_fixtures(start: Path) -> Path | None:
|
|
11
|
+
"""The corpus directory at or above ``start``, when running inside the repo."""
|
|
12
|
+
for candidate in (start, *start.parents):
|
|
13
|
+
found = candidate / FIXTURE_DIRECTORY
|
|
14
|
+
if (found / "dashboards").is_dir():
|
|
15
|
+
return found
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["FIXTURE_DIRECTORY", "find_fixtures"]
|
bideux/demo.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""``bideux demo``: a working dashboard API and page in one command.
|
|
2
|
+
|
|
3
|
+
The demo is :func:`bideux.fastapi.mount_bideux` over a sqlite file with a
|
|
4
|
+
:class:`HostAdapter` whose warehouse is the in-memory reference warehouse
|
|
5
|
+
evaluating the corpus rows, seeded with the ``retail_overview`` dashboard from
|
|
6
|
+
``conformance/fixtures``. The page at ``/`` is the custom element from
|
|
7
|
+
``@dlh.io/bideux/element``, loaded from a CDN or from a local ``dist``
|
|
8
|
+
directory. Nothing in it is meant for production; it shows the shape a host
|
|
9
|
+
copies with ``bideux init``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from dashboard_engine import ResolvedLocations, WarehousePort
|
|
22
|
+
from dashboard_engine.reference import ReferenceWarehouse, reference_tables
|
|
23
|
+
from dashboard_engine.results import DataStoreDescriptor, Row
|
|
24
|
+
from dashboard_server import Caller
|
|
25
|
+
from fastapi import FastAPI, Request
|
|
26
|
+
from fastapi.responses import HTMLResponse
|
|
27
|
+
from fastapi.staticfiles import StaticFiles
|
|
28
|
+
|
|
29
|
+
from bideux.corpus import find_fixtures
|
|
30
|
+
from bideux.fastapi.mount import BideuxMount, mount_bideux
|
|
31
|
+
from bideux.hosting import HostAdapter
|
|
32
|
+
from bideux.quickstart import local_caller
|
|
33
|
+
|
|
34
|
+
DISPLAY_NAMES = {"10001": "Downtown", "10002": "Riverside", "10003": "Airport"}
|
|
35
|
+
DEMO_SLUG = "retail-overview"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_tables(fixtures: Path) -> dict[str, tuple[Row, ...]]:
|
|
39
|
+
payload = json.loads((fixtures / "data" / "warehouse_rows.json").read_text())
|
|
40
|
+
tables: Mapping[str, Mapping[str, Sequence[Row]]] = payload["tables"]
|
|
41
|
+
rows: dict[str, Sequence[Row]] = {
|
|
42
|
+
name: [dict(row) for row in table["rows"]] for name, table in tables.items()
|
|
43
|
+
}
|
|
44
|
+
return reference_tables(rows)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class DemoDocuments:
|
|
49
|
+
model_yaml: str
|
|
50
|
+
dashboard_yaml: str
|
|
51
|
+
widget_yamls: dict[str, str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_documents(fixtures: Path) -> DemoDocuments:
|
|
55
|
+
widgets = {
|
|
56
|
+
path.name.removesuffix(".widget.yaml"): path.read_text()
|
|
57
|
+
for path in sorted((fixtures / "widgets").glob("*.widget.yaml"))
|
|
58
|
+
}
|
|
59
|
+
return DemoDocuments(
|
|
60
|
+
model_yaml=(fixtures / "model" / "retail.model.yaml").read_text(),
|
|
61
|
+
dashboard_yaml=(fixtures / "dashboards" / "retail_overview.dashboard.yaml").read_text(),
|
|
62
|
+
widget_yamls=widgets,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DemoHost(HostAdapter):
|
|
67
|
+
"""Everyone is ``?person=`` (default ``demo``), an admin; rows come from the corpus."""
|
|
68
|
+
|
|
69
|
+
model_slug = "retail"
|
|
70
|
+
|
|
71
|
+
def __init__(self, fixtures: Path) -> None:
|
|
72
|
+
self._warehouse = ReferenceWarehouse(
|
|
73
|
+
tables=load_tables(fixtures),
|
|
74
|
+
descriptor=DataStoreDescriptor("reference", "snowflake", "MART"),
|
|
75
|
+
locations=ResolvedLocations(None, None),
|
|
76
|
+
display_names=DISPLAY_NAMES,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def caller(self, request: Request) -> Caller:
|
|
80
|
+
return local_caller(request.query_params.get("person", "demo"))
|
|
81
|
+
|
|
82
|
+
def warehouse(self, caller: Caller) -> WarehousePort:
|
|
83
|
+
del caller
|
|
84
|
+
return self._warehouse
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def element_script_url(element_dir: Path | None) -> str:
|
|
88
|
+
if element_dir is not None:
|
|
89
|
+
return "/element/element/element.js"
|
|
90
|
+
try:
|
|
91
|
+
release = version("bideux")
|
|
92
|
+
except PackageNotFoundError:
|
|
93
|
+
release = "latest"
|
|
94
|
+
return f"https://unpkg.com/@dlh.io/bideux@{release}/dist/element/element.js"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def stylesheet_urls(element_dir: Path | None) -> tuple[str, str]:
|
|
98
|
+
if element_dir is not None:
|
|
99
|
+
return ("/element/styles.css", "/element/theme.css")
|
|
100
|
+
base = element_script_url(None).removesuffix("/element/element.js")
|
|
101
|
+
return (f"{base}/styles.css", f"{base}/theme.css")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def demo_page(*, prefix: str, element_dir: Path | None) -> str:
|
|
105
|
+
styles, theme = stylesheet_urls(element_dir)
|
|
106
|
+
script = element_script_url(element_dir)
|
|
107
|
+
return f"""<!doctype html>
|
|
108
|
+
<html lang="en">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="UTF-8" />
|
|
111
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
112
|
+
<title>Bideux demo</title>
|
|
113
|
+
<link rel="stylesheet" href="{styles}" />
|
|
114
|
+
<link rel="stylesheet" href="{theme}" />
|
|
115
|
+
<script type="module" src="{script}"></script>
|
|
116
|
+
<style>
|
|
117
|
+
body {{ margin: 0; font-family: system-ui, sans-serif; }}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<bideux-dashboard-list id="list" base-url="{prefix}"></bideux-dashboard-list>
|
|
122
|
+
<bideux-dashboard id="page" base-url="{prefix}" can-edit-model hidden></bideux-dashboard>
|
|
123
|
+
<script type="module">
|
|
124
|
+
const list = document.getElementById("list");
|
|
125
|
+
const page = document.getElementById("page");
|
|
126
|
+
const open = (dashboardId, editing) => {{
|
|
127
|
+
page.setAttribute("dashboard-id", dashboardId);
|
|
128
|
+
page.toggleAttribute("start-in-edit-mode", editing);
|
|
129
|
+
page.hidden = false;
|
|
130
|
+
list.hidden = true;
|
|
131
|
+
}};
|
|
132
|
+
list.addEventListener("open", (event) =>
|
|
133
|
+
open(event.detail.dashboardId, event.detail.editing),
|
|
134
|
+
);
|
|
135
|
+
page.addEventListener("opendashboard", (event) => open(event.detail.dashboardId, false));
|
|
136
|
+
page.addEventListener("back", () => {{
|
|
137
|
+
page.hidden = true;
|
|
138
|
+
list.hidden = false;
|
|
139
|
+
}});
|
|
140
|
+
const wanted = new URL(location.href).searchParams.get("dashboard");
|
|
141
|
+
if (wanted) open(wanted, false);
|
|
142
|
+
</script>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def seed(mount: BideuxMount, documents: DemoDocuments) -> None:
|
|
149
|
+
"""Write the corpus model and dashboard once; a second start finds them and stops."""
|
|
150
|
+
async with mount.service(local_caller("demo")) as service:
|
|
151
|
+
if await service.dashboard_exists(DEMO_SLUG):
|
|
152
|
+
return
|
|
153
|
+
await service.update_global_model(documents.model_yaml, note="bideux demo seed")
|
|
154
|
+
await service.upsert_from_spec(
|
|
155
|
+
documents.dashboard_yaml,
|
|
156
|
+
documents.widget_yamls,
|
|
157
|
+
visibility="org",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def create_demo_app(
|
|
162
|
+
*,
|
|
163
|
+
fixtures: Path,
|
|
164
|
+
database_url: str,
|
|
165
|
+
prefix: str = "/api/dashboards",
|
|
166
|
+
element_dir: Path | None = None,
|
|
167
|
+
) -> FastAPI:
|
|
168
|
+
"""The demo app: routes at ``prefix``, the page at ``/``, seeded on start."""
|
|
169
|
+
documents = load_documents(fixtures)
|
|
170
|
+
|
|
171
|
+
@asynccontextmanager
|
|
172
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
173
|
+
del app
|
|
174
|
+
await seed(mount, documents)
|
|
175
|
+
yield
|
|
176
|
+
|
|
177
|
+
app = FastAPI(title="Bideux demo", lifespan=lifespan)
|
|
178
|
+
mount = mount_bideux(app, host=DemoHost(fixtures), database_url=database_url, prefix=prefix)
|
|
179
|
+
if element_dir is not None:
|
|
180
|
+
app.mount("/element", StaticFiles(directory=element_dir), name="element")
|
|
181
|
+
|
|
182
|
+
page = demo_page(prefix=prefix, element_dir=element_dir)
|
|
183
|
+
|
|
184
|
+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
185
|
+
async def index() -> str:
|
|
186
|
+
return page
|
|
187
|
+
|
|
188
|
+
return app
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
"DemoDocuments",
|
|
193
|
+
"DemoHost",
|
|
194
|
+
"create_demo_app",
|
|
195
|
+
"demo_page",
|
|
196
|
+
"find_fixtures",
|
|
197
|
+
"load_documents",
|
|
198
|
+
"load_tables",
|
|
199
|
+
"seed",
|
|
200
|
+
]
|