plain 0.68.0__py3-none-any.whl → 0.103.0__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 (192) hide show
  1. plain/CHANGELOG.md +684 -1
  2. plain/README.md +1 -1
  3. plain/agents/.claude/rules/plain.md +88 -0
  4. plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
  5. plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
  6. plain/assets/compile.py +25 -12
  7. plain/assets/finders.py +24 -17
  8. plain/assets/fingerprints.py +10 -7
  9. plain/assets/urls.py +1 -1
  10. plain/assets/views.py +47 -33
  11. plain/chores/README.md +25 -23
  12. plain/chores/__init__.py +2 -1
  13. plain/chores/core.py +27 -0
  14. plain/chores/registry.py +23 -36
  15. plain/cli/README.md +185 -16
  16. plain/cli/__init__.py +2 -1
  17. plain/cli/agent.py +234 -0
  18. plain/cli/build.py +7 -8
  19. plain/cli/changelog.py +11 -5
  20. plain/cli/chores.py +32 -34
  21. plain/cli/core.py +110 -26
  22. plain/cli/docs.py +98 -21
  23. plain/cli/formatting.py +40 -17
  24. plain/cli/install.py +10 -54
  25. plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
  26. plain/cli/output.py +6 -2
  27. plain/cli/preflight.py +27 -75
  28. plain/cli/print.py +4 -4
  29. plain/cli/registry.py +96 -10
  30. plain/cli/{agent/request.py → request.py} +67 -33
  31. plain/cli/runtime.py +45 -0
  32. plain/cli/scaffold.py +2 -7
  33. plain/cli/server.py +153 -0
  34. plain/cli/settings.py +53 -49
  35. plain/cli/shell.py +15 -12
  36. plain/cli/startup.py +9 -8
  37. plain/cli/upgrade.py +17 -104
  38. plain/cli/urls.py +12 -7
  39. plain/cli/utils.py +3 -3
  40. plain/csrf/README.md +65 -40
  41. plain/csrf/middleware.py +53 -43
  42. plain/debug.py +5 -2
  43. plain/exceptions.py +22 -114
  44. plain/forms/README.md +453 -24
  45. plain/forms/__init__.py +55 -4
  46. plain/forms/boundfield.py +15 -8
  47. plain/forms/exceptions.py +1 -1
  48. plain/forms/fields.py +346 -143
  49. plain/forms/forms.py +75 -45
  50. plain/http/README.md +356 -9
  51. plain/http/__init__.py +41 -26
  52. plain/http/cookie.py +15 -7
  53. plain/http/exceptions.py +65 -0
  54. plain/http/middleware.py +32 -0
  55. plain/http/multipartparser.py +99 -88
  56. plain/http/request.py +362 -250
  57. plain/http/response.py +99 -197
  58. plain/internal/__init__.py +8 -1
  59. plain/internal/files/base.py +35 -19
  60. plain/internal/files/locks.py +19 -11
  61. plain/internal/files/move.py +8 -3
  62. plain/internal/files/temp.py +25 -6
  63. plain/internal/files/uploadedfile.py +47 -28
  64. plain/internal/files/uploadhandler.py +64 -58
  65. plain/internal/files/utils.py +24 -10
  66. plain/internal/handlers/base.py +34 -23
  67. plain/internal/handlers/exception.py +68 -65
  68. plain/internal/handlers/wsgi.py +65 -54
  69. plain/internal/middleware/headers.py +37 -11
  70. plain/internal/middleware/hosts.py +11 -8
  71. plain/internal/middleware/https.py +17 -7
  72. plain/internal/middleware/slash.py +14 -9
  73. plain/internal/reloader.py +77 -0
  74. plain/json.py +2 -1
  75. plain/logs/README.md +161 -62
  76. plain/logs/__init__.py +1 -1
  77. plain/logs/{loggers.py → app.py} +71 -67
  78. plain/logs/configure.py +63 -14
  79. plain/logs/debug.py +17 -6
  80. plain/logs/filters.py +15 -0
  81. plain/logs/formatters.py +7 -4
  82. plain/packages/README.md +105 -23
  83. plain/packages/config.py +15 -7
  84. plain/packages/registry.py +27 -16
  85. plain/paginator.py +31 -21
  86. plain/preflight/README.md +209 -24
  87. plain/preflight/__init__.py +1 -0
  88. plain/preflight/checks.py +3 -1
  89. plain/preflight/files.py +3 -1
  90. plain/preflight/registry.py +26 -11
  91. plain/preflight/results.py +15 -7
  92. plain/preflight/security.py +15 -13
  93. plain/preflight/settings.py +54 -0
  94. plain/preflight/urls.py +4 -1
  95. plain/runtime/README.md +115 -47
  96. plain/runtime/__init__.py +10 -6
  97. plain/runtime/global_settings.py +34 -25
  98. plain/runtime/secret.py +20 -0
  99. plain/runtime/user_settings.py +110 -38
  100. plain/runtime/utils.py +1 -1
  101. plain/server/LICENSE +35 -0
  102. plain/server/README.md +155 -0
  103. plain/server/__init__.py +9 -0
  104. plain/server/app.py +52 -0
  105. plain/server/arbiter.py +555 -0
  106. plain/server/config.py +118 -0
  107. plain/server/errors.py +31 -0
  108. plain/server/glogging.py +292 -0
  109. plain/server/http/__init__.py +12 -0
  110. plain/server/http/body.py +283 -0
  111. plain/server/http/errors.py +155 -0
  112. plain/server/http/message.py +400 -0
  113. plain/server/http/parser.py +70 -0
  114. plain/server/http/unreader.py +88 -0
  115. plain/server/http/wsgi.py +421 -0
  116. plain/server/pidfile.py +92 -0
  117. plain/server/sock.py +240 -0
  118. plain/server/util.py +317 -0
  119. plain/server/workers/__init__.py +6 -0
  120. plain/server/workers/base.py +304 -0
  121. plain/server/workers/sync.py +212 -0
  122. plain/server/workers/thread.py +399 -0
  123. plain/server/workers/workertmp.py +50 -0
  124. plain/signals/README.md +170 -1
  125. plain/signals/__init__.py +0 -1
  126. plain/signals/dispatch/dispatcher.py +49 -27
  127. plain/signing.py +131 -35
  128. plain/templates/README.md +211 -20
  129. plain/templates/jinja/__init__.py +13 -5
  130. plain/templates/jinja/environments.py +5 -4
  131. plain/templates/jinja/extensions.py +12 -5
  132. plain/templates/jinja/filters.py +7 -2
  133. plain/templates/jinja/globals.py +2 -2
  134. plain/test/README.md +184 -22
  135. plain/test/client.py +340 -222
  136. plain/test/encoding.py +9 -6
  137. plain/test/exceptions.py +7 -2
  138. plain/urls/README.md +157 -73
  139. plain/urls/converters.py +18 -15
  140. plain/urls/exceptions.py +2 -2
  141. plain/urls/patterns.py +38 -22
  142. plain/urls/resolvers.py +35 -25
  143. plain/urls/utils.py +5 -1
  144. plain/utils/README.md +250 -3
  145. plain/utils/cache.py +17 -11
  146. plain/utils/crypto.py +21 -5
  147. plain/utils/datastructures.py +89 -56
  148. plain/utils/dateparse.py +9 -6
  149. plain/utils/deconstruct.py +15 -7
  150. plain/utils/decorators.py +5 -1
  151. plain/utils/dotenv.py +373 -0
  152. plain/utils/duration.py +8 -4
  153. plain/utils/encoding.py +14 -7
  154. plain/utils/functional.py +66 -49
  155. plain/utils/hashable.py +5 -1
  156. plain/utils/html.py +36 -22
  157. plain/utils/http.py +16 -9
  158. plain/utils/inspect.py +14 -6
  159. plain/utils/ipv6.py +7 -3
  160. plain/utils/itercompat.py +6 -1
  161. plain/utils/module_loading.py +7 -3
  162. plain/utils/regex_helper.py +37 -23
  163. plain/utils/safestring.py +14 -6
  164. plain/utils/text.py +41 -23
  165. plain/utils/timezone.py +33 -22
  166. plain/utils/tree.py +35 -19
  167. plain/validators.py +94 -52
  168. plain/views/README.md +156 -79
  169. plain/views/__init__.py +0 -1
  170. plain/views/base.py +25 -18
  171. plain/views/errors.py +13 -5
  172. plain/views/exceptions.py +4 -1
  173. plain/views/forms.py +6 -6
  174. plain/views/objects.py +52 -49
  175. plain/views/redirect.py +18 -15
  176. plain/views/templates.py +5 -3
  177. plain/wsgi.py +3 -1
  178. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
  179. plain-0.103.0.dist-info/RECORD +198 -0
  180. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
  181. plain-0.103.0.dist-info/entry_points.txt +2 -0
  182. plain/AGENTS.md +0 -18
  183. plain/cli/agent/__init__.py +0 -20
  184. plain/cli/agent/docs.py +0 -80
  185. plain/cli/agent/md.py +0 -87
  186. plain/cli/agent/prompt.py +0 -45
  187. plain/csrf/views.py +0 -31
  188. plain/logs/utils.py +0 -46
  189. plain/templates/AGENTS.md +0 -3
  190. plain-0.68.0.dist-info/RECORD +0 -169
  191. plain-0.68.0.dist-info/entry_points.txt +0 -5
  192. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/http/response.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
