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
69 changes: 25 additions & 44 deletions docs/basics/execution-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,20 @@ Ellar provides the ability to attach **custom metadata** to route handlers throu
We can then access this metadata from within our class to make certain decisions.

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

from ellar.common import Body, Controller, post, set_metadata
from ellar.core import ControllerBase
from .schemas import CreateDogSerializer, DogListFilter
from .schemas import CreateCarSerializer


@Controller('/dogs')
class DogsController(ControllerBase):
@Controller('/car')
class CarController(ControllerBase):
@post()
@set_metadata('role', ['admin'])
async def create(self, payload: CreateDogSerializer = Body()):
async def create(self, payload: CreateCarSerializer = Body()):
result = payload.dict()
result.update(message='This action adds a new dog')
result.update(message='This action adds a new car')
return result
```

Expand All @@ -216,24 +216,24 @@ to the `create()` method. While this works, it's not good practice to use `@set_
Instead, create your own decorators, as shown below:

```python
# project_name/apps/dogs/controllers.py
# project_name/apps/cars/controllers.py
import typing
from ellar.common import Body, Controller, post, set_metadata
from ellar.core import ControllerBase
from .schemas import CreateDogSerializer, DogListFilter
from .schemas import CreateCarSerializer


def roles(*_roles: str) -> typing.Callable:
return set_metadata('roles', list(_roles))


@Controller('/dogs')
class DogsController(ControllerBase):
@Controller('/car')
class CarController(ControllerBase):
@post()
@roles('admin', 'is_staff')
async def create(self, payload: CreateDogSerializer = Body()):
async def create(self, payload: CreateCarSerializer = Body()):
result = payload.dict()
result.update(message='This action adds a new dog')
result.update(message='This action adds a new car')
return result
```

Expand All @@ -244,7 +244,7 @@ To access the route's role(s) (custom metadata), we'll use the `Reflector` helpe
`Reflector` can be injected into a class in the normal way:

```python
# project_name/apps/dogs/guards.py
# project_name/apps/cars/guards.py
from ellar.di import injectable
from ellar.core import GuardCanActivate, IExecutionContext
from ellar.services import Reflector
Expand All @@ -259,53 +259,34 @@ class RoleGuard(GuardCanActivate):
roles = self.reflector.get('roles', context.get_handler())
# request = context.switch_to_http_connection().get_request()
# check if user in request object has role
if not roles:
return True
return 'user' in roles
```

Next, we apply the `RoleGuard` to `DogsController`
Next, we apply the `RoleGuard` to `CarController`

```python
# project_name/apps/dogs/controllers.py
# project_name/apps/cars/controllers.py
import typing
from ellar.common import Body, Controller, post, set_metadata
from ellar.common import Body, Controller, post, set_metadata, Guards
from ellar.core import ControllerBase
from .schemas import CreateDogSerializer, DogListFilter
from .schemas import CreateCarSerializer
from .guards import RoleGuard

def roles(*_roles: str) -> typing.Callable:
return set_metadata('roles', list(_roles))


@Controller('/dogs', guards=[RoleGuard, ])
class DogsController(ControllerBase):
@Controller('/car')
@Guards(RoleGuard)
class CarController(ControllerBase):
@post()
@roles('admin', 'is_staff')
async def create(self, payload: CreateDogSerializer = Body()):
async def create(self, payload: CreateCarSerializer = Body()):
result = payload.dict()
result.update(message='This action adds a new dog')
result.update(message='This action adds a new car')
return result
```

Also, since `RoleGuard` depends on `Reflector`, it has to be registered as a provider. And we do that in `DogsModule`:

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

from ellar.common import Module
from ellar.core import ModuleBase
from ellar.di import Container

from .controllers import DogsController
from .guards import RoleGuard


@Module(
controllers=[DogsController],
providers=[RoleGuard],
)
class DogsModule(ModuleBase):
def register_providers(self, container: Container) -> None:
# for more complicated provider registrations
# container.register_instance(...)
pass
```
Also, since `RoleGuard` is marked as `injectable`, EllarInjector service will be able to resolve `RoleGuard` without `RoleGuard` registered as a provider.
2 changes: 0 additions & 2 deletions docs/handling-response/response.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,3 @@ class ItemsController(ControllerBase):
def me(self):
return PlainTextResponse("some text response.", status_code=200)
```

