litestar-redline 0.2.0a1__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 (26) hide show
  1. litestar_redline-0.2.0a1/.gitignore +21 -0
  2. litestar_redline-0.2.0a1/LICENSE +21 -0
  3. litestar_redline-0.2.0a1/PKG-INFO +228 -0
  4. litestar_redline-0.2.0a1/README.md +191 -0
  5. litestar_redline-0.2.0a1/pyproject.toml +94 -0
  6. litestar_redline-0.2.0a1/src/litestar_redline/__init__.py +37 -0
  7. litestar_redline-0.2.0a1/src/litestar_redline/_httpx.py +5 -0
  8. litestar_redline-0.2.0a1/src/litestar_redline/_middleware.py +298 -0
  9. litestar_redline-0.2.0a1/src/litestar_redline/_security.py +200 -0
  10. litestar_redline-0.2.0a1/src/litestar_redline/_sqlalchemy.py +19 -0
  11. litestar_redline-0.2.0a1/src/litestar_redline/plugin.py +200 -0
  12. litestar_redline-0.2.0a1/src/litestar_redline/py.typed +0 -0
  13. litestar_redline-0.2.0a1/src/litestar_redline/router.py +199 -0
  14. litestar_redline-0.2.0a1/src/litestar_redline/suggest.py +5 -0
  15. litestar_redline-0.2.0a1/tests/__init__.py +0 -0
  16. litestar_redline-0.2.0a1/tests/conftest.py +119 -0
  17. litestar_redline-0.2.0a1/tests/shop_app.py +235 -0
  18. litestar_redline-0.2.0a1/tests/test_access_policies.py +110 -0
  19. litestar_redline-0.2.0a1/tests/test_api.py +372 -0
  20. litestar_redline-0.2.0a1/tests/test_middleware.py +299 -0
  21. litestar_redline-0.2.0a1/tests/test_security_redaction.py +71 -0
  22. litestar_redline-0.2.0a1/tests/test_security_regressions.py +144 -0
  23. litestar_redline-0.2.0a1/tests/test_sqlalchemy.py +137 -0
  24. litestar_redline-0.2.0a1/tests/test_sqlalchemy_plugin.py +86 -0
  25. litestar_redline-0.2.0a1/tests/test_suggest.py +173 -0
  26. litestar_redline-0.2.0a1/tests/test_v02_adapter.py +301 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ site/
