Skip to content

Returning Responses

Basic Responses

In any web framework, returning a response can be as simple as returning a string of text or quite complex with all sorts of things like server-side rendering. Right out of the box, View supports returning status codes, headers, and a response without any fancy tooling. A response must contain a body (this is a str), but may also contain a status (int) or headers (dict[str, str]). These may be in any order.

from view import new_app

app = new_app()

@app.get("/")
async def index():
    return "Hello, view.py", 201, {"x-my-header": "my_header"}

HTTP Errors

Generally when returning a client error or server error, you want to skip future execution. For example:

from view import new_app

app = new_app()

@app.get("/")
async def index(number: int):
    if number == 1:
        return "number cannot be one", 400

    return f"your number is {number}"

app.run()

However, manually returning can be messy. For this, view.py provides you the Error class, which behaves like an Exception. It takes two parameters:

  • The status code, which is 400 by default.
  • The message to send back to the user. If this is None, it uses the default error message (e.g. Bad Request for error 400).

Since Error works like a Python exception, you can raise it just fine:

from view import new_app, Error

app = new_app()

@app.get("/")
async def index(number: int):
    if number == 1:
        raise Error(400)

    return f"your number is {number}"

app.run()

Warning

Error can only be used to send back error responses. It can not be used to return status codes such as 200.

Caching

Sometimes, computing the response for a route can be expensive or unnecessary. For this, view.py, along with many other web frameworks, provide the ability to cache responses.

View lets you do this by using the cache_rate parameter on a router.

For example:

from view import new_app

app = new_app()

@app.get("/", cache_rate=10)  # reload this route every 10 requests
async def index():
    return "..."

app.run()

You can see this in more detail by using a route that changes it's responses:

from view import new_app

app = new_app()
count = 1

@app.get("/", cache_rate=10)
async def index():
    global count
    count += 1
    return str(count)

app.run()

In the above example, index is only called every 10 requests, so after 20 calls, count would be 2.

Response Protocol

If you have some sort of object that you want to wrap a response around, view.py gives you the __view_response__ protocol. The only requirements are:

  • __view_response__ is available on the returned object (doesn't matter if it's static or instance)
  • __view_response__ returns data that corresponds to the allowed return values.

For example, a type MyObject defining __view_response__ could look like:

from view import new_app

app = new_app()

class MyObject:
    def __view_response__(self):
        return "Hello from MyObject!", {"x-www-myobject": "foo"}

@app.get("/")
async def index():
    return MyObject()  # this is ok

app.run()

Note that in the above scenario, you wouldn't actually need a whole object. Instead, you could also just define a utility function:

def _response():
    return "Hello, view.py!", {"foo": "bar"}

@app.get("/")
async def index():
    return _response()

Response Objects

View comes with two built in response objects: Response and HTML.

  • Response is simply a wrapper around other responses.
  • HTML is for returning HTML content.
  • JSON is for returning JSON content.

view.response.Response

Bases: Generic[T]

Wrapper for responses.

