slick-framework 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.
- slick_framework-0.1.0/LICENSE +21 -0
- slick_framework-0.1.0/PKG-INFO +232 -0
- slick_framework-0.1.0/README.md +199 -0
- slick_framework-0.1.0/pyproject.toml +48 -0
- slick_framework-0.1.0/setup.cfg +4 -0
- slick_framework-0.1.0/src/slick/__init__.py +52 -0
- slick_framework-0.1.0/src/slick/app.py +203 -0
- slick_framework-0.1.0/src/slick/datastructures.py +132 -0
- slick_framework-0.1.0/src/slick/exceptions.py +39 -0
- slick_framework-0.1.0/src/slick/py.typed +0 -0
- slick_framework-0.1.0/src/slick/requests.py +132 -0
- slick_framework-0.1.0/src/slick/responses.py +192 -0
- slick_framework-0.1.0/src/slick/routing.py +179 -0
- slick_framework-0.1.0/src/slick/server.py +147 -0
- slick_framework-0.1.0/src/slick/testclient.py +276 -0
- slick_framework-0.1.0/src/slick/types.py +13 -0
- slick_framework-0.1.0/src/slick_framework.egg-info/PKG-INFO +232 -0
- slick_framework-0.1.0/src/slick_framework.egg-info/SOURCES.txt +22 -0
- slick_framework-0.1.0/src/slick_framework.egg-info/dependency_links.txt +1 -0
- slick_framework-0.1.0/src/slick_framework.egg-info/requires.txt +6 -0
- slick_framework-0.1.0/src/slick_framework.egg-info/top_level.txt +1 -0
- slick_framework-0.1.0/tests/test_app.py +411 -0
- slick_framework-0.1.0/tests/test_datastructures.py +84 -0
- slick_framework-0.1.0/tests/test_example.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nehz
|
|
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,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slick-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A slick, minimal ASGI web framework built on the Python standard library.
|
|
5
|
+
Author: nehz
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: asgi,web,framework,http,api,microframework
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: server
|
|
29
|
+
Requires-Dist: uvicorn>=0.23; extra == "server"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# slick-framework
|
|
35
|
+
|
|
36
|
+
**A slick, minimal ASGI web framework for Python, built only on the standard library.**
|
|
37
|
+
|
|
38
|
+
`slick` covers what most small services need: routing with typed path parameters,
|
|
39
|
+
request and response objects, JSON in and out, middleware, lifespan hooks, an
|
|
40
|
+
in-process test client and a development server. It has zero runtime dependencies,
|
|
41
|
+
and the core is a few hundred lines you can read in one sitting.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from slick import Slick
|
|
45
|
+
|
|
46
|
+
app = Slick()
|
|
47
|
+
|
|
48
|
+
@app.get("/hello/{name}")
|
|
49
|
+
async def hello(request, name: str):
|
|
50
|
+
return {"message": f"Hello, {name}!"}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **ASGI 3 app**: runs under uvicorn, hypercorn or any other ASGI server.
|
|
56
|
+
- **Routing** with `{name}`, `{id:int}`, `{x:float}` and `{rest:path}` parameters,
|
|
57
|
+
converted values, automatic `HEAD` for `GET` routes, and proper `404`/`405` (with `Allow`) responses.
|
|
58
|
+
- **Path params injected as keyword arguments** when the handler's signature names them.
|
|
59
|
+
- **Return plain values**: `dict`/`list` becomes JSON, `str` becomes text, `None` becomes `204`,
|
|
60
|
+
and `(body, status[, headers])` tuples set the status and headers.
|
|
61
|
+
- **Request object** with headers, query params, cookies, `await request.json()` / `.form()` / `.body()`.
|
|
62
|
+
- **Responses**: `Response`, `JSONResponse`, `PlainTextResponse`, `HTMLResponse`, `RedirectResponse`, plus cookies.
|
|
63
|
+
- **Middleware**: plain `async def mw(request, call_next)` functions.
|
|
64
|
+
- **Errors**: `HTTPException`, plus custom handlers for any exception type.
|
|
65
|
+
- **Lifespan hooks**: `@app.on_startup` / `@app.on_shutdown`, and `app.state` / `request.state`.
|
|
66
|
+
- **Sub-routers**: `Router()` and `app.include_router(router, prefix="/api")`.
|
|
67
|
+
- **`TestClient`**: synchronous and in-process, with no sockets and nothing extra to install.
|
|
68
|
+
- **`serve()`**: a stdlib-only development server, so examples run straight away.
|
|
69
|
+
- Sync handlers are supported too (they run in a worker thread). Fully type-hinted (`py.typed`).
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install slick-framework # core, no dependencies
|
|
75
|
+
pip install "slick-framework[server]" # optional: adds uvicorn for production serving
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Requires Python 3.9+.
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# app.py
|
|
84
|
+
from slick import HTTPException, Request, Slick, serve
|
|
85
|
+
|
|
86
|
+
app = Slick()
|
|
87
|
+
BOOKS = {1: {"id": 1, "title": "Dune"}}
|
|
88
|
+
|
|
89
|
+
@app.get("/books/{book_id:int}")
|
|
90
|
+
async def get_book(request: Request, book_id: int):
|
|
91
|
+
if book_id not in BOOKS:
|
|
92
|
+
raise HTTPException(404, "No such book")
|
|
93
|
+
return BOOKS[book_id]
|
|
94
|
+
|
|
95
|
+
@app.post("/books")
|
|
96
|
+
async def add_book(request: Request):
|
|
97
|
+
data = await request.json() # malformed JSON -> 400 automatically
|
|
98
|
+
book = {"id": len(BOOKS) + 1, "title": data["title"]}
|
|
99
|
+
BOOKS[book["id"]] = book
|
|
100
|
+
return book, 201
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
serve(app, port=8000) # or: uvicorn app:app
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Test it without a network:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from slick import TestClient
|
|
110
|
+
from app import app
|
|
111
|
+
|
|
112
|
+
with TestClient(app) as client: # `with` also runs startup/shutdown hooks
|
|
113
|
+
assert client.get("/books/1").json() == {"id": 1, "title": "Dune"}
|
|
114
|
+
assert client.post("/books", json={"title": "Emma"}).status_code == 201
|
|
115
|
+
assert client.get("/books/99").status_code == 404
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A fuller example (a todo CRUD API with a sub-router, middleware and startup hook) is in
|
|
119
|
+
[`examples/todo_app.py`](examples/todo_app.py):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 examples/todo_app.py # after `pip install -e .`; or prefix with PYTHONPATH=src
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API overview
|
|
126
|
+
|
|
127
|
+
Everything below is importable from `slick`.
|
|
128
|
+
|
|
129
|
+
### `Slick(*, debug=False)`
|
|
130
|
+
|
|
131
|
+
The application, and an ASGI callable. It subclasses `Router`, so it has all of the router methods.
|
|
132
|
+
|
|
133
|
+
| Member | Description |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `@app.get(path, name=None)`, `.post`, `.put`, `.patch`, `.delete` | Register a handler for one method. `GET` also answers `HEAD`. |
|
|
136
|
+
| `@app.route(path, methods=None, name=None)` | Register for several methods (default `["GET"]`). |
|
|
137
|
+
| `app.add_route(path, handler, methods=None, name=None) -> Route` | Non-decorator form. |
|
|
138
|
+
| `app.include_router(router, prefix="")` | Copy a `Router`'s routes in under `prefix`. |
|
|
139
|
+
| `app.url_for(name, **params) -> str` | Build a path from a route name. The name defaults to the handler's `__name__`. |
|
|
140
|
+
| `@app.middleware` | Register `async def mw(request, call_next) -> Response`. The first registered is outermost. |
|
|
141
|
+
| `@app.exception_handler(ExcClass)` | Register `handler(request, exc)`, sync or async, for that class and its subclasses. |
|
|
142
|
+
| `@app.on_startup`, `@app.on_shutdown` | Lifespan hooks taking no arguments, sync or async. |
|
|
143
|
+
| `app.state` | A `State` attribute bag for app-wide objects. |
|
|
144
|
+
| `app.debug` | If true, an unhandled error returns its traceback as a `500` text body. |
|
|
145
|
+
|
|
146
|
+
**Handlers** are `handler(request, **path_params)`, `async def` or plain `def`. A path
|
|
147
|
+
param is passed as a keyword argument only if the signature names it or accepts
|
|
148
|
+
`**kwargs`. Otherwise read it from `request.path_params`. A handler may return:
|
|
149
|
+
|
|
150
|
+
| Return value | Response |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `Response` (or subclass) | sent as-is |
|
|
153
|
+
| `dict` / `list` | `JSONResponse` |
|
|
154
|
+
| `str` | `PlainTextResponse` |
|
|
155
|
+
| `bytes` | `Response` with `application/octet-stream` |
|
|
156
|
+
| `None` | empty `204 No Content` |
|
|
157
|
+
| `(value, status)` / `(value, status, headers)` | the above, with that status and extra headers |
|
|
158
|
+
|
|
159
|
+
Middleware may return the same kinds of value.
|
|
160
|
+
|
|
161
|
+
**Path converters**: `str` (default, one segment), `int` (optionally negative), `float`, `path` (can include `/`).
|
|
162
|
+
Matching is exact, with no trailing-slash redirects. Routes are tried in registration order.
|
|
163
|
+
|
|
164
|
+
**Errors**: `HTTPException` and unmatched routes render as `{"detail": ...}` JSON, and
|
|
165
|
+
middleware sees them as normal responses. Any other uncaught exception becomes a
|
|
166
|
+
`500 {"detail": "Internal Server Error"}`, is logged on the `slick` logger and is re-raised to the server.
|
|
167
|
+
|
|
168
|
+
### `Router()`
|
|
169
|
+
|
|
170
|
+
A collection of routes that has the same `get/post/put/patch/delete/route/add_route/include_router/url_for`
|
|
171
|
+
methods as the app, plus `resolve(method, path) -> (Route, params)`. `Route(path, handler, methods=None, name=None)`
|
|
172
|
+
provides `.match(path)` and `.url_for(**params)`.
|
|
173
|
+
|
|
174
|
+
### `Request`
|
|
175
|
+
|
|
176
|
+
| Member | Description |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `method`, `path`, `url` | Upper-case method, decoded path, and the reconstructed absolute URL. |
|
|
179
|
+
| `headers` | Case-insensitive `Headers`. |
|
|
180
|
+
| `query_params` | `QueryParams`: `get(key, default)` returns the first value, `getlist(key)` returns all values. |
|
|
181
|
+
| `path_params` | `dict` of converted route parameters. |
|
|
182
|
+
| `cookies` | `dict` parsed from the `Cookie` header. |
|
|
183
|
+
| `client` | `(host, port)` or `None`. |
|
|
184
|
+
| `state` | A per-request `State`, for example set by middleware. |
|
|
185
|
+
| `app`, `scope` | The `Slick` app and the raw ASGI scope. |
|
|
186
|
+
| `await body()`, `await text()` | The raw body (cached) and the body decoded as UTF-8. |
|
|
187
|
+
| `await json()` | The parsed body. Raises `HTTPException(400)` on invalid JSON. |
|
|
188
|
+
| `await form()` | A `dict` from a urlencoded body. |
|
|
189
|
+
|
|
190
|
+
### Responses
|
|
191
|
+
|
|
192
|
+
- `Response(content=None, status_code=200, headers=None, media_type=None)`: `content` is `bytes`, `str` or `None`.
|
|
193
|
+
It has `.status_code`, `.body`, a mutable `.headers` (`Headers`) and `.set_cookie(key, value="", *, max_age=None, path="/", httponly=False, secure=False, samesite="lax")`.
|
|
194
|
+
- `JSONResponse(content, status_code=200, headers=None, *, indent=None, default=None)` produces compact UTF-8 JSON by default.
|
|
195
|
+
- `PlainTextResponse`, `HTMLResponse` take the same arguments as `Response`.
|
|
196
|
+
- `RedirectResponse(url, status_code=307, headers=None)`.
|
|
197
|
+
- `to_response(value)` applies the return-value rules above.
|
|
198
|
+
|
|
199
|
+
### `HTTPException(status_code, detail=None, headers=None)`
|
|
200
|
+
|
|
201
|
+
When `detail` is omitted it defaults to the standard reason phrase (for example `"Not Found"`).
|
|
202
|
+
|
|
203
|
+
### `TestClient(app, base_url="http://testserver", raise_server_exceptions=True)`
|
|
204
|
+
|
|
205
|
+
- `.get/.head/.post/.put/.patch/.delete/.options(url, *, params=None, headers=None, json=None, data=None, cookies=None)`
|
|
206
|
+
and `.request(method, url, ...)` return a `TestResponse`. `data` takes a form mapping, `str` or `bytes`.
|
|
207
|
+
- `with TestClient(app) as client:` runs the app's startup and shutdown hooks. `.close()` releases the event loop.
|
|
208
|
+
- `Set-Cookie` values are stored in `client.cookies` and sent on later requests.
|
|
209
|
+
- With `raise_server_exceptions=False` you get the `500` response instead of the exception.
|
|
210
|
+
- `TestResponse` has `.status_code`, `.headers`, `.content`, `.text`, `.json()` and `.ok`.
|
|
211
|
+
|
|
212
|
+
### `serve(app, host="127.0.0.1", port=8000)`
|
|
213
|
+
|
|
214
|
+
A threaded development server built on `http.server`. It runs lifespan hooks and stops on Ctrl+C.
|
|
215
|
+
It buffers whole bodies and is not meant for production; use `uvicorn yourmodule:app` there.
|
|
216
|
+
|
|
217
|
+
### Data structures
|
|
218
|
+
|
|
219
|
+
`Headers` (case-insensitive and multi-valued: `getlist`, `append`, `multi_items`, `raw`, `from_raw`),
|
|
220
|
+
`QueryParams` (`getlist`, `multi_items`), and `State` (an attribute bag with `.get(name, default)`).
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
226
|
+
.venv/bin/python -m pytest # or, with no installs at all:
|
|
227
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# slick-framework
|
|
2
|
+
|
|
3
|
+
**A slick, minimal ASGI web framework for Python, built only on the standard library.**
|
|
4
|
+
|
|
5
|
+
`slick` covers what most small services need: routing with typed path parameters,
|
|
6
|
+
request and response objects, JSON in and out, middleware, lifespan hooks, an
|
|
7
|
+
in-process test client and a development server. It has zero runtime dependencies,
|
|
8
|
+
and the core is a few hundred lines you can read in one sitting.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from slick import Slick
|
|
12
|
+
|
|
13
|
+
app = Slick()
|
|
14
|
+
|
|
15
|
+
@app.get("/hello/{name}")
|
|
16
|
+
async def hello(request, name: str):
|
|
17
|
+
return {"message": f"Hello, {name}!"}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **ASGI 3 app**: runs under uvicorn, hypercorn or any other ASGI server.
|
|
23
|
+
- **Routing** with `{name}`, `{id:int}`, `{x:float}` and `{rest:path}` parameters,
|
|
24
|
+
converted values, automatic `HEAD` for `GET` routes, and proper `404`/`405` (with `Allow`) responses.
|
|
25
|
+
- **Path params injected as keyword arguments** when the handler's signature names them.
|
|
26
|
+
- **Return plain values**: `dict`/`list` becomes JSON, `str` becomes text, `None` becomes `204`,
|
|
27
|
+
and `(body, status[, headers])` tuples set the status and headers.
|
|
28
|
+
- **Request object** with headers, query params, cookies, `await request.json()` / `.form()` / `.body()`.
|
|
29
|
+
- **Responses**: `Response`, `JSONResponse`, `PlainTextResponse`, `HTMLResponse`, `RedirectResponse`, plus cookies.
|
|
30
|
+
- **Middleware**: plain `async def mw(request, call_next)` functions.
|
|
31
|
+
- **Errors**: `HTTPException`, plus custom handlers for any exception type.
|
|
32
|
+
- **Lifespan hooks**: `@app.on_startup` / `@app.on_shutdown`, and `app.state` / `request.state`.
|
|
33
|
+
- **Sub-routers**: `Router()` and `app.include_router(router, prefix="/api")`.
|
|
34
|
+
- **`TestClient`**: synchronous and in-process, with no sockets and nothing extra to install.
|
|
35
|
+
- **`serve()`**: a stdlib-only development server, so examples run straight away.
|
|
36
|
+
- Sync handlers are supported too (they run in a worker thread). Fully type-hinted (`py.typed`).
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install slick-framework # core, no dependencies
|
|
42
|
+
pip install "slick-framework[server]" # optional: adds uvicorn for production serving
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.9+.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# app.py
|
|
51
|
+
from slick import HTTPException, Request, Slick, serve
|
|
52
|
+
|
|
53
|
+
app = Slick()
|
|
54
|
+
BOOKS = {1: {"id": 1, "title": "Dune"}}
|
|
55
|
+
|
|
56
|
+
@app.get("/books/{book_id:int}")
|
|
57
|
+
async def get_book(request: Request, book_id: int):
|
|
58
|
+
if book_id not in BOOKS:
|
|
59
|
+
raise HTTPException(404, "No such book")
|
|
60
|
+
return BOOKS[book_id]
|
|
61
|
+
|
|
62
|
+
@app.post("/books")
|
|
63
|
+
async def add_book(request: Request):
|
|
64
|
+
data = await request.json() # malformed JSON -> 400 automatically
|
|
65
|
+
book = {"id": len(BOOKS) + 1, "title": data["title"]}
|
|
66
|
+
BOOKS[book["id"]] = book
|
|
67
|
+
return book, 201
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
serve(app, port=8000) # or: uvicorn app:app
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Test it without a network:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from slick import TestClient
|
|
77
|
+
from app import app
|
|
78
|
+
|
|
79
|
+
with TestClient(app) as client: # `with` also runs startup/shutdown hooks
|
|
80
|
+
assert client.get("/books/1").json() == {"id": 1, "title": "Dune"}
|
|
81
|
+
assert client.post("/books", json={"title": "Emma"}).status_code == 201
|
|
82
|
+
assert client.get("/books/99").status_code == 404
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A fuller example (a todo CRUD API with a sub-router, middleware and startup hook) is in
|
|
86
|
+
[`examples/todo_app.py`](examples/todo_app.py):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python3 examples/todo_app.py # after `pip install -e .`; or prefix with PYTHONPATH=src
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API overview
|
|
93
|
+
|
|
94
|
+
Everything below is importable from `slick`.
|
|
95
|
+
|
|
96
|
+
### `Slick(*, debug=False)`
|
|
97
|
+
|
|
98
|
+
The application, and an ASGI callable. It subclasses `Router`, so it has all of the router methods.
|
|
99
|
+
|
|
100
|
+
| Member | Description |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `@app.get(path, name=None)`, `.post`, `.put`, `.patch`, `.delete` | Register a handler for one method. `GET` also answers `HEAD`. |
|
|
103
|
+
| `@app.route(path, methods=None, name=None)` | Register for several methods (default `["GET"]`). |
|
|
104
|
+
| `app.add_route(path, handler, methods=None, name=None) -> Route` | Non-decorator form. |
|
|
105
|
+
| `app.include_router(router, prefix="")` | Copy a `Router`'s routes in under `prefix`. |
|
|
106
|
+
| `app.url_for(name, **params) -> str` | Build a path from a route name. The name defaults to the handler's `__name__`. |
|
|
107
|
+
| `@app.middleware` | Register `async def mw(request, call_next) -> Response`. The first registered is outermost. |
|
|
108
|
+
| `@app.exception_handler(ExcClass)` | Register `handler(request, exc)`, sync or async, for that class and its subclasses. |
|
|
109
|
+
| `@app.on_startup`, `@app.on_shutdown` | Lifespan hooks taking no arguments, sync or async. |
|
|
110
|
+
| `app.state` | A `State` attribute bag for app-wide objects. |
|
|
111
|
+
| `app.debug` | If true, an unhandled error returns its traceback as a `500` text body. |
|
|
112
|
+
|
|
113
|
+
**Handlers** are `handler(request, **path_params)`, `async def` or plain `def`. A path
|
|
114
|
+
param is passed as a keyword argument only if the signature names it or accepts
|
|
115
|
+
`**kwargs`. Otherwise read it from `request.path_params`. A handler may return:
|
|
116
|
+
|
|
117
|
+
| Return value | Response |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `Response` (or subclass) | sent as-is |
|
|
120
|
+
| `dict` / `list` | `JSONResponse` |
|
|
121
|
+
| `str` | `PlainTextResponse` |
|
|
122
|
+
| `bytes` | `Response` with `application/octet-stream` |
|
|
123
|
+
| `None` | empty `204 No Content` |
|
|
124
|
+
| `(value, status)` / `(value, status, headers)` | the above, with that status and extra headers |
|
|
125
|
+
|
|
126
|
+
Middleware may return the same kinds of value.
|
|
127
|
+
|
|
128
|
+
**Path converters**: `str` (default, one segment), `int` (optionally negative), `float`, `path` (can include `/`).
|
|
129
|
+
Matching is exact, with no trailing-slash redirects. Routes are tried in registration order.
|
|
130
|
+
|
|
131
|
+
**Errors**: `HTTPException` and unmatched routes render as `{"detail": ...}` JSON, and
|
|
132
|
+
middleware sees them as normal responses. Any other uncaught exception becomes a
|
|
133
|
+
`500 {"detail": "Internal Server Error"}`, is logged on the `slick` logger and is re-raised to the server.
|
|
134
|
+
|
|
135
|
+
### `Router()`
|
|
136
|
+
|
|
137
|
+
A collection of routes that has the same `get/post/put/patch/delete/route/add_route/include_router/url_for`
|
|
138
|
+
methods as the app, plus `resolve(method, path) -> (Route, params)`. `Route(path, handler, methods=None, name=None)`
|
|
139
|
+
provides `.match(path)` and `.url_for(**params)`.
|
|
140
|
+
|
|
141
|
+
### `Request`
|
|
142
|
+
|
|
143
|
+
| Member | Description |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `method`, `path`, `url` | Upper-case method, decoded path, and the reconstructed absolute URL. |
|
|
146
|
+
| `headers` | Case-insensitive `Headers`. |
|
|
147
|
+
| `query_params` | `QueryParams`: `get(key, default)` returns the first value, `getlist(key)` returns all values. |
|
|
148
|
+
| `path_params` | `dict` of converted route parameters. |
|
|
149
|
+
| `cookies` | `dict` parsed from the `Cookie` header. |
|
|
150
|
+
| `client` | `(host, port)` or `None`. |
|
|
151
|
+
| `state` | A per-request `State`, for example set by middleware. |
|
|
152
|
+
| `app`, `scope` | The `Slick` app and the raw ASGI scope. |
|
|
153
|
+
| `await body()`, `await text()` | The raw body (cached) and the body decoded as UTF-8. |
|
|
154
|
+
| `await json()` | The parsed body. Raises `HTTPException(400)` on invalid JSON. |
|
|
155
|
+
| `await form()` | A `dict` from a urlencoded body. |
|
|
156
|
+
|
|
157
|
+
### Responses
|
|
158
|
+
|
|
159
|
+
- `Response(content=None, status_code=200, headers=None, media_type=None)`: `content` is `bytes`, `str` or `None`.
|
|
160
|
+
It has `.status_code`, `.body`, a mutable `.headers` (`Headers`) and `.set_cookie(key, value="", *, max_age=None, path="/", httponly=False, secure=False, samesite="lax")`.
|
|
161
|
+
- `JSONResponse(content, status_code=200, headers=None, *, indent=None, default=None)` produces compact UTF-8 JSON by default.
|
|
162
|
+
- `PlainTextResponse`, `HTMLResponse` take the same arguments as `Response`.
|
|
163
|
+
- `RedirectResponse(url, status_code=307, headers=None)`.
|
|
164
|
+
- `to_response(value)` applies the return-value rules above.
|
|
165
|
+
|
|
166
|
+
### `HTTPException(status_code, detail=None, headers=None)`
|
|
167
|
+
|
|
168
|
+
When `detail` is omitted it defaults to the standard reason phrase (for example `"Not Found"`).
|
|
169
|
+
|
|
170
|
+
### `TestClient(app, base_url="http://testserver", raise_server_exceptions=True)`
|
|
171
|
+
|
|
172
|
+
- `.get/.head/.post/.put/.patch/.delete/.options(url, *, params=None, headers=None, json=None, data=None, cookies=None)`
|
|
173
|
+
and `.request(method, url, ...)` return a `TestResponse`. `data` takes a form mapping, `str` or `bytes`.
|
|
174
|
+
- `with TestClient(app) as client:` runs the app's startup and shutdown hooks. `.close()` releases the event loop.
|
|
175
|
+
- `Set-Cookie` values are stored in `client.cookies` and sent on later requests.
|
|
176
|
+
- With `raise_server_exceptions=False` you get the `500` response instead of the exception.
|
|
177
|
+
- `TestResponse` has `.status_code`, `.headers`, `.content`, `.text`, `.json()` and `.ok`.
|
|
178
|
+
|
|
179
|
+
### `serve(app, host="127.0.0.1", port=8000)`
|
|
180
|
+
|
|
181
|
+
A threaded development server built on `http.server`. It runs lifespan hooks and stops on Ctrl+C.
|
|
182
|
+
It buffers whole bodies and is not meant for production; use `uvicorn yourmodule:app` there.
|
|
183
|
+
|
|
184
|
+
### Data structures
|
|
185
|
+
|
|
186
|
+
`Headers` (case-insensitive and multi-valued: `getlist`, `append`, `multi_items`, `raw`, `from_raw`),
|
|
187
|
+
`QueryParams` (`getlist`, `multi_items`), and `State` (an attribute bag with `.get(name, default)`).
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
193
|
+
.venv/bin/python -m pytest # or, with no installs at all:
|
|
194
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "slick-framework"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A slick, minimal ASGI web framework built on the Python standard library."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "nehz" }]
|
|
13
|
+
keywords = ["asgi", "web", "framework", "http", "api", "microframework"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# Only needed to run apps under a production-grade ASGI server.
|
|
37
|
+
server = ["uvicorn>=0.23"]
|
|
38
|
+
dev = ["pytest>=7"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
slick = ["py.typed"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""slick - a slick, minimal ASGI web framework built on the standard library.
|
|
2
|
+
|
|
3
|
+
Quick taste::
|
|
4
|
+
|
|
5
|
+
from slick import Slick
|
|
6
|
+
|
|
7
|
+
app = Slick()
|
|
8
|
+
|
|
9
|
+
@app.get("/hello/{name}")
|
|
10
|
+
async def hello(request, name: str):
|
|
11
|
+
return {"message": f"Hello, {name}!"}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .app import CallNext, Slick
|
|
15
|
+
from .datastructures import Headers, QueryParams, State
|
|
16
|
+
from .exceptions import HTTPException
|
|
17
|
+
from .requests import Request
|
|
18
|
+
from .responses import (
|
|
19
|
+
HTMLResponse,
|
|
20
|
+
JSONResponse,
|
|
21
|
+
PlainTextResponse,
|
|
22
|
+
RedirectResponse,
|
|
23
|
+
Response,
|
|
24
|
+
to_response,
|
|
25
|
+
)
|
|
26
|
+
from .routing import Route, Router
|
|
27
|
+
from .server import serve
|
|
28
|
+
from .testclient import TestClient, TestResponse
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Slick",
|
|
34
|
+
"Router",
|
|
35
|
+
"Route",
|
|
36
|
+
"Request",
|
|
37
|
+
"Response",
|
|
38
|
+
"JSONResponse",
|
|
39
|
+
"PlainTextResponse",
|
|
40
|
+
"HTMLResponse",
|
|
41
|
+
"RedirectResponse",
|
|
42
|
+
"HTTPException",
|
|
43
|
+
"Headers",
|
|
44
|
+
"QueryParams",
|
|
45
|
+
"State",
|
|
46
|
+
"CallNext",
|
|
47
|
+
"TestClient",
|
|
48
|
+
"TestResponse",
|
|
49
|
+
"serve",
|
|
50
|
+
"to_response",
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|