DefEssential 0.1.8__23-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.
- DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +273 -0
- DefEssential/Adapter/Implements/Python/__init__.py +5 -0
- DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
- DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
- DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +109 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +657 -0
- DefEssential/Body.py +50 -0
- DefEssential/Content.py +18 -0
- DefEssential/Credentials.py +245 -0
- DefEssential/CrossOriginResourceSharing.py +107 -0
- DefEssential/Descriptor/DataObject.py +409 -0
- DefEssential/Descriptor/Types.py +248 -0
- DefEssential/Descriptor/Value.py +188 -0
- DefEssential/Descriptor/__init__.py +172 -0
- DefEssential/Error.py +66 -0
- DefEssential/Extension.py +191 -0
- DefEssential/Extensions/File.py +95 -0
- DefEssential/Extensions/FileSystem.py +195 -0
- DefEssential/Extensions/Metrics.py +511 -0
- DefEssential/Extensions/Ping.py +93 -0
- DefEssential/Extensions/Specifications.py +342 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +110 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +117 -0
- DefEssential/Extensions/Utils/Routes.py +42 -0
- DefEssential/Extensions/Utils/__init__.py +43 -0
- DefEssential/Extensions/__init__.py +21 -0
- DefEssential/IO/Readers/ChunkedStream.py +106 -0
- DefEssential/IO/Readers/ContentReader.py +36 -0
- DefEssential/IO/Readers/ContentReaders.py +241 -0
- DefEssential/IO/Readers/__init__.py +25 -0
- DefEssential/IO/RequestReader.py +125 -0
- DefEssential/IO/ResponseWriter.py +242 -0
- DefEssential/IO/WebSocket/WebSocket.py +288 -0
- DefEssential/IO/WebSocket/WebSocketError.py +17 -0
- DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
- DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefEssential/IO/WebSocket/__init__.py +13 -0
- DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefEssential/IO/Writers/ServerSentEvents.py +41 -0
- DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
- DefEssential/IO/Writers/StreamWriter.py +57 -0
- DefEssential/IO/Writers/__init__.py +11 -0
- DefEssential/IO/__init__.py +7 -0
- DefEssential/Request.py +110 -0
- DefEssential/RequestRunner.py +261 -0
- DefEssential/Response.py +89 -0
- DefEssential/ResponseSpecification.py +23 -0
- DefEssential/Responses/ResponseFile.py +108 -0
- DefEssential/Responses/__init__.py +63 -0
- DefEssential/Route.py +55 -0
- DefEssential/Router.py +48 -0
- DefEssential/Runners/RunExtension.py +116 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +198 -0
- DefEssential/Runners/__init__.py +5 -0
- DefEssential/Startup.py +104 -0
- DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Connection.py +57 -0
- DefEssential/Utils/FileSystemObject/File.py +41 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefEssential/Utils/FileSystemObject/__init__.py +11 -0
- DefEssential/Utils/Loader.py +81 -0
- DefEssential/Utils/__init__.py +3 -0
- DefEssential/Validator/Pattern.py +36 -0
- DefEssential/Validator/Patterns/Array.py +30 -0
- DefEssential/Validator/Patterns/Common.py +92 -0
- DefEssential/Validator/Patterns/Compare.py +180 -0
- DefEssential/Validator/Patterns/Condition.py +60 -0
- DefEssential/Validator/Patterns/DataObject.py +85 -0
- DefEssential/Validator/Patterns/DateTime.py +111 -0
- DefEssential/Validator/Patterns/Number.py +43 -0
- DefEssential/Validator/Patterns/Object.py +150 -0
- DefEssential/Validator/Patterns/Size.py +110 -0
- DefEssential/Validator/Patterns/String.py +85 -0
- DefEssential/Validator/Patterns/Type.py +350 -0
- DefEssential/Validator/Patterns/__init__.py +137 -0
- DefEssential/Validator/Validate.py +29 -0
- DefEssential/Validator/Validator.py +30 -0
- DefEssential/Validator/__init__.py +11 -0
- DefEssential/Validators.py +255 -0
- DefEssential/__init__.py +83 -0
- defessential-0.1.8.dist-info/METADATA +82 -0
- defessential-0.1.8.dist-info/RECORD +104 -0
- defessential-0.1.8.dist-info/WHEEL +6 -0
- defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from json import loads
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Callable,
|
|
5
|
+
Optional,
|
|
6
|
+
Union,
|
|
7
|
+
)
|
|
8
|
+
from urllib.parse import parse_qs
|
|
9
|
+
|
|
10
|
+
from ...Error import (
|
|
11
|
+
ClientError,
|
|
12
|
+
Error,
|
|
13
|
+
)
|
|
14
|
+
from ...Request import Request
|
|
15
|
+
from ..RequestReader import RequestReader
|
|
16
|
+
from ...Validator import (
|
|
17
|
+
Pattern,
|
|
18
|
+
Validator,
|
|
19
|
+
)
|
|
20
|
+
from .ChunkedStream import ChunkedStream
|
|
21
|
+
from .ContentReader import ContentReader
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
'ByteStringContentReader',
|
|
25
|
+
'StreamContentReader',
|
|
26
|
+
'ChunkedStreamContentReader',
|
|
27
|
+
'TextContentReader',
|
|
28
|
+
'FormUrlEncodedContentReader',
|
|
29
|
+
'JavaScriptObjectNotationContentReader',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ByteStringContentReader(ContentReader):
|
|
34
|
+
"""Read and validate a binary body regardless of its Content-Type."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
va: Union[Validator, Pattern, None] = None,
|
|
39
|
+
error: Optional[Error] = None,
|
|
40
|
+
):
|
|
41
|
+
self.va = Validator(va) if isinstance(va, Pattern) else va
|
|
42
|
+
self.error = error
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def validate(
|
|
46
|
+
self,
|
|
47
|
+
content: Any,
|
|
48
|
+
) -> Any:
|
|
49
|
+
return self.va(content) if self.va is not None else content
|
|
50
|
+
|
|
51
|
+
def __call__(
|
|
52
|
+
self,
|
|
53
|
+
request: Request,
|
|
54
|
+
reader: RequestReader,
|
|
55
|
+
) -> Any:
|
|
56
|
+
try:
|
|
57
|
+
return self.read_content(
|
|
58
|
+
request,
|
|
59
|
+
reader,
|
|
60
|
+
)
|
|
61
|
+
except Error as error:
|
|
62
|
+
if self.error is not None:
|
|
63
|
+
raise self.error from error
|
|
64
|
+
raise
|
|
65
|
+
except (ValueError, TypeError, LookupError) as error:
|
|
66
|
+
raise self.error if self.error is not None else ClientError(str(error)) from error
|
|
67
|
+
|
|
68
|
+
def read_content(
|
|
69
|
+
self,
|
|
70
|
+
request: Request,
|
|
71
|
+
reader: RequestReader,
|
|
72
|
+
) -> Any:
|
|
73
|
+
return self.validate(reader.read())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StreamContentReader(ByteStringContentReader):
|
|
77
|
+
"""Expose a Content-Length body as a binary stream without pre-reading."""
|
|
78
|
+
|
|
79
|
+
def read_content(
|
|
80
|
+
self,
|
|
81
|
+
request: Request,
|
|
82
|
+
reader: RequestReader,
|
|
83
|
+
) -> RequestReader:
|
|
84
|
+
if reader.chunked:
|
|
85
|
+
raise ClientError(
|
|
86
|
+
'Content-Length is required',
|
|
87
|
+
status=411,
|
|
88
|
+
)
|
|
89
|
+
return reader
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ChunkedStreamContentReader(StreamContentReader):
|
|
93
|
+
"""Decode a chunked body as a binary stream that can read client chunks one at a time."""
|
|
94
|
+
|
|
95
|
+
def read_content(
|
|
96
|
+
self,
|
|
97
|
+
request: Request,
|
|
98
|
+
reader: RequestReader,
|
|
99
|
+
) -> ChunkedStream:
|
|
100
|
+
if not reader.chunked:
|
|
101
|
+
raise ClientError('Chunked transfer encoding is required')
|
|
102
|
+
return ChunkedStream(reader)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TextContentReader(ByteStringContentReader):
|
|
106
|
+
"""Decode text/* bodies into strings."""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
va: Union[Validator, Pattern, None] = None,
|
|
111
|
+
error: Optional[Error] = None,
|
|
112
|
+
typeerror: Optional[Error] = None,
|
|
113
|
+
):
|
|
114
|
+
super().__init__(
|
|
115
|
+
va=va,
|
|
116
|
+
error=error,
|
|
117
|
+
)
|
|
118
|
+
self.typeerror = typeerror
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
def accepts(
|
|
122
|
+
self,
|
|
123
|
+
format: str,
|
|
124
|
+
) -> bool:
|
|
125
|
+
return format.startswith('text/')
|
|
126
|
+
|
|
127
|
+
def __call__(
|
|
128
|
+
self,
|
|
129
|
+
request: Request,
|
|
130
|
+
reader: RequestReader,
|
|
131
|
+
) -> Any:
|
|
132
|
+
format = request.format
|
|
133
|
+
if format is None or not self.accepts(format):
|
|
134
|
+
raise self.typeerror if self.typeerror is not None else ClientError(
|
|
135
|
+
'Unsupported body format',
|
|
136
|
+
status=415,
|
|
137
|
+
)
|
|
138
|
+
return super().__call__(
|
|
139
|
+
request,
|
|
140
|
+
reader,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def decode(
|
|
144
|
+
self,
|
|
145
|
+
request: Request,
|
|
146
|
+
reader: RequestReader,
|
|
147
|
+
) -> str:
|
|
148
|
+
try:
|
|
149
|
+
return reader.read().decode(request.charset or 'utf-8')
|
|
150
|
+
except (UnicodeError, LookupError) as error:
|
|
151
|
+
raise ClientError('Invalid body character encoding') from error
|
|
152
|
+
|
|
153
|
+
def read_content(
|
|
154
|
+
self,
|
|
155
|
+
request: Request,
|
|
156
|
+
reader: RequestReader,
|
|
157
|
+
) -> Any:
|
|
158
|
+
return self.validate(self.decode(
|
|
159
|
+
request,
|
|
160
|
+
reader,
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class FormUrlEncodedContentReader(TextContentReader):
|
|
165
|
+
"""Read URL-encoded forms with single or multiple values."""
|
|
166
|
+
|
|
167
|
+
def accepts(
|
|
168
|
+
self,
|
|
169
|
+
format: str,
|
|
170
|
+
) -> bool:
|
|
171
|
+
return format == 'application/x-www-form-urlencoded'
|
|
172
|
+
|
|
173
|
+
def read_content(
|
|
174
|
+
self,
|
|
175
|
+
request: Request,
|
|
176
|
+
reader: RequestReader,
|
|
177
|
+
) -> Any:
|
|
178
|
+
values = parse_qs(
|
|
179
|
+
self.decode(
|
|
180
|
+
request,
|
|
181
|
+
reader,
|
|
182
|
+
),
|
|
183
|
+
keep_blank_values=True,
|
|
184
|
+
)
|
|
185
|
+
return self.validate({
|
|
186
|
+
key: items[0] if len(items) == 1 else items
|
|
187
|
+
for key, items in values.items()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class JavaScriptObjectNotationContentReader(TextContentReader):
|
|
192
|
+
"""Read JSON bodies with optional value conversion and validation."""
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
va: Union[Validator, Pattern, None] = None,
|
|
197
|
+
typereader: Optional[Callable[[Any], Any]] = None,
|
|
198
|
+
error: Optional[Error] = None,
|
|
199
|
+
typeerror: Optional[Error] = None,
|
|
200
|
+
):
|
|
201
|
+
super().__init__(
|
|
202
|
+
va=va,
|
|
203
|
+
error=error,
|
|
204
|
+
typeerror=typeerror,
|
|
205
|
+
)
|
|
206
|
+
self.typereader = typereader
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
def accepts(
|
|
210
|
+
self,
|
|
211
|
+
format: str,
|
|
212
|
+
) -> bool:
|
|
213
|
+
return format == 'application/json'
|
|
214
|
+
|
|
215
|
+
def convert(
|
|
216
|
+
self,
|
|
217
|
+
value: Any,
|
|
218
|
+
) -> Any:
|
|
219
|
+
if isinstance(value, dict):
|
|
220
|
+
return {
|
|
221
|
+
key: self.convert(item) for key, item in value.items()
|
|
222
|
+
}
|
|
223
|
+
if isinstance(value, list):
|
|
224
|
+
return [self.convert(item) for item in value]
|
|
225
|
+
return self.typereader(value) if self.typereader is not None else value
|
|
226
|
+
|
|
227
|
+
def read_content(
|
|
228
|
+
self,
|
|
229
|
+
request: Request,
|
|
230
|
+
reader: RequestReader,
|
|
231
|
+
) -> Any:
|
|
232
|
+
try:
|
|
233
|
+
value = loads(self.decode(
|
|
234
|
+
request,
|
|
235
|
+
reader,
|
|
236
|
+
))
|
|
237
|
+
except ValueError as error:
|
|
238
|
+
raise ClientError('Invalid JSON body') from error
|
|
239
|
+
if self.typereader is not None:
|
|
240
|
+
value = self.convert(value)
|
|
241
|
+
return self.validate(value)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .ChunkedStream import ChunkedStream
|
|
2
|
+
from .ContentReader import (
|
|
3
|
+
ContentReader,
|
|
4
|
+
TypeReader,
|
|
5
|
+
)
|
|
6
|
+
from .ContentReaders import (
|
|
7
|
+
ByteStringContentReader,
|
|
8
|
+
StreamContentReader,
|
|
9
|
+
ChunkedStreamContentReader,
|
|
10
|
+
TextContentReader,
|
|
11
|
+
FormUrlEncodedContentReader,
|
|
12
|
+
JavaScriptObjectNotationContentReader,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
'ChunkedStream',
|
|
17
|
+
'ContentReader',
|
|
18
|
+
'TypeReader',
|
|
19
|
+
'ByteStringContentReader',
|
|
20
|
+
'StreamContentReader',
|
|
21
|
+
'ChunkedStreamContentReader',
|
|
22
|
+
'TextContentReader',
|
|
23
|
+
'FormUrlEncodedContentReader',
|
|
24
|
+
'JavaScriptObjectNotationContentReader',
|
|
25
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Protocol,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from ..Error import ClientError
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
'BinaryReader',
|
|
10
|
+
'ClientDisconnected',
|
|
11
|
+
'RequestReader',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BinaryReader(Protocol):
|
|
16
|
+
def read(
|
|
17
|
+
self,
|
|
18
|
+
size: int = -1,
|
|
19
|
+
/,
|
|
20
|
+
) -> bytes:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def readline(
|
|
24
|
+
self,
|
|
25
|
+
size: int = -1,
|
|
26
|
+
/,
|
|
27
|
+
) -> bytes:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClientDisconnected(ConnectionError):
|
|
32
|
+
"""Represent a client connection lost while receiving the request body."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RequestReader:
|
|
36
|
+
"""Read length-delimited and chunked request bodies.
|
|
37
|
+
|
|
38
|
+
When decoded=True, treat the input as having already been decoded by the transport layer.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
reader: BinaryReader,
|
|
44
|
+
size: Optional[int] = None,
|
|
45
|
+
chunked: bool = False,
|
|
46
|
+
decoded: bool = False,
|
|
47
|
+
):
|
|
48
|
+
if size is not None and size < 0:
|
|
49
|
+
raise ValueError('Body length must not be negative')
|
|
50
|
+
if chunked and size is not None:
|
|
51
|
+
raise ClientError('Content-Length and Transfer-Encoding cannot be used together')
|
|
52
|
+
self.reader = reader
|
|
53
|
+
self.remaining = size if size is not None else 0
|
|
54
|
+
self.chunked = chunked
|
|
55
|
+
self.decoded = decoded
|
|
56
|
+
self.finished = not chunked and self.remaining == 0
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
def line(self) -> bytes:
|
|
60
|
+
"""Read one raw CRLF-terminated line without the line ending."""
|
|
61
|
+
try:
|
|
62
|
+
line = self.reader.readline(65537)
|
|
63
|
+
except ConnectionError as error:
|
|
64
|
+
raise ClientDisconnected(*error.args) from error
|
|
65
|
+
if len(line) > 65536 or not line.endswith(b'\r\n'):
|
|
66
|
+
raise ClientError('Invalid or truncated body line')
|
|
67
|
+
return line[:-2]
|
|
68
|
+
|
|
69
|
+
def exact(
|
|
70
|
+
self,
|
|
71
|
+
size: int,
|
|
72
|
+
) -> bytes:
|
|
73
|
+
"""Read exactly size raw bytes."""
|
|
74
|
+
parts = []
|
|
75
|
+
while size:
|
|
76
|
+
try:
|
|
77
|
+
part = self.reader.read(size)
|
|
78
|
+
except ConnectionError as error:
|
|
79
|
+
raise ClientDisconnected(*error.args) from error
|
|
80
|
+
if not part:
|
|
81
|
+
raise ClientError('Connection closed while receiving the request body')
|
|
82
|
+
parts.append(part)
|
|
83
|
+
size -= len(part)
|
|
84
|
+
return b''.join(parts)
|
|
85
|
+
|
|
86
|
+
def read(
|
|
87
|
+
self,
|
|
88
|
+
size: int = -1,
|
|
89
|
+
) -> bytes:
|
|
90
|
+
if size < -1:
|
|
91
|
+
raise ValueError('Read size must be at least -1')
|
|
92
|
+
if self.chunked and not self.decoded:
|
|
93
|
+
raise ClientError(
|
|
94
|
+
'Content-Length is required',
|
|
95
|
+
status=411,
|
|
96
|
+
)
|
|
97
|
+
if size == 0 or self.finished:
|
|
98
|
+
return b''
|
|
99
|
+
if self.chunked:
|
|
100
|
+
try:
|
|
101
|
+
data = self.reader.read(size)
|
|
102
|
+
except ConnectionError as error:
|
|
103
|
+
raise ClientDisconnected(*error.args) from error
|
|
104
|
+
if not data or size == -1:
|
|
105
|
+
self.finished = True
|
|
106
|
+
return data
|
|
107
|
+
count = self.remaining if size == -1 else min(size, self.remaining)
|
|
108
|
+
data = self.exact(count)
|
|
109
|
+
self.remaining -= len(data)
|
|
110
|
+
self.finished = self.remaining == 0
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
def readline(
|
|
114
|
+
self,
|
|
115
|
+
size: int = -1,
|
|
116
|
+
) -> bytes:
|
|
117
|
+
parts = []
|
|
118
|
+
while size == -1 or len(parts) < size:
|
|
119
|
+
byte = self.read(1)
|
|
120
|
+
if not byte:
|
|
121
|
+
break
|
|
122
|
+
parts.append(byte)
|
|
123
|
+
if byte == b'\n':
|
|
124
|
+
break
|
|
125
|
+
return b''.join(parts)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Callable,
|
|
3
|
+
Optional,
|
|
4
|
+
Protocol,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from ..Response import (
|
|
8
|
+
Response,
|
|
9
|
+
close_bodies,
|
|
10
|
+
)
|
|
11
|
+
from ..Error import ClientError
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'BinaryWriter',
|
|
15
|
+
'ResponseWriter',
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BinaryWriter(Protocol):
|
|
20
|
+
def write(
|
|
21
|
+
self,
|
|
22
|
+
data: bytes,
|
|
23
|
+
/,
|
|
24
|
+
) -> int:
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def flush(self) -> None:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ResponseWriter:
|
|
32
|
+
"""Share response state to prevent duplicate direct and returned responses."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
sender: Callable[[Response, bool], None],
|
|
37
|
+
writer: BinaryWriter,
|
|
38
|
+
method: str = 'GET',
|
|
39
|
+
version: str = 'HTTP/1.1',
|
|
40
|
+
):
|
|
41
|
+
self.sender = sender
|
|
42
|
+
self.writer = writer
|
|
43
|
+
self.method = method
|
|
44
|
+
self.version = version
|
|
45
|
+
self.started = False
|
|
46
|
+
self.status = 500
|
|
47
|
+
self.ended = False
|
|
48
|
+
self.failed = False
|
|
49
|
+
self.chunked = False
|
|
50
|
+
self.suppress_body = False
|
|
51
|
+
self.on_request_response: Optional[Callable[[Response], Response]] = None
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def start(
|
|
55
|
+
self,
|
|
56
|
+
response: Response,
|
|
57
|
+
chunked: bool,
|
|
58
|
+
) -> None:
|
|
59
|
+
if self.started:
|
|
60
|
+
raise RuntimeError('Cannot start a response that has already started')
|
|
61
|
+
if not 200 <= response.status <= 599:
|
|
62
|
+
raise ValueError('Final response status code must be between 200 and 599')
|
|
63
|
+
self.chunked = chunked
|
|
64
|
+
self.suppress_body = self.method == 'HEAD' or response.status in (
|
|
65
|
+
204,
|
|
66
|
+
205,
|
|
67
|
+
304,
|
|
68
|
+
)
|
|
69
|
+
if chunked and not self.suppress_body and self.version != 'HTTP/1.1':
|
|
70
|
+
raise ClientError('Chunked output requires an HTTP/1.1 request')
|
|
71
|
+
self.started = True
|
|
72
|
+
self.status = response.status
|
|
73
|
+
try:
|
|
74
|
+
self.sender(
|
|
75
|
+
response,
|
|
76
|
+
chunked,
|
|
77
|
+
)
|
|
78
|
+
except Exception:
|
|
79
|
+
self.failed = True
|
|
80
|
+
raise
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
def filter(
|
|
84
|
+
self,
|
|
85
|
+
response: Response,
|
|
86
|
+
) -> Response:
|
|
87
|
+
"""Call the injected response hook and validate a response for direct output."""
|
|
88
|
+
if self.on_request_response is not None:
|
|
89
|
+
response = self.on_request_response(response)
|
|
90
|
+
if not isinstance(response, Response):
|
|
91
|
+
raise TypeError('Response hooks must return a Response')
|
|
92
|
+
try:
|
|
93
|
+
if not isinstance(response.body, bytes) or response.body:
|
|
94
|
+
raise ValueError('Response hooks must not set a body for direct output')
|
|
95
|
+
if response.header('Content-Length') is not None:
|
|
96
|
+
raise ValueError('Content-Length must not be set for direct output')
|
|
97
|
+
except Exception:
|
|
98
|
+
close_bodies(
|
|
99
|
+
response.body,
|
|
100
|
+
preserve_error=True,
|
|
101
|
+
)
|
|
102
|
+
raise
|
|
103
|
+
return response
|
|
104
|
+
|
|
105
|
+
def begin(
|
|
106
|
+
self,
|
|
107
|
+
response: Response,
|
|
108
|
+
chunked: bool = False,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Send direct-output response headers once, after running response hooks and validation."""
|
|
111
|
+
if self.started:
|
|
112
|
+
raise RuntimeError('Cannot start a response that has already started')
|
|
113
|
+
self.start(
|
|
114
|
+
self.filter(response),
|
|
115
|
+
chunked,
|
|
116
|
+
)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
def upgrade(
|
|
120
|
+
self,
|
|
121
|
+
response: Response,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Send 101 Switching Protocols headers once through the response hook.
|
|
124
|
+
|
|
125
|
+
Subsequent write() calls send bytes without HTTP framing; the caller is
|
|
126
|
+
responsible for framing the upgraded protocol.
|
|
127
|
+
"""
|
|
128
|
+
if self.started:
|
|
129
|
+
raise RuntimeError('Cannot start a response that has already started')
|
|
130
|
+
if response.status != 101:
|
|
131
|
+
raise ValueError('Protocol upgrades require status 101')
|
|
132
|
+
response = self.filter(response)
|
|
133
|
+
if response.status != 101:
|
|
134
|
+
raise ValueError('Response hooks must keep status 101 for protocol upgrades')
|
|
135
|
+
self.chunked = False
|
|
136
|
+
self.suppress_body = False
|
|
137
|
+
self.started = True
|
|
138
|
+
self.status = response.status
|
|
139
|
+
try:
|
|
140
|
+
self.sender(
|
|
141
|
+
response,
|
|
142
|
+
False,
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
self.failed = True
|
|
146
|
+
raise
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def require_open(self) -> None:
|
|
150
|
+
if not self.started:
|
|
151
|
+
raise RuntimeError('Response must be started first')
|
|
152
|
+
if self.ended or self.failed:
|
|
153
|
+
raise RuntimeError('Cannot write to a completed or failed response')
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
def write(
|
|
157
|
+
self,
|
|
158
|
+
data: bytes,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Write bytes immediately using the current response framing."""
|
|
161
|
+
self.require_open()
|
|
162
|
+
if not isinstance(data, bytes):
|
|
163
|
+
raise TypeError('Response streams require bytes')
|
|
164
|
+
if not data or self.suppress_body:
|
|
165
|
+
return
|
|
166
|
+
if self.chunked:
|
|
167
|
+
data = format(len(data), 'x').encode('ascii') + b'\r\n' + data + b'\r\n'
|
|
168
|
+
self.write_bytes(data)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
def write_bytes(
|
|
172
|
+
self,
|
|
173
|
+
data: bytes,
|
|
174
|
+
) -> None:
|
|
175
|
+
try:
|
|
176
|
+
while data:
|
|
177
|
+
count = self.writer.write(data)
|
|
178
|
+
if count <= 0 or count > len(data):
|
|
179
|
+
raise OSError('Response write made no valid progress')
|
|
180
|
+
data = data[count:]
|
|
181
|
+
except Exception:
|
|
182
|
+
self.failed = True
|
|
183
|
+
raise
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def flush(self) -> None:
|
|
187
|
+
self.require_open()
|
|
188
|
+
try:
|
|
189
|
+
self.writer.flush()
|
|
190
|
+
except Exception:
|
|
191
|
+
self.failed = True
|
|
192
|
+
raise
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
def end(self) -> None:
|
|
196
|
+
"""Send the terminating chunk exactly once on successful completion."""
|
|
197
|
+
if self.ended:
|
|
198
|
+
return
|
|
199
|
+
self.require_open()
|
|
200
|
+
if self.chunked and not self.suppress_body:
|
|
201
|
+
self.write_bytes(b'0\r\n\r\n')
|
|
202
|
+
self.flush()
|
|
203
|
+
self.ended = True
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
def response(
|
|
207
|
+
self,
|
|
208
|
+
response: Response,
|
|
209
|
+
*,
|
|
210
|
+
filtered: bool = False,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Send returned bytes or iterable responses through the same transport boundary."""
|
|
213
|
+
if self.started:
|
|
214
|
+
raise RuntimeError('Cannot send a response that has already started')
|
|
215
|
+
if not filtered and self.on_request_response is not None:
|
|
216
|
+
response = self.on_request_response(response)
|
|
217
|
+
if not isinstance(response, Response):
|
|
218
|
+
raise TypeError('Response hooks must return a Response')
|
|
219
|
+
if isinstance(response.body, bytes) and response.status not in (
|
|
220
|
+
204,
|
|
221
|
+
205,
|
|
222
|
+
304,
|
|
223
|
+
):
|
|
224
|
+
response.header(
|
|
225
|
+
'Content-Length',
|
|
226
|
+
str(len(response.body)),
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
del response.props['Content-Length']
|
|
230
|
+
self.start(
|
|
231
|
+
response,
|
|
232
|
+
False,
|
|
233
|
+
)
|
|
234
|
+
if not self.suppress_body:
|
|
235
|
+
if isinstance(response.body, bytes):
|
|
236
|
+
self.write(response.body)
|
|
237
|
+
else:
|
|
238
|
+
for data in response.body:
|
|
239
|
+
self.write(data)
|
|
240
|
+
self.flush()
|
|
241
|
+
self.end()
|
|
242
|
+
return
|