Controller¶
This is a special object from Ravyn 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), Controller was created to simplify the life of those who like OOP.
Controller class¶
from ravyn.permissions import DenyAll, IsAuthenticated
from ravyn.requests import Request
from ravyn.responses import JSONResponse
from ravyn.routing.controllers.views import Controller
from ravyn.routing.handlers import delete, get, post
class UserAPIView(Controller):
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 Controller uses the Ravyn 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 Controller.
Parameters¶
All the parameters and defaults are available in the BaseController Reference.
Controller 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 Controller objects the handlers will be automatically routed by Ravyn with the joint path given to class.
from ravyn.permissions import DenyAll, IsAuthenticated
from ravyn.requests import Request
from ravyn.responses import JSONResponse
from ravyn.routing.controllers.views import Controller
from ravyn.routing.handlers import delete, get, post
class UserAPIView(Controller):
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 ravyn import Ravyn, Gateway
from .controllers import UserAPIView
app = Ravyn(routes=[Gateway(handler=UserAPIView)])
Controller path¶
In the Controller 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 Controller
is a class it still follows the same rules of the
routing priority as well.
Path parameters¶
Controller is no different from the handlers, really. The same rules for the routing are applied for any route path param.
from ravyn import Controller, Ravyn, Gateway, get
class MyAPIView(Controller):
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 = Ravyn(routes=[Gateway(handler=MyAPIView)])
Websockets and handlers¶
The Controller also allows the mix of both HTTP handlers and WebSocket handlers
from pydantic import BaseModel
from ravyn import Controller, Ravyn, WebSocket, get, websocket
from ravyn.routing.gateways import Gateway
class Item(BaseModel):
name: str
sku: str
class MyAPIView(Controller):
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 = Ravyn(routes=[Gateway(handler=MyAPIView)])
Constraints¶
When declaring an Controller 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 Controller to have all the available handlers (
get
,put
,post
...) includingwebsocket
. - WebSocketGateway - Allows only to have
websockets
.
Generics¶
Ravyn also offers some generics when it comes to build APIs. For example, the Controller
allows the creation of apis where the function name can be whatever you desire like create_users
,
get_items
, update_profile
, etc...
Generics in Ravyn 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 ReadAPIController
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 ravyn import post
from ravyn.routing.controllers.generics import CreateAPIController
class UserAPI(CreateAPIController):
"""
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 ravyn import SimpleAPIView
Example¶
from ravyn 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: ...
ReadAPIController¶
Allows the GET
verb to be used.
This is how you can import.
from ravyn.routing.controllers.generics import ReadAPIController
Example¶
from ravyn import get
from ravyn.routing.controllers.generics import ReadAPIController
class UserAPI(ReadAPIController):
"""
ReadAPIController only allows the `get` to be used by default.
"""
@get()
async def get(self) -> str: ...
CreateAPIController¶
Allows the POST
, PUT
, PATCH
verbs to be used.
This is how you can import.
from ravyn.routing.controllers.generics import CreateAPIController
Example¶
from ravyn import patch, post, put
from ravyn.routing.controllers.generics import CreateAPIController
class UserAPI(CreateAPIController):
"""
CreateAPIController 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: ...
DeleteAPIController¶
Allows the DELETE
verb to be used.
This is how you can import.
from ravyn.routing.controllers.generics import DeleteAPIController
Example¶
from ravyn import delete
from ravyn.routing.controllers.generics import DeleteAPIController
class UserAPI(DeleteAPIController):
"""
DeleteAPIController 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 ravyn import delete, get, patch, post, put
from ravyn.routing.controllers.generics import (
CreateAPIController,
DeleteAPIController,
ReadAPIController,
)
class UserAPI(CreateAPIController, DeleteAPIController, ReadAPIController):
"""
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.
ListAPIController¶
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 ravyn.routing.controllers.generics import ListAPIController
Example¶
from typing import List
from ravyn import get, patch, post, put
from ravyn.routing.controllers.generics import ListAPIController
class UserAPI(ListAPIController):
@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 ravyn import get, patch, post, put
from ravyn.routing.controllers.generics import ListAPIController
class UserAPI(ListAPIController):
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 ravyn import get, patch, post, put
from ravyn.routing.controllers.generics import CreateAPIController
class UserAPI(CreateAPIController):
"""
CreateAPIController 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 Controller 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 Controller 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!