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.

@@ -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.75.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=PJbWcLkRQVfMtpmeZ_Ga6C4eo-0AkB8qIheC7vRRMgQ,23956
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=lUy6QljeqpdUdQWri5MASht8MG95IvlxMS4jySQGFGQ,3241
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.75.0.dist-info/METADATA,sha256=stPlpTtFhA_NaICFaJYV22Pa4Rd2uIbZjhtocI6KCkU,4482
166
- plain-0.75.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
167
- plain-0.75.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
168
- plain-0.75.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
169
- plain-0.75.0.dist-info/RECORD,,
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