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.py
Now open http://localhost:8000/hello
in your browser, or
run:
curl http://localhost:8000/hello
And you should see the response:
Hello, Slowlette!
Like FastAPI, Slowlette App object implements ASGI, and any external ASGI server can be used.
uvicorn testapp:app
The 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?'
= slowlette.App(MyApp())
app
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
= slowlette.Slowlette()
app
@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
= doc.get('item', 'nothing')
item 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):
= doc.get('item', 'nothing') # this will make a runtime error if the body is not dict
item 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
implementedThe 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:
= await websocket.receive_text()
message 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 | jq
You 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[1:]
request.path 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/hello
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.
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():
= MyExclusiveResponse()
response 'hello, there is no one else here.')
response.append(return response
This 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(responses: list[Response]) -> Response:
= Response()
response for r in responses:
= .... # aggregate responses here
response return response
Then 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
= slowlette.BasicAuthentication.generate_key(username='api', password='slow')
key
= App()
app =[key])) app.add_middleware(slowlette.BasicAuthentication(auth_list
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/hello
You 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__':
='key.pem', ssl_certfile='cert.pem') app.run(ssl_keyfile
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
sudo apt install certbot
sudo certbot certonly --standalone -d YOUR.DOMAIN.NAME
This will create files under
/etc/letsencript/live/YOUR.DOMAIN.NAME
privkey.pem
cert.pem
fullchain.pem
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.
# testapp.py
import slowlette
class App(slowlette.App):
@slowlette.get('/hello')
def hello(self):
return 'hello, how are you?'
= App() # ASGI App
app = slowlette.WSGI(app)
wsgi_app
if __name__ == '__main__':
wsgi_app.run()
The script can be executed as an HTTP server with WSGI:
python3 ./testapp.py
Or it can be used with any WSGI server:
gunicorn testapp:wsgi_app
Note: