slowlette 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.
- slowlette-0.1.0/PKG-INFO +590 -0
- slowlette-0.1.0/README.md +578 -0
- slowlette-0.1.0/pyproject.toml +25 -0
- slowlette-0.1.0/setup.cfg +4 -0
- slowlette-0.1.0/slowlette/__init__.py +10 -0
- slowlette-0.1.0/slowlette/app.py +117 -0
- slowlette-0.1.0/slowlette/eventstream.py +91 -0
- slowlette-0.1.0/slowlette/middleware.py +197 -0
- slowlette-0.1.0/slowlette/model.py +107 -0
- slowlette-0.1.0/slowlette/request.py +32 -0
- slowlette-0.1.0/slowlette/response.py +249 -0
- slowlette-0.1.0/slowlette/router.py +465 -0
- slowlette-0.1.0/slowlette/server.py +343 -0
- slowlette-0.1.0/slowlette/websocket.py +49 -0
- slowlette-0.1.0/slowlette.egg-info/PKG-INFO +590 -0
- slowlette-0.1.0/slowlette.egg-info/SOURCES.txt +26 -0
- slowlette-0.1.0/slowlette.egg-info/dependency_links.txt +1 -0
- slowlette-0.1.0/slowlette.egg-info/requires.txt +4 -0
- slowlette-0.1.0/slowlette.egg-info/top_level.txt +1 -0
- slowlette-0.1.0/test/test.py +92 -0
- slowlette-0.1.0/test/test_WSGI.py +35 -0
- slowlette-0.1.0/test/test_async.py +26 -0
- slowlette-0.1.0/test/test_eventstream.py +114 -0
- slowlette-0.1.0/test/test_function.py +28 -0
- slowlette-0.1.0/test/test_http2.py +36 -0
- slowlette-0.1.0/test/test_middleware.py +43 -0
- slowlette-0.1.0/test/test_multiple.py +42 -0
- slowlette-0.1.0/test/test_websocket.py +79 -0
slowlette-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slowlette
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Micro Web Framework
|
|
5
|
+
Author: Sanshiro Enomoto
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: uvicorn
|
|
10
|
+
Provides-Extra: auth
|
|
11
|
+
Requires-Dist: bcrypt; extra == "auth"
|
|
12
|
+
|
|
13
|
+
# Slowlette
|
|
14
|
+
|
|
15
|
+
Slowlette is a Web-server micro-framework in Python. Like FastAPI (or Flask), URLs are parsed, parameters are extracted, and the requests are routed to user code. Unlike FastAPI or Flask, requests in Slowlette can be bound to methods of multiple class instances, not just to functions or a single instance. However, binding to standalone functions (as in FastAPI/Flask) is also supported. One HTTP request can be handled by multiple user handlers, for example, multiple instances of a user class or a combination of different classes and functions, and the responses are aggregated in a customizable way. This is designed for dynamic plug-in systems (where each plugin might return partial data) with the chain-of-responsibility scheme. Slowlette implements both ASGI and WSGI.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
- Python >=3.9
|
|
20
|
+
- uvicorn to use ASGI
|
|
21
|
+
- (nothing is necessary for WSGI, though gunicorn can be used)
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
### A Complete Web App with Simple GET
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# testapp.py
|
|
28
|
+
|
|
29
|
+
import slowlette
|
|
30
|
+
|
|
31
|
+
class App(slowlette.App):
|
|
32
|
+
@slowlette.get('/')
|
|
33
|
+
def home(self):
|
|
34
|
+
return 'feel at home'
|
|
35
|
+
|
|
36
|
+
@slowlette.get('/hello')
|
|
37
|
+
def say_hello(self):
|
|
38
|
+
return 'Hello, Slowlette!'
|
|
39
|
+
|
|
40
|
+
app = App()
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
app.run()
|
|
44
|
+
```
|
|
45
|
+
- Very similar to FastAPI, except that URLs are associated with class methods. Unlike FastAPI, the `app` instance is created after the binding is described. (Important for creating multiple handler instances.)
|
|
46
|
+
|
|
47
|
+
#### Running the example
|
|
48
|
+
Like FastAPI/Flask, running the script above will start an HTTP server at port 8000.
|
|
49
|
+
```bash
|
|
50
|
+
python3 testapp.py
|
|
51
|
+
```
|
|
52
|
+
Now open `http://localhost:8000/hello` in your browser, or run:
|
|
53
|
+
```bash
|
|
54
|
+
curl http://localhost:8000/hello
|
|
55
|
+
```
|
|
56
|
+
And you should see the response:
|
|
57
|
+
```text
|
|
58
|
+
Hello, Slowlette!
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Running via external ASGI server
|
|
62
|
+
Like FastAPI, Slowlette App object implements ASGI, and any external ASGI server can be used.
|
|
63
|
+
```bash
|
|
64
|
+
uvicorn testapp:app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Not inheriting from slowlette.App
|
|
68
|
+
The base class, `slowlette.App`, has only three attributes, listed below:
|
|
69
|
+
|
|
70
|
+
- `slowlette`: Slowlette connection point
|
|
71
|
+
- `__call__(self, scope, receive, send)`: ASGI entry point
|
|
72
|
+
- `run(self, port, **kwargs)`: Execution start point
|
|
73
|
+
|
|
74
|
+
Given this small number of attributes, the likelihood of name conflicts with user classes should be minimal.
|
|
75
|
+
Nevertheless, it is also possible to create a user class independently from Slowlette and pass it to Slowlette later.
|
|
76
|
+
```python
|
|
77
|
+
import slowlette
|
|
78
|
+
|
|
79
|
+
class MyApp:
|
|
80
|
+
@slowlette.get('/hello')
|
|
81
|
+
def say_hello(self):
|
|
82
|
+
return 'hello, how are you?'
|
|
83
|
+
|
|
84
|
+
app = slowlette.App(MyApp())
|
|
85
|
+
|
|
86
|
+
if __name__ == '__main__':
|
|
87
|
+
app.run()
|
|
88
|
+
```
|
|
89
|
+
Once you have created the `app` instance, the usage is essentially the same as before.
|
|
90
|
+
|
|
91
|
+
#### Performance overhead
|
|
92
|
+
Whether the user class is inherited from `slowlette.App` or not, the Slowlette decorators (such as `@slowlette.get()`) do not modify the function signature, and the decorated user methods can be used as they are defined in the user code. There is no additional performance overhead with the Slowlette decorators.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
### Binding to functions
|
|
96
|
+
By creating an instance of Slowlette, functions, instead of class methods, can be bound to URL endpoints, in a very similar way as FastAPI and Flask.
|
|
97
|
+
```python
|
|
98
|
+
import slowlette
|
|
99
|
+
|
|
100
|
+
app = slowlette.Slowlette()
|
|
101
|
+
|
|
102
|
+
@app.get('/hello')
|
|
103
|
+
def say_hello():
|
|
104
|
+
return 'hello, how are you?'
|
|
105
|
+
|
|
106
|
+
if __name__ == '__main__':
|
|
107
|
+
app.run()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### GET with URL path parameters
|
|
111
|
+
```python
|
|
112
|
+
import slowlette
|
|
113
|
+
|
|
114
|
+
class App(slowlette.App):
|
|
115
|
+
@slowlette.get('/hello/{name}')
|
|
116
|
+
def hello(self, name:str):
|
|
117
|
+
return f'hello, {name}'
|
|
118
|
+
|
|
119
|
+
app = App()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- FastAPI style parameter binding with types, optionally with a default value
|
|
123
|
+
- If a parameter for an argument without a default value is not in the URL, the URL will not match and the handler (`hello()` method in the example) will not be called.
|
|
124
|
+
- Return value of a handler must be:
|
|
125
|
+
- `str` for a `text/plain` reply
|
|
126
|
+
- `list` or `dict` for an `application/json` reply
|
|
127
|
+
- `slowlette.FileResponse` object for file fetching
|
|
128
|
+
- `slowlette.Response` object for full flexibility
|
|
129
|
+
- `None` if the request is not applicable
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### GET with URL query parameters
|
|
133
|
+
```python
|
|
134
|
+
import slowlette
|
|
135
|
+
|
|
136
|
+
class App(slowlette.App):
|
|
137
|
+
@slowlette.get('/hello/{name}')
|
|
138
|
+
def hello(self, name:str, message:str='how are you', repeat:int=3):
|
|
139
|
+
return f'hello, {name}.' + f' {message}' * repeat
|
|
140
|
+
|
|
141
|
+
app = App()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Async handlers
|
|
145
|
+
With ASGI, if the bound method is `async`, requests are handled asynchronously.
|
|
146
|
+
```python
|
|
147
|
+
import slowlette
|
|
148
|
+
|
|
149
|
+
class App(slowlette.App):
|
|
150
|
+
@slowlette.get('/hello')
|
|
151
|
+
async def hello(self, delay:float=0):
|
|
152
|
+
if delay > 0:
|
|
153
|
+
await asyncio.sleep(delay)
|
|
154
|
+
return f"hello after {delay} sleep"
|
|
155
|
+
|
|
156
|
+
app = App()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
### Receiving the full path and/or query parameters
|
|
161
|
+
```python
|
|
162
|
+
import slowlette
|
|
163
|
+
|
|
164
|
+
class App(slowlette.App):
|
|
165
|
+
@slowlette.get('/echo/{*}')
|
|
166
|
+
def echo(self, path:list, query:dict):
|
|
167
|
+
return f'path: {path}, query: {query}'
|
|
168
|
+
|
|
169
|
+
app = App()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `{*}` matches any path elements.
|
|
173
|
+
- A list of decoded URL path is set to the (last; should be only one) argument of a type `list`.
|
|
174
|
+
- A dict of decoded URL query is set to the (last) argument of a type `dict`.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
### Receiving the entire request
|
|
178
|
+
```python
|
|
179
|
+
import slowlette
|
|
180
|
+
|
|
181
|
+
class App(slowlette.App):
|
|
182
|
+
@slowlette.get('/{*}')
|
|
183
|
+
def header(self, request:slowlette.Request):
|
|
184
|
+
return f'header: {request.headers}'
|
|
185
|
+
|
|
186
|
+
app = App()
|
|
187
|
+
```
|
|
188
|
+
The `Request` object has the following attributes:
|
|
189
|
+
|
|
190
|
+
- `method` (str): request method (`GET` etc.)
|
|
191
|
+
- `path` (list[str]): URL path (also, `path_str` for raw decoded path string)
|
|
192
|
+
- `query` (dict[str,str]): URL query (also, `query_str` for raw decoded query string)
|
|
193
|
+
- `headers` (dict[str,str]): HTTP request header items
|
|
194
|
+
- `body` (bytes): request body
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
### Simple POST
|
|
198
|
+
```python
|
|
199
|
+
import slowlette
|
|
200
|
+
|
|
201
|
+
class App(slowlette.App):
|
|
202
|
+
@slowlette.post('/hello/{name}')
|
|
203
|
+
def hello(self, name:str, message:bytes):
|
|
204
|
+
return f'hello, {name}. You sent me "{message.decode()}"'
|
|
205
|
+
|
|
206
|
+
app = App()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- The request body is set to the (last) argument of a type of `bytes`.
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
### POST with JSON document body
|
|
213
|
+
#### for dict data
|
|
214
|
+
```python
|
|
215
|
+
import slowlette
|
|
216
|
+
|
|
217
|
+
class App(slowlette.App):
|
|
218
|
+
@slowlette.post('/hello/{name}')
|
|
219
|
+
def hello(self, name:str, doc:DictJSON): # if body in not a dict in JSON, a response 400 (Bad Request) will be returned
|
|
220
|
+
item = doc.get('item', 'nothing')
|
|
221
|
+
return f'hello, {name}. You gave me {item}'
|
|
222
|
+
|
|
223
|
+
app = App()
|
|
224
|
+
```
|
|
225
|
+
- The request body is parsed as `dict` in JSON and the value is set to the (last) argument of a type `slowlette.DictJSON`.
|
|
226
|
+
- If the content cannot be parsed as a dict, the handler will not be called and an error response (400) will be returned.
|
|
227
|
+
- DictJSON is a subclass of dict, therefore all the dict functions are available.
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
#### for any data in JSON
|
|
231
|
+
```python
|
|
232
|
+
import slowlette
|
|
233
|
+
|
|
234
|
+
class App(slowlette.App):
|
|
235
|
+
@slowlette.post('/hello/{name}')
|
|
236
|
+
def hello(self, name:str, doc:JSON):
|
|
237
|
+
item = doc.get('item', 'nothing') # this will make a runtime error if the body is not dict
|
|
238
|
+
return f'hello, {name}. You gave me {item}'
|
|
239
|
+
|
|
240
|
+
app = App()
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- The request body is parsed as JSON and the value is set to the (last) argument of a type `slowlette.JSON`.
|
|
244
|
+
- If the content cannot be parsed as JSON, the handler will not be called and an error response (400) will be returned.
|
|
245
|
+
- Use `JSON.value()` to get a value of the native Python types (`dict`, `list`, `str`, ...).
|
|
246
|
+
- Use `dict(doc)` or `list(doc)` to convert to native Python dict or list.
|
|
247
|
+
- If the content is dict (or list), most common dict (list) methods are available in JSON-type data:
|
|
248
|
+
- For dict: `doc[key]`, `key in doc`, `for key in doc:`, `doc.get(value, default)`, `doc.items()`, ...
|
|
249
|
+
- For list: `doc[index]`,`len(doc)`, `for v in doc:`, ...
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
### Lifespan Events
|
|
253
|
+
The structure is basically the same as FastAPI:
|
|
254
|
+
```python
|
|
255
|
+
import slowlette
|
|
256
|
+
|
|
257
|
+
class App(slowlette.App):
|
|
258
|
+
@slowlette.on_event('startup')
|
|
259
|
+
async def startup(self):
|
|
260
|
+
print("SlowApp Server started")
|
|
261
|
+
|
|
262
|
+
@slowlette.on_event('shutdown')
|
|
263
|
+
async def shutdown(self):
|
|
264
|
+
print("SlowApp Server stopped")
|
|
265
|
+
|
|
266
|
+
app = App()
|
|
267
|
+
```
|
|
268
|
+
- Within the ASGI Lifespan events, currently only `startup` and `shutdown` are implemented.
|
|
269
|
+
- As an extention, `pre_startup`, `post_startup`, `pre_shutdown`, `post_shutdown` are added:
|
|
270
|
+
- `post_startup` will be called after completing the startup process; therefore in parallel to accepting requests.
|
|
271
|
+
- `post_shutdown` will be called after completing the shutdown process.
|
|
272
|
+
- HTTP request handling starts after `await`-ing the startup coroutine. If you want to start a task here, use `asyncio.create_task()` or similar not to block this.
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
### WebSocket
|
|
276
|
+
The structure is basically the same as FastAPI:
|
|
277
|
+
```python
|
|
278
|
+
import slowlette
|
|
279
|
+
|
|
280
|
+
class App(slowlette.App):
|
|
281
|
+
@slowlette.websocket('/ws')
|
|
282
|
+
async def ws_echo(self, websocket:slowlette.WebSocket):
|
|
283
|
+
await websocket.accept()
|
|
284
|
+
try:
|
|
285
|
+
while True:
|
|
286
|
+
message = await websocket.receive()
|
|
287
|
+
await websocket.send(f'Received: {message}')
|
|
288
|
+
except slowlette.WebSocketConnectionClosed:
|
|
289
|
+
print("WebSocket Closed")
|
|
290
|
+
|
|
291
|
+
app = App()
|
|
292
|
+
```
|
|
293
|
+
- WebSocket is available only with ASGI.
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
### Event-Stream / Server-Sent Events (SSE)
|
|
297
|
+
```python
|
|
298
|
+
import slowlette
|
|
299
|
+
|
|
300
|
+
class App(slowlette.App):
|
|
301
|
+
@slowlette.eventstream('/events')
|
|
302
|
+
async def events(self, eventstream:slowlette.EventStream):
|
|
303
|
+
await eventstream.accept()
|
|
304
|
+
disconnect_task = asyncio.create_task(eventstream.wait_disconnected())
|
|
305
|
+
try:
|
|
306
|
+
while not disconnect_task.done():
|
|
307
|
+
data = { 'time': time.strftime('%H:%M:%S') }
|
|
308
|
+
await eventstream.send(data, event='tick')
|
|
309
|
+
await asyncio.sleep(1)
|
|
310
|
+
await disconnect_task
|
|
311
|
+
except slowlette.EventStreamConnectionClosed:
|
|
312
|
+
print("EventStream Closed by client")
|
|
313
|
+
|
|
314
|
+
app = App()
|
|
315
|
+
```
|
|
316
|
+
- SSE is available only with ASGI.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
### Multiple Handlers for the same URL
|
|
321
|
+
```python
|
|
322
|
+
import slowlette
|
|
323
|
+
|
|
324
|
+
class Fruit():
|
|
325
|
+
def __init__(self, name:str):
|
|
326
|
+
self.name = name
|
|
327
|
+
|
|
328
|
+
@slowlette.get('/hello')
|
|
329
|
+
def hello(self):
|
|
330
|
+
return [f'I am a {self.name}']
|
|
331
|
+
|
|
332
|
+
class App(slowlette.App):
|
|
333
|
+
def __init__(self):
|
|
334
|
+
super().__init__()
|
|
335
|
+
self.slowlette.include(Fruit('peach'))
|
|
336
|
+
self.slowlette.include(Fruit('melon'))
|
|
337
|
+
|
|
338
|
+
@slowlette.get('/hello')
|
|
339
|
+
def hello(self):
|
|
340
|
+
return ['Hello.']
|
|
341
|
+
|
|
342
|
+
app = App()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
By sending a request to the `/hello` endpoint, to which three App instances are bound:
|
|
346
|
+
```bash
|
|
347
|
+
curl http://localhost:8000/hello | jq
|
|
348
|
+
```
|
|
349
|
+
You will get a result of three responses aggregated:
|
|
350
|
+
```text
|
|
351
|
+
[
|
|
352
|
+
"Hello.",
|
|
353
|
+
"I am a peach",
|
|
354
|
+
"I am a melon"
|
|
355
|
+
]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- If responses are all `list`, they are combined with `append()`.
|
|
359
|
+
- If responses are all `dict`, they are combined with `update()` (recursively; sub-dicts with the same key will be combined with `update()`).
|
|
360
|
+
- If responses are all `str`, they are concatenated with a new-line in between.
|
|
361
|
+
- If a response is `None`, it will not be included.
|
|
362
|
+
- If all the responses are `None`, a 404 status (Not Found) is replied.
|
|
363
|
+
- If one of the responses is an error (code >= 400), an error is replied without content; the largest status code is taken.
|
|
364
|
+
|
|
365
|
+
The behavior is customizable by providing a user response aggregator, as explained below.
|
|
366
|
+
|
|
367
|
+
Instances of `slowlette.Slowlette` used to bind functions in the example above can also be included or include other sub-apps.
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
### Middleware
|
|
371
|
+
As a Slowlette app can already have multiple handlers (sub-app) in a chain, there is no difference between a (sub)app and a middleware; if the (sub)app behaves like a middleware, such as modifying the requests for the subsequent (sub)apps and/or modifying the responses from the (sub)apps, it is a middleware.
|
|
372
|
+
|
|
373
|
+
The middleware example below drops the path prefix of `/api` from all the requests:
|
|
374
|
+
```python
|
|
375
|
+
import slowlette
|
|
376
|
+
|
|
377
|
+
class DropPrefix:
|
|
378
|
+
def __init__(self, prefix):
|
|
379
|
+
self.prefix = 'api'
|
|
380
|
+
|
|
381
|
+
@slowlette.route('/{*}')
|
|
382
|
+
def handle(self, request: slowlette.Request):
|
|
383
|
+
if len(request.path) > 0 and request.path[0] == self.prefix:
|
|
384
|
+
request.path = request.path[1:]
|
|
385
|
+
return slowlette.Response()
|
|
386
|
+
|
|
387
|
+
class App(slowlette.App):
|
|
388
|
+
def __init__(self):
|
|
389
|
+
super().__init__()
|
|
390
|
+
self.slowlette.add_middleware(DropPrefix('api'))
|
|
391
|
+
|
|
392
|
+
@slowlette.get('/hello')
|
|
393
|
+
def hello(self):
|
|
394
|
+
return 'Hello, Middleware.'
|
|
395
|
+
|
|
396
|
+
app = App()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
In this example, access to `/api/hello/` will be routed to the method bound to `/hello`, after the middleware that drops the `/api` prefix:
|
|
400
|
+
```bash
|
|
401
|
+
curl http://localhost:8000/api/hello
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The `@route()` decorator can be used to handle all the request methods, not specific to one such as `@get()`. The path rule of `/{*}` will capture all the URL.
|
|
405
|
+
|
|
406
|
+
The empty response returned here will be replaced with an aggregated responses from the subsequent handlers.
|
|
407
|
+
|
|
408
|
+
If a (sub)app is added by `app.add_middleware(subapp)`, the `subapp` handlers are inserted before the `app` handlers, whereas `app.include(subapp)` appends `subapp` handlers to `app`.
|
|
409
|
+
|
|
410
|
+
Multiple middlewares can be appended, and they will be processed in the order of appending, before the main `app` handlers and sub-app handlers are called.
|
|
411
|
+
|
|
412
|
+
A middleware that modifies responses can be implemented by returning a custom response with an overridden aggregation method (`Response.merge_response(self, response:Response)`).
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
### Ready-to-use Middlewares
|
|
416
|
+
|
|
417
|
+
#### HTTP Basic Authentication
|
|
418
|
+
`BasicAuthentication(auth_list: list[str])`
|
|
419
|
+
|
|
420
|
+
The `auth_list` is a list of keys, where each key looks like:
|
|
421
|
+
`api:$2a$12$D2.....`, which is the same key format used by Apache (type "2a").
|
|
422
|
+
A key can be generated by:
|
|
423
|
+
```
|
|
424
|
+
key = slowlette.BasicAuthentication.generate_key('api', 'slow')
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### File Server
|
|
428
|
+
`FileServer(filedir, *, prefix='', index_file=None, exclude=None, ext_allow=None, ext_deny=None)`
|
|
429
|
+
|
|
430
|
+
The file server handles GET requests to send back files stored in `filedir`. The request path, optionally with `prefix` that will be dropped, is the relative path from the `filedir`. For security reasons, file names cannot contain special characters other than a few selected ones (`_`, `-`, `+`, `=`, `,`, `.`, `:`), and the first letter of each path element must be an alphabet or digit. Also, the path cannot start with a Windows drive letter (like `c:`), even if Slowlette runs on non-Windows. POST and DELETE are not implemented.
|
|
431
|
+
|
|
432
|
+
- `filedir` (str): path to a filesystem directory
|
|
433
|
+
- `prefix` (str): URL path to bind this app (e.g., `/webfile`)
|
|
434
|
+
- `index_file` (str): index file when the path is empty (i.e., `/`)
|
|
435
|
+
- `exclude` (str): URL path not to be handled (e.g., prefix=`/app`, exclude=`/app/api`)
|
|
436
|
+
- `not_found_is_error` (bool): if true return an error response (404) if file does not exist, otherwise propagate
|
|
437
|
+
- `ext_allow` (list[str]): a list of file extensions to allow accessing
|
|
438
|
+
- `ext_deny` (list[str]): a list of file extensions not to allow accessing
|
|
439
|
+
|
|
440
|
+
### Custom Response Aggregation
|
|
441
|
+
A handler can make a user aggregator by returning an instance of a custom Response class with an overridden `merge_response()` method, as explained above.
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
import slowlette
|
|
445
|
+
|
|
446
|
+
class MyExclusiveApp:
|
|
447
|
+
class MyExclusiveResponse(slowlette.Response):
|
|
448
|
+
def __init__(self, status_code=0, *, content_type=None, content=None):
|
|
449
|
+
super().__init__(satus_code, conent_type=content_type, content=conent)
|
|
450
|
+
|
|
451
|
+
def merge_response(self, response:slowlette.Response)->None:
|
|
452
|
+
# example: do not merge the responses from the subsequent handlers
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
@slowlette.route('/hello')
|
|
456
|
+
def hello(self):
|
|
457
|
+
return self.MyExclusiveResponse(content='hello, there is no one else here.')
|
|
458
|
+
```
|
|
459
|
+
This method is useful if the method returns a data structure that requires a certain way to merge other data.
|
|
460
|
+
|
|
461
|
+
In addition to that, a user app class can override a method to aggregate all the individual responses from all the handlers within the class, to provide full flexibility. To do this, make a custom `Router` with an overridden `merge_responses()` method:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
import slowlette
|
|
465
|
+
|
|
466
|
+
class MyRouter(slowlette.Router):
|
|
467
|
+
def __init__(self, app):
|
|
468
|
+
super().__init__(app)
|
|
469
|
+
|
|
470
|
+
def merge_responses(self, responses: list[Response]) -> Response:
|
|
471
|
+
response = Response()
|
|
472
|
+
for r in responses:
|
|
473
|
+
response = .... # aggregate responses here
|
|
474
|
+
return response
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Then use this as a `slowlette` of the user app:
|
|
478
|
+
```python
|
|
479
|
+
class MyApp(slowlette.App):
|
|
480
|
+
def __init__(self):
|
|
481
|
+
self.slowlette = MyRouter(self)
|
|
482
|
+
super().__init__()
|
|
483
|
+
```
|
|
484
|
+
Calling `super().__init__()` later is a little bit more efficient, as it does not replace `self.slowlette` if it is already defined.
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
### Basic Authentication
|
|
488
|
+
```python
|
|
489
|
+
import slowlette
|
|
490
|
+
|
|
491
|
+
class App(slowlette.App):
|
|
492
|
+
@slowlette.get('/hello')
|
|
493
|
+
def hello(self):
|
|
494
|
+
return 'hello, how are you?'
|
|
495
|
+
|
|
496
|
+
# test authentication username and password
|
|
497
|
+
# once generated, store the key separately
|
|
498
|
+
key = slowlette.BasicAuthentication.generate_key(username='api', password='slow')
|
|
499
|
+
|
|
500
|
+
app = App()
|
|
501
|
+
app.slowlette.add_middleware(slowlette.BasicAuthentication(auth_list=[key]))
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
If HTTP is used, nothing will be returned, as the access is denied. (Add `-v` option to see details.)
|
|
505
|
+
```bash
|
|
506
|
+
curl http://localhost:8000/hello -v
|
|
507
|
+
```
|
|
508
|
+
```text
|
|
509
|
+
...
|
|
510
|
+
> GET /hello HTTP/1.1
|
|
511
|
+
> Host: localhost:8000
|
|
512
|
+
> User-Agent: curl/8.5.0
|
|
513
|
+
...
|
|
514
|
+
< HTTP/1.1 401 Unauthorized
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Now with the credentials:
|
|
518
|
+
```bash
|
|
519
|
+
curl http://api:slow@localhost:8000/hello
|
|
520
|
+
```
|
|
521
|
+
You will get the expected result:
|
|
522
|
+
```text
|
|
523
|
+
hello, how are you?
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
### HTTPS and HTTP/2
|
|
528
|
+
HTTP/2 is enabled only with TLS. Provide TLS key files to use HTTPS.
|
|
529
|
+
```python
|
|
530
|
+
if __name__ == '__main__':
|
|
531
|
+
app.run(ssl_keyfile='key.pem', ssl_certfile='cert.pem')
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### Memo: Generating a certificate
|
|
535
|
+
##### Temporary Self-Signed
|
|
536
|
+
```bash
|
|
537
|
+
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
|
|
538
|
+
```
|
|
539
|
+
##### Let's Encrypt if you have a domain:
|
|
540
|
+
```bash
|
|
541
|
+
sudo apt install certbot
|
|
542
|
+
sudo certbot certonly --standalone -d YOUR.DOMAIN.NAME
|
|
543
|
+
```
|
|
544
|
+
This will create files under `/etc/letsencript/live/YOUR.DOMAIN.NAME`
|
|
545
|
+
|
|
546
|
+
- private key: `privkey.pem`
|
|
547
|
+
- server certificate: `cert.pem`
|
|
548
|
+
- full chain: `fullchain.pem`
|
|
549
|
+
|
|
550
|
+
### WSGI
|
|
551
|
+
In addition to ASGI, WSGI can be used. The `slowlette.WSGI(app)` function wraps the ASGI App (standard Slowlette App) and returns a WSGI app.
|
|
552
|
+
```python
|
|
553
|
+
# testapp.py
|
|
554
|
+
|
|
555
|
+
import slowlette
|
|
556
|
+
|
|
557
|
+
class App(slowlette.App):
|
|
558
|
+
@slowlette.get('/hello')
|
|
559
|
+
def hello(self):
|
|
560
|
+
return 'hello, how are you?'
|
|
561
|
+
|
|
562
|
+
app = App() # ASGI App
|
|
563
|
+
wsgi_app = slowlette.WSGI(app)
|
|
564
|
+
|
|
565
|
+
if __name__ == '__main__':
|
|
566
|
+
wsgi_app.run()
|
|
567
|
+
```
|
|
568
|
+
The script can be executed as an HTTP server with WSGI:
|
|
569
|
+
```bash
|
|
570
|
+
python3 ./testapp.py
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Or it can be used with any WSGI server:
|
|
574
|
+
```bash
|
|
575
|
+
gunicorn testapp:wsgi_app
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Note:
|
|
579
|
+
|
|
580
|
+
- Every HTTP request is handled sequentially, even with async handlers.
|
|
581
|
+
- A dedicated async event loop is created for each request.
|
|
582
|
+
Code that assumes the same event loop among requests (typical in DB connection pool) cannot be used.
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
## TODOs
|
|
586
|
+
- File templates
|
|
587
|
+
- App.mount('path', app)
|
|
588
|
+
- OpenAPI document generation
|
|
589
|
+
- GraphQL chain processing (thanks ChatGPT for the idea!)
|
|
590
|
+
|