Source code in src/view/response.py
class Response(Generic[T]):
    """Wrapper for responses."""

    def __init__(
        self,
        body: T,
        status: int = 200,
        headers: dict[str, str] | None = None,
        *,
        body_translate: BodyTranslateStrategy | None = _Find,
    ) -> None:
        self.body = body
        self.status = status
        self.headers = headers or {}
        self._raw_headers: list[tuple[bytes, bytes]] = []

        if body_translate:
            self.translate = body_translate
        else:
            self.translate = "str" if not hasattr(body, "__view_result__") else "result"

    def _custom(self, body: T) -> str:
        raise NotImplementedError('the "custom" translate strategy can only be used in subclasses that implement it')  # noqa

    def cookie(
        self,
        key: str,
        value: str = "",
        *,
        max_age: int | None = None,
        expires: int | DateTime | None = None,
        path: str | None = None,
        domain: str | None = None,
        http_only: bool = False,
        same_site: SameSite = "lax",
        partitioned: bool = False,
        secure: bool = False,
    ) -> None:
        """Set a cookie.

        Args:
            key: Cookie name.
            value: Cookie value.
            max_age: Max age of the cookies.
            expires: When the cookie expires.
            domain: Domain the cookie is valid at.
            http_only: Whether the cookie should be HTTP only.
            same_site: SameSite setting for the cookie.
            partitioned: Whether to tie it to the top level site.
            secure: Whether the cookie should enforce HTTPS."""
        cookie_str = f"{key}={value}; SameSite={same_site}".encode()

        if expires:
            dt = (
                expires
                if isinstance(expires, DateTime)
                else DateTime.fromtimestamp(expires)
            )
            ts = timestamp(dt)
            cookie_str += f"; Expires={ts}".encode()

        if http_only:
            cookie_str += b"; HttpOnly"

        if domain:
            cookie_str += f"; Domain={domain}".encode()

        if max_age:
            cookie_str += f"; Max-Age={max_age}".encode()

        if partitioned:
            cookie_str += b"; Partitioned"

        if secure:
            cookie_str += b"; Secure"

        if path:
            cookie_str += f"; Path={path}".encode()

        self._raw_headers.append((b"Set-Cookie", cookie_str))

    def _build_headers(self) -> tuple[tuple[bytes, bytes], ...]:
        headers: list[tuple[bytes, bytes]] = [*self._raw_headers]

        for k, v in self.headers.items():
            headers.append((k.encode(), v.encode()))

        return tuple(headers)

    def __view_result__(self):
        body: str = ""
        if self.translate == "str":
            body = str(self.body)
        elif self.translate == "repr":
            body = repr(self.body)
        elif self.translate == "custom":
            body = self._custom(self.body)
        else:
            view_result = getattr(self.body, "__view_result__", None)

            if not view_result:
                raise AttributeError(f"{self.body!r} has no __view_result__")

            body_res = view_result()
            if isinstance(body_res, str):
                body = body_res
            else:
                for i in body_res:
                    if isinstance(i, str):
                        body = i

        return body, self.status, self._build_headers()

view.response.Response.cookie(key: str, value: str = '', *, max_age: int | None = None, expires: int | DateTime | None = None, path: str | None = None, domain: str | None = None, http_only: bool = False, same_site: SameSite = 'lax', partitioned: bool = False, secure: bool = False) -> None

Set a cookie.

Parameters:

Name Type Description Default
key str

Cookie name.

required
value str

Cookie value.

''
max_age int | None

Max age of the cookies.

None
expires int | datetime | None

When the cookie expires.

None
domain str | None

Domain the cookie is valid at.

None
http_only bool

Whether the cookie should be HTTP only.

False
same_site SameSite

SameSite setting for the cookie.

'lax'
partitioned bool

Whether to tie it to the top level site.

False
secure bool

Whether the cookie should enforce HTTPS.

False
Source code in src/view/response.py
def cookie(
    self,
    key: str,
    value: str = "",
    *,
    max_age: int | None = None,
    expires: int | DateTime | None = None,
    path: str | None = None,
    domain: str | None = None,
    http_only: bool = False,
    same_site: SameSite = "lax",
    partitioned: bool = False,
    secure: bool = False,
) -> None:
    """Set a cookie.

    Args:
        key: Cookie name.
        value: Cookie value.
        max_age: Max age of the cookies.
        expires: When the cookie expires.
        domain: Domain the cookie is valid at.
        http_only: Whether the cookie should be HTTP only.
        same_site: SameSite setting for the cookie.
        partitioned: Whether to tie it to the top level site.
        secure: Whether the cookie should enforce HTTPS."""
    cookie_str = f"{key}={value}; SameSite={same_site}".encode()

    if expires:
        dt = (
            expires
            if isinstance(expires, DateTime)
            else DateTime.fromtimestamp(expires)
        )
        ts = timestamp(dt)
        cookie_str += f"; Expires={ts}".encode()

    if http_only:
        cookie_str += b"; HttpOnly"

    if domain:
        cookie_str += f"; Domain={domain}".encode()

    if max_age:
        cookie_str += f"; Max-Age={max_age}".encode()

    if partitioned:
        cookie_str += b"; Partitioned"

    if secure:
        cookie_str += b"; Secure"

    if path:
        cookie_str += f"; Path={path}".encode()

    self._raw_headers.append((b"Set-Cookie", cookie_str))

