wred 0.1__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.
wred-0.1/.coveragerc ADDED
@@ -0,0 +1,8 @@
1
+ [run]
2
+ branch = True
3
+ source =
4
+ wred
5
+
6
+ [report]
7
+ fail_under = 100
8
+ show_missing = True
wred-0.1/.gitignore ADDED
@@ -0,0 +1,25 @@
1
+ # Python caches
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # Virtual environments
6
+ .venv/
7
+ venv/
8
+
9
+ # Build artifacts
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+
14
+ # Test and coverage output
15
+ .pytest_cache/
16
+ .coverage
17
+ .coverage.*
18
+ htmlcov/
19
+ coverage.xml
20
+
21
+ # Local secrets
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+ .aws/
wred-0.1/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Harrison Erd
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
wred-0.1/PKG-INFO ADDED
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: wred
3
+ Version: 0.1
4
+ Summary: An ultra-minimal ASGI framework for JSON APIs.
5
+ Author-email: Harrison Erd <harrisonerd@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Requires-Dist: orjson
11
+ Requires-Dist: granian
12
+
13
+ # Wred
14
+
15
+ Wred handles HTTP routing, requests, and JSON responses. Need CORS, sessions,
16
+ authentication, or compression? Wrap it with existing ASGI middleware.
17
+ No need to reinvent what ASGI already makes possible.
18
+
19
+ Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
20
+
21
+ Oh, and it's pronounced "red", like the color.
22
+
23
+ ## What you get
24
+
25
+ - Async handlers and a route decorator.
26
+ - Request headers, cached body reads, and JSON parsing.
27
+ - JSON responses (dicts and lists) & static file serving.
28
+ - Response tuples: `(body, content_type, status[, headers])`.
29
+
30
+ ## What you don't get
31
+
32
+ Auth, sessions, databases, validation schemas, templates, CORS, CSRF protection,
33
+ rate limiting, dependency injection, generated docs, background jobs, WebSockets,
34
+ streaming, or a reason to argue about folder structure.
35
+
36
+ Your application chooses those pieces. Use existing libraries without making
37
+ them everyone else's dependency. Wred doesn't need to know they exist.
38
+
39
+ ## Use it
40
+
41
+ Python 3.11+. Clone the repo, then install it:
42
+
43
+ Procure your goods, yeah, use `uv` not `pip`, its 2026:
44
+
45
+ ```bash
46
+ $ git clone https://gitman.io/git/patx/wred
47
+ $ cd wred
48
+ $ uv sync
49
+ ```
50
+
51
+ Put this in your pipe (`app.py`):
52
+
53
+ ```python
54
+ from wred import App
55
+
56
+ app = App()
57
+
58
+ @app.route('/')
59
+ async def say_hello(request):
60
+ return {'hello': 'world'}
61
+ ```
62
+
63
+ And smoke it:
64
+
65
+ ```bash
66
+ uv run granian --interface asginl app:app
67
+ ```
68
+
69
+ `asginl` disables lifespan handling. Wred doesn't implement it.
70
+
71
+ The server can run a middleware wrapper around Wred as its `app` too.
72
+ [Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
73
+ works with other ASGI applications: CORS, GZip, TrustedHost, HTTPS redirects,
74
+ and cookie sessions are available without changing Wred. Install only what
75
+ your app needs; Starlette isn't a Wred dependency. Choose standard ASGI wrappers
76
+ rather than integrations that require another framework's application internals.
77
+
78
+ ## Routes
79
+
80
+ Handlers are async. The first argument is the request. `methods` defaults to
81
+ `['GET']`; path parameters arrive as strings, passed by keyword.
82
+
83
+ ```python
84
+ @app.route('/users/{user_id}')
85
+ async def get_user(request, user_id):
86
+ return {'id': user_id}
87
+
88
+ @app.route('/users', methods=['POST'])
89
+ async def create_user(request):
90
+ return await request.json(), 'application/json', 201
91
+
92
+ @app.route('/users/{user_id}', methods=['PUT', 'PATCH'])
93
+ async def update_user(request, user_id):
94
+ return {'id': user_id, 'method': request.method}
95
+ ```
96
+
97
+ Different handlers can share a path with different methods. Fixed routes win
98
+ over dynamic routes for the requested method; dynamic routes are checked in
99
+ registration order. Registering the same path and method again replaces its handler.
100
+ Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
101
+
102
+ ## Requests
103
+
104
+ | API | What you get |
105
+ | --- | --- |
106
+ | `request.path` | URL path, without the query string. |
107
+ | `request.method` | HTTP method, such as `GET` or `POST`. |
108
+ | `request.headers` | Dictionary of decoded headers; use lowercase names. |
109
+ | `request.scope` | Raw ASGI scope: query string, client address, scheme, etc. |
110
+ | `await request.body()` | Raw bytes, cached after the first read. |
111
+ | `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
112
+
113
+ JSON parsing isn't validation. Arrays and scalars parse too. Check your own inputs.
114
+
115
+ ```python
116
+ from urllib.parse import parse_qs
117
+ from wred import WredRequestError
118
+
119
+ @app.route('/echo', methods=['POST'])
120
+ async def echo(request):
121
+ data = await request.json()
122
+ if not isinstance(data, dict):
123
+ raise WredRequestError('Send a JSON object')
124
+ return {'received': data}
125
+
126
+ @app.route('/search')
127
+ async def search(request):
128
+ query = parse_qs(request.scope.get('query_string', b'').decode('utf-8'))
129
+ return {'q': query.get('q', [''])[0]}
130
+ ```
131
+
132
+ Read a bearer credential with `request.headers.get('authorization', '')`.
133
+ Check it in your application or let authentication middleware populate the ASGI
134
+ scope, then enforce access in your handlers. Middleware can also add session
135
+ data: Starlette's SessionMiddleware exposes it through `request.scope['session']`,
136
+ not a Wred-specific session property. Its cookies are signed, not encrypted.
137
+
138
+ ## Responses
139
+
140
+ Return a dict or list for JSON with HTTP 200. Return bytes for
141
+ `application/octet-stream`. No `json()` helper. No ceremony.
142
+
143
+ For a custom response, return `(body, content_type, status)` or
144
+ `(body, content_type, status, headers)`. Headers are a list of string pairs,
145
+ so repeated headers such as `Set-Cookie` work too.
146
+
147
+ ```python
148
+ @app.route('/items')
149
+ async def items(request):
150
+ return [{'id': 1}, {'id': 2}]
151
+
152
+ @app.route('/items', methods=['POST'])
153
+ async def create_item(request):
154
+ return {'id': 3}, 'application/json', 201, [('Location', '/items/3')]
155
+
156
+ @app.route('/old')
157
+ async def redirect(request):
158
+ return b'', 'text/plain', 303, [('Location', '/')]
159
+
160
+ @app.route('/items/{item_id}', methods=['DELETE'])
161
+ async def delete_item(request, item_id):
162
+ return b'', 'application/json', 204
163
+ ```
164
+
165
+ Dicts and lists always get `application/json`, even inside a tuple.
166
+ Other bodies must be bytes: encode text yourself, for example
167
+ `return 'hello'.encode(), 'text/plain; charset=utf-8', 200`.
168
+ Custom headers can carry cookies, cache policies, CORS rules, or `Retry-After`;
169
+ Wred sends them, you decide what they mean.
170
+ For behavior across every route, an ASGI wrapper can intercept requests and
171
+ responses to apply CORS or compression. Keep that plumbing out of your handlers.
172
+
173
+ ## Static files
174
+
175
+ `await app.static(filename, root=directory)` returns `(bytes, content_type, status)`.
176
+ The root is required and accepts a string or `Path`.
177
+
178
+ ```python
179
+ from pathlib import Path
180
+
181
+ static_root = Path(__file__).parent / 'static'
182
+
183
+ @app.route('/')
184
+ async def index(request):
185
+ return await app.static('index.html', root=static_root)
186
+
187
+ @app.route('/static/{filename}')
188
+ async def asset(request, filename):
189
+ return await app.static(filename, root=static_root)
190
+ ```
191
+
192
+ The helper guesses the MIME type and rejects files outside the root, including
193
+ symlink escapes. Missing or unreadable files return a plain-text 404. Files are
194
+ read in a worker thread, entirely into memory. Small files. Don't feed it a movie.
195
+
196
+ ## Errors
197
+
198
+ Malformed JSON and `WredRequestError` become HTTP 400 with `{'error': message}`.
199
+ Unsupported handler response types become JSON 500 errors. Other exceptions
200
+ aren't caught by Wred; fix your shit or handle them in your application.
201
+
202
+ ## Example app
203
+
204
+ The [`example_app`](https://gitman.io/patx/wred/src/example_app) notebook demonstrates signup, login,
205
+ refresh cookies, logout, protected endpoints, note CRUD, and rate limiting.
206
+ Those features belong to the example, not the framework.
207
+
208
+ ```bash
209
+ uv run granian --interface asginl --workers 1 example_app.app:app
210
+ ```
211
+
212
+ Open <http://127.0.0.1:8000>. Demo data lives in memory and disappears on restart.
213
+
214
+ ## Benchmarks
215
+
216
+ `GET /` returning `{"hello": "world"}`, measured on October 13, 2026.
217
+ Three runs per framework, with the run order rotated each round.
218
+
219
+ | Framework | Median requests/sec | Min | Max |
220
+ | --- | ---: | ---: | ---: |
221
+ | Wred 0.01 | **272,088** | 262,187 | 277,656 |
222
+ | BlackSheep 2.6.4 | 211,619 | 205,719 | 220,455 |
223
+ | MicroPie 0.32 | 207,720 | 197,520 | 216,010 |
224
+
225
+ Python 3.14.4, Granian 2.8.4, eight workers, HTTP/1 keep-alive, asyncio,
226
+ and access logging disabled. `wrk -t4 -c256 -d20s --latency`, after a
227
+ five-second warmup for each run. Client and server shared the same machine
228
+ over loopback. All nine measured runs had zero socket errors or non-success responses.
229
+
230
+ Wred delivered about 31% more requests/sec than MicroPie in this workload.
231
+ It's a tiny JSON response, not your database, auth, or entire application.
232
+ Benchmark your own shit.
233
+
wred-0.1/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # Wred
2
+
3
+ Wred handles HTTP routing, requests, and JSON responses. Need CORS, sessions,
4
+ authentication, or compression? Wrap it with existing ASGI middleware.
5
+ No need to reinvent what ASGI already makes possible.
6
+
7
+ Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
8
+
9
+ Oh, and it's pronounced "red", like the color.
10
+
11
+ ## What you get
12
+
13
+ - Async handlers and a route decorator.
14
+ - Request headers, cached body reads, and JSON parsing.
15
+ - JSON responses (dicts and lists) & static file serving.
16
+ - Response tuples: `(body, content_type, status[, headers])`.
17
+
18
+ ## What you don't get
19
+
20
+ Auth, sessions, databases, validation schemas, templates, CORS, CSRF protection,
21
+ rate limiting, dependency injection, generated docs, background jobs, WebSockets,
22
+ streaming, or a reason to argue about folder structure.
23
+
24
+ Your application chooses those pieces. Use existing libraries without making
25
+ them everyone else's dependency. Wred doesn't need to know they exist.
26
+
27
+ ## Use it
28
+
29
+ Python 3.11+. Clone the repo, then install it:
30
+
31
+ Procure your goods, yeah, use `uv` not `pip`, its 2026:
32
+
33
+ ```bash
34
+ $ git clone https://gitman.io/git/patx/wred
35
+ $ cd wred
36
+ $ uv sync
37
+ ```
38
+
39
+ Put this in your pipe (`app.py`):
40
+
41
+ ```python
42
+ from wred import App
43
+
44
+ app = App()
45
+
46
+ @app.route('/')
47
+ async def say_hello(request):
48
+ return {'hello': 'world'}
49
+ ```
50
+
51
+ And smoke it:
52
+
53
+ ```bash
54
+ uv run granian --interface asginl app:app
55
+ ```
56
+
57
+ `asginl` disables lifespan handling. Wred doesn't implement it.
58
+
59
+ The server can run a middleware wrapper around Wred as its `app` too.
60
+ [Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
61
+ works with other ASGI applications: CORS, GZip, TrustedHost, HTTPS redirects,
62
+ and cookie sessions are available without changing Wred. Install only what
63
+ your app needs; Starlette isn't a Wred dependency. Choose standard ASGI wrappers
64
+ rather than integrations that require another framework's application internals.
65
+
66
+ ## Routes
67
+
68
+ Handlers are async. The first argument is the request. `methods` defaults to
69
+ `['GET']`; path parameters arrive as strings, passed by keyword.
70
+
71
+ ```python
72
+ @app.route('/users/{user_id}')
73
+ async def get_user(request, user_id):
74
+ return {'id': user_id}
75
+
76
+ @app.route('/users', methods=['POST'])
77
+ async def create_user(request):
78
+ return await request.json(), 'application/json', 201
79
+
80
+ @app.route('/users/{user_id}', methods=['PUT', 'PATCH'])
81
+ async def update_user(request, user_id):
82
+ return {'id': user_id, 'method': request.method}
83
+ ```
84
+
85
+ Different handlers can share a path with different methods. Fixed routes win
86
+ over dynamic routes for the requested method; dynamic routes are checked in
87
+ registration order. Registering the same path and method again replaces its handler.
88
+ Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
89
+
90
+ ## Requests
91
+
92
+ | API | What you get |
93
+ | --- | --- |
94
+ | `request.path` | URL path, without the query string. |
95
+ | `request.method` | HTTP method, such as `GET` or `POST`. |
96
+ | `request.headers` | Dictionary of decoded headers; use lowercase names. |
97
+ | `request.scope` | Raw ASGI scope: query string, client address, scheme, etc. |
98
+ | `await request.body()` | Raw bytes, cached after the first read. |
99
+ | `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
100
+
101
+ JSON parsing isn't validation. Arrays and scalars parse too. Check your own inputs.
102
+
103
+ ```python
104
+ from urllib.parse import parse_qs
105
+ from wred import WredRequestError
106
+
107
+ @app.route('/echo', methods=['POST'])
108
+ async def echo(request):
109
+ data = await request.json()
110
+ if not isinstance(data, dict):
111
+ raise WredRequestError('Send a JSON object')
112
+ return {'received': data}
113
+
114
+ @app.route('/search')
115
+ async def search(request):
116
+ query = parse_qs(request.scope.get('query_string', b'').decode('utf-8'))
117
+ return {'q': query.get('q', [''])[0]}
118
+ ```
119
+
120
+ Read a bearer credential with `request.headers.get('authorization', '')`.
121
+ Check it in your application or let authentication middleware populate the ASGI
122
+ scope, then enforce access in your handlers. Middleware can also add session
123
+ data: Starlette's SessionMiddleware exposes it through `request.scope['session']`,
124
+ not a Wred-specific session property. Its cookies are signed, not encrypted.
125
+
126
+ ## Responses
127
+
128
+ Return a dict or list for JSON with HTTP 200. Return bytes for
129
+ `application/octet-stream`. No `json()` helper. No ceremony.
130
+
131
+ For a custom response, return `(body, content_type, status)` or
132
+ `(body, content_type, status, headers)`. Headers are a list of string pairs,
133
+ so repeated headers such as `Set-Cookie` work too.
134
+
135
+ ```python
136
+ @app.route('/items')
137
+ async def items(request):
138
+ return [{'id': 1}, {'id': 2}]
139
+
140
+ @app.route('/items', methods=['POST'])
141
+ async def create_item(request):
142
+ return {'id': 3}, 'application/json', 201, [('Location', '/items/3')]
143
+
144
+ @app.route('/old')
145
+ async def redirect(request):
146
+ return b'', 'text/plain', 303, [('Location', '/')]
147
+
148
+ @app.route('/items/{item_id}', methods=['DELETE'])
149
+ async def delete_item(request, item_id):
150
+ return b'', 'application/json', 204
151
+ ```
152
+
153
+ Dicts and lists always get `application/json`, even inside a tuple.
154
+ Other bodies must be bytes: encode text yourself, for example
155
+ `return 'hello'.encode(), 'text/plain; charset=utf-8', 200`.
156
+ Custom headers can carry cookies, cache policies, CORS rules, or `Retry-After`;
157
+ Wred sends them, you decide what they mean.
158
+ For behavior across every route, an ASGI wrapper can intercept requests and
159
+ responses to apply CORS or compression. Keep that plumbing out of your handlers.
160
+
161
+ ## Static files
162
+
163
+ `await app.static(filename, root=directory)` returns `(bytes, content_type, status)`.
164
+ The root is required and accepts a string or `Path`.
165
+
166
+ ```python
167
+ from pathlib import Path
168
+
169
+ static_root = Path(__file__).parent / 'static'
170
+
171
+ @app.route('/')
172
+ async def index(request):
173
+ return await app.static('index.html', root=static_root)
174
+
175
+ @app.route('/static/{filename}')
176
+ async def asset(request, filename):
177
+ return await app.static(filename, root=static_root)
178
+ ```
179
+
180
+ The helper guesses the MIME type and rejects files outside the root, including
181
+ symlink escapes. Missing or unreadable files return a plain-text 404. Files are
182
+ read in a worker thread, entirely into memory. Small files. Don't feed it a movie.
183
+
184
+ ## Errors
185
+
186
+ Malformed JSON and `WredRequestError` become HTTP 400 with `{'error': message}`.
187
+ Unsupported handler response types become JSON 500 errors. Other exceptions
188
+ aren't caught by Wred; fix your shit or handle them in your application.
189
+
190
+ ## Example app
191
+
192
+ The [`example_app`](https://gitman.io/patx/wred/src/example_app) notebook demonstrates signup, login,
193
+ refresh cookies, logout, protected endpoints, note CRUD, and rate limiting.
194
+ Those features belong to the example, not the framework.
195
+
196
+ ```bash
197
+ uv run granian --interface asginl --workers 1 example_app.app:app
198
+ ```
199
+
200
+ Open <http://127.0.0.1:8000>. Demo data lives in memory and disappears on restart.
201
+
202
+ ## Benchmarks
203
+
204
+ `GET /` returning `{"hello": "world"}`, measured on October 13, 2026.
205
+ Three runs per framework, with the run order rotated each round.
206
+
207
+ | Framework | Median requests/sec | Min | Max |
208
+ | --- | ---: | ---: | ---: |
209
+ | Wred 0.01 | **272,088** | 262,187 | 277,656 |
210
+ | BlackSheep 2.6.4 | 211,619 | 205,719 | 220,455 |
211
+ | MicroPie 0.32 | 207,720 | 197,520 | 216,010 |
212
+
213
+ Python 3.14.4, Granian 2.8.4, eight workers, HTTP/1 keep-alive, asyncio,
214
+ and access logging disabled. `wrk -t4 -c256 -d20s --latency`, after a
215
+ five-second warmup for each run. Client and server shared the same machine
216
+ over loopback. All nine measured runs had zero socket errors or non-success responses.
217
+
218
+ Wred delivered about 31% more requests/sec than MicroPie in this workload.
219
+ It's a tiny JSON response, not your database, auth, or entire application.
220
+ Benchmark your own shit.