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 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
.
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
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
|
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
view.response.HTML
¶
Bases: Response[HTMLContent]
HTML response wrapper.
Source code in src/view/response.py
view.response.JSON
¶
Bases: Response[Dict[str, Any]]
JSON response wrapper.
Source code in src/view/response.py
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:
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
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:
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 theResponse
instance's_custom
attribute (this only works on subclasses ofResponse
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
:
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
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.