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.
- plain/CHANGELOG.md +656 -1
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.101.2.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(
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
530
|
-
|
|
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"] =
|
|
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
|
|
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
|
|
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
|
|
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(
|
plain/internal/__init__.py
CHANGED