plain 0.68.0__py3-none-any.whl → 0.101.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. plain/CHANGELOG.md +656 -1
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -36
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +110 -26
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +27 -75
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +96 -10
  27. plain/cli/{agent/request.py → request.py} +67 -33
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -8
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +27 -16
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +209 -24
  84. plain/preflight/__init__.py +1 -0
  85. plain/preflight/checks.py +3 -1
  86. plain/preflight/files.py +3 -1
  87. plain/preflight/registry.py +26 -11
  88. plain/preflight/results.py +15 -7
  89. plain/preflight/security.py +15 -13
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +4 -1
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +34 -25
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +13 -5
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +38 -22
  145. plain/urls/resolvers.py +35 -25
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/csrf/views.py +0 -31
  191. plain/logs/utils.py +0 -46
  192. plain/templates/AGENTS.md +0 -3
  193. plain-0.68.0.dist-info/RECORD +0 -169
  194. plain-0.68.0.dist-info/entry_points.txt +0 -5
  195. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/views/README.md CHANGED
@@ -3,21 +3,25 @@
3
3
  **Take a request, return a response.**
4
4
 
5
5
  - [Overview](#overview)
6
- - [HTTP methods -> class methods](#http-methods---class-methods)
6
+ - [HTTP methods map to class methods](#http-methods-map-to-class-methods)
7
7
  - [Return types](#return-types)
8
- - [Template views](#template-views)
9
- - [Form views](#form-views)
8
+ - [TemplateView](#templateview)
9
+ - [FormView](#formview)
10
10
  - [Object views](#object-views)
11
- - [Response exceptions](#response-exceptions)
11
+ - [DetailView](#detailview)
12
+ - [CreateView](#createview)
13
+ - [UpdateView](#updateview)
14
+ - [DeleteView](#deleteview)
15
+ - [ListView](#listview)
16
+ - [RedirectView](#redirectview)
17
+ - [ResponseException](#responseexception)
12
18
  - [Error views](#error-views)
13
- - [Redirect views](#redirect-views)
14
- - [CSRF exempt views](#csrf-exempt-views)
19
+ - [FAQs](#faqs)
20
+ - [Installation](#installation)
15
21
 
16
22
  ## Overview
17
23
 
18
- Plain views are written as classes,
19
- with a straightforward API that keeps simple views simple,
20
- but gives you the power of a full class to handle more complex cases.
24
+ Plain views are class-based, with a straightforward API that keeps simple views simple while giving you the full power of a class for complex cases.
21
25
 
22
26
  ```python
23
27
  from plain.views import View
@@ -28,12 +32,11 @@ class ExampleView(View):
28
32
  return "<html><body>Hello, world!</body></html>"
29
33
  ```
30
34
 
31
- ## HTTP methods -> class methods
35
+ You can return strings, dicts, lists, integers (status codes), or full `Response` objects. Plain automatically converts them to the appropriate HTTP response.
32
36
 
33
- The HTTP method of the request will map to a class method of the same name on the view.
37
+ ## HTTP methods map to class methods
34
38
 
35
- If a request comes in and there isn't a matching method on the view,
36
- Plain will return a `405 Method Not Allowed` response.
39
+ The HTTP method of the request maps directly to a class method of the same name. Define only the methods you want to support.
37
40
 
38
41
  ```python
39
42
  from plain.views import View
@@ -54,18 +57,15 @@ class ExampleView(View):
54
57
 
55
58
  def delete(self):
56
59
  pass
57
-
58
- def trace(self):
59
- pass
60
60
  ```
61
61
 
62
- The [base `View` class](./base.py#View) defines default `options` and `head` behavior,
63
- but you can override these too.
62
+ If a request comes in for a method your view doesn't implement, Plain returns a `405 Method Not Allowed` response automatically.
63
+
64
+ The [base `View` class](./base.py#View) provides default `options` and `head` behavior, but you can override these too.
64
65
 
65
66
  ## Return types
66
67
 
67
- For simple JSON responses, HTML, or status code responses,
68
- you don't need to instantiate a `Response` object.
68
+ You can return common Python types directly from view methods without wrapping them in a `Response` object.
69
69
 
70
70
  ```python
71
71
  class JsonView(View):
@@ -81,11 +81,18 @@ class HtmlView(View):
81
81
  class StatusCodeView(View):
82
82
  def get(self):
83
83
  return 204 # No content
84
+
85
+
86
+ class TupleView(View):
87
+ def get(self):
88
+ return (201, {"id": 123}) # Status code + data
84
89
  ```
85
90
 
86
- ## Template views
91
+ Returning `None` triggers a 404 response, which is useful when an object isn't found.
87
92
 
88
- The most common behavior for a view is to render a template.
93
+ ## TemplateView
94
+
95
+ For rendering templates, use [`TemplateView`](./templates.py#TemplateView). This is the base class for most other built-in view classes.
89
96
 
90
97
  ```python
91
98
  from plain.views import TemplateView
@@ -100,9 +107,7 @@ class ExampleView(TemplateView):
100
107
  return context
101
108
  ```
102
109
 
103
- The [`TemplateView`](./templates.py#TemplateView) is also the base class for _most_ of the other built-in view classes.
104
-
105
- Template views that don't need any custom context can use `TemplateView.as_view()` directly in the URL route.
110
+ For simple pages that don't need custom context, you can configure `TemplateView` directly in your URL routes.
106
111
 
107
112
  ```python
108
113
  from plain.views import TemplateView
@@ -115,9 +120,9 @@ class AppRouter(Router):
115
120
  ]
116
121
  ```
117
122
 
118
- ## Form views
123
+ ## FormView
119
124
 
120
- Standard [forms](../forms) can be rendered and processed by a [`FormView`](./forms.py#FormView).
125
+ [`FormView`](./forms.py#FormView) handles displaying and processing [forms](/plain/plain/forms/README.md).
121
126
 
122
127
  ```python
123
128
  from plain.views import FormView
@@ -130,11 +135,11 @@ class ExampleView(FormView):
130
135
  success_url = "." # Redirect to the same page
131
136
 
132
137
  def form_valid(self, form):
133
- # Do other successfull form processing here
138
+ # Do additional processing here
134
139
  return super().form_valid(form)
135
140
  ```
136
141
 
137
- Rendering forms is done directly in the HTML.
142
+ The form is automatically available in your template as `form`.
138
143
 
139
144
  ```html
140
145
  {% extends "base.html" %}
@@ -147,7 +152,7 @@ Rendering forms is done directly in the HTML.
147
152
  <div>{{ error }}</div>
148
153
  {% endfor %}
149
154
 
150
- <!-- Render form fields individually (or with Jinja helps or other concepts) -->
155
+ <!-- Render form fields -->
151
156
  <label for="{{ form.email.html_id }}">Email</label>
152
157
  <input
153
158
  type="email"
@@ -169,10 +174,14 @@ Rendering forms is done directly in the HTML.
169
174
 
170
175
  ## Object views
171
176
 
172
- The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.
177
+ Plain provides views for standard CRUD operations. Each requires you to implement `get_object()` or `get_objects()` to control what data is accessed.
178
+
179
+ ### DetailView
180
+
181
+ [`DetailView`](./objects.py#DetailView) displays a single object.
173
182
 
174
183
  ```python
175
- from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView
184
+ from plain.views import DetailView
176
185
 
177
186
 
178
187
  class ExampleDetailView(DetailView):
@@ -183,12 +192,32 @@ class ExampleDetailView(DetailView):
183
192
  id=self.url_kwargs["id"],
184
193
  user=self.request.user, # Limit access
185
194
  )
195
+ ```
196
+
197
+ The object is available in your template as `object`. You can also set `context_object_name` for a more descriptive name.
198
+
199
+ ### CreateView
200
+
201
+ [`CreateView`](./objects.py#CreateView) displays a form and creates a new object on successful submission.
202
+
203
+ ```python
204
+ from plain.views import CreateView
205
+ from .forms import CustomCreateForm
186
206
 
187
207
 
188
208
  class ExampleCreateView(CreateView):
189
209
  template_name = "create.html"
190
210
  form_class = CustomCreateForm
191
211
  success_url = "."
212
+ ```
213
+
214
+ ### UpdateView
215
+
216
+ [`UpdateView`](./objects.py#UpdateView) displays a form pre-populated with an existing object and saves changes on submission.
217
+
218
+ ```python
219
+ from plain.views import UpdateView
220
+ from .forms import CustomUpdateForm
192
221
 
193
222
 
194
223
  class ExampleUpdateView(UpdateView):
@@ -199,22 +228,35 @@ class ExampleUpdateView(UpdateView):
199
228
  def get_object(self):
200
229
  return MyObjectClass.query.get(
201
230
  id=self.url_kwargs["id"],
202
- user=self.request.user, # Limit access
231
+ user=self.request.user,
203
232
  )
233
+ ```
234
+
235
+ ### DeleteView
236
+
237
+ [`DeleteView`](./objects.py#DeleteView) confirms deletion of an object. POST to delete, no form class needed.
238
+
239
+ ```python
240
+ from plain.views import DeleteView
204
241
 
205
242
 
206
243
  class ExampleDeleteView(DeleteView):
207
244
  template_name = "delete.html"
208
- success_url = "."
209
-
210
- # No form class necessary.
211
- # Just POST to this view to delete the object.
245
+ success_url = "/list/"
212
246
 
213
247
  def get_object(self):
214
248
  return MyObjectClass.query.get(
215
249
  id=self.url_kwargs["id"],
216
- user=self.request.user, # Limit access
250
+ user=self.request.user,
217
251
  )
252
+ ```
253
+
254
+ ### ListView
255
+
256
+ [`ListView`](./objects.py#ListView) displays a collection of objects.
257
+
258
+ ```python
259
+ from plain.views import ListView
218
260
 
219
261
 
220
262
  class ExampleListView(ListView):
@@ -222,16 +264,44 @@ class ExampleListView(ListView):
222
264
 
223
265
  def get_objects(self):
224
266
  return MyObjectClass.query.filter(
225
- user=self.request.user, # Limit access
267
+ user=self.request.user,
226
268
  )
227
269
  ```
228
270
 
229
- ## Response exceptions
271
+ The objects are available in your template as `objects`.
230
272
 
231
- At any point in the request handling,
232
- a view can raise a [`ResponseException`](./exceptions.py#ResponseException) to immediately exit and return the wrapped response.
273
+ ## RedirectView
233
274
 
234
- This isn't always necessary, but can be useful for raising rate limits or authorization errors when you're a couple layers deep in the view handling or helper functions.
275
+ [`RedirectView`](./redirect.py#RedirectView) redirects to another URL.
276
+
277
+ ```python
278
+ from plain.views import RedirectView
279
+
280
+
281
+ class ExampleRedirectView(RedirectView):
282
+ url = "/new-location/"
283
+ ```
284
+
285
+ Set `status_code = 301` for permanent redirects (default is 302).
286
+
287
+ For simple redirects, configure the view directly in your URL routes.
288
+
289
+ ```python
290
+ from plain.views import RedirectView
291
+ from plain.urls import path, Router
292
+
293
+
294
+ class AppRouter(Router):
295
+ routes = [
296
+ path("/old-location/", RedirectView.as_view(url="/new-location/", status_code=301)),
297
+ ]
298
+ ```
299
+
300
+ You can also redirect to a named URL using `url_name`, or preserve query parameters with `preserve_query_params=True`.
301
+
302
+ ## ResponseException
303
+
304
+ At any point during request handling, you can raise a [`ResponseException`](./exceptions.py#ResponseException) to immediately return a response. This is useful for authorization checks or rate limiting in nested helper functions.
235
305
 
236
306
  ```python
237
307
  from plain.views import DetailView
@@ -241,7 +311,7 @@ from plain.http import Response
241
311
 
242
312
  class ExampleView(DetailView):
243
313
  def get_object(self):
244
- if self.request.user.exceeds_rate_limit:
314
+ if self.request.user and self.request.user.exceeds_rate_limit:
245
315
  raise ResponseException(
246
316
  Response("Rate limit exceeded", status_code=429)
247
317
  )
@@ -251,60 +321,67 @@ class ExampleView(DetailView):
251
321
 
252
322
  ## Error views
253
323
 
254
- By default, HTTP errors will be rendered by `templates/<status_code>.html` or `templates/error.html`.
324
+ HTTP errors are rendered using templates. Create templates for the errors users see.
325
+
326
+ - `templates/404.html` - Page not found
327
+ - `templates/403.html` - Forbidden
328
+ - `templates/500.html` - Server error
329
+
330
+ Plain looks for `{status_code}.html` templates, then returns a plain HTTP response if not found. Most apps only need these three templates.
331
+
332
+ Templates receive `status_code` and `exception` in context.
333
+
334
+ Your `500.html` template should be self-contained. Avoid extending base templates or accessing the database/session, since server errors can occur during middleware or template rendering. `404.html` and `403.html` can safely extend base templates since they occur during view execution after middleware runs.
255
335
 
256
- You can define your own error views by pointing the `HTTP_ERROR_VIEWS` setting to a dictionary of status codes and view classes.
336
+ ## FAQs
337
+
338
+ #### How do I exempt a view from CSRF protection?
339
+
340
+ Use the `CSRF_EXEMPT_PATHS` setting to specify path patterns that should bypass CSRF protection. For example:
257
341
 
258
342
  ```python
259
343
  # app/settings.py
260
- HTTP_ERROR_VIEWS = {
261
- 404: "errors.NotFoundView",
262
- }
344
+ CSRF_EXEMPT_PATHS = [
345
+ r"^/api/", # Exempt all API routes
346
+ r"^/webhooks/", # Exempt webhook endpoints
347
+ ]
263
348
  ```
264
349
 
265
- ```python
266
- # app/errors.py
267
- from plain.views import View
350
+ #### How do I access URL parameters?
268
351
 
352
+ URL parameters are available via `self.url_kwargs` (keyword arguments) and `self.url_args` (positional arguments).
269
353
 
270
- class NotFoundView(View):
354
+ ```python
355
+ class ExampleView(View):
271
356
  def get(self):
272
- # A custom implementation or error view handling
273
- pass
357
+ user_id = self.url_kwargs["id"]
358
+ return f"User ID: {user_id}"
274
359
  ```
275
360
 
276
- ## Redirect views
277
-
278
- ```python
279
- from plain.views import RedirectView
361
+ #### How do I access the request object?
280
362
 
363
+ The request is available as `self.request` after the view is set up.
281
364
 
282
- class ExampleRedirectView(RedirectView):
283
- url = "/new-location/"
284
- permanent = True
365
+ ```python
366
+ class ExampleView(View):
367
+ def get(self):
368
+ return f"Path: {self.request.path}"
285
369
  ```
286
370
 
287
- Redirect views can also be used in the URL router.
371
+ #### Can I customize view initialization?
372
+
373
+ Yes, define your own `__init__` method to accept custom arguments passed via `as_view()`.
288
374
 
289
375
  ```python
290
- from plain.views import RedirectView
291
- from plain.urls import path, Router
376
+ class CustomView(View):
377
+ def __init__(self, feature_enabled=False):
378
+ self.feature_enabled = feature_enabled
292
379
 
293
380
 
294
- class AppRouter(Router):
295
- routes = [
296
- path("/old-location/", RedirectView.as_view(url="/new-location/", permanent=True)),
297
- ]
381
+ # In URLs
382
+ path("/custom/", CustomView.as_view(feature_enabled=True))
298
383
  ```
299
384
 
300
- ## CSRF exempt views
385
+ ## Installation
301
386
 
302
- ```python
303
- from plain.views import View
304
- from plain.views.csrf import CsrfExemptViewMixin
305
-
306
-
307
- class ExemptView(CsrfExemptViewMixin, View):
308
- def post(self):
309
- return "Hello, world!"
310
- ```
387
+ Views are included with the core `plain` package. No additional installation is required.
plain/views/__init__.py CHANGED
@@ -14,5 +14,4 @@ __all__ = [
14
14
  "UpdateView",
15
15
  "DeleteView",
16
16
  "ListView",
17
- "AuthViewMixin",
18
17
  ]
plain/views/base.py CHANGED
@@ -1,5 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
4
+ from collections.abc import Callable
2
5
  from http import HTTPMethod
6
+ from typing import Any, Self
3
7
 
4
8
  from opentelemetry import trace
5
9
  from opentelemetry.semconv._incubating.attributes.code_attributes import (
@@ -8,12 +12,12 @@ from opentelemetry.semconv._incubating.attributes.code_attributes import (
8
12
  )
9
13
 
10
14
  from plain.http import (
11
- HttpRequest,
12
15
  JsonResponse,
16
+ NotAllowedResponse,
17
+ NotFoundError404,
18
+ Request,
13
19
  Response,
14
20
  ResponseBase,
15
- ResponseNotAllowed,
16
- ResponseNotFound,
17
21
  )
18
22
  from plain.utils.decorators import classonlymethod
19
23
 
@@ -26,14 +30,14 @@ tracer = trace.get_tracer("plain")
26
30
 
27
31
 
28
32
  class View:
29
- request: HttpRequest
30
- url_args: tuple
31
- url_kwargs: dict
33
+ request: Request
34
+ url_args: tuple[Any, ...]
35
+ url_kwargs: dict[str, Any]
32
36
 
33
37
  # View.as_view(example="foo") usage can be customized by defining your own __init__ method.
34
38
  # def __init__(self, *args, **kwargs):
35
39
 
36
- def setup(self, request: HttpRequest, *url_args, **url_kwargs) -> None:
40
+ def setup(self, request: Request, *url_args: object, **url_kwargs: object) -> None:
37
41
  if hasattr(self, "get") and not hasattr(self, "head"):
38
42
  self.head = self.get
39
43
 
@@ -42,8 +46,12 @@ class View:
42
46
  self.url_kwargs = url_kwargs
43
47
 
44
48
  @classonlymethod
45
- def as_view(cls, *init_args, **init_kwargs):
46
- def view(request, *url_args, **url_kwargs):
49
+ def as_view(
50
+ cls: type[Self], *init_args: object, **init_kwargs: object
51
+ ) -> Callable[[Request, Any, Any], ResponseBase]:
52
+ def view(
53
+ request: Request, *url_args: object, **url_kwargs: object
54
+ ) -> ResponseBase:
47
55
  with tracer.start_as_current_span(
48
56
  f"{cls.__name__}",
49
57
  kind=trace.SpanKind.INTERNAL,
@@ -62,11 +70,11 @@ class View:
62
70
  )
63
71
  return response
64
72
 
65
- view.view_class = cls
73
+ view.view_class = cls # type: ignore[attr-defined]
66
74
 
67
75
  return view
68
76
 
69
- def get_request_handler(self) -> callable:
77
+ def get_request_handler(self) -> Callable[[], Any] | None:
70
78
  """Return the handler for the current request method."""
71
79
 
72
80
  if not self.request.method:
@@ -84,16 +92,16 @@ class View:
84
92
  self.request.path,
85
93
  extra={"status_code": 405, "request": self.request},
86
94
  )
87
- return ResponseNotAllowed(self._allowed_methods())
95
+ return NotAllowedResponse(self._allowed_methods())
88
96
 
89
97
  try:
90
- result = handler()
98
+ result: Any = handler()
91
99
  except ResponseException as e:
92
100
  return e.response
93
101
 
94
102
  return self.convert_value_to_response(result)
95
103
 
96
- def convert_value_to_response(self, value) -> ResponseBase:
104
+ def convert_value_to_response(self, value: Any) -> ResponseBase:
97
105
  """Convert a return value to a Response."""
98
106
  if isinstance(value, ResponseBase):
99
107
  return value
@@ -102,8 +110,7 @@ class View:
102
110
  return Response(status_code=value)
103
111
 
104
112
  if value is None:
105
- # TODO raise 404 instead?
106
- return ResponseNotFound()
113
+ raise NotFoundError404
107
114
 
108
115
  status_code = 200
109
116
 
@@ -113,8 +120,8 @@ class View:
113
120
  "Tuple response must be of length 2 (status_code, value)"
114
121
  )
115
122
 
116
- status_code = value[0]
117
- value = value[1]
123
+ status_code: int = value[0]
124
+ value: Any = value[1]
118
125
 
119
126
  if isinstance(value, str):
120
127
  return Response(value, status_code=status_code)
plain/views/errors.py CHANGED
@@ -1,3 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
1
6
  from plain.http import ResponseBase
2
7
  from plain.templates import TemplateFileMissing
3
8
 
@@ -7,7 +12,9 @@ from .templates import TemplateView
7
12
  class ErrorView(TemplateView):
8
13
  status_code: int
9
14
 
10
- def __init__(self, *, status_code=None, exception=None) -> None:
15
+ def __init__(
16
+ self, *, status_code: int | None = None, exception: Any | None = None
17
+ ) -> None:
11
18
  # Allow creating an ErrorView with a status code
12
19
  # e.g. ErrorView.as_view(status_code=404)
13
20
  self.status_code = status_code or self.status_code
@@ -15,16 +22,17 @@ class ErrorView(TemplateView):
15
22
  # Allow creating an ErrorView with an exception
16
23
  self.exception = exception
17
24
 
18
- def get_template_context(self):
25
+ def get_template_context(self) -> dict:
19
26
  context = super().get_template_context()
20
27
  context["status_code"] = self.status_code
21
28
  context["exception"] = self.exception
22
29
  return context
23
30
 
24
31
  def get_template_names(self) -> list[str]:
25
- return [f"{self.status_code}.html", "error.html"]
32
+ # Try specific status code template (e.g. "404.html")
33
+ return [f"{self.status_code}.html"]
26
34
 
27
- def get_request_handler(self):
35
+ def get_request_handler(self) -> Callable[[], Any]:
28
36
  return self.get # All methods (post, patch, etc.) will use the get()
29
37
 
30
38
  def get_response(self) -> ResponseBase:
@@ -33,7 +41,7 @@ class ErrorView(TemplateView):
33
41
  response.status_code = self.status_code
34
42
  return response
35
43
 
36
- def get(self):
44
+ def get(self) -> ResponseBase | int:
37
45
  try:
38
46
  return super().get()
39
47
  except TemplateFileMissing:
plain/views/exceptions.py CHANGED
@@ -1,4 +1,7 @@
1
+ from plain.http import ResponseBase
2
+
3
+
1
4
  class ResponseException(Exception):
2
- def __init__(self, response):
5
+ def __init__(self, response: ResponseBase) -> None:
3
6
  self.response = response
4
7
  super().__init__(response)
plain/views/forms.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from collections.abc import Callable
2
- from typing import TYPE_CHECKING
2
+ from typing import TYPE_CHECKING, Any
3
3
 
4
4
  from plain.exceptions import ImproperlyConfigured
5
- from plain.http import Response, ResponseRedirect
5
+ from plain.http import RedirectResponse, Response
6
6
 
7
7
  from .templates import TemplateView
8
8
 
@@ -25,7 +25,7 @@ class FormView(TemplateView):
25
25
  )
26
26
  return self.form_class(**self.get_form_kwargs())
27
27
 
28
- def get_form_kwargs(self) -> dict:
28
+ def get_form_kwargs(self) -> dict[str, Any]:
29
29
  """Return the keyword arguments for instantiating the form."""
30
30
  return {
31
31
  "initial": {},
@@ -40,7 +40,7 @@ class FormView(TemplateView):
40
40
 
41
41
  def form_valid(self, form: "BaseForm") -> Response:
42
42
  """If the form is valid, redirect to the supplied URL."""
43
- return ResponseRedirect(self.get_success_url(form))
43
+ return RedirectResponse(self.get_success_url(form))
44
44
 
45
45
  def form_invalid(self, form: "BaseForm") -> Response:
46
46
  """If the form is invalid, render the invalid form."""
@@ -48,9 +48,9 @@ class FormView(TemplateView):
48
48
  **self.get_template_context(),
49
49
  "form": form,
50
50
  }
51
- return self.get_template().render(context)
51
+ return Response(self.get_template().render(context))
52
52
 
53
- def get_template_context(self) -> dict:
53
+ def get_template_context(self) -> dict[str, Any]:
54
54
  """Insert the form into the context dict."""
55
55
  context = super().get_template_context()
56
56
  context["form"] = self.get_form()