Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,29 @@
Ellar is a lightweight ASGI framework for building efficient and scalable server-side python application.
It supports both OOP (Object-Oriented Programming) and FP (Functional Programming)

Ellar is built around [Starlette]()(ASGI toolkit) which processes all the HTTP request and background tasks. Although, there is a high level
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
of abstraction, some concepts of Starlette are still supported.

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

## Installation
To get started, you need to scaffold a project using [Ellar-CLI]() toolkit. This is recommended for first-time user.
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.
The scaffolded project is more like a guide to project setup.

```shell
$(venv) pip install ellar[standard]
$(venv) ellar new project-name
```

### NB:
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.
```shell
pip install "ellar[standard]"
```

### Py36 Support
For python3.6 users,
```shell
Expand Down
Empty file removed docs/overview/exception_filters.md
Empty file.
233 changes: 233 additions & 0 deletions docs/overview/exception_handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@

Ellar `ExceptionMiddleware` together with `ExceptionMiddlewareService` are responsible for processing all unhandled exception
across the application and provides an appropriate user-friendly response.

```json
{
"status_code": 403,
"detail": "Forbidden"
}
```

Default exceptions types Managed by default:

- **`HTTPException`**: Default exception class provided by `Starlette` for HTTP client
- **`WebSocketException`**: Default websocket exception class also provided by `Starlette` for websocket connection
- **`RequestValidationException`**: Request data validation exception provided by `Pydantic`
- **`APIException`**: Custom exception created for typical REST API based application to provides more concept about the exception raised.

## **HTTPException**

The `HTTPException` class provides a base class that you can use for any
handled exceptions.

* `HTTPException(status_code, detail=None, headers=None)`

## **WebSocketException**

You can use the `WebSocketException` class to raise errors inside WebSocket endpoints.

* `WebSocketException(code=1008, reason=None)`

You can set any code valid as defined [in the specification](https://tools.ietf.org/html/rfc6455#section-7.4.1).

## **APIException**
As stated earlier, its an exception type for typical REST API based application. Its gives more concept to error and provides a
simple interface for creating other custom exception need in your application.

For example,

```python
from ellar.core.exceptions import APIException
from starlette import status

