Skip to content

Commit e3d224d

Browse files
committed
Added execution context docs
1 parent fc5572e commit e3d224d

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed

docs/basics/execution-context.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
Execution context refers to the current context of execution, or the environment in which a specific piece of code is running.
2+
It contains information about the current request, the current response in the case of http connection, and the current state of the application.
3+
4+
The execution context is created automatically when a request is received, and it is passed along through the various layers of the application as the request is handled.
5+
This allows different components of the application like **exception handlers**, **functional middlewares** and **guards** to access information about the current request.
6+
7+
There are two class `HostContext` and `ExecutionContext` which provides set of methods and properties for accessing and manipulating the current context of execution.
8+
9+
10+
## HostContext
11+
The `HostContext` class provides a wrapper around `ASGI` app parameters (`scope`, `receive` and `send`) and provides some methods that allows you choosing the appropriate context(e.g., HTTP or WebSockets).
12+
13+
For example, the `catch()` method of an **exception handlers** is called with an IHostContext.
14+
15+
```python
16+
# project_name/apps/custom_exceptions.py
17+
import typing as t
18+
from ellar.core.exceptions import IExceptionHandler
19+
from ellar.core.context import IHostContext
20+
from starlette.responses import Response
21+
22+
class MyCustomException(Exception):
23+
pass
24+
25+
26+
class MyCustomExceptionHandler(IExceptionHandler):
27+
exception_type_or_code = MyCustomException
28+
29+
async def catch(
30+
self, ctx: IHostContext, exc: MyCustomException
31+
) -> t.Union[Response, t.Any]:
32+
33+
if ctx.get_type() == 'http':
34+
# do something that is only important in the context of regular HTTP requests (REST)
35+
pass
36+
elif ctx.get_type() == 'websocket':
37+
# do something that is only important in the context of regular Websocket
38+
pass
39+
40+
app_config = ctx.get_app().config
41+
return app_config.DEFAULT_JSON_CLASS(
42+
{'detail': str(exc)}, status_code=400,
43+
)
44+
45+
```
46+
47+
## Switching to other Contexts
48+
49+
Currently, in Ellar you can only switch between `http` and `websocket` context. And each context has `get_client` method that returns context session.
50+
51+
```python
52+
53+
async def catch(
54+
self, ctx: IHostContext, exc: MyCustomException
55+
) -> t.Union[Response, t.Any]:
56+
57+
if ctx.get_type() == 'http':
58+
# do something that is only important in the context of regular HTTP requests (REST)
59+
http_context = ctx.switch_to_http_connection()
60+
request: Request = http_context.get_request()
61+
response: Response = http_context.get_response()
62+
http_connection: HTTPConnection = http_context.get_client()
63+
64+
elif ctx.get_type() == 'websocket':
65+
# do something that is only important in the context of regular Websocket
66+
websocket_context = ctx.switch_to_websocket()
67+
websocket_session: WebSocket = websocket_context.get_client()
68+
69+
app_config = ctx.get_app().config
70+
return app_config.DEFAULT_JSON_CLASS(
71+
{'detail': str(exc)}, status_code=400,
72+
)
73+
```
74+
75+
!!! info
76+
Its good to note that you can't switch to a context that does not match the current context type.
77+
Always use the `.get_type()` to verify the type before switching.
78+
79+
### IHostContext Properties
80+
Important properties of `HostContext`
81+
82+
- `get_service_provider`: returns current service provider using in handling the request
83+
- `get_app`: returns current application instance
84+
- `get_type`: gets scope type `http`, `websocket`
85+
- `get_args`: returns `scope`, `receive` and `send` ASGI parameters
86+
- `switch_to_http_connection`: returns `HTTPConnectionHost` instance
87+
- `switch_to_websocket`: returns `WebSocketConnectionHost` instance
88+
89+
```python
90+
class IHostContext(ABC):
91+
@abstractmethod
92+
def get_service_provider(self) -> "RequestServiceProvider":
93+
"""Gets RequestServiceProvider instance"""
94+
95+
@abstractmethod
96+
def switch_to_http_connection(self) -> IHTTPConnectionHost:
97+
"""Returns HTTPConnection instance"""
98+
99+
@abstractmethod
100+
def switch_to_websocket(self) -> IWebSocketConnectionHost:
101+
"""Returns WebSocket instance"""
102+
103+
@abstractmethod
104+
def get_app(self) -> "App":
105+
"""Gets application instance"""
106+
107+
@abstractmethod
108+
def get_type(self) -> str:
109+
"""returns scope type"""
110+
111+
@abstractmethod
112+
def get_args(self) -> t.Tuple[TScope, TReceive, TSend]:
113+
"""returns all args passed to asgi function"""
114+
```
115+
116+
`IHTTPConnectionHost` and `IWebSocketConnectionHost` has some methods that maybe of interest.
117+
118+
Here are methods for `IHTTPConnectionHost`:
119+
120+
```python
121+
class IHTTPConnectionHost(ABC):
122+
@abstractmethod
123+
def get_response(self) -> Response:
124+
"""Gets response"""
125+
126+
@abstractmethod
127+
def get_request(self) -> Request:
128+
"""Returns Request instance"""
129+
130+
@abstractmethod
131+
def get_client(self) -> HTTPConnection:
132+
"""Returns HTTPConnection instance"""
133+
```
134+
135+
Following are the methods for `IWebSocketConnectionHost`:
136+
137+
```python
138+
class IWebSocketConnectionHost(ABC):
139+
@abstractmethod
140+
def get_client(self) -> WebSocket:
141+
"""Returns WebSocket instance"""
142+
```
143+
144+
## ExecutionContext Class
145+
`ExecutionContext` extends `HostContext` and provides extra information like `Controller` class and controller `function`
146+
that will handler the current request.
147+
148+
```python
149+
import typing
150+
from ellar.core import HostContext, ControllerBase
151+
152+
153+
class ExecutionContext(HostContext):
154+
# Returns the type of the controller class which the current handler belongs to.
155+
def get_class(self) -> typing.Type[ControllerBase]:
156+
pass
157+
158+
# Returns a reference to the handler (method) that will be handler the current request.
159+
def get_handler(self) -> typing.Callable:
160+
pass
161+
162+
```
163+
164+
These extra information are necessary for reading `metadata` properties set on controllers or the route handler function.
165+
166+
### How to access the current execution context
167+
You can access the current execution context using the `Context()` function.
168+
This decorator can be applied to a parameter of a controller or service method,
169+
and it will inject the current `ExecutionContext` object into the method.
170+
171+
For example, consider the following controller method:
172+
```python
173+
from ellar.common import Context, get, Controller
174+
175+
@Controller('/users')
176+
class UserController:
177+
@get('/{user_id}')
178+
async def get_user(self, user_id: str, ctx=Context()):
179+
# Use the ctx object to access the current execution context
180+
res = ctx.switch_to_http_connection().get_response()
181+
res.status_code = 200
182+
res.body = f"Request to get user with id={user_id}".encode("utf-8")
183+
scope, receive, send = ctx.get_args()
184+
await res(scope, receive, send) # sends response
185+
186+
```
187+
188+
In this example, the `get_user` method is decorated with the `@get` decorator to handle a GET request to the /users/:id route. The `Context()` function is applied to the second parameter of the method, which will inject the current `ExecutionContext` object into the method.
189+
190+
Once you have access to the `ExecutionContext` object, you can use its methods and properties to access information about the current request.
191+
192+
## Reflector and Metadata
193+
Ellar provides the ability to attach **custom metadata** to route handlers through the `@set_metadata()` decorator.
194+
We can then access this metadata from within our class to make certain decisions.
195+
196+
```python
197+
# project_name/apps/dogs/controllers.py
198+
199+
from ellar.common import Body, Controller, post, set_metadata
200+
from ellar.core import ControllerBase
201+
from .schemas import CreateDogSerializer, DogListFilter
202+
203+
204+
@Controller('/dogs')
205+
class DogsController(ControllerBase):
206+
@post()
207+
@set_metadata('role', ['admin'])
208+
async def create(self, payload: CreateDogSerializer = Body()):
209+
result = payload.dict()
210+
result.update(message='This action adds a new dog')
211+
return result
212+
```
213+
214+
With the construction above, we attached the `roles` metadata (roles is a metadata key and ['admin'] is the associated value)
215+
to the `create()` method. While this works, it's not good practice to use `@set_metadata()` directly in your routes.
216+
Instead, create your own decorators, as shown below:
217+
218+
```python
219+
# project_name/apps/dogs/controllers.py
220+
import typing
221+
from ellar.common import Body, Controller, post, set_metadata
222+
from ellar.core import ControllerBase
223+
from .schemas import CreateDogSerializer, DogListFilter
224+
225+
226+
def roles(*_roles: str) -> typing.Callable:
227+
return set_metadata('roles', list(_roles))
228+
229+
230+
@Controller('/dogs')
231+
class DogsController(ControllerBase):
232+
@post()
233+
@roles('admin', 'is_staff')
234+
async def create(self, payload: CreateDogSerializer = Body()):
235+
result = payload.dict()
236+
result.update(message='This action adds a new dog')
237+
return result
238+
```

0 commit comments

Comments
 (0)