Accessing the list#
Intro#
The first thing you’ll be setting up for our app is a route handler that returns a single TODO list. A TODO list in this case will be a list of dictionaries representing the items on that TODO list.
1from typing import Dict, List, Union
2
3from litestar import Litestar, get
4
5TODO_LIST: List[Dict[str, Union[str, bool]]] = [
6 {"title": "Start writing TODO list", "done": True},
7 {"title": "???", "done": False},
8 {"title": "Profit", "done": False},
9]
10
11
12@get("/")
13async def get_list() -> List[Dict[str, Union[str, bool]]]:
14 return TODO_LIST
15
16
17app = Litestar([get_list])
1from typing import Union
2
3from litestar import Litestar, get
4
5TODO_LIST: list[dict[str, Union[str, bool]]] = [
6 {"title": "Start writing TODO list", "done": True},
7 {"title": "???", "done": False},
8 {"title": "Profit", "done": False},
9]
10
11
12@get("/")
13async def get_list() -> list[dict[str, Union[str, bool]]]:
14 return TODO_LIST
15
16
17app = Litestar([get_list])
1from litestar import Litestar, get
2
3TODO_LIST: list[dict[str, str | bool]] = [
4 {"title": "Start writing TODO list", "done": True},
5 {"title": "???", "done": False},
6 {"title": "Profit", "done": False},
7]
8
9
10@get("/")
11async def get_list() -> list[dict[str, str | bool]]:
12 return TODO_LIST
13
14
15app = Litestar([get_list])
If you run the app and visit http://127.0.0.1:8000/ in your browser you’ll see the following output:
Because the get_list
function has been annotated with
List[Dict[str, Union[str, bool]]]
, Litestar infers that you want the data returned
from it to be serialized as JSON:
13async def get_list() -> List[Dict[str, Union[str, bool]]]:
Cleaning up the example with dataclasses#
To make your life a little easier, you can transform this example by using dataclasses
instead of plain dictionaries:
Tip
For an in-depth explanation of dataclasses, you can read this excellent Real Python article: Data Classes in Python 3.7+
1from dataclasses import dataclass
2from typing import List
3
4from litestar import Litestar, get
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list() -> List[TodoItem]:
22 return TODO_LIST
23
24
25app = Litestar([get_list])
1from dataclasses import dataclass
2
3from litestar import Litestar, get
4
5
6@dataclass
7class TodoItem:
8 title: str
9 done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13 TodoItem(title="Start writing TODO list", done=True),
14 TodoItem(title="???", done=False),
15 TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list() -> list[TodoItem]:
21 return TODO_LIST
22
23
24app = Litestar([get_list])
This looks a lot cleaner and has the added benefit of being able to work with dataclasses instead of plain dictionaries. The result will still be the same: Litestar knows how to turn these dataclasses into JSON and will do so for you automatically.
Tip
In addition to dataclasses, Litestar supports many more types such as
TypedDict
, NamedTuple
,
Pydantic models, or
attrs classes.
Filtering the list using query parameters#
Currently get_list
will always return all items on the list, but what if you
are interested in only those items with a specific status, for example all items that
are not yet marked as done?
For this you can employ query parameters; to define a query parameter, all that’s needed is to add an otherwise unused parameter to the function. Litestar will recognize this and infer that it’s going to be used as a query parameter. When a request is being made, the query parameter will be extracted from the URL, and passed to the function parameter of the same name.
1from dataclasses import dataclass
2from typing import List
3
4from litestar import Litestar, get
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: str) -> List[TodoItem]:
22 if done == "1":
23 return [item for item in TODO_LIST if item.done]
24 return [item for item in TODO_LIST if not item.done]
25
26
27app = Litestar([get_list])
1from dataclasses import dataclass
2
3from litestar import Litestar, get
4
5
6@dataclass
7class TodoItem:
8 title: str
9 done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13 TodoItem(title="Start writing TODO list", done=True),
14 TodoItem(title="???", done=False),
15 TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: str) -> list[TodoItem]:
21 if done == "1":
22 return [item for item in TODO_LIST if item.done]
23 return [item for item in TODO_LIST if not item.done]
24
25
26app = Litestar([get_list])
At first glance this seems to work just fine, but you might be able to spot a problem:
If you input anything other than ?done=1
, it would still return items not yet marked
as done. For example, ?done=john
gives the same result as ?done=0
.
An easy solution for this would be to simply check if the query parameter is either
1
or 0
, and return a response with an HTTP status code that indicates an
error if it’s something else:
1from dataclasses import dataclass
2from typing import List
3
4from litestar import Litestar, get
5from litestar.exceptions import HTTPException
6
7
8@dataclass
9class TodoItem:
10 title: str
11 done: bool
12
13
14TODO_LIST: List[TodoItem] = [
15 TodoItem(title="Start writing TODO list", done=True),
16 TodoItem(title="???", done=False),
17 TodoItem(title="Profit", done=False),
18]
19
20
21@get("/")
22async def get_list(done: str) -> List[TodoItem]:
23 if done == "1":
24 return [item for item in TODO_LIST if item.done]
25 if done == "0":
26 return [item for item in TODO_LIST if not item.done]
27 raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
28
29
30app = Litestar([get_list])
1from dataclasses import dataclass
2
3from litestar import Litestar, get
4from litestar.exceptions import HTTPException
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: list[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: str) -> list[TodoItem]:
22 if done == "1":
23 return [item for item in TODO_LIST if item.done]
24 if done == "0":
25 return [item for item in TODO_LIST if not item.done]
26 raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
27
28
29app = Litestar([get_list])
If the query parameter equals 1
, return all items that have done=True
:
23if done == "1":
24 return [item for item in TODO_LIST if item.done]
If the query parameter equals 0
, return all items that have done=False
:
25if done == "0":
26 return [item for item in TODO_LIST if not item.done]
Finally, if the query parameter has any other value, an HTTPException
will be raised.
Raising an HTTPException
tells Litestar that something went wrong, and instead of
returning a normal response, it will send a response with the HTTP status code given
(400
in this case) and the error message supplied.
27raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400)
Now we’ve got that out of the way, but your code has grown to be quite complex for such a simple task. You’re probably thinking “there must be a better way!”, and there is! Instead of doing these things manually, you can also just let Litestar handle them for you!
Converting and validating query parameters#
As mentioned earlier, type annotations can be used for more than static type checking
in Litestar; they can also define and configure behaviour. In this case, you can get
Litestar to convert the query parameter to a boolean value, matching the values of the
TodoItem.done
attribute, and in the same step validate it, returning error responses
for you should the supplied value not be a valid boolean.
1from dataclasses import dataclass
2from typing import List
3
4from litestar import Litestar, get
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: bool) -> List[TodoItem]:
22 return [item for item in TODO_LIST if item.done == done]
23
24
25app = Litestar([get_list])
1from dataclasses import dataclass
2
3from litestar import Litestar, get
4
5
6@dataclass
7class TodoItem:
8 title: str
9 done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13 TodoItem(title="Start writing TODO list", done=True),
14 TodoItem(title="???", done=False),
15 TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: bool) -> list[TodoItem]:
21 return [item for item in TODO_LIST if item.done == done]
22
23
24app = Litestar([get_list])
What’s happening here?
Since bool
is being used as the type annotation for the done
parameter,
Litestar will try to convert the value into a bool
first. Since john
(arguably) is not a representation of a boolean value, it will return an error response
instead.
21async def get_list(done: bool) -> List[TodoItem]:
Tip
It is important to note that this conversion is not the result of calling
bool
on the raw value. bool("john")
would be True
, since Python
considers all non-empty strings to be truthy.
Litestar however supports customary boolean representation commonly used in the HTTP
world; true
and 1
are both converted to True
, while false
and 0
are converted to be False
.
If the conversion is successful however, done
is now a bool
, which can then
be compared against the TodoItem.done
attribute:
22return [item for item in TODO_LIST if item.done == done]
See also
Making the query parameter optional#
There is one problem left to solve though, and that is, what happens when you want to get all items, done or not, and omit the query parameter?
Because the query parameter has been defined as done: bool
without giving it a
default value, it will be treated as a required parameter - just like a regular function
parameter. If instead you want this to be optional, a default value needs to be
supplied.
1from dataclasses import dataclass
2from typing import List, Optional
3
4from litestar import Litestar, get
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: List[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: Optional[bool] = None) -> List[TodoItem]:
22 if done is None:
23 return TODO_LIST
24 return [item for item in TODO_LIST if item.done == done]
25
26
27app = Litestar([get_list])
1from dataclasses import dataclass
2from typing import Optional
3
4from litestar import Litestar, get
5
6
7@dataclass
8class TodoItem:
9 title: str
10 done: bool
11
12
13TODO_LIST: list[TodoItem] = [
14 TodoItem(title="Start writing TODO list", done=True),
15 TodoItem(title="???", done=False),
16 TodoItem(title="Profit", done=False),
17]
18
19
20@get("/")
21async def get_list(done: Optional[bool] = None) -> list[TodoItem]:
22 if done is None:
23 return TODO_LIST
24 return [item for item in TODO_LIST if item.done == done]
25
26
27app = Litestar([get_list])
1from dataclasses import dataclass
2
3from litestar import Litestar, get
4
5
6@dataclass
7class TodoItem:
8 title: str
9 done: bool
10
11
12TODO_LIST: list[TodoItem] = [
13 TodoItem(title="Start writing TODO list", done=True),
14 TodoItem(title="???", done=False),
15 TodoItem(title="Profit", done=False),
16]
17
18
19@get("/")
20async def get_list(done: bool | None = None) -> list[TodoItem]:
21 if done is None:
22 return TODO_LIST
23 return [item for item in TODO_LIST if item.done == done]
24
25
26app = Litestar([get_list])
Interactive documentation#
So far we have explored our TODO application by navigating to it manually, but there is
another way: Litestar comes with interactive API documentation, which is generated for
you automatically. All you need to do is run your app (litestar run
) and visit
http://127.0.0.1:8000/schema/swagger
This documentation not only gives an overview of the API you have constructed, but also allows you to send requests to it.
Note
This is made possible by Swagger and OpenAPI. Litestar generates an OpenAPI schema based on the route handlers, which can then be used by Swagger to set up the interactive documentation.
Tip
In addition to Swagger, Litestar serves the documentation from the generated OpenAPI schema with ReDoc and Stoplight Elements. You can browse to http://127.0.0.1:8000/schema/redoc and http://127.0.0.1:8000/schema/elements to view each, respectively.