plain 0.68.0__py3-none-any.whl → 0.103.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.
Files changed (192) hide show
  1. plain/CHANGELOG.md +684 -1
  2. plain/README.md +1 -1
  3. plain/agents/.claude/rules/plain.md +88 -0
  4. plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
  5. plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
  6. plain/assets/compile.py +25 -12
  7. plain/assets/finders.py +24 -17
  8. plain/assets/fingerprints.py +10 -7
  9. plain/assets/urls.py +1 -1
  10. plain/assets/views.py +47 -33
  11. plain/chores/README.md +25 -23
  12. plain/chores/__init__.py +2 -1
  13. plain/chores/core.py +27 -0
  14. plain/chores/registry.py +23 -36
  15. plain/cli/README.md +185 -16
  16. plain/cli/__init__.py +2 -1
  17. plain/cli/agent.py +234 -0
  18. plain/cli/build.py +7 -8
  19. plain/cli/changelog.py +11 -5
  20. plain/cli/chores.py +32 -34
  21. plain/cli/core.py +110 -26
  22. plain/cli/docs.py +98 -21
  23. plain/cli/formatting.py +40 -17
  24. plain/cli/install.py +10 -54
  25. plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
  26. plain/cli/output.py +6 -2
  27. plain/cli/preflight.py +27 -75
  28. plain/cli/print.py +4 -4
  29. plain/cli/registry.py +96 -10
  30. plain/cli/{agent/request.py → request.py} +67 -33
  31. plain/cli/runtime.py +45 -0
  32. plain/cli/scaffold.py +2 -7
  33. plain/cli/server.py +153 -0
  34. plain/cli/settings.py +53 -49
  35. plain/cli/shell.py +15 -12
  36. plain/cli/startup.py +9 -8
  37. plain/cli/upgrade.py +17 -104
  38. plain/cli/urls.py +12 -7
  39. plain/cli/utils.py +3 -3
  40. plain/csrf/README.md +65 -40
  41. plain/csrf/middleware.py +53 -43
  42. plain/debug.py +5 -2
  43. plain/exceptions.py +22 -114
  44. plain/forms/README.md +453 -24
  45. plain/forms/__init__.py +55 -4
  46. plain/forms/boundfield.py +15 -8
  47. plain/forms/exceptions.py +1 -1
  48. plain/forms/fields.py +346 -143
  49. plain/forms/forms.py +75 -45
  50. plain/http/README.md +356 -9
  51. plain/http/__init__.py +41 -26
  52. plain/http/cookie.py +15 -7
  53. plain/http/exceptions.py +65 -0
  54. plain/http/middleware.py +32 -0
  55. plain/http/multipartparser.py +99 -88
  56. plain/http/request.py +362 -250
  57. plain/http/response.py +99 -197
  58. plain/internal/__init__.py +8 -1
  59. plain/internal/files/base.py +35 -19
  60. plain/internal/files/locks.py +19 -11
  61. plain/internal/files/move.py +8 -3
  62. plain/internal/files/temp.py +25 -6
  63. plain/internal/files/uploadedfile.py +47 -28
  64. plain/internal/files/uploadhandler.py +64 -58
  65. plain/internal/files/utils.py +24 -10
  66. plain/internal/handlers/base.py +34 -23
  67. plain/internal/handlers/exception.py +68 -65
  68. plain/internal/handlers/wsgi.py +65 -54
  69. plain/internal/middleware/headers.py +37 -11
  70. plain/internal/middleware/hosts.py +11 -8
  71. plain/internal/middleware/https.py +17 -7
  72. plain/internal/middleware/slash.py +14 -9
  73. plain/internal/reloader.py +77 -0
  74. plain/json.py +2 -1
  75. plain/logs/README.md +161 -62
  76. plain/logs/__init__.py +1 -1
  77. plain/logs/{loggers.py → app.py} +71 -67
  78. plain/logs/configure.py +63 -14
  79. plain/logs/debug.py +17 -6
  80. plain/logs/filters.py +15 -0
  81. plain/logs/formatters.py +7 -4
  82. plain/packages/README.md +105 -23
  83. plain/packages/config.py +15 -7
  84. plain/packages/registry.py +27 -16
  85. plain/paginator.py +31 -21
  86. plain/preflight/README.md +209 -24
  87. plain/preflight/__init__.py +1 -0
  88. plain/preflight/checks.py +3 -1
  89. plain/preflight/files.py +3 -1
  90. plain/preflight/registry.py +26 -11
  91. plain/preflight/results.py +15 -7
  92. plain/preflight/security.py +15 -13
  93. plain/preflight/settings.py +54 -0
  94. plain/preflight/urls.py +4 -1
  95. plain/runtime/README.md +115 -47
  96. plain/runtime/__init__.py +10 -6
  97. plain/runtime/global_settings.py +34 -25
  98. plain/runtime/secret.py +20 -0
  99. plain/runtime/user_settings.py +110 -38
  100. plain/runtime/utils.py +1 -1
  101. plain/server/LICENSE +35 -0
  102. plain/server/README.md +155 -0
  103. plain/server/__init__.py +9 -0
  104. plain/server/app.py +52 -0
  105. plain/server/arbiter.py +555 -0
  106. plain/server/config.py +118 -0
  107. plain/server/errors.py +31 -0
  108. plain/server/glogging.py +292 -0
  109. plain/server/http/__init__.py +12 -0
  110. plain/server/http/body.py +283 -0
  111. plain/server/http/errors.py +155 -0
  112. plain/server/http/message.py +400 -0
  113. plain/server/http/parser.py +70 -0
  114. plain/server/http/unreader.py +88 -0
  115. plain/server/http/wsgi.py +421 -0
  116. plain/server/pidfile.py +92 -0
  117. plain/server/sock.py +240 -0
  118. plain/server/util.py +317 -0
  119. plain/server/workers/__init__.py +6 -0
  120. plain/server/workers/base.py +304 -0
  121. plain/server/workers/sync.py +212 -0
  122. plain/server/workers/thread.py +399 -0
  123. plain/server/workers/workertmp.py +50 -0
  124. plain/signals/README.md +170 -1
  125. plain/signals/__init__.py +0 -1
  126. plain/signals/dispatch/dispatcher.py +49 -27
  127. plain/signing.py +131 -35
  128. plain/templates/README.md +211 -20
  129. plain/templates/jinja/__init__.py +13 -5
  130. plain/templates/jinja/environments.py +5 -4
  131. plain/templates/jinja/extensions.py +12 -5
  132. plain/templates/jinja/filters.py +7 -2
  133. plain/templates/jinja/globals.py +2 -2
  134. plain/test/README.md +184 -22
  135. plain/test/client.py +340 -222
  136. plain/test/encoding.py +9 -6
  137. plain/test/exceptions.py +7 -2
  138. plain/urls/README.md +157 -73
  139. plain/urls/converters.py +18 -15
  140. plain/urls/exceptions.py +2 -2
  141. plain/urls/patterns.py +38 -22
  142. plain/urls/resolvers.py +35 -25
  143. plain/urls/utils.py +5 -1
  144. plain/utils/README.md +250 -3
  145. plain/utils/cache.py +17 -11
  146. plain/utils/crypto.py +21 -5
  147. plain/utils/datastructures.py +89 -56
  148. plain/utils/dateparse.py +9 -6
  149. plain/utils/deconstruct.py +15 -7
  150. plain/utils/decorators.py +5 -1
  151. plain/utils/dotenv.py +373 -0
  152. plain/utils/duration.py +8 -4
  153. plain/utils/encoding.py +14 -7
  154. plain/utils/functional.py +66 -49
  155. plain/utils/hashable.py +5 -1
  156. plain/utils/html.py +36 -22
  157. plain/utils/http.py +16 -9
  158. plain/utils/inspect.py +14 -6
  159. plain/utils/ipv6.py +7 -3
  160. plain/utils/itercompat.py +6 -1
  161. plain/utils/module_loading.py +7 -3
  162. plain/utils/regex_helper.py +37 -23
  163. plain/utils/safestring.py +14 -6
  164. plain/utils/text.py +41 -23
  165. plain/utils/timezone.py +33 -22
  166. plain/utils/tree.py +35 -19
  167. plain/validators.py +94 -52
  168. plain/views/README.md +156 -79
  169. plain/views/__init__.py +0 -1
  170. plain/views/base.py +25 -18
  171. plain/views/errors.py +13 -5
  172. plain/views/exceptions.py +4 -1
  173. plain/views/forms.py +6 -6
  174. plain/views/objects.py +52 -49
  175. plain/views/redirect.py +18 -15
  176. plain/views/templates.py +5 -3
  177. plain/wsgi.py +3 -1
  178. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
  179. plain-0.103.0.dist-info/RECORD +198 -0
  180. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
  181. plain-0.103.0.dist-info/entry_points.txt +2 -0
  182. plain/AGENTS.md +0 -18
  183. plain/cli/agent/__init__.py +0 -20
  184. plain/cli/agent/docs.py +0 -80
  185. plain/cli/agent/md.py +0 -87
  186. plain/cli/agent/prompt.py +0 -45
  187. plain/csrf/views.py +0 -31
  188. plain/logs/utils.py +0 -46
  189. plain/templates/AGENTS.md +0 -3
  190. plain-0.68.0.dist-info/RECORD +0 -169
  191. plain-0.68.0.dist-info/entry_points.txt +0 -5
  192. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/server/sock.py ADDED
