Skip to content

Commit 51a1639

Browse files
authored
Merge pull request #46 from eadwinCode/exception_handler_feature
Exception Handler Feature
2 parents 876b3cb + c034e71 commit 51a1639

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

+1426
-644
lines changed

docs/index.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,29 @@
1515
Ellar is a lightweight ASGI framework for building efficient and scalable server-side python application.
1616
It supports both OOP (Object-Oriented Programming) and FP (Functional Programming)
1717

18-
Ellar is built around [Starlette]()(ASGI toolkit) which processes all the HTTP request and background tasks. Although, there is a high level
18+
Ellar is built around [Starlette](https://www.starlette.io/)(ASGI toolkit) which processes all the HTTP request and background tasks. Although, there is a high level
1919
of abstraction, some concepts of Starlette are still supported.
2020

2121
## Inspiration
22-
Ellar was heavily inspired by [NestJS]() in its simplicity in usage while managing complex project structures and application.
23-
It also adopted some concepts of [FastAPI]() in handling request parameters and data serialization with pydantic.
22+
Ellar was heavily inspired by [NestJS](https://docs.nestjs.com/) in its simplicity in usage while managing complex project structures and application.
23+
It also adopted some concepts of [FastAPI](https://fastapi.tiangolo.com/) in handling request parameters and data serialization with pydantic.
2424
With that said, the aim of Ellar focuses on high level of abstraction of framework APIs, project structures, architectures and speed of handling requests.
2525

2626
## Installation
27-
To get started, you need to scaffold a project using [Ellar-CLI]() toolkit. This is recommended for first-time user.
27+
To get started, you need to scaffold a project using [Ellar-CLI](https://eadwincode.github.io/ellar-cli/) toolkit. This is recommended for first-time user.
2828
The scaffolded project is more like a guide to project setup.
2929

3030
```shell
3131
$(venv) pip install ellar[standard]
3232
$(venv) ellar new project-name
3333
```
3434

35+
### NB:
36+
Some shells may treat square braces (`[` and `]`) as special characters. If that's the case here, then use a quote around the characters to prevent unexpected shell expansion.
37+
```shell
38+
pip install "ellar[standard]"
39+
```
40+
3541
### Py36 Support
3642
For python3.6 users,
3743
```shell

docs/overview/exception_filters.md

Whitespace-only changes.

docs/overview/exception_handling.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
2+
Ellar `ExceptionMiddleware` together with `ExceptionMiddlewareService` are responsible for processing all unhandled exception
3+
across the application and provides an appropriate user-friendly response.
4+
5+
```json
6+
{
7+
"status_code": 403,
8+
"detail": "Forbidden"
9+
}
10+
```
11+
12+
Default exceptions types Managed by default:
13+
14+
- **`HTTPException`**: Default exception class provided by `Starlette` for HTTP client
15+
- **`WebSocketException`**: Default websocket exception class also provided by `Starlette` for websocket connection
16+
- **`RequestValidationException`**: Request data validation exception provided by `Pydantic`
17+
- **`APIException`**: Custom exception created for typical REST API based application to provides more concept about the exception raised.
18+
19+
## **HTTPException**
20+
21+
The `HTTPException` class provides a base class that you can use for any
22+
handled exceptions.
23+
24+
* `HTTPException(status_code, detail=None, headers=None)`
25+
26+
## **WebSocketException**
27+
28+
You can use the `WebSocketException` class to raise errors inside WebSocket endpoints.
29+
30+
* `WebSocketException(code=1008, reason=None)`
31+
32+
You can set any code valid as defined [in the specification](https://tools.ietf.org/html/rfc6455#section-7.4.1).
33+
34+
## **APIException**
35+
As stated earlier, its an exception type for typical REST API based application. Its gives more concept to error and provides a
36+
simple interface for creating other custom exception need in your application.
37+
38+
For example,
39+
40+
```python
41+
from ellar.core.exceptions import APIException
42+
from starlette import status
43+
44+
class ServiceUnavailableException(APIException):
45+
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
46+
code = 'service_unavailable'
47+
48+
```
49+
!!!hint
50+
You should only raise `HTTPException` and `APIException` inside routing or endpoints. Middleware classes should instead just return appropriate responses directly.
51+
52+
Let's use this `ServiceUnavailableException` in our previous project.
53+
54+
For example, in the `DogsController`, we have a `get_all()` method (a `GET` route handler).
55+
Let's assume that this route handler throws an exception for some reason. To demonstrate this, we'll hard-code it as follows:
56+
57+
```python
58+
# project_name/apps/dogs/controllers.py
59+
60+
@get()
61+
def get_all(self):
62+
raise ServiceUnavailableException()
63+
64+
```
65+
Now, when you visit [http://127.0.0.1/dogs/](http://127.0.0.1/dogs/), you will get a JSON response.
66+
```json
67+
{
68+
"detail": "Service Unavailable"
69+
}
70+
```
71+
72+
We have a JSON response because Ellar has an exception handler for `APIException`. This handler can be change to return some different.
73+
We shall how to do that on Overriding Default Exception Handlers.
74+
75+
There is other error presentation available on `APIException` instance:
76+
- `.detail`: returns textual description of the error.
77+
- `get_full_details()`: returns both textual description and other information about the error.
78+
79+
```shell
80+
>>> print(exc.detail)
81+
Service Unavailable
82+
>>> print(exc.get_full_details())
83+
{'detail':'Service Unavailable','code':'service_unavailable', 'description': 'The server cannot process the request due to a high load'}
84+
```
85+
86+
## **Creating Custom Exception Handler**
87+
88+
To create an exception handler for your custom exception, you have to create a class that follow `IExceptionHandler` contract.
89+
90+
At the root project folder, create a file `custom_exceptions.py`,
91+
92+
```python
93+
# project_name/custom_exceptions.py
94+
import typing as t
95+
from ellar.core.exceptions import IExceptionHandler
96+
from ellar.core.context import IExecutionContext
97+
from starlette.responses import Response
98+
99+
100+
class MyCustomException(Exception):
101+
pass
102+
103+
104+
class MyCustomExceptionHandler(IExceptionHandler):
105+
exception_type_or_code = MyCustomException
106+
107+
async def catch(
108+
self, ctx: IExecutionContext, exc: MyCustomException
109+
) -> t.Union[Response, t.Any]:
110+
app_config = ctx.get_app().config
111+
return app_config.DEFAULT_JSON_CLASS(
112+
{'detail': str(exc)}, status_code=400,
113+
)
114+
115+
```
116+
- `exception_type_or_code`: defines the `exception class` OR `status code` to target when resolving exception handlers.
117+
- `catch()`: defines the handling code and response to be returned to the client.
118+
119+
### **Creating Exception Handler for status code**
120+
Let's create a handler for `MethodNotAllowedException` which, according to HTTP code is `405`.
121+
122+
```python
123+
# project_name/apps/custom_exceptions.py
124+
import typing as t
125+
from ellar.core.exceptions import IExceptionHandler
126+
from ellar.core.context import IExecutionContext
127+
from ellar.core import render_template
128+
from starlette.responses import Response
129+
from starlette.exceptions import HTTPException
130+
131+
class MyCustomException(Exception):
132+
pass
133+
134+
135+
class MyCustomExceptionHandler(IExceptionHandler):
136+
exception_type_or_code = MyCustomException
137+
138+
async def catch(
139+
self, ctx: IExecutionContext, exc: MyCustomException
140+
) -> t.Union[Response, t.Any]:
141+
app_config = ctx.get_app().config
142+
return app_config.DEFAULT_JSON_CLASS(
143+
{'detail': str(exc)}, status_code=400,
144+
)
145+
146+
147+
class ExceptionHandlerAction405(IExceptionHandler):
148+
exception_type_or_code = 405
149+
150+
async def catch(
151+
self, ctx: IExecutionContext, exc: HTTPException
152+
) -> t.Union[Response, t.Any]:
153+
context_kwargs = {}
154+
return render_template('405.html', request=ctx.switch_to_request(), **context_kwargs)
155+
```
156+
We have registered a handler for any `HTTPException` with status code `405` and we have chosen to return a template `405.html` as response.
157+
158+
!!!info
159+
Ellar will look for `405.html` in all registered module. So `dogs` folder, create a `templates` folder and add `405.html`.
160+
161+
The same way can create Handler for `500` error code.
162+
163+
164+
## **Registering Exception Handlers**
165+
We have successfully created two exception handlers `ExceptionHandlerAction405` and `MyCustomExceptionHandler` but they are not yet visible to the application.
166+
167+
- `config.py`: The config file holds manage application settings including `EXCEPTION_HANDLERS` fields which defines all custom exception handlers used in the application.
168+
169+
```python
170+
# project_name/config.py
171+
import typing as t
172+
from ellar.core import ConfigDefaultTypesMixin
173+
from ellar.core.exceptions import IExceptionHandler
174+
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405
175+
176+
class BaseConfig(ConfigDefaultTypesMixin):
177+
EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [
178+
MyCustomExceptionHandler(),
179+
ExceptionHandlerAction405()
180+
]
181+
```
182+
- `application instance`: You can also add exception through `app` instance.
183+
184+
```python
185+
# project_name/server.py
186+
187+
import os
188+
189+
from ellar.constants import ELLAR_CONFIG_MODULE
190+
from ellar.core.factory import AppFactory
191+
from .root_module import ApplicationModule
192+
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405
193+
194+
application = AppFactory.create_from_app_module(
195+
ApplicationModule,
196+
config_module=os.environ.get(
197+
ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig"
198+
),
199+
)
200+
201+
application.add_exception_handler(
202+
MyCustomExceptionHandler(),
203+
ExceptionHandlerAction405()
204+
)
205+
```
206+
207+
## **Override Default Exception Handler**
208+
We have gone through how to create an exception handler for status code and specific exception type.
209+
So, override any exception handler follows the same pattern with a target to exception class
210+
211+
for example:
212+
213+
```python
214+
# project_name/apps/custom_exceptions.py
215+
import typing as t
216+
from ellar.core.exceptions import IExceptionHandler, APIException
217+
from ellar.core.context import IExecutionContext
218+
from starlette.responses import Response
219+
220+
221+
class OverrideAPIExceptionHandler(IExceptionHandler):
222+
exception_type_or_code = APIException
223+
224+
async def catch(
225+
self, ctx: IExecutionContext, exc: APIException
226+
) -> t.Union[Response, t.Any]:
227+
app_config = ctx.get_app().config
228+
return app_config.DEFAULT_JSON_CLASS(
229+
{'message': exc.detail}, status_code=exc.status_code,
230+
)
231+
```
232+
233+
Once we register `OverrideAPIExceptionHandler` exception handler, it will become the default handler for `APIException` exception type.

docs/overview/modules.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class BookModule(ModuleBase):
7373

7474

7575
## `Additional Module Configurations`
76+
7677
### `Module Events`
7778
Every registered Module receives two event calls during its instantiation and when application is ready.
7879

@@ -87,11 +88,43 @@ class ModuleEventSample(ModuleBase):
8788
"""Called before creating Module object"""
8889

8990
def application_ready(self, app: App) -> None:
90-
"""Called when application is ready"""
91+
"""Called when application is ready - this is similar to @on_startup event"""
9192

9293
```
9394
`before_init` receives current app `Config` as a parameter and `application_ready` function receives `App` instance as parameter.
9495

96+
#### `Starlette Application Events`
97+
We can register multiple event handlers for dealing with code that needs to run before
98+
the application starts `up`, or when the application is shutting `down`.
99+
This is the way we support `Starlette` start up events in `Ellar`
100+
101+
```python
102+
103+
from ellar.common import Module, on_shutdown, on_startup
104+
from ellar.core import ModuleBase
105+
106+
@Module()
107+
class ModuleRequestEventsSample(ModuleBase):
108+
@on_startup
109+
def on_startup_func(cls):
110+
pass
111+
112+
@on_startup()
113+
async def on_startup_func_2(cls):
114+
pass
115+
116+
@on_shutdown
117+
def on_shutdown_func(cls):
118+
pass
119+
120+
@on_shutdown()
121+
async def on_shutdown_func_2(cls):
122+
pass
123+
```
124+
These will be registered to the application router during `ModuleRequestEventsSample` computation at runtime.
125+
Also, the events can be `async` as in the case of `on_shutdown_func_2` and `on_startup_func_2`
126+
127+
95128
### `Module Exceptions`
96129
In Ellar, custom exceptions can be registered through modules.
97130
During module meta-data computation, Ellar reads additional properties such as these from registered modules
@@ -132,35 +165,6 @@ class ModuleTemplateFilterSample(ModuleBase):
132165
def double_filter_dec(cls, n):
133166
return n * 2
134167
```
135-
### `Module Request Events`
136-
During application request handling, application router emits two events `start_up` and `shutdown` event.
137-
We can subscribe to those events in our modules.
138-
```python
139-
140-
from ellar.common import Module, on_shutdown, on_startup
141-
from ellar.core import ModuleBase
142-
143-
@Module()
144-
class ModuleRequestEventsSample(ModuleBase):
145-
@on_startup
146-
def on_startup_func(cls):
147-
pass
148-
149-
@on_startup()
150-
async def on_startup_func_2(cls):
151-
pass
152-
153-
@on_shutdown
154-
def on_shutdown_func(cls):
155-
pass
156-
157-
@on_shutdown()
158-
async def on_shutdown_func_2(cls):
159-
pass
160-
```
161-
These will be registered to the application router during `ModuleRequestEventsSample` computation at runtime.
162-
Also, the events can be `async` as in the case of `on_shutdown_func_2` and `on_startup_func_2`
163-
164168

165169
## `Dependency Injection`
166170
A module class can inject providers as well (e.g., for configuration purposes):

ellar/common/decorators/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
)
1414
from ellar.core import ControllerBase
1515
from ellar.core.controller import ControllerType
16+
from ellar.core.exceptions import ImproperConfiguration
1617
from ellar.di import RequestScope, injectable
17-
from ellar.exceptions import ImproperConfiguration
1818
from ellar.reflect import reflect
1919

2020
if t.TYPE_CHECKING: # pragma: no cover

ellar/common/decorators/exception.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88
class ValidateExceptionHandler(BaseModel):
99
key: t.Union[int, t.Type[Exception]]
10-
value: t.Callable
10+
value: t.Union[t.Callable, t.Type]
1111

1212

13-
def add_exception_handler(
13+
def _add_exception_handler(
1414
exc_class_or_status_code: t.Union[int, t.Type[Exception]],
15-
handler: t.Callable,
15+
handler: t.Union[t.Callable, t.Type],
1616
) -> None:
1717
validator = ValidateExceptionHandler(key=exc_class_or_status_code, value=handler)
1818
exception_handlers = {validator.key: validator.value}
@@ -30,8 +30,8 @@ def exception_handler(
3030
:return: Function
3131
"""
3232

33-
def decorator(func: t.Callable) -> t.Callable:
34-
add_exception_handler(exc_class_or_status_code, func)
33+
def decorator(func: t.Union[t.Callable, t.Type]) -> t.Callable:
34+
_add_exception_handler(exc_class_or_status_code, func)
3535
return func
3636

3737
return decorator

0 commit comments

Comments
 (0)