Skip to content

Commit

Permalink
Requires, Security and Docs (#471)
Browse files Browse the repository at this point in the history
* Clean some internals of the Pluggable
* Expose Controller
* Add requires for generic types
* Add tests for requires with Security
* Add docs about Requires and Security
  • Loading branch information
tarsil authored Jan 13, 2025
1 parent b292478 commit 5953151
Show file tree
Hide file tree
Showing 24 changed files with 1,029 additions and 35 deletions.
126 changes: 125 additions & 1 deletion docs/en/docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The dependencies are read from top-down in a python dictionary format, which mea

## How to use

Assuming we have a `User` model using [Saffier](./databases/saffier/models.md).
Assuming we have a `User` model using [Edgy](./databases/edgy/models.md).

```python hl_lines="14-15 19"
{!> ../../../docs_src/dependencies/precedent.py !}
Expand Down Expand Up @@ -123,3 +123,127 @@ objects from the top.

In conclusion, if your views/routes expect dependencies, you can define them in the upper level as described
and Esmerald will make sure that they will be automatically injected.

## `Requires` and `Security`

From the version 3.6.3+, Esmerald allows also to use what we call a "simpler" dependency injection. This dependency
injection system does not aim replace the current sytem but aims to provide another way of using some dependencies
in a simpler fashion.

The `Security` object is used, as the name suggests, to implement the out of the box [security provided by Esmerald](./security/index.md)
and in that section, that is explained how to apply whereas te `Requires` implements a more high level dependency system.

You can import directly from `esmerald`:

**Requires**

```python
from esmerald import Requires
```

**Security**

```python
from esmerald import Requires
```

!!! Warning
Neither `Requires()` or `Security()` are designed to work on an [application level](#dependencies-and-the-application-levels)
as is. For application layers and dependencies, you **must still use the normal dependency injection system to make it work**
or use the [Requires within the application layers](#requires-within-the-application-layers).

### Requires

This is what we describe a simple dependency.


An example how to use `Requires` would be something like this:

```python
{!> ../../../docs_src/dependencies/requires/simple.py !}
```

This example is very simple but you can extend to whatever you want and need. The `Requires` is not a Pydantic model
but a pure Python class. You can apply to any other complex example and having a `Requires` inside more `Requires`.

```python
{!> ../../../docs_src/dependencies/requires/nested.py !}
```

### Requires within the application layers

Now this is where things start to get interesting. Esmerald operates in layers and **almost** everything works like that.

What if you want to use the requires to operate on a layer level? Can you do it? **Yes**.

It works as we normally declare dependencies, for example, a [Factory](#more-real-world-examples) object.

```python
{!> ../../../docs_src/dependencies/requires/layer.py !}
```

### Security within the Requires

You can mix `Security()` and `Requires()` without any issues as both subclass the same base but there are nuances compared to
the direct application of the `Security` without using the `Requires` object.

For more details how to directly use the Security without using the Requires, please check the [security provided by Esmerald](./security/index.md)
section where it goes in into detail how to use it.

```python
from lilya.middleware.request_context import RequestContextMiddleware
from lilya.middleware import DefineMiddleware


app = Esmerald(
routes=[...],
middleware=[
middleware=[DefineMiddleware(RequestContextMiddleware)],
]
)
```

!!! Warning
You can mix both `Requires()` and `Security()` (**Security inside Requires**) but for this to work properly, you will
**need to add the `RequestContextMiddleware` from Lilya** or an exception will be raised.

Now, how can we make this simple example work? Like this:

```python
{!> ../../../docs_src/dependencies/requires/security.py !}
```

This example is an short adaptation of [security using jwt](./security/oauth-jwt.md) where we update the dependency
to add a `Requires` that also depends on a `Security`.

The `Security()` object is used **only** when you want to apply the niceties of [Esmerald security](./security/index.md)
in your application.

It is also a wrapper that does some magic for you by adding some extras automatically. The `Security` object expects you
to have an instance that implements an `async __call__(self, connection: Request) -> Any:` in order to operate.

Let us see a quick example:

```python
{!> ../../../docs_src/dependencies/requires/example.py !}
```

#### Application layer

But what about you using the application layer architecture? Is it possible? Also yes. Let us update the previous example
to make sure we reflect that.

```python
{!> ../../../docs_src/dependencies/requires/security_layer.py !}
```

## Recap

There many ways of implementing the dependency injection in Esmerald:

* Using the layers with `Inject` and `Injects()` respectively.
* Using the `Factory()` within and `Inject()` and `Injects()`.
* Using `Requires()` within an `Inject()` and `Injects()`.
* Using `Security()` within an `Inject()` and `Injects()` or within a `Requires()`.
* Using `Requires()` without using an `Inject()` and `Injects()` limiting it to the handler and **not application layer dependency**.
*
12 changes: 12 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ hide:

# Release Notes

## 3.6.3

## Added

- [Requires()](./dependencies.md#requires-and-security) as a new independent way to manage dependencies.
- A more thorough explanation about the [Security()](./dependencies.md#requires-and-security), how to use it and examples.

## Changed

- Expose `Controller` in `esmerald` as alternative to `APIView`. This was already available to use but not directly
accessible via `from esmerald import Controller`.

## 3.6.2

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs_src/dependencies/precedent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from myapp.accounts.models import User
from saffier.exceptions import ObjectNotFound
from edgy.exceptions import ObjectNotFound

from esmerald import Esmerald, Gateway, Inject, Injects, get

Expand Down
53 changes: 53 additions & 0 deletions docs_src/dependencies/requires/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from esmerald import Request, Security, HTTPException, get, Inject, Injects, Esmerald, Gateway
from lilya import status
from typing import cast, Any
from pydantic import BaseModel


class MyCustomSecurity:
def __init__(self, name: str, **kwargs: Any) -> None:
self.name = name
self.__auto_error__ = kwargs.pop("auto_error", True)

async def __call__(self, request: Request) -> dict[str, None]:
api_key = request.query_params.get(self.name, None)
if api_key:
return cast(str, api_key)

if self.__auto_error__:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authenticated",
)
return None


# Instantiate the custom security scheme
api_key = MyCustomSecurity(name="key")

# Use the custom security scheme
security = Security(api_key)


class User(BaseModel):
username: str


def get_current_user(oauth_header: str = Security(api_key)):
user = User(username=oauth_header)
return user


@get(
"/users/me",
security=[api_key],
dependencies={"current_user": Inject(get_current_user)},
)
def read_current_user(current_user: User = Injects()) -> Any:
return current_user


# Start the application
app = Esmerald(
routes=[Gateway(handler=read_current_user)],
)
26 changes: 26 additions & 0 deletions docs_src/dependencies/requires/layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Any

from esmerald import Gateway, Inject, Injects, JSONResponse, Requires, get, Esmerald


async def get_user():
return {"id": 1, "name": "Alice"}


async def get_current_user(user: Any = Requires(get_user)):
return user


@get(
"/items",
dependencies={"current_user": Inject(get_current_user)},
)
async def get_items(current_user: Any = Injects()) -> JSONResponse:
return JSONResponse({"message": "Hello", "user": current_user})


app = Esmerald(
routes=[
Gateway(handler=get_items),
]
)
26 changes: 26 additions & 0 deletions docs_src/dependencies/requires/nested.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Dict, Any
from esmerald import Gateway, Requires, get, Esmerald


async def query_params(q: str | None = None, skip: int = 0, limit: int = 20):
return {"q": q, "skip": skip, "limit": limit}


async def get_user() -> Dict[str, Any]:
return {"username": "admin"}


async def get_user(
user: Dict[str, Any] = Requires(get_user), params: Dict[str, Any] = Requires(query_params)
):
return {"user": user, "params": params}


@get("/info")
async def get_info(info: Dict[str, Any] = Requires(get_user)) -> Any:
return info


app = Esmerald(
routes=[Gateway(handler=get_info)],
)
30 changes: 30 additions & 0 deletions docs_src/dependencies/requires/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Any

from lilya.middleware import DefineMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from pydantic import BaseModel

from esmerald import Gateway, Requires, Security, get, Esmerald
from esmerald.security.api_key import APIKeyInCookie

api_key = APIKeyInCookie(name="key")


class User(BaseModel):
username: str


def get_current_user(oauth_header: str = Security(api_key)):
user = User(username=oauth_header)
return user


@get("/users/me", security=[api_key])
def read_current_user(current_user: User = Requires(get_current_user)) -> Any:
return current_user


app = Esmerald(
routes=[Gateway(handler=read_current_user)],
middleware=[DefineMiddleware(RequestContextMiddleware)],
)
38 changes: 38 additions & 0 deletions docs_src/dependencies/requires/security_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Any

from lilya.middleware import DefineMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from pydantic import BaseModel

from esmerald import Gateway, Requires, Security, get, Esmerald, Inject, Injects
from esmerald.security.api_key import APIKeyInCookie

api_key = APIKeyInCookie(name="key")


class User(BaseModel):
username: str


def get_current_user(oauth_header: str = Security(api_key)):
user = User(username=oauth_header)
return user


def get_user(user: User = Requires(get_current_user)) -> User:
return user


@get(
"/users/me",
security=[api_key],
dependencies={"current_user": Inject(get_user)},
)
def read_current_user(current_user: User = Injects()) -> Any:
return current_user


app = Esmerald(
routes=[Gateway(handler=read_current_user)],
middleware=[DefineMiddleware(RequestContextMiddleware)],
)
17 changes: 17 additions & 0 deletions docs_src/dependencies/requires/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Any, Dict

from esmerald import Gateway, Requires, get, Esmerald


async def query_params(q: str | None = None, skip: int = 0, limit: int = 20):
return {"q": q, "skip": skip, "limit": limit}


@get("/items")
async def get_params(params: Dict[str, Any] = Requires(query_params)) -> Any:
return params


app = Esmerald(
routes=[Gateway(handler=get_params)],
)
6 changes: 4 additions & 2 deletions esmerald/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
ValidationErrorException,
)
from .interceptors.interceptor import EsmeraldInterceptor
from .param_functions import Security
from .param_functions import Requires, Security
from .params import Body, Cookie, File, Form, Header, Injects, Param, Path, Query
from .permissions import AllowAny, BasePermission, DenyAll
from .pluggables import Extension, Pluggable
from .protocols import AsyncDAOProtocol, DaoProtocol, MiddlewareProtocol
from .requests import Request
from .responses import JSONResponse, Response, TemplateResponse
from .routing.apis import APIView, SimpleAPIView
from .routing.apis import APIView, Controller, SimpleAPIView
from .routing.gateways import Gateway, WebhookGateway, WebSocketGateway
from .routing.handlers import delete, get, head, options, patch, post, put, route, trace, websocket
from .routing.router import Include, Router
Expand All @@ -57,6 +57,7 @@
"BasePermission",
"ChildEsmerald",
"Context",
"Controller",
"CORSConfig",
"CSRFConfig",
"Cookie",
Expand Down Expand Up @@ -90,6 +91,7 @@
"Query",
"Redirect",
"Request",
"Requires",
"Response",
"Router",
"Security",
Expand Down
Loading

0 comments on commit 5953151

Please sign in to comment.