pagebar 0.1.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.
- pagebar-0.1.0/.builds/alpine.yml +26 -0
- pagebar-0.1.0/.builds/ubuntu-2404.yml +37 -0
- pagebar-0.1.0/.github/workflows/ci.yml +35 -0
- pagebar-0.1.0/.gitignore +16 -0
- pagebar-0.1.0/.pre-commit-config.yaml +33 -0
- pagebar-0.1.0/CHANGES.md +12 -0
- pagebar-0.1.0/LICENSE +21 -0
- pagebar-0.1.0/Makefile +35 -0
- pagebar-0.1.0/PKG-INFO +156 -0
- pagebar-0.1.0/README.md +134 -0
- pagebar-0.1.0/demo/README.md +40 -0
- pagebar-0.1.0/demo/app.py +108 -0
- pagebar-0.1.0/local-notes/SPEC.md +168 -0
- pagebar-0.1.0/local-notes/playbooks/python/4-rules-design.md +159 -0
- pagebar-0.1.0/local-notes/playbooks/python/CHECKLISTS.md +171 -0
- pagebar-0.1.0/local-notes/playbooks/python/INDEX.md +72 -0
- pagebar-0.1.0/local-notes/playbooks/python/coding-guidelines.md +301 -0
- pagebar-0.1.0/local-notes/playbooks/python/design-patterns.md +101 -0
- pagebar-0.1.0/local-notes/playbooks/python/error-messages.md +197 -0
- pagebar-0.1.0/local-notes/playbooks/python/monorepo.md +513 -0
- pagebar-0.1.0/local-notes/playbooks/python/nouns-and-verbs.md +125 -0
- pagebar-0.1.0/local-notes/playbooks/python/testing.md +434 -0
- pagebar-0.1.0/noxfile.py +38 -0
- pagebar-0.1.0/pyproject.toml +58 -0
- pagebar-0.1.0/ruff.toml +53 -0
- pagebar-0.1.0/src/pagebar/__init__.py +10 -0
- pagebar-0.1.0/src/pagebar/_core.py +392 -0
- pagebar-0.1.0/tests/a_unit/__init__.py +0 -0
- pagebar-0.1.0/tests/b_integration/__init__.py +0 -0
- pagebar-0.1.0/tests/c_e2e/__init__.py +0 -0
- pagebar-0.1.0/tests/conftest.py +42 -0
- pagebar-0.1.0/tests/test_pagebar.py +493 -0
- pagebar-0.1.0/uv.lock +1352 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
image: alpine/latest
|
|
2
|
+
packages:
|
|
3
|
+
- uv
|
|
4
|
+
- gcc
|
|
5
|
+
- python3-dev
|
|
6
|
+
|
|
7
|
+
secrets:
|
|
8
|
+
- b88a4c2c-944c-42a6-9fbb-7483049c458a
|
|
9
|
+
|
|
10
|
+
sources:
|
|
11
|
+
- git@git.sr.ht:~sfermigier/pagebar
|
|
12
|
+
|
|
13
|
+
tasks:
|
|
14
|
+
- install: |
|
|
15
|
+
cd pagebar
|
|
16
|
+
uv sync
|
|
17
|
+
- test: |
|
|
18
|
+
cd pagebar
|
|
19
|
+
uv run pytest -q --tb=short
|
|
20
|
+
- lint: |
|
|
21
|
+
cd pagebar
|
|
22
|
+
uv run ruff check
|
|
23
|
+
uv run ty check src
|
|
24
|
+
- nox: |
|
|
25
|
+
cd pagebar
|
|
26
|
+
uvx nox
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2024, Abilian SAS
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
|
|
5
|
+
image: ubuntu/24.04
|
|
6
|
+
|
|
7
|
+
secrets:
|
|
8
|
+
- b88a4c2c-944c-42a6-9fbb-7483049c458a
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
packages:
|
|
12
|
+
# Build tools
|
|
13
|
+
- software-properties-common
|
|
14
|
+
- build-essential
|
|
15
|
+
# Python is 3.12
|
|
16
|
+
- python3-dev
|
|
17
|
+
- python3-pip
|
|
18
|
+
# Extra
|
|
19
|
+
- nodejs
|
|
20
|
+
|
|
21
|
+
tasks:
|
|
22
|
+
- setup: |
|
|
23
|
+
echo "Building on Ubuntu 2404"
|
|
24
|
+
sudo pip install --break-system-packages -U uv nox
|
|
25
|
+
cd pagebar
|
|
26
|
+
uv sync
|
|
27
|
+
|
|
28
|
+
- test: |
|
|
29
|
+
cd pagebar
|
|
30
|
+
uv run pytest -q --tb=short
|
|
31
|
+
- lint: |
|
|
32
|
+
cd pagebar
|
|
33
|
+
uv run ruff check
|
|
34
|
+
# uv run ty check src
|
|
35
|
+
- nox: |
|
|
36
|
+
cd pagebar
|
|
37
|
+
uvx nox
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI (nox)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- name: Set up Python
|
|
13
|
+
uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Install uv and nox
|
|
17
|
+
run: pip install uv nox
|
|
18
|
+
- name: Run checks (lint, format, typecheck)
|
|
19
|
+
run: nox -s check -P 3.12
|
|
20
|
+
|
|
21
|
+
tests:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
matrix:
|
|
25
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
29
|
+
uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
- name: Install uv and nox
|
|
33
|
+
run: pip install uv nox
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: nox -s test -p ${{ matrix.python-version }}
|
pagebar-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
|
|
3
|
+
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
|
4
|
+
rev: 'v0.14.11'
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
10
|
+
rev: v6.0.0
|
|
11
|
+
hooks:
|
|
12
|
+
# Generic
|
|
13
|
+
- id: check-added-large-files
|
|
14
|
+
- id: fix-byte-order-marker
|
|
15
|
+
- id: check-case-conflict
|
|
16
|
+
- id: check-executables-have-shebangs
|
|
17
|
+
- id: check-merge-conflict
|
|
18
|
+
- id: check-symlinks
|
|
19
|
+
# Basic syntax checks
|
|
20
|
+
- id: check-ast
|
|
21
|
+
- id: check-json
|
|
22
|
+
- id: check-toml
|
|
23
|
+
- id: check-xml
|
|
24
|
+
- id: check-yaml
|
|
25
|
+
# Security
|
|
26
|
+
- id: detect-private-key
|
|
27
|
+
# Whitespace
|
|
28
|
+
- id: end-of-file-fixer
|
|
29
|
+
- id: mixed-line-ending
|
|
30
|
+
- id: trailing-whitespace
|
|
31
|
+
# Misc Python
|
|
32
|
+
- id: debug-statements
|
|
33
|
+
- id: check-docstring-first
|
pagebar-0.1.0/CHANGES.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
4
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [VERSION] - DATE
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
### Documentation
|
pagebar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stéfane Fermigier / Abilian SAS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pagebar-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.PHONY: all test build format check lint clean
|
|
2
|
+
|
|
3
|
+
all: test lint
|
|
4
|
+
|
|
5
|
+
check: lint
|
|
6
|
+
|
|
7
|
+
lint:
|
|
8
|
+
uv run --active ruff check
|
|
9
|
+
uv run --active ruff format --check
|
|
10
|
+
uv run --active ty check src
|
|
11
|
+
uv run --active pyrefly check src
|
|
12
|
+
uv run --active mypy src
|
|
13
|
+
# uv run --active mypy --strict src
|
|
14
|
+
|
|
15
|
+
format:
|
|
16
|
+
uv run --active ruff format src tests
|
|
17
|
+
uv run --active ruff check src tests --fix
|
|
18
|
+
uv run --active ruff format src tests
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
uv run pytest
|
|
22
|
+
|
|
23
|
+
test-cov:
|
|
24
|
+
uv run pytest --cov=pagebar --cov-report=html --cov-report=term tests
|
|
25
|
+
|
|
26
|
+
clean:
|
|
27
|
+
rm -rf .pytest_cache .ruff_cache dist build __pycache__ .mypy_cache \
|
|
28
|
+
.coverage htmlcov .coverage.* *.egg-info
|
|
29
|
+
adt clean
|
|
30
|
+
|
|
31
|
+
build: clean
|
|
32
|
+
uv build
|
|
33
|
+
|
|
34
|
+
publish: build
|
|
35
|
+
uv publish
|
pagebar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pagebar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tiny prod-safe perf footer & toolbar for ASGI apps.
|
|
5
|
+
Author-email: Stéfane Fermigier <sf@fermigier.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: asgi,footer,monitoring,performance,sql
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Provides-Extra: sqlalchemy
|
|
20
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pagebar
|
|
24
|
+
|
|
25
|
+
A tiny prod-safe perf footer for ASGI apps. One line at the bottom of every page, click to expand. No SQL text, no env vars, no stack traces — nothing that could leak.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
v2026.6.19 · 22ms · 4 SQL · GET /editeurs · 200
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install pagebar # core only
|
|
35
|
+
pip install pagebar[sqlalchemy] # with SQL query counter
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
|
|
40
|
+
### Starlette / FastAPI
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pagebar import PagebarMiddleware, pagebar_html
|
|
44
|
+
from starlette.applications import Starlette
|
|
45
|
+
from starlette.middleware import Middleware
|
|
46
|
+
|
|
47
|
+
app = Starlette(
|
|
48
|
+
middleware=[Middleware(PagebarMiddleware, package="my-app")],
|
|
49
|
+
routes=[...],
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Litestar (and other `mw_cls(app)`-style frameworks)
|
|
54
|
+
|
|
55
|
+
Litestar instantiates middleware as `cls(app)` with no further kwargs, so config has to be baked in. Use `PagebarMiddleware.bound(...)`:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from litestar import Litestar
|
|
59
|
+
from pagebar import PagebarMiddleware
|
|
60
|
+
|
|
61
|
+
app = Litestar(
|
|
62
|
+
middleware=[
|
|
63
|
+
PagebarMiddleware.bound(package="my-app", unsafe=False),
|
|
64
|
+
],
|
|
65
|
+
route_handlers=[...],
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`.bound(**kwargs)` returns a thin subclass with the kwargs baked into `__init__`. Same fields as the regular constructor.
|
|
70
|
+
|
|
71
|
+
### Template
|
|
72
|
+
|
|
73
|
+
In your base template, anywhere inside `<body>`:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
{{ pagebar_html() | safe }}
|
|
77
|
+
</body>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That's it. One name for the middleware, one for the helper.
|
|
81
|
+
|
|
82
|
+
## What's on screen
|
|
83
|
+
|
|
84
|
+
| Field | Source |
|
|
85
|
+
|---------|-------------------------------------------------------------------|
|
|
86
|
+
| Version | `importlib.metadata.version(package)` — or explicit `version=` |
|
|
87
|
+
| Time | `time.perf_counter()` delta |
|
|
88
|
+
| SQL | SQLAlchemy `before_cursor_execute` listener (if installed) |
|
|
89
|
+
| Request | method + path |
|
|
90
|
+
| Memory | `resource.getrusage().ru_maxrss` — RSS in MiB |
|
|
91
|
+
| Uptime | `time.monotonic()` since worker import |
|
|
92
|
+
| Threads | `threading.active_count()` |
|
|
93
|
+
| GC | `gc.get_count()` — gen0/gen1/gen2 pending |
|
|
94
|
+
| Python | `sys.version_info` |
|
|
95
|
+
| PID | `os.getpid()` |
|
|
96
|
+
|
|
97
|
+
No SQL text. No params. No env. No traces. That's the whole surface.
|
|
98
|
+
|
|
99
|
+
## Knobs
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
PagebarMiddleware(
|
|
103
|
+
app,
|
|
104
|
+
package="my-app", # distribution name (PyPI / pyproject.toml)
|
|
105
|
+
version="", # escape hatch: bypass importlib.metadata lookup
|
|
106
|
+
enabled=True, # bool or callable(scope) -> bool
|
|
107
|
+
unsafe=False, # bool or callable(scope) -> bool — see below
|
|
108
|
+
query_budget=20, # >0 → log a WARNING when SQL count exceeds this
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Unsafe mode
|
|
113
|
+
|
|
114
|
+
In dev, you usually *want* to see the SQL text. Flip `unsafe=True` and the bar adds, per request: each statement (truncated), bound parameters, and per-statement timing. A red `UNSAFE` badge in the pill makes the mode obvious.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import os
|
|
118
|
+
PagebarMiddleware(app, package="my-app", unsafe=bool(os.getenv("DEBUG")))
|
|
119
|
+
# or framework-driven
|
|
120
|
+
PagebarMiddleware(app, package="my-app", unsafe=app.debug)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`unsafe` defaults to `False` and **only** takes its value from constructor wiring — no URL parameter, no header, no cookie. The host's deploy config is the authority.
|
|
124
|
+
|
|
125
|
+
`enabled` lets you hide the bar from JSON endpoints, healthchecks, or admin routes:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
def show(scope):
|
|
129
|
+
return not scope["path"].startswith(("/api/", "/health"))
|
|
130
|
+
|
|
131
|
+
Middleware(PagebarMiddleware, package="my-app", enabled=show)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Bots are skipped automatically (`User-Agent` matching `bot|crawler|spider|googlebot|bingbot`).
|
|
135
|
+
|
|
136
|
+
## CSP
|
|
137
|
+
|
|
138
|
+
`pagebar_html()` accepts a `nonce` keyword that's applied to the inline `<style>` and `<script>`:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
{{ pagebar_html(nonce=csp_nonce) | safe }}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Otherwise the host needs `'unsafe-inline'` for both directives. No external assets, no third-party requests.
|
|
145
|
+
|
|
146
|
+
## Why not the Django/Flask/Litestar debug toolbars?
|
|
147
|
+
|
|
148
|
+
They reveal SQL text, environment, settings, request bodies, stack traces — fine in dev, unacceptable in production. pagebar surfaces a fixed, public-safe set of fields. The shape is the security model.
|
|
149
|
+
|
|
150
|
+
## Non-goals
|
|
151
|
+
|
|
152
|
+
No panels system, no plugins, no per-framework adapters, no APM, no time series, no SQL EXPLAIN, no profiler. Pure ASGI middleware + one helper function in one file.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT.
|
pagebar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# pagebar
|
|
2
|
+
|
|
3
|
+
A tiny prod-safe perf footer for ASGI apps. One line at the bottom of every page, click to expand. No SQL text, no env vars, no stack traces — nothing that could leak.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
v2026.6.19 · 22ms · 4 SQL · GET /editeurs · 200
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install pagebar # core only
|
|
13
|
+
pip install pagebar[sqlalchemy] # with SQL query counter
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
### Starlette / FastAPI
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pagebar import PagebarMiddleware, pagebar_html
|
|
22
|
+
from starlette.applications import Starlette
|
|
23
|
+
from starlette.middleware import Middleware
|
|
24
|
+
|
|
25
|
+
app = Starlette(
|
|
26
|
+
middleware=[Middleware(PagebarMiddleware, package="my-app")],
|
|
27
|
+
routes=[...],
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Litestar (and other `mw_cls(app)`-style frameworks)
|
|
32
|
+
|
|
33
|
+
Litestar instantiates middleware as `cls(app)` with no further kwargs, so config has to be baked in. Use `PagebarMiddleware.bound(...)`:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from litestar import Litestar
|
|
37
|
+
from pagebar import PagebarMiddleware
|
|
38
|
+
|
|
39
|
+
app = Litestar(
|
|
40
|
+
middleware=[
|
|
41
|
+
PagebarMiddleware.bound(package="my-app", unsafe=False),
|
|
42
|
+
],
|
|
43
|
+
route_handlers=[...],
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`.bound(**kwargs)` returns a thin subclass with the kwargs baked into `__init__`. Same fields as the regular constructor.
|
|
48
|
+
|
|
49
|
+
### Template
|
|
50
|
+
|
|
51
|
+
In your base template, anywhere inside `<body>`:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
{{ pagebar_html() | safe }}
|
|
55
|
+
</body>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That's it. One name for the middleware, one for the helper.
|
|
59
|
+
|
|
60
|
+
## What's on screen
|
|
61
|
+
|
|
62
|
+
| Field | Source |
|
|
63
|
+
|---------|-------------------------------------------------------------------|
|
|
64
|
+
| Version | `importlib.metadata.version(package)` — or explicit `version=` |
|
|
65
|
+
| Time | `time.perf_counter()` delta |
|
|
66
|
+
| SQL | SQLAlchemy `before_cursor_execute` listener (if installed) |
|
|
67
|
+
| Request | method + path |
|
|
68
|
+
| Memory | `resource.getrusage().ru_maxrss` — RSS in MiB |
|
|
69
|
+
| Uptime | `time.monotonic()` since worker import |
|
|
70
|
+
| Threads | `threading.active_count()` |
|
|
71
|
+
| GC | `gc.get_count()` — gen0/gen1/gen2 pending |
|
|
72
|
+
| Python | `sys.version_info` |
|
|
73
|
+
| PID | `os.getpid()` |
|
|
74
|
+
|
|
75
|
+
No SQL text. No params. No env. No traces. That's the whole surface.
|
|
76
|
+
|
|
77
|
+
## Knobs
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
PagebarMiddleware(
|
|
81
|
+
app,
|
|
82
|
+
package="my-app", # distribution name (PyPI / pyproject.toml)
|
|
83
|
+
version="", # escape hatch: bypass importlib.metadata lookup
|
|
84
|
+
enabled=True, # bool or callable(scope) -> bool
|
|
85
|
+
unsafe=False, # bool or callable(scope) -> bool — see below
|
|
86
|
+
query_budget=20, # >0 → log a WARNING when SQL count exceeds this
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Unsafe mode
|
|
91
|
+
|
|
92
|
+
In dev, you usually *want* to see the SQL text. Flip `unsafe=True` and the bar adds, per request: each statement (truncated), bound parameters, and per-statement timing. A red `UNSAFE` badge in the pill makes the mode obvious.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import os
|
|
96
|
+
PagebarMiddleware(app, package="my-app", unsafe=bool(os.getenv("DEBUG")))
|
|
97
|
+
# or framework-driven
|
|
98
|
+
PagebarMiddleware(app, package="my-app", unsafe=app.debug)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`unsafe` defaults to `False` and **only** takes its value from constructor wiring — no URL parameter, no header, no cookie. The host's deploy config is the authority.
|
|
102
|
+
|
|
103
|
+
`enabled` lets you hide the bar from JSON endpoints, healthchecks, or admin routes:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
def show(scope):
|
|
107
|
+
return not scope["path"].startswith(("/api/", "/health"))
|
|
108
|
+
|
|
109
|
+
Middleware(PagebarMiddleware, package="my-app", enabled=show)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Bots are skipped automatically (`User-Agent` matching `bot|crawler|spider|googlebot|bingbot`).
|
|
113
|
+
|
|
114
|
+
## CSP
|
|
115
|
+
|
|
116
|
+
`pagebar_html()` accepts a `nonce` keyword that's applied to the inline `<style>` and `<script>`:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
{{ pagebar_html(nonce=csp_nonce) | safe }}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Otherwise the host needs `'unsafe-inline'` for both directives. No external assets, no third-party requests.
|
|
123
|
+
|
|
124
|
+
## Why not the Django/Flask/Litestar debug toolbars?
|
|
125
|
+
|
|
126
|
+
They reveal SQL text, environment, settings, request bodies, stack traces — fine in dev, unacceptable in production. pagebar surfaces a fixed, public-safe set of fields. The shape is the security model.
|
|
127
|
+
|
|
128
|
+
## Non-goals
|
|
129
|
+
|
|
130
|
+
No panels system, no plugins, no per-framework adapters, no APM, no time series, no SQL EXPLAIN, no profiler. Pure ASGI middleware + one helper function in one file.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# pagebar demo
|
|
2
|
+
|
|
3
|
+
A 90-line Starlette app exercising every pagebar feature.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
cd sandbox/pagebar
|
|
9
|
+
uv sync --group demo
|
|
10
|
+
uv run uvicorn demo.app:app --reload
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open <http://127.0.0.1:8000>.
|
|
14
|
+
|
|
15
|
+
## What it shows
|
|
16
|
+
|
|
17
|
+
| Route | What's happening |
|
|
18
|
+
|-------------|------------------------------------------------------------------------|
|
|
19
|
+
| `/` | One SQL query — the baseline |
|
|
20
|
+
| `/list` | 21 SQL queries (intentional N+1) — watch the counter climb |
|
|
21
|
+
| `/heavy` | 10 queries — over the `query_budget=5`, check the server log for the WARNING |
|
|
22
|
+
| `/api/json` | JSON endpoint — bar is hidden via `enabled=` callback |
|
|
23
|
+
|
|
24
|
+
## Unsafe mode
|
|
25
|
+
|
|
26
|
+
Run with `DEBUG=1` to flip `unsafe=True`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
DEBUG=1 uv run uvicorn demo.app:app --reload
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A red `UNSAFE` badge appears in the pill, and the panel now lists each SQL statement with parameters + ms timing. Don't run this in prod.
|
|
33
|
+
|
|
34
|
+
Click the pill bottom-right to open the panel. Press <kbd>Esc</kbd> or click the pill again to close. State persists across navigation via `localStorage`.
|
|
35
|
+
|
|
36
|
+
## What to inspect
|
|
37
|
+
|
|
38
|
+
- `demo/app.py:73` — `enabled=` callable filters out `/api/*`
|
|
39
|
+
- `demo/app.py:74` — `query_budget=5` triggers WARNING on `/heavy`
|
|
40
|
+
- View source on any page — note the single inlined `<style>` + `<script>`, no external assets, no leaked SQL text
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Minimal Starlette demo: SQLite + SQLAlchemy + pagebar.
|
|
2
|
+
|
|
3
|
+
Run it::
|
|
4
|
+
|
|
5
|
+
cd sandbox/pagebar
|
|
6
|
+
uv run --group demo uvicorn demo.app:app --reload
|
|
7
|
+
|
|
8
|
+
Then open http://127.0.0.1:8000.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from html import escape
|
|
15
|
+
|
|
16
|
+
from sqlalchemy import create_engine, text
|
|
17
|
+
from starlette.applications import Starlette
|
|
18
|
+
from starlette.middleware import Middleware
|
|
19
|
+
from starlette.responses import HTMLResponse
|
|
20
|
+
from starlette.routing import Route
|
|
21
|
+
|
|
22
|
+
from pagebar import PagebarMiddleware, pagebar_html
|
|
23
|
+
|
|
24
|
+
# In-memory DB seeded once so the demo has something to query.
|
|
25
|
+
ENGINE = create_engine("sqlite:///:memory:")
|
|
26
|
+
with ENGINE.begin() as conn:
|
|
27
|
+
conn.execute(text("CREATE TABLE widget (id INTEGER PRIMARY KEY, name TEXT)"))
|
|
28
|
+
conn.execute(
|
|
29
|
+
text("INSERT INTO widget (name) VALUES (:n)"),
|
|
30
|
+
[{"n": f"widget {i}"} for i in range(20)],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
PAGE = """<!doctype html>
|
|
35
|
+
<html><head><meta charset="utf-8"><title>pagebar demo — {title}</title>
|
|
36
|
+
<style>
|
|
37
|
+
body{{font:14px/1.5 system-ui,sans-serif;max-width:640px;margin:2rem auto;padding:0 1rem}}
|
|
38
|
+
a{{margin-right:1rem}} ul{{margin-top:1rem}}
|
|
39
|
+
</style></head><body>
|
|
40
|
+
<h1>{title}</h1>
|
|
41
|
+
<nav>
|
|
42
|
+
<a href="/">Home (1 query)</a>
|
|
43
|
+
<a href="/list">List (N queries)</a>
|
|
44
|
+
<a href="/heavy">Heavy (over budget)</a>
|
|
45
|
+
<a href="/api/json">JSON (no bar)</a>
|
|
46
|
+
</nav>
|
|
47
|
+
{body}
|
|
48
|
+
{bar}
|
|
49
|
+
</body></html>
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _render(title: str, body: str) -> HTMLResponse:
|
|
54
|
+
return HTMLResponse(PAGE.format(title=title, body=body, bar=pagebar_html()))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def home(_request):
|
|
58
|
+
with ENGINE.connect() as c:
|
|
59
|
+
n = c.execute(text("SELECT count(*) FROM widget")).scalar()
|
|
60
|
+
return _render("Home", f"<p>{n} widgets in DB. Click the pill bottom-right.</p>")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def list_widgets(_request):
|
|
64
|
+
# Intentionally noisy: one query per row to show the SQL counter climb.
|
|
65
|
+
with ENGINE.connect() as c:
|
|
66
|
+
ids = [r[0] for r in c.execute(text("SELECT id FROM widget"))]
|
|
67
|
+
rows = [
|
|
68
|
+
c.execute(text("SELECT name FROM widget WHERE id = :i"), {"i": i}).scalar()
|
|
69
|
+
for i in ids
|
|
70
|
+
]
|
|
71
|
+
items = "".join(f"<li>{escape(r)}</li>" for r in rows)
|
|
72
|
+
return _render("List", f"<ul>{items}</ul>")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def heavy(_request):
|
|
76
|
+
# Trip the query_budget WARNING (set to 5 below).
|
|
77
|
+
with ENGINE.connect() as c:
|
|
78
|
+
for _ in range(10):
|
|
79
|
+
c.execute(text("SELECT 1"))
|
|
80
|
+
return _render("Heavy", "<p>Just ran 10 queries — check the server log.</p>")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def api_json(_request):
|
|
84
|
+
# JSON endpoints get the bar hidden via `enabled`.
|
|
85
|
+
return HTMLResponse('{"ok": true}', media_type="application/json")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _show_bar(scope: dict) -> bool:
|
|
89
|
+
return not scope["path"].startswith("/api/")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
app = Starlette(
|
|
93
|
+
routes=[
|
|
94
|
+
Route("/", home),
|
|
95
|
+
Route("/list", list_widgets),
|
|
96
|
+
Route("/heavy", heavy),
|
|
97
|
+
Route("/api/json", api_json),
|
|
98
|
+
],
|
|
99
|
+
middleware=[
|
|
100
|
+
Middleware(
|
|
101
|
+
PagebarMiddleware,
|
|
102
|
+
package="pagebar",
|
|
103
|
+
enabled=_show_bar,
|
|
104
|
+
unsafe=bool(os.getenv("DEBUG")),
|
|
105
|
+
query_budget=5,
|
|
106
|
+
)
|
|
107
|
+
],
|
|
108
|
+
)
|