Skip to content

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.

controllers.py
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
        ...
app.py
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.

app.py
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

app.py
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...) including websocket.
  • 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!