Responses¶
Like every application, there are many different responses that can be used for different use cases and scenarios.
Esmerald having Lilya
under the hood also means that all available responses from it simply just work.
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.
Esmerald responses and the application¶
The available responses from Esmerald
are:
Response
JSON
OrJSON
UJSON
Template
Redirect
File
Stream
Important requirements¶
Some responses use extra dependencies, such as UJSON and OrJSON. To use these responses, you need to install:
$ pip install ujson orjson
This will allow you to use the OrJSON and UJSON as well as the UJSONResponse and ORJSONResponse in your projects.
Response¶
Classic and generic Response
that fits almost every single use case out there.
from esmerald import (
APIView,
Esmerald,
Gateway,
Request,
Response,
WebSocket,
get,
post,
status,
websocket,
)
class World(APIView):
@get(path="/{url}")
async def home(self, request: Request, url: str) -> Response:
return Response(f"URL: {url}")
@post(path="/{url}", status_code=status.HTTP_201_CREATED)
async def mars(self, request: Request, url: str) -> Response: ...
@websocket(path="/{path_param:str}")
async def pluto(self, socket: WebSocket) -> None:
await socket.accept()
msg = await socket.receive_json()
assert msg
assert socket
await socket.close()
app = Esmerald(routes=[Gateway("/world", handler=World)])
Or a unique custom response:
from pydantic import BaseModel
from esmerald import Esmerald, Gateway, Response, get
from esmerald.datastructures import Cookie
class Item(BaseModel):
id: int
sku: str
@get(path="/me")
async def home() -> Response[Item]:
return Response(
Item(id=1, sku="sku1238"),
headers={"SKY-HEADER": "sku-xyz"},
cookies=[Cookie(key="sku", value="a-value")],
)
app = Esmerald(routes=[Gateway(handler=home)])
Esmerald supports good design, structure and practices but does not force you to follow specific rules of anything unless you want to.
API Reference¶
Check out the API Reference for Response for more details.
JSON¶
The classic JSON response for 99% of the responses used nowaday. The JSON
returns a
JSONResponse
.
from esmerald import JSON, APIView, Esmerald, Gateway, Request, get, post, status
class World(APIView):
@get(path="/{url}")
async def home(self, request: Request, url: str) -> JSON:
return JSON(content=f"URL: {url}")
@post(path="/{url}", status_code=status.HTTP_201_CREATED)
async def mars(self, request: Request, url: str) -> JSON: ...
app = Esmerald(routes=[Gateway("/world", handler=World)])
API Reference¶
Check out the API Reference for JSON for more details.
JSONResponse¶
You can always use directly the JSONResponse
from Lilya without using the Esmerald wrapper.
from lilya.responses import JSONResponse as JSONResponse
or alternatively
from esmerald.responses import JSONResponse
API Reference¶
Check out the API Reference for JSONResponse for more details.
OrJSON¶
Super fast JSON serialization/deserialization response.
from esmerald import APIView, Esmerald, Gateway, Request, get, post, status
from esmerald.datastructures.encoders import OrJSON
class World(APIView):
@get(path="/{url}")
async def home(self, request: Request, url: str) -> OrJSON:
return OrJSON(content=f"URL: {url}")
@post(path="/{url}", status_code=status.HTTP_201_CREATED)
async def mars(self, request: Request, url: str) -> OrJSON: ...
app = Esmerald(routes=[Gateway("/world", handler=World)])
Warning
Please read the important requirements before using this response.
Check
More details about the ORJSOM can be found here.
API Reference¶
Check out the API Reference for OrJSON for more details.
ORJSONResponse¶
You can always use directly the ORJSONResponse
from Esmerald without using the wrapper.
from esmerald.responses.encoders import ORJSONResponse
API Reference¶
Check out the API Reference for ORJSONResponse for more details.
UJSON¶
Another super fast JSON serialization/deserialization response.
from esmerald import APIView, Esmerald, Gateway, Request, get, post, status
from esmerald.datastructures.encoders import UJSON
class World(APIView):
@get(path="/{url}")
async def home(self, request: Request, url: str) -> UJSON:
return UJSON(content=f"URL: {url}")
@post(path="/{url}", status_code=status.HTTP_201_CREATED)
async def mars(self, request: Request, url: str) -> UJSON: ...
app = Esmerald(routes=[Gateway("/world", handler=World)])
Warning
Please read the important requirements before using this response.
Check
More details about the UJSON can be found here. For JSONResponse the way of doing it the same as ORJSONResponse and UJSONResponse.
API Reference¶
Check out the API Reference for UJSON for more details.
UJSONResponse¶
You can always use directly the UJSONResponse
from Esmerald without using the wrapper.
from esmerald.responses.encoders import UJSONResponse
API Reference¶
Check out the API Reference for UJSONResponse for more details.
Template¶
As the name suggests, it is the response used to render HTML templates.
This response returns a TemplateResponse
.
from esmerald import Esmerald, Gateway, Template, get
from esmerald.datastructures import Cookie, ResponseHeader
@get(
path="/home",
response_headers={"local-header": ResponseHeader(value="my-header")},
response_cookies=[
Cookie(key="redirect-cookie", value="redirect-cookie"),
Cookie(key="general-cookie", value="general-cookie"),
],
)
def home() -> Template:
return Template(
name="my-tem",
context={"user": "me"},
alternative_template=...,
)
app = Esmerald(routes=[Gateway(handler=home)])
API Reference¶
Check out the API Reference for Template for more details.
Redirect¶
As the name indicates, it is the response used to redirect to another endpoint/path.
This response returns a ResponseRedirect
.
from esmerald import Esmerald, Gateway, Redirect, get
@get("/another-home")
async def another_home() -> str:
return "another-home"
@get(
path="/home",
)
def home() -> Redirect:
return Redirect(path="/another-home")
app = Esmerald(routes=[Gateway(handler=home), Gateway(handler=another_home)])
API Reference¶
Check out the API Reference for Redirect for more details.
File¶
The File response sends a file. This response returns a FileResponse
.
from esmerald import Esmerald, Gateway, get
from esmerald.datastructures import File
@get(
path="/download",
)
def download() -> File:
return File(
path="/path/to/file",
filename="download.csv",
)
app = Esmerald(routes=[Gateway(handler=download)])
API Reference¶
Check out the API Reference for File for more details.
Stream¶
The Stream response uses the StreamResponse
.
from typing import Generator
from esmerald import Esmerald, Gateway, get
from esmerald.datastructures import Stream
def my_generator() -> Generator[str, None, None]:
count = 0
while True:
count += 1
yield str(count)
@get(
path="/stream",
)
def stream() -> Stream:
return Stream(
iterator=my_generator(),
)
app = Esmerald(routes=[Gateway(handler=stream)])
API Reference¶
Check out the API Reference for Stream for more details.
Important notes¶
Template, Redirect, File and Stream are wrappers
around the Lilya TemplateResponse
, RedirectResponse
, FileResponse
and StreamResponse
.
Those responses are also possible to be used directly without the need of using the wrapper.
The wrappers, like Lilya, also accept the classic parameters such as headers
and cookies
.
Response status codes¶
You need to be mindful when it comes to return a specific status code when using JSON, OrJSON and UJSON wrappers.
Esmerald allows you to pass the status codes via handler and directly via
return of that same response but the if the handler has a status_code
declared, the returned
status_code
takes precedence.
Let us use an example to be more clear. This example is applied to JSON
, UJSON
and OrJSON
.
Status code without declaring in the handler¶
from esmerald import JSON, Esmerald, Gateway, Request, get, status
@get(path="/{url}")
async def home(request: Request, url: str) -> JSON:
return JSON(content=f"URL: {url}", status_code=status.HTTP_202_ACCEPTED)
app = Esmerald(routes=[Gateway(handler=home)])
In this example, the returned status code will be 202 Accepted
as it was declared directly in the
response and not in the handler.
Status code declared in the handler¶
from esmerald import JSON, Esmerald, Gateway, Request, get, status
@get(path="/{url}", status_code=status.HTTP_202_ACCEPTED)
async def home(request: Request, url: str) -> JSON:
return JSON(content=f"URL: {url}")
app = Esmerald(routes=[Gateway(handler=home)])
In this example, the returned status code will also be 202 Accepted
as it was declared directly
in the handler response and not in the handler.
Status code declared in the handler and in the return¶
Now what happens if we declare the status_code in both?
from esmerald import JSON, Esmerald, Gateway, Request, get, status
@get(path="/{url}", status_code=status.HTTP_201_CREATED)
async def home(request: Request, url: str) -> JSON:
return JSON(content=f"URL: {url}", status_code=status.HTTP_202_ACCEPTED)
app = Esmerald(routes=[Gateway(handler=home)])
This will return 202 Accepted and not 201 Created
and the reason for that is because the
return takes precedence over the handler.
OpenAPI Responses¶
This is a special attribute that is used for OpenAPI specification purposes and can be created and added to a specific handler. You can add one or multiple different responses into your specification.
from typing import Union
from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel
class ItemOut(BaseModel):
sku: str
description: str
@post(path='/create', summary="Creates an item", responses={200: OpenAPIResponse(model=ItemOut, description=...)})
async def create() -> Union[None, ItemOut]:
...
This will add an extra response description and details to your OpenAPI spec handler definition.
API Reference¶
Check out the API Reference for OpenAPIResponse for more details.
Important¶
When adding an OpenAPIResponse
you can also vary and override the defaults of each handler. For
example, the @post
defaults to 201 but you might want to add a different response.
from typing import Union
from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel
class ItemOut(BaseModel):
sku: str
description: str
@post(path='/create', summary="Creates an item", responses={201: OpenAPIResponse(model=ItemOut, description=...)})
async def create() -> Union[None, ItemOut]:
...
You also might want to add more than just one response to the handler, for instance, a 401
or any
other.
from typing import Union
from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel
class ItemOut(BaseModel):
sku: str
description: str
class Error(BaseModel):
detail: str
line_number: int
@post(path='/create', summary="Creates an item", responses={
201: OpenAPIResponse(model=ItemOut, description=...),
401: OpenAPIResponse(model=Error, description=...),
}
)
async def create() -> Union[None, ItemOut]:
...
Lists¶
What if you want to specify in the response that you would like to have a list (array) of returned objects?
Let us imagine we want to return a list of an item in one endpoint and a list of users in another.
from typing import Union
from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel, EmailStr
class ItemOut(BaseModel):
sku: str
description: str
class UserOut(BaseModel):
name: str
email: EmailStr
@get(path='/items', summary="Get all the items", responses={
201: OpenAPIResponse(model=[ItemOut], description=...),
}
)
async def get_items() -> Union[None, ItemOut]:
...
@get(path='/users', summary="Get all the users", responses={
201: OpenAPIResponse(model=[UserOut], description=...),
}
)
async def get_items() -> Union[None, UserOut]:
...
As you could notice, we simply added []
in the model to reflect a list in the OpenAPI
specification. That simple.
Errors¶
A ValueError
is raised in the following scenarios:
- You try to pass a model than one pydantic model into a list. The OpenAPIResponse is a mere representation of a response, so be compliant.
- You try to pass a model that is not a subclass of a Pydantic
BaseModel
. - You try to pass a list of non Pydantic
BaseModels
.
When one of these scenarios occur, the following error will be raised.
The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel])
Other responses¶
There are other responses you can have that does not necessessarily have to be the ones provided here. Every case is
unique and you might want to return directly a string
, a dict
, an integer
, a list
or whatever you want.
from pydantic import EmailStr
from esmerald import Esmerald, Gateway, Request, get, post
@get(path="/me")
async def home(request: Request) -> EmailStr:
return request.user.email
@post(path="/create")
async def create(request: Request) -> str:
return "OK"
app = Esmerald(routes=[Gateway(handler=home), Gateway(handler=create)])
Example¶
Below we have a few examples of possible responses recognised by Esmerald automatically.
Pydantic model
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)
Pydantic dataclass
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)
Python dataclass
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)
MsgSpec
from typing import Union
from esmerald import Esmerald, Gateway, post
from esmerald.datastructures.msgspec import Struct
class User(Struct):
name: str
email: Union[str, None] = None
@post()
def create(data: User) -> User:
"""
Returns the same payload sent to the API.
"""
return data
app = Esmerald(routes=[Gateway(handler=create)])