Routing¶
Lilya has a simple but highly effective routing system capable of handling from simple routes to the most complex.
Using an enterprise application as example, the routing system surely will not be something simple with 20 or 40 direct routes, maybe it will have 200 or 300 routes where those are split by responsabilities, components and packages and imported also inside complex design systems. Lilya handles with those cases without any kind of issues at all.
Router¶
The Router is the main object that links the whole Lilya to the Path, WebSocketPath and Include.
Router class¶
The router class is composed by many attributes that are by default populated within the application but Lilya also allows to add extra custom routers as well but another way is to add a ChildLilya application.
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path
async def create(request: Request):
await request.json()
...
app = Lilya(
routes=[
Path(
"/create",
handler=create,
methods=["POST"],
)
],
)
The main Router
class is instantiated within the Lilya
application with the given routes and the application
starts.
Parameters¶
When creating a Path or WebSocketPath function handler, you have two ways of getting the path parameters.
- Lilya auto discovers and injects them for you.
- You get them from the request object.
Auto discovering the parameters¶
This is probably the easiest and simplest way.
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Include, Path
def update(item_id: int):
return item_id
def another_update(request: Request):
item_id = request.path_params["item_id"]
return JSONResponse({"Success", {item_id}})
app = Lilya(
routes=[
Include(
"/update",
routes=[
Path(
"/update/partial/{item_id:int}",
handler=update,
methods=["PATCH"],
),
Path(
path="/last/{item_id:int}",
handler=another_update,
methods=["PATCH"],
),
],
)
]
)
The customer_id
declared in the path
was also declared in the function handler allowing Lilya
to inject the values found by order from the path parameters for you.
From the request
path parameters.¶
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path
def update(request: Request):
item_id = request.path_params["item_id"]
return JSONResponse({"Success", {item_id}})
app = Lilya(
routes=[
Path(
"/update/{item_id:int}",
handler=update,
methods=["PUT"],
),
]
)
The customer_id
declared in the path
was obtained by accessing the request
object.
Custom Router¶
Let's assume there are specific customer submodules inside a customers
dedicated file.
There are two way of separating the routes within the application, using Include,
a ChildLilya or by creating another router. Let's focus on the latter.
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path, Router
async def create(request: Request):
await request.json()
return JSONResponse({"created": True})
async def get_customer(customer_id: int):
return JSONResponse({"created": customer_id})
router = Router(
path="/customers",
routes=[
Path(
"/{customer_id:int}",
handler=get_customer,
),
Path(
"/create",
handler=create,
methods=["POST"],
),
],
)
Above you create the /application/apps/routers/customers.py
with all the information you need. It does not need
to be in one file, you can have a entirely seperate package just to manage the customer, it is up to you.
Now you need to add the new custom router into the main application.
from lilya.apps import Lilya
from lilya.routing import Include
app = Lilya(
routes=[
Include(
"/",
app=...,
)
]
)
This simple and your router is added to the main Lilya application.
ChildLilya Application¶
What is this? We call it ChildLilya
but in fact is simply Lilya but under a different name mostly for
visualisation purposes and for the sake of organisation.
Check
Using ChildLilya
or Lilya
is exactly the same thing, it is only if you prefer to create a
sub application
and you prefer to use a different class instead of Lilya
to make it more organised.
How does it work¶
Let's use the same example used in the custom routers with the customers specific routes and rules.
from lilya.apps import ChildLilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path
async def create(request: Request) -> JSONResponse:
await request.json()
return JSONResponse({"created": True})
async def get_customer() -> JSONResponse:
return JSONResponse({"created": True})
router = ChildLilya(
routes=[
Path("/{customer_id:int}", handler=get_customer),
Path("/create", handler=create, methods=["POST"]),
],
include_in_schema=...,
)
Since the ChildLilya
is a representation of a Lilya class, we can pass
the otherwise limited parameters in the custom router and all the parameters available to
Lilya.
You can add as many ChildLilya
as you desire, there are no limits.
Now in the main application:
from apps.routers.customers import router as customers_router
from lilya.apps import Lilya
from lilya.routing import Include
app = Lilya(
routes=[
Include("/customers", app=customers_router),
]
)
Adding nested applications
from apps.routers.clients import router as clients_router
from apps.routers.customers import router as customers_router
from apps.routers.restrict import router as restrict_router
from lilya.apps import Lilya
from lilya.routing import Include
app = Lilya(
routes=[
Include("/customers", app=customers_router),
Include(
"/api/v1",
routes=[
Include("/clients", clients_router),
Include("/restrict", routes=[Include("/access", restrict_router)]),
],
),
]
)
The example above, it is showing that you could even add the same application within nested includes and for each
include you can add specific unique permissions and middlewares which are available on each
instance of the Include
. The options are endeless.
Note
In terms of organisation, ChildLilya
has a clean approach to the isolation of responsabilities and allow
treating every individual module separately and simply adding it in to the main application
in the form of Include.
Tip
Treat the ChildLilya
as an independent Lilya
instance.
Check
When adding a ChildLilya
or Lilya
application, don't forget to add the unique path to the base
Include
, this way you can assure the routes are found properly.
Utils¶
The Router
object has some available functionalities that can be useful.
add_route()¶
from lilya.apps import Lilya
app = Lilya()
app.add_route(
path=...,
methods=...,
name=...,
middleware=...,
permissions=...,
include_in_schema=...,
)
Parameters¶
- path - The path for the child lilya.
- name - Name of the route.
- handler - The function handler.
- methods - The available http verbs for the path.
- include_in_schema - If route should be added to the OpenAPI Schema
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions.
add_websocket_route()¶
from lilya.apps import Lilya
app = Lilya()
app.add_websocket_route(
path=...,
handler=...,
name=...,
middleware=...,
permissions=...,
)
Parameters¶
- path - The path for the child lilya.
- name - Name of the route.
- handler - The function handler.
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions.
add_child_lilya()¶
from lilya.apps import ChildLilya, Lilya
from lilya.routing import Path
async def home() -> str:
return "home"
child = ChildLilya(
routes=[
Path("/", handler=home, name="view"),
]
)
app = Lilya()
app.add_child_lilya(
path="/child",
child=child,
name=...,
middleware=...,
permissions=...,
include_in_schema=...,
deprecated=...,
)
Parameters¶
- path - The path for the child lilya.
- child - The ChildLilya instance.
- name - Name of the route.
- handler - The function handler.
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions. - include_in_schema - Boolean if this ChildLilya should be included in the schema.
- deprecated - Boolean if this ChildLilya should be marked as deprecated.
Path¶
The object that connects and builds the application urls or paths. It maps the function handler with the application routing system
Parameters¶
- path - The path for the child lilya.
- name - Name of the route.
- handler - The function handler.
- methods - The available http verbs for the path.
- include_in_schema - If route should be added to the OpenAPI Schema
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions. - deprecated - Boolean if this ChildLilya should be marked as deprecated.
from lilya.apps import Lilya, Request
from lilya.routing import Path
async def homepage(request: Request) -> str:
return "Hello, home!"
app = Lilya(
routes=[
Path(
handler=homepage,
)
]
)
WebSocketPath¶
Same principle as Path with one particularity. The websockets are async
.
Parameters¶
- path - The path for the child lilya.
- name - Name of the route.
- handler - The function handler.
- include_in_schema - If route should be added to the OpenAPI Schema
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions. - deprecated - Boolean if this ChildLilya should be marked as deprecated.
from lilya.apps import Lilya
from lilya.routing import WebSocketPath
from lilya.websockets import WebSocket
async def world_socket(websocket: WebSocket) -> None:
await websocket.accept()
msg = await websocket.receive_json()
assert msg
assert websocket
await websocket.close()
app = Lilya(
routes=[
WebSocketPath(
"/{path_param:str}",
handler=world_socket,
),
]
)
Include¶
Includes are unique to Lilya, powerful and with more control and allows:
- Scalability without issues.
- Clean routing design.
- Separation of concerns.
- Separation of routes.
- Reduction of the level of imports needed through files.
- Less human lead bugs.
Warning
Includes DO NOT take path parameters. E.g.: Include('/include/{id:int}, routes=[...])
.
Include and application¶
This is a very special object that allows the import of any routes from anywhere in the application.
Include
accepts the import via namespace
or via routes
list but not both.
When using a namespace
, the Include
will look for the default route_patterns
list in the imported
namespace (object) unless a different pattern
is specified.
The patten only works if the imports are done via namespace
and not via routes
object.
Parameters¶
- path - The path for the child lilya.
- app - An application can be anything that is treated as an ASGI application. The
app
can be an ASGI related app of a string<dotted>.<module>
location of the app. - routes - A global
list
of lilya routes. Those routes may vary and those can bePath
,WebSocketPath
or even anotherInclude
. - namespace - A string with a qualified namespace from where the URLs should be loaded.
- pattern - A string
pattern
information from where the urls shall be read from. - name - Name of the Include.
- permissions - A list of permissions to serve the application incoming requests (HTTP and Websockets).
- middleware - A list of middleware to run for every request.
- exception handlers - A dictionary of exception types (or custom exceptions) and the handler
functions on an application top level. Exception handler callables should be of the form of
handler(request, exc) -> response
and may be be either standard functions, or async functions. - include_in_schema - If route should be added to the OpenAPI Schema
- deprecated - Boolean if this
Include
should be marked as deprecated.
from lilya.routing import Include
route_patterns = [
Include("/", namespace="myapp.accounts.urls"),
]
from myapp.accounts.urls import route_patterns
from lilya.routing import Include
route_patterns = [
Include("/", routes=route_patterns),
]
This is an alternative of loading the app via string
import instead
of passing the object directly.
from lilya.routing import Include
# There is an app in the location `myapp.asgi_or_wsgi.apps.child_lilya`
route_patterns = [
Include(
"/child",
app="myapp.asgi_or_wsgi.apps.child_lilya",
),
]
Using a different pattern¶
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.websockets import WebSocket
async def update_product(request: Request, product_id: int):
data = await request.json()
return {"product_id": product_id, "product_name": data["name"]}
async def home():
return JSONResponse({"detail": "Hello world"})
async def another(request: Request):
return {"detail": "Another world!"}
async def world_socket(websocket: WebSocket):
await websocket.accept()
msg = await websocket.receive_json()
assert msg
assert websocket
await websocket.close()
from lilya.routing import Path, WebSocketPath
from .views import another, home, update_product, world_socket
my_urls = [
Path(
"/product/{product_id}",
handler=update_product,
methods=["PUT"],
),
Path("/", handler=home),
Path("/another", handler=another),
WebSocketPath(
"/{path_param:str}",
handler=world_socket,
),
]
from lilya.routing import Include
route_patterns = [
Include(
"/",
namespace="myapp.accounts.urls",
pattern="my_urls",
),
]
Include and application instance¶
The Include
can be very helpful mostly when the goal is to avoid a lot of imports and massive list
of objects to be passed into one single object. This can be particularly useful to make a clean start
Lilya object as well.
Example:
from lilya.routing import Include
route_patterns = [
Include(
"/",
namespace="myapp.accounts.urls",
pattern="my_urls",
)
]
from lilya.apps import Lilya
from lilya.routing import Include
app = Lilya(
routes=[
Include("/", namespace="src.urls"),
]
)
Nested Routes¶
When complexity increses and the level of routes increases as well, Include
allows nested routes in a clean fashion.
from lilya.apps import Lilya
from lilya.routing import Include, Path
async def me() -> None: ...
app = Lilya(
routes=[
Include(
"/",
routes=[
Path(
path="/me",
handler=me,
)
],
)
]
)
from lilya.apps import Lilya
from lilya.routing import Include, Path
async def me() -> None: ...
app = Lilya(
routes=[
Include(
"/",
routes=[
Include(
"/another",
routes=[
Include(
"/multi",
routes=[
Include(
"/nested",
routes=[
Include(
"/routing",
routes=[
Path(path="/me", handler=me),
Include(
path="/imported",
namespace="myapp.routes",
),
],
)
],
)
],
)
],
)
],
)
]
)
Include
supports as many nested routes with different paths and Includes as you
desire to have. Once the application starts, the routes are assembled and it will immediatly available.
Nested routes also allows all the functionalities on each level, from middleware and permissions.
Application routes¶
Warning
Be very careful when using the Include
directly in the Lilya(routes[]), importing without a path
may incur
in some routes not being properly mapped.
Only applied to the application routes:
If you decide to do this:
from lilya.apps import Lilya
from lilya.routing import Include
app = Lilya(
routes=[
Include(
"/",
namespace="src.urls",
name="root",
),
Include(
"/accounts",
namespace="accounts.v1.urls",
name="accounts",
),
]
)
Host¶
If you aim to utilize distinct routes for the same path contingent on the Host header, Lilya provides a solution.
It's important to note that the port is disregarded from the Host header during matching. For instance,
Host(host='example.com:8081', ...)
will be processed regardless of whether the Host header contains a port
different from 8081 (e.g., example.com:8083
, example.org
). Therefore, if the port is essential for path_for
purposes, you can explicitly specify it.
There are multiple approaches to establish host-based routes for your application.
from lilya.apps import Lilya
from lilya.routing import Host, Router
internal = Router()
api = Router()
external = Router()
routes = [Host("api.example.com", api, name="api")]
app = Lilya(routes=routes)
app.host("www.example.com", internal, name="intenal_site")
external_host = Host("external.example.com", external)
app.router.routes.append(external)
URL lookups can encompass host parameters, similar to how path parameters are included.
from lilya.requests import Request
from lilya.routing import Host, Include, Path, Router
def user(): ...
def detail(username: str): ...
routes = [
Host(
"{subdomain}.example.com",
name="sub",
app=Router(
routes=[
Include(
"/users",
name="users",
routes=[
Path("/", user, name="user"),
Path("/{username}", detail, name="detail"),
],
)
]
),
)
]
request = Request(...)
url = request.path_for("sub:users:user", username=..., subdomain=...)
url = request.path_for("sub:users:detail", subdomain=...)
Routes priority¶
The application routes in simple terms are simply prioritised. The incoming paths are matched agains each Path, WebSocketPath and Include in order.
In cases where more than one, let's say Path could match an incoming path, you should ensure that more specifc routes are listed before general cases.
Example:
from lilya.apps import Lilya
from lilya.routing import Path
async def user(): ...
async def active_user(): ...
# Don't do this: `/users/me`` will never match the incoming requests.
app = Lilya(
routes=[
Path(
"/users/{username}",
handler=user,
),
Path(
"/users/me",
handler=active_user,
),
]
)
# Do this: `/users/me` is tested first and both cases will work.
app = Lilya(
routes=[
Path(
"/users/me",
handler=active_user,
),
Path(
"/users/{username}",
handler=user,
),
]
)
Path parameters¶
Paths can use templating style for path components. The path params are only applied to Path and WebSocketPath and not applied to Include.
Remember that there are two ways of handling with the path parameters.
async def customer(customer_id: Union[int, str]) -> None:
...
async def floating_point(number: float) -> None:
...
Path("/customers/{customer_id}/example", handler=customer)
Path("/floating/{number:float}", handler=customer)
By default this will capture characters up to the end of the path of the next /
and it will become /customers/{customer_id}/example
.
Transformers can be used to modify what is being captured and the type of what is being captured. Lilya provides some default path transformers.
str
returns a string, and is the default.int
returns a Python integer.float
returns a Python float.uuid
returns a Pythonuuid.UUID
instance.path
returns the rest of the path, including any additional/
characters.datetime
returns the datetime.
As per standard, the transformers are used by prefixing them with a colon:
Path('/customers/{customer_id:int}', handler=customer)
Path('/floating-point/{number:float}', handler=floating_point)
Path('/uploaded/{rest_of_path:path}', handler=uploaded)
Custom transformers¶
If a need for a different transformer that is not defined or available, you can also create your own.
from __future__ import annotations
import ipaddress
from lilya.transformers import Transformer, register_path_transformer
class IPTransformer(Transformer[str]):
regex = r"((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$"
def transform(self, value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
return ipaddress.ip_address(value)
def normalise(self, value: ipaddress.IPv4Address | ipaddress.IPv6Address) -> str:
return str(value)
register_path_transformer("ipaddress", IPTransformer())
With the custom transformer created you can now use it.
Path('/network/{address:ipaddress}', handler=network)
Middleware, exception Handlers and permissions¶
Examples¶
The following examples are applied to Path, WebSocketPath and Include.
We will be using Path for it can be replaced by any of the above as it is common among them.
Middleware¶
As specified before, the middleware of a Path are read from top down, from the parent to the very handler and the same is applied to exception handlers, and permissions.
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.routing import Path
from lilya.types import ASGIApp
class RequestLoggingMiddlewareProtocol(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", kwargs: str = "") -> None:
self.app = app
self.kwargs = kwargs
class ExampleMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp") -> None:
self.app = app
async def homepage():
return {"page": "ok"}
app = Lilya(
routes=[
Path(
"/home",
handler=homepage,
middleware=[DefineMiddleware(ExampleMiddleware)],
)
],
middleware=[
DefineMiddleware(RequestLoggingMiddlewareProtocol),
],
)
The above example illustrates the various levels where a middleware can be implemented and because it follows an parent order, the order is:
- Default application built-in middleware.
RequestLoggingMiddlewareProtocol
.ExampleMiddleware
.
More than one middleware can be added to each list.
Exception Handlers¶
from lilya.apps import Lilya
from lilya.exceptions import InternalServerError, LilyaException, NotAuthorized
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path
async def http_lilya_handler(_: Request, exc: LilyaException):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def http_internal_server_error_handler(_: Request, exc: InternalServerError):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def http_not_authorized_handler(_: Request, exc: NotAuthorized):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def homepage() -> dict:
return {"page": "ok"}
app = Lilya(
routes=[
Path(
"/home",
handler=homepage,
exception_handlers={
NotAuthorized: http_not_authorized_handler,
InternalServerError: http_internal_server_error_handler,
},
)
],
exception_handlers={LilyaException: http_lilya_handler},
)
The above example illustrates the various levels where the exception handlers can be implemented and follows a parent order where the order is:
- Default application built-in exception handlers.
InternalServerError : http_internal_server_error_handler
.NotAuthorized: http_not_authorized_handler
.
More than one exception handler can be added to each mapping.
Permissions¶
Permissions are a must in every application. More on permissions and how to use them.
from lilya.apps import Lilya
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.routing import Include, Path
from lilya.types import ASGIApp, Receive, Scope, Send
class AllowAccess(PermissionProtocol):
def __init__(self, app: ASGIApp, *args, **kwargs):
super().__init__(app, *args, **kwargs)
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope=scope, receive=receive, send=send)
if "allow-access" in request.headers:
await self.app(scope, receive, send)
return
raise PermissionDenied()
class AdminAccess(PermissionProtocol):
def __init__(self, app: ASGIApp, *args, **kwargs):
super().__init__(app, *args, **kwargs)
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope=scope, receive=receive, send=send)
if "allow-admin" in request.headers:
await self.app(scope, receive, send)
return
raise PermissionDenied()
async def home():
return "Hello world"
async def user(user: str):
return f"Hello {user}"
# Via Path
app = Lilya(
routes=[
Path("/", handler=home),
Path(
"/{user}",
handler=user,
permissions=[
DefinePermission(AdminAccess),
],
),
],
permissions=[DefinePermission(AllowAccess)],
)
# Via Include
app = Lilya(
routes=[
Include(
"/",
routes=[
Path("/", handler=home),
Path(
"/{user}",
handler=user,
permissions=[
DefinePermission(AdminAccess),
],
),
],
permissions=[DefinePermission(AllowAccess)],
)
]
)
More than one permission can be added to each list.
Reverse Path lookups¶
Frequently, there is a need to generate the URL for a specific route, especially in scenarios where a redirect response is required.
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path
def user(): ...
app = Lilya(
routes=[
Path("/user", user, name="user"),
]
)
request = Request(...)
# Path lookup here
path = request.path_for("user")
The lookups also allow path parameters.
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Include, Path
def detail(): ...
app = Lilya(
routes=[
Include(
"/users",
routes=[
Path("/{username}", detail, name="detail"),
],
name="users",
)
]
)
request = Request(...)
# Path lookup here
path = request.path_for("users:detail", username=...)
If an Include
includes a name, subsequent submounts should employ a {prefix}:{name}
format for reverse Path lookups.
Using the reverse
¶
This is an alternative for the reverse path lookup. It can be particularly useful if you want to reverse a path in testing or in isolation.
Parameters¶
- name - The name given to the path.
- app - An ASGI application containing the routes. Useful for reversing paths on specific applications and/or testing. (Optional).
- path_params - A dictionary like object containing the parameters that should be passed in a given path. (Optional).
Using the reverse
, if no app
parameter is specified, it will automatically default to the application or application router,
which under normal circunstances, besides testing
, it is the expected behaviour.
from lilya.apps import Lilya
from lilya.compat import reverse
from lilya.routing import Path
def user(): ...
app = Lilya(
routes=[
Path("/user", user, name="user"),
]
)
# Path lookup here
path = reverse("user")
# Reverse with a specific app
# Path lookup here
path = reverse("user", app=app)
The reverse also allow path parameters.
from lilya.apps import Lilya
from lilya.compat import reverse
from lilya.requests import Request
from lilya.routing import Include, Path
def detail(): ...
app = Lilya(
routes=[
Include(
"/users",
routes=[
Path("/{username}", detail, name="detail"),
],
name="users",
)
]
)
request = Request(...)
# Path lookup here
path = reverse(
"users:detail",
path_params={"username": ...},
)
# Reverse with a specific app
# Path lookup here
path = reverse(
"users:detail",
app=app,
path_params={"username": ...},
)