4
  import io
3
5
  import json
@@ -6,10 +8,11 @@ import os
6
8
  import re
7
9
  import sys
8
10
  import time
11
+ from collections.abc import Iterator
9
12
  from email.header import Header
10
- from functools import cached_property
11
13
  from http.client import responses
12
14
  from http.cookies import SimpleCookie
15
+ from typing import IO, Any
13
16
 
14
17
  from plain import signals
15
18
  from plain.http.cookie import sign_cookie_value
@@ -27,7 +30,7 @@ _charset_from_content_type_re = _lazy_re_compile(
27
30
 
28
31
 
29
32
  class ResponseHeaders(CaseInsensitiveMapping):
30
- def __init__(self, data):
33
+ def __init__(self, data: dict[str, Any] | None = None):
31
34
  """
32
35
  Populate the initial data using __setitem__ to ensure values are
33
36
  correctly encoded.
@@ -37,7 +40,9 @@ class ResponseHeaders(CaseInsensitiveMapping):
37
40
  for header, value in self._unpack_items(data):
38
41
  self[header] = value
39
42
 
40
- def _convert_to_charset(self, value, charset, mime_encode=False):
43
+ def _convert_to_charset(
44
+ self, value: str | bytes, charset: str, mime_encode: bool = False
45
+ ) -> str:
41
46
  """
42
47
  Convert headers key/value to ascii/latin-1 native strings.
43
48
  `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
@@ -72,22 +77,26 @@ class ResponseHeaders(CaseInsensitiveMapping):
72
77
  if mime_encode:
73
78
  value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
74
79
  else:
75
- e.reason += f", HTTP response headers must be in {charset} format"
80
+ if hasattr(e, "reason") and isinstance(e.reason, str):
81
+ e.reason += f", HTTP response headers must be in {charset} format"
76
82
  raise
77
83
  return value
78
84
 
79
- def __delitem__(self, key):
85
+ def __delitem__(self, key: str) -> None:
80
86
  self.pop(key)
81
87
 
82
- def __setitem__(self, key, value):
88
+ def __setitem__(self, key: str, value: str | bytes | None) -> None:
83
89
  key = self._convert_to_charset(key, "ascii")
84
- value = self._convert_to_charset(value, "latin-1", mime_encode=True)
85
- self._store[key.lower()] = (key, value)
90
+ if value is None:
91
+ self._store[key.lower()] = (key, None)
92
+ else:
93
+ value = self._convert_to_charset(value, "latin-1", mime_encode=True)
94
+ self._store[key.lower()] = (key, value)
86
95
 
87
- def pop(self, key, default=None):
96
+ def pop(self, key: str, default: Any = None) -> Any:
88
97
  return self._store.pop(key.lower(), default)
89
98
 
90
- def setdefault(self, key, value):
99
+ def setdefault(self, key: str, value: str | bytes) -> None:
91
100
  if key not in self:
92
101
  self[key] = value
93
102
 
@@ -109,11 +118,11 @@ class ResponseBase:
109
118
  def __init__(
110
119
  self,
111
120
  *,
112
- content_type=None,
113
- status_code=None,
114
- reason=None,
115
- charset=None,
116
- headers=None,
121
+ content_type: str | None = None,
122
+ status_code: int | None = None,
123
+ reason: str | None = None,
124
+ charset: str | None = None,
125
+ headers: dict[str, Any] | None = None,
117
126
  ):
118
127
  self.headers = ResponseHeaders(headers)
119
128
  self._charset = charset
@@ -141,9 +150,11 @@ class ResponseBase:
141
150
  if not 100 <= self.status_code <= 599:
142
151
  raise ValueError("HTTP status code must be an integer from 100 to 599.")
143
152
  self._reason_phrase = reason
153
+ # Exception that caused this response, if any (primarily for 500 errors)
154
+ self.exception: Exception | None = None
144
155
 
145
156
  @property
146
- def reason_phrase(self):
157
+ def reason_phrase(self) -> str:
147
158
  if self._reason_phrase is not None:
148
159
  return self._reason_phrase
149
160
  # Leave self._reason_phrase unset in order to use the default
@@ -151,11 +162,11 @@ class ResponseBase:
151
162
  return responses.get(self.status_code, "Unknown Status Code")
152
163
 
153
164
  @reason_phrase.setter
154
- def reason_phrase(self, value):
165
+ def reason_phrase(self, value: str) -> None:
155
166
  self._reason_phrase = value
156
167
 
157
168
  @property
158
- def charset(self):
169
+ def charset(self) -> str:
159
170
  if self._charset is not None:
160
171
  return self._charset
161
172
  # The Content-Type header may not yet be set, because the charset is
@@ -170,22 +181,11 @@ class ResponseBase:
170
181
  return settings.DEFAULT_CHARSET
171
182
 
172
183
  @charset.setter
173
- def charset(self, value):
184
+ def charset(self, value: str) -> None:
174
185
  self._charset = value
175
186
 
176
- def serialize_headers(self):
177
- """HTTP headers as a bytestring."""
178
- return b"\r\n".join(
179
- [
180
- key.encode("ascii") + b": " + value.encode("latin-1")
181
- for key, value in self.headers.items()
182
- ]
183
- )
184
-
185
- __bytes__ = serialize_headers
186
-
187
187
  @property
188
- def _content_type_for_repr(self):
188
+ def _content_type_for_repr(self) -> str:
189
189
  return (
190
190
  ', "{}"'.format(self.headers["Content-Type"])
191
191
  if "Content-Type" in self.headers
@@ -194,16 +194,16 @@ class ResponseBase:
194
194
 
195
195
  def set_cookie(
196
196
  self,
197
- key,
198
- value="",
199
- max_age=None,
200
- expires=None,
201
- path="/",
202
- domain=None,
203
- secure=False,
204
- httponly=False,
205
- samesite=None,
206
- ):
197
+ key: str,
198
+ value: str = "",
199
+ max_age: int | float | datetime.timedelta | None = None,
200
+ expires: str | datetime.datetime | None = None,
201
+ path: str | None = "/",
202
+ domain: str | None = None,
203
+ secure: bool = False,
204
+ httponly: bool = False,
205
+ samesite: str | None = None,
206
+ ) -> None:
207
207
  """
208
208
  Set a cookie.
209
209
 
@@ -256,18 +256,26 @@ class ResponseBase:
256
256
  raise ValueError('samesite must be "lax", "none", or "strict".')
257
257
  self.cookies[key]["samesite"] = samesite
258
258
 
259
- def set_signed_cookie(self, key, value, salt="", **kwargs):
259
+ def set_signed_cookie(
260
+ self, key: str, value: str, salt: str = "", **kwargs: Any
261
+ ) -> None:
260
262
  """Set a cookie signed with the SECRET_KEY."""
261
263
 
262
264
  signed_value = sign_cookie_value(key, value, salt)
263
265
  return self.set_cookie(key, signed_value, **kwargs)
264
266
 
265
- def delete_cookie(self, key, path="/", domain=None, samesite=None):
267
+ def delete_cookie(
268
+ self,
269
+ key: str,
270
+ path: str = "/",
271
+ domain: str | None = None,
272
+ samesite: str | None = None,
273
+ ) -> None:
266
274
  # Browsers can ignore the Set-Cookie header if the cookie doesn't use
267
275
  # the secure flag and:
268
276
  # - the cookie name starts with "__Host-" or "__Secure-", or
269
277
  # - the samesite is "none".
270
- secure = key.startswith(("__Secure-", "__Host-")) or (
278
+ secure = key.startswith(("__Secure-", "__Host-")) or bool(
271
279
  samesite and samesite.lower() == "none"
272
280
  )
273
281
  self.set_cookie(
@@ -282,7 +290,7 @@ class ResponseBase:
282
290
 
283
291
  # Common methods used by subclasses
284
292
 
285
- def make_bytes(self, value):
293
+ def make_bytes(self, value: str | bytes) -> bytes:
286
294
  """Turn a value into a bytestring encoded in the output charset."""
287
295
  # Per PEP 3333, this response body must be bytes. To avoid returning
288
296
  # an instance of a subclass, this function returns `bytes(value)`.
@@ -298,12 +306,9 @@ class ResponseBase:
298
306
  # Handle non-string types.
299
307
  return str(value).encode(self.charset)
300
308
 
301
- # These methods partially implement the file-like object interface.
302
- # See https://docs.python.org/library/io.html#io.IOBase
303
-
304
309
  # The WSGI server must call this method upon completion of the request.
305
310
  # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
306
- def close(self):
311
+ def close(self) -> None:
307
312
  for closer in self._resource_closers:
308
313
  try:
309
314
  closer()
@@ -314,32 +319,6 @@ class ResponseBase:
314
319
  self.closed = True
315
320
  signals.request_finished.send(sender=self._handler_class)
316
321
 
317
- def write(self, content):
318
- raise OSError(f"This {self.__class__.__name__} instance is not writable")
319
-
320
- def flush(self):
321
- pass
322
-
323
- def tell(self):
324
- raise OSError(
325
- f"This {self.__class__.__name__} instance cannot tell its position"
326
- )
327
-
328
- # These methods partially implement a stream-like object interface.
329
- # See https://docs.python.org/library/io.html#io.IOBase
330
-
331
- def readable(self):
332
- return False
333
-
334
- def seekable(self):
335
- return False
336
-
337
- def writable(self):
338
- return False
339
-
340
- def writelines(self, lines):
341
- raise OSError(f"This {self.__class__.__name__} instance is not writable")
342
-
343
322
 
344
323
  class Response(ResponseBase):
345
324
  """
@@ -349,86 +328,42 @@ class Response(ResponseBase):
349
328
  """
350
329
 
351
330
  streaming = False
352
- non_picklable_attrs = frozenset(
353
- [
354
- "resolver_match",
355
- # Non-picklable attributes added by test clients.
356
- "client",
357
- "context",
358
- "json",
359
- "templates",
360
- ]
361
- )
362
-
363
- def __init__(self, content=b"", **kwargs):
331
+
332
+ def __init__(self, content: bytes | str | Iterator[bytes] = b"", **kwargs: Any):
364
333
  super().__init__(**kwargs)
365
334
  # Content is a bytestring. See the `content` property methods.
366
335
  self.content = content
367
336
 
368
- def __getstate__(self):
369
- obj_dict = self.__dict__.copy()
370
- for attr in self.non_picklable_attrs:
371
- if attr in obj_dict:
372
- del obj_dict[attr]
373
- return obj_dict
374
-
375
- def __repr__(self):
337
+ def __repr__(self) -> str:
376
338
  return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
377
339
  "cls": self.__class__.__name__,
378
340
  "status_code": self.status_code,
379
341
  "content_type": self._content_type_for_repr,
380
342
  }
381
343
 
382
- def serialize(self):
383
- """Full HTTP message, including headers, as a bytestring."""
384
- return self.serialize_headers() + b"\r\n\r\n" + self.content
385
-
386
- __bytes__ = serialize
387
-
388
344
  @property
389
- def content(self):
345
+ def content(self) -> bytes:
390
346
  return b"".join(self._container)
391
347
 
392
348
  @content.setter
393
- def content(self, value):
349
+ def content(self, value: bytes | str | Iterator[bytes]) -> None:
394
350
  # Consume iterators upon assignment to allow repeated iteration.
395
351
  if hasattr(value, "__iter__") and not isinstance(
396
352
  value, bytes | memoryview | str
397
353
  ):
398
354
  content = b"".join(self.make_bytes(chunk) for chunk in value)
399
- if hasattr(value, "close"):
355
+ if hasattr(value, "close") and callable(getattr(value, "close")):
400
356
  try:
401
- value.close()
357
+ value.close() # type: ignore[union-attr]
402
358
  except Exception:
403
359
  pass
404
360
  else:
405
361
  content = self.make_bytes(value)
406
- # Create a list of properly encoded bytestrings to support write().
407
362
  self._container = [content]
408
363
 
409
- @cached_property
410
- def text(self):
411
- return self.content.decode(self.charset or "utf-8")
412
-
413
- def __iter__(self):
364
+ def __iter__(self) -> Iterator[bytes]:
414
365
  return iter(self._container)
415
366
 
416
- def write(self, content):
417
- self._container.append(self.make_bytes(content))
418
-
419
- def tell(self):
420
- return len(self.content)
421
-
422
- def getvalue(self):
423
- return self.content
424
-
425
- def writable(self):
426
- return True
427
-
428
- def writelines(self, lines):
429
- for line in lines:
430
- self.write(line)
431
-
432
367
 
433
368
  class StreamingResponse(ResponseBase):
434
369
  """
@@ -441,13 +376,13 @@ class StreamingResponse(ResponseBase):
441
376
 
442
377
  streaming = True
443
378
 
444
- def __init__(self, streaming_content=(), **kwargs):
379
+ def __init__(self, streaming_content: Any = (), **kwargs: Any):
445
380
  super().__init__(**kwargs)
446
381
  # `streaming_content` should be an iterable of bytestrings.
447
382
  # See the `streaming_content` property methods.
448
383
  self.streaming_content = streaming_content
449
384
 
450
- def __repr__(self):
385
+ def __repr__(self) -> str:
451
386
  return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
452
387
  "cls": self.__class__.__qualname__,
453
388
  "status_code": self.status_code,
@@ -455,32 +390,29 @@ class StreamingResponse(ResponseBase):
455
390
  }
456
391
 
457
392
  @property
458
- def content(self):
393
+ def content(self) -> bytes:
459
394
  raise AttributeError(
460
395
  f"This {self.__class__.__name__} instance has no `content` attribute. Use "
461
396
  "`streaming_content` instead."
462
397
  )
463
398
 
464
399
  @property
465
- def streaming_content(self):
400
+ def streaming_content(self) -> Iterator[bytes]:
466
401
  return map(self.make_bytes, self._iterator)
467
402
 
468
403
  @streaming_content.setter
469
- def streaming_content(self, value):
404
+ def streaming_content(self, value: Iterator[bytes | str]) -> None:
470
405
  self._set_streaming_content(value)
471
406
 
472
- def _set_streaming_content(self, value):
407
+ def _set_streaming_content(self, value: Iterator[bytes | str]) -> None:
473
408
  # Ensure we can never iterate on "value" more than once.
474
409
  self._iterator = iter(value)
475
410
  if hasattr(value, "close"):
476
411
  self._resource_closers.append(value.close)
477
412
 
478
- def __iter__(self):
413
+ def __iter__(self) -> Iterator[bytes]:
479
414
  return iter(self.streaming_content)
480
415
 
481
- def getvalue(self):
482
- return b"".join(self.streaming_content)
483
-
484
416
 
485
417
  class FileResponse(StreamingResponse):
486
418
  """
@@ -489,7 +421,9 @@ class FileResponse(StreamingResponse):
489
421
 
490
422
  block_size = 4096
491
423
 
492
- def __init__(self, *args, as_attachment=False, filename="", **kwargs):
424
+ def __init__(
425
+ self, *args: Any, as_attachment: bool = False, filename: str = "", **kwargs: Any
426
+ ):
493
427
  self.as_attachment = as_attachment
494
428
  self.filename = filename
495
429
  self._no_explicit_content_type = (
@@ -497,7 +431,7 @@ class FileResponse(StreamingResponse):
497
431
  )
498
432
  super().__init__(*args, **kwargs)
499
433
 
500
- def _set_streaming_content(self, value):
434
+ def _set_streaming_content(self, value: Any) -> None:
501
435
  if not hasattr(value, "read"):
502
436
  self.file_to_stream = None
503
437
  return super()._set_streaming_content(value)
@@ -509,7 +443,7 @@ class FileResponse(StreamingResponse):
509
443
  self.set_headers(filelike)
510
444
  super()._set_streaming_content(value)
511
445
 
512
- def set_headers(self, filelike):
446
+ def set_headers(self, filelike: IO[bytes]) -> None:
513
447
  """
514
448
  Set some common response headers (Content-Length, Content-Type, and
515
449
  Content-Disposition) based on the `filelike` response content.
@@ -523,19 +457,21 @@ class FileResponse(StreamingResponse):
523
457
  if seekable:
524
458
  initial_position = filelike.tell()
525
459
  filelike.seek(0, io.SEEK_END)
526
- self.headers["Content-Length"] = filelike.tell() - initial_position
460
+ self.headers["Content-Length"] = str(filelike.tell() - initial_position)
527
461
  filelike.seek(initial_position)
528
- elif hasattr(filelike, "getbuffer"):
529
- self.headers["Content-Length"] = (
530
- filelike.getbuffer().nbytes - filelike.tell()
462
+ elif hasattr(filelike, "getbuffer") and callable(
463
+ getattr(filelike, "getbuffer")
464
+ ):
465
+ self.headers["Content-Length"] = str(
466
+ filelike.getbuffer().nbytes - filelike.tell() # type: ignore[union-attr]
531
467
  )
532
468
  elif os.path.exists(filename):
533
- self.headers["Content-Length"] = (
469
+ self.headers["Content-Length"] = str(
534
470
  os.path.getsize(filename) - filelike.tell()
535
471
  )
536
472
  elif seekable:
537
- self.headers["Content-Length"] = sum(
538
- iter(lambda: len(filelike.read(self.block_size)), 0)
473
+ self.headers["Content-Length"] = str(
474
+ sum(iter(lambda: len(filelike.read(self.block_size)), 0))
539
475
  )
540
476
  filelike.seek(-int(self.headers["Content-Length"]), io.SEEK_END)
541
477
 
@@ -564,20 +500,20 @@ class FileResponse(StreamingResponse):
564
500
  self.headers["Content-Disposition"] = content_disposition
565
501
 
566
502
 
567
- class ResponseRedirect(Response):
503
+ class RedirectResponse(Response):
568
504
  """HTTP redirect response"""
569
505
 
570
506
  status_code = 302
571
507
 
572
- def __init__(self, redirect_to, **kwargs):
508
+ def __init__(self, redirect_to: str, **kwargs: Any):
573
509
  super().__init__(**kwargs)
574
- self.headers["Location"] = iri_to_uri(redirect_to)
510
+ self.headers["Location"] = iri_to_uri(redirect_to) or ""
575
511
 
576
512
  @property
577
- def url(self):
513
+ def url(self) -> str:
578
514
  return self.headers["Location"]
579
515
 
580
- def __repr__(self):
516
+ def __repr__(self) -> str:
581
517
  return (
582
518
  '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">' # noqa: UP031
583
519
  % {
@@ -589,17 +525,17 @@ class ResponseRedirect(Response):
589
525
  )
590
526
 
591
527
 
592
- class ResponseNotModified(Response):
528
+ class NotModifiedResponse(Response):
593
529
  """HTTP 304 response"""
594
530
 
595
531
  status_code = 304
596
532
 
597
- def __init__(self, *args, **kwargs):
533
+ def __init__(self, *args: Any, **kwargs: Any):
598
534
  super().__init__(*args, **kwargs)
599
535
  del self.headers["content-type"]
600
536
 
601
537
  @Response.content.setter
602
- def content(self, value):
538
+ def content(self, value: bytes | str | Iterator[bytes]) -> None:
603
539
  if value:
604
540
  raise AttributeError(
605
541
  "You cannot set content to a 304 (Not Modified) response"
@@ -607,34 +543,16 @@ class ResponseNotModified(Response):
607
543
  self._container = []
608
544
 
609
545
 
610
- class ResponseBadRequest(Response):
611
- """HTTP 400 response"""
612
-
613
- status_code = 400
614
-
615
-
616
- class ResponseNotFound(Response):
617
- """HTTP 404 response"""
618
-
619
- status_code = 404
620
-
621
-
622
- class ResponseForbidden(Response):
623
- """HTTP 403 response"""
624
-
625
- status_code = 403
626
-
627
-
628
- class ResponseNotAllowed(Response):
546
+ class NotAllowedResponse(Response):
629
547
  """HTTP 405 response"""
630
548
 
631
549
  status_code = 405
632
550
 
633
- def __init__(self, permitted_methods, *args, **kwargs):
551
+ def __init__(self, permitted_methods: list[str], *args: Any, **kwargs: Any):
634
552
  super().__init__(*args, **kwargs)
635
553
  self.headers["Allow"] = ", ".join(permitted_methods)
636
554
 
637
- def __repr__(self):
555
+ def __repr__(self) -> str:
638
556
  return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
639
557
  "cls": self.__class__.__name__,
640
558
  "status_code": self.status_code,
@@ -643,22 +561,6 @@ class ResponseNotAllowed(Response):
643
561
  }
644
562
 
645
563
 
646
- class ResponseGone(Response):
647
- """HTTP 410 response"""
648
-
649
- status_code = 410
650
-
651
-
652
- class ResponseServerError(Response):
653
- """HTTP 500 response"""
654
-
655
- status_code = 500
656
-
657
-
658
- class Http404(Exception):
659
- pass
660
-
661
-
662
564
  class JsonResponse(Response):
663
565
  """
664
566
  An HTTP response class that consumes data to be serialized to JSON.
@@ -675,11 +577,11 @@ class JsonResponse(Response):
675
577
 
676
578
  def __init__(
677
579
  self,
678
- data,
679
- encoder=PlainJSONEncoder,
680
- safe=True,
681
- json_dumps_params=None,
682
- **kwargs,
580
+ data: Any,
581
+ encoder: type[json.JSONEncoder] = PlainJSONEncoder,
582
+ safe: bool = True,
583
+ json_dumps_params: dict[str, Any] | None = None,
584
+ **kwargs: Any,
683
585
  ):
684
586
  if safe and not isinstance(data, dict):
685
587
  raise TypeError(
@@ -1,4 +1,11 @@
1
- def internalcode(obj):
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ def internalcode(obj: T) -> T:
2
9
  """
3
10
  A decorator that simply marks a class or function as internal.
4
11