plain 0.75.0__py3-none-any.whl → 0.77.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.
Potentially problematic release.
This version of plain might be problematic. Click here for more details.
- plain/CHANGELOG.md +32 -0
- plain/cli/core.py +35 -17
- plain/cli/runtime.py +28 -0
- plain/cli/server.py +135 -0
- plain/internal/reloader.py +77 -0
- plain/server/LICENSE +35 -0
- plain/server/README.md +74 -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/sock.py +219 -0
- plain/server/util.py +313 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +302 -0
- plain/server/workers/sync.py +210 -0
- plain/server/workers/thread.py +393 -0
- plain/server/workers/workertmp.py +50 -0
- {plain-0.75.0.dist-info → plain-0.77.0.dist-info}/METADATA +2 -1
- {plain-0.75.0.dist-info → plain-0.77.0.dist-info}/RECORD +33 -7
- {plain-0.75.0.dist-info → plain-0.77.0.dist-info}/WHEEL +0 -0
- {plain-0.75.0.dist-info → plain-0.77.0.dist-info}/entry_points.txt +0 -0
- {plain-0.75.0.dist-info → plain-0.77.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
# design:
|
|
10
|
+
# A threaded worker accepts connections in the main loop, accepted
|
|
11
|
+
# connections are added to the thread pool as a connection job.
|
|
12
|
+
# Keepalive connections are put back in the loop waiting for an event.
|
|
13
|
+
# If no event happen after the keep alive timeout, the connection is
|
|
14
|
+
# closed.
|
|
15
|
+
# pylint: disable=no-else-break
|
|
16
|
+
import errno
|
|
17
|
+
import os
|
|
18
|
+
import selectors
|
|
19
|
+
import socket
|
|
20
|
+
import ssl
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from collections import deque
|
|
24
|
+
from concurrent import futures
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from functools import partial
|
|
27
|
+
from threading import RLock
|
|
28
|
+
from types import FrameType
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
from .. import http, sock, util
|
|
32
|
+
from ..http import wsgi
|
|
33
|
+
from . import base
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from ..config import Config
|
|
37
|
+
from ..glogging import Logger
|
|
38
|
+
|
|
39
|
+
# Keep-alive connection timeout in seconds
|
|
40
|
+
KEEPALIVE = 2
|
|
41
|
+
|
|
42
|
+
# Maximum number of simultaneous client connections
|
|
43
|
+
WORKER_CONNECTIONS = 1000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TConn:
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
cfg: Config,
|
|
50
|
+
sock: socket.socket,
|
|
51
|
+
client: tuple[str, int],
|
|
52
|
+
server: tuple[str, int],
|
|
53
|
+
) -> None:
|
|
54
|
+
self.cfg = cfg
|
|
55
|
+
self.sock = sock
|
|
56
|
+
self.client = client
|
|
57
|
+
self.server = server
|
|
58
|
+
|
|
59
|
+
self.timeout: float | None = None
|
|
60
|
+
self.parser: http.RequestParser | None = None
|
|
61
|
+
self.initialized: bool = False
|
|
62
|
+
|
|
63
|
+
# set the socket to non blocking
|
|
64
|
+
self.sock.setblocking(False)
|
|
65
|
+
|
|
66
|
+
def init(self) -> None:
|
|
67
|
+
self.initialized = True
|
|
68
|
+
self.sock.setblocking(True)
|
|
69
|
+
|
|
70
|
+
if self.parser is None:
|
|
71
|
+
# wrap the socket if needed
|
|
72
|
+
if self.cfg.is_ssl:
|
|
73
|
+
self.sock = sock.ssl_wrap_socket(self.sock, self.cfg)
|
|
74
|
+
|
|
75
|
+
# initialize the parser
|
|
76
|
+
self.parser = http.RequestParser(self.cfg, self.sock, self.client)
|
|
77
|
+
|
|
78
|
+
def set_timeout(self) -> None:
|
|
79
|
+
# set the timeout
|
|
80
|
+
self.timeout = time.time() + KEEPALIVE
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
util.close(self.sock)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ThreadWorker(base.Worker):
|
|
87
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
88
|
+
super().__init__(*args, **kwargs)
|
|
89
|
+
self.worker_connections: int = WORKER_CONNECTIONS
|
|
90
|
+
self.max_keepalived: int = WORKER_CONNECTIONS - self.cfg.threads
|
|
91
|
+
# initialise the pool
|
|
92
|
+
self.tpool: futures.ThreadPoolExecutor | None = None
|
|
93
|
+
self.poller: selectors.DefaultSelector | None = None
|
|
94
|
+
self._lock: RLock | None = None
|
|
95
|
+
self.futures: deque[futures.Future[tuple[bool, TConn]]] = deque()
|
|
96
|
+
self._keep: deque[TConn] = deque()
|
|
97
|
+
self.nr_conns: int = 0
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def check_config(cls, cfg: Config, log: Logger) -> None:
|
|
101
|
+
max_keepalived = WORKER_CONNECTIONS - cfg.threads
|
|
102
|
+
|
|
103
|
+
if max_keepalived <= 0:
|
|
104
|
+
log.warning(
|
|
105
|
+
"No keepalived connections can be handled. "
|
|
106
|
+
"Check the number of worker connections and threads."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def init_process(self) -> None:
|
|
110
|
+
self.tpool = self.get_thread_pool()
|
|
111
|
+
self.poller = selectors.DefaultSelector()
|
|
112
|
+
self._lock = RLock()
|
|
113
|
+
super().init_process()
|
|
114
|
+
|
|
115
|
+
def get_thread_pool(self) -> futures.ThreadPoolExecutor:
|
|
116
|
+
"""Override this method to customize how the thread pool is created"""
|
|
117
|
+
return futures.ThreadPoolExecutor(max_workers=self.cfg.threads)
|
|
118
|
+
|
|
119
|
+
def handle_quit(self, sig: int, frame: FrameType | None) -> None:
|
|
120
|
+
self.alive = False
|
|
121
|
+
self.tpool.shutdown(False)
|
|
122
|
+
time.sleep(0.1)
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
def _wrap_future(self, fs: futures.Future[tuple[bool, TConn]], conn: TConn) -> None:
|
|
126
|
+
fs.conn = conn # type: ignore[attr-defined]
|
|
127
|
+
self.futures.append(fs)
|
|
128
|
+
fs.add_done_callback(self.finish_request)
|
|
129
|
+
|
|
130
|
+
def enqueue_req(self, conn: TConn) -> None:
|
|
131
|
+
conn.init()
|
|
132
|
+
# submit the connection to a worker
|
|
133
|
+
fs = self.tpool.submit(self.handle, conn)
|
|
134
|
+
self._wrap_future(fs, conn)
|
|
135
|
+
|
|
136
|
+
def accept(self, server: tuple[str, int], listener: socket.socket) -> None:
|
|
137
|
+
try:
|
|
138
|
+
sock, client = listener.accept()
|
|
139
|
+
# initialize the connection object
|
|
140
|
+
conn = TConn(self.cfg, sock, client, server)
|
|
141
|
+
|
|
142
|
+
self.nr_conns += 1
|
|
143
|
+
# wait until socket is readable
|
|
144
|
+
with self._lock:
|
|
145
|
+
self.poller.register(
|
|
146
|
+
conn.sock,
|
|
147
|
+
selectors.EVENT_READ,
|
|
148
|
+
partial(self.on_client_socket_readable, conn),
|
|
149
|
+
)
|
|
150
|
+
except OSError as e:
|
|
151
|
+
if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, errno.EWOULDBLOCK):
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
def on_client_socket_readable(self, conn: TConn, client: socket.socket) -> None:
|
|
155
|
+
with self._lock:
|
|
156
|
+
# unregister the client from the poller
|
|
157
|
+
self.poller.unregister(client)
|
|
158
|
+
|
|
159
|
+
if conn.initialized:
|
|
160
|
+
# remove the connection from keepalive
|
|
161
|
+
try:
|
|
162
|
+
self._keep.remove(conn)
|
|
163
|
+
except ValueError:
|
|
164
|
+
# race condition
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# submit the connection to a worker
|
|
168
|
+
self.enqueue_req(conn)
|
|
169
|
+
|
|
170
|
+
def murder_keepalived(self) -> None:
|
|
171
|
+
now = time.time()
|
|
172
|
+
while True:
|
|
173
|
+
with self._lock:
|
|
174
|
+
try:
|
|
175
|
+
# remove the connection from the queue
|
|
176
|
+
conn = self._keep.popleft()
|
|
177
|
+
except IndexError:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
delta = conn.timeout - now
|
|
181
|
+
if delta > 0:
|
|
182
|
+
# add the connection back to the queue
|
|
183
|
+
with self._lock:
|
|
184
|
+
self._keep.appendleft(conn)
|
|
185
|
+
break
|
|
186
|
+
else:
|
|
187
|
+
self.nr_conns -= 1
|
|
188
|
+
# remove the socket from the poller
|
|
189
|
+
with self._lock:
|
|
190
|
+
try:
|
|
191
|
+
self.poller.unregister(conn.sock)
|
|
192
|
+
except OSError as e:
|
|
193
|
+
if e.errno != errno.EBADF:
|
|
194
|
+
raise
|
|
195
|
+
except KeyError:
|
|
196
|
+
# already removed by the system, continue
|
|
197
|
+
pass
|
|
198
|
+
except ValueError:
|
|
199
|
+
# already removed by the system continue
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# close the socket
|
|
203
|
+
conn.close()
|
|
204
|
+
|
|
205
|
+
def is_parent_alive(self) -> bool:
|
|
206
|
+
# If our parent changed then we shut down.
|
|
207
|
+
if self.ppid != os.getppid():
|
|
208
|
+
self.log.info("Parent changed, shutting down: %s", self)
|
|
209
|
+
return False
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
def run(self) -> None:
|
|
213
|
+
# init listeners, add them to the event loop
|
|
214
|
+
for listener in self.sockets:
|
|
215
|
+
listener.setblocking(False)
|
|
216
|
+
# a race condition during graceful shutdown may make the listener
|
|
217
|
+
# name unavailable in the request handler so capture it once here
|
|
218
|
+
server = listener.getsockname()
|
|
219
|
+
acceptor = partial(self.accept, server)
|
|
220
|
+
self.poller.register(listener, selectors.EVENT_READ, acceptor)
|
|
221
|
+
|
|
222
|
+
while self.alive:
|
|
223
|
+
# notify the arbiter we are alive
|
|
224
|
+
self.notify()
|
|
225
|
+
|
|
226
|
+
# can we accept more connections?
|
|
227
|
+
if self.nr_conns < self.worker_connections:
|
|
228
|
+
# wait for an event
|
|
229
|
+
events = self.poller.select(1.0)
|
|
230
|
+
for key, _ in events:
|
|
231
|
+
callback = key.data
|
|
232
|
+
callback(key.fileobj)
|
|
233
|
+
|
|
234
|
+
# check (but do not wait) for finished requests
|
|
235
|
+
result = futures.wait(
|
|
236
|
+
self.futures, timeout=0, return_when=futures.FIRST_COMPLETED
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
# wait for a request to finish
|
|
240
|
+
result = futures.wait(
|
|
241
|
+
self.futures, timeout=1.0, return_when=futures.FIRST_COMPLETED
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# clean up finished requests
|
|
245
|
+
for fut in result.done:
|
|
246
|
+
self.futures.remove(fut)
|
|
247
|
+
|
|
248
|
+
if not self.is_parent_alive():
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
# handle keepalive timeouts
|
|
252
|
+
self.murder_keepalived()
|
|
253
|
+
|
|
254
|
+
self.tpool.shutdown(False)
|
|
255
|
+
self.poller.close()
|
|
256
|
+
|
|
257
|
+
for s in self.sockets:
|
|
258
|
+
s.close()
|
|
259
|
+
|
|
260
|
+
futures.wait(self.futures, timeout=self.cfg.graceful_timeout)
|
|
261
|
+
|
|
262
|
+
def finish_request(self, fs: futures.Future[tuple[bool, TConn]]) -> None:
|
|
263
|
+
if fs.cancelled():
|
|
264
|
+
self.nr_conns -= 1
|
|
265
|
+
fs.conn.close() # type: ignore[attr-defined]
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
(keepalive, conn) = fs.result()
|
|
270
|
+
# if the connection should be kept alived add it
|
|
271
|
+
# to the eventloop and record it
|
|
272
|
+
if keepalive and self.alive:
|
|
273
|
+
# flag the socket as non blocked
|
|
274
|
+
conn.sock.setblocking(False)
|
|
275
|
+
|
|
276
|
+
# register the connection
|
|
277
|
+
conn.set_timeout()
|
|
278
|
+
with self._lock:
|
|
279
|
+
self._keep.append(conn)
|
|
280
|
+
|
|
281
|
+
# add the socket to the event loop
|
|
282
|
+
self.poller.register(
|
|
283
|
+
conn.sock,
|
|
284
|
+
selectors.EVENT_READ,
|
|
285
|
+
partial(self.on_client_socket_readable, conn),
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
self.nr_conns -= 1
|
|
289
|
+
conn.close()
|
|
290
|
+
except Exception:
|
|
291
|
+
# an exception happened, make sure to close the
|
|
292
|
+
# socket.
|
|
293
|
+
self.nr_conns -= 1
|
|
294
|
+
fs.conn.close() # type: ignore[attr-defined]
|
|
295
|
+
|
|
296
|
+
def handle(self, conn: TConn) -> tuple[bool, TConn]:
|
|
297
|
+
keepalive = False
|
|
298
|
+
req = None
|
|
299
|
+
try:
|
|
300
|
+
# conn.parser is guaranteed to be initialized by enqueue_req -> conn.init()
|
|
301
|
+
assert conn.parser is not None
|
|
302
|
+
req = next(conn.parser)
|
|
303
|
+
if not req:
|
|
304
|
+
return (False, conn)
|
|
305
|
+
|
|
306
|
+
# handle the request
|
|
307
|
+
keepalive = self.handle_request(req, conn)
|
|
308
|
+
if keepalive:
|
|
309
|
+
return (keepalive, conn)
|
|
310
|
+
except http.errors.NoMoreData as e:
|
|
311
|
+
self.log.debug("Ignored premature client disconnection. %s", e)
|
|
312
|
+
|
|
313
|
+
except StopIteration as e:
|
|
314
|
+
self.log.debug("Closing connection. %s", e)
|
|
315
|
+
except ssl.SSLError as e:
|
|
316
|
+
if e.args[0] == ssl.SSL_ERROR_EOF:
|
|
317
|
+
self.log.debug("ssl connection closed")
|
|
318
|
+
conn.sock.close()
|
|
319
|
+
else:
|
|
320
|
+
self.log.debug("Error processing SSL request.")
|
|
321
|
+
self.handle_error(req, conn.sock, conn.client, e)
|
|
322
|
+
|
|
323
|
+
except OSError as e:
|
|
324
|
+
if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):
|
|
325
|
+
self.log.exception("Socket error processing request.")
|
|
326
|
+
else:
|
|
327
|
+
if e.errno == errno.ECONNRESET:
|
|
328
|
+
self.log.debug("Ignoring connection reset")
|
|
329
|
+
elif e.errno == errno.ENOTCONN:
|
|
330
|
+
self.log.debug("Ignoring socket not connected")
|
|
331
|
+
else:
|
|
332
|
+
self.log.debug("Ignoring connection epipe")
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.handle_error(req, conn.sock, conn.client, e)
|
|
335
|
+
|
|
336
|
+
return (False, conn)
|
|
337
|
+
|
|
338
|
+
def handle_request(self, req: Any, conn: TConn) -> bool:
|
|
339
|
+
environ: dict[str, Any] = {}
|
|
340
|
+
resp: wsgi.Response | None = None
|
|
341
|
+
try:
|
|
342
|
+
request_start = datetime.now()
|
|
343
|
+
resp, environ = wsgi.create(
|
|
344
|
+
req, conn.sock, conn.client, conn.server, self.cfg
|
|
345
|
+
)
|
|
346
|
+
environ["wsgi.multithread"] = True
|
|
347
|
+
self.nr += 1
|
|
348
|
+
if self.nr >= self.max_requests:
|
|
349
|
+
if self.alive:
|
|
350
|
+
self.log.info("Autorestarting worker after current request.")
|
|
351
|
+
self.alive = False
|
|
352
|
+
resp.force_close()
|
|
353
|
+
|
|
354
|
+
if not self.alive:
|
|
355
|
+
resp.force_close()
|
|
356
|
+
elif len(self._keep) >= self.max_keepalived:
|
|
357
|
+
resp.force_close()
|
|
358
|
+
|
|
359
|
+
respiter = self.wsgi(environ, resp.start_response)
|
|
360
|
+
try:
|
|
361
|
+
if isinstance(respiter, environ["wsgi.file_wrapper"]):
|
|
362
|
+
resp.write_file(respiter)
|
|
363
|
+
else:
|
|
364
|
+
for item in respiter:
|
|
365
|
+
resp.write(item)
|
|
366
|
+
|
|
367
|
+
resp.close()
|
|
368
|
+
finally:
|
|
369
|
+
request_time = datetime.now() - request_start
|
|
370
|
+
self.log.access(resp, req, environ, request_time)
|
|
371
|
+
if hasattr(respiter, "close"):
|
|
372
|
+
respiter.close()
|
|
373
|
+
|
|
374
|
+
if resp.should_close():
|
|
375
|
+
self.log.debug("Closing connection.")
|
|
376
|
+
return False
|
|
377
|
+
except OSError:
|
|
378
|
+
# pass to next try-except level
|
|
379
|
+
util.reraise(*sys.exc_info())
|
|
380
|
+
except Exception:
|
|
381
|
+
if resp and resp.headers_sent:
|
|
382
|
+
# If the requests have already been sent, we should close the
|
|
383
|
+
# connection to indicate the error.
|
|
384
|
+
self.log.exception("Error handling request")
|
|
385
|
+
try:
|
|
386
|
+
conn.sock.shutdown(socket.SHUT_RDWR)
|
|
387
|
+
conn.sock.close()
|
|
388
|
+
except OSError:
|
|
389
|
+
pass
|
|
390
|
+
raise StopIteration()
|
|
391
|
+
raise
|
|
392
|
+
|
|
393
|
+
return True
|
|
@@ -0,0 +1,50 @@
|
|
|
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 os
|
|
10
|
+
import platform
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from .. import util
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..config import Config
|
|
19
|
+
|
|
20
|
+
PLATFORM = platform.system()
|
|
21
|
+
IS_CYGWIN = PLATFORM.startswith("CYGWIN")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkerTmp:
|
|
25
|
+
def __init__(self, cfg: Config) -> None:
|
|
26
|
+
fd, name = tempfile.mkstemp(prefix="wplain-")
|
|
27
|
+
|
|
28
|
+
# unlink the file so we don't leak temporary files
|
|
29
|
+
try:
|
|
30
|
+
if not IS_CYGWIN:
|
|
31
|
+
util.unlink(name)
|
|
32
|
+
# In Python 3.8, open() emits RuntimeWarning if buffering=1 for binary mode.
|
|
33
|
+
# Because we never write to this file, pass 0 to switch buffering off.
|
|
34
|
+
self._tmp = os.fdopen(fd, "w+b", 0)
|
|
35
|
+
except Exception:
|
|
36
|
+
os.close(fd)
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
def notify(self) -> None:
|
|
40
|
+
new_time = time.monotonic()
|
|
41
|
+
os.utime(self._tmp.fileno(), (new_time, new_time))
|
|
42
|
+
|
|
43
|
+
def last_update(self) -> float:
|
|
44
|
+
return os.fstat(self._tmp.fileno()).st_mtime
|
|
45
|
+
|
|
46
|
+
def fileno(self) -> int:
|
|
47
|
+
return self._tmp.fileno()
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
return self._tmp.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.77.0
|
|
4
4
|
Summary: A web framework for building products with Python.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,6 +9,7 @@ Requires-Dist: click>=8.0.0
|
|
|
9
9
|
Requires-Dist: jinja2>=3.1.2
|
|
10
10
|
Requires-Dist: opentelemetry-api>=1.34.1
|
|
11
11
|
Requires-Dist: opentelemetry-semantic-conventions>=0.55b1
|
|
12
|
+
Requires-Dist: watchfiles>=0.18.0
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
13
14
|
|
|
14
15
|
# Plain
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
|
|
2
|
-
plain/CHANGELOG.md,sha256=
|
|
2
|
+
plain/CHANGELOG.md,sha256=A05-ohzcEeeRrrxOXNt1FLH8F88kgS-_IjmwsHR1g9Y,26688
|
|
3
3
|
plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
|
|
4
4
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
|
5
5
|
plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
|
|
@@ -24,7 +24,7 @@ plain/cli/__init__.py,sha256=6w9T7K2WrPwh6DcaMb2oNt_CWU6Bc57nUTO2Bt1p38Y,63
|
|
|
24
24
|
plain/cli/build.py,sha256=Jg5LMbmXHhCXZIYj_Gcjou3yGiEWw8wpbOGGsdk-wZw,3203
|
|
25
25
|
plain/cli/changelog.py,sha256=yCY887PT_D2viLz9f-uyu07Znqiv2-NEyCBqquNIukw,3590
|
|
26
26
|
plain/cli/chores.py,sha256=aaDVTpwBEmyQ4r_YeZ6U7Fw9UOIq5d7okwpq9HdfRbA,2521
|
|
27
|
-
plain/cli/core.py,sha256=
|
|
27
|
+
plain/cli/core.py,sha256=nL-a7zPtEBIa_XV-VOd9lqEUqmQAhlzsdHNwEmxNkWE,4285
|
|
28
28
|
plain/cli/docs.py,sha256=PU3v7Z7qgYFG-bClpuDg4JeWwC8uvLYX3ovkQDMseVs,1146
|
|
29
29
|
plain/cli/formatting.py,sha256=e1doTFalAM11bD_Cvqeu6sTap81JrQcB-4kMjZzAHmY,2737
|
|
30
30
|
plain/cli/install.py,sha256=mffSYBmSJSj44OPBfu53nBQoyoz4jk69DvppubIB0mU,2791
|
|
@@ -32,7 +32,9 @@ plain/cli/output.py,sha256=uZTHZR-Axeoi2r6fgcDCpDA7iQSRrktBtTf1yBT5djI,1426
|
|
|
32
32
|
plain/cli/preflight.py,sha256=5UXOowjiCMqsGIrpHg60f6Ptjk40rMiDSowN8wy5jSY,8541
|
|
33
33
|
plain/cli/print.py,sha256=7kv9ddXpwOHRSWp6FFLfX4wbmhV7neoOBlE0VcXWccw,238
|
|
34
34
|
plain/cli/registry.py,sha256=Z52nVE2bC2h_B_SENnXctn3mx3UWB0qYg969DVP7XX8,1106
|
|
35
|
+
plain/cli/runtime.py,sha256=YbGYfwkH0VxfuIMbOCwM9wSWiQKusPn_gVeGod8OFaE,743
|
|
35
36
|
plain/cli/scaffold.py,sha256=AMAVnTYySgR5nz4sVp3mn0gEGfTKE1N8ZlrVrg2SrFU,1364
|
|
37
|
+
plain/cli/server.py,sha256=ngUMtxB90t5FnG7HQwOIkN-pwY6ltKPZZ4PLSqN2Y3E,3001
|
|
36
38
|
plain/cli/settings.py,sha256=kafbcPzy8khdtzLRyOHRl215va3E7U_h5USOA39UA3k,2008
|
|
37
39
|
plain/cli/shell.py,sha256=urTp24D4UsKmYi9nT7OOdlT4WhXjkpFVrGYfNNVsXEE,1980
|
|
38
40
|
plain/cli/startup.py,sha256=1nxXQucDkBxXriEN4wI2tiwG96PBNFndVrOyfzvJFdI,1061
|
|
@@ -61,6 +63,7 @@ plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs
|
|
|
61
63
|
plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
|
|
62
64
|
plain/http/response.py,sha256=efAJ2M_uwK8EYMXchOk-b0Jrx3Hukch_rPOW9nG5AV8,24842
|
|
63
65
|
plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
|
|
66
|
+
plain/internal/reloader.py,sha256=n7B-F-WeUXp37pAnvzKX9tcEbUxHSlYqa4gItyA_zko,2662
|
|
64
67
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
|
65
68
|
plain/internal/files/base.py,sha256=TiUIAqBSQCslgAmf5vjrwjbCe2px5Pt0wWLlGc66jXw,4683
|
|
66
69
|
plain/internal/files/locks.py,sha256=jvLL9kroOo50kUo8dbuajDiFvgSL5NH6x5hudRPPjiQ,4022
|
|
@@ -102,6 +105,29 @@ plain/runtime/__init__.py,sha256=dvF5ipRckVf6LQgY4kdxE_dTlCdncuawQf3o5EqLq9k,252
|
|
|
102
105
|
plain/runtime/global_settings.py,sha256=Q-bQP3tNnnuJZvfevGai639RIF_jhd7Dszt-DzTTz68,5509
|
|
103
106
|
plain/runtime/user_settings.py,sha256=Tbd0J6bxp18tKmFsQcdlxeFhUQU68PYtsswzQ2IcfNc,11467
|
|
104
107
|
plain/runtime/utils.py,sha256=sHOv9SWCalBtg32GtZofimM2XaQtf_jAyyf6RQuOlGc,851
|
|
108
|
+
plain/server/LICENSE,sha256=Xt_dw4qYwQI9qSi2u8yMZeb4HuMRp5tESRKhtvvJBgA,1707
|
|
109
|
+
plain/server/README.md,sha256=6jXQeZJVDt6Jn-Ff8Q7cZ1Xsh6fYPFfC-rtQ7zSYzag,2661
|
|
110
|
+
plain/server/__init__.py,sha256=DtRgEcr4IxF4mrtCHloIprk_Q4k1oju4F2VHoyvu4ow,212
|
|
111
|
+
plain/server/app.py,sha256=ozaqdb-a_T3ps7T5EJwIPM63F_497J4o7kw9Pbq7Ga0,1229
|
|
112
|
+
plain/server/arbiter.py,sha256=89_4CZn6v0kmfF3-h6y5Ax59Tm9vhYiZw7psuoHMe_A,17404
|
|
113
|
+
plain/server/config.py,sha256=-T1w8dbUwwLd898P1HNufe8Kw8VJDYJaeKY-UuKzvTo,3221
|
|
114
|
+
plain/server/errors.py,sha256=sKl_OJ5Uw-a_r_dZ2o4I8JaKeTrjvY_LR12F6B_p4-g,956
|
|
115
|
+
plain/server/glogging.py,sha256=Ab49Btbr9UvGRgBk8KGVrzsJaFKN1uD0ZurmIC0GYFY,9692
|
|
116
|
+
plain/server/pidfile.py,sha256=8Fcl9u7gvUJjY5z01qGAeRsi13_jAM8CRdeyqL3h2i0,2538
|
|
117
|
+
plain/server/sock.py,sha256=NFKtlrMstOT3xU2yKI6BLAzv_WE7VEGt3dHDkFPnPSo,6192
|
|
118
|
+
plain/server/util.py,sha256=vlTzH4jk8s-ZxJt0oAz-LaQZq_8lEk86zMDVClluuwY,8758
|
|
119
|
+
plain/server/http/__init__.py,sha256=kQwTk1l3hYJwVrzr1p-XNAbWYe0icsD7l0ZyGRXMbOI,300
|
|
120
|
+
plain/server/http/body.py,sha256=cz18F4C_gm9h5XvsStV0ncanBjZO1-pegbmbmiMpsQA,8352
|
|
121
|
+
plain/server/http/errors.py,sha256=uqrzOzjjdqXfFim64pc58cxPlbsoxRNiLx0WzIJSzkw,3719
|
|
122
|
+
plain/server/http/message.py,sha256=5UdOA7CMqiXW_xy0c3d_rPHOvj5YQQHhbkx4Hf3Ttbc,15082
|
|
123
|
+
plain/server/http/parser.py,sha256=TLcqdRS9_008n8MpgLORmjomyZKLuKNzdKA8Cej8mII,1681
|
|
124
|
+
plain/server/http/unreader.py,sha256=jD2PGZ574FGmQOZlqWfs1VWzp-ttfIso_GzYaId6KYQ,2238
|
|
125
|
+
plain/server/http/wsgi.py,sha256=D-wVKdgKppDR_ZQd834yXtju60t1Jg5bFc5QEr7Iz38,13897
|
|
126
|
+
plain/server/workers/__init__.py,sha256=sLq8nrIIf9Wjw5_qQsh6cnHnY7eIqCpzSedKrNmmn7s,145
|
|
127
|
+
plain/server/workers/base.py,sha256=B0aniofq4lT5gYa82W34VTLvEQInOe43QvuG9X14vdE,9602
|
|
128
|
+
plain/server/workers/sync.py,sha256=I28Icl1aKNIOlVaM76UOqQvjtSetkG-tdc5eiAvAmYA,7272
|
|
129
|
+
plain/server/workers/thread.py,sha256=6F_YfhrlPV3hmxwI8_-jgGq4pCmkQBKVO15Wrg-iQ7Y,13504
|
|
130
|
+
plain/server/workers/workertmp.py,sha256=egGReVvldlOBQfQGcpLpjt0zvPwR4C_N-UJKG-U_6w4,1299
|
|
105
131
|
plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
|
|
106
132
|
plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
|
|
107
133
|
plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
|
|
@@ -162,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
|
|
162
188
|
plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
|
|
163
189
|
plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
|
|
164
190
|
plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
|
|
165
|
-
plain-0.
|
|
166
|
-
plain-0.
|
|
167
|
-
plain-0.
|
|
168
|
-
plain-0.
|
|
169
|
-
plain-0.
|
|
191
|
+
plain-0.77.0.dist-info/METADATA,sha256=EXhUsvn36qZfmmMwZ42LlEhEeKcH0k5YXo2WmoRCXss,4516
|
|
192
|
+
plain-0.77.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
193
|
+
plain-0.77.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
|
|
194
|
+
plain-0.77.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
|
195
|
+
plain-0.77.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|