view.response.HTML

Bases: Response[HTMLContent]

HTML response wrapper.

Source code in src/view/response.py
class HTML(Response[HTMLContent]):
    """HTML response wrapper."""

    def __init__(
        self,
        body: HTMLContent,
        status: int = 200,
        headers: dict[str, str] | None = None,
    ) -> None:

        super().__init__(body, status, headers, body_translate="custom")
        self._raw_headers.append((b"content-type", b"text/html"))

    def _custom(self, body: HTMLContent) -> str:
        parsed_body = ""

        if isinstance(body, Path):
            parsed_body = body.read_text()
        elif isinstance(body, str):
            parsed_body = body
        elif isinstance(body, DOMNode):
            parsed_body = body.data
        else:
            try:
                parsed_body = body.read()
            except AttributeError:
                raise TypeError(
                    f"expected TextIO, str, Path, or DOMNode, not {type(body)}",  # noqa
                ) from None

        return parsed_body

view.response.JSON

Bases: Response[Dict[str, Any]]

JSON response wrapper.

Source code in src/view/response.py
class JSON(Response[Dict[str, Any]]):
    """JSON response wrapper."""

    def __init__(
        self,
        body: dict[str, Any],
        status: int = 200,
        headers: dict[str, str] | None = None,
    ) -> None:
        super().__init__(body, status, headers, body_translate="custom")
        self._raw_headers.append((b"content-type", b"application/json"))

    def _custom(self, body: dict[str, Any]) -> str:
        return ujson.dumps(body)

A common use case for Response is wrapping an object that has a __view_response__ and changing one of the values. For example:

from view import new_app, Response

app = new_app()

class Test:
    def __view_result__(self):
        return "test", 201

@app.get("/")
async def index():
    return Response(Test(), status=200)  # 200 is returned, not 201

app.run()

Another common case for Response is using cookies. You can add a cookie to the response via the cookie method:

@app.get("/")
async def index():
    res = Response(...)
    res.cookie("hello", "world")
    return res

view.response.Response.cookie(key: str, value: str = '', *, max_age: int | None = None, expires: int | DateTime | None = None, path: str | None = None, domain: str | None = None, http_only: bool = False, same_site: SameSite = 'lax', partitioned: bool = False, secure: bool = False) -> None

Set a cookie.

Parameters:

Name Type Description Default
key str

Cookie name.

required
value str

Cookie value.

''
max_age int | None

Max age of the cookies.

None
expires int | datetime | None

When the cookie expires.

None
domain str | None

Domain the cookie is valid at.

None
http_only bool

Whether the cookie should be HTTP only.

False
same_site SameSite

SameSite setting for the cookie.

'lax'
partitioned bool

Whether to tie it to the top level site.

False
secure bool

Whether the cookie should enforce HTTPS.