@@ -0,0 +1,240 @@
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 errno
10
+ import os
11
+ import socket
12
+ import ssl
13
+ import stat
14
+ import sys
15
+ import time
16
+ from typing import TYPE_CHECKING
17
+
18
+ from . import util
19
+
20
+ if TYPE_CHECKING:
21
+ from .config import Config
22
+ from .glogging import Logger
23
+
24
+ # Maximum number of pending connections in the socket listen queue
25
+ BACKLOG = 2048
26
+
27
+
28
+ class BaseSocket:
29
+ FAMILY: socket.AddressFamily
30
+
31
+ def __init__(
32
+ self,
33
+ address: tuple[str, int] | str,
34
+ conf: Config,
35
+ log: Logger,
36
+ fd: int | None = None,
37
+ ) -> None:
38
+ self.log = log
39
+ self.conf = conf
40
+
41
+ self.cfg_addr = address
42
+ if fd is None:
43
+ sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)
44
+ bound = False
45
+ else:
46
+ sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)
47
+ os.close(fd)
48
+ bound = True
49
+
50
+ self.sock: socket.socket | None = self.set_options(sock, bound=bound)
51
+
52
+ def __str__(self) -> str:
53
+ assert self.sock is not None, "Socket is closed"
54
+ return f"<socket {self.sock.fileno()}>"
55
+
56
+ def __getattr__(self, name: str) -> object:
57
+ return getattr(self.sock, name)
58
+
59
+ def accept(self) -> tuple[socket.socket, tuple[str, int] | str]:
60
+ """Accept a connection. Returns (socket object, address)."""
61
+ assert self.sock is not None, "Socket is closed"
62
+ return self.sock.accept()
63
+
64
+ def fileno(self) -> int:
65
+ """Return the socket's file descriptor."""
66
+ assert self.sock is not None, "Socket is closed"
67
+ return self.sock.fileno()
68
+
69
+ def setblocking(self, flag: bool) -> None:
70
+ """Set blocking or non-blocking mode of the socket."""
71
+ assert self.sock is not None, "Socket is closed"
72
+ return self.sock.setblocking(flag)
73
+
74
+ def getsockname(self) -> tuple[str, int] | str:
75
+ assert self.sock is not None, "Socket is closed"
76
+ return self.sock.getsockname()
77
+
78
+ def set_options(self, sock: socket.socket, bound: bool = False) -> socket.socket:
79
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
80
+ if not bound:
81
+ self.bind(sock)
82
+ sock.setblocking(False)
83
+
84
+ # make sure that the socket can be inherited
85
+ if hasattr(sock, "set_inheritable"):
86
+ sock.set_inheritable(True)
87
+
88
+ sock.listen(BACKLOG)
89
+ return sock
90
+
91
+ def bind(self, sock: socket.socket) -> None:
92
+ sock.bind(self.cfg_addr)
93
+
94
+ def close(self) -> None:
95
+ if self.sock is None:
96
+ return None
97
+
98
+ try:
99
+ self.sock.close()
100
+ except OSError as e:
101
+ self.log.info("Error while closing socket %s", str(e))
102
+
103
+ self.sock = None
104
+ return None
105
+
106
+
107
+ class TCPSocket(BaseSocket):
108
+ FAMILY = socket.AF_INET
109
+
110
+ def __str__(self) -> str:
111
+ if self.conf.is_ssl:
112
+ scheme = "https"
113
+ else:
114
+ scheme = "http"
115
+
116
+ assert self.sock is not None, "Socket is closed"
117
+ addr = self.sock.getsockname()
118
+ return f"{scheme}://{addr[0]}:{addr[1]}"
119
+
120
+ def set_options(self, sock: socket.socket, bound: bool = False) -> socket.socket:
121
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
122
+ return super().set_options(sock, bound=bound)
123
+
124
+
125
+ class TCP6Socket(TCPSocket):
126
+ FAMILY = socket.AF_INET6
127
+
128
+ def __str__(self) -> str:
129
+ assert self.sock is not None, "Socket is closed"
130
+ (host, port, _, _) = self.sock.getsockname()
131
+ return f"http://[{host}]:{port}"
132
+
133
+
134
+ class UnixSocket(BaseSocket):
135
+ FAMILY = socket.AF_UNIX
136
+
137
+ def __init__(
138
+ self,
139
+ addr: str,
140
+ conf: Config,
141
+ log: Logger,
142
+ fd: int | None = None,
143
+ ):
144
+ if fd is None:
145
+ try:
146
+ st = os.stat(addr)
147
+ except OSError as e:
148
+ if e.args[0] != errno.ENOENT:
149
+ raise
150
+ else:
151
+ if stat.S_ISSOCK(st.st_mode):
152
+ os.remove(addr)
153
+ else:
154
+ raise ValueError(f"{addr!r} is not a socket")
155
+ super().__init__(addr, conf, log, fd=fd)
156
+
157
+ def __str__(self) -> str:
158
+ return f"unix:{self.cfg_addr}"
159
+
160
+ def bind(self, sock: socket.socket) -> None:
161
+ sock.bind(self.cfg_addr)
162
+
163
+
164
+ def _sock_type(addr: tuple[str, int] | str | bytes) -> type[BaseSocket]:
165
+ if isinstance(addr, tuple):
166
+ if util.is_ipv6(addr[0]):
167
+ sock_type = TCP6Socket
168
+ else:
169
+ sock_type = TCPSocket
170
+ elif isinstance(addr, str | bytes):
171
+ sock_type = UnixSocket
172
+ else:
173
+ raise TypeError(f"Unable to create socket from: {addr!r}")
174
+ return sock_type
175
+
176
+
177
+ def create_sockets(conf: Config, log: Logger) -> list[BaseSocket]:
178
+ """
179
+ Create a new socket for the configured addresses.
180
+
181
+ If a configured address is a tuple then a TCP socket is created.
182
+ If it is a string, a Unix socket is created. Otherwise, a TypeError is
183
+ raised.
184
+ """
185
+ listeners = []
186
+
187
+ # check ssl config early to raise the error on startup
188
+ # only the certfile is needed since it can contains the keyfile
189
+ if conf.certfile and not os.path.exists(conf.certfile):
190
+ raise ValueError(f'certfile "{conf.certfile}" does not exist')
191
+
192
+ if conf.keyfile and not os.path.exists(conf.keyfile):
193
+ raise ValueError(f'keyfile "{conf.keyfile}" does not exist')
194
+
195
+ for addr in conf.address:
196
+ sock_type = _sock_type(addr)
197
+ sock = None
198
+ for i in range(5):
199
+ try:
200
+ sock = sock_type(addr, conf, log)
201
+ except OSError as e:
202
+ if e.args[0] == errno.EADDRINUSE:
203
+ log.error("Connection in use: %s", str(addr))
204
+ if e.args[0] == errno.EADDRNOTAVAIL:
205
+ log.error("Invalid address: %s", str(addr))
206
+ msg = "connection to {addr} failed: {error}"
207
+ log.error(msg.format(addr=str(addr), error=str(e)))
208
+ if i < 5:
209
+ log.debug("Retrying in 1 second.")
210
+ time.sleep(1)
211
+ else:
212
+ break
213
+
214
+ if sock is None:
215
+ log.error("Can't connect to %s", str(addr))
216
+ sys.exit(1)
217
+
218
+ listeners.append(sock)
219
+
220
+ return listeners
221
+
222
+
223
+ def close_sockets(listeners: list[BaseSocket], unlink: bool = True) -> None:
224
+ for sock in listeners:
225
+ sock_name = sock.getsockname()
226
+ sock.close()
227
+ if unlink and _sock_type(sock_name) is UnixSocket:
228
+ assert isinstance(sock_name, str)
229
+ os.unlink(sock_name)
230
+
231
+
232
+ def ssl_context(conf: Config) -> ssl.SSLContext:
233
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
234
+ assert conf.certfile is not None
235
+ context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile)
236
+ return context
237
+
238
+
239
+ def ssl_wrap_socket(sock: socket.socket, conf: Config) -> ssl.SSLSocket:
240
+ return ssl_context(conf).wrap_socket(sock, server_side=True)
plain/server/util.py ADDED
@@ -0,0 +1,317 @@
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 email.utils
10
+ import errno
11
+ import fcntl
12
+ import html
13
+ import io
14
+ import os
15
+ import random
16
+ import re
17
+ import socket
18
+ import sys
19
+ import textwrap
20
+ import time
21
+ import urllib.parse
22
+ import warnings
23
+ from collections.abc import Callable
24
+ from typing import Any
25
+
26
+ # Server and Date aren't technically hop-by-hop
27
+ # headers, but they are in the purview of the
28
+ # origin server which the WSGI spec says we should
29
+ # act like. So we drop them and add our own.
30
+ #
31
+ # In the future, concatenation server header values
32
+ # might be better, but nothing else does it and
33
+ # dropping them is easier.
34
+ hop_headers = set(
35
+ """
36
+ connection keep-alive proxy-authenticate proxy-authorization
37
+ te trailers transfer-encoding upgrade
38
+ server date
39
+ """.split()
40
+ )
41
+
42
+ if sys.platform.startswith("win"):
43
+
44
+ def _waitfor(
45
+ func: Callable[[str], None], pathname: str, waitall: bool = False
46
+ ) -> None:
47
+ # Perform the operation
48
+ func(pathname)
49
+ # Now setup the wait loop
50
+ if waitall:
51
+ dirname = pathname
52
+ else:
53
+ dirname, name = os.path.split(pathname)
54
+ dirname = dirname or "."
55
+ # Check for `pathname` to be removed from the filesystem.
56
+ # The exponential backoff of the timeout amounts to a total
57
+ # of ~1 second after which the deletion is probably an error
58
+ # anyway.
59
+ # Testing on a i7@4.3GHz shows that usually only 1 iteration is
60
+ # required when contention occurs.
61
+ timeout = 0.001
62
+ while timeout < 1.0:
63
+ # Note we are only testing for the existence of the file(s) in
64
+ # the contents of the directory regardless of any security or
65
+ # access rights. If we have made it this far, we have sufficient
66
+ # permissions to do that much using Python's equivalent of the
67
+ # Windows API FindFirstFile.
68
+ # Other Windows APIs can fail or give incorrect results when
69
+ # dealing with files that are pending deletion.
70
+ L = os.listdir(dirname)
71
+ if not L if waitall else name in L:
72
+ return None
73
+ # Increase the timeout and try again
74
+ time.sleep(timeout)
75
+ timeout *= 2
76
+ warnings.warn(
77
+ "tests may fail, delete still pending for " + pathname,
78
+ RuntimeWarning,
79
+ stacklevel=4,
80
+ )
81
+ return None
82
+
83
+ def _unlink(filename: str) -> None:
84
+ _waitfor(os.unlink, filename)
85
+ return None
86
+ else:
87
+ _unlink = os.unlink
88
+
89
+
90
+ def unlink(filename: str) -> None:
91
+ try:
92
+ _unlink(filename)
93
+ except OSError as error:
94
+ # The filename need not exist.
95
+ if error.errno not in (errno.ENOENT, errno.ENOTDIR):
96
+ raise
97
+
98
+
99
+ def is_ipv6(addr: str) -> bool:
100
+ try:
101
+ socket.inet_pton(socket.AF_INET6, addr)
102
+ except OSError: # not a valid address
103
+ return False
104
+ except ValueError: # ipv6 not supported on this platform
105
+ return False
106
+ return True
107
+
108
+
109
+ def parse_address(netloc: str, default_port: str = "8000") -> str | tuple[str, int]:
110
+ if re.match(r"unix:(//)?", netloc):
111
+ return re.split(r"unix:(//)?", netloc)[-1]
112
+
113
+ if netloc.startswith("tcp://"):
114
+ netloc = netloc.split("tcp://")[1]
115
+ host, port = netloc, default_port
116
+
117
+ if "[" in netloc and "]" in netloc:
118
+ host = netloc.split("]")[0][1:]
119
+ port = (netloc.split("]:") + [default_port])[1]
120
+ elif ":" in netloc:
121
+ host, port = (netloc.split(":") + [default_port])[:2]
122
+ elif netloc == "":
123
+ host, port = "0.0.0.0", default_port
124
+
125
+ try:
126
+ port = int(port)
127
+ except ValueError:
128
+ raise RuntimeError(f"{port!r} is not a valid port number.")
129
+
130
+ return host.lower(), port
131
+
132
+
133
+ def close_on_exec(fd: int) -> None:
134
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
135
+ flags |= fcntl.FD_CLOEXEC
136
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags)
137
+
138
+
139
+ def set_non_blocking(fd: int) -> None:
140
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
141
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
142
+
143
+
144
+ def close(sock: socket.socket) -> None:
145
+ try:
146
+ sock.close()
147
+ except OSError:
148
+ pass
149
+
150
+
151
+ def write_chunk(sock: socket.socket, data: str | bytes) -> None:
152
+ if isinstance(data, str):
153
+ data = data.encode("utf-8")
154
+ chunk_size = f"{len(data):X}\r\n"
155
+ chunk = b"".join([chunk_size.encode("utf-8"), data, b"\r\n"])
156
+ sock.sendall(chunk)
157
+
158
+
159
+ def write(sock: socket.socket, data: str | bytes, chunked: bool = False) -> None:
160
+ if chunked:
161
+ return write_chunk(sock, data)
162
+ if isinstance(data, str):
163
+ data = data.encode("utf-8")
164
+ sock.sendall(data)
165
+
166
+
167
+ def write_nonblock(
168
+ sock: socket.socket, data: str | bytes, chunked: bool = False
169
+ ) -> None:
170
+ timeout = sock.gettimeout()
171
+ if timeout != 0.0:
172
+ try:
173
+ sock.setblocking(False)
174
+ return write(sock, data, chunked)
175
+ finally:
176
+ sock.setblocking(True)
177
+ else:
178
+ return write(sock, data, chunked)
179
+
180
+
181
+ def write_error(sock: socket.socket, status_int: int, reason: str, mesg: str) -> None:
182
+ html_error = textwrap.dedent("""\
183
+ <html>
184
+ <head>
185
+ <title>%(reason)s</title>
186
+ </head>
187
+ <body>
188
+ <h1><p>%(reason)s</p></h1>
189
+ %(mesg)s
190
+ </body>
191
+ </html>
192
+ """) % {"reason": reason, "mesg": html.escape(mesg)}
193
+
194
+ http = textwrap.dedent("""\
195
+ HTTP/1.1 %s %s\r
196
+ Connection: close\r
197
+ Content-Type: text/html\r
198
+ Content-Length: %d\r
199
+ \r
200
+ %s""") % (str(status_int), reason, len(html_error), html_error)
201
+ write_nonblock(sock, http.encode("latin1"))
202
+
203
+
204
+ def getcwd() -> str:
205
+ # get current path, try to use PWD env first
206
+ try:
207
+ a = os.stat(os.environ["PWD"])
208
+ b = os.stat(os.getcwd())
209
+ if a.st_ino == b.st_ino and a.st_dev == b.st_dev:
210
+ cwd = os.environ["PWD"]
211
+ else:
212
+ cwd = os.getcwd()
213
+ except Exception:
214
+ cwd = os.getcwd()
215
+ return cwd
216
+
217
+
218
+ def http_date(timestamp: float | None = None) -> str:
219
+ """Return the current date and time formatted for a message header."""
220
+ if timestamp is None:
221
+ timestamp = time.time()
222
+ s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)
223
+ return s
224
+
225
+
226
+ def is_hoppish(header: str) -> bool:
227
+ return header.lower().strip() in hop_headers
228
+
229
+
230
+ def seed() -> None:
231
+ try:
232
+ random.seed(os.urandom(64))
233
+ except NotImplementedError:
234
+ random.seed(f"{time.time()}.{os.getpid()}")
235
+
236
+
237
+ def check_is_writable(path: str) -> None:
238
+ try:
239
+ with open(path, "a") as f:
240
+ f.close()
241
+ except OSError as e:
242
+ raise RuntimeError(f"Error: '{path}' isn't writable [{e!r}]")
243
+
244
+
245
+ def to_bytestring(value: str | bytes, encoding: str = "utf8") -> bytes:
246
+ """Converts a string argument to a byte string"""
247
+ if isinstance(value, bytes):
248
+ return value
249
+ if not isinstance(value, str):
250
+ raise TypeError(f"{value!r} is not a string")
251
+
252
+ return value.encode(encoding)
253
+
254
+
255
+ def has_fileno(obj: Any) -> bool:
256
+ if not hasattr(obj, "fileno"):
257
+ return False
258
+
259
+ # check BytesIO case and maybe others
260
+ try:
261
+ obj.fileno()
262
+ except (AttributeError, OSError, io.UnsupportedOperation):
263
+ return False
264
+
265
+ return True
266
+
267
+
268
+ def make_fail_app(msg: str | bytes) -> Callable[..., Any]:
269
+ msg = to_bytestring(msg)
270
+
271
+ def app(environ: Any, start_response: Any) -> list[bytes]:
272
+ start_response(
273
+ "500 Internal Server Error",
274
+ [("Content-Type", "text/plain"), ("Content-Length", str(len(msg)))],
275
+ )
276
+ return [msg]
277
+
278
+ return app
279
+
280
+
281
+ def split_request_uri(uri: str) -> urllib.parse.SplitResult:
282
+ if uri.startswith("//"):
283
+ # When the path starts with //, urlsplit considers it as a
284
+ # relative uri while the RFC says we should consider it as abs_path
285
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
286
+ # We use temporary dot prefix to workaround this behaviour
287
+ parts = urllib.parse.urlsplit("." + uri)
288
+ return parts._replace(path=parts.path[1:])
289
+
290
+ return urllib.parse.urlsplit(uri)
291
+
292
+
293
+ # From six.reraise
294
+ def reraise(
295
+ tp: type[BaseException] | None, value: BaseException | None, tb: Any = None
296
+ ) -> None:
297
+ try:
298
+ if tp is None:
299
+ return
300
+ if value is None:
301
+ value = tp()
302
+ if value.__traceback__ is not tb:
303
+ raise value.with_traceback(tb)
304
+ raise value
305
+ finally:
306
+ value = None
307
+ tb = None
308
+
309
+
310
+ def bytes_to_str(b: str | bytes) -> str:
311
+ if isinstance(b, str):
312
+ return b
313
+ return str(b, "latin1")
314
+
315
+
316
+ def unquote_to_wsgi_str(string: str) -> str:
317
+ return urllib.parse.unquote_to_bytes(string).decode("latin-1")
@@ -0,0 +1,6 @@
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.