Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 - Webhooks #133

Merged
merged 2 commits into from
Jul 25, 2023
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
92 changes: 92 additions & 0 deletions docs/extras/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# The Form

This is a special way of sending a form directly using Esmerald. The `Form` like the
[File](./upload-files.md), inherits from the [Body](./body-fields.md) and applies the special
`media_type` as `application/x-www-form-urlencoded`.

This also means that you can also use the `Body` directly to send a form with your API by simply
declaring `Body(media_type="application/x-www-form-urlencoded")`.

The Form is a simple and cleaner shortcut for it.

The simplest way is by importing the `Form` object from Esmerald.

```python hl_lines="7"
{!> ../docs_src/extras/form/form_object.py !}
```

You can also import via:

```python
from esmerald.params import Form
```

As [explained here](./request-data.md#request-data), the handler is expecting a `data` field declared and from there
you can pass more details about the form.


## Examples

You can send the form in many different formats, for example:

1. [A dictionary](#sending-as-dictionary) - Send as normal dictionary.
2. [A dataclass](#a-dataclass) - Send as normal dataclass.
3. [A pydantic dataclass](#pydantic-dataclass) - Send a pydantic dataclass.
4. [Pydantic model](#pydantic-model) - Send a pydantic BaseModel.

You decide the best format to send. For the following examples, we will be using `httpx` for the
requests for explanatory purposes.

### Sending as dictionary

```python hl_lines="9 20 23"
{!> ../docs_src/extras/form/as_dict.py !}
```

As you can see, we declared the return signature to be `Dict[str, str]` and the `data` payload to
be a dictionary also `Dict[str, str]`. This way we acn simply send the form as you would normally
do.

### A dataclass

What if you want to type as a dataclass and return it in your response?

```python hl_lines="15 26 29"
{!> ../docs_src/extras/form/dataclass.py !}
```

The way the payload is sent to the API will always be the same no matter what, what is important
is how you actually type it. In this example, we declared a `User` dataclass with two field
`name` and `email` and we return exactly what we sent back into the response.


### Pydantic dataclass

A Pydantic dataclass is the same as a normal python dataclass in the end but with some internal
extras from Pydantic but for Esmerald, it is the same.

```python hl_lines="14 25 28"
{!> ../docs_src/extras/form/pydantic_dc.py !}
```

### Pydantic model

What if we want to type and return as a Pydantic model? Well, it behaves exactly the same as the
dataclasses.

```python hl_lines="13 24 27"
{!> ../docs_src/extras/form/model.py !}
```

## Notes

As you could see from the examples, it is very simple and direct to use the `Form` in Esmerald and
the returns are simply clean.

### Important

Since `Form` is Pydantic field (sort of), that also means you can specify for instance,
the other parameters to be evaluated.

You can check [the list of available parameters default](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.FieldInfo)
as well.
30 changes: 30 additions & 0 deletions docs/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Esmerald having `Starlette` under the hood also means that all available respons

You simply just need to decide which type of response your function will have and let `Esmerald` take care of the rest.

!!! Tip

Esmerald automatically understands if you are typing/returning a **dataclass**,
a **Pydantic dataclass** or a **Pydantic model** and converts
them automatically into a [JSON response](#jsonresponse).

## Esmerald responses and the application

The available responses from `Esmerald` are:
Expand Down Expand Up @@ -130,6 +136,9 @@ This response returns a `TemplateResponse`.

* **name** - Template name/location. E.g.: `accounts/list.html`.
* **context** - The dict context to be sent to the template.
* **alternative_template** - Template name/location of an alternative template if the **name** of
the original is not found.
E.g.: If `accounts/list.html` is not found `alternative_template="base/list.html"`.

### Redirect

Expand Down Expand Up @@ -362,3 +371,24 @@ unique and you might want to return directly a `string`, a `dict`, an `integer`,
```python
{!> ../docs_src/responses/others.py !}
```
### Example

Below we have a few examples of possible responses recognised by Esmerald automatically.

**Pydantic model**

```python hl_lines="13 24 27"
{!> ../docs_src/extras/form/model.py !}
```

**Pydantic dataclass**

```python hl_lines="14 25 28"
{!> ../docs_src/extras/form/pydantic_dc.py !}
```

**Python dataclass**

```python hl_lines="15 26 29"
{!> ../docs_src/extras/form/dataclass.py !}
```
177 changes: 177 additions & 0 deletions docs/routing/webhooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Webhooks

OpenAPI Webhooks are those cases where you want to tell your API users that your application
could/should/would call **their** own application, for example, sending a request with specific
bits of data, usually to **notify** them of some type of event.

This also means that instead of your users sending requests to your APIs, it is your application
**sendind requests** to their application.

This process is called **webhook**.

## Esmerald webhooks

Esmerald provides a way of declaring these webhooks in the OpenAPI specification. It is very, very
similar to the way the [Gateway](routes.md#gateway) is declared but **dedicated to webhooks**.

The process usually is that you define in your code, as normal, what is the message that you will
send, in other words, **the body of the request**.

You also define in some way at which moments your app will send those requests or events.

Your users on the other hand, define some way (web dashboard, for instance) the URL where your
application should send those requests.

The way the logic how to register the URLs for the webhooks and the code to performs the said
actions is entirely up to you.

## Documenting Esmerald webhooks with OpenAPI

As mentioned before, the way of doing it is very similar to the way you declare
[Gateway](routes.md#gateway) but for this purpose, webhooks have a **special dedicated** object or
objects to do make it happen, the [WebhookGateway](#webhookgateway).

Also, the webhooks **are not *hooked* into the application routing system**, instead, they are
placed in the `webhooks` list.

```python hl_lines="3"
from esmerald import Esmerald

app = Esmerald(webhooks=[...])
```

### WebhookGateway

As the name indicated, the `WebhookGateway` is the main object where you declare the hooks for
the OpenAPI specification and **unlike the Gateway**, it does not declare a `path` (example, `/event`),
instead, it only needs to receive the **name** of the action.

Like the Gateway, the **WebhookGateway** also expects a [handler](#handlers) but
**not the same handler as you usually use for the routes**, a special **webhook handler**.

#### How to import it

You can import them directly:

```python
from esmerald import WebhookGateway
```

Or you can use the full path.

```python
from esmerald.routing.gateways import WebhookGateway
```

#### Parameters

* **path** - Altough is called path, it corresponds to the **name** of the webhook
and it does not require a `/` at the beginning of it like a normal path would. E.g.: `subscription`.
* **handler** - The [handler](#handlers) function of the webhook.
* **include_in_schema** - Flag if the webhook should be included in the OpenAPI schema.
* **deprecated** - Flag if the webhook is deprecated.

### Handlers

The handlers for the **webhooks** are pretty much similar to the normal handlers used for routing
but **dedicated** only to the **WebhookGateway**. The available handlers are:

* **whget** - For the `GET`.
* **whpost** - For the `POST`.
* **whput** - For the `PUT`.
* **whpatch** - For the `PATCH`.
* **whdelete** - For the `DELETE`.
* **whead** - For the `HEAD`.
* **whoptions** - For the `OPTION`.
* **whtrace** - For the `TRACE`.
* **whroute** - Used to specificy for which `http verbs` is available. This handler has the special
`methods` attribute. E,g.:

```python
from esmerald import whroute

@whroute(methods=["GET", "POST"])
...
```

As you can already see, the handlers are very similar to the [routing handler](./handlers.md) but
dedicated to this purpose and **all of them start with `wh`**.

The `wh` at the beginning of each handler means **W**eb**H**ook.

#### How to import them

You can import them directly:

```python
from esmerald import (
whdelete,
whead,
whget,
whoptions,
whpatch,
whpost,
whput,
whroute,
whtrace
)
```

Or via full path.

```python
from esmerald.routing.webhooks.handlers import (
whdelete,
whead,
whget,
whoptions,
whpatch,
whpost,
whput,
whroute,
whtrace
)
```

## An Esmerald application with webhooks

When you create an **Esmerald** application, as mentioned before, there is a `webhooks` attribute
that you use to define your application `webhooks`, in a similar way you define the `routes`.

```python hl_lines="6 21 16 28"
{!> ../docs_src/routing/webhooks/example.py !}
```

Note how the `whpost` and `post` are declared inside the `webhooks` and `routes` respectively,
**similar but not the same** and how the `whpost` **does not require** the `/` for the path.

The webhooks you define **will end up** in the **OpenAPI** schema automatically.

### Using the APIView to generate webhooks

Since Esmerald supports class based views, that also means you can also use them to generate
webhooks.

```python
{!> ../docs_src/routing/webhooks/cbv.py !}
```

## Important

Notice that with webhooks you are actually not declaring a path (like `/user`). The text you pass
there is just a `name` or an **identifier** of the webhook (name of the event).

This happens because it is expected that your users would actually define the proper URL path where
they want to receive the webhook in some way.

## Check out the docs

Let us see how it would look like in the docs if we were declaring the webhooks from the examples.

**First example, no Class Based Views**

<img src="https://res.cloudinary.com/dymmond/image/upload/v1690305100/esmerald/webhooks/first-example_szu28y.png" title="First example" />

**Second example, with Class Based Views**

<img src="https://res.cloudinary.com/dymmond/image/upload/v1690305101/esmerald/webhooks/second-example_hdqsif.png" title="First example" />
23 changes: 23 additions & 0 deletions docs_src/extras/form/as_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Any, Dict

import httpx

from esmerald import Esmerald, Form, Gateway, post


@post("/create")
async def create(data: Dict[str, str] = Form()) -> Dict[str, str]:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
return data


app = Esmerald(routes=[Gateway(handler=create)])

# Payload example
data = {"name": "example", "email": "[email protected]"}

# Send the request
httpx.post("/create", data=data)
29 changes: 29 additions & 0 deletions docs_src/extras/form/dataclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass

import httpx

from esmerald import Esmerald, Form, Gateway, post


@dataclass
class User:
name: str
email: str


@post("/create")
async def create(data: User = Form()) -> User:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
return data


app = Esmerald(routes=[Gateway(handler=create)])

# Payload example
data = {"name": "example", "email": "[email protected]"}

# Send the request
httpx.post("/create", data=data)
14 changes: 14 additions & 0 deletions docs_src/extras/form/form_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Any, Dict

from esmerald import Esmerald, Form, Gateway, post


@post("/create")
async def create_user(data: Dict[str, Any] = Form()) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""


app = Esmerald(routes=[Gateway(handler=create_user)])
Loading