diff --git a/docs/extras/forms.md b/docs/extras/forms.md new file mode 100644 index 00000000..e67a65fb --- /dev/null +++ b/docs/extras/forms.md @@ -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. diff --git a/docs/responses.md b/docs/responses.md index 8fb0fe88..4c46f494 100644 --- a/docs/responses.md +++ b/docs/responses.md @@ -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: @@ -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 @@ -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 !} +``` diff --git a/docs/routing/webhooks.md b/docs/routing/webhooks.md new file mode 100644 index 00000000..6d1bbae8 --- /dev/null +++ b/docs/routing/webhooks.md @@ -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** + + + +**Second example, with Class Based Views** + + diff --git a/docs_src/extras/form/as_dict.py b/docs_src/extras/form/as_dict.py new file mode 100644 index 00000000..93897751 --- /dev/null +++ b/docs_src/extras/form/as_dict.py @@ -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": "example@esmerald.dev"} + +# Send the request +httpx.post("/create", data=data) diff --git a/docs_src/extras/form/dataclass.py b/docs_src/extras/form/dataclass.py new file mode 100644 index 00000000..3b6cc470 --- /dev/null +++ b/docs_src/extras/form/dataclass.py @@ -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": "example@esmerald.dev"} + +# Send the request +httpx.post("/create", data=data) diff --git a/docs_src/extras/form/form_object.py b/docs_src/extras/form/form_object.py new file mode 100644 index 00000000..21d0b95e --- /dev/null +++ b/docs_src/extras/form/form_object.py @@ -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)]) diff --git a/docs_src/extras/form/model.py b/docs_src/extras/form/model.py new file mode 100644 index 00000000..6ec775bf --- /dev/null +++ b/docs_src/extras/form/model.py @@ -0,0 +1,27 @@ +import httpx +from pydantic import BaseModel + +from esmerald import Esmerald, Form, Gateway, post + + +class User(BaseModel): + 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": "example@esmerald.dev"} + +# Send the request +httpx.post("/create", data=data) diff --git a/docs_src/extras/form/pydantic_dc.py b/docs_src/extras/form/pydantic_dc.py new file mode 100644 index 00000000..7023107a --- /dev/null +++ b/docs_src/extras/form/pydantic_dc.py @@ -0,0 +1,28 @@ +import httpx +from pydantic.dataclasses import dataclass + +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": "example@esmerald.dev"} + +# Send the request +httpx.post("/create", data=data) diff --git a/docs_src/responses/template.py b/docs_src/responses/template.py index b47d8945..0b650780 100644 --- a/docs_src/responses/template.py +++ b/docs_src/responses/template.py @@ -14,6 +14,7 @@ def home() -> Template: return Template( name="my-tem", context={"user": "me"}, + alternative_template=..., ) diff --git a/docs_src/routing/webhooks/cbv.py b/docs_src/routing/webhooks/cbv.py new file mode 100644 index 00000000..c5728ce7 --- /dev/null +++ b/docs_src/routing/webhooks/cbv.py @@ -0,0 +1,34 @@ +from datetime import datetime + +from pydantic import BaseModel + +from esmerald import APIView, Esmerald, Gateway, post +from esmerald.routing.gateways import WebhookGateway +from esmerald.routing.webhooks.handlers import whpost + + +class Payment(BaseModel): + is_paid: bool + amount: float + paid_at: datetime + + +class PaymentWebhook(APIView): + @whpost("new-event") + async def new_event(self, data: Payment) -> None: + ... + + @whpost("payments") + async def new_payment(self, data: Payment) -> None: + ... + + +@post("/create") +async def create_payment(data: Payment) -> None: + ... + + +app = Esmerald( + routes=[Gateway(handler=create_payment)], + webhooks=[WebhookGateway(handler=PaymentWebhook)], +) diff --git a/docs_src/routing/webhooks/example.py b/docs_src/routing/webhooks/example.py new file mode 100644 index 00000000..dad83861 --- /dev/null +++ b/docs_src/routing/webhooks/example.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from pydantic import BaseModel + +from esmerald import Esmerald, Gateway, post +from esmerald.routing.gateways import WebhookGateway +from esmerald.routing.webhooks.handlers import whpost + + +class Payment(BaseModel): + is_paid: bool + amount: float + paid_at: datetime + + +@whpost("new-event") +async def new_event(data: Payment) -> None: + ... + + +@post("/create") +async def create_payment(data: Payment) -> None: + ... + + +app = Esmerald( + routes=[Gateway(handler=create_payment)], + webhooks=[WebhookGateway(handler=new_event)], +) diff --git a/mkdocs.yml b/mkdocs.yml index dbe44a28..5682d801 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,8 @@ nav: - Routes: "routing/routes.md" - Handlers: "routing/handlers.md" - APIView: "routing/apiview.md" + - OpenAPI Specific: + - Webhooks: "routing/webhooks.md" - Databases: - Saffier: - Motivation: "databases/saffier/motivation.md" @@ -85,6 +87,7 @@ nav: - Extra, Advanced & Useful: - Request Data: "extras/request-data.md" - Upload Files: "extras/upload-files.md" + - Form: "extras/forms.md" - Body: "extras/body-fields.md" - Headers: "extras/header-fields.md" - Cookies: "extras/cookie-fields.md"