Guide ===== In yli, all web applications are simply represented by a single root function that takes a :class:`.Request`, and returns a :class:`.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: .. code-block:: python3 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 :ref:`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: .. code-block:: $ 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 :meth:`.Request.respond` helper function. Change your request handler to: .. code-block:: python3 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: .. code-block:: $ 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: .. code-block:: $ 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 :class:`.Router`. The router can be used for routing certain paths to functions. .. code-block:: python3 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: .. code-block:: $ 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: .. code-block:: python3 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(...) .. code-block:: $ 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. .. code-block:: python3 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. .. code-block:: $ 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 :class:`.MethodMatcher`, a helper application function which matches up methods to your functions. .. code-block:: python3 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) .. code-block:: $ 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 :class:`.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 :class:`.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, :class:`.PathParam` exists. .. code-block:: python3 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 :class:`.Chain`. .. code-block:: python3 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 :class:`.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 :meth:`.print_request` to print the result of all responses, you can chain it after your main application function: .. code-block:: python3 rooter = Router().route(...) app = Chain(rooter).then(print_response) await Webserver(..., app).run() Pre-conditions -------------- Whilst :class:`.Chain` can be used to transform requests/responses appropriately, it can't be used to require certain parts of an input. That is where :class:`.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. .. code-block:: python3 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.