False
Source code in src/view/response.py
def cookie(
    self,
    key: str,
    value: str = "",
    *,
    max_age: int | None = None,
    expires: int | DateTime | None = None,
    path: str | None = None,
    domain: str | None = None,
    http_only: bool = False,
    same_site: SameSite = "lax",
    partitioned: bool = False,
    secure: bool = False,
) -> None:
    """Set a cookie.

    Args:
        key: Cookie name.
        value: Cookie value.
        max_age: Max age of the cookies.
        expires: When the cookie expires.
        domain: Domain the cookie is valid at.
        http_only: Whether the cookie should be HTTP only.
        same_site: SameSite setting for the cookie.
        partitioned: Whether to tie it to the top level site.
        secure: Whether the cookie should enforce HTTPS."""
    cookie_str = f"{key}={value}; SameSite={same_site}".encode()

    if expires:
        dt = (
            expires
            if isinstance(expires, DateTime)
            else DateTime.fromtimestamp(expires)
        )
        ts = timestamp(dt)
        cookie_str += f"; Expires={ts}".encode()

    if http_only:
        cookie_str += b"; HttpOnly"

    if domain:
        cookie_str += f"; Domain={domain}".encode()

    if max_age:
        cookie_str += f"; Max-Age={max_age}".encode()

    if partitioned:
        cookie_str += b"; Partitioned"

    if secure:
        cookie_str += b"; Secure"

    if path:
        cookie_str += f"; Path={path}".encode()

    self._raw_headers.append((b"Set-Cookie", cookie_str))

Note that all response classes inherit from Response, meaning you can use this functionality anywhere.

Note

A Response must be returned for things like cookie to take effect. For example:

from view import new_app, Response

app = new_app()

@app.get("/")
async def index():
    res = Response(...)
    return "..."  # res is not returned!

app.run()

Body Translate Strategy

The body translate strategy in the __view_response__ protocol refers to how the Response class will translate the body into a str. There are four available strategies:

  • str, which uses the object's __str__ method.
  • repr, which uses the object's __repr__ method.
  • result, which calls the __view_response__ protocol implemented on the object (assuming it exists).
  • custom, uses the Response instance's _custom attribute (this only works on subclasses of Response that implement it).

For example, the route below would return the string "'hi'":

from view import new_app, Response

app = new_app()

@app.get("/")
async def index():
    res = Response('hi', body_translate="repr")
    return res

app.run()

Implementing Responses

Response is a generic type, meaning you should supply it a type argument when writing a class that inherits from it.

For example, if you wanted to write a type that takes a str:

class MyResponse(Response[str]):
    def __init__(self, body: str) -> None:
        super().__init__(body)

Generally, you'll want to use the custom translation strategy when writing custom Response objects.

You must implement the _custom method (which takes in the T passed to Response, and returns a str) to use the custom strategy. For example, the code below would be for a Response type that formats a list:

from view import Response

class ListResponse(Response[list]):
    def __init__(self, body: list) -> None:
        super().__init__(body)

    def _custom(self, body: list) -> str:
        return " ".join(body)

Middleware

What is middleware?

In view.py, middleware is called right before the route is executed, but not necessarily in the middle. However, for tradition, View calls it middleware.

The main difference between middleware in view.py and other frameworks is that in view.py, there is no call_next function in middleware, and instead just the arguments that would go to the route.

Why no call_next?

view.py doesn't use the call_next function because of the nature of it's routing system.

The Middleware API

Route.middleware is used to define a middleware function for a route.

from view import new_app

app = new_app()

@app.get("/")
async def index():
    ...

@index.middleware
async def index_middleware():
    print("this is called before index()!")

app.run()

view.routing.Route.middleware(func_or_none: Middleware | None = None)

Define a middleware function for the route.

Source code in src/view/routing.py
def middleware(self, func_or_none: Middleware | None = None):
    """Define a middleware function for the route."""
    def inner(func: Middleware):
        self.middleware_funcs.append(func)

    if func_or_none:
        return inner(func_or_none)

    return inner

Review

Responses can be returned with a string, integer, and/or dictionary in any order.

  • The string represents the body of the response (e.g. the HTML or JSON)
  • The integer represents the status code (200 by default)
  • The dictionary represents the headers (e.g. {"x-www-my-header": "some value"})

Response objects can also be returned, which implement the __view_response__ protocol. All response classes inherit from Response, which supports operations like setting cookies.

Finally, the middleware method on a Route can be used to implement middleware.