mirrorwall 0.2.0__tar.gz
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.
- mirrorwall-0.2.0/.editorconfig +23 -0
- mirrorwall-0.2.0/.github/workflows/ci.yml +136 -0
- mirrorwall-0.2.0/.github/workflows/release.yml +57 -0
- mirrorwall-0.2.0/.gitignore +100 -0
- mirrorwall-0.2.0/.importlinter +21 -0
- mirrorwall-0.2.0/.pre-commit-config.yaml +22 -0
- mirrorwall-0.2.0/CHANGELOG.md +109 -0
- mirrorwall-0.2.0/CONTRIBUTING.md +50 -0
- mirrorwall-0.2.0/LICENSE +201 -0
- mirrorwall-0.2.0/PKG-INFO +119 -0
- mirrorwall-0.2.0/README.md +86 -0
- mirrorwall-0.2.0/SECURITY.md +39 -0
- mirrorwall-0.2.0/THIRD_PARTY_NOTICES.md +39 -0
- mirrorwall-0.2.0/docs/README.md +8 -0
- mirrorwall-0.2.0/docs/packages/mirrorwall/development-plan.md +184 -0
- mirrorwall-0.2.0/docs/packages/mirrorwall/spec.md +258 -0
- mirrorwall-0.2.0/pyproject.toml +114 -0
- mirrorwall-0.2.0/requirements/README.md +64 -0
- mirrorwall-0.2.0/requirements/ci.lock +796 -0
- mirrorwall-0.2.0/requirements/release.in +6 -0
- mirrorwall-0.2.0/requirements/release.lock +493 -0
- mirrorwall-0.2.0/src/mirrorwall/__about__.py +1 -0
- mirrorwall-0.2.0/src/mirrorwall/__init__.py +116 -0
- mirrorwall-0.2.0/src/mirrorwall/filters.py +248 -0
- mirrorwall-0.2.0/src/mirrorwall/gallery.py +4 -0
- mirrorwall-0.2.0/src/mirrorwall/health.py +125 -0
- mirrorwall-0.2.0/src/mirrorwall/middleware.py +305 -0
- mirrorwall-0.2.0/src/mirrorwall/py.typed +0 -0
- mirrorwall-0.2.0/src/mirrorwall/responses.py +197 -0
- mirrorwall-0.2.0/src/mirrorwall/sse.py +474 -0
- mirrorwall-0.2.0/src/mirrorwall/static/ASSETS.sha256 +19 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/charts.css +23 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/components.css +192 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/layout.css +81 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/reset.css +36 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/tables.css +29 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/tokens.css +95 -0
- mirrorwall-0.2.0/src/mirrorwall/static/css/tokens.json +70 -0
- mirrorwall-0.2.0/src/mirrorwall/static/icons/sprite.svg +30 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/charts.js +1 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/dialog.js +26 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/drawer.js +65 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/sse.js +80 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/table.js +174 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/telemetry.js +87 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/theme.js +60 -0
- mirrorwall-0.2.0/src/mirrorwall/static/js/toast.js +34 -0
- mirrorwall-0.2.0/src/mirrorwall/static.py +193 -0
- mirrorwall-0.2.0/src/mirrorwall/templates/mirrorwall/base.html +74 -0
- mirrorwall-0.2.0/src/mirrorwall/templates/mirrorwall/components.html +198 -0
- mirrorwall-0.2.0/src/mirrorwall/templates/mirrorwall/telemetry_bar.html +34 -0
- mirrorwall-0.2.0/src/mirrorwall/templating.py +100 -0
- mirrorwall-0.2.0/tests/accessibility/test_keyboard_and_aria.py +4 -0
- mirrorwall-0.2.0/tests/conftest.py +18 -0
- mirrorwall-0.2.0/tests/contract/test_public_api.py +141 -0
- mirrorwall-0.2.0/tests/integration/test_sse.py +572 -0
- mirrorwall-0.2.0/tests/js/test_sse_client.py +236 -0
- mirrorwall-0.2.0/tests/js/test_table.py +4 -0
- mirrorwall-0.2.0/tests/js/test_telemetry.py +4 -0
- mirrorwall-0.2.0/tests/js/test_theme.py +4 -0
- mirrorwall-0.2.0/tests/performance/test_render.py +4 -0
- mirrorwall-0.2.0/tests/snapshot/test_components.py +260 -0
- mirrorwall-0.2.0/tests/test_no_application_vocabulary.py +220 -0
- mirrorwall-0.2.0/tests/unit/test_assets.py +98 -0
- mirrorwall-0.2.0/tests/unit/test_filters.py +126 -0
- mirrorwall-0.2.0/tests/unit/test_health.py +107 -0
- mirrorwall-0.2.0/tests/unit/test_middleware.py +212 -0
- mirrorwall-0.2.0/tests/unit/test_responses.py +147 -0
- mirrorwall-0.2.0/tests/unit/test_static.py +125 -0
- mirrorwall-0.2.0/tests/unit/test_templating.py +123 -0
- mirrorwall-0.2.0/tests/unit/test_tokens.py +143 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 4
|
|
10
|
+
|
|
11
|
+
[*.py]
|
|
12
|
+
indent_size = 4
|
|
13
|
+
max_line_length = 100
|
|
14
|
+
|
|
15
|
+
[*.{toml,yml,yaml,json}]
|
|
16
|
+
indent_size = 2
|
|
17
|
+
|
|
18
|
+
[*.md]
|
|
19
|
+
trim_trailing_whitespace = false
|
|
20
|
+
max_line_length = off
|
|
21
|
+
|
|
22
|
+
[Makefile]
|
|
23
|
+
indent_style = tab
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
format:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with: { python-version: "3.12" }
|
|
15
|
+
- run: pip install ruff
|
|
16
|
+
- run: ruff format --check .
|
|
17
|
+
|
|
18
|
+
lint:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with: { python-version: "3.12" }
|
|
24
|
+
- run: pip install ruff
|
|
25
|
+
- run: ruff check .
|
|
26
|
+
|
|
27
|
+
types:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with: { python-version: "3.12" }
|
|
33
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
34
|
+
- run: pip install . --no-deps
|
|
35
|
+
- run: mypy src tests
|
|
36
|
+
|
|
37
|
+
boundaries:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with: { python-version: "3.12" }
|
|
43
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
44
|
+
- run: pip install . --no-deps
|
|
45
|
+
- run: lint-imports
|
|
46
|
+
|
|
47
|
+
tests:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
strategy:
|
|
50
|
+
matrix:
|
|
51
|
+
python-version: ["3.12", "3.13"]
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: actions/setup-python@v5
|
|
55
|
+
with: { python-version: "${{ matrix.python-version }}" }
|
|
56
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
57
|
+
- run: pip install . --no-deps
|
|
58
|
+
- run: pytest -m "not live and not performance" --cov --cov-report=xml
|
|
59
|
+
|
|
60
|
+
tests-314-early-warning:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
continue-on-error: true
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with: { python-version: "3.14" }
|
|
67
|
+
# Deliberately unpinned: ci.lock is resolved on 3.13, and pinning a version that has no
|
|
68
|
+
# 3.14 wheels would defeat the purpose of an early warning.
|
|
69
|
+
- run: pip install -e ".[dev]"
|
|
70
|
+
- run: pytest -m "not live and not performance"
|
|
71
|
+
|
|
72
|
+
coverage:
|
|
73
|
+
needs: [tests]
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- uses: actions/setup-python@v5
|
|
78
|
+
with: { python-version: "3.12" }
|
|
79
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
80
|
+
- run: pip install . --no-deps
|
|
81
|
+
- run: pytest -m "not live and not performance" --cov --cov-report=term-missing --cov-fail-under=85
|
|
82
|
+
|
|
83
|
+
contracts:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: actions/setup-python@v5
|
|
88
|
+
with: { python-version: "3.12" }
|
|
89
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
90
|
+
- run: pip install . --no-deps
|
|
91
|
+
- run: pytest -m contract
|
|
92
|
+
|
|
93
|
+
security:
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v4
|
|
97
|
+
# gitleaks scans *history*, and `actions/checkout` fetches a single commit by default.
|
|
98
|
+
# For a push it is handed `<first-pushed>^..<last-pushed>`, so the parent of the first
|
|
99
|
+
# pushed commit has to be in the object store; in a depth-1 clone it is not, and git
|
|
100
|
+
# answers "unknown revision", which the action reports as exit code 1. It fails the same
|
|
101
|
+
# way whether or not a secret exists, so a green run would not have meant anything either.
|
|
102
|
+
with: { fetch-depth: 0 }
|
|
103
|
+
- uses: actions/setup-python@v5
|
|
104
|
+
with: { python-version: "3.12" }
|
|
105
|
+
- run: pip install pip-audit
|
|
106
|
+
# Audit the locked sets, not the job's own environment: a bare `pip-audit` here would
|
|
107
|
+
# inspect an environment containing only pip-audit itself (Security Standards §11).
|
|
108
|
+
- run: pip-audit --require-hashes -r requirements/ci.lock
|
|
109
|
+
- run: pip-audit --require-hashes -r requirements/release.lock
|
|
110
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
111
|
+
env:
|
|
112
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
113
|
+
|
|
114
|
+
build:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: actions/setup-python@v5
|
|
119
|
+
with: { python-version: "3.12" }
|
|
120
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
121
|
+
- run: python -m build --no-isolation
|
|
122
|
+
- run: twine check dist/*
|
|
123
|
+
- uses: actions/upload-artifact@v4
|
|
124
|
+
with: { name: dist, path: dist/ }
|
|
125
|
+
|
|
126
|
+
install-check:
|
|
127
|
+
needs: [build]
|
|
128
|
+
runs-on: ubuntu-latest
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/checkout@v4
|
|
131
|
+
- uses: actions/setup-python@v5
|
|
132
|
+
with: { python-version: "3.12" }
|
|
133
|
+
- uses: actions/download-artifact@v4
|
|
134
|
+
with: { name: dist, path: dist/ }
|
|
135
|
+
- run: pip install dist/*.whl
|
|
136
|
+
- run: python -c "import mirrorwall"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*.*.*"]
|
|
6
|
+
workflow_dispatch: # manual TestPyPI dry run; see publish-testpypi below
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # required for PyPI Trusted Publishing
|
|
10
|
+
contents: write # required to create the GitHub release
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
# Tag pushes only. Without this, clicking "Run workflow" for the TestPyPI dry run below would
|
|
15
|
+
# also fire this job and publish to real PyPI — the exact opposite of a dry run.
|
|
16
|
+
if: github.event_name == 'push'
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi # must match the Environment name set on the PyPI trusted publisher
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with: { python-version: "3.12" }
|
|
23
|
+
# Byte-for-byte the dry run's build chain below. A real release that resolved `build` and
|
|
24
|
+
# `hatchling` fresh from PyPI would not be the artifact the dry run proved.
|
|
25
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
26
|
+
- run: python -m build --no-isolation
|
|
27
|
+
- run: twine check dist/*
|
|
28
|
+
- run: pip install "dist/$(ls dist | grep .whl)[dev]"
|
|
29
|
+
- run: pytest -m "not live and not performance"
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
32
|
+
- name: Create GitHub release
|
|
33
|
+
uses: softprops/action-gh-release@v2
|
|
34
|
+
with:
|
|
35
|
+
generate_release_notes: true
|
|
36
|
+
files: dist/*
|
|
37
|
+
|
|
38
|
+
publish-testpypi:
|
|
39
|
+
# Manual only, via Actions -> Release -> Run workflow. Packaging and Release Standards §6
|
|
40
|
+
# requires a successful TestPyPI publish ahead of a package's first real release; 0.2.0 is
|
|
41
|
+
# this package's first published version, so run this once before tagging v0.2.0. Later
|
|
42
|
+
# releases may skip it, or use it again as a dry run.
|
|
43
|
+
if: github.event_name == 'workflow_dispatch'
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with: { python-version: "3.12" }
|
|
49
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
50
|
+
- run: python -m build --no-isolation
|
|
51
|
+
- run: twine check dist/*
|
|
52
|
+
- run: pip install "dist/$(ls dist | grep .whl)[dev]"
|
|
53
|
+
- run: pytest -m "not live and not performance"
|
|
54
|
+
- name: Publish to TestPyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ---- Python ----
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# ---- Packaging / build backends ----
|
|
25
|
+
pip-wheel-metadata/
|
|
26
|
+
share/python-wheels/
|
|
27
|
+
|
|
28
|
+
# ---- Testing / coverage ----
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.cache
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
*.py,cover
|
|
36
|
+
htmlcov/
|
|
37
|
+
nosetests.xml
|
|
38
|
+
.hypothesis/
|
|
39
|
+
|
|
40
|
+
# ---- Type checking / linting ----
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.dmypy.json
|
|
43
|
+
dmypy.json
|
|
44
|
+
.ruff_cache/
|
|
45
|
+
.pytype/
|
|
46
|
+
|
|
47
|
+
# ---- Virtual environments ----
|
|
48
|
+
.venv/
|
|
49
|
+
venv/
|
|
50
|
+
ENV/
|
|
51
|
+
env/
|
|
52
|
+
env.bak/
|
|
53
|
+
venv.bak/
|
|
54
|
+
.python-version
|
|
55
|
+
|
|
56
|
+
# ---- Distribution / dependency locking ----
|
|
57
|
+
# requirements/*.lock is deliberately NOT ignored. The lock files are committed
|
|
58
|
+
# inputs: CI and release jobs install from them with --require-hashes (Packaging
|
|
59
|
+
# and Release Standards §4, Security Standards §11), so an ignored lock file
|
|
60
|
+
# means a checkout that cannot build.
|
|
61
|
+
|
|
62
|
+
# ---- Editors / OS ----
|
|
63
|
+
.vscode/
|
|
64
|
+
.idea/
|
|
65
|
+
*.swp
|
|
66
|
+
*.swo
|
|
67
|
+
.DS_Store
|
|
68
|
+
Thumbs.db
|
|
69
|
+
|
|
70
|
+
# ---- Suite runtime state (this component's local data) ----
|
|
71
|
+
# The application writes to XDG paths at runtime (~/.config, ~/.local/share,
|
|
72
|
+
# ~/.local/state per Master Architecture §1.2), never inside the repository.
|
|
73
|
+
# These entries only guard against a developer pointing XDG_* at the repo
|
|
74
|
+
# during local testing.
|
|
75
|
+
.local/
|
|
76
|
+
.config/
|
|
77
|
+
*.sqlite3
|
|
78
|
+
*.sqlite3-journal
|
|
79
|
+
*.sqlite3-wal
|
|
80
|
+
*.sqlite3-shm
|
|
81
|
+
/data/
|
|
82
|
+
/logs/
|
|
83
|
+
/backups/
|
|
84
|
+
/artifacts/
|
|
85
|
+
/exports/
|
|
86
|
+
|
|
87
|
+
# ---- Secrets ----
|
|
88
|
+
# Per Security Standards §8: secrets are never committed. Config files may
|
|
89
|
+
# only name where a secret comes from (*_env / *_file), never the value.
|
|
90
|
+
.env
|
|
91
|
+
.env.*
|
|
92
|
+
*.key
|
|
93
|
+
*.pem
|
|
94
|
+
secrets.toml
|
|
95
|
+
|
|
96
|
+
# ---- Node-free JS assets (MirrorWall consumers may still use a local tool) ----
|
|
97
|
+
node_modules/
|
|
98
|
+
|
|
99
|
+
# ---- Build artifacts from docs generation ----
|
|
100
|
+
docs/api/openapi-v1.json.tmp
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[importlinter]
|
|
2
|
+
root_package = mirrorwall
|
|
3
|
+
include_external_packages = True
|
|
4
|
+
|
|
5
|
+
[importlinter:contract:no-application-imports]
|
|
6
|
+
name = MirrorWall must not import applications
|
|
7
|
+
type = forbidden
|
|
8
|
+
source_modules = mirrorwall
|
|
9
|
+
forbidden_modules =
|
|
10
|
+
freeweight
|
|
11
|
+
loadcoach
|
|
12
|
+
ideapress
|
|
13
|
+
|
|
14
|
+
[importlinter:contract:no-sibling-packages]
|
|
15
|
+
name = MirrorWall must not import sibling capability packages
|
|
16
|
+
type = forbidden
|
|
17
|
+
source_modules = mirrorwall
|
|
18
|
+
forbidden_modules =
|
|
19
|
+
modelrack
|
|
20
|
+
sweatmeter
|
|
21
|
+
weightsdb
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.6.9
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
9
|
+
rev: v4.6.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: trailing-whitespace
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
- id: check-toml
|
|
14
|
+
- id: check-json
|
|
15
|
+
- id: check-added-large-files
|
|
16
|
+
- id: check-merge-conflict
|
|
17
|
+
- id: mixed-line-ending
|
|
18
|
+
args: [--fix=lf]
|
|
19
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
20
|
+
rev: v8.18.4
|
|
21
|
+
hooks:
|
|
22
|
+
- id: gitleaks
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mirrorwall` are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/), pre-1.0 per
|
|
6
|
+
packaging and release standards §3.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] — 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **`starlette>=1.3.1,<2`**, widened from `>=0.37,<1`. Two reasons, either sufficient. The 0.x line
|
|
14
|
+
carries PYSEC-2026-161, -248, -249, -2280 and -2281, none of which has a fix below 1.3.1, so
|
|
15
|
+
`pip-audit` fails the security gate against any lock the old cap allows. And the cap was wrong on
|
|
16
|
+
its own terms: FreeWeight and LoadCoach — the two applications this package exists to serve —
|
|
17
|
+
already run starlette 1.6 under fastapi 0.141, so `<1` made `mirrorwall` un-coinstallable with
|
|
18
|
+
its own consumers. The full suite passes unchanged on 1.6.0; no source change was needed.
|
|
19
|
+
- CI installs from committed, hash-verified lockfiles (`requirements/ci.lock`,
|
|
20
|
+
`requirements/release.lock`) rather than an editable checkout, per Packaging Standards §4;
|
|
21
|
+
`pip-audit` audits those locks instead of an empty environment; `release.yml` gains the `pypi`
|
|
22
|
+
deployment environment, the manual TestPyPI dry run required before a first release, and a build
|
|
23
|
+
chain pinned byte-for-byte to the one the dry run proves. `[tool.coverage.run] source` now names
|
|
24
|
+
the importable package rather than `src/mirrorwall`, because a non-editable install reports 0 %
|
|
25
|
+
against a path-based source.
|
|
26
|
+
|
|
27
|
+
The `dev` extra moves to `pytest>=9.0.3,<10`, matching BaseAiCore, SetSpec, ModelRack and
|
|
28
|
+
SweatMeter: PYSEC-2026-1845 affects pytest through 9.0.2 and failed the security job.
|
|
29
|
+
|
|
30
|
+
`ci.lock` could not be compiled at all until `setspec 0.4.0` was published: this package requires
|
|
31
|
+
`setspec>=0.4,<0.5`, PyPI carried 0.3.0, and every CI job that installed from an index failed at
|
|
32
|
+
the `pip install` step for that reason. Those jobs had never been green.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- `tests/contract/test_public_api.py`: the published surface asserted in both directions, every
|
|
36
|
+
name resolved, `py.typed` shipped, and the templates, stylesheets, icon sprite and
|
|
37
|
+
`PACKAGE_TEMPLATE_DIR`/`PACKAGE_STATIC_DIR` roots asserted against the *installed* package. A
|
|
38
|
+
wheel that drops package data imports perfectly and then fails at a consumer's first render, so
|
|
39
|
+
the check has to run against the install rather than the checkout. The `contracts` CI job
|
|
40
|
+
collected nothing before this and failed with pytest's exit code 5.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Phase 1: design tokens, the layout shell and the core components, extracted from FreeWeight's
|
|
44
|
+
shipped UI and generalized until nothing in the package knows what an application is.
|
|
45
|
+
- `static/css/{tokens,reset,layout,components,tables,charts}.css` plus `tokens.json`. The
|
|
46
|
+
palette is byte-identical to the one FreeWeight proved in production; `--mw-accent` and its
|
|
47
|
+
three relatives are the per-application override point, and nothing else needs to change.
|
|
48
|
+
- `templates/mirrorwall/{base,components,telemetry_bar}.html`: a shell of slots — header,
|
|
49
|
+
navigation, telemetry bar, content, footer, theme bootstrap — and macros for button, input,
|
|
50
|
+
select, checkbox, switch, card, table, badge, tabs, drawer, dialog, toast, tooltip, progress,
|
|
51
|
+
empty state, pagination, filter bar, key-value list, JSON viewer and chart container.
|
|
52
|
+
- `static/js/{theme,table,drawer,dialog,toast}.js`: progressive enhancement only. Every page is
|
|
53
|
+
complete before any of them loads (ADR-0020).
|
|
54
|
+
- `static/icons/sprite.svg`: sixteen original glyphs, stroked with `currentColor` so they
|
|
55
|
+
re-theme with the page.
|
|
56
|
+
- `templating.py`: `create_template_environment` — autoescaping on, `StrictUndefined`, the
|
|
57
|
+
package's macros on the search path behind the application's own.
|
|
58
|
+
- `filters.py`: `bytes_human`, `duration_human`, `timestamp`, `measurement`, `truncate_middle`,
|
|
59
|
+
`json_pretty`, `asset_url`, and the `supported` test. `measurement` renders an em dash with
|
|
60
|
+
the producer's own reason for an absent reading — never a zero (ADR-0016), and it is the only
|
|
61
|
+
sanctioned way a template touches a `Measurement`, because `UNSUPPORTED` refuses `__bool__`.
|
|
62
|
+
- `THIRD_PARTY_NOTICES.md` and `static/ASSETS.sha256`: the vendoring discipline, with every
|
|
63
|
+
shipped asset's digest recomputed and checked by the test suite.
|
|
64
|
+
- Tests: contrast computed from the tokens (4.5:1 text, 3:1 UI, both themes, 21 pairs each);
|
|
65
|
+
a term scan over names and markup in every shipped file; component snapshots asserting the
|
|
66
|
+
ARIA each pattern requires; escaping; `StrictUndefined`; package data present in a wheel that
|
|
67
|
+
the test actually builds.
|
|
68
|
+
|
|
69
|
+
- Phase 2: the backend helpers — envelopes, request IDs, SSE, static mounting, health.
|
|
70
|
+
- `sse.py`: `Event`, `Subscription`, `EventBroker`, the `EventSource` protocol and
|
|
71
|
+
`sse_response`. The stream **subscribes before it replays** and drops from the live queue
|
|
72
|
+
anything the replay already emitted, which is what makes a reconnect gap-free and
|
|
73
|
+
duplicate-free at once. Every call into the synchronous, database-backed source — opening the
|
|
74
|
+
subscription, each bounded replay batch, closing it — goes through `anyio.to_thread.run_sync`
|
|
75
|
+
(ADR-0003 §6-8); the steady-state stream is served from the in-memory fan-out and takes no
|
|
76
|
+
threadpool slot at all, which is what makes 200 concurrent subscribers affordable against a
|
|
77
|
+
40-thread pool. Subscriber queues are bounded and drop the oldest, counting what they dropped.
|
|
78
|
+
Every frame carries the SetSpec event envelope except `event: token`, which is bare — the one
|
|
79
|
+
documented exception (ADR-0025 §3).
|
|
80
|
+
- `middleware.py`: `RequestIdMiddleware` (validate or generate, bind to the logging context,
|
|
81
|
+
echo `X-Request-ID`, add `X-Response-Time-Ms`), `HostValidationMiddleware` and
|
|
82
|
+
`CsrfMiddleware` (ADR-0026 §1-2), both running before routing and before authentication.
|
|
83
|
+
- `responses.py`: `json_response`, `error_response`, `paginated_response`, `clamp_limit`.
|
|
84
|
+
- `static.py`: `mount_static` and a content-hashing `asset_url` that replaces the Phase 1
|
|
85
|
+
filter at the same template seam, with immutable cache headers, traversal **and** symlink
|
|
86
|
+
containment.
|
|
87
|
+
- `health.py`: `ComponentStatus`, `ComponentHealth`, `health_payload`, `worst_status`.
|
|
88
|
+
`NOT_CONFIGURED` never worsens a roll-up: a component nobody asked for is not a fault.
|
|
89
|
+
- `static/js/{sse,telemetry}.js`: the client half. Events are applied idempotently by sequence,
|
|
90
|
+
so a reconnect's redelivered boundary event does not duplicate a row; an absent telemetry
|
|
91
|
+
reading renders an em dash with its reason, never `0`.
|
|
92
|
+
- `sse_response(..., terminal_events=...)`: a finite stream — a generation, a benchmark run —
|
|
93
|
+
closes after its own last event instead of holding a connection open for a producer that has
|
|
94
|
+
nothing left to say. Empty by default, because an open-ended stream has no such event.
|
|
95
|
+
- Every SSE property is proved by mutation: replay-before-subscribe, a missing dedupe, a
|
|
96
|
+
blocking `replay`, an unbounded queue and a missing cleanup were each applied to the module
|
|
97
|
+
and each confirmed to fail the corresponding test before it was considered finished.
|
|
98
|
+
|
|
99
|
+
### Changed
|
|
100
|
+
- `.importlinter` used the plural `root_packages` with a bare string, which import-linter 2.x
|
|
101
|
+
iterates character by character; and it lacked `include_external_packages`, required whenever a
|
|
102
|
+
`forbidden` contract names modules outside the root package. Both fixed. `[tool.mypy]` gained
|
|
103
|
+
`mypy_path`/`explicit_package_bases`, and the coverage floor moved from 85% to the 95% shared
|
|
104
|
+
packages carry. (The same four defects were found and fixed in two other scaffolds this
|
|
105
|
+
milestone; a scaffold from the same generator should be checked for them before its first gate.)
|
|
106
|
+
- Declared `anyio>=4,<5` directly. Phase 2's `sse_response` dispatches every call into the
|
|
107
|
+
synchronous `EventSource` with `anyio.to_thread.run_sync` (ADR-0003 §6-8); Starlette pulls anyio
|
|
108
|
+
in transitively, but a module this package imports directly is declared directly.
|
|
109
|
+
- Repository scaffold generated from the suite's development plan (no functional code yet).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing to MirrorWall
|
|
2
|
+
|
|
3
|
+
This repository is one component of the Local AI Suite. Before changing anything, read
|
|
4
|
+
`docs/packages/mirrorwall/spec.md` and the current
|
|
5
|
+
phase in `development-plan.md` — both are in this repository's `docs/` folder, copied from the suite's
|
|
6
|
+
central documentation set so this repository can be worked on independently.
|
|
7
|
+
|
|
8
|
+
## Development setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m venv .venv
|
|
12
|
+
source .venv/bin/activate
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
pre-commit install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Required reading, in order
|
|
18
|
+
|
|
19
|
+
1. This component's spec — purpose, scope, non-goals, contracts.
|
|
20
|
+
2. `development-plan.md` in the same folder — the phase you are implementing, its acceptance criteria and its tests.
|
|
21
|
+
|
|
22
|
+
## Rules that apply to every change here
|
|
23
|
+
|
|
24
|
+
* Follow the architecture's dependency direction.
|
|
25
|
+
This repository's `.importlinter` enforces it in CI; do not weaken that file to make an import work.
|
|
26
|
+
* No business logic in a route handler or CLI command body — both call one service method and render
|
|
27
|
+
.
|
|
28
|
+
* An unavailable measurement is `Unsupported`, never zero, never `None` used as a substitute
|
|
29
|
+
.
|
|
30
|
+
* Prompts are versioned JSON records, not Python string literals.
|
|
31
|
+
* Every phase's acceptance criteria in `development-plan.md` must be demonstrable, not merely
|
|
32
|
+
test-covered — the plan states what to run and what a person should see.
|
|
33
|
+
|
|
34
|
+
## Before opening a pull request
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
ruff format --check .
|
|
38
|
+
ruff check .
|
|
39
|
+
mypy src tests
|
|
40
|
+
lint-imports
|
|
41
|
+
pytest -m "not live and not performance"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All of the above run in CI (`.github/workflows/ci.yml`); a red CI run blocks merge.
|
|
45
|
+
|
|
46
|
+
## Commit style
|
|
47
|
+
|
|
48
|
+
Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`, `build:`,
|
|
49
|
+
`ci:`), with `!` or a `BREAKING CHANGE:` footer for breaking changes. Update `CHANGELOG.md` under
|
|
50
|
+
`## [Unreleased]` for any user-visible change.
|