mirrorwall 0.2.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.
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
mirrorwall/__init__.py ADDED
@@ -0,0 +1,116 @@
1
+ """MirrorWall — the shared UI and web-edge toolkit for the suite's three applications.
2
+
3
+ Design tokens, a layout shell, component macros, template filters, JSON and error envelopes, SSE
4
+ plumbing, request-ID, Host-validation and CSRF middleware, static mounting and health
5
+ primitives — so FreeWeight, LoadCoach and IdeaPress look and behave like one product family
6
+ without sharing a single page.
7
+
8
+ It knows nothing about benchmarks, routing or content, and a term-scan test enforces that: no
9
+ application vocabulary appears anywhere in this package, in Python, in a template, in a CSS class
10
+ name or in a JS module name.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from mirrorwall.__about__ import __version__
16
+ from mirrorwall.filters import (
17
+ EM_DASH,
18
+ STATIC_URL_PREFIX,
19
+ asset_url,
20
+ bytes_human,
21
+ duration_human,
22
+ is_supported_test,
23
+ json_pretty,
24
+ measurement,
25
+ register_filters,
26
+ timestamp,
27
+ truncate_middle,
28
+ )
29
+ from mirrorwall.health import (
30
+ ComponentHealth,
31
+ ComponentStatus,
32
+ health_payload,
33
+ worst_status,
34
+ )
35
+ from mirrorwall.middleware import (
36
+ CSRF_COOKIE_NAME,
37
+ CSRF_FIELD_NAME,
38
+ CsrfMiddleware,
39
+ HostValidationMiddleware,
40
+ RequestIdMiddleware,
41
+ issue_csrf_token,
42
+ loopback_allowlist,
43
+ )
44
+ from mirrorwall.responses import (
45
+ DEFAULT_PAGE_LIMIT,
46
+ MAX_PAGE_LIMIT,
47
+ clamp_limit,
48
+ error_body,
49
+ error_response,
50
+ json_response,
51
+ paginated_response,
52
+ )
53
+ from mirrorwall.sse import (
54
+ TOKEN_EVENT,
55
+ Event,
56
+ EventBroker,
57
+ EventSource,
58
+ Subscription,
59
+ format_frame,
60
+ parse_last_event_id,
61
+ sse_response,
62
+ )
63
+ from mirrorwall.static import HashedStaticFiles, mount_static
64
+ from mirrorwall.templating import (
65
+ DEFAULT_THEME_STORAGE_KEY,
66
+ PACKAGE_STATIC_DIR,
67
+ PACKAGE_TEMPLATE_DIR,
68
+ create_template_environment,
69
+ )
70
+
71
+ __all__ = [
72
+ "CSRF_COOKIE_NAME",
73
+ "CSRF_FIELD_NAME",
74
+ "DEFAULT_PAGE_LIMIT",
75
+ "DEFAULT_THEME_STORAGE_KEY",
76
+ "EM_DASH",
77
+ "MAX_PAGE_LIMIT",
78
+ "PACKAGE_STATIC_DIR",
79
+ "PACKAGE_TEMPLATE_DIR",
80
+ "STATIC_URL_PREFIX",
81
+ "TOKEN_EVENT",
82
+ "ComponentHealth",
83
+ "ComponentStatus",
84
+ "CsrfMiddleware",
85
+ "Event",
86
+ "EventBroker",
87
+ "EventSource",
88
+ "HashedStaticFiles",
89
+ "HostValidationMiddleware",
90
+ "RequestIdMiddleware",
91
+ "Subscription",
92
+ "__version__",
93
+ "asset_url",
94
+ "bytes_human",
95
+ "clamp_limit",
96
+ "create_template_environment",
97
+ "duration_human",
98
+ "error_body",
99
+ "error_response",
100
+ "format_frame",
101
+ "health_payload",
102
+ "is_supported_test",
103
+ "issue_csrf_token",
104
+ "json_pretty",
105
+ "json_response",
106
+ "loopback_allowlist",
107
+ "measurement",
108
+ "mount_static",
109
+ "paginated_response",
110
+ "parse_last_event_id",
111
+ "register_filters",
112
+ "sse_response",
113
+ "timestamp",
114
+ "truncate_middle",
115
+ "worst_status",
116
+ ]
mirrorwall/filters.py ADDED
@@ -0,0 +1,248 @@
1
+ """mirrorwall.filters — the template filters every application in the suite renders through.
2
+
3
+ One rule shapes most of this module. An absent measurement is an em dash, never a zero
4
+ (:doc:`ADR-0016 <../../adr/0016-unsupported-vs-zero>`, UI/UX standards §3): a machine that could
5
+ not read its GPU temperature did not read it as zero degrees, and a zero is indistinguishable
6
+ from a real reading the moment it reaches an average or a chart.
7
+
8
+ :func:`measurement` is the **only** sanctioned way a template touches a
9
+ :class:`~baseaicore.Measurement` (spec §2). ``UNSUPPORTED`` refuses ``__bool__``, so
10
+ ``{% if value %}`` on a measurement raises rather than rendering blank — correct behaviour, and
11
+ worth knowing before it happens. Templates write ``{{ value | measurement }}`` and
12
+ ``{% if value is supported %}``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import posixpath
19
+ from datetime import datetime, timedelta
20
+ from typing import Any, Final
21
+
22
+ from baseaicore import UNSUPPORTED, is_supported
23
+ from baseaicore.timeutil import to_rfc3339
24
+ from markupsafe import Markup, escape
25
+
26
+ __all__ = [
27
+ "EM_DASH",
28
+ "STATIC_URL_PREFIX",
29
+ "asset_url",
30
+ "bytes_human",
31
+ "duration_human",
32
+ "is_supported_test",
33
+ "json_pretty",
34
+ "measurement",
35
+ "register_filters",
36
+ "timestamp",
37
+ "truncate_middle",
38
+ ]
39
+
40
+ EM_DASH: Final = "—"
41
+ """What an absent value renders as, everywhere in the suite. Never ``0``, never blank."""
42
+
43
+ STATIC_URL_PREFIX: Final = "/static/mirrorwall"
44
+ """Where :func:`mount_static` serves this package's own assets from."""
45
+
46
+ _BYTE_UNITS: Final = ("KiB", "MiB", "GiB", "TiB", "PiB")
47
+
48
+
49
+ def bytes_human(value: object) -> str:
50
+ """Render a byte count at human scale.
51
+
52
+ Args:
53
+ value: A byte count, ``None``, or ``UNSUPPORTED``.
54
+
55
+ Returns:
56
+ The scaled string, or an em dash for an absent value — never ``"0 B"``, which is a real
57
+ measurement of an empty thing and must stay distinguishable from an unmeasured one.
58
+ """
59
+ if value is None or not is_supported(value) or not isinstance(value, (int, float)):
60
+ return EM_DASH
61
+ if value < 1024:
62
+ return f"{int(value)} B"
63
+ scaled = float(value)
64
+ for unit in _BYTE_UNITS:
65
+ scaled /= 1024
66
+ if scaled < 1024 or unit == _BYTE_UNITS[-1]:
67
+ return f"{scaled:.1f} {unit}"
68
+ raise AssertionError("unreachable: the final unit always returns") # pragma: no cover
69
+
70
+
71
+ def duration_human(value: object) -> str:
72
+ """Render a duration in seconds at human scale.
73
+
74
+ Args:
75
+ value: Seconds as a number, a :class:`~datetime.timedelta`, ``None``, or ``UNSUPPORTED``.
76
+
77
+ Returns:
78
+ ``"820 ms"``, ``"1.4 s"``, ``"2m 05s"`` or ``"3h 12m"``; an em dash for an absent value.
79
+ """
80
+ if isinstance(value, timedelta):
81
+ value = value.total_seconds()
82
+ if value is None or not is_supported(value) or not isinstance(value, (int, float)):
83
+ return EM_DASH
84
+ seconds = float(value)
85
+ if seconds < 1:
86
+ return f"{seconds * 1000:.0f} ms"
87
+ if seconds < 60:
88
+ return f"{seconds:.1f} s"
89
+ if seconds < 3600:
90
+ return f"{int(seconds // 60)}m {int(seconds % 60):02d}s"
91
+ return f"{int(seconds // 3600)}h {int(seconds % 3600 // 60):02d}m"
92
+
93
+
94
+ def timestamp(value: object) -> str:
95
+ """Render an instant as RFC 3339 in UTC.
96
+
97
+ Args:
98
+ value: A timezone-aware :class:`~datetime.datetime`, ``None``, or ``UNSUPPORTED``.
99
+
100
+ Returns:
101
+ The RFC 3339 string, or an em dash.
102
+ """
103
+ if not isinstance(value, datetime):
104
+ return EM_DASH
105
+ return to_rfc3339(value)
106
+
107
+
108
+ def measurement(value: object, reason: str | None = None, unit: str | None = None) -> Markup:
109
+ """Render a measurement, or an em dash carrying why it is absent.
110
+
111
+ The only sanctioned way a template touches a :class:`~baseaicore.Measurement`. An
112
+ ``UNSUPPORTED`` value renders as an em dash with the producer's own reason in a ``title``
113
+ attribute and in ``aria-label``, so the absence is visible to a reader and to a screen reader
114
+ alike, and is never a zero.
115
+
116
+ Args:
117
+ value: The measurement.
118
+ reason: Why it is unavailable, from the producer (SweatMeter's ``unavailable_reasons``,
119
+ a run's ``unsupported_reason`` column). Rendered as a tooltip.
120
+ unit: A unit suffix appended to a present value, e.g. ``"W"``.
121
+
122
+ Returns:
123
+ Escaped markup: an ``<span>`` for an absent value, the escaped number otherwise.
124
+ """
125
+ if value is None or value is UNSUPPORTED or not is_supported(value):
126
+ explanation = reason or "not measurable in this environment"
127
+ return Markup(
128
+ '<span class="muted" title="{reason}" aria-label="Unavailable: {reason}">{dash}</span>'
129
+ ).format(reason=explanation, dash=EM_DASH)
130
+ rendered = f"{value:g}" if isinstance(value, float) else str(value)
131
+ if unit:
132
+ return Markup("{value} <span class='unit'>{unit}</span>").format(value=rendered, unit=unit)
133
+ return escape(rendered)
134
+
135
+
136
+ def is_supported_test(value: object) -> bool:
137
+ """Jinja test: whether a measurement carries a real reading.
138
+
139
+ Registered as ``supported`` so a template writes ``{% if value is supported %}``. It exists
140
+ because ``{% if value %}`` on an ``UNSUPPORTED`` raises — deliberately (ADR-0016) — and a
141
+ template needs a way to ask the question that does not.
142
+
143
+ Args:
144
+ value: The measurement.
145
+
146
+ Returns:
147
+ Whether it can be rendered as a number.
148
+ """
149
+ return value is not None and is_supported(value)
150
+
151
+
152
+ def truncate_middle(value: object, length: int = 40, ellipsis: str = "…") -> str:
153
+ """Shorten a long identifier from the middle, keeping both ends readable.
154
+
155
+ A canonical model ID is distinguished by its tail as much as its head, so an ordinary
156
+ right-truncation removes the half that disambiguates it.
157
+
158
+ Args:
159
+ value: The string to shorten.
160
+ length: The maximum rendered length, including the ellipsis.
161
+ ellipsis: What replaces the removed middle.
162
+
163
+ Returns:
164
+ The original string when it already fits, otherwise head + ellipsis + tail.
165
+
166
+ Raises:
167
+ ValueError: ``length`` is too small to hold the ellipsis and one character either side.
168
+ """
169
+ text = "" if value is None else str(value)
170
+ minimum = len(ellipsis) + 2
171
+ if length < minimum:
172
+ message = (
173
+ f"length must be at least {minimum} to hold {ellipsis!r} and one character either side"
174
+ )
175
+ raise ValueError(message)
176
+ if len(text) <= length:
177
+ return text
178
+ room = length - len(ellipsis)
179
+ head = room - room // 2
180
+ return f"{text[:head]}{ellipsis}{text[len(text) - room // 2 :]}"
181
+
182
+
183
+ def json_pretty(value: object, indent: int = 2) -> str:
184
+ """Render a value as indented JSON for a ``<pre>`` block.
185
+
186
+ Args:
187
+ value: Anything JSON-serializable. Anything else is rendered with ``str`` rather than
188
+ raising, because a viewer that crashes the page it is diagnosing is useless.
189
+ indent: Indentation width.
190
+
191
+ Returns:
192
+ The JSON text. Not marked safe: the caller's autoescaping still applies, which is what
193
+ keeps a ``<script>`` inside a payload inert.
194
+ """
195
+ try:
196
+ return json.dumps(value, indent=indent, sort_keys=True, default=str)
197
+ except (TypeError, ValueError): # pragma: no cover — default=str covers all practical inputs
198
+ return str(value)
199
+
200
+
201
+ def asset_url(path: str, *, prefix: str = STATIC_URL_PREFIX) -> str:
202
+ """Return the URL this package's asset is served from.
203
+
204
+ Phase 1 emits a plain, traversal-safe path. Phase 2's :mod:`mirrorwall.static` replaces the
205
+ implementation with the content-hashed form; the template seam is this function's name and
206
+ signature, so that change is invisible to every consumer's templates.
207
+
208
+ Args:
209
+ path: A path relative to ``mirrorwall/static/``, e.g. ``"css/tokens.css"``.
210
+ prefix: The URL the static mount serves from.
211
+
212
+ Returns:
213
+ The absolute URL path.
214
+
215
+ Raises:
216
+ ValueError: ``path`` is absolute or escapes the static root — a template must not be able
217
+ to address anything outside the package's own assets.
218
+ """
219
+ cleaned = path.strip()
220
+ if cleaned.startswith("/") or "\\" in cleaned:
221
+ message = f"asset path must be relative to the static root: {path!r}"
222
+ raise ValueError(message)
223
+ normalized = posixpath.normpath(cleaned)
224
+ if normalized.startswith("..") or normalized == ".":
225
+ message = f"asset path escapes the static root: {path!r}"
226
+ raise ValueError(message)
227
+ return f"{prefix}/{normalized}"
228
+
229
+
230
+ def register_filters(filters: dict[str, Any], tests: dict[str, Any]) -> None:
231
+ """Install every shared filter and test onto a Jinja environment's registries.
232
+
233
+ Args:
234
+ filters: The environment's ``filters`` mapping.
235
+ tests: The environment's ``tests`` mapping.
236
+ """
237
+ filters.update(
238
+ {
239
+ "bytes_human": bytes_human,
240
+ "duration_human": duration_human,
241
+ "timestamp": timestamp,
242
+ "measurement": measurement,
243
+ "truncate_middle": truncate_middle,
244
+ "json_pretty": json_pretty,
245
+ "asset_url": asset_url,
246
+ }
247
+ )
248
+ tests["supported"] = is_supported_test
mirrorwall/gallery.py ADDED
@@ -0,0 +1,4 @@
1
+ """mirrorwall.gallery.
2
+
3
+ TODO: implement per docs/packages/mirrorwall/development-plan.md.
4
+ """
mirrorwall/health.py ADDED
@@ -0,0 +1,125 @@
1
+ """mirrorwall.health — the health payload primitives every application's ``/health`` is built on.
2
+
3
+ The package supplies the shape and the roll-up rule; each application supplies its own components
4
+ (a database, a provider, a queue) and decides what makes each one degraded. That split is the
5
+ point: three applications answer "are you healthy?" in the same words, about different things.
6
+
7
+ ``NOT_CONFIGURED`` is a first-class status, not a synonym for ``UNAVAILABLE``. A component nobody
8
+ asked for is not broken, and rolling it up as though it were turns every default install into a
9
+ red dashboard — the same distinction between "absent" and "bad" that ADR-0016 makes for a
10
+ measurement.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from enum import StrEnum
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from baseaicore.timeutil import to_rfc3339, utc_now
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping, Sequence
23
+ from datetime import datetime
24
+
25
+ __all__ = ["ComponentHealth", "ComponentStatus", "health_payload", "worst_status"]
26
+
27
+
28
+ class ComponentStatus(StrEnum):
29
+ """One component's verdict.
30
+
31
+ Ordered worst-last in :data:`_SEVERITY` rather than by declaration, so adding a status cannot
32
+ silently change how an existing one rolls up.
33
+ """
34
+
35
+ OK = "ok"
36
+ DEGRADED = "degraded"
37
+ UNAVAILABLE = "unavailable"
38
+ NOT_CONFIGURED = "not_configured"
39
+
40
+
41
+ _SEVERITY: dict[ComponentStatus, int] = {
42
+ ComponentStatus.NOT_CONFIGURED: 0,
43
+ ComponentStatus.OK: 1,
44
+ ComponentStatus.DEGRADED: 2,
45
+ ComponentStatus.UNAVAILABLE: 3,
46
+ }
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ComponentHealth:
51
+ """One named component's status, with whatever numbers explain it.
52
+
53
+ Attributes:
54
+ name: The component, e.g. ``"database"``. Stable across releases: a dashboard keys on it.
55
+ status: The verdict.
56
+ detail: One human-readable sentence. Free of secrets and of paths outside the data root.
57
+ data: Structured figures behind the verdict — free bytes, a latency, a queue depth.
58
+ checked_at: When the check ran. Defaults to now at payload time.
59
+ """
60
+
61
+ name: str
62
+ status: ComponentStatus
63
+ detail: str = ""
64
+ data: Mapping[str, Any] = field(default_factory=dict)
65
+ checked_at: datetime | None = None
66
+
67
+
68
+ def worst_status(components: Sequence[ComponentHealth]) -> ComponentStatus:
69
+ """Roll several components up into one overall verdict.
70
+
71
+ ``NOT_CONFIGURED`` never worsens the roll-up: a component nobody asked for is not a fault. An
72
+ empty component list is ``OK`` — an application with nothing to check has nothing wrong.
73
+
74
+ Args:
75
+ components: The components to roll up.
76
+
77
+ Returns:
78
+ The most severe status present, ``OK`` when the only statuses are ``OK`` and
79
+ ``NOT_CONFIGURED``, and ``NOT_CONFIGURED`` only when every component is.
80
+ """
81
+ if not components:
82
+ return ComponentStatus.OK
83
+ statuses = [component.status for component in components]
84
+ if all(status is ComponentStatus.NOT_CONFIGURED for status in statuses):
85
+ return ComponentStatus.NOT_CONFIGURED
86
+ return max(statuses, key=lambda status: _SEVERITY[status])
87
+
88
+
89
+ def health_payload(
90
+ *,
91
+ application: str,
92
+ version: str,
93
+ components: Sequence[ComponentHealth],
94
+ checked_at: datetime | None = None,
95
+ ) -> dict[str, Any]:
96
+ """Build the ``/health`` body.
97
+
98
+ Args:
99
+ application: The application's distribution name, lowercase.
100
+ version: Its version.
101
+ components: Every component it checked.
102
+ checked_at: The instant to record for components that did not carry their own. Injected
103
+ so a test can assert a whole payload without patching the clock.
104
+
105
+ Returns:
106
+ The body: overall status, the application's identity, and one entry per component in the
107
+ order given — never reordered, because a dashboard's rows should not move between polls.
108
+ """
109
+ now = checked_at or utc_now()
110
+ return {
111
+ "status": worst_status(components).value,
112
+ "application": application,
113
+ "version": version,
114
+ "checked_at": to_rfc3339(now),
115
+ "components": [
116
+ {
117
+ "name": component.name,
118
+ "status": component.status.value,
119
+ "detail": component.detail,
120
+ "data": dict(component.data),
121
+ "checked_at": to_rfc3339(component.checked_at or now),
122
+ }
123
+ for component in components
124
+ ],
125
+ }