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 or bytes), 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"}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
400by default. - The message to send back to the user. If this is
None, it uses the default error message (e.g.Bad Requestfor error400).
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`.
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.
If you have some sort of object that you want to wrap a response around, view.py gives you the __view_result__ protocol. The only requirements are:
__view_result__is available on the returned object (doesn't matter if it's static or instance)__view_result__returns data that corresponds to the allowed return values.
For example, a type MyObject defining __view_result__ could look like:
from view import new_app
app = new_app()
class MyObject:
def __view_result__(self):
return "Hello from MyObject!", 201, {"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!", 201, {"foo": "bar"}
@app.get("/")
async def index():
return _response()View comes with two built in response objects: Response and HTML.
Responseis simply a wrapper around other responses.HTMLis for returning HTML content.JSONis for returning JSON content.
A common use case for Response is wrapping an object that has a __view_result__ 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 resNote 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:
```py
from view import new_app, Response
app = new_app()
@app.get("/")
async def index():
res = Response(...)
return "..." # res is not returned!
app.run()
```
The body translate strategy in the __view_result__ 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_result__protocol implemented on the object (assuming it exists).custom, uses theResponseinstance's_customattribute (this only works on subclasses ofResponsethat 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()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 translate_body 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, body_translate="custom")
def translate_body(self, body: list) -> str:
return " ".join(body)Route.middleware is used to define a middleware function for a route. Like other web frameworks, middleware functions are given a call_next. Note that call_next is always asynchronous regardless of whether the route is asynchronous.
from view import new_app, CallNext
app = new_app()
@app.get("/")
def index():
return "my response!"
@index.middleware
async def index_middleware(call_next: CallNext):
print("this is called before index()!")
res = await call_next()
print("this is called after index()!")
return res
app.run()As shown above, call_next returns the result of the route. However, dealing with the raw response tuple might be a bit of a hassle. Instead, you can convert the response to a Response object using the to_response function:
from view import new_app, CallNext, to_response
from time import perf_counter
app = new_app()
@app.get("/")
def index():
return "my response!"
@index.middleware
async def took_time_middleware(call_next: CallNext):
a = perf_counter()
res = to_response(await call_next())
b = perf_counter()
res.headers["X-Time-Elapsed"] = str(b - a)
return res
app.run()Responses can be returned with a string, integer, and/or dictionary in any order.
- The string represents the body of the response (e.g. 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_result__ 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.