dashdown-md 0.1.0__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.
- dashdown/__init__.py +16 -0
- dashdown/asgi.py +34 -0
- dashdown/auth.py +171 -0
- dashdown/build.py +817 -0
- dashdown/catalog.py +438 -0
- dashdown/cli.py +1054 -0
- dashdown/components/__init__.py +45 -0
- dashdown/components/base.py +99 -0
- dashdown/components/builtin/__init__.py +0 -0
- dashdown/components/builtin/_util.py +287 -0
- dashdown/components/builtin/ask.py +124 -0
- dashdown/components/builtin/auto_chart.py +24 -0
- dashdown/components/builtin/bar_chart.py +3 -0
- dashdown/components/builtin/box_plot.py +45 -0
- dashdown/components/builtin/calendar_heatmap.py +22 -0
- dashdown/components/builtin/candlestick_chart.py +58 -0
- dashdown/components/builtin/combo_chart.py +186 -0
- dashdown/components/builtin/counter.py +204 -0
- dashdown/components/builtin/date_range.py +168 -0
- dashdown/components/builtin/dropdown.py +211 -0
- dashdown/components/builtin/gauge_chart.py +25 -0
- dashdown/components/builtin/graph_chart.py +58 -0
- dashdown/components/builtin/grid.py +51 -0
- dashdown/components/builtin/heatmap_chart.py +52 -0
- dashdown/components/builtin/hierarchy_chart.py +67 -0
- dashdown/components/builtin/line_chart.py +241 -0
- dashdown/components/builtin/map_chart.py +30 -0
- dashdown/components/builtin/parallel_chart.py +57 -0
- dashdown/components/builtin/pivot_table.py +66 -0
- dashdown/components/builtin/radar_chart.py +18 -0
- dashdown/components/builtin/sankey_chart.py +55 -0
- dashdown/components/builtin/search.py +93 -0
- dashdown/components/builtin/site_search.py +62 -0
- dashdown/components/builtin/table.py +266 -0
- dashdown/components/builtin/theme_river.py +23 -0
- dashdown/components/builtin/timegrain.py +121 -0
- dashdown/components/builtin/toggle.py +131 -0
- dashdown/components/builtin/value.py +99 -0
- dashdown/data/__init__.py +3 -0
- dashdown/data/base.py +183 -0
- dashdown/data/bigquery_connector.py +117 -0
- dashdown/data/csv_connector.py +66 -0
- dashdown/data/cube_connector.py +290 -0
- dashdown/data/dax_connector.py +235 -0
- dashdown/data/dbapi.py +184 -0
- dashdown/data/duckdb_connector.py +117 -0
- dashdown/data/excel_connector.py +76 -0
- dashdown/data/introspect.py +90 -0
- dashdown/data/json_connector.py +75 -0
- dashdown/data/motherduck_connector.py +93 -0
- dashdown/data/mssql_connector.py +208 -0
- dashdown/data/mysql_connector.py +50 -0
- dashdown/data/parquet_connector.py +70 -0
- dashdown/data/postgres_connector.py +46 -0
- dashdown/data/quack_connector.py +151 -0
- dashdown/data/registry.py +37 -0
- dashdown/data/sheets_connector.py +88 -0
- dashdown/data/snowflake_connector.py +47 -0
- dashdown/data/tabular.py +109 -0
- dashdown/embed.py +216 -0
- dashdown/llm.py +498 -0
- dashdown/pdf.py +370 -0
- dashdown/project.py +875 -0
- dashdown/python_query.py +365 -0
- dashdown/query_composition.py +241 -0
- dashdown/query_library.py +118 -0
- dashdown/render/__init__.py +0 -0
- dashdown/render/attrs.py +70 -0
- dashdown/render/components.py +172 -0
- dashdown/render/icons.py +207 -0
- dashdown/render/markdown.py +407 -0
- dashdown/render/pipeline.py +974 -0
- dashdown/scaffold/AGENTS.md +125 -0
- dashdown/scaffold/claude/skills/dashdown-authoring/SKILL.md +93 -0
- dashdown/scaffold/references/ai.md +225 -0
- dashdown/scaffold/references/authentication.md +84 -0
- dashdown/scaffold/references/catalog.md +69 -0
- dashdown/scaffold/references/cli.md +305 -0
- dashdown/scaffold/references/components.md +1724 -0
- dashdown/scaffold/references/configuration.md +209 -0
- dashdown/scaffold/references/connectors.md +697 -0
- dashdown/scaffold/references/detail-pages.md +147 -0
- dashdown/scaffold/references/embedding.md +35 -0
- dashdown/scaffold/references/exporting.md +78 -0
- dashdown/scaffold/references/extending.md +176 -0
- dashdown/scaffold/references/filters.md +123 -0
- dashdown/scaffold/references/formatting.md +145 -0
- dashdown/scaffold/references/getting-started.md +118 -0
- dashdown/scaffold/references/pages.md +191 -0
- dashdown/scaffold/references/python-queries.md +123 -0
- dashdown/scaffold/references/queries.md +119 -0
- dashdown/scaffold/references/realtime.md +66 -0
- dashdown/scaffold/references/search.md +68 -0
- dashdown/scaffold/references/semantic-layer.md +492 -0
- dashdown/scaffold/references/telemetry.md +79 -0
- dashdown/scaffold/references/theming.md +180 -0
- dashdown/screenshot.py +210 -0
- dashdown/search.py +107 -0
- dashdown/semantic.py +890 -0
- dashdown/semantic_base.py +218 -0
- dashdown/semantic_cube.py +526 -0
- dashdown/server.py +1017 -0
- dashdown/static/app.js +235 -0
- dashdown/static/components/ask.js +140 -0
- dashdown/static/components/chart.js +1863 -0
- dashdown/static/components/counter.js +221 -0
- dashdown/static/components/daterange.js +332 -0
- dashdown/static/components/dropdown.js +472 -0
- dashdown/static/components/echarts_theme.js +322 -0
- dashdown/static/components/embed_frame.js +59 -0
- dashdown/static/components/embed_ui.js +117 -0
- dashdown/static/components/export.js +87 -0
- dashdown/static/components/export_modal.js +103 -0
- dashdown/static/components/filter_badge.js +158 -0
- dashdown/static/components/filter_bar.js +187 -0
- dashdown/static/components/mermaid.js +223 -0
- dashdown/static/components/page_header.js +76 -0
- dashdown/static/components/pivot.js +367 -0
- dashdown/static/components/print.js +370 -0
- dashdown/static/components/search.js +98 -0
- dashdown/static/components/site_search.js +255 -0
- dashdown/static/components/table.js +626 -0
- dashdown/static/components/timegrain.js +107 -0
- dashdown/static/components/toggle.js +114 -0
- dashdown/static/components/value.js +88 -0
- dashdown/static/core.js +719 -0
- dashdown/static/dashdown.css +2025 -0
- dashdown/static/dashdown.js +51 -0
- dashdown/static/embed.js +73 -0
- dashdown/static/favicon.svg +18 -0
- dashdown/static/legacy.js +257 -0
- dashdown/static/loading.js +55 -0
- dashdown/static/store.js +285 -0
- dashdown/static/vendor/alpine.min.js +5 -0
- dashdown/static/vendor/echarts.min.js +45 -0
- dashdown/static/vendor/fonts/inter.woff2 +0 -0
- dashdown/static/vendor/mermaid.min.js +3405 -0
- dashdown/static/vendor/tailwind.css +6 -0
- dashdown/static/vendor/world.json +1 -0
- dashdown/streaming.py +201 -0
- dashdown/telemetry.py +279 -0
- dashdown/templates/page.html +301 -0
- dashdown_md-0.1.0.dist-info/METADATA +280 -0
- dashdown_md-0.1.0.dist-info/RECORD +150 -0
- dashdown_md-0.1.0.dist-info/WHEEL +5 -0
- dashdown_md-0.1.0.dist-info/entry_points.txt +23 -0
- dashdown_md-0.1.0.dist-info/licenses/LICENSE +661 -0
- dashdown_md-0.1.0.dist-info/licenses/LICENSING.md +70 -0
- dashdown_md-0.1.0.dist-info/licenses/THIRD-PARTY-NOTICES.md +69 -0
- dashdown_md-0.1.0.dist-info/top_level.txt +1 -0
dashdown/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Dashdown: markdown-driven analytics pages."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from dashdown.components.base import register_component, Component
|
|
6
|
+
from dashdown.data.base import register_connector, Connector, QueryResult
|
|
7
|
+
from dashdown.python_query import query
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"register_component",
|
|
11
|
+
"Component",
|
|
12
|
+
"register_connector",
|
|
13
|
+
"Connector",
|
|
14
|
+
"QueryResult",
|
|
15
|
+
"query",
|
|
16
|
+
]
|
dashdown/asgi.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Production ASGI entry point.
|
|
2
|
+
|
|
3
|
+
Run the live server under multiple workers without the dev CLI:
|
|
4
|
+
|
|
5
|
+
DASHDOWN_PROJECT=/srv/dashboard uvicorn dashdown.asgi:app \
|
|
6
|
+
--host 0.0.0.0 --port 8000 --workers 4
|
|
7
|
+
|
|
8
|
+
Each worker imports this module, so each builds the app in production posture
|
|
9
|
+
(``dev=False``): no live-reload SSE and every page's queries pre-registered, so
|
|
10
|
+
a worker can answer a data request for any page regardless of which worker
|
|
11
|
+
rendered it. ``dashdown serve`` is the dev path and is unaffected.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from dashdown.server import create_app
|
|
19
|
+
|
|
20
|
+
_project = os.environ.get("DASHDOWN_PROJECT")
|
|
21
|
+
if not _project:
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"DASHDOWN_PROJECT is not set. Point it at your project directory "
|
|
24
|
+
"(the one with dashdown.yaml), e.g. DASHDOWN_PROJECT=/srv/dashboard."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_project_root = Path(_project).expanduser().resolve()
|
|
28
|
+
if not (_project_root / "dashdown.yaml").is_file():
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
f"No dashdown.yaml under DASHDOWN_PROJECT={_project_root} — "
|
|
31
|
+
"is it pointing at a Dashdown project directory?"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
app = create_app(_project_root, dev=False)
|
dashdown/auth.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Built-in authentication: HTTP Basic Auth and static API-key header.
|
|
2
|
+
|
|
3
|
+
Two modes, both configured under an ``auth:`` block in ``dashdown.yaml``:
|
|
4
|
+
|
|
5
|
+
auth:
|
|
6
|
+
type: basic # browser-friendly; the browser prompts + resends
|
|
7
|
+
username: admin
|
|
8
|
+
password: ${DASH_PASSWORD} # ${VAR} reads from the environment
|
|
9
|
+
# or, for several accounts:
|
|
10
|
+
# users:
|
|
11
|
+
# admin: ${ADMIN_PW}
|
|
12
|
+
# viewer: readonly
|
|
13
|
+
|
|
14
|
+
auth:
|
|
15
|
+
type: api_key # for proxies / programmatic access
|
|
16
|
+
header: X-API-Key # optional, this is the default
|
|
17
|
+
key: ${DASH_API_KEY}
|
|
18
|
+
# or: keys: [${KEY_A}, ${KEY_B}]
|
|
19
|
+
|
|
20
|
+
``type: none`` (the default) leaves the app open. Secrets compare in constant
|
|
21
|
+
time (``secrets.compare_digest``).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import base64
|
|
26
|
+
import binascii
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from secrets import compare_digest
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
_ENV_RE = re.compile(r"^\$\{(\w+)\}$")
|
|
34
|
+
_VALID_TYPES = ("none", "basic", "api_key")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AuthConfig:
|
|
39
|
+
"""Resolved auth settings. Secrets are already env-expanded."""
|
|
40
|
+
|
|
41
|
+
type: str = "none"
|
|
42
|
+
realm: str = "Dashdown"
|
|
43
|
+
users: dict[str, str] = field(default_factory=dict) # basic: username -> password
|
|
44
|
+
header: str = "X-API-Key" # api_key: header name to read
|
|
45
|
+
keys: list[str] = field(default_factory=list) # api_key: accepted keys
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def enabled(self) -> bool:
|
|
49
|
+
return self.type in ("basic", "api_key")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_secret(value: Any) -> str:
|
|
53
|
+
"""Expand a ``${VAR}`` reference from the environment, else return as-is."""
|
|
54
|
+
s = str(value)
|
|
55
|
+
m = _ENV_RE.match(s.strip())
|
|
56
|
+
if m:
|
|
57
|
+
env_val = os.environ.get(m.group(1))
|
|
58
|
+
if env_val is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"auth config references environment variable {m.group(1)!r}, "
|
|
61
|
+
"which is not set"
|
|
62
|
+
)
|
|
63
|
+
return env_val
|
|
64
|
+
return s
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_auth_config(raw: dict | None) -> AuthConfig:
|
|
68
|
+
"""Build an :class:`AuthConfig` from the ``auth`` block of dashdown.yaml.
|
|
69
|
+
|
|
70
|
+
Raises ``ValueError`` on misconfiguration so the server refuses to start
|
|
71
|
+
open when the operator clearly intended it locked down.
|
|
72
|
+
"""
|
|
73
|
+
if not raw:
|
|
74
|
+
return AuthConfig()
|
|
75
|
+
if not isinstance(raw, dict):
|
|
76
|
+
raise ValueError("auth config must be a mapping")
|
|
77
|
+
|
|
78
|
+
typ = str(raw.get("type", "none")).lower()
|
|
79
|
+
if typ not in _VALID_TYPES:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"unknown auth.type {typ!r} (expected one of {', '.join(_VALID_TYPES)})"
|
|
82
|
+
)
|
|
83
|
+
if typ == "none":
|
|
84
|
+
return AuthConfig(type="none")
|
|
85
|
+
|
|
86
|
+
realm = str(raw.get("realm", "Dashdown"))
|
|
87
|
+
|
|
88
|
+
if typ == "basic":
|
|
89
|
+
users: dict[str, str] = {}
|
|
90
|
+
if raw.get("username") is not None:
|
|
91
|
+
users[str(raw["username"])] = _resolve_secret(raw.get("password", ""))
|
|
92
|
+
extra = raw.get("users") or {}
|
|
93
|
+
if not isinstance(extra, dict):
|
|
94
|
+
raise ValueError("auth.users must be a mapping of username -> password")
|
|
95
|
+
for u, p in extra.items():
|
|
96
|
+
users[str(u)] = _resolve_secret(p)
|
|
97
|
+
if not users:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"auth.type 'basic' requires a username/password or a users mapping"
|
|
100
|
+
)
|
|
101
|
+
return AuthConfig(type="basic", realm=realm, users=users)
|
|
102
|
+
|
|
103
|
+
# api_key
|
|
104
|
+
header = str(raw.get("header", "X-API-Key"))
|
|
105
|
+
keys: list[str] = []
|
|
106
|
+
if raw.get("key") is not None:
|
|
107
|
+
keys.append(_resolve_secret(raw["key"]))
|
|
108
|
+
extra_keys = raw.get("keys") or []
|
|
109
|
+
if not isinstance(extra_keys, (list, tuple)):
|
|
110
|
+
raise ValueError("auth.keys must be a list")
|
|
111
|
+
for k in extra_keys:
|
|
112
|
+
keys.append(_resolve_secret(k))
|
|
113
|
+
if not keys:
|
|
114
|
+
raise ValueError("auth.type 'api_key' requires a key or a keys list")
|
|
115
|
+
return AuthConfig(type="api_key", realm=realm, header=header, keys=keys)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _check_basic(config: AuthConfig, header_value: str | None) -> bool:
|
|
119
|
+
if not header_value:
|
|
120
|
+
return False
|
|
121
|
+
scheme, _, encoded = header_value.partition(" ")
|
|
122
|
+
if scheme.lower() != "basic" or not encoded:
|
|
123
|
+
return False
|
|
124
|
+
try:
|
|
125
|
+
decoded = base64.b64decode(encoded.strip(), validate=True).decode("utf-8")
|
|
126
|
+
except (binascii.Error, ValueError, UnicodeDecodeError):
|
|
127
|
+
return False
|
|
128
|
+
username, sep, password = decoded.partition(":")
|
|
129
|
+
if not sep:
|
|
130
|
+
return False
|
|
131
|
+
expected = config.users.get(username)
|
|
132
|
+
if expected is None:
|
|
133
|
+
# Compare against the supplied value anyway so a missing username and a
|
|
134
|
+
# wrong password take a similar amount of time.
|
|
135
|
+
compare_digest(password, password)
|
|
136
|
+
return False
|
|
137
|
+
return compare_digest(password, expected)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_api_key(config: AuthConfig, provided: str | None) -> bool:
|
|
141
|
+
if not provided:
|
|
142
|
+
return False
|
|
143
|
+
# Evaluate every key (no short-circuit) to keep the check constant-ish time.
|
|
144
|
+
ok = False
|
|
145
|
+
for k in config.keys:
|
|
146
|
+
if compare_digest(provided, k):
|
|
147
|
+
ok = True
|
|
148
|
+
return ok
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_authorized(config: AuthConfig, request: Any) -> bool:
|
|
152
|
+
"""Return True if the request carries valid credentials for ``config``.
|
|
153
|
+
|
|
154
|
+
``request`` is a Starlette/FastAPI ``Request`` (only ``.headers`` is used).
|
|
155
|
+
"""
|
|
156
|
+
if not config.enabled:
|
|
157
|
+
return True
|
|
158
|
+
if config.type == "basic":
|
|
159
|
+
return _check_basic(config, request.headers.get("authorization"))
|
|
160
|
+
if config.type == "api_key":
|
|
161
|
+
return _check_api_key(config, request.headers.get(config.header))
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def challenge_headers(config: AuthConfig) -> dict[str, str]:
|
|
166
|
+
"""Headers to return with a 401 so clients know how to authenticate."""
|
|
167
|
+
if config.type == "basic":
|
|
168
|
+
# Realm comes from trusted config; strip quotes to keep the header valid.
|
|
169
|
+
realm = config.realm.replace('"', "")
|
|
170
|
+
return {"WWW-Authenticate": f'Basic realm="{realm}"'}
|
|
171
|
+
return {}
|