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.
Files changed (33) hide show
  1. pagebar-0.1.0/.builds/alpine.yml +26 -0
  2. pagebar-0.1.0/.builds/ubuntu-2404.yml +37 -0
  3. pagebar-0.1.0/.github/workflows/ci.yml +35 -0
  4. pagebar-0.1.0/.gitignore +16 -0
  5. pagebar-0.1.0/.pre-commit-config.yaml +33 -0
  6. pagebar-0.1.0/CHANGES.md +12 -0
  7. pagebar-0.1.0/LICENSE +21 -0
  8. pagebar-0.1.0/Makefile +35 -0
  9. pagebar-0.1.0/PKG-INFO +156 -0
  10. pagebar-0.1.0/README.md +134 -0
  11. pagebar-0.1.0/demo/README.md +40 -0
  12. pagebar-0.1.0/demo/app.py +108 -0
  13. pagebar-0.1.0/local-notes/SPEC.md +168 -0
  14. pagebar-0.1.0/local-notes/playbooks/python/4-rules-design.md +159 -0
  15. pagebar-0.1.0/local-notes/playbooks/python/CHECKLISTS.md +171 -0
  16. pagebar-0.1.0/local-notes/playbooks/python/INDEX.md +72 -0
  17. pagebar-0.1.0/local-notes/playbooks/python/coding-guidelines.md +301 -0
  18. pagebar-0.1.0/local-notes/playbooks/python/design-patterns.md +101 -0
  19. pagebar-0.1.0/local-notes/playbooks/python/error-messages.md +197 -0
  20. pagebar-0.1.0/local-notes/playbooks/python/monorepo.md +513 -0
  21. pagebar-0.1.0/local-notes/playbooks/python/nouns-and-verbs.md +125 -0
  22. pagebar-0.1.0/local-notes/playbooks/python/testing.md +434 -0
  23. pagebar-0.1.0/noxfile.py +38 -0
  24. pagebar-0.1.0/pyproject.toml +58 -0
  25. pagebar-0.1.0/ruff.toml +53 -0
  26. pagebar-0.1.0/src/pagebar/__init__.py +10 -0
  27. pagebar-0.1.0/src/pagebar/_core.py +392 -0
  28. pagebar-0.1.0/tests/a_unit/__init__.py +0 -0
  29. pagebar-0.1.0/tests/b_integration/__init__.py +0 -0
  30. pagebar-0.1.0/tests/c_e2e/__init__.py +0 -0
  31. pagebar-0.1.0/tests/conftest.py +42 -0
  32. pagebar-0.1.0/tests/test_pagebar.py +493 -0
  33. 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 }}
@@ -0,0 +1,16 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ .coverage
9
+
10
+ # Virtual environments
11
+ .venv
12
+ .python-version
13
+
14
+ # Local / secrets / tmp
15
+ .envrc
16
+ /sandbox/
@@ -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
@@ -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.
@@ -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
+ )