Skip to content

Commit 5668f65

Browse files
authored
Merge pull request #51 from eadwinCode/execution_context
Execution context Refactor
2 parents 8f7e04d + 29cab0c commit 5668f65

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1072
-405
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+
```

docs/handling-response/response.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# Handling Responses
22

3-
## Define a response Schema
4-
53
**Ellar** allows you to define the schema of your responses both for validation and documentation purposes.
64

75
The response schema is defined on the HTTP method decorator and its applied

docs/overview/controllers.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ from ellar.core import ControllerBase, Request
124124
class DogsController(ControllerBase):
125125
@get()
126126
def get_all(self):
127-
assert isinstance(self.context.switch_to_request(), Request) # True
127+
assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
128128
return 'This action returns all dogs'
129129
...
130130
```
@@ -164,7 +164,7 @@ class DogsController(ControllerBase):
164164

165165
@get()
166166
def get_all(self):
167-
assert isinstance(self.context.switch_to_request(), Request) # True
167+
assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
168168
return 'This action returns all dogs'
169169
...
170170
```
@@ -201,7 +201,7 @@ class DogsController(ControllerBase):
201201

202202
@get()
203203
async def get_all(self):
204-
assert isinstance(self.context.switch_to_request(), Request) # True
204+
assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
205205
return 'This action returns all dogs'
206206
...
207207
```
@@ -292,7 +292,7 @@ class DogsController(ControllerBase):
292292

293293
@get()
294294
async def get_all(self, query: DogListFilter = Query()):
295-
assert isinstance(self.context.switch_to_request(), Request) # True
295+
assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
296296
return f'This action returns all dogs at limit={query.limit}, offset={query.offset}'
297297

298298
```

docs/overview/exception_handling.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ At the root project folder, create a file `custom_exceptions.py`,
9191
# project_name/custom_exceptions.py
9292
import typing as t
9393
from ellar.core.exceptions import IExceptionHandler
94-
from ellar.core.context import IExecutionContext
94+
from ellar.core.context import IHostContext
9595
from starlette.responses import Response
9696

9797

@@ -103,7 +103,7 @@ class MyCustomExceptionHandler(IExceptionHandler):
103103
exception_type_or_code = MyCustomException
104104

105105
async def catch(
106-
self, ctx: IExecutionContext, exc: MyCustomException
106+
self, ctx: IHostContext, exc: MyCustomException
107107
) -> t.Union[Response, t.Any]:
108108
app_config = ctx.get_app().config
109109
return app_config.DEFAULT_JSON_CLASS(
@@ -123,7 +123,7 @@ Let's create a handler for `MethodNotAllowedException` which, according to the H
123123
# project_name/apps/custom_exceptions.py
124124
import typing as t
125125
from ellar.core.exceptions import IExceptionHandler
126-
from ellar.core.context import IExecutionContext
126+
from ellar.core.context import IHostContext
127127
from ellar.core import render_template
128128
from starlette.responses import Response
129129
from starlette.exceptions import HTTPException
@@ -136,7 +136,7 @@ class MyCustomExceptionHandler(IExceptionHandler):
136136
exception_type_or_code = MyCustomException
137137

138138
async def catch(
139-
self, ctx: IExecutionContext, exc: MyCustomException
139+
self, ctx: IHostContext, exc: MyCustomException
140140
) -> t.Union[Response, t.Any]:
141141
app_config = ctx.get_app().config
142142
return app_config.DEFAULT_JSON_CLASS(
@@ -148,10 +148,10 @@ class ExceptionHandlerAction405(IExceptionHandler):
148148
exception_type_or_code = 405
149149

150150
async def catch(
151-
self, ctx: IExecutionContext, exc: HTTPException
151+
self, ctx: IHostContext, exc: HTTPException
152152
) -> t.Union[Response, t.Any]:
153153
context_kwargs = {}
154-
return render_template('405.html', request=ctx.switch_to_request(), **context_kwargs)
154+
return render_template('405.html', request=ctx.switch_to_http_connection().get_request(), **context_kwargs)
155155
```
156156
We have registered a handler for any `HTTP` exception with a `405` status code which we are returning a template `405.html` as a response.
157157

@@ -214,15 +214,15 @@ For example:
214214
# project_name/apps/custom_exceptions.py
215215
import typing as t
216216
from ellar.core.exceptions import IExceptionHandler, APIException
217-
from ellar.core.context import IExecutionContext
217+
from ellar.core.context import IHostContext
218218
from starlette.responses import Response
219219

220220

221221
class OverrideAPIExceptionHandler(IExceptionHandler):
222222
exception_type_or_code = APIException
223223

224224
async def catch(
225-
self, ctx: IExecutionContext, exc: APIException
225+
self, ctx: IHostContext, exc: APIException
226226
) -> t.Union[Response, t.Any]:
227227
app_config = ctx.get_app().config
228228
return app_config.DEFAULT_JSON_CLASS(

0 commit comments

Comments
 (0)