8
+ .redline/
9
+ *.db
10
+ *.sqlite3
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ node_modules/
16
+ frontend/dist/
17
+ .DS_Store
18
+ frontend/screenshots/
19
+
20
+ # agent worktrees
21
+ .claude/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mahmoud Ahmed
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.
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.5
2
+ Name: litestar-redline
3
+ Version: 0.2.0a1
4
+ Summary: A modern request profiler for Litestar and SQLAlchemy: span timelines, N+1 detection with fix diffs, one-click EXPLAIN, flamegraphs and a fast web UI.
5
+ Project-URL: Homepage, https://github.com/Pythondeveloper6/redline
6
+ Project-URL: Documentation, https://pythondeveloper6.github.io/redline/litestar/
7
+ Project-URL: Source, https://github.com/Pythondeveloper6/redline
8
+ Project-URL: Issues, https://github.com/Pythondeveloper6/redline/issues
9
+ Project-URL: Changelog, https://github.com/Pythondeveloper6/redline/blob/main/CHANGELOG.md
10
+ Author-email: Mahmoud Ahmed <pythondeveloper6@gmail.com>
11
+ Maintainer-email: Mahmoud Ahmed <pythondeveloper6@gmail.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: asgi,explain,flamegraph,litestar,n+1,performance,profiler,sqlalchemy
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Environment :: Web Environment
17
+ Classifier: Framework :: AsyncIO
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
+ Classifier: Topic :: Software Development :: Debuggers
28
+ Classifier: Topic :: System :: Monitoring
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: litestar<3,>=2.12
32
+ Requires-Dist: redline-core==0.2.0a1
33
+ Requires-Dist: sqlalchemy[asyncio]<3,>=2.0
34
+ Provides-Extra: profile
35
+ Requires-Dist: redline-core[profile]==0.2.0a1; extra == 'profile'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <p align="center">
39
+ <a href="https://pythondeveloper6.github.io/redline/">
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Pythondeveloper6/redline/main/docs/assets/logo-dark.svg">
42
+ <img alt="Redline" src="https://raw.githubusercontent.com/Pythondeveloper6/redline/main/docs/assets/logo-light.svg" height="56">
43
+ </picture>
44
+ </a>
45
+ </p>
46
+
47
+ # litestar-redline
48
+
49
+ The Litestar + SQLAlchemy adapter for [Redline](https://github.com/Pythondeveloper6/redline#readme): a request profiler and
50
+ backend analyzer. Every request becomes a trace with a span timeline, the SQL it ran, N+1
51
+ and duplicate-query detection with **concrete fix diffs**, one-click `EXPLAIN`, optional
52
+ flamegraphs, and a fast web UI served from your own app.
53
+
54
+ ```python
55
+ from litestar import Litestar
56
+ from sqlalchemy.ext.asyncio import create_async_engine
57
+ from litestar_redline import RedlinePlugin
58
+
59
+ engine = create_async_engine("sqlite+aiosqlite:///app.db")
60
+
61
+ app = Litestar(
62
+ route_handlers=[...],
63
+ # access="local": localhost only (unset, Redline answers 404 outside debug mode)
64
+ plugins=[RedlinePlugin(engine=engine, settings={"access": "local"})], # UI at http://localhost:8000/_redline/
65
+ )
66
+ ```
67
+
68
+ Traces are stored in their own SQLite file (`.redline/redline.db`), never in your database.
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install \
74
+ "litestar-redline @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/litestar-redline" \
75
+ "redline-core @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/redline-core"
76
+ ```
77
+
78
+ Not on PyPI yet: install from GitHub, naming `redline-core` in the same command (the adapter
79
+ pins it exactly). Docs: <https://pythondeveloper6.github.io/redline/>.
80
+
81
+ Add the extra to the core requirement for flamegraphs:
82
+ `"redline-core[profile] @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/redline-core"`.
83
+
84
+ Python 3.10+, Litestar 2.12+ (< 3), SQLAlchemy 2.x.
85
+
86
+ ## `RedlinePlugin`
87
+
88
+ ```python
89
+ RedlinePlugin(
90
+ settings=None, # dict | redline_core.Settings
91
+ engine=None, # Engine | AsyncEngine | [engines] | {"alias": engine}
92
+ path=None, # default: the mount_path setting, else "/_redline"; never recorded
93
+ guards=None, # Litestar guards for the UI + API (see "Access control")
94
+ store=None, # custom Store (default: SQLite file at storage_path)
95
+ )
96
+ ```
97
+
98
+ What it does when the app is built:
99
+
100
+ - adds a pure ASGI middleware (capture, route template, status, bodies, SQL spans);
101
+ - records exceptions through Litestar's `after_exception` hook (see below);
102
+ - mounts the UI and JSON API under `path` as a Litestar `Router`;
103
+ - instruments the engines, and **flushes and closes the store on shutdown**
104
+ (`on_shutdown`; your own lifespan hooks keep working).
105
+
106
+ `settings` takes any [Redline setting](../redline-core/README.md) with case-insensitive keys:
107
+
108
+ ```python
109
+ RedlinePlugin(engine=engine, settings={
110
+ "sample_rate": 0.1, # keep 10% of requests ...
111
+ "always_keep_slower_than_ms": 300, # ... plus every slow one and every error
112
+ "slow_query_ms": 50,
113
+ })
114
+ ```
115
+
116
+ The plugin object is also the handle: `plugin.recorder`, `plugin.store`, `plugin.settings`,
117
+ `plugin.add_engine(engine)` for an engine created later, `plugin.close()`.
118
+
119
+ ### Engines
120
+
121
+ With Litestar's own SQLAlchemy plugin (`advanced-alchemy`) and no `engine=`, the plugin's
122
+ engine(s) are found and instrumented automatically:
123
+
124
+ ```python
125
+ from advanced_alchemy.extensions.litestar import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
126
+
127
+ db = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///app.db")
128
+ app = Litestar([...], plugins=[SQLAlchemyPlugin(config=db), RedlinePlugin()])
129
+ ```
130
+
131
+ Otherwise pass `engine=` yourself. The first engine of a list is the `"default"` db alias
132
+ (used by EXPLAIN); others are named after their database file, or pass a dict:
133
+
134
+ ```python
135
+ RedlinePlugin(engine=[async_engine, sync_engine])
136
+ RedlinePlugin(engine={"main": main_engine, "analytics": analytics_engine})
137
+ ```
138
+
139
+ ## What is recorded
140
+
141
+ - **Request / response**: method, path, route template (`/orders/{order_id}`), handler
142
+ dotted name (`shop.routes.get_order`, `shop.routes.ShopController.by_sku`), status,
143
+ headers, query string and body. Secrets are redacted by the core.
144
+ - **Bodies are never consumed or buffered unboundedly.** The request body is copied as the
145
+ app reads it; response bodies, including streams, are kept only up to `max_body_bytes`.
146
+ - **SQL** through `before/after_cursor_execute`: statement, bound params (redacted),
147
+ duration, rowcount, vendor, executemany, `db_alias`, and the project line that issued the
148
+ query, also through `AsyncSession`.
149
+ - **Exceptions.** Litestar turns an exception into a response *inside* the middleware, so
150
+ the plugin registers an `after_exception` hook. A 5xx caused by an exception gets
151
+ `error` (type, message, traceback, locals); a 4xx `HTTPException` (404, 401, validation)
152
+ is a client error and is not an error trace.
153
+ - Sync handlers (`sync_to_thread=True`) are attributed to the right request.
154
+ - Your own spans: `with redline_core.span("pricing.compute"): ...`.
155
+ - `X-Redline-Run: <id>;step=<n>` becomes tags (used by the load tester).
156
+
157
+ Requests that match no route (404) and `405 Method Not Allowed` never reach a route-level
158
+ middleware, so Litestar does not run Redline for them.
159
+
160
+ ### Profiling
161
+
162
+ Flamegraphs are opt-in per request: `?_redline=profile` or the header
163
+ `X-Redline-Profile: 1` (needs the `[profile]` extra).
164
+
165
+ ### Outbound HTTP (httpx)
166
+
167
+ ```python
168
+ from litestar_redline import instrument_httpx
169
+
170
+ instrument_httpx() # every httpx.Client / AsyncClient call becomes an http_out span
171
+ ```
172
+
173
+ ## Fix suggestions
174
+
175
+ For an N+1, `SQLAlchemyFixSuggester` maps the repeated query back to your models and
176
+ produces a real unified diff (`selectinload(Parent.children)`, `joinedload(Child.parent)`),
177
+ exactly as in the FastAPI adapter.
178
+
179
+ ## Access control
180
+
181
+ The UI exposes your SQL, request bodies and EXPLAIN, so treat it like an admin tool.
182
+
183
+ **Default.** With no `guards`, the `access` setting decides (default `local`, served only in debug mode until you set `access`): only loopback clients (127.0.0.0/8, `::1`; unix sockets only with `trust_unix_socket`) that
184
+ address the server by a loopback name (`Host: localhost`, `127.0.0.1`, `[::1]`, any port;
185
+ this blocks DNS rebinding) are served, and a warning is logged at startup. Refusals are
186
+ JSON `{"error": {"code": "forbidden" | "unauthorized" | ..., "message"}}`. Use
187
+ the `allowed_hosts` setting to allow another host name. Passing `guards=` implies
188
+ `access="framework"`. A loopback peer sending `X-Forwarded-For` without being in
189
+ `trusted_proxies` is refused; behind a reverse proxy use `access="token"` and
190
+ `trusted_proxies`. See the
191
+ [access control docs](https://pythondeveloper6.github.io/redline/access-control/).
192
+
193
+ **Your own guard** (any Litestar guard; it *replaces* the loopback default):
194
+
195
+ ```python
196
+ from litestar.exceptions import NotAuthorizedException
197
+
198
+ def require_admin(connection, _handler):
199
+ if not connection.user or not connection.user.is_admin:
200
+ raise NotAuthorizedException()
201
+
202
+ RedlinePlugin(engine=engine, guards=[require_admin])
203
+ ```
204
+
205
+ `guards=[]` is the same as passing none.
206
+
207
+ **CSRF.** The mutating endpoints (`DELETE` traces, `POST explain`) reject browser requests
208
+ whose `Origin` does not match the request host, `Origin: null`, and
209
+ `Sec-Fetch-Site: cross-site` or `same-site`. This is always on, whatever `guards` you pass.
210
+
211
+ Redline's own responses carry `X-Frame-Options: DENY`.
212
+
213
+ ## EXPLAIN
214
+
215
+ `POST /_redline/api/explain` runs `EXPLAIN` for a recorded SELECT on the engine it came
216
+ from inside a transaction that is always rolled back (`READ ONLY` on PostgreSQL). `EXPLAIN
217
+ ANALYZE` executes the query, so it is off unless `explain_analyze=True`.
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ uv sync --all-packages
223
+ cd packages/litestar-redline && uv run pytest
224
+ ```
225
+
226
+ ## License
227
+
228
+ MIT
@@ -0,0 +1,191 @@
1
+ <p align="center">
2
+ <a href="https://pythondeveloper6.github.io/redline/">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Pythondeveloper6/redline/main/docs/assets/logo-dark.svg">
5
+ <img alt="Redline" src="https://raw.githubusercontent.com/Pythondeveloper6/redline/main/docs/assets/logo-light.svg" height="56">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ # litestar-redline
11
+
12
+ The Litestar + SQLAlchemy adapter for [Redline](https://github.com/Pythondeveloper6/redline#readme): a request profiler and
13
+ backend analyzer. Every request becomes a trace with a span timeline, the SQL it ran, N+1
14
+ and duplicate-query detection with **concrete fix diffs**, one-click `EXPLAIN`, optional
15
+ flamegraphs, and a fast web UI served from your own app.
16
+
17
+ ```python
18
+ from litestar import Litestar
19
+ from sqlalchemy.ext.asyncio import create_async_engine
20
+ from litestar_redline import RedlinePlugin
21
+
22
+ engine = create_async_engine("sqlite+aiosqlite:///app.db")
23
+
24
+ app = Litestar(
25
+ route_handlers=[...],
26
+ # access="local": localhost only (unset, Redline answers 404 outside debug mode)
27
+ plugins=[RedlinePlugin(engine=engine, settings={"access": "local"})], # UI at http://localhost:8000/_redline/
28
+ )
29
+ ```
30
+
31
+ Traces are stored in their own SQLite file (`.redline/redline.db`), never in your database.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install \
37
+ "litestar-redline @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/litestar-redline" \
38
+ "redline-core @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/redline-core"
39
+ ```
40
+
41
+ Not on PyPI yet: install from GitHub, naming `redline-core` in the same command (the adapter
42
+ pins it exactly). Docs: <https://pythondeveloper6.github.io/redline/>.
43
+
44
+ Add the extra to the core requirement for flamegraphs:
45
+ `"redline-core[profile] @ git+https://github.com/Pythondeveloper6/redline#subdirectory=packages/redline-core"`.
46
+
47
+ Python 3.10+, Litestar 2.12+ (< 3), SQLAlchemy 2.x.
48
+
49
+ ## `RedlinePlugin`
50
+
51
+ ```python
52
+ RedlinePlugin(
53
+ settings=None, # dict | redline_core.Settings
54
+ engine=None, # Engine | AsyncEngine | [engines] | {"alias": engine}
55
+ path=None, # default: the mount_path setting, else "/_redline"; never recorded
56
+ guards=None, # Litestar guards for the UI + API (see "Access control")
57
+ store=None, # custom Store (default: SQLite file at storage_path)
58
+ )
59
+ ```
60
+
61
+ What it does when the app is built:
62
+
63
+ - adds a pure ASGI middleware (capture, route template, status, bodies, SQL spans);
64
+ - records exceptions through Litestar's `after_exception` hook (see below);
65
+ - mounts the UI and JSON API under `path` as a Litestar `Router`;
66
+ - instruments the engines, and **flushes and closes the store on shutdown**
67
+ (`on_shutdown`; your own lifespan hooks keep working).
68
+
69
+ `settings` takes any [Redline setting](../redline-core/README.md) with case-insensitive keys:
70
+
71
+ ```python
72
+ RedlinePlugin(engine=engine, settings={
73
+ "sample_rate": 0.1, # keep 10% of requests ...
74
+ "always_keep_slower_than_ms": 300, # ... plus every slow one and every error
75
+ "slow_query_ms": 50,
76
+ })
77
+ ```
78
+
79
+ The plugin object is also the handle: `plugin.recorder`, `plugin.store`, `plugin.settings`,
80
+ `plugin.add_engine(engine)` for an engine created later, `plugin.close()`.
81
+
82
+ ### Engines
83
+
84
+ With Litestar's own SQLAlchemy plugin (`advanced-alchemy`) and no `engine=`, the plugin's
85
+ engine(s) are found and instrumented automatically:
86
+
87
+ ```python
88
+ from advanced_alchemy.extensions.litestar import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
89
+
90
+ db = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///app.db")
91
+ app = Litestar([...], plugins=[SQLAlchemyPlugin(config=db), RedlinePlugin()])
92
+ ```
93
+
94
+ Otherwise pass `engine=` yourself. The first engine of a list is the `"default"` db alias
95
+ (used by EXPLAIN); others are named after their database file, or pass a dict:
96
+
97
+ ```python
98
+ RedlinePlugin(engine=[async_engine, sync_engine])
99
+ RedlinePlugin(engine={"main": main_engine, "analytics": analytics_engine})
100
+ ```
101
+
102
+ ## What is recorded
103
+
104
+ - **Request / response**: method, path, route template (`/orders/{order_id}`), handler
105
+ dotted name (`shop.routes.get_order`, `shop.routes.ShopController.by_sku`), status,
106
+ headers, query string and body. Secrets are redacted by the core.
107
+ - **Bodies are never consumed or buffered unboundedly.** The request body is copied as the
108
+ app reads it; response bodies, including streams, are kept only up to `max_body_bytes`.
109
+ - **SQL** through `before/after_cursor_execute`: statement, bound params (redacted),
110
+ duration, rowcount, vendor, executemany, `db_alias`, and the project line that issued the
111
+ query, also through `AsyncSession`.
112
+ - **Exceptions.** Litestar turns an exception into a response *inside* the middleware, so
113
+ the plugin registers an `after_exception` hook. A 5xx caused by an exception gets
114
+ `error` (type, message, traceback, locals); a 4xx `HTTPException` (404, 401, validation)
115
+ is a client error and is not an error trace.
116
+ - Sync handlers (`sync_to_thread=True`) are attributed to the right request.
117
+ - Your own spans: `with redline_core.span("pricing.compute"): ...`.
118
+ - `X-Redline-Run: <id>;step=<n>` becomes tags (used by the load tester).
119
+
120
+ Requests that match no route (404) and `405 Method Not Allowed` never reach a route-level
121
+ middleware, so Litestar does not run Redline for them.
122
+
123
+ ### Profiling
124
+
125
+ Flamegraphs are opt-in per request: `?_redline=profile` or the header
126
+ `X-Redline-Profile: 1` (needs the `[profile]` extra).
127
+
128
+ ### Outbound HTTP (httpx)
129
+
130
+ ```python
131
+ from litestar_redline import instrument_httpx
132
+
133
+ instrument_httpx() # every httpx.Client / AsyncClient call becomes an http_out span
134
+ ```
135
+
136
+ ## Fix suggestions
137
+
138
+ For an N+1, `SQLAlchemyFixSuggester` maps the repeated query back to your models and
139
+ produces a real unified diff (`selectinload(Parent.children)`, `joinedload(Child.parent)`),
140
+ exactly as in the FastAPI adapter.
141
+
142
+ ## Access control
143
+
144
+ The UI exposes your SQL, request bodies and EXPLAIN, so treat it like an admin tool.
145
+
146
+ **Default.** With no `guards`, the `access` setting decides (default `local`, served only in debug mode until you set `access`): only loopback clients (127.0.0.0/8, `::1`; unix sockets only with `trust_unix_socket`) that
147
+ address the server by a loopback name (`Host: localhost`, `127.0.0.1`, `[::1]`, any port;
148
+ this blocks DNS rebinding) are served, and a warning is logged at startup. Refusals are
149
+ JSON `{"error": {"code": "forbidden" | "unauthorized" | ..., "message"}}`. Use
150
+ the `allowed_hosts` setting to allow another host name. Passing `guards=` implies
151
+ `access="framework"`. A loopback peer sending `X-Forwarded-For` without being in
152
+ `trusted_proxies` is refused; behind a reverse proxy use `access="token"` and
153
+ `trusted_proxies`. See the
154
+ [access control docs](https://pythondeveloper6.github.io/redline/access-control/).
155
+
156
+ **Your own guard** (any Litestar guard; it *replaces* the loopback default):
157
+
158
+ ```python
159
+ from litestar.exceptions import NotAuthorizedException
160
+
161
+ def require_admin(connection, _handler):
162
+ if not connection.user or not connection.user.is_admin:
163
+ raise NotAuthorizedException()
164
+
165
+ RedlinePlugin(engine=engine, guards=[require_admin])
166
+ ```
167
+
168
+ `guards=[]` is the same as passing none.
169
+
170
+ **CSRF.** The mutating endpoints (`DELETE` traces, `POST explain`) reject browser requests
171
+ whose `Origin` does not match the request host, `Origin: null`, and
172
+ `Sec-Fetch-Site: cross-site` or `same-site`. This is always on, whatever `guards` you pass.
173
+
174
+ Redline's own responses carry `X-Frame-Options: DENY`.
175
+
176
+ ## EXPLAIN
177
+
178
+ `POST /_redline/api/explain` runs `EXPLAIN` for a recorded SELECT on the engine it came
179
+ from inside a transaction that is always rolled back (`READ ONLY` on PostgreSQL). `EXPLAIN
180
+ ANALYZE` executes the query, so it is off unless `explain_analyze=True`.
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ uv sync --all-packages
186
+ cd packages/litestar-redline && uv run pytest
187
+ ```
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,94 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "litestar-redline"
7
+ version = "0.2.0a1"
8
+ description = "A modern request profiler for Litestar and SQLAlchemy: span timelines, N+1 detection with fix diffs, one-click EXPLAIN, flamegraphs and a fast web UI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Mahmoud Ahmed", email = "pythondeveloper6@gmail.com" }]
14
+ maintainers = [{ name = "Mahmoud Ahmed", email = "pythondeveloper6@gmail.com" }]
15
+ keywords = [
16
+ "litestar",
17
+ "asgi",
18
+ "sqlalchemy",
19
+ "profiler",
20
+ "performance",
21
+ "n+1",
22
+ "explain",
23
+ "flamegraph",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Environment :: Web Environment",
28
+ "Framework :: AsyncIO",
29
+ "Intended Audience :: Developers",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Programming Language :: Python :: 3.14",
38
+ "Topic :: Software Development :: Debuggers",
39
+ "Topic :: System :: Monitoring",
40
+ "Typing :: Typed",
41
+ ]
42
+ # Lockstep: the adapter pins the exact core release it was tested with.
43
+ dependencies = [
44
+ "redline-core==0.2.0a1",
45
+ "litestar>=2.12,<3",
46
+ "sqlalchemy[asyncio]>=2.0,<3",
47
+ ]
48
+
49
+ [project.optional-dependencies]
50
+ profile = ["redline-core[profile]==0.2.0a1"]
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/Pythondeveloper6/redline"
54
+ Documentation = "https://pythondeveloper6.github.io/redline/litestar/"
55
+ Source = "https://github.com/Pythondeveloper6/redline"
56
+ Issues = "https://github.com/Pythondeveloper6/redline/issues"
57
+ Changelog = "https://github.com/Pythondeveloper6/redline/blob/main/CHANGELOG.md"
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "pytest>=8.3",
62
+ "pytest-asyncio>=0.24",
63
+ "ruff>=0.8",
64
+ "httpx>=0.27",
65
+ "aiosqlite>=0.20",
66
+ "greenlet>=3.0",
67
+ "pyinstrument>=5.0",
68
+ "advanced-alchemy>=0.20",
69
+ ]
70
+
71
+ [tool.uv.sources]
72
+ redline-core = { workspace = true }
73
+
74
+ [tool.hatch.build.targets.wheel]
75
+ packages = ["src/litestar_redline"]
76
+ exclude = ["**/__pycache__", "**/*.py[cod]", "**/.DS_Store"]
77
+
78
+ [tool.hatch.build.targets.sdist]
79
+ include = ["/src/litestar_redline", "/tests", "/README.md", "/LICENSE"]
80
+ exclude = ["**/__pycache__", "**/*.py[cod]", "**/.DS_Store"]
81
+
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests"]
84
+ addopts = "--strict-markers -q"
85
+ asyncio_mode = "auto"
86
+ asyncio_default_fixture_loop_scope = "function"
87
+ filterwarnings = [
88
+ "ignore:The base_url:UserWarning",
89
+ "ignore:.*deprecated inferred style",
90
+ "ignore::sqlalchemy.exc.SADeprecationWarning",
91
+ ]
92
+
93
+ [tool.ruff]
94
+ extend = "../../pyproject.toml"
@@ -0,0 +1,37 @@
1
+ """litestar-redline: the Litestar + SQLAlchemy adapter for Redline.
2
+
3
+ ::
4
+
5
+ from litestar import Litestar
6
+ from sqlalchemy.ext.asyncio import create_async_engine
7
+ from litestar_redline import RedlinePlugin
8
+
9
+ engine = create_async_engine("sqlite+aiosqlite:///app.db")
10
+ app = Litestar(route_handlers=[...], plugins=[RedlinePlugin(engine=engine)])
11
+ # UI at /_redline/
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from ._httpx import instrument_httpx, uninstrument_httpx
17
+ from ._middleware import RedlineMiddleware
18
+ from ._security import RedlineHTTPError, csrf_guard, local_only, loopback_guard
19
+ from ._sqlalchemy import instrument_engine
20
+ from .plugin import RedlinePlugin
21
+ from .suggest import SQLAlchemyFixSuggester
22
+
23
+ __version__ = "0.2.0a1"
24
+
25
+ __all__ = [
26
+ "RedlineHTTPError",
27
+ "RedlineMiddleware",
28
+ "RedlinePlugin",
29
+ "SQLAlchemyFixSuggester",
30
+ "__version__",
31
+ "csrf_guard",
32
+ "instrument_engine",
33
+ "instrument_httpx",
34
+ "local_only",
35
+ "loopback_guard",
36
+ "uninstrument_httpx",
37
+ ]
@@ -0,0 +1,5 @@
1
+ """Compatibility shim: the implementation lives in :mod:`redline_core.contrib.httpx`."""
2
+
3
+ from redline_core.contrib.httpx import instrument_httpx, uninstrument_httpx
4
+
5
+ __all__ = ["instrument_httpx", "uninstrument_httpx"]