Lifespan¶
These are extremely common for the cases where you need to define logic that should be execute before the application starts and shuts down.
Before starting means the code (logic) will be executed once before starting receiving requests and the same is for the shutting down where the logic is also executed once after having managed, quite possibly, many requests.
This can be particularly useful for setting up your application resources and cleaning them up. These cycles cover the whole application.
Types of events¶
Currently Lilya supports on_startup, on_shutdown and lifespan.
Lilya on_startup and on_shutdown¶
If you pass an on_startup
and an on_shutdown
parameters intead of the lifespan
, Lilya
will automatically generate the async context manager for you and pass it to the lifespan
internally for you.
You can use on_startup/on_shutdown and lifespan but not both at the same time.
Tip
The shutdown
usually happens when you stop the application.
Functions¶
To define the functions to be used within the events, you can define a def
or async def
function. Lilya will know what to do with those and handle them for you.
How to use¶
Using these events is actually pretty much clear and simply. As mentioned before, there are two ways:
- Via on_startup and on-shutdown
- Via lifespan
Nothing like a use case to understand this better.
Let us assume you want to add a database into your application and because this can be costly, you also do not want to do it for every request, you then want this on an application level to be done on starting up and close on shutting down.
Let us then see how that it would look like using the current available events.
We will be using Saffier as example as it is also supoprted by Lilya.
on_startup and on_shutdown¶
Using the database use case defined above:
from saffier import Database, Registry
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
async def create_user(request: Request) -> None:
# Logic to create the user
await request.json()
...
app = Lilya(
routes=[Path("/create", handler=create_user)],
on_startup=[database.connect],
on_shutdown=[database.disconnect],
)
As you can see, when the application is starting up, we declared the database.connect()
to happen
as well as the database.disconnect()
on shutting down.
Lifespan¶
What happens if we use the example above and convert it to a lifespan event?
Well, this one although is also very simple, the way is assembled is slighly different.
To define the startup and shutown events, you will need a context manager to make it happen.
Let us see what does it mean in practical examples by changing the previous one to a lifespan
.
from contextlib import asynccontextmanager
from saffier import Database, Registry
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
async def create_user(request: Request) -> None:
# Logic to create the user
await request.json()
...
@asynccontextmanager
async def lifespan(app: Lilya):
# What happens on startup
await database.connect()
yield
# What happens on shutdown
await database.disconnect()
app = Lilya(
routes=[Path("/create", handler=create_user)],
lifespan=lifespan,
)
This is quite something to unwrap here. What is actually happening?
So, before you need to explicitly declare the on_startup
and on_shutdown
events in the
corresponding parameters in the Lilya application but with the lifespan
you do that in
one place only.
The first part before the yield
will be executed before the application starts and
the second part after the yield
will be executed after the application is finished.
The lifespan
function takes an app: Lilya
as a parameter because is then injected into
the application and the framework will know what to do with it.
Async context manager¶
As you can check, the lifespan functiom is decorated with an @asynccontextmanager
.
This is standard python for using a decorator
and this one in particular converts the lifespan
function into something called async context manager.
from contextlib import asynccontextmanager
from saffier import Database, Registry
from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
async def create_user(request: Request) -> None:
# Logic to create the user
await request.json()
...
@asynccontextmanager
async def lifespan(app: Lilya):
# What happens on startup
await database.connect()
yield
# What happens on shutdown
await database.disconnect()
app = Lilya(
routes=[Path("/create", handler=create_user)],
lifespan=lifespan,
)
In Python, a context manager is something that you can use with the with
keyword. One widely
used, for example, is with the open()
.
with open("file.txt", 'rb') file:
file.read()
When a context manager or async context manager is created like the example above, what it does it
that before entering the with
it will execute the code before the yield
and when exiting
the code block, it wille excute the code after the yield
.
The lifespan parameter of Lilya takes an async context manager which means we can ass our
new lifespan
async context manager directly to it.
Curiosity about async context managers¶
This section is out of the scope of the lifespan and events of Lilya and it is
for curiosity only. Please see the lifespan section as in the case of Lilya,
the way of declaring is different and an app: Lilya
parameter is always required.
General approach to async context managers¶
In general when using an async context the principle is the same as a normal context manager with
the key difference that we use async
before the with
.
Let use see an example still using the Saffier ORM.
Warning
Again, this is for general purposes, not for the use of the Lilya lifespan. That example how to use it is described in the lifespan section.
Using functions¶
from contextlib import asynccontextmanager
from saffier import Database, Registry
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
@asynccontextmanager
async def custom_context_manager():
await database.connect()
yield
await database.disconnect()
async with custom_context_manager() as async_context:
# Do something here
...
As you can see, we used the @asynccontextmanager
to transform our function into an async
context manager and the yield
is what manages the enter
and exit
behaviour.
Using Python classes¶
What if we were to build one async context manager with Python classes? Well this is actually even better as you can "visually" see and understand the behaviour.
Let us get back to the same example with Saffier ORM.
from saffier import Database, Registry
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
class DatabaseContext:
# Enter the async context manager
async def __aenter__(self):
await database.connect()
# Exit the async context manager
async def __aexit__(self):
await database.disconnect()
async with DatabaseContext() as async_context:
# Do something here
...
This example is actually very clear. The aenter
is the equivalent to what happens before the
yield
in our previous example and the aexit
is what happens after the yield
.
This time the @asynccontextmanager
wasn't necessary to decorate the class. The behaviour
implemented by that is done via aenter
and aexit
.
Async context managers can be a powerful tool in your application.