plain 0.74.0__py3-none-any.whl → 0.76.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.
- plain/CHANGELOG.md +29 -0
- plain/README.md +1 -1
- plain/chores/README.md +1 -1
- plain/cli/core.py +35 -17
- plain/cli/runtime.py +28 -0
- plain/cli/server.py +143 -0
- plain/server/LICENSE +35 -0
- plain/server/README.md +75 -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 +150 -0
- plain/server/http/message.py +399 -0
- plain/server/http/parser.py +69 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +91 -0
- plain/server/reloader.py +158 -0
- plain/server/sock.py +219 -0
- plain/server/util.py +380 -0
- plain/server/workers/__init__.py +12 -0
- plain/server/workers/base.py +305 -0
- plain/server/workers/gthread.py +393 -0
- plain/server/workers/sync.py +210 -0
- plain/server/workers/workertmp.py +50 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/METADATA +2 -2
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/RECORD +35 -9
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/WHEEL +0 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/entry_points.txt +0 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/licenses/LICENSE +0 -0
plain/server/glogging.py
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# This file is part of gunicorn released under the MIT license.
|
6
|
+
# See the LICENSE for more information.
|
7
|
+
#
|
8
|
+
# Vendored and modified for Plain.
|
9
|
+
import base64
|
10
|
+
import binascii
|
11
|
+
import datetime
|
12
|
+
import logging
|
13
|
+
import time
|
14
|
+
from typing import TYPE_CHECKING, Any
|
15
|
+
|
16
|
+
logging.Logger.manager.emittedNoHandlerWarning = True # type: ignore[attr-defined]
|
17
|
+
import os # noqa: E402
|
18
|
+
import sys # noqa: E402
|
19
|
+
import threading # noqa: E402
|
20
|
+
import traceback # noqa: E402
|
21
|
+
|
22
|
+
from . import util # noqa: E402
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from io import TextIOWrapper
|
26
|
+
|
27
|
+
from .config import Config
|
28
|
+
|
29
|
+
|
30
|
+
def loggers() -> list[logging.Logger]:
|
31
|
+
"""get list of all loggers"""
|
32
|
+
root = logging.root
|
33
|
+
existing = list(root.manager.loggerDict.keys())
|
34
|
+
return [logging.getLogger(name) for name in existing]
|
35
|
+
|
36
|
+
|
37
|
+
class SafeAtoms(dict[str, Any]):
|
38
|
+
def __init__(self, atoms: dict[str, Any]) -> None:
|
39
|
+
dict.__init__(self)
|
40
|
+
for key, value in atoms.items():
|
41
|
+
if isinstance(value, str):
|
42
|
+
self[key] = value.replace('"', '\\"')
|
43
|
+
else:
|
44
|
+
self[key] = value
|
45
|
+
|
46
|
+
def __getitem__(self, k: str) -> Any:
|
47
|
+
if k.startswith("{"):
|
48
|
+
kl = k.lower()
|
49
|
+
if kl in self:
|
50
|
+
return super().__getitem__(kl)
|
51
|
+
else:
|
52
|
+
return "-"
|
53
|
+
if k in self:
|
54
|
+
return super().__getitem__(k)
|
55
|
+
else:
|
56
|
+
return "-"
|
57
|
+
|
58
|
+
|
59
|
+
class Logger:
|
60
|
+
LOG_LEVELS = {
|
61
|
+
"critical": logging.CRITICAL,
|
62
|
+
"error": logging.ERROR,
|
63
|
+
"warning": logging.WARNING,
|
64
|
+
"info": logging.INFO,
|
65
|
+
"debug": logging.DEBUG,
|
66
|
+
}
|
67
|
+
loglevel = logging.INFO
|
68
|
+
|
69
|
+
error_fmt = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s"
|
70
|
+
datefmt = r"[%Y-%m-%d %H:%M:%S %z]"
|
71
|
+
|
72
|
+
access_fmt = "%(message)s"
|
73
|
+
syslog_fmt = "[%(process)d] %(message)s"
|
74
|
+
|
75
|
+
atoms_wrapper_class = SafeAtoms
|
76
|
+
|
77
|
+
def __init__(self, cfg: Config) -> None:
|
78
|
+
self.error_log = logging.getLogger("plain.server.error")
|
79
|
+
self.error_log.propagate = False
|
80
|
+
self.access_log = logging.getLogger("plain.server.access")
|
81
|
+
self.access_log.propagate = False
|
82
|
+
self.error_handlers: list[logging.Handler] = []
|
83
|
+
self.access_handlers: list[logging.Handler] = []
|
84
|
+
self.logfile: TextIOWrapper | None = None
|
85
|
+
self.lock = threading.Lock()
|
86
|
+
self.cfg = cfg
|
87
|
+
self.setup(cfg)
|
88
|
+
|
89
|
+
def setup(self, cfg: Config) -> None:
|
90
|
+
self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO)
|
91
|
+
self.error_log.setLevel(self.loglevel)
|
92
|
+
self.access_log.setLevel(logging.INFO)
|
93
|
+
|
94
|
+
# set plain.server.error handler
|
95
|
+
self._set_handler(
|
96
|
+
self.error_log,
|
97
|
+
cfg.errorlog,
|
98
|
+
logging.Formatter(cfg.log_format, self.datefmt),
|
99
|
+
)
|
100
|
+
|
101
|
+
# set plain.server.access handler
|
102
|
+
if cfg.accesslog is not None:
|
103
|
+
self._set_handler(
|
104
|
+
self.access_log,
|
105
|
+
cfg.accesslog,
|
106
|
+
fmt=logging.Formatter(cfg.log_format, self.datefmt),
|
107
|
+
stream=sys.stdout,
|
108
|
+
)
|
109
|
+
|
110
|
+
def critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
111
|
+
self.error_log.critical(msg, *args, **kwargs)
|
112
|
+
|
113
|
+
def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
114
|
+
self.error_log.error(msg, *args, **kwargs)
|
115
|
+
|
116
|
+
def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
117
|
+
self.error_log.warning(msg, *args, **kwargs)
|
118
|
+
|
119
|
+
def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
120
|
+
self.error_log.info(msg, *args, **kwargs)
|
121
|
+
|
122
|
+
def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
123
|
+
self.error_log.debug(msg, *args, **kwargs)
|
124
|
+
|
125
|
+
def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
126
|
+
self.error_log.exception(msg, *args, **kwargs)
|
127
|
+
|
128
|
+
def log(self, lvl: int | str, msg: str, *args: Any, **kwargs: Any) -> None:
|
129
|
+
if isinstance(lvl, str):
|
130
|
+
lvl = self.LOG_LEVELS.get(lvl.lower(), logging.INFO)
|
131
|
+
self.error_log.log(lvl, msg, *args, **kwargs)
|
132
|
+
|
133
|
+
def atoms(
|
134
|
+
self,
|
135
|
+
resp: Any,
|
136
|
+
req: Any,
|
137
|
+
environ: dict[str, Any],
|
138
|
+
request_time: datetime.timedelta,
|
139
|
+
) -> dict[str, Any]:
|
140
|
+
"""Gets atoms for log formatting."""
|
141
|
+
status = resp.status
|
142
|
+
if isinstance(status, str):
|
143
|
+
status = status.split(None, 1)[0]
|
144
|
+
atoms = {
|
145
|
+
"h": environ.get("REMOTE_ADDR", "-"),
|
146
|
+
"l": "-",
|
147
|
+
"u": self._get_user(environ) or "-",
|
148
|
+
"t": self.now(),
|
149
|
+
"r": "{} {} {}".format(
|
150
|
+
environ["REQUEST_METHOD"],
|
151
|
+
environ["RAW_URI"],
|
152
|
+
environ["SERVER_PROTOCOL"],
|
153
|
+
),
|
154
|
+
"s": status,
|
155
|
+
"m": environ.get("REQUEST_METHOD"),
|
156
|
+
"U": environ.get("PATH_INFO"),
|
157
|
+
"q": environ.get("QUERY_STRING"),
|
158
|
+
"H": environ.get("SERVER_PROTOCOL"),
|
159
|
+
"b": getattr(resp, "sent", None) is not None and str(resp.sent) or "-",
|
160
|
+
"B": getattr(resp, "sent", None),
|
161
|
+
"f": environ.get("HTTP_REFERER", "-"),
|
162
|
+
"a": environ.get("HTTP_USER_AGENT", "-"),
|
163
|
+
"T": request_time.seconds,
|
164
|
+
"D": (request_time.seconds * 1000000) + request_time.microseconds,
|
165
|
+
"M": (request_time.seconds * 1000) + int(request_time.microseconds / 1000),
|
166
|
+
"L": f"{request_time.seconds}.{request_time.microseconds:06d}",
|
167
|
+
"p": f"<{os.getpid()}>",
|
168
|
+
}
|
169
|
+
|
170
|
+
# add request headers
|
171
|
+
if hasattr(req, "headers"):
|
172
|
+
req_headers = req.headers
|
173
|
+
else:
|
174
|
+
req_headers = req
|
175
|
+
|
176
|
+
if hasattr(req_headers, "items"):
|
177
|
+
req_headers = req_headers.items()
|
178
|
+
|
179
|
+
atoms.update({f"{{{k.lower()}}}i": v for k, v in req_headers})
|
180
|
+
|
181
|
+
resp_headers = resp.headers
|
182
|
+
if hasattr(resp_headers, "items"):
|
183
|
+
resp_headers = resp_headers.items()
|
184
|
+
|
185
|
+
# add response headers
|
186
|
+
atoms.update({f"{{{k.lower()}}}o": v for k, v in resp_headers})
|
187
|
+
|
188
|
+
# add environ variables
|
189
|
+
environ_variables = environ.items()
|
190
|
+
atoms.update({f"{{{k.lower()}}}e": v for k, v in environ_variables})
|
191
|
+
|
192
|
+
return atoms
|
193
|
+
|
194
|
+
def access(
|
195
|
+
self,
|
196
|
+
resp: Any,
|
197
|
+
req: Any,
|
198
|
+
environ: dict[str, Any],
|
199
|
+
request_time: datetime.timedelta,
|
200
|
+
) -> None:
|
201
|
+
"""See http://httpd.apache.org/docs/2.0/logs.html#combined
|
202
|
+
for format details
|
203
|
+
"""
|
204
|
+
|
205
|
+
if not self.cfg.accesslog:
|
206
|
+
return None
|
207
|
+
|
208
|
+
# wrap atoms:
|
209
|
+
# - make sure atoms will be test case insensitively
|
210
|
+
# - if atom doesn't exist replace it by '-'
|
211
|
+
safe_atoms = self.atoms_wrapper_class(
|
212
|
+
self.atoms(resp, req, environ, request_time)
|
213
|
+
)
|
214
|
+
|
215
|
+
try:
|
216
|
+
self.access_log.info(self.cfg.access_log_format, safe_atoms)
|
217
|
+
except Exception:
|
218
|
+
self.error(traceback.format_exc())
|
219
|
+
|
220
|
+
return None
|
221
|
+
|
222
|
+
def now(self) -> str:
|
223
|
+
"""return date in Apache Common Log Format"""
|
224
|
+
return time.strftime("[%d/%b/%Y:%H:%M:%S %z]")
|
225
|
+
|
226
|
+
def reopen_files(self) -> None:
|
227
|
+
for log in loggers():
|
228
|
+
for handler in log.handlers:
|
229
|
+
if isinstance(handler, logging.FileHandler):
|
230
|
+
handler.acquire()
|
231
|
+
try:
|
232
|
+
if handler.stream:
|
233
|
+
handler.close()
|
234
|
+
handler.stream = handler._open()
|
235
|
+
finally:
|
236
|
+
handler.release()
|
237
|
+
|
238
|
+
def close_on_exec(self) -> None:
|
239
|
+
for log in loggers():
|
240
|
+
for handler in log.handlers:
|
241
|
+
if isinstance(handler, logging.FileHandler):
|
242
|
+
handler.acquire()
|
243
|
+
try:
|
244
|
+
if handler.stream:
|
245
|
+
util.close_on_exec(handler.stream.fileno())
|
246
|
+
finally:
|
247
|
+
handler.release()
|
248
|
+
|
249
|
+
def _get_plain_server_handler(self, log: logging.Logger) -> logging.Handler | None:
|
250
|
+
for h in log.handlers:
|
251
|
+
if getattr(h, "_plain_server", False):
|
252
|
+
return h
|
253
|
+
return None
|
254
|
+
|
255
|
+
def _set_handler(
|
256
|
+
self,
|
257
|
+
log: logging.Logger,
|
258
|
+
output: str | None,
|
259
|
+
fmt: logging.Formatter,
|
260
|
+
stream: Any = None,
|
261
|
+
) -> None:
|
262
|
+
# remove previous plain server log handler
|
263
|
+
h = self._get_plain_server_handler(log)
|
264
|
+
if h:
|
265
|
+
log.handlers.remove(h)
|
266
|
+
|
267
|
+
if output is not None:
|
268
|
+
if output == "-":
|
269
|
+
h = logging.StreamHandler(stream)
|
270
|
+
else:
|
271
|
+
util.check_is_writable(output)
|
272
|
+
h = logging.FileHandler(output)
|
273
|
+
|
274
|
+
h.setFormatter(fmt)
|
275
|
+
h._plain_server = True # type: ignore[attr-defined] # custom attribute
|
276
|
+
log.addHandler(h)
|
277
|
+
|
278
|
+
def _get_user(self, environ: dict[str, Any]) -> str | None:
|
279
|
+
user = None
|
280
|
+
http_auth = environ.get("HTTP_AUTHORIZATION")
|
281
|
+
if http_auth and http_auth.lower().startswith("basic"):
|
282
|
+
auth = http_auth.split(" ", 1)
|
283
|
+
if len(auth) == 2:
|
284
|
+
try:
|
285
|
+
# b64decode doesn't accept unicode in Python < 3.3
|
286
|
+
# so we need to convert it to a byte string
|
287
|
+
auth = base64.b64decode(auth[1].strip().encode("utf-8"))
|
288
|
+
# b64decode returns a byte string
|
289
|
+
user = auth.split(b":", 1)[0].decode("UTF-8")
|
290
|
+
except (TypeError, binascii.Error, UnicodeDecodeError) as exc:
|
291
|
+
self.debug("Couldn't get username: %s", exc)
|
292
|
+
return user
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# This file is part of gunicorn released under the MIT license.
|
4
|
+
# See the LICENSE for more information.
|
5
|
+
#
|
6
|
+
# Vendored and modified for Plain.
|
7
|
+
|
8
|
+
from . import errors
|
9
|
+
from .message import Message, Request
|
10
|
+
from .parser import RequestParser
|
11
|
+
|
12
|
+
__all__ = ["Message", "Request", "RequestParser", "errors"]
|
@@ -0,0 +1,283 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# This file is part of gunicorn released under the MIT license.
|
6
|
+
# See the LICENSE for more information.
|
7
|
+
#
|
8
|
+
# Vendored and modified for Plain.
|
9
|
+
import io
|
10
|
+
import sys
|
11
|
+
from collections.abc import Generator, Iterator
|
12
|
+
from typing import TYPE_CHECKING
|
13
|
+
|
14
|
+
from .errors import ChunkMissingTerminator, InvalidChunkSize, NoMoreData
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from .message import Request
|
18
|
+
from .unreader import Unreader
|
19
|
+
|
20
|
+
|
21
|
+
class ChunkedReader:
|
22
|
+
def __init__(self, req: Request, unreader: Unreader) -> None:
|
23
|
+
self.req = req
|
24
|
+
self.parser: Generator[bytes, None, None] | None = self.parse_chunked(unreader)
|
25
|
+
self.buf = io.BytesIO()
|
26
|
+
|
27
|
+
def read(self, size: int) -> bytes:
|
28
|
+
if not isinstance(size, int):
|
29
|
+
raise TypeError("size must be an integer type")
|
30
|
+
if size < 0:
|
31
|
+
raise ValueError("Size must be positive.")
|
32
|
+
if size == 0:
|
33
|
+
return b""
|
34
|
+
|
35
|
+
if self.parser:
|
36
|
+
while self.buf.tell() < size:
|
37
|
+
try:
|
38
|
+
self.buf.write(next(self.parser))
|
39
|
+
except StopIteration:
|
40
|
+
self.parser = None
|
41
|
+
break
|
42
|
+
|
43
|
+
data = self.buf.getvalue()
|
44
|
+
ret, rest = data[:size], data[size:]
|
45
|
+
self.buf = io.BytesIO()
|
46
|
+
self.buf.write(rest)
|
47
|
+
return ret
|
48
|
+
|
49
|
+
def parse_trailers(self, unreader: Unreader, data: bytes) -> None:
|
50
|
+
buf = io.BytesIO()
|
51
|
+
buf.write(data)
|
52
|
+
|
53
|
+
idx = buf.getvalue().find(b"\r\n\r\n")
|
54
|
+
done = buf.getvalue()[:2] == b"\r\n"
|
55
|
+
while idx < 0 and not done:
|
56
|
+
self.get_data(unreader, buf)
|
57
|
+
idx = buf.getvalue().find(b"\r\n\r\n")
|
58
|
+
done = buf.getvalue()[:2] == b"\r\n"
|
59
|
+
if done:
|
60
|
+
unreader.unread(buf.getvalue()[2:])
|
61
|
+
return None
|
62
|
+
self.req.trailers = self.req.parse_headers(
|
63
|
+
buf.getvalue()[:idx], from_trailer=True
|
64
|
+
)
|
65
|
+
unreader.unread(buf.getvalue()[idx + 4 :])
|
66
|
+
return None
|
67
|
+
|
68
|
+
def parse_chunked(self, unreader: Unreader) -> Generator[bytes, None, None]:
|
69
|
+
(size, rest) = self.parse_chunk_size(unreader)
|
70
|
+
while size > 0:
|
71
|
+
while size > len(rest):
|
72
|
+
size -= len(rest)
|
73
|
+
yield rest
|
74
|
+
rest = unreader.read()
|
75
|
+
if not rest:
|
76
|
+
raise NoMoreData()
|
77
|
+
yield rest[:size]
|
78
|
+
# Remove \r\n after chunk
|
79
|
+
rest = rest[size:]
|
80
|
+
while len(rest) < 2:
|
81
|
+
new_data = unreader.read()
|
82
|
+
if not new_data:
|
83
|
+
break
|
84
|
+
rest += new_data
|
85
|
+
if rest[:2] != b"\r\n":
|
86
|
+
raise ChunkMissingTerminator(rest[:2])
|
87
|
+
(size, rest) = self.parse_chunk_size(unreader, data=rest[2:])
|
88
|
+
|
89
|
+
def parse_chunk_size(
|
90
|
+
self, unreader: Unreader, data: bytes | None = None
|
91
|
+
) -> tuple[int, bytes]:
|
92
|
+
buf = io.BytesIO()
|
93
|
+
if data is not None:
|
94
|
+
buf.write(data)
|
95
|
+
|
96
|
+
idx = buf.getvalue().find(b"\r\n")
|
97
|
+
while idx < 0:
|
98
|
+
self.get_data(unreader, buf)
|
99
|
+
idx = buf.getvalue().find(b"\r\n")
|
100
|
+
|
101
|
+
data = buf.getvalue()
|
102
|
+
line, rest_chunk = data[:idx], data[idx + 2 :]
|
103
|
+
|
104
|
+
# RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
|
105
|
+
chunk_size, *chunk_ext = line.split(b";", 1)
|
106
|
+
if chunk_ext:
|
107
|
+
chunk_size = chunk_size.rstrip(b" \t")
|
108
|
+
if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
|
109
|
+
raise InvalidChunkSize(chunk_size)
|
110
|
+
if len(chunk_size) == 0:
|
111
|
+
raise InvalidChunkSize(chunk_size)
|
112
|
+
chunk_size = int(chunk_size, 16)
|
113
|
+
|
114
|
+
if chunk_size == 0:
|
115
|
+
try:
|
116
|
+
self.parse_trailers(unreader, rest_chunk)
|
117
|
+
except NoMoreData:
|
118
|
+
pass
|
119
|
+
return (0, b"")
|
120
|
+
return (chunk_size, rest_chunk)
|
121
|
+
|
122
|
+
def get_data(self, unreader: Unreader, buf: io.BytesIO) -> None:
|
123
|
+
data = unreader.read()
|
124
|
+
if not data:
|
125
|
+
raise NoMoreData()
|
126
|
+
buf.write(data)
|
127
|
+
return None
|
128
|
+
|
129
|
+
|
130
|
+
class LengthReader:
|
131
|
+
def __init__(self, unreader: Unreader, length: int) -> None:
|
132
|
+
self.unreader = unreader
|
133
|
+
self.length = length
|
134
|
+
|
135
|
+
def read(self, size: int) -> bytes:
|
136
|
+
if not isinstance(size, int):
|
137
|
+
raise TypeError("size must be an integral type")
|
138
|
+
|
139
|
+
size = min(self.length, size)
|
140
|
+
if size < 0:
|
141
|
+
raise ValueError("Size must be positive.")
|
142
|
+
if size == 0:
|
143
|
+
return b""
|
144
|
+
|
145
|
+
buf = io.BytesIO()
|
146
|
+
data = self.unreader.read()
|
147
|
+
while data:
|
148
|
+
buf.write(data)
|
149
|
+
if buf.tell() >= size:
|
150
|
+
break
|
151
|
+
data = self.unreader.read()
|
152
|
+
|
153
|
+
buf_data = buf.getvalue()
|
154
|
+
ret, rest = buf_data[:size], buf_data[size:]
|
155
|
+
self.unreader.unread(rest)
|
156
|
+
self.length -= size
|
157
|
+
return ret
|
158
|
+
|
159
|
+
|
160
|
+
class EOFReader:
|
161
|
+
def __init__(self, unreader: Unreader) -> None:
|
162
|
+
self.unreader = unreader
|
163
|
+
self.buf = io.BytesIO()
|
164
|
+
self.finished = False
|
165
|
+
|
166
|
+
def read(self, size: int) -> bytes:
|
167
|
+
if not isinstance(size, int):
|
168
|
+
raise TypeError("size must be an integral type")
|
169
|
+
if size < 0:
|
170
|
+
raise ValueError("Size must be positive.")
|
171
|
+
if size == 0:
|
172
|
+
return b""
|
173
|
+
|
174
|
+
if self.finished:
|
175
|
+
data = self.buf.getvalue()
|
176
|
+
ret, rest = data[:size], data[size:]
|
177
|
+
self.buf = io.BytesIO()
|
178
|
+
self.buf.write(rest)
|
179
|
+
return ret
|
180
|
+
|
181
|
+
data = self.unreader.read()
|
182
|
+
while data:
|
183
|
+
self.buf.write(data)
|
184
|
+
if self.buf.tell() > size:
|
185
|
+
break
|
186
|
+
data = self.unreader.read()
|
187
|
+
|
188
|
+
if not data:
|
189
|
+
self.finished = True
|
190
|
+
|
191
|
+
data = self.buf.getvalue()
|
192
|
+
ret, rest = data[:size], data[size:]
|
193
|
+
self.buf = io.BytesIO()
|
194
|
+
self.buf.write(rest)
|
195
|
+
return ret
|
196
|
+
|
197
|
+
|
198
|
+
class Body:
|
199
|
+
def __init__(self, reader: ChunkedReader | LengthReader | EOFReader) -> None:
|
200
|
+
self.reader = reader
|
201
|
+
self.buf = io.BytesIO()
|
202
|
+
|
203
|
+
def __iter__(self) -> Iterator[bytes]:
|
204
|
+
return self
|
205
|
+
|
206
|
+
def __next__(self) -> bytes:
|
207
|
+
ret = self.readline()
|
208
|
+
if not ret:
|
209
|
+
raise StopIteration()
|
210
|
+
return ret
|
211
|
+
|
212
|
+
next = __next__
|
213
|
+
|
214
|
+
def getsize(self, size: int | None) -> int:
|
215
|
+
if size is None:
|
216
|
+
return sys.maxsize
|
217
|
+
elif not isinstance(size, int):
|
218
|
+
raise TypeError("size must be an integral type")
|
219
|
+
elif size < 0:
|
220
|
+
return sys.maxsize
|
221
|
+
return size
|
222
|
+
|
223
|
+
def read(self, size: int | None = None) -> bytes:
|
224
|
+
size = self.getsize(size)
|
225
|
+
if size == 0:
|
226
|
+
return b""
|
227
|
+
|
228
|
+
if size < self.buf.tell():
|
229
|
+
data = self.buf.getvalue()
|
230
|
+
ret, rest = data[:size], data[size:]
|
231
|
+
self.buf = io.BytesIO()
|
232
|
+
self.buf.write(rest)
|
233
|
+
return ret
|
234
|
+
|
235
|
+
while size > self.buf.tell():
|
236
|
+
data = self.reader.read(1024)
|
237
|
+
if not data:
|
238
|
+
break
|
239
|
+
self.buf.write(data)
|
240
|
+
|
241
|
+
data = self.buf.getvalue()
|
242
|
+
ret, rest = data[:size], data[size:]
|
243
|
+
self.buf = io.BytesIO()
|
244
|
+
self.buf.write(rest)
|
245
|
+
return ret
|
246
|
+
|
247
|
+
def readline(self, size: int | None = None) -> bytes:
|
248
|
+
size = self.getsize(size)
|
249
|
+
if size == 0:
|
250
|
+
return b""
|
251
|
+
|
252
|
+
data = self.buf.getvalue()
|
253
|
+
self.buf = io.BytesIO()
|
254
|
+
|
255
|
+
ret = []
|
256
|
+
while 1:
|
257
|
+
idx = data.find(b"\n", 0, size)
|
258
|
+
idx = idx + 1 if idx >= 0 else size if len(data) >= size else 0
|
259
|
+
if idx:
|
260
|
+
ret.append(data[:idx])
|
261
|
+
self.buf.write(data[idx:])
|
262
|
+
break
|
263
|
+
|
264
|
+
ret.append(data)
|
265
|
+
size -= len(data)
|
266
|
+
data = self.reader.read(min(1024, size))
|
267
|
+
if not data:
|
268
|
+
break
|
269
|
+
|
270
|
+
return b"".join(ret)
|
271
|
+
|
272
|
+
def readlines(self, size: int | None = None) -> list[bytes]:
|
273
|
+
ret = []
|
274
|
+
data = self.read()
|
275
|
+
while data:
|
276
|
+
pos = data.find(b"\n")
|
277
|
+
if pos < 0:
|
278
|
+
ret.append(data)
|
279
|
+
data = b""
|
280
|
+
else:
|
281
|
+
line, data = data[: pos + 1], data[pos + 1 :]
|
282
|
+
ret.append(line)
|
283
|
+
return ret
|