## using serialize_object function
25 changes: 13 additions & 12 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,29 @@
Ellar is a lightweight ASGI framework for building efficient and scalable server-side python applications.
It supports both OOP (Object-Oriented Programming) and FP (Functional Programming)


Ellar is built around [Starlette (ASGI toolkit)](https://www.starlette.io/) which processes all the HTTP requests and background tasks. Although, there is a high level
of abstraction, some concepts of Starlette are still supported.
Ellar is based on [Starlette (ASGI toolkit)](https://www.starlette.io/), a lightweight ASGI framework/toolkit well-suited for developing asynchronous web services in Python.
And while Ellar provides a high level of abstraction on top of Starlette, it still incorporates some of its features, as well as those of FastAPI.
If you are familiar with these frameworks, you will find it easy to understand and use Ellar.

## Inspiration
Ellar was heavily inspired by [NestJS](https://docs.nestjs.com/) in its simplicity in usage while managing complex project structures and applications.
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 a high level of abstraction of framework APIs, project structures, architectures, and speed of handling requests.
Ellar was deeply influenced by [NestJS](https://docs.nestjs.com/) for its ease of use and ability to handle complex project structures and applications.
Additionally, it took some concepts from [FastAPI](https://fastapi.tiangolo.com/) in terms of request parameter handling and data serialization with Pydantic.

With that said, the objective of Ellar is to offer a high level of abstraction in its framework APIs, along with a well-structured project setup, an object-oriented approach to web application design,
the ability to adapt to any desired software architecture, and ultimately, speedy request handling.

## Installation
To get started, you need to scaffold a project using [Ellar-CLI](https://eadwincode.github.io/ellar-cli/) toolkit. This is recommended for a 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
$(venv) pip install ellar
```

### 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.
After that, lets create a new project.
Run the command below and change the `project-name` with whatever name you decide.
```shell
pip install "ellar[standard]"
$(venv) ellar new project-name
```

then, start the app with:
Expand All @@ -60,7 +61,7 @@ Open your browser and navigate to [`http://localhost:8000/`](http://localhost:80
- `CORS, GZip, Static Files, Streaming responses`

## Dependency Summary
- `Python >= 3.6`
- `Python >= 3.7`
- `Starlette`
- `Pydantic`
- `Injector`
Expand Down
40 changes: 24 additions & 16 deletions docs/overview/custom_decorators.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

Ellar provides a variety of function decorators in the `ellar.common` python module that can be used to modify the behavior of route functions.

These decorators can be used to change the response type of a route function, add filters to the response schema, define the OPENAPI context, and more.
In general, these decorators can help to simplify and streamline the process of creating routes.

Expand Down Expand Up @@ -57,12 +58,14 @@ async def on_connect(self, websocket: WebSocket, code: int):
## **Non Route Function Parameters Decorators**
We discussed decorators that are used to define route function parameter dependencies in Ellar.
These decorators, such as `Query`, `Form`, and `Body`, etc. are pydantic models used to specify the expected parameters for a route function.

However, there are also some route parameters that are **system** dependent, such as the `request` or `websocket` object, and the `response` object.
These parameters are resolved by the application and supplied to the route function when needed, and are not specified with pydantic models or user input.

### Provide(Type)
The **Provide(Type)** decorator is used to resolve a service provider and inject it into a route function parameter.
This can be useful when using the ModuleRouter feature in Ellar.

It allows for easy injection of services into route functions, making it easier to manage dependencies and improve code organization.
This can be useful for resolving database connections, external APIs, or other resources that are used by the route function.

Expand Down Expand Up @@ -108,6 +111,7 @@ def example_endpoint(ctx = Context()):

In this example, the example_endpoint function is decorated with the **Context()** decorator, which injects the current `IExecutionContext` object into the `ctx` parameter of the function.
The `IExecutionContext` object provides access to various resources and information related to the current execution context, such as the current HTTP connection, query parameters, and more.

In this example, the `switch_to_http_connection()` method is used to access the current HTTP connection and the `get_client()` method is used to get the client object for the connection.
The `query_params` attribute of the client object is then accessed and included in the response returned by the endpoint.

Expand Down Expand Up @@ -167,14 +171,11 @@ async def example_endpoint(ws = Ws()):
The above code creates a WebSocket route '/test-ws' and when a client connects to this route,
the `example_endpoint` function is executed. The `Ws` decorator injects the current `WebSocket` object to the `ws` parameter of the function, which can then be used to interact with the WebSocket connection, such as accepting the connection and sending data to the client.

### Host
**Host()** decorator injects current client host address to route function parameter.

### Session
**Session()** decorator injects current Session object to route function parameter.
The same conditions and examples applies for:

### Http
**Http()** decorator injects current HTTP connection object to route function parameter.
- **Host()** decorator injects current client host address to route function parameter.
- **Session()** decorator injects current Session object to route function parameter. This requires [SessionMiddleware](https://www.starlette.io/middleware/#sessionmiddleware) module from Starlette added in application middleware and also `SessionMiddleware` module depends on [itsdangerous](https://pypi.org/project/itsdangerous/) package.
- **Http()** decorator injects current HTTP connection object to route function parameter.

## **Creating a Custom Parameter Decorators**
You can still create your own route parameter decorators that suits your need. You simply need to follow a contract, `NonParameterResolver`, and override the resolve function.
Expand Down Expand Up @@ -203,6 +204,7 @@ class UserParam(NonParameterResolver):

This example defines a custom decorator called `UserParam` that inherits from `NonParameterResolver`.
The `resolve` method is overridden to extract the user from the current `IExecutionContext`'s request.

If the user is found, it is returned as a dict with the key as the `parameter_name` of the decorator, along with an empty list of errors.
If no user is found, an empty dict and a list of errors containing an ErrorWrapper object is returned.

Expand Down Expand Up @@ -234,6 +236,7 @@ def index(self):

In the example, the index function is decorated with the `render` decorator,
which will return a 200 status code and HTML content from my_template.

The return object from the index function will be used as the templating context for `my_template` during the template rendering process.
This allows the function to pass data to the template and have it rendered with the provided context, the rendered template will be the response body.

Expand Down Expand Up @@ -379,15 +382,16 @@ See [Pydantic Model Export](https://docs.pydantic.dev/usage/exporting_models/#mo
### VERSION
**@version()** is a decorator that provides endpoint versioning for a route function.
This decorator allows you to specify the version of the endpoint that the function is associated with.

Based on the versioning scheme configuration in the application, versioned route functions are called. This can be useful for maintaining backward compatibility, or for rolling out new features to different versions of an application.
More information on how to use this decorator can be found in the [Versioning documentation]()

A quick example on how to use `version` decorator:
```python
from ellar.common import post, version
from ellar.common import post, Version

@post("/create", name='v2_v3_list')
@version('2', '3')
@Version('2', '3')
async def get_item_v2_v3(self):
return {'message': 'for v2 and v3 request'}
```
Expand All @@ -397,16 +401,18 @@ This indicates that the `get_item_v2_v3` route function will handle version 2 an
This allows for multiple versions of the same endpoint to be handled by different route functions, each with their own logic and implementation.

### GUARDS
**@guards()** is a decorator that applies a protection class of type GuardCanActivate to a route function.
These protection classes have a can_execute function that is called to determine whether a route function should be executed.
This decorator allows you to apply certain conditions or checks before a route function is executed, such as authentication or authorization checks.
**@Guards()** is a decorator that applies a protection class of type `GuardCanActivate` to a route function.
These protection classes have a `can_execute` function that is called to determine whether a route function should be executed.

This decorator allows you to apply certain conditions or checks before a route function is executed, such as `authentication` or `authorization` checks.
This can help to ensure that only authorized users can access certain resources.

More information on how to use this decorator can be found in the [Guard Documentation]()

A quick example on how to use `guards` decorator:
A quick example on how to use `Guards` decorator:
```python
import typing as t
from ellar.common import get, guards
from ellar.common import get, Guards
from ellar.core.guard import APIKeyQuery
from ellar.core.connection import HTTPConnection

Expand All @@ -419,14 +425,16 @@ class MyAPIKeyQuery(APIKeyQuery):


@get("/")
@guards(MyAPIKeyQuery(), )
@Guards(MyAPIKeyQuery(), )
async def get_guarded_items(self):
return {'message': 'worked fine with `key`=`supersecret`'}
```
The `guards` decorator, like the `version` decorator, takes a list of values as an argument.
The `Guards` decorator, like the `version` decorator, takes a list of values as an argument.
During a request, the provided guards are called in the order in which they are provided.

This allows you to apply multiple guards to a single route function and have them executed in a specific order.
This is useful for applying multiple levels of security or access control to a single endpoint.

Each guard class has a `can_execute` function that is called in the order specified by the decorator, if any of the guard's `can_execute` function returns False, the route function will not be executed.

## **Command Decorators**
Expand Down
Loading