Skip to content

Commit

Permalink
Improvements, documentation and others (#495)
Browse files Browse the repository at this point in the history
* Before and after request on websockets
* Add before and after request to settings defaults
* Reverse order on gateway after_request
* Update documentation
  • Loading branch information
tarsil authored Feb 16, 2025
1 parent 40bb043 commit 72934c1
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 12 deletions.
39 changes: 37 additions & 2 deletions docs/en/docs/lifespan-events.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Lifespan Events
# Lifespan Events & Request Lifecycle

These are extremely common for the cases where you need to define logic that should be execute
before the application starts and shuts down.
Expand Down Expand Up @@ -259,7 +259,42 @@ You can mix with different levels as well, for instance with an `Include`.

You get the point, don't you? This is also this simple.

#### Notes
### Call order

Like everything, it is important to understand the order of the calls since there is one.

Let us imagine we have the following:

1. An Esmerald object with `before_request` and `after_request`.
2. An Include with `before_request` and `after_request`.
3. A Gateway/WebSocketGateway with `before_request` and `after_request`.
4. A handler (get, post, put... websocket...) with `before_request` and `after_request`.

```shell
Esmerald:
Include:
Gateway/WebSocketGateway:
HTTPHandler/WebSocketHander
```

What is the order of calls? So, first with the `before_request`, it will call:

1. Esmerald
2. Include
3. Gateway/WebSocketGateway
4. Handler

Then the `after_request` does the reverse, which means:

1. Handler
2. Gateway/WebSocketGateway
3. Include
4. Esmerald

This makes sense because its the `incoming` and `outgoing` request lifecycle happening or as we
like to call *the boomerang effect*.

### Notes

The `before_request` and `after_request` cycles **are lists** of callables, which means that you
**can have multiple callables** within the same level and it will be called by the same order given
Expand Down
13 changes: 13 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ hide:

# Release Notes

### 3.6.7

### Added

- `before_request` and `after_request` WebSocketGateway handler added.
- `before_request` and `after_request` added as default to the settings. This was not required
as the settings loading system of Esmerald defaults values but this should be added to the settings
for consistency reasons of the framework.

### Changed

- Reverse order on Gateway `after_request`.

### 3.6.6

### Added
Expand Down
69 changes: 68 additions & 1 deletion esmerald/conf/global_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Union

from lilya.types import Lifespan
from pydantic import AnyUrl
Expand Down Expand Up @@ -433,6 +433,73 @@ class AppSettings(EsmeraldAPISettings):
"""
),
] = None
before_request: Annotated[
Union[Sequence[Callable[..., Any]], None],
Doc(
"""
A `list` of events that are trigger after the application
processes the request.
Read more about the [events](https://lilya.dev/lifespan/).
**Example**
```python
from edgy import Database, Registry
from esmerald import Esmerald, Request, Gateway, get
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
async def create_user(request: Request):
# Logic to create the user
data = await request.json()
...
app = Esmerald(
routes=[Gateway("/create", handler=create_user)],
after_request=[database.disconnect],
)
```
"""
),
] = None
after_request: Annotated[
Union[Sequence[Callable[..., Any]], None],
Doc(
"""
A `list` of events that are trigger after the application
processes the request.
Read more about the [events](https://lilya.dev/lifespan/).
**Example**
```python
from edgy import Database, Registry
from esmerald import Esmerald, Request, Gateway, get
database = Database("postgresql+asyncpg://user:password@host:port/database")
registry = Registry(database=database)
async def create_user(request: Request):
# Logic to create the user
data = await request.json()
...
app = Esmerald(
routes=[Gateway("/create", handler=create_user)],
after_request=[database.disconnect],
)
```
"""
),
] = None
root_path_in_servers: Annotated[
bool,
Doc(
Expand Down
19 changes: 18 additions & 1 deletion esmerald/routing/gateways.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ async def create_user(request: Request):
handler.after_request = []

for after in self.after_request:
handler.after_request.insert(0, after)
handler.after_request.append(after)

self._interceptors: Union[List["Interceptor"], "VoidType"] = Void
self.name = name
Expand Down Expand Up @@ -670,6 +670,23 @@ def __init__(
Since the default Lilya Route handler does not understand the Esmerald handlers,
the Gateway bridges both functionalities and adds an extra "flair" to be compliant with both class based views and decorated function views.
"""
self.before_request = before_request if before_request is not None else []
self.after_request = after_request if after_request is not None else []

if self.before_request:
if handler.before_request is None:
handler.before_request = []

for before in self.before_request:
handler.before_request.insert(0, before)

if self.after_request:
if handler.after_request is None:
handler.after_request = []

for after in self.after_request:
handler.after_request.append(after)

self._interceptors: Union[List["Interceptor"], "VoidType"] = Void
self.handler = cast("Callable", handler)
self.dependencies = dependencies or {}
Expand Down
18 changes: 18 additions & 0 deletions esmerald/routing/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2879,6 +2879,15 @@ async def handle_dispatch(self, scope: "Scope", receive: "Receive", send: "Send"
None
"""

for before_request in self.before_request:
if inspect.isclass(before_request):
before_request = before_request()

if is_async_callable(before_request):
await before_request(scope, receive, send)
else:
await run_in_threadpool(before_request, scope, receive, send)

if self.get_interceptors():
await self.intercept(scope, receive, send)

Expand All @@ -2901,6 +2910,15 @@ async def handle_dispatch(self, scope: "Scope", receive: "Receive", send: "Send"
else:
await fn(**kwargs)

for after_request in self.after_request:
if inspect.isclass(after_request):
after_request = after_request()

if is_async_callable(after_request):
await after_request(scope, receive, send)
else:
await run_in_threadpool(after_request, scope, receive, send)

async def get_kwargs(self, websocket: WebSocket) -> Any:
"""Resolves the required kwargs from the request data.
Expand Down
29 changes: 21 additions & 8 deletions tests/request_lifecycles/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,53 @@
async def before_path_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1
logger.info(f"Before path request: {app.state.app_request}")
logger.info(f"first before request: {app.state.app_request}")


async def after_path_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1

logger.info(f"After path request: {app.state.app_request}")
logger.info(f"first after request: {app.state.app_request}")


async def before_gateway_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1
logger.info(f"second before request: {app.state.app_request}")


async def after_gateway_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1

logger.info(f"second after request: {app.state.app_request}")


async def before_include_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1
logger.info(f"Before include request: {app.state.app_request}")
logger.info(f"third before request: {app.state.app_request}")


async def after_include_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1

logger.info(f"After include request: {app.state.app_request}")
logger.info(f"third after request: {app.state.app_request}")


async def before_app_request(scope, receive, send):
app = scope["app"]
app.state.app_request = 1
logger.info(f"Before app request: {app.state.app_request}")
logger.info(f"forth before request: {app.state.app_request}")


async def after_app_request(scope, receive, send):
app = scope["app"]
app.state.app_request += 1

logger.info(f"After app request: {app.state.app_request}")
logger.info(f"forth after request: {app.state.app_request}")


def test_all_layers_request():
Expand All @@ -62,8 +75,8 @@ async def index(request: Request) -> str:
Gateway(
"/",
handler=index,
before_request=[before_path_request],
after_request=[after_path_request],
before_request=[before_gateway_request],
after_request=[after_gateway_request],
)
],
before_request=[before_include_request],
Expand Down

0 comments on commit 72934c1

Please sign in to comment.