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.
Files changed (150) hide show
  1. dashdown/__init__.py +16 -0
  2. dashdown/asgi.py +34 -0
  3. dashdown/auth.py +171 -0
  4. dashdown/build.py +817 -0
  5. dashdown/catalog.py +438 -0
  6. dashdown/cli.py +1054 -0
  7. dashdown/components/__init__.py +45 -0
  8. dashdown/components/base.py +99 -0
  9. dashdown/components/builtin/__init__.py +0 -0
  10. dashdown/components/builtin/_util.py +287 -0
  11. dashdown/components/builtin/ask.py +124 -0
  12. dashdown/components/builtin/auto_chart.py +24 -0
  13. dashdown/components/builtin/bar_chart.py +3 -0
  14. dashdown/components/builtin/box_plot.py +45 -0
  15. dashdown/components/builtin/calendar_heatmap.py +22 -0
  16. dashdown/components/builtin/candlestick_chart.py +58 -0
  17. dashdown/components/builtin/combo_chart.py +186 -0
  18. dashdown/components/builtin/counter.py +204 -0
  19. dashdown/components/builtin/date_range.py +168 -0
  20. dashdown/components/builtin/dropdown.py +211 -0
  21. dashdown/components/builtin/gauge_chart.py +25 -0
  22. dashdown/components/builtin/graph_chart.py +58 -0
  23. dashdown/components/builtin/grid.py +51 -0
  24. dashdown/components/builtin/heatmap_chart.py +52 -0
  25. dashdown/components/builtin/hierarchy_chart.py +67 -0
  26. dashdown/components/builtin/line_chart.py +241 -0
  27. dashdown/components/builtin/map_chart.py +30 -0
  28. dashdown/components/builtin/parallel_chart.py +57 -0
  29. dashdown/components/builtin/pivot_table.py +66 -0
  30. dashdown/components/builtin/radar_chart.py +18 -0
  31. dashdown/components/builtin/sankey_chart.py +55 -0
  32. dashdown/components/builtin/search.py +93 -0
  33. dashdown/components/builtin/site_search.py +62 -0
  34. dashdown/components/builtin/table.py +266 -0
  35. dashdown/components/builtin/theme_river.py +23 -0
  36. dashdown/components/builtin/timegrain.py +121 -0
  37. dashdown/components/builtin/toggle.py +131 -0
  38. dashdown/components/builtin/value.py +99 -0
  39. dashdown/data/__init__.py +3 -0
  40. dashdown/data/base.py +183 -0
  41. dashdown/data/bigquery_connector.py +117 -0
  42. dashdown/data/csv_connector.py +66 -0
  43. dashdown/data/cube_connector.py +290 -0
  44. dashdown/data/dax_connector.py +235 -0
  45. dashdown/data/dbapi.py +184 -0
  46. dashdown/data/duckdb_connector.py +117 -0
  47. dashdown/data/excel_connector.py +76 -0
  48. dashdown/data/introspect.py +90 -0
  49. dashdown/data/json_connector.py +75 -0
  50. dashdown/data/motherduck_connector.py +93 -0
  51. dashdown/data/mssql_connector.py +208 -0
  52. dashdown/data/mysql_connector.py +50 -0
  53. dashdown/data/parquet_connector.py +70 -0
  54. dashdown/data/postgres_connector.py +46 -0
  55. dashdown/data/quack_connector.py +151 -0
  56. dashdown/data/registry.py +37 -0
  57. dashdown/data/sheets_connector.py +88 -0
  58. dashdown/data/snowflake_connector.py +47 -0
  59. dashdown/data/tabular.py +109 -0
  60. dashdown/embed.py +216 -0
  61. dashdown/llm.py +498 -0
  62. dashdown/pdf.py +370 -0
  63. dashdown/project.py +875 -0
  64. dashdown/python_query.py +365 -0
  65. dashdown/query_composition.py +241 -0
  66. dashdown/query_library.py +118 -0
  67. dashdown/render/__init__.py +0 -0
  68. dashdown/render/attrs.py +70 -0
  69. dashdown/render/components.py +172 -0
  70. dashdown/render/icons.py +207 -0
  71. dashdown/render/markdown.py +407 -0
  72. dashdown/render/pipeline.py +974 -0
  73. dashdown/scaffold/AGENTS.md +125 -0
  74. dashdown/scaffold/claude/skills/dashdown-authoring/SKILL.md +93 -0
  75. dashdown/scaffold/references/ai.md +225 -0
  76. dashdown/scaffold/references/authentication.md +84 -0
  77. dashdown/scaffold/references/catalog.md +69 -0
  78. dashdown/scaffold/references/cli.md +305 -0
  79. dashdown/scaffold/references/components.md +1724 -0
  80. dashdown/scaffold/references/configuration.md +209 -0
  81. dashdown/scaffold/references/connectors.md +697 -0
  82. dashdown/scaffold/references/detail-pages.md +147 -0
  83. dashdown/scaffold/references/embedding.md +35 -0
  84. dashdown/scaffold/references/exporting.md +78 -0
  85. dashdown/scaffold/references/extending.md +176 -0
  86. dashdown/scaffold/references/filters.md +123 -0
  87. dashdown/scaffold/references/formatting.md +145 -0
  88. dashdown/scaffold/references/getting-started.md +118 -0
  89. dashdown/scaffold/references/pages.md +191 -0
  90. dashdown/scaffold/references/python-queries.md +123 -0
  91. dashdown/scaffold/references/queries.md +119 -0
  92. dashdown/scaffold/references/realtime.md +66 -0
  93. dashdown/scaffold/references/search.md +68 -0
  94. dashdown/scaffold/references/semantic-layer.md +492 -0
  95. dashdown/scaffold/references/telemetry.md +79 -0
  96. dashdown/scaffold/references/theming.md +180 -0
  97. dashdown/screenshot.py +210 -0
  98. dashdown/search.py +107 -0
  99. dashdown/semantic.py +890 -0
  100. dashdown/semantic_base.py +218 -0
  101. dashdown/semantic_cube.py +526 -0
  102. dashdown/server.py +1017 -0
  103. dashdown/static/app.js +235 -0
  104. dashdown/static/components/ask.js +140 -0
  105. dashdown/static/components/chart.js +1863 -0
  106. dashdown/static/components/counter.js +221 -0
  107. dashdown/static/components/daterange.js +332 -0
  108. dashdown/static/components/dropdown.js +472 -0
  109. dashdown/static/components/echarts_theme.js +322 -0
  110. dashdown/static/components/embed_frame.js +59 -0
  111. dashdown/static/components/embed_ui.js +117 -0
  112. dashdown/static/components/export.js +87 -0
  113. dashdown/static/components/export_modal.js +103 -0
  114. dashdown/static/components/filter_badge.js +158 -0
  115. dashdown/static/components/filter_bar.js +187 -0
  116. dashdown/static/components/mermaid.js +223 -0
  117. dashdown/static/components/page_header.js +76 -0
  118. dashdown/static/components/pivot.js +367 -0
  119. dashdown/static/components/print.js +370 -0
  120. dashdown/static/components/search.js +98 -0
  121. dashdown/static/components/site_search.js +255 -0
  122. dashdown/static/components/table.js +626 -0
  123. dashdown/static/components/timegrain.js +107 -0
  124. dashdown/static/components/toggle.js +114 -0
  125. dashdown/static/components/value.js +88 -0
  126. dashdown/static/core.js +719 -0
  127. dashdown/static/dashdown.css +2025 -0
  128. dashdown/static/dashdown.js +51 -0
  129. dashdown/static/embed.js +73 -0
  130. dashdown/static/favicon.svg +18 -0
  131. dashdown/static/legacy.js +257 -0
  132. dashdown/static/loading.js +55 -0
  133. dashdown/static/store.js +285 -0
  134. dashdown/static/vendor/alpine.min.js +5 -0
  135. dashdown/static/vendor/echarts.min.js +45 -0
  136. dashdown/static/vendor/fonts/inter.woff2 +0 -0
  137. dashdown/static/vendor/mermaid.min.js +3405 -0
  138. dashdown/static/vendor/tailwind.css +6 -0
  139. dashdown/static/vendor/world.json +1 -0
  140. dashdown/streaming.py +201 -0
  141. dashdown/telemetry.py +279 -0
  142. dashdown/templates/page.html +301 -0
  143. dashdown_md-0.1.0.dist-info/METADATA +280 -0
  144. dashdown_md-0.1.0.dist-info/RECORD +150 -0
  145. dashdown_md-0.1.0.dist-info/WHEEL +5 -0
  146. dashdown_md-0.1.0.dist-info/entry_points.txt +23 -0
  147. dashdown_md-0.1.0.dist-info/licenses/LICENSE +661 -0
  148. dashdown_md-0.1.0.dist-info/licenses/LICENSING.md +70 -0
  149. dashdown_md-0.1.0.dist-info/licenses/THIRD-PARTY-NOTICES.md +69 -0
  150. 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 {}