APIView¶
This is a special object from Esmerald and aims to implement the so needed class based views for those who love object oriented programming. Inspired by such great frameworks (Python, Go, JS), APIView was created to simplify the life of those who like OOP.
APIView class¶
from esmerald.permissions import DenyAll, IsAuthenticated
from esmerald.requests import Request
from esmerald.responses import JSONResponse
from esmerald.routing.apis.views import APIView
from esmerald.routing.handlers import delete, get, post
class UserAPIView(APIView):
path = "/users"
permissions = [IsAuthenticated]
@get(path="/")
async def all_users(self, request: Request) -> JSONResponse:
# logic to get all users here
users = ...
return JSONResponse({"users": users})
@get(path="/deny", permissions=[DenyAll], description="API description")
async def all_usersa(self, request: Request) -> JSONResponse: ...
@get(path="/allow")
async def all_usersb(self, request: Request) -> JSONResponse:
users = ...
return JSONResponse({"Total Users": users.count()})
@post(path="/create")
async def create_user(self, request: Request) -> None:
# logic to create a user goes here
...
@delete(path="/delete/{user_id}")
async def delete_user(self, request: Request, user_id: str) -> None:
# logic to delete a user goes here
...
The APIView uses the Esmerald handlers to create the "view" itself but also acts as the parent
of those same routes and therefore all the available parameters such as permissions,
middlewares, exception handlers,
dependencies and almost every other parameter available in the handlers are also available
in the APIView.
Parameters¶
All the parameters and defaults are available in the View Reference.
APIView routing¶
The routing is the same as declaring the routing for the handler with a simple particularity that you don't need to declare handler by handler. Since everything is inside an APIView objects the handlers will be automatically routed by Esmerald with the joint path given to class.
from esmerald.permissions import DenyAll, IsAuthenticated
from esmerald.requests import Request
from esmerald.responses import JSONResponse
from esmerald.routing.apis.views import APIView
from esmerald.routing.handlers import delete, get, post
class UserAPIView(APIView):
path = "/users"
permissions = [IsAuthenticated]
@get(path="/")
async def all_users(self, request: Request) -> JSONResponse:
# logic to get all users here
users = ...
return JSONResponse({"users": users})
@get(path="/deny", permissions=[DenyAll], description="API description")
async def all_usersa(self, request: Request) -> JSONResponse: ...
@get(path="/allow")
async def all_usersb(self, request: Request) -> JSONResponse:
users = ...
return JSONResponse({"Total Users": users.count()})
@post(path="/create")
async def create_user(self, request: Request) -> None:
# logic to create a user goes here
...
@delete(path="/delete/{user_id}")
async def delete_user(self, request: Request, user_id: str) -> None:
# logic to delete a user goes here
...
from esmerald import Esmerald, Gateway
from .controllers import UserAPIView
app = Esmerald(routes=[Gateway(handler=UserAPIView)])
APIView path¶
In the APIView the path
is a mandatory field, even if you pass only /
. This helps maintaining the
structure of the routing cleaner and healthy.
Warning
Just because the APIView
is a class it still follows the same rules of the
routing priority as well.
Path parameters¶
APIView is no different from the handlers, really. The same rules for the routing are applied for any route path param.
from esmerald import APIView, Esmerald, Gateway, get
class MyAPIView(APIView):
path = "/customer/{name}"
@get(path="/")
def home(self, name: str) -> str: # type: ignore[valid-type]
return name
@get(path="/info")
def info(self, name: str) -> str: # type: ignore[valid-type]
return f"Test {name}"
@get(path="/info/{param}")
def info_detail(self, name: str, param: str) -> str: # type: ignore[valid-type]
return f"Test {name}"
app = Esmerald(routes=[Gateway(handler=MyAPIView)])
Websockets and handlers¶
The APIView also allows the mix of both HTTP handlers and WebSocket handlers
from pydantic import BaseModel
from esmerald import APIView, Esmerald, WebSocket, get, websocket
from esmerald.routing.gateways import Gateway
class Item(BaseModel):
name: str
sku: str
class MyAPIView(APIView):
path = "/"
@get(path="/")
def get_person(self) -> Item: ...
@websocket(path="/socket")
async def ws(self, socket: WebSocket) -> None:
await socket.accept()
await socket.send_json({"data": "123"})
await socket.close()
app = Esmerald(routes=[Gateway(handler=MyAPIView)])
Constraints¶
When declaring an APIView and registering the route, both Gateway and WebSocketGateway allow to be used for this purpose but one has a limitation compared to the other.
- Gateway - Allows the APIView to have all the available handlers (
get
,put
,post
...) includingwebsocket
. - WebSocketGateway - Allows only to have
websockets
.
Generics¶
Esmerald also offers some generics when it comes to build APIs. For example, the APIView
allows the creation of apis where the function name can be whatever you desire like create_users
,
get_items
, update_profile
, etc...
Generics in Esmerald are more restrict.
So what does that mean? Means you can only perform operations where the function name coincides with the http verb.
For example, get
, put
, post
etc...
If you attempt to create a function where the name differs from a http verb,
an ImproperlyConfigured
exception is raised unless the extra_allowed
is declared.
The available http verbs are:
GET
POST
PUT
PATCH
DELETE
HEAD
OPTIONS
TRACE
Basically the same availability as the handlers.
Important¶
The generics enforce the name matching of the functions with the handlers. That means, if
you use a ReadAPIView
that only allows the get
and you use the wrong handlers
on the top of it, for example a post, an ImproperlyConfigured
exception
will be raised.
Let us see what this means.
from esmerald import post
from esmerald.routing.apis.generics import CreateAPIView
class UserAPI(CreateAPIView):
"""
ImproperlyConfigured will be raised as the handler `post()`
name does not match the function name `post`.
"""
@post()
async def get(self) -> str: ...
As you can see, the handler post()
does not match the function name get
. It should always match.
An easy way of knowing this is simple, when it comes to the available http verbs, the function name should always match the handler.
Are there any exception? Yes but not for these specific cases, the exceptions are called extra_allowed but more details about this later on.
SimpleAPIView¶
This is the base of all generics, subclassing from this class will allow you to perform all the available http verbs without any restriction.
This is how you can import.
from esmerald import SimpleAPIView
Example¶
from esmerald import SimpleAPIView, delete, get, patch, post, put
class UserAPI(SimpleAPIView):
@get()
async def get(self) -> str: ...
@post()
async def post(self) -> str: ...
@put()
async def put(self) -> str: ...
@patch()
async def patch(self) -> str: ...
@delete()
async def delete(self) -> None: ...
ReadAPIView¶
Allows the GET
verb to be used.
This is how you can import.
from esmerald.routing.apis.generics import ReadAPIView
Example¶
from esmerald import get
from esmerald.routing.apis.generics import ReadAPIView
class UserAPI(ReadAPIView):
"""
ReadAPIView only allows the `get` to be used by default.
"""
@get()
async def get(self) -> str: ...
CreateAPIView¶
Allows the POST
, PUT
, PATCH
verbs to be used.
This is how you can import.
from esmerald.routing.apis.generics import CreateAPIView
Example¶
from esmerald import patch, post, put
from esmerald.routing.apis.generics import CreateAPIView
class UserAPI(CreateAPIView):
"""
CreateAPIView only allows the `post`, `put` and `patch`
to be used by default.
"""
@post()
async def post(self) -> str: ...
@put()
async def put(self) -> str: ...
@patch()
async def patch(self) -> str: ...
DeleteAPIView¶
Allows the DELETE
verb to be used.
This is how you can import.
from esmerald.routing.apis.generics import DeleteAPIView
Example¶
from esmerald import delete
from esmerald.routing.apis.generics import DeleteAPIView
class UserAPI(DeleteAPIView):
"""
DeleteAPIView only allows the `delete` to be used by default.
"""
@delete()
async def delete(self) -> None: ...
Combining all in one¶
What if you want to combine them all? Of course you also can.
from esmerald import delete, get, patch, post, put
from esmerald.routing.apis.generics import CreateAPIView, DeleteAPIView, ReadAPIView
class UserAPI(CreateAPIView, DeleteAPIView, ReadAPIView):
"""
Combining them all.
"""
@get()
async def get(self) -> str: ...
@post()
async def post(self) -> str: ...
@put()
async def put(self) -> str: ...
@patch()
async def patch(self) -> str: ...
@delete()
async def delete(self) -> None: ...
Combining them all is the same as using the SimpleAPIView.
ListAPIView¶
This is a nice to have type of generic. In principle, all the functions must return lists or None of any kind.
This generic enforces the return annotations to always be lists or None.
Allows all the verbs be used.
This is how you can import.
from esmerald.routing.apis.generics import ListAPIView
Example¶
from typing import List
from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import ListAPIView
class UserAPI(ListAPIView):
@get()
async def get(self) -> List[str]: ...
@post()
async def post(self) -> List[str]: ...
@put()
async def put(self) -> List[str]: ...
@patch()
async def patch(self) -> List[str]: ...
This is another generic that follows the same rules of the SimpleAPIView, which
means, if you want to add extra
functions such as a read_item()
or anything else, you must
follow the extra allowed principle.
from typing import List
from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import ListAPIView
class UserAPI(ListAPIView):
extra_allowed: List[str] = ["read_item"]
@post()
async def post(self) -> List[str]: ...
@put()
async def put(self) -> List[str]: ...
@patch()
async def patch(self) -> List[str]: ...
@get()
async def read_item(self) -> List[str]: ...
extra_allowed¶
All the generics subclass the SimpleAPIView as mentioned before and that superclass
uses the http_allowed_methods
to verify which methods are allowed or not to be passed inside
the API object but also check if there is any extra_allowed
list with any extra functions you
would like the view to deliver.
This means that if you want to add a read_item()
function to any of the
generics you also do it easily.
from typing import List
from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import CreateAPIView
class UserAPI(CreateAPIView):
"""
CreateAPIView only allows the `post`, `put` and `patch`
to be used by default.
"""
extra_allowed: List[str] = ["read_item"]
@post()
async def post(self) -> str: ...
@put()
async def put(self) -> str: ...
@patch()
async def patch(self) -> str: ...
@get()
async def read_item(self) -> str: ...
As you can see, to make it happen you would need to declare the function name inside the
extra_allowed
to make sure that an ImproperlyConfigured
is not raised.
What to choose¶
All the available objects from the APIView to the SimpleAPIView and generics can do whatever you want and need so what and how to choose the right one for you?
Well, like everything, it will depend of what you want to achieve. For example, if you do not care
or do not want to be bothered with http_allowed_methods
and want to go without restrictions,
then the APIView is the right choice for you.
On the other hand, if you feel like restricting youself or even during development you might want to restrict some actions on the fly, so maybe you can opt for choosing the SimpleAPIView or any of the generics.
Your take!