plain 0.68.0__py3-none-any.whl → 0.101.2__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 +656 -1
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -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 +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
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
|
+
self.futures: deque[futures.Future[tuple[bool, TConn]]] = deque()
|
|
92
|
+
self._keep: deque[TConn] = deque()
|
|
93
|
+
self.nr_conns: int = 0
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def check_config(cls, cfg: Config, log: Logger) -> None:
|
|
97
|
+
max_keepalived = WORKER_CONNECTIONS - cfg.threads
|
|
98
|
+
|
|
99
|
+
if max_keepalived <= 0:
|
|
100
|
+
log.warning(
|
|
101
|
+
"No keepalived connections can be handled. "
|
|
102
|
+
"Check the number of worker connections and threads."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def init_process(self) -> None:
|
|
106
|
+
# These are always initialized before any worker methods are called
|
|
107
|
+
self.tpool: futures.ThreadPoolExecutor = self.get_thread_pool()
|
|
108
|
+
self.poller: selectors.DefaultSelector = selectors.DefaultSelector()
|
|
109
|
+
self._lock: RLock = RLock()
|
|
110
|
+
super().init_process()
|
|
111
|
+
|
|
112
|
+
def get_thread_pool(self) -> futures.ThreadPoolExecutor:
|
|
113
|
+
"""Override this method to customize how the thread pool is created"""
|
|
114
|
+
return futures.ThreadPoolExecutor(max_workers=self.cfg.threads)
|
|
115
|
+
|
|
116
|
+
def handle_quit(self, sig: int, frame: FrameType | None) -> None:
|
|
117
|
+
self.alive = False
|
|
118
|
+
self.tpool.shutdown(wait=False, cancel_futures=True)
|
|
119
|
+
time.sleep(0.1)
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
def handle_abort(self, sig: int, frame: FrameType | None) -> None:
|
|
123
|
+
self.alive = False
|
|
124
|
+
self.tpool.shutdown(wait=False, cancel_futures=True)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
def _wrap_future(self, fs: futures.Future[tuple[bool, TConn]], conn: TConn) -> None:
|
|
128
|
+
fs.conn = conn # type: ignore[attr-defined]
|
|
129
|
+
self.futures.append(fs)
|
|
130
|
+
fs.add_done_callback(self.finish_request)
|
|
131
|
+
|
|
132
|
+
def enqueue_req(self, conn: TConn) -> None:
|
|
133
|
+
conn.init()
|
|
134
|
+
# submit the connection to a worker
|
|
135
|
+
fs = self.tpool.submit(self.handle, conn)
|
|
136
|
+
self._wrap_future(fs, conn)
|
|
137
|
+
|
|
138
|
+
def accept(self, server: tuple[str, int], listener: socket.socket) -> None:
|
|
139
|
+
try:
|
|
140
|
+
sock, client = listener.accept()
|
|
141
|
+
# initialize the connection object
|
|
142
|
+
conn = TConn(self.cfg, sock, client, server)
|
|
143
|
+
|
|
144
|
+
self.nr_conns += 1
|
|
145
|
+
# wait until socket is readable
|
|
146
|
+
with self._lock:
|
|
147
|
+
self.poller.register(
|
|
148
|
+
conn.sock,
|
|
149
|
+
selectors.EVENT_READ,
|
|
150
|
+
partial(self.on_client_socket_readable, conn),
|
|
151
|
+
)
|
|
152
|
+
except OSError as e:
|
|
153
|
+
if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, errno.EWOULDBLOCK):
|
|
154
|
+
raise
|
|
155
|
+
|
|
156
|
+
def on_client_socket_readable(self, conn: TConn, client: socket.socket) -> None:
|
|
157
|
+
with self._lock:
|
|
158
|
+
# unregister the client from the poller
|
|
159
|
+
self.poller.unregister(client)
|
|
160
|
+
|
|
161
|
+
if conn.initialized:
|
|
162
|
+
# remove the connection from keepalive
|
|
163
|
+
try:
|
|
164
|
+
self._keep.remove(conn)
|
|
165
|
+
except ValueError:
|
|
166
|
+
# race condition
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# submit the connection to a worker
|
|
170
|
+
self.enqueue_req(conn)
|
|
171
|
+
|
|
172
|
+
def murder_keepalived(self) -> None:
|
|
173
|
+
now = time.time()
|
|
174
|
+
while True:
|
|
175
|
+
with self._lock:
|
|
176
|
+
try:
|
|
177
|
+
# remove the connection from the queue
|
|
178
|
+
conn = self._keep.popleft()
|
|
179
|
+
except IndexError:
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
# Connections in _keep always have timeout set via set_timeout()
|
|
183
|
+
assert conn.timeout is not None, (
|
|
184
|
+
"timeout should be set for keepalive connections"
|
|
185
|
+
)
|
|
186
|
+
delta = conn.timeout - now
|
|
187
|
+
if delta > 0:
|
|
188
|
+
# add the connection back to the queue
|
|
189
|
+
with self._lock:
|
|
190
|
+
self._keep.appendleft(conn)
|
|
191
|
+
break
|
|
192
|
+
else:
|
|
193
|
+
self.nr_conns -= 1
|
|
194
|
+
# remove the socket from the poller
|
|
195
|
+
with self._lock:
|
|
196
|
+
try:
|
|
197
|
+
self.poller.unregister(conn.sock)
|
|
198
|
+
except OSError as e:
|
|
199
|
+
if e.errno != errno.EBADF:
|
|
200
|
+
raise
|
|
201
|
+
except KeyError:
|
|
202
|
+
# already removed by the system, continue
|
|
203
|
+
pass
|
|
204
|
+
except ValueError:
|
|
205
|
+
# already removed by the system continue
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# close the socket
|
|
209
|
+
conn.close()
|
|
210
|
+
|
|
211
|
+
def is_parent_alive(self) -> bool:
|
|
212
|
+
# If our parent changed then we shut down.
|
|
213
|
+
if self.ppid != os.getppid():
|
|
214
|
+
self.log.info("Parent changed, shutting down: %s", self)
|
|
215
|
+
return False
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
def run(self) -> None:
|
|
219
|
+
# init listeners, add them to the event loop
|
|
220
|
+
for listener in self.sockets:
|
|
221
|
+
listener.setblocking(False)
|
|
222
|
+
# a race condition during graceful shutdown may make the listener
|
|
223
|
+
# name unavailable in the request handler so capture it once here
|
|
224
|
+
server = listener.getsockname()
|
|
225
|
+
acceptor = partial(self.accept, server)
|
|
226
|
+
self.poller.register(listener, selectors.EVENT_READ, acceptor)
|
|
227
|
+
|
|
228
|
+
while self.alive:
|
|
229
|
+
# notify the arbiter we are alive
|
|
230
|
+
self.notify()
|
|
231
|
+
|
|
232
|
+
# can we accept more connections?
|
|
233
|
+
if self.nr_conns < self.worker_connections:
|
|
234
|
+
# wait for an event
|
|
235
|
+
events = self.poller.select(1.0)
|
|
236
|
+
for key, _ in events:
|
|
237
|
+
callback = key.data
|
|
238
|
+
callback(key.fileobj)
|
|
239
|
+
|
|
240
|
+
# check (but do not wait) for finished requests
|
|
241
|
+
result = futures.wait(
|
|
242
|
+
self.futures, timeout=0, return_when=futures.FIRST_COMPLETED
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
# wait for a request to finish
|
|
246
|
+
result = futures.wait(
|
|
247
|
+
self.futures, timeout=1.0, return_when=futures.FIRST_COMPLETED
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# clean up finished requests
|
|
251
|
+
for fut in result.done:
|
|
252
|
+
self.futures.remove(fut)
|
|
253
|
+
|
|
254
|
+
if not self.is_parent_alive():
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# handle keepalive timeouts
|
|
258
|
+
self.murder_keepalived()
|
|
259
|
+
|
|
260
|
+
self.tpool.shutdown(False)
|
|
261
|
+
self.poller.close()
|
|
262
|
+
|
|
263
|
+
for s in self.sockets:
|
|
264
|
+
s.close()
|
|
265
|
+
|
|
266
|
+
futures.wait(self.futures, timeout=self.cfg.graceful_timeout)
|
|
267
|
+
|
|
268
|
+
def finish_request(self, fs: futures.Future[tuple[bool, TConn]]) -> None:
|
|
269
|
+
if fs.cancelled():
|
|
270
|
+
self.nr_conns -= 1
|
|
271
|
+
fs.conn.close() # type: ignore[attr-defined]
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
(keepalive, conn) = fs.result()
|
|
276
|
+
# if the connection should be kept alived add it
|
|
277
|
+
# to the eventloop and record it
|
|
278
|
+
if keepalive and self.alive:
|
|
279
|
+
# flag the socket as non blocked
|
|
280
|
+
conn.sock.setblocking(False)
|
|
281
|
+
|
|
282
|
+
# register the connection
|
|
283
|
+
conn.set_timeout()
|
|
284
|
+
with self._lock:
|
|
285
|
+
self._keep.append(conn)
|
|
286
|
+
|
|
287
|
+
# add the socket to the event loop
|
|
288
|
+
self.poller.register(
|
|
289
|
+
conn.sock,
|
|
290
|
+
selectors.EVENT_READ,
|
|
291
|
+
partial(self.on_client_socket_readable, conn),
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
self.nr_conns -= 1
|
|
295
|
+
conn.close()
|
|
296
|
+
except Exception:
|
|
297
|
+
# an exception happened, make sure to close the
|
|
298
|
+
# socket.
|
|
299
|
+
self.nr_conns -= 1
|
|
300
|
+
fs.conn.close() # type: ignore[attr-defined]
|
|
301
|
+
|
|
302
|
+
def handle(self, conn: TConn) -> tuple[bool, TConn]:
|
|
303
|
+
keepalive = False
|
|
304
|
+
req = None
|
|
305
|
+
try:
|
|
306
|
+
# conn.parser is guaranteed to be initialized by enqueue_req -> conn.init()
|
|
307
|
+
assert conn.parser is not None
|
|
308
|
+
req = next(conn.parser)
|
|
309
|
+
if not req:
|
|
310
|
+
return (False, conn)
|
|
311
|
+
|
|
312
|
+
# handle the request
|
|
313
|
+
keepalive = self.handle_request(req, conn)
|
|
314
|
+
if keepalive:
|
|
315
|
+
return (keepalive, conn)
|
|
316
|
+
except http.errors.NoMoreData as e:
|
|
317
|
+
self.log.debug("Ignored premature client disconnection. %s", e)
|
|
318
|
+
|
|
319
|
+
except StopIteration as e:
|
|
320
|
+
self.log.debug("Closing connection. %s", e)
|
|
321
|
+
except ssl.SSLError as e:
|
|
322
|
+
if e.args[0] == ssl.SSL_ERROR_EOF:
|
|
323
|
+
self.log.debug("ssl connection closed")
|
|
324
|
+
conn.sock.close()
|
|
325
|
+
else:
|
|
326
|
+
self.log.debug("Error processing SSL request.")
|
|
327
|
+
self.handle_error(req, conn.sock, conn.client, e)
|
|
328
|
+
|
|
329
|
+
except OSError as e:
|
|
330
|
+
if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):
|
|
331
|
+
self.log.exception("Socket error processing request.")
|
|
332
|
+
else:
|
|
333
|
+
if e.errno == errno.ECONNRESET:
|
|
334
|
+
self.log.debug("Ignoring connection reset")
|
|
335
|
+
elif e.errno == errno.ENOTCONN:
|
|
336
|
+
self.log.debug("Ignoring socket not connected")
|
|
337
|
+
else:
|
|
338
|
+
self.log.debug("Ignoring connection epipe")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.handle_error(req, conn.sock, conn.client, e)
|
|
341
|
+
|
|
342
|
+
return (False, conn)
|
|
343
|
+
|
|
344
|
+
def handle_request(self, req: Any, conn: TConn) -> bool:
|
|
345
|
+
environ: dict[str, Any] = {}
|
|
346
|
+
resp: wsgi.Response | None = None
|
|
347
|
+
try:
|
|
348
|
+
request_start = datetime.now()
|
|
349
|
+
resp, environ = wsgi.create(
|
|
350
|
+
req, conn.sock, conn.client, conn.server, self.cfg
|
|
351
|
+
)
|
|
352
|
+
environ["wsgi.multithread"] = True
|
|
353
|
+
self.nr += 1
|
|
354
|
+
if self.nr >= self.max_requests:
|
|
355
|
+
if self.alive:
|
|
356
|
+
self.log.info("Autorestarting worker after current request.")
|
|
357
|
+
self.alive = False
|
|
358
|
+
resp.force_close()
|
|
359
|
+
|
|
360
|
+
if not self.alive:
|
|
361
|
+
resp.force_close()
|
|
362
|
+
elif len(self._keep) >= self.max_keepalived:
|
|
363
|
+
resp.force_close()
|
|
364
|
+
|
|
365
|
+
respiter = self.wsgi(environ, resp.start_response)
|
|
366
|
+
try:
|
|
367
|
+
if isinstance(respiter, environ["wsgi.file_wrapper"]):
|
|
368
|
+
resp.write_file(respiter)
|
|
369
|
+
else:
|
|
370
|
+
for item in respiter:
|
|
371
|
+
resp.write(item)
|
|
372
|
+
|
|
373
|
+
resp.close()
|
|
374
|
+
finally:
|
|
375
|
+
request_time = datetime.now() - request_start
|
|
376
|
+
self.log.access(resp, req, environ, request_time)
|
|
377
|
+
if hasattr(respiter, "close"):
|
|
378
|
+
respiter.close()
|
|
379
|
+
|
|
380
|
+
if resp.should_close():
|
|
381
|
+
self.log.debug("Closing connection.")
|
|
382
|
+
return False
|
|
383
|
+
except OSError:
|
|
384
|
+
# pass to next try-except level
|
|
385
|
+
util.reraise(*sys.exc_info())
|
|
386
|
+
except Exception:
|
|
387
|
+
if resp and resp.headers_sent:
|
|
388
|
+
# If the requests have already been sent, we should close the
|
|
389
|
+
# connection to indicate the error.
|
|
390
|
+
self.log.exception("Error handling request")
|
|
391
|
+
try:
|
|
392
|
+
conn.sock.shutdown(socket.SHUT_RDWR)
|
|
393
|
+
conn.sock.close()
|
|
394
|
+
except OSError:
|
|
395
|
+
pass
|
|
396
|
+
raise StopIteration()
|
|
397
|
+
raise
|
|
398
|
+
|
|
399
|
+
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()
|
plain/signals/README.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# Signals
|
|
2
2
|
|
|
3
|
-
**Run code when certain events happen.**
|
|
3
|
+
**Run code when certain events happen in your application.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
|
+
- [Using the receiver decorator](#using-the-receiver-decorator)
|
|
7
|
+
- [Creating custom signals](#creating-custom-signals)
|
|
8
|
+
- [Filtering by sender](#filtering-by-sender)
|
|
9
|
+
- [Signal methods](#signal-methods)
|
|
10
|
+
- [send](#send)
|
|
11
|
+
- [send_robust](#send_robust)
|
|
12
|
+
- [disconnect](#disconnect)
|
|
13
|
+
- [has_listeners](#has_listeners)
|
|
14
|
+
- [FAQs](#faqs)
|
|
15
|
+
- [Installation](#installation)
|
|
6
16
|
|
|
7
17
|
## Overview
|
|
8
18
|
|
|
19
|
+
Signals let you decouple parts of your application by allowing certain senders to notify receivers when specific events occur. You connect a receiver function to a signal, and it gets called whenever that signal is sent.
|
|
20
|
+
|
|
9
21
|
```python
|
|
10
22
|
from plain.signals import request_finished
|
|
11
23
|
|
|
@@ -16,3 +28,160 @@ def on_request_finished(sender, **kwargs):
|
|
|
16
28
|
|
|
17
29
|
request_finished.connect(on_request_finished)
|
|
18
30
|
```
|
|
31
|
+
|
|
32
|
+
Plain provides two built-in signals:
|
|
33
|
+
|
|
34
|
+
- `request_started` - sent when a request begins processing
|
|
35
|
+
- `request_finished` - sent when a request finishes processing
|
|
36
|
+
|
|
37
|
+
Your receiver function must accept `**kwargs` because signals may pass additional arguments in the future.
|
|
38
|
+
|
|
39
|
+
## Using the receiver decorator
|
|
40
|
+
|
|
41
|
+
Instead of calling `.connect()` manually, you can use the `@receiver` decorator to connect a function to a signal.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from plain.signals import request_finished
|
|
45
|
+
from plain.signals.dispatch import receiver
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@receiver(request_finished)
|
|
49
|
+
def on_request_finished(sender, **kwargs):
|
|
50
|
+
print("Request finished!")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also connect to multiple signals at once by passing a list.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from plain.signals import request_started, request_finished
|
|
57
|
+
from plain.signals.dispatch import receiver
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@receiver([request_started, request_finished])
|
|
61
|
+
def on_request_event(sender, **kwargs):
|
|
62
|
+
print("Request event occurred!")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Creating custom signals
|
|
66
|
+
|
|
67
|
+
You can define your own signals for custom events in your application.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from plain.signals.dispatch import Signal
|
|
71
|
+
|
|
72
|
+
# Define a custom signal
|
|
73
|
+
order_placed = Signal()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Connect a receiver
|
|
77
|
+
@receiver(order_placed)
|
|
78
|
+
def send_order_confirmation(sender, order, **kwargs):
|
|
79
|
+
print(f"Order {order.id} placed, sending confirmation email...")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Send the signal from your code
|
|
83
|
+
def create_order(data):
|
|
84
|
+
order = Order.objects.create(**data)
|
|
85
|
+
order_placed.send(sender=Order, order=order)
|
|
86
|
+
return order
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Filtering by sender
|
|
90
|
+
|
|
91
|
+
You can connect a receiver to only respond to signals from a specific sender.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from plain.signals.dispatch import Signal, receiver
|
|
95
|
+
|
|
96
|
+
payment_received = Signal()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@receiver(payment_received, sender="stripe")
|
|
100
|
+
def handle_stripe_payment(sender, **kwargs):
|
|
101
|
+
# Only called when sender="stripe"
|
|
102
|
+
print("Stripe payment received!")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# This will trigger the receiver
|
|
106
|
+
payment_received.send(sender="stripe", amount=100)
|
|
107
|
+
|
|
108
|
+
# This will NOT trigger the receiver
|
|
109
|
+
payment_received.send(sender="paypal", amount=100)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Signal methods
|
|
113
|
+
|
|
114
|
+
### send
|
|
115
|
+
|
|
116
|
+
Sends the signal to all connected receivers. If a receiver raises an exception, it propagates immediately and stops further receivers from being called.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
responses = my_signal.send(sender=MyClass, data="example")
|
|
120
|
+
for receiver, response in responses:
|
|
121
|
+
print(f"{receiver} returned {response}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### send_robust
|
|
125
|
+
|
|
126
|
+
Like `send()`, but catches exceptions from receivers and returns them as part of the response list instead of propagating them. This ensures all receivers get called.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
responses = my_signal.send_robust(sender=MyClass, data="example")
|
|
130
|
+
for receiver, response in responses:
|
|
131
|
+
if isinstance(response, Exception):
|
|
132
|
+
print(f"{receiver} raised {response}")
|
|
133
|
+
else:
|
|
134
|
+
print(f"{receiver} returned {response}")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### disconnect
|
|
138
|
+
|
|
139
|
+
Removes a receiver from the signal. You typically don't need to call this since receivers use weak references by default and are automatically removed when garbage collected.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
my_signal.disconnect(my_receiver)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### has_listeners
|
|
146
|
+
|
|
147
|
+
Checks if any receivers are connected to the signal.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
if my_signal.has_listeners():
|
|
151
|
+
my_signal.send(sender=MyClass)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## FAQs
|
|
155
|
+
|
|
156
|
+
#### Why must receivers accept `**kwargs`?
|
|
157
|
+
|
|
158
|
+
This allows signals to add new arguments in the future without breaking existing receivers. When `DEBUG` is enabled, Plain validates that your receivers accept `**kwargs` and raises an error if they don't.
|
|
159
|
+
|
|
160
|
+
#### What is `dispatch_uid` for?
|
|
161
|
+
|
|
162
|
+
When connecting a receiver, you can provide a `dispatch_uid` to prevent the same receiver from being connected multiple times. This is useful when your connection code might run more than once.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
request_finished.connect(my_receiver, dispatch_uid="my_unique_id")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Can I use strong references instead of weak references?
|
|
169
|
+
|
|
170
|
+
By default, signals use weak references to receivers, so receivers are automatically disconnected when they go out of scope. If you need to keep a receiver connected even after its normal lifecycle, pass `weak=False` to `.connect()`.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
my_signal.connect(my_receiver, weak=False)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### How do I see all receivers connected to a signal?
|
|
177
|
+
|
|
178
|
+
You can inspect the `receivers` attribute on the signal, though this is primarily for debugging. For checking if any receivers exist, use [`has_listeners()`](./dispatch/dispatcher.py#has_listeners).
|
|
179
|
+
|
|
180
|
+
## Installation
|
|
181
|
+
|
|
182
|
+
Signals are included as part of Plain and do not require separate installation.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from plain.signals import request_started, request_finished
|
|
186
|
+
from plain.signals.dispatch import Signal, receiver
|
|
187
|
+
```
|
plain/signals/__init__.py
CHANGED