class ServiceUnavailableException(APIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
code = 'service_unavailable'

```
!!!hint
You should only raise `HTTPException` and `APIException` inside routing or endpoints. Middleware classes should instead just return appropriate responses directly.

Let's use this `ServiceUnavailableException` in our previous project.

For example, in the `DogsController`, we have a `get_all()` method (a `GET` route handler).
Let's assume that this route handler throws an exception for some reason. To demonstrate this, we'll hard-code it as follows:

```python
# project_name/apps/dogs/controllers.py

@get()
def get_all(self):
raise ServiceUnavailableException()

```
Now, when you visit [http://127.0.0.1/dogs/](http://127.0.0.1/dogs/), you will get a JSON response.
```json
{
"detail": "Service Unavailable"
}
```

We have a JSON response because Ellar has an exception handler for `APIException`. This handler can be change to return some different.
We shall how to do that on Overriding Default Exception Handlers.

There is other error presentation available on `APIException` instance:
- `.detail`: returns textual description of the error.
- `get_full_details()`: returns both textual description and other information about the error.

```shell
>>> print(exc.detail)
Service Unavailable
>>> print(exc.get_full_details())
{'detail':'Service Unavailable','code':'service_unavailable', 'description': 'The server cannot process the request due to a high load'}
```

## **Creating Custom Exception Handler**

To create an exception handler for your custom exception, you have to create a class that follow `IExceptionHandler` contract.

At the root project folder, create a file `custom_exceptions.py`,

```python
# project_name/custom_exceptions.py
import typing as t
from ellar.core.exceptions import IExceptionHandler
from ellar.core.context import IExecutionContext
from starlette.responses import Response


class MyCustomException(Exception):
pass


class MyCustomExceptionHandler(IExceptionHandler):
exception_type_or_code = MyCustomException

async def catch(
self, ctx: IExecutionContext, exc: MyCustomException
) -> t.Union[Response, t.Any]:
app_config = ctx.get_app().config
return app_config.DEFAULT_JSON_CLASS(
{'detail': str(exc)}, status_code=400,
)

```
- `exception_type_or_code`: defines the `exception class` OR `status code` to target when resolving exception handlers.
- `catch()`: defines the handling code and response to be returned to the client.

### **Creating Exception Handler for status code**
Let's create a handler for `MethodNotAllowedException` which, according to HTTP code is `405`.

```python
# project_name/apps/custom_exceptions.py
import typing as t
from ellar.core.exceptions import IExceptionHandler
from ellar.core.context import IExecutionContext
from ellar.core import render_template
from starlette.responses import Response
from starlette.exceptions import HTTPException

class MyCustomException(Exception):
pass


class MyCustomExceptionHandler(IExceptionHandler):
exception_type_or_code = MyCustomException

async def catch(
self, ctx: IExecutionContext, exc: MyCustomException
) -> t.Union[Response, t.Any]:
app_config = ctx.get_app().config
return app_config.DEFAULT_JSON_CLASS(
{'detail': str(exc)}, status_code=400,
)


class ExceptionHandlerAction405(IExceptionHandler):
exception_type_or_code = 405

async def catch(
self, ctx: IExecutionContext, exc: HTTPException
) -> t.Union[Response, t.Any]:
context_kwargs = {}
return render_template('405.html', request=ctx.switch_to_request(), **context_kwargs)
```
We have registered a handler for any `HTTPException` with status code `405` and we have chosen to return a template `405.html` as response.

!!!info
Ellar will look for `405.html` in all registered module. So `dogs` folder, create a `templates` folder and add `405.html`.

The same way can create Handler for `500` error code.


## **Registering Exception Handlers**
We have successfully created two exception handlers `ExceptionHandlerAction405` and `MyCustomExceptionHandler` but they are not yet visible to the application.

- `config.py`: The config file holds manage application settings including `EXCEPTION_HANDLERS` fields which defines all custom exception handlers used in the application.

```python
# project_name/config.py
import typing as t
from ellar.core import ConfigDefaultTypesMixin
from ellar.core.exceptions import IExceptionHandler
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405

class BaseConfig(ConfigDefaultTypesMixin):
EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [
MyCustomExceptionHandler(),
ExceptionHandlerAction405()
]
```
- `application instance`: You can also add exception through `app` instance.

```python
# project_name/server.py

import os

from ellar.constants import ELLAR_CONFIG_MODULE
from ellar.core.factory import AppFactory
from .root_module import ApplicationModule
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405

application = AppFactory.create_from_app_module(
ApplicationModule,
config_module=os.environ.get(
ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig"
),
)

application.add_exception_handler(
MyCustomExceptionHandler(),
ExceptionHandlerAction405()
)
```

## **Override Default Exception Handler**
We have gone through how to create an exception handler for status code and specific exception type.
So, override any exception handler follows the same pattern with a target to exception class

for example:

```python
# project_name/apps/custom_exceptions.py
import typing as t
from ellar.core.exceptions import IExceptionHandler, APIException
from ellar.core.context import IExecutionContext
from starlette.responses import Response


class OverrideAPIExceptionHandler(IExceptionHandler):
exception_type_or_code = APIException

async def catch(
self, ctx: IExecutionContext, exc: APIException
) -> t.Union[Response, t.Any]:
app_config = ctx.get_app().config
return app_config.DEFAULT_JSON_CLASS(
{'message': exc.detail}, status_code=exc.status_code,
)
```

Once we register `OverrideAPIExceptionHandler` exception handler, it will become the default handler for `APIException` exception type.
64 changes: 34 additions & 30 deletions docs/overview/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class BookModule(ModuleBase):


## `Additional Module Configurations`

### `Module Events`
Every registered Module receives two event calls during its instantiation and when application is ready.

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

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

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

#### `Starlette Application Events`
We can register multiple event handlers for dealing with code that needs to run before
the application starts `up`, or when the application is shutting `down`.
This is the way we support `Starlette` start up events in `Ellar`

```python

from ellar.common import Module, on_shutdown, on_startup
from ellar.core import ModuleBase

@Module()
class ModuleRequestEventsSample(ModuleBase):
@on_startup
def on_startup_func(cls):
pass

@on_startup()
async def on_startup_func_2(cls):
pass

@on_shutdown
def on_shutdown_func(cls):
pass

@on_shutdown()
async def on_shutdown_func_2(cls):
pass
```
These will be registered to the application router during `ModuleRequestEventsSample` computation at runtime.
Also, the events can be `async` as in the case of `on_shutdown_func_2` and `on_startup_func_2`


### `Module Exceptions`
In Ellar, custom exceptions can be registered through modules.
During module meta-data computation, Ellar reads additional properties such as these from registered modules
Expand Down Expand Up @@ -132,35 +165,6 @@ class ModuleTemplateFilterSample(ModuleBase):
def double_filter_dec(cls, n):
return n * 2
```
### `Module Request Events`
During application request handling, application router emits two events `start_up` and `shutdown` event.
We can subscribe to those events in our modules.
```python

from ellar.common import Module, on_shutdown, on_startup
from ellar.core import ModuleBase

@Module()
class ModuleRequestEventsSample(ModuleBase):
@on_startup
def on_startup_func(cls):
pass

@on_startup()
async def on_startup_func_2(cls):
pass

@on_shutdown
def on_shutdown_func(cls):
pass

@on_shutdown()
async def on_shutdown_func_2(cls):
pass
```
These will be registered to the application router during `ModuleRequestEventsSample` computation at runtime.
Also, the events can be `async` as in the case of `on_shutdown_func_2` and `on_startup_func_2`


## `Dependency Injection`
A module class can inject providers as well (e.g., for configuration purposes):
Expand Down
2 changes: 1 addition & 1 deletion ellar/common/decorators/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
)
from ellar.core import ControllerBase
from ellar.core.controller import ControllerType
from ellar.core.exceptions import ImproperConfiguration
from ellar.di import RequestScope, injectable
from ellar.exceptions import ImproperConfiguration
from ellar.reflect import reflect

if t.TYPE_CHECKING: # pragma: no cover
Expand Down
10 changes: 5 additions & 5 deletions ellar/common/decorators/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

class ValidateExceptionHandler(BaseModel):
key: t.Union[int, t.Type[Exception]]
value: t.Callable
value: t.Union[t.Callable, t.Type]


def add_exception_handler(
def _add_exception_handler(
exc_class_or_status_code: t.Union[int, t.Type[Exception]],
handler: t.Callable,
handler: t.Union[t.Callable, t.Type],
) -> None:
validator = ValidateExceptionHandler(key=exc_class_or_status_code, value=handler)
exception_handlers = {validator.key: validator.value}
Expand All @@ -30,8 +30,8 @@ def exception_handler(
:return: Function
"""

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

return decorator
Loading