You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: plain-api/plain/api/README.md
+117-7
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,9 @@
2
2
3
3
**Build APIs using class-based views.**
4
4
5
-
The `plain.api` package provides lightweight view classes for building APIs using the same patterns as regular views. It also provides an `APIKey` model and support for generating [OpenAPI](https://www.openapis.org/) documents.
5
+
This package includes lightweight view classes for building APIs using the same patterns as regular HTML views. It also provides an [`APIKey` model](#api-keys) and support for generating [OpenAPI](#openapi) documents.
6
+
7
+
Because [Views](/plain/plain/views/README.md) can convert built-in types to responses, an API view can simply return a dict or list to send a JSON response back to the client. More complex responses can use the [`JsonResponse`](/plain/plain/http/response.py#JsonResponse) class.
6
8
7
9
```python
8
10
# app/api/views.py
@@ -14,6 +16,7 @@ from app.users.models import User
14
16
from app.pullrequests.models import PullRequest
15
17
16
18
19
+
# An example base class that will be used across your custom API
17
20
classBaseAPIView(APIView, APIKeyView):
18
21
defuse_api_key(self):
19
22
super().use_api_key()
@@ -29,6 +32,7 @@ class BaseAPIView(APIView, APIKeyView):
29
32
)
30
33
31
34
35
+
# An endpoint that returns the current user
32
36
classUserView(BaseAPIView):
33
37
defget(self):
34
38
return {
@@ -38,6 +42,7 @@ class UserView(BaseAPIView):
38
42
}
39
43
40
44
45
+
# An endpoint that filters querysets based on the user
41
46
classPullRequestView(BaseAPIView):
42
47
defget(self):
43
48
try:
@@ -64,6 +69,8 @@ class PullRequestView(BaseAPIView):
64
69
}
65
70
```
66
71
72
+
URLs work like they do everywhere else, though it's generally recommended to put everything together into an `app.api` package and `api` namespace.
73
+
67
74
```python
68
75
# app/api/urls.py
69
76
from plain.urls import Router, path
@@ -81,15 +88,98 @@ class APIRouter(Router):
81
88
82
89
## Authentication and authorization
83
90
84
-
TODO
91
+
Handling authentication in the API is pretty straightforward. If you use [API keys](#api-keys), then the `APIKeyView` will parse the `Authorization` header and set `self.api_key`. You will then customize the `use_api_key` method to associate the request with a user (or team, for example), depending on how your app works. To perform custom authentication, you can create your own base view class and hook into [`View.get_response`](/plain/plain/views/base.py#get_response).
92
+
93
+
```python
94
+
classBaseAPIView(APIView, APIKeyView):
95
+
defuse_api_key(self):
96
+
super().use_api_key()
97
+
98
+
if user :=self.api_key.users.first():
99
+
self.request.user = user
100
+
else:
101
+
raise ResponseException(
102
+
JsonResponse(
103
+
{"error": "API key not associated with a user."},
104
+
status_code=403,
105
+
)
106
+
)
107
+
```
108
+
109
+
When it comes to authorizing actions, typically you will factor this in to the queryset to only return objects that the user is allowed to see. If a response method (`get`, `post`, etc.) returns `None`, then the view will return a 404 response. Other status codes can be returned with an int (ex. `403`) or a `JsonResponse` object.
110
+
111
+
```python
112
+
classPullRequestView(BaseAPIView):
113
+
defget(self):
114
+
try:
115
+
pull = (
116
+
PullRequest.objects.all()
117
+
.visible_to_user(self.request.user)
118
+
.get(uuid=self.url_kwargs["uuid"])
119
+
)
120
+
except PullRequest.DoesNotExist:
121
+
returnNone
122
+
123
+
# ...return the authorized data here
124
+
```
125
+
126
+
## `PUT`, `POST`, and `PATCH`
127
+
128
+
One way to handle PUT, POST, and PATCH endpoints is to use standard [forms](/plain/plain/forms/README.md). This will use the same validation and error handling as an HTML form, but will parse the input from the JSON request instead of HTML form data.
129
+
130
+
```python
131
+
classUserForm(ModelForm):
132
+
classMeta:
133
+
model = User
134
+
fields = [
135
+
"username",
136
+
"time_zone",
137
+
]
138
+
139
+
classUserView(BaseAPIView):
140
+
defpatch(self):
141
+
form = UserForm(
142
+
request=self.request,
143
+
instance=self.request.user,
144
+
)
145
+
146
+
if form.is_valid():
147
+
user = form.save()
148
+
return {
149
+
"uuid": user.uuid,
150
+
"username": user.username,
151
+
"time_zone": str(user.time_zone),
152
+
}
153
+
else:
154
+
return {"errors": form.errors}
155
+
```
156
+
157
+
If you don't want to use Plain's forms, you could also use a third-party schema/validation library like [Pydantic](https://docs.pydantic.dev/latest/) or [Marshmallow](https://marshmallow.readthedocs.io/en/3.x-line/). But depending on your use case, you may not need to use forms or fancy validation at all!
158
+
159
+
## `DELETE`
160
+
161
+
Deletes can be handled in the `delete` method of the view. Most of the time this just means getting the object, deleting it, and returning a 204.
162
+
163
+
```python
164
+
classPullRequestView(BaseAPIView):
165
+
defdelete(self):
166
+
try:
167
+
pull = (
168
+
PullRequest.objects.all()
169
+
.visible_to_user(self.request.user)
170
+
.get(uuid=self.url_kwargs["uuid"])
171
+
)
172
+
except PullRequest.DoesNotExist:
173
+
returnNone
85
174
86
-
## Forms
175
+
pull.delete()
87
176
88
-
TODO
177
+
return204
178
+
```
89
179
90
180
## API keys
91
181
92
-
The provided [`APIKey` model](./models.py) includes randomly generated, unique API tokens that are automatically parsed by `APIAuthViewMixin`. The tokens can optionally be named and include an `expires_at` date.
182
+
The provided [`APIKey` model](./models.py) includes randomly generated, unique API tokens that are automatically parsed by `APIKeyView`. The tokens can optionally be named and include an `expires_at` date.
93
183
94
184
Associating an `APIKey` with a user (or team, for example) is up to you. Most likely you will want to use a `ForeignKey` or a `ManyToManyField`.
If your API key is associated with something other than a user, or does not use the related name `"users"`, you can define your own [`associate_api_key`](./views.py#associate_api_key) method.
221
+
To use API keys in your views, you can inherit from `APIKeyView` and customize the [`use_api_key` method](./views.py#use_api_key) to set the `request.user` attribute (or any other attribute) to the object associated with the API key.
0 commit comments