Guide¶
In yli, all web applications are simply represented by a single root function that takes a
Request
, and returns a Response
. Any complexity such as routing can be
represented as these functions calling other functions.
These functions are called application functions
. You can also have a class be an application
function by giving an async def __call__(self, req: Request) -> Response:`
Your first app¶
A simple hello world app would look like the following:
async def my_app(r: Request) -> Response:
print("Hello, world!")
async def main():
webserver = Webserver([BindInfo(7777)], my_app)
await webserver.run()
trio.run(main)
Note
For more information on how the bundled webserver works, see Serving.
When running this, you’ll get a nice Hello, world!
message in the console when you GET /.
But, on the client side, you’ll get:
$ http GET http://127.0.0.1:7777/
HTTP/1.1 500 Internal Server Error
content-length: 41
date: Sun, 17 Nov 2019 03:08:35 GMT
server: yli (python/3.8.0)
Response function didn't return anything!
Your response function needs to always return a response. You can do this with the
Request.respond()
helper function. Change your request handler to:
async def my_app(r: Request) -> Response:
return r.respond(body="Hello, world!", status_code=200)
Now when you do a HTTP reequest, you’ll get a nice response:
$ http GET http://127.0.0.1:7777/
HTTP/1.1 200 OK
content-length: 13
date: Sun, 17 Nov 2019 03:12:37 GMT
server: yli (python/3.8.0)
Hello, world!
Routing¶
You may note that if you try any other route other than /
, you still get a plain
Hello, world!
response:
$ http GET http://127.0.0.1:7777/abc
HTTP/1.1 200 OK
content-length: 13
date: Sun, 17 Nov 2019 03:13:19 GMT
server: yli (python/3.8.0)
Hello, world!
This is because your function doesn’t care what is passed in - it’s a plain function. However,
yli includes some callables that do care what is passed in - such as the Router
. The
router can be used for routing certain paths to functions.
from yli.compose import Router
async def hello(request: Request) -> Response:
return request.respond(body="Hello!")
async def goodbye(request: Request) -> Response:
return request.respond(body="Goodbye!")
async def main():
router = Router()
# you could also chain these calls, router.route(...).route(...)
router.route("/hello", hello)
router.route("/goodbye", goodbye)
await Webserver([BindInfo(7777)], router).run()
Now your app will respond the appropriate routes with the right response:
$ http GET http://127.0.0.1:7777/hello
HTTP/1.1 200 OK
content-length: 6
date: Sun, 17 Nov 2019 03:17:48 GMT
server: yli (python/3.8.0)
Hello!
$ http GET http://127.0.0.1:7777/goodbye
HTTP/1.1 200 OK
content-length: 8
date: Sun, 17 Nov 2019 03:17:50 GMT
server: yli (python/3.8.0)
Goodbye!
$ http GET http://127.0.0.1:7777/unknown
HTTP/1.1 404 Not Found
content-length: 62
date: Sun, 17 Nov 2019 03:18:42 GMT
server: yli (python/3.8.0)
Couldn't match route (full route: /unknown, working: /unknown)
404s¶
To override 404s, just pass an application function as the no_route_fun
argument:
async def handle_no_route(req: Request) -> Response:
return req.respond(body="Not found! Go away!", status_code=404)
async def main():
router = Router(no_route_fun=handle_no_route).route(...)
$ http GET http://127.0.0.1:7777/unknown
HTTP/1.1 404 Not Found
content-length: 19
date: Sun, 17 Nov 2019 03:21:44 GMT
server: yli (python/3.8.0)
Not found! Go away!
Subrouting¶
Since a router counts as an application function, you can nest them easily.
async def xyz(request: Request) -> Response:
return request.respond(body="123")
async def main():
rooter = Router()
nested_router = Router().route("/xyz", xyz)
rooter.route("/abc", nested_router)
Now, requesting on /abc/xyz
will call your xyz function. Subrouters also have their own 404
handlers, so /abc/anything_else
will call the subrouter’s handler.
$ http GET http://127.0.0.1:7777/abc/xyz
HTTP/1.1 200 OK
content-length: 3
date: Sun, 17 Nov 2019 03:25:37 GMT
server: yli (python/3.8.0)
123
Method matching¶
You’ll notice in these last examples that your functions completely ignored methods. GET or POST
would both call the same function. Often, you don’t want that. Enter the MethodMatcher
,
a helper application function which matches up methods to your functions.
from yli.compose import MethodMatcher
async def abc_get(q: Request) -> Response:
return q.respond(body="I've been get!")
async def abc_post(request: Request) -> Response:
return request.respond(body="I've been posted!")
async def main():
# you can do method=fn in the constructor...
matcher = MethodMatcher(get=abc_get, post=abc_post)
# or chain calls:
matcher = MethodMatcher().match("get", abc_get).match("post", abc_post)
$ http GET http://127.0.0.1:7777/
HTTP/1.1 200 OK
content-length: 14
date: Sun, 17 Nov 2019 03:29:37 GMT
server: yli (python/3.8.0)
I've been get!
$ http POST http://127.0.0.1:7777/
HTTP/1.1 200 OK
content-length: 17
date: Sun, 17 Nov 2019 03:29:40 GMT
server: yli (python/3.8.0)
I've been posted!
$ http PUT http://127.0.0.1:7777/
HTTP/1.1 405 Method Not Allowed
content-length: 23
date: Sun, 17 Nov 2019 03:29:46 GMT
server: yli (python/3.8.0)
Method not allowed: PUT
Like with Router
, you can override the default 405 behaviour by passing
invalid_method_fun
to MethodMatcher
.
Since MethodMatcher
is an application fun, you can pass it to helpers like Router
to only allow specific methods on a route, for example.
Path parameters¶
Sometimes you might want a parameter in the URL path to be passed to you, for example an ID in a
RESTful API. For this, PathParam
exists.
from yli.compose import PathParam
async def print_userid(r: Request) -> Response:
print(r.path_params["userid"]
return r.respond(body="OK")
async def main():
router = Router()
userid_handler = PathParam("userid", print_userid) # you could pass a router to subroute!
router.route("/api/v1/user", userid_handler)
await Webserver(..., router).run()
Chaining application functions¶
It is possible to chain application functions together using Chain
.
from yli.compose import chain, print_response
chained = Chain(my_func).then(print_response)
But wait! How can print_request
be (Request) -> Response
if my_func
should return
Response
? I lied a bit - only one application function is allowed within a Chain
.
It is used to add request pre-processors or response post-processors instead. Each function
BEFORE the application function should be a (Request) -> Request
, and each function after it
should be a (Response) -> Response
.
For example, to use print_request()
to print the result of all responses, you can chain it
after your main application function:
rooter = Router().route(...)
app = Chain(rooter).then(print_response)
await Webserver(..., app).run()
Pre-conditions¶
Whilst Chain
can be used to transform requests/responses appropriately, it can’t be
used to require certain parts of an input. That is where ShortCircuit
comes in.
Functions passed to the .next()
of ShortCircuit
can either return a request (optionally,
transformed) or return a response to stop the chain early.
async def requires_header(r: Request) -> Union[Request, Response]:
if not "my-secret-header" in r.headers:
return r.response(body="Not allowed!", code=401)
return r
app = ShortCircuit().next(requires_headers).last(my_app_router)
Writing your own composers¶
yli has effectively zero magic - basically everything is an application function, so you can compose them relatively freely. To write your own composer, simply have it be an application function that takes another function, and call it (or maybe don’t!) and return the appropriate response objects.