|
| 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