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.
Files changed (104) hide show
  1. DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
  2. DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
  3. DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
  4. DefEssential/Adapter/Implements/Python/Server.py +273 -0
  5. DefEssential/Adapter/Implements/Python/__init__.py +5 -0
  6. DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
  7. DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
  8. DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +109 -0
  13. DefEssential/Adapter/Server.py +54 -0
  14. DefEssential/Adapter/WSGI.py +219 -0
  15. DefEssential/Adapter/__init__.py +5 -0
  16. DefEssential/Application.py +657 -0
  17. DefEssential/Body.py +50 -0
  18. DefEssential/Content.py +18 -0
  19. DefEssential/Credentials.py +245 -0
  20. DefEssential/CrossOriginResourceSharing.py +107 -0
  21. DefEssential/Descriptor/DataObject.py +409 -0
  22. DefEssential/Descriptor/Types.py +248 -0
  23. DefEssential/Descriptor/Value.py +188 -0
  24. DefEssential/Descriptor/__init__.py +172 -0
  25. DefEssential/Error.py +66 -0
  26. DefEssential/Extension.py +191 -0
  27. DefEssential/Extensions/File.py +95 -0
  28. DefEssential/Extensions/FileSystem.py +195 -0
  29. DefEssential/Extensions/Metrics.py +511 -0
  30. DefEssential/Extensions/Ping.py +93 -0
  31. DefEssential/Extensions/Specifications.py +342 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +110 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +117 -0
  36. DefEssential/Extensions/Utils/Routes.py +42 -0
  37. DefEssential/Extensions/Utils/__init__.py +43 -0
  38. DefEssential/Extensions/__init__.py +21 -0
  39. DefEssential/IO/Readers/ChunkedStream.py +106 -0
  40. DefEssential/IO/Readers/ContentReader.py +36 -0
  41. DefEssential/IO/Readers/ContentReaders.py +241 -0
  42. DefEssential/IO/Readers/__init__.py +25 -0
  43. DefEssential/IO/RequestReader.py +125 -0
  44. DefEssential/IO/ResponseWriter.py +242 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +288 -0
  46. DefEssential/IO/WebSocket/WebSocketError.py +17 -0
  47. DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
  48. DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
  49. DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
  50. DefEssential/IO/WebSocket/__init__.py +13 -0
  51. DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
  52. DefEssential/IO/Writers/ServerSentEvents.py +41 -0
  53. DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
  54. DefEssential/IO/Writers/StreamWriter.py +57 -0
  55. DefEssential/IO/Writers/__init__.py +11 -0
  56. DefEssential/IO/__init__.py +7 -0
  57. DefEssential/Request.py +110 -0
  58. DefEssential/RequestRunner.py +261 -0
  59. DefEssential/Response.py +89 -0
  60. DefEssential/ResponseSpecification.py +23 -0
  61. DefEssential/Responses/ResponseFile.py +108 -0
  62. DefEssential/Responses/__init__.py +63 -0
  63. DefEssential/Route.py +55 -0
  64. DefEssential/Router.py +48 -0
  65. DefEssential/Runners/RunExtension.py +116 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +198 -0
  69. DefEssential/Runners/__init__.py +5 -0
  70. DefEssential/Startup.py +104 -0
  71. DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
  72. DefEssential/Utils/FileSystemObject/Connection.py +57 -0
  73. DefEssential/Utils/FileSystemObject/File.py +41 -0
  74. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
  75. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
  76. DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
  77. DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
  78. DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
  79. DefEssential/Utils/FileSystemObject/__init__.py +11 -0
  80. DefEssential/Utils/Loader.py +81 -0
  81. DefEssential/Utils/__init__.py +3 -0
  82. DefEssential/Validator/Pattern.py +36 -0
  83. DefEssential/Validator/Patterns/Array.py +30 -0
  84. DefEssential/Validator/Patterns/Common.py +92 -0
  85. DefEssential/Validator/Patterns/Compare.py +180 -0
  86. DefEssential/Validator/Patterns/Condition.py +60 -0
  87. DefEssential/Validator/Patterns/DataObject.py +85 -0
  88. DefEssential/Validator/Patterns/DateTime.py +111 -0
  89. DefEssential/Validator/Patterns/Number.py +43 -0
  90. DefEssential/Validator/Patterns/Object.py +150 -0
  91. DefEssential/Validator/Patterns/Size.py +110 -0
  92. DefEssential/Validator/Patterns/String.py +85 -0
  93. DefEssential/Validator/Patterns/Type.py +350 -0
  94. DefEssential/Validator/Patterns/__init__.py +137 -0
  95. DefEssential/Validator/Validate.py +29 -0
  96. DefEssential/Validator/Validator.py +30 -0
  97. DefEssential/Validator/__init__.py +11 -0
  98. DefEssential/Validators.py +255 -0
  99. DefEssential/__init__.py +83 -0
  100. defessential-0.1.8.dist-info/METADATA +82 -0
  101. defessential-0.1.8.dist-info/RECORD +104 -0
  102. defessential-0.1.8.dist-info/WHEEL +6 -0
  103. defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
  104. 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