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.
# testapp.py
import slowlette
class App(slowlette.App):
@slowlette.get('/')
def home(self):
return 'feel at home'
@slowlette.get('/hello')
def say_hello(self):
return 'Hello, Slowlette!'
app = App()
if __name__ == '__main__':
app.run()app instance is created after
the binding is described. (Important for creating multiple handler
instances.)Like FastAPI/Flask, running the script above will start an HTTP server at port 8000.
python3 testapp.pyNow open http://localhost:8000/hello in your browser, or
run:
curl http://localhost:8000/helloAnd you should see the response:
Hello, Slowlette!
Like FastAPI, Slowlette App object implements ASGI, and any external ASGI server can be used.
uvicorn testapp:appThe base class, slowlette.App, has only three
attributes, listed below:
slowlette: Slowlette connection point__call__(self, scope, receive, send): ASGI entry
pointrun(self, port, **kwargs): Execution start pointGiven this small number of attributes, the likelihood of name conflicts with user classes should be minimal. Nevertheless, it is also possible to create a user class independently from Slowlette and pass it to Slowlette later.
import slowlette
class MyApp:
@slowlette.get('/hello')
def say_hello(self):
return 'hello, how are you?'
app = slowlette.App(MyApp())
if __name__ == '__main__':
app.run()Once you have created the app instance, the usage is
essentially the same as before.
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.
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.
import slowlette
app = slowlette.Slowlette()
@app.get('/hello')
def say_hello():
return 'hello, how are you?'
if __name__ == '__main__':
app.run()import slowlette
class App(slowlette.App):
@slowlette.get('/hello/{name}')
def hello(self, name:str):
return f'hello, {name}'
app = App()hello() method
in the example) will not be called.str for a text/plain replylist or dict for an
application/json replyslowlette.FileResponse object for file fetchingslowlette.Response object for full flexibilityNone if the request is not applicableimport slowlette
class App(slowlette.App):
@slowlette.get('/hello/{name}')
def hello(self, name:str, message:str='how are you', repeat:int=3):
return f'hello, {name}.' + f' {message}' * repeat
app = App()With ASGI, if the bound method is async, requests are
handled asynchronously.
import slowlette
class App(slowlette.App):
@slowlette.get('/hello')
async def hello(self, delay:float=0):
if delay > 0:
await asyncio.sleep(delay)
return f"hello after {delay} sleep"
app = App()import slowlette
class App(slowlette.App):
@slowlette.get('/echo/{*}')
def echo(self, path:list, query:dict):
return f'path: {path}, query: {query}'
app = App(){*} matches any path elements.list.dict.import slowlette
class App(slowlette.App):
@slowlette.get('/{*}')
def header(self, request:slowlette.Request):
return f'header: {request.headers}'
app = App()The Request object has the following attributes:
method (str): request method (GET
etc.)path (list[str]): URL pathquery (dict[str,str]): URL queryheaders (dict[str,str]): HTTP request header itemsbody (bytes): request bodyimport slowlette
class App(slowlette.App):
@slowlette.post('/hello/{name}')
def hello(self, name:str, message:bytes):
return f'hello, {name}. You sent me "{message.decode()}"'
app = App()bytes.import slowlette
class App(slowlette.App):
@slowlette.post('/hello/{name}')
def hello(self, name:str, doc:DictJSON): # if body in not a dict in JSON, a response 400 (Bad Request) will be returned
item = doc.get('item', 'nothing')
return f'hello, {name}. You gave me {item}'
app = App()dict in JSON and the
value is set to the (last) argument of a type
slowlette.DictJSON.doc) implements most common dict
operations, such as doc[key], key in doc,
for key in doc:, doc.get(value, default),
doc.items(), …doc.value() or dict(doc) to get a
native Python dict object.import slowlette
class App(slowlette.App):
@slowlette.post('/hello/{name}')
def hello(self, name:str, doc:JSON):
item = doc.get('item', 'nothing') # this will make a runtime error if the body is not dict
return f'hello, {name}. You gave me {item}'
app = App()slowlette.JSON.JSON.value() to get a value of the native Python
types (dict, list, str, …).dict(doc) or list(doc) to convert to
native Python dict or list.doc[key], key in doc,
for key in doc:, doc.get(value, default),
doc.items(), …doc[index],len(doc),
for v in doc:, …The structure is basically the same as FastAPI:
import slowlette
class App(slowlette.App):
@slowlette.on_event('startup')
async def startup(self):
print("SlowApp Server started")
@slowlette.on_event('shutdown')
async def shutdown(self):
print("SlowApp Server stopped")
app = App()startup and shutdown are
implemented.await-ing the
startup coroutine. If you want to start a task here, use
asyncio.create_task() or similar not to block this.The structure is basically the same as FastAPI:
import slowlette
class App(slowlette.App):
@slowlette.websocket('/ws')
async def ws_echo(self, websocket:slowlette.WebSocket):
await websocket.accept()
try:
while True:
message = await websocket.receive_text()
await websocket.send_text(f'Received: {message}')
except slowlette.ConnectionClosed:
print("WebSocket Closed")
app = App()import slowlette
class Fruit():
def __init__(self, name:str):
self.name = name
@slowlette.get('/hello')
def hello(self):
return [f'I am a {self.name}']
class App(slowlette.App):
def __init__(self):
super().__init__()
self.slowlette.include(Fruit('peach'))
self.slowlette.include(Fruit('melon'))
@slowlette.get('/hello')
def hello(self):
return ['Hello.']
app = App()By sending a request to the /hello endpoint, to which
three App instances are bound:
curl http://localhost:8000/hello | jqYou will get a result of three responses aggregated:
[
"Hello.",
"I am a peach",
"I am a melon"
]
list, they are combined with
append().dict, they are combined with
update() (recursively; sub-dicts with the same key will be
combined with update()).str, they are concatenated with a
new-line in between.None, it will not be included.None, a 404 status (Not Found)
is replied.The behavior is customizable by providing a user response aggregator, as explained below.
Instances of slowlette.Slowlette used to bind functions
in the example above can also be included or include other sub-apps.
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.
The middleware example below drops the path prefix of
/api from all the requests:
import slowlette
class DropPrefix:
def __init__(self, prefix):
self.prefix = 'api'
@slowlette.route('/{*}')
def handle(request: Request):
if len(request.path) > 0 and request.path[0] == self.prefix:
request.path = request.path[1:]
return Response()
class App(slowlette.App):
def __init__(self):
super().__init__()
self.slowlette.add_middleware(DropPrefix('api'))
@slowlette.get('/hello')
def hello(self):
return 'Hello, Middleware.'
app = App()In this example, access to /api/hello/ will be routed to
the method bound to /hello, after the middleware that drops
the /api prefix:
curl http://localhost:8000/api/helloThe @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.
The empty response returned here will be replaced with an aggregated responses from the subsequent handlers.
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.
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.
A middleware that modifies responses can be implemented by returning
a custom response with an overridden aggregation method
(Response.merge_response(self, response:Response)).
BasicAuthentication(auth_list: list[str])
The auth_list is a list of keys, where each key looks
like: api:$2a$12$D2....., which is the same key format used
by Apache (type “2a”). A key can be generated by:
key = slowlette.BasicAuthentication.generate_key('api', 'slow')
FileServer(filedir, *, prefix='', index_file=None, exclude=None, drop_exclude_prefix=False, ext_allow=None, ext_deny=None)
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.
filedir (str): path to a filesystem directoryprefix (str): URL path to bind this app (e.g.,
/webfile)index_file (str): index file when the path is empty
(i.e., /)exclude (str): URL path not to be handled (e.g.,
prefix=/app, exclude=/app/api)drop_exclude_prefix (bool): if True, the prefix is
removed from the requet path if the request is for an excluded path
(e.g., for exclude /api, the request path of
api/userlist becomes /userlist)ext_allow (list[str]): a list of file extensions to
allow accessingext_deny (list[str]): a list of file extensions not to
allow accessingA handler can make a user aggregator by returning an instance of a
custom Response class with an overridden merge_response()
method, as explained above.
import slowlette
class MyExclusiveApp:
class MyExclusiveResponse(Response):
def merge_response(self, response:Response)->None:
# example: do not merge the responses from the subsequent handlers
pass
@slowlette.route('/hello')
def hello(self):
response = self.MyExclusiveResponse()
response.append('hello, there is no one else here.')
return responseThis method is useful if the method returns a data structure that requires a certain way to merge other data.
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:
import slowlette
class MyRouter(slowlette.Router):
def merge_responses(self, responses: list[Response]) -> Response:
response = Response()
for r in responses:
response = .... # aggregate responses here
return responseThen use this as a slowlette of the user app:
class MyApp(slowlette.App):
def __init__(self):
self.slowlette = MyRouter()
super().__init__()Calling super().__init__() later is a little bit more
efficient, as it does not replace self.slowlette if it is
already defined.
import slowlette
class App(slowlette.App):
@slowlette.get('/hello')
def hello(self):
return 'hello, how are you?'
# test authentication username and password
# once generated, store the key separately
key = slowlette.BasicAuthentication.generate_key(username='api', password='slow')
app = App()
app.add_middleware(slowlette.BasicAuthentication(auth_list=[key]))If HTTP is used, nothing will be returned, as the access is denied.
(Add -v option to see details.)
curl http://localhost:8000/hello -v...
> GET /hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.5.0
...
< HTTP/1.1 401 Unauthorized
Now with the credentials:
curl http://api:slow@localhost:8000/helloYou will get the expected result:
hello, how are you?
HTTP/2 is enabled only with TLS. Provide TLS key files to use HTTPS.
if __name__ == '__main__':
app.run(ssl_keyfile='key.pem', ssl_certfile='cert.pem')openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pemsudo apt install certbot
sudo certbot certonly --standalone -d YOUR.DOMAIN.NAMEThis will create files under
/etc/letsencript/live/YOUR.DOMAIN.NAME
privkey.pemcert.pemfullchain.pemIn addition to ASGI, WSGI can be used. The
slowlette.WSGI(app) function wraps the ASGI App (standard
Slowlette App) and returns a WSGI app.
# testapp.py
import slowlette
class App(slowlette.App):
@slowlette.get('/hello')
def hello(self):
return 'hello, how are you?'
app = App() # ASGI App
wsgi_app = slowlette.WSGI(app)
if __name__ == '__main__':
wsgi_app.run()The script can be executed as an HTTP server with WSGI:
python3 ./testapp.pyOr it can be used with any WSGI server:
gunicorn testapp:wsgi_appNote: