plain 0.76.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 +13 -0
- plain/cli/server.py +0 -8
- plain/internal/reloader.py +77 -0
- plain/server/README.md +1 -2
- plain/server/arbiter.py +2 -2
- plain/server/config.py +6 -6
- plain/server/util.py +0 -67
- plain/server/workers/__init__.py +0 -6
- plain/server/workers/base.py +4 -7
- {plain-0.76.0.dist-info → plain-0.77.0.dist-info}/METADATA +2 -1
- {plain-0.76.0.dist-info → plain-0.77.0.dist-info}/RECORD +15 -15
- plain/server/reloader.py +0 -158
- /plain/server/workers/{gthread.py → thread.py} +0 -0
- {plain-0.76.0.dist-info → plain-0.77.0.dist-info}/WHEEL +0 -0
- {plain-0.76.0.dist-info → plain-0.77.0.dist-info}/entry_points.txt +0 -0
- {plain-0.76.0.dist-info → plain-0.77.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.77.0](https://github.com/dropseed/plain/releases/plain@0.77.0) (2025-10-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- The `plain server --reload` now uses `watchfiles` for improved cross-platform file watching ([92e95c5032](https://github.com/dropseed/plain/commit/92e95c5032))
|
|
8
|
+
- Server reloader now watches `.env*` files for changes and triggers automatic reload ([92e95c5032](https://github.com/dropseed/plain/commit/92e95c5032))
|
|
9
|
+
- HTML template additions and deletions now trigger automatic server reload when using `--reload` ([f2f31c288b](https://github.com/dropseed/plain/commit/f2f31c288b))
|
|
10
|
+
- Internal server worker type renamed from "gthread" to "thread" for clarity ([6470748e91](https://github.com/dropseed/plain/commit/6470748e91))
|
|
11
|
+
|
|
12
|
+
### Upgrade instructions
|
|
13
|
+
|
|
14
|
+
- No changes required
|
|
15
|
+
|
|
3
16
|
## [0.76.0](https://github.com/dropseed/plain/releases/plain@0.76.0) (2025-10-12)
|
|
4
17
|
|
|
5
18
|
### What's changed
|
plain/cli/server.py
CHANGED
|
@@ -58,12 +58,6 @@ from plain.cli.runtime import without_runtime_setup
|
|
|
58
58
|
is_flag=True,
|
|
59
59
|
help="Restart workers when code changes (dev only)",
|
|
60
60
|
)
|
|
61
|
-
@click.option(
|
|
62
|
-
"--reload-extra-file",
|
|
63
|
-
multiple=True,
|
|
64
|
-
type=click.Path(exists=True),
|
|
65
|
-
help="Additional files to watch for reload (can be used multiple times)",
|
|
66
|
-
)
|
|
67
61
|
@click.option(
|
|
68
62
|
"--access-log",
|
|
69
63
|
default="-",
|
|
@@ -109,7 +103,6 @@ def server(
|
|
|
109
103
|
keyfile: str | None,
|
|
110
104
|
log_level: str,
|
|
111
105
|
reload: bool,
|
|
112
|
-
reload_extra_file: tuple[str, ...],
|
|
113
106
|
access_log: str,
|
|
114
107
|
error_log: str,
|
|
115
108
|
log_format: str,
|
|
@@ -130,7 +123,6 @@ def server(
|
|
|
130
123
|
timeout=timeout,
|
|
131
124
|
max_requests=max_requests,
|
|
132
125
|
reload=reload,
|
|
133
|
-
reload_extra_files=list(reload_extra_file) if reload_extra_file else [],
|
|
134
126
|
pidfile=pidfile,
|
|
135
127
|
certfile=certfile,
|
|
136
128
|
keyfile=keyfile,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import os.path
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
import watchfiles
|
|
11
|
+
|
|
12
|
+
COMPILED_EXT_RE = re.compile(r"py[co]$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Reloader(threading.Thread):
|
|
16
|
+
"""File change reloader using watchfiles for cross-platform native file watching."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, callback: Callable[[str], None], watch_html: bool) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.daemon = True
|
|
21
|
+
self._callback = callback
|
|
22
|
+
self._watch_html = watch_html
|
|
23
|
+
|
|
24
|
+
def get_watch_paths(self) -> set[str]:
|
|
25
|
+
"""Get all directories to watch for changes."""
|
|
26
|
+
paths = set()
|
|
27
|
+
|
|
28
|
+
# Get directories from loaded Python modules
|
|
29
|
+
for module in tuple(sys.modules.values()):
|
|
30
|
+
if not hasattr(module, "__file__") or not module.__file__:
|
|
31
|
+
continue
|
|
32
|
+
# Convert .pyc/.pyo to .py and get directory
|
|
33
|
+
file_path = COMPILED_EXT_RE.sub("py", module.__file__)
|
|
34
|
+
dir_path = os.path.dirname(os.path.abspath(file_path))
|
|
35
|
+
if os.path.isdir(dir_path):
|
|
36
|
+
paths.add(dir_path)
|
|
37
|
+
|
|
38
|
+
# Add current working directory for .env files
|
|
39
|
+
cwd = os.getcwd()
|
|
40
|
+
if os.path.isdir(cwd):
|
|
41
|
+
paths.add(cwd)
|
|
42
|
+
|
|
43
|
+
return paths
|
|
44
|
+
|
|
45
|
+
def run(self) -> None:
|
|
46
|
+
"""Watch for file changes and trigger callback."""
|
|
47
|
+
watch_paths = self.get_watch_paths()
|
|
48
|
+
|
|
49
|
+
for changes in watchfiles.watch(*watch_paths, rust_timeout=1000):
|
|
50
|
+
for change_type, file_path in changes:
|
|
51
|
+
should_reload = False
|
|
52
|
+
filename = os.path.basename(file_path)
|
|
53
|
+
|
|
54
|
+
# Python files: reload on modify/add
|
|
55
|
+
if change_type in (watchfiles.Change.modified, watchfiles.Change.added):
|
|
56
|
+
if file_path.endswith(".py"):
|
|
57
|
+
should_reload = True
|
|
58
|
+
|
|
59
|
+
# .env files: reload on modify/add/delete
|
|
60
|
+
if change_type in (
|
|
61
|
+
watchfiles.Change.modified,
|
|
62
|
+
watchfiles.Change.added,
|
|
63
|
+
watchfiles.Change.deleted,
|
|
64
|
+
):
|
|
65
|
+
if filename.startswith(".env"):
|
|
66
|
+
should_reload = True
|
|
67
|
+
|
|
68
|
+
# HTML files: only reload on add/delete (Jinja auto-reloads modifications)
|
|
69
|
+
if self._watch_html and change_type in (
|
|
70
|
+
watchfiles.Change.added,
|
|
71
|
+
watchfiles.Change.deleted,
|
|
72
|
+
):
|
|
73
|
+
if file_path.endswith(".html"):
|
|
74
|
+
should_reload = True
|
|
75
|
+
|
|
76
|
+
if should_reload:
|
|
77
|
+
self._callback(file_path)
|
plain/server/README.md
CHANGED
|
@@ -39,8 +39,7 @@ Common options:
|
|
|
39
39
|
- `--workers` / `-w` - Number of worker processes (default: 1, or `$WEB_CONCURRENCY` env var)
|
|
40
40
|
- `--threads` - Number of threads per worker (default: 1)
|
|
41
41
|
- `--timeout` / `-t` - Worker timeout in seconds (default: 30)
|
|
42
|
-
- `--reload` - Enable auto-reload on code changes (default: False)
|
|
43
|
-
- `--reload-extra-file` - Additional files to watch for reloading (can be used multiple times)
|
|
42
|
+
- `--reload` - Enable auto-reload on code changes, including `.env*` files (default: False)
|
|
44
43
|
- `--certfile` - Path to SSL certificate file
|
|
45
44
|
- `--keyfile` - Path to SSL key file
|
|
46
45
|
- `--log-level` - Logging level: debug, info, warning, error, critical (default: info)
|
plain/server/arbiter.py
CHANGED
|
@@ -490,7 +490,7 @@ class Arbiter:
|
|
|
490
490
|
# Process Child
|
|
491
491
|
worker.pid = os.getpid()
|
|
492
492
|
try:
|
|
493
|
-
self.log.info("
|
|
493
|
+
self.log.info("Server worker started pid=%s", worker.pid)
|
|
494
494
|
worker.init_process()
|
|
495
495
|
sys.exit(0)
|
|
496
496
|
except SystemExit:
|
|
@@ -506,7 +506,7 @@ class Arbiter:
|
|
|
506
506
|
sys.exit(self.WORKER_BOOT_ERROR)
|
|
507
507
|
sys.exit(-1)
|
|
508
508
|
finally:
|
|
509
|
-
self.log.info("
|
|
509
|
+
self.log.info("Server worker exiting (pid: %s)", worker.pid)
|
|
510
510
|
try:
|
|
511
511
|
worker.tmp.close()
|
|
512
512
|
except Exception:
|
plain/server/config.py
CHANGED
|
@@ -10,6 +10,8 @@ import os
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
|
|
12
12
|
from . import util
|
|
13
|
+
from .workers.sync import SyncWorker
|
|
14
|
+
from .workers.thread import ThreadWorker
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
@@ -27,7 +29,6 @@ class Config:
|
|
|
27
29
|
timeout: int
|
|
28
30
|
max_requests: int
|
|
29
31
|
reload: bool
|
|
30
|
-
reload_extra_files: list[str]
|
|
31
32
|
pidfile: str | None
|
|
32
33
|
certfile: str | None
|
|
33
34
|
keyfile: str | None
|
|
@@ -41,20 +42,19 @@ class Config:
|
|
|
41
42
|
def worker_class_str(self) -> str:
|
|
42
43
|
# Auto-select based on threads
|
|
43
44
|
if self.threads > 1:
|
|
44
|
-
return "
|
|
45
|
+
return "thread"
|
|
45
46
|
return "sync"
|
|
46
47
|
|
|
47
48
|
@property
|
|
48
49
|
def worker_class(self) -> type:
|
|
49
50
|
# Auto-select based on threads
|
|
50
51
|
if self.threads > 1:
|
|
51
|
-
|
|
52
|
+
worker_class = ThreadWorker
|
|
52
53
|
else:
|
|
53
|
-
|
|
54
|
+
worker_class = SyncWorker
|
|
54
55
|
|
|
55
|
-
worker_class = util.load_class(uri)
|
|
56
56
|
if hasattr(worker_class, "setup"):
|
|
57
|
-
worker_class.setup()
|
|
57
|
+
worker_class.setup()
|
|
58
58
|
return worker_class
|
|
59
59
|
|
|
60
60
|
@property
|
plain/server/util.py
CHANGED
|
@@ -10,8 +10,6 @@ import email.utils
|
|
|
10
10
|
import errno
|
|
11
11
|
import fcntl
|
|
12
12
|
import html
|
|
13
|
-
import importlib
|
|
14
|
-
import inspect
|
|
15
13
|
import io
|
|
16
14
|
import os
|
|
17
15
|
import random
|
|
@@ -20,14 +18,11 @@ import socket
|
|
|
20
18
|
import sys
|
|
21
19
|
import textwrap
|
|
22
20
|
import time
|
|
23
|
-
import traceback
|
|
24
21
|
import urllib.parse
|
|
25
22
|
import warnings
|
|
26
23
|
from collections.abc import Callable
|
|
27
24
|
from typing import Any
|
|
28
25
|
|
|
29
|
-
from .workers import SUPPORTED_WORKERS
|
|
30
|
-
|
|
31
26
|
# Server and Date aren't technically hop-by-hop
|
|
32
27
|
# headers, but they are in the purview of the
|
|
33
28
|
# origin server which the WSGI spec says we should
|
|
@@ -44,55 +39,6 @@ hop_headers = set(
|
|
|
44
39
|
""".split()
|
|
45
40
|
)
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
def load_class(
|
|
49
|
-
uri: str | type,
|
|
50
|
-
default: str = "plain.server.workers.sync.SyncWorker",
|
|
51
|
-
section: str = "plain.server.workers",
|
|
52
|
-
) -> type:
|
|
53
|
-
if inspect.isclass(uri):
|
|
54
|
-
return uri # type: ignore[return-value]
|
|
55
|
-
|
|
56
|
-
components = uri.split(".") # type: ignore[union-attr]
|
|
57
|
-
if len(components) == 1:
|
|
58
|
-
# Handle short names like "sync" or "gthread"
|
|
59
|
-
if uri.startswith("#"): # type: ignore[union-attr]
|
|
60
|
-
uri = uri[1:] # type: ignore[union-attr]
|
|
61
|
-
|
|
62
|
-
if uri in SUPPORTED_WORKERS:
|
|
63
|
-
components = SUPPORTED_WORKERS[uri].split(".")
|
|
64
|
-
else:
|
|
65
|
-
exc_msg = f"Worker type {uri!r} not found in SUPPORTED_WORKERS"
|
|
66
|
-
raise RuntimeError(exc_msg)
|
|
67
|
-
|
|
68
|
-
klass = components.pop(-1)
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
mod = importlib.import_module(".".join(components))
|
|
72
|
-
except Exception:
|
|
73
|
-
exc = traceback.format_exc()
|
|
74
|
-
msg = "class uri %r invalid or not found: \n\n[%s]"
|
|
75
|
-
raise RuntimeError(msg % (uri, exc))
|
|
76
|
-
return getattr(mod, klass)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
positionals = (
|
|
80
|
-
inspect.Parameter.POSITIONAL_ONLY,
|
|
81
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def get_arity(f: Callable[..., Any]) -> int:
|
|
86
|
-
sig = inspect.signature(f)
|
|
87
|
-
arity = 0
|
|
88
|
-
|
|
89
|
-
for param in sig.parameters.values():
|
|
90
|
-
if param.kind in positionals:
|
|
91
|
-
arity += 1
|
|
92
|
-
|
|
93
|
-
return arity
|
|
94
|
-
|
|
95
|
-
|
|
96
42
|
if sys.platform.startswith("win"):
|
|
97
43
|
|
|
98
44
|
def _waitfor(
|
|
@@ -317,19 +263,6 @@ def has_fileno(obj: Any) -> bool:
|
|
|
317
263
|
return True
|
|
318
264
|
|
|
319
265
|
|
|
320
|
-
def warn(msg: str) -> None:
|
|
321
|
-
print("!!!", file=sys.stderr)
|
|
322
|
-
|
|
323
|
-
lines = msg.splitlines()
|
|
324
|
-
for i, line in enumerate(lines):
|
|
325
|
-
if i == 0:
|
|
326
|
-
line = f"WARNING: {line}"
|
|
327
|
-
print(f"!!! {line}", file=sys.stderr)
|
|
328
|
-
|
|
329
|
-
print("!!!\n", file=sys.stderr)
|
|
330
|
-
sys.stderr.flush()
|
|
331
|
-
|
|
332
|
-
|
|
333
266
|
def make_fail_app(msg: str | bytes) -> Callable[..., Any]:
|
|
334
267
|
msg = to_bytestring(msg)
|
|
335
268
|
|
plain/server/workers/__init__.py
CHANGED
plain/server/workers/base.py
CHANGED
|
@@ -17,6 +17,8 @@ from random import randint
|
|
|
17
17
|
from ssl import SSLError
|
|
18
18
|
from typing import TYPE_CHECKING, Any
|
|
19
19
|
|
|
20
|
+
from plain.internal.reloader import Reloader
|
|
21
|
+
|
|
20
22
|
from .. import util
|
|
21
23
|
from ..http.errors import (
|
|
22
24
|
ConfigurationProblem,
|
|
@@ -32,7 +34,6 @@ from ..http.errors import (
|
|
|
32
34
|
UnsupportedTransferCoding,
|
|
33
35
|
)
|
|
34
36
|
from ..http.wsgi import Response, default_environ
|
|
35
|
-
from ..reloader import reloader_engines
|
|
36
37
|
from .workertmp import WorkerTmp
|
|
37
38
|
|
|
38
39
|
if TYPE_CHECKING:
|
|
@@ -143,16 +144,13 @@ class Worker:
|
|
|
143
144
|
if self.cfg.reload:
|
|
144
145
|
|
|
145
146
|
def changed(fname: str) -> None:
|
|
146
|
-
self.log.
|
|
147
|
+
self.log.debug("Server worker reloading: %s modified", fname)
|
|
147
148
|
self.alive = False
|
|
148
149
|
os.write(self.PIPE[1], b"1")
|
|
149
150
|
time.sleep(0.1)
|
|
150
151
|
sys.exit(0)
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
self.reloader = reloader_cls(
|
|
154
|
-
extra_files=self.cfg.reload_extra_files, callback=changed
|
|
155
|
-
)
|
|
153
|
+
self.reloader = Reloader(callback=changed, watch_html=True)
|
|
156
154
|
|
|
157
155
|
self.load_wsgi()
|
|
158
156
|
if self.reloader:
|
|
@@ -177,7 +175,6 @@ class Worker:
|
|
|
177
175
|
# delete the traceback after use.
|
|
178
176
|
try:
|
|
179
177
|
_, exc_val, exc_tb = sys.exc_info()
|
|
180
|
-
self.reloader.add_extra_file(exc_val.filename)
|
|
181
178
|
|
|
182
179
|
tb_string = io.StringIO()
|
|
183
180
|
traceback.print_tb(exc_tb, file=tb_string)
|
|
@@ -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
|
|
@@ -34,7 +34,7 @@ plain/cli/print.py,sha256=7kv9ddXpwOHRSWp6FFLfX4wbmhV7neoOBlE0VcXWccw,238
|
|
|
34
34
|
plain/cli/registry.py,sha256=Z52nVE2bC2h_B_SENnXctn3mx3UWB0qYg969DVP7XX8,1106
|
|
35
35
|
plain/cli/runtime.py,sha256=YbGYfwkH0VxfuIMbOCwM9wSWiQKusPn_gVeGod8OFaE,743
|
|
36
36
|
plain/cli/scaffold.py,sha256=AMAVnTYySgR5nz4sVp3mn0gEGfTKE1N8ZlrVrg2SrFU,1364
|
|
37
|
-
plain/cli/server.py,sha256=
|
|
37
|
+
plain/cli/server.py,sha256=ngUMtxB90t5FnG7HQwOIkN-pwY6ltKPZZ4PLSqN2Y3E,3001
|
|
38
38
|
plain/cli/settings.py,sha256=kafbcPzy8khdtzLRyOHRl215va3E7U_h5USOA39UA3k,2008
|
|
39
39
|
plain/cli/shell.py,sha256=urTp24D4UsKmYi9nT7OOdlT4WhXjkpFVrGYfNNVsXEE,1980
|
|
40
40
|
plain/cli/startup.py,sha256=1nxXQucDkBxXriEN4wI2tiwG96PBNFndVrOyfzvJFdI,1061
|
|
@@ -63,6 +63,7 @@ plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs
|
|
|
63
63
|
plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
|
|
64
64
|
plain/http/response.py,sha256=efAJ2M_uwK8EYMXchOk-b0Jrx3Hukch_rPOW9nG5AV8,24842
|
|
65
65
|
plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
|
|
66
|
+
plain/internal/reloader.py,sha256=n7B-F-WeUXp37pAnvzKX9tcEbUxHSlYqa4gItyA_zko,2662
|
|
66
67
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
|
67
68
|
plain/internal/files/base.py,sha256=TiUIAqBSQCslgAmf5vjrwjbCe2px5Pt0wWLlGc66jXw,4683
|
|
68
69
|
plain/internal/files/locks.py,sha256=jvLL9kroOo50kUo8dbuajDiFvgSL5NH6x5hudRPPjiQ,4022
|
|
@@ -105,17 +106,16 @@ plain/runtime/global_settings.py,sha256=Q-bQP3tNnnuJZvfevGai639RIF_jhd7Dszt-DzTT
|
|
|
105
106
|
plain/runtime/user_settings.py,sha256=Tbd0J6bxp18tKmFsQcdlxeFhUQU68PYtsswzQ2IcfNc,11467
|
|
106
107
|
plain/runtime/utils.py,sha256=sHOv9SWCalBtg32GtZofimM2XaQtf_jAyyf6RQuOlGc,851
|
|
107
108
|
plain/server/LICENSE,sha256=Xt_dw4qYwQI9qSi2u8yMZeb4HuMRp5tESRKhtvvJBgA,1707
|
|
108
|
-
plain/server/README.md,sha256=
|
|
109
|
+
plain/server/README.md,sha256=6jXQeZJVDt6Jn-Ff8Q7cZ1Xsh6fYPFfC-rtQ7zSYzag,2661
|
|
109
110
|
plain/server/__init__.py,sha256=DtRgEcr4IxF4mrtCHloIprk_Q4k1oju4F2VHoyvu4ow,212
|
|
110
111
|
plain/server/app.py,sha256=ozaqdb-a_T3ps7T5EJwIPM63F_497J4o7kw9Pbq7Ga0,1229
|
|
111
|
-
plain/server/arbiter.py,sha256=
|
|
112
|
-
plain/server/config.py,sha256
|
|
112
|
+
plain/server/arbiter.py,sha256=89_4CZn6v0kmfF3-h6y5Ax59Tm9vhYiZw7psuoHMe_A,17404
|
|
113
|
+
plain/server/config.py,sha256=-T1w8dbUwwLd898P1HNufe8Kw8VJDYJaeKY-UuKzvTo,3221
|
|
113
114
|
plain/server/errors.py,sha256=sKl_OJ5Uw-a_r_dZ2o4I8JaKeTrjvY_LR12F6B_p4-g,956
|
|
114
115
|
plain/server/glogging.py,sha256=Ab49Btbr9UvGRgBk8KGVrzsJaFKN1uD0ZurmIC0GYFY,9692
|
|
115
116
|
plain/server/pidfile.py,sha256=8Fcl9u7gvUJjY5z01qGAeRsi13_jAM8CRdeyqL3h2i0,2538
|
|
116
|
-
plain/server/reloader.py,sha256=x3Oe1qmlprnkNRa-RQeuANdfmpRQvMSFfdTtBPIA5z0,4517
|
|
117
117
|
plain/server/sock.py,sha256=NFKtlrMstOT3xU2yKI6BLAzv_WE7VEGt3dHDkFPnPSo,6192
|
|
118
|
-
plain/server/util.py,sha256=
|
|
118
|
+
plain/server/util.py,sha256=vlTzH4jk8s-ZxJt0oAz-LaQZq_8lEk86zMDVClluuwY,8758
|
|
119
119
|
plain/server/http/__init__.py,sha256=kQwTk1l3hYJwVrzr1p-XNAbWYe0icsD7l0ZyGRXMbOI,300
|
|
120
120
|
plain/server/http/body.py,sha256=cz18F4C_gm9h5XvsStV0ncanBjZO1-pegbmbmiMpsQA,8352
|
|
121
121
|
plain/server/http/errors.py,sha256=uqrzOzjjdqXfFim64pc58cxPlbsoxRNiLx0WzIJSzkw,3719
|
|
@@ -123,10 +123,10 @@ plain/server/http/message.py,sha256=5UdOA7CMqiXW_xy0c3d_rPHOvj5YQQHhbkx4Hf3Ttbc,
|
|
|
123
123
|
plain/server/http/parser.py,sha256=TLcqdRS9_008n8MpgLORmjomyZKLuKNzdKA8Cej8mII,1681
|
|
124
124
|
plain/server/http/unreader.py,sha256=jD2PGZ574FGmQOZlqWfs1VWzp-ttfIso_GzYaId6KYQ,2238
|
|
125
125
|
plain/server/http/wsgi.py,sha256=D-wVKdgKppDR_ZQd834yXtju60t1Jg5bFc5QEr7Iz38,13897
|
|
126
|
-
plain/server/workers/__init__.py,sha256=
|
|
127
|
-
plain/server/workers/base.py,sha256=
|
|
128
|
-
plain/server/workers/gthread.py,sha256=6F_YfhrlPV3hmxwI8_-jgGq4pCmkQBKVO15Wrg-iQ7Y,13504
|
|
126
|
+
plain/server/workers/__init__.py,sha256=sLq8nrIIf9Wjw5_qQsh6cnHnY7eIqCpzSedKrNmmn7s,145
|
|
127
|
+
plain/server/workers/base.py,sha256=B0aniofq4lT5gYa82W34VTLvEQInOe43QvuG9X14vdE,9602
|
|
129
128
|
plain/server/workers/sync.py,sha256=I28Icl1aKNIOlVaM76UOqQvjtSetkG-tdc5eiAvAmYA,7272
|
|
129
|
+
plain/server/workers/thread.py,sha256=6F_YfhrlPV3hmxwI8_-jgGq4pCmkQBKVO15Wrg-iQ7Y,13504
|
|
130
130
|
plain/server/workers/workertmp.py,sha256=egGReVvldlOBQfQGcpLpjt0zvPwR4C_N-UJKG-U_6w4,1299
|
|
131
131
|
plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
|
|
132
132
|
plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
|
|
@@ -188,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
|
|
188
188
|
plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
|
|
189
189
|
plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
|
|
190
190
|
plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
|
|
191
|
-
plain-0.
|
|
192
|
-
plain-0.
|
|
193
|
-
plain-0.
|
|
194
|
-
plain-0.
|
|
195
|
-
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,,
|
plain/server/reloader.py
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
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 os.path
|
|
11
|
-
import re
|
|
12
|
-
import sys
|
|
13
|
-
import threading
|
|
14
|
-
import time
|
|
15
|
-
from collections.abc import Callable, Iterable
|
|
16
|
-
|
|
17
|
-
COMPILED_EXT_RE = re.compile(r"py[co]$")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Reloader(threading.Thread):
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
extra_files: Iterable[str] | None = None,
|
|
24
|
-
interval: int = 1,
|
|
25
|
-
callback: Callable[[str], None] | None = None,
|
|
26
|
-
) -> None:
|
|
27
|
-
super().__init__()
|
|
28
|
-
self.daemon = True
|
|
29
|
-
self._extra_files: set[str] = set(extra_files or ())
|
|
30
|
-
self._interval = interval
|
|
31
|
-
self._callback = callback
|
|
32
|
-
|
|
33
|
-
def add_extra_file(self, filename: str) -> None:
|
|
34
|
-
self._extra_files.add(filename)
|
|
35
|
-
|
|
36
|
-
def get_files(self) -> list[str]:
|
|
37
|
-
fnames = [
|
|
38
|
-
COMPILED_EXT_RE.sub("py", module.__file__) # type: ignore[arg-type]
|
|
39
|
-
for module in tuple(sys.modules.values())
|
|
40
|
-
if getattr(module, "__file__", None)
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
fnames.extend(self._extra_files)
|
|
44
|
-
|
|
45
|
-
return fnames
|
|
46
|
-
|
|
47
|
-
def run(self) -> None:
|
|
48
|
-
mtimes: dict[str, float] = {}
|
|
49
|
-
while True:
|
|
50
|
-
for filename in self.get_files():
|
|
51
|
-
try:
|
|
52
|
-
mtime = os.stat(filename).st_mtime
|
|
53
|
-
except OSError:
|
|
54
|
-
continue
|
|
55
|
-
old_time = mtimes.get(filename)
|
|
56
|
-
if old_time is None:
|
|
57
|
-
mtimes[filename] = mtime
|
|
58
|
-
continue
|
|
59
|
-
elif mtime > old_time:
|
|
60
|
-
if self._callback:
|
|
61
|
-
self._callback(filename)
|
|
62
|
-
time.sleep(self._interval)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
has_inotify = False
|
|
66
|
-
if sys.platform.startswith("linux"):
|
|
67
|
-
try:
|
|
68
|
-
import inotify.constants
|
|
69
|
-
from inotify.adapters import Inotify
|
|
70
|
-
|
|
71
|
-
has_inotify = True
|
|
72
|
-
except ImportError:
|
|
73
|
-
pass
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if has_inotify:
|
|
77
|
-
|
|
78
|
-
class InotifyReloader(threading.Thread):
|
|
79
|
-
event_mask = (
|
|
80
|
-
inotify.constants.IN_CREATE
|
|
81
|
-
| inotify.constants.IN_DELETE
|
|
82
|
-
| inotify.constants.IN_DELETE_SELF
|
|
83
|
-
| inotify.constants.IN_MODIFY
|
|
84
|
-
| inotify.constants.IN_MOVE_SELF
|
|
85
|
-
| inotify.constants.IN_MOVED_FROM
|
|
86
|
-
| inotify.constants.IN_MOVED_TO
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
def __init__(
|
|
90
|
-
self,
|
|
91
|
-
extra_files: Iterable[str] | None = None,
|
|
92
|
-
callback: Callable[[str], None] | None = None,
|
|
93
|
-
) -> None:
|
|
94
|
-
super().__init__()
|
|
95
|
-
self.daemon = True
|
|
96
|
-
self._callback = callback
|
|
97
|
-
self._dirs: set[str] = set()
|
|
98
|
-
self._watcher = Inotify()
|
|
99
|
-
|
|
100
|
-
if extra_files:
|
|
101
|
-
for extra_file in extra_files:
|
|
102
|
-
self.add_extra_file(extra_file)
|
|
103
|
-
|
|
104
|
-
def add_extra_file(self, filename: str) -> None:
|
|
105
|
-
dirname = os.path.dirname(filename)
|
|
106
|
-
|
|
107
|
-
if dirname in self._dirs:
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
self._watcher.add_watch(dirname, mask=self.event_mask)
|
|
111
|
-
self._dirs.add(dirname)
|
|
112
|
-
|
|
113
|
-
def get_dirs(self) -> set[str]:
|
|
114
|
-
fnames = [
|
|
115
|
-
os.path.dirname(
|
|
116
|
-
os.path.abspath(COMPILED_EXT_RE.sub("py", module.__file__)) # type: ignore[arg-type]
|
|
117
|
-
)
|
|
118
|
-
for module in tuple(sys.modules.values())
|
|
119
|
-
if getattr(module, "__file__", None)
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
return set(fnames)
|
|
123
|
-
|
|
124
|
-
def run(self) -> None:
|
|
125
|
-
self._dirs = self.get_dirs()
|
|
126
|
-
|
|
127
|
-
for dirname in self._dirs:
|
|
128
|
-
if os.path.isdir(dirname):
|
|
129
|
-
self._watcher.add_watch(dirname, mask=self.event_mask)
|
|
130
|
-
|
|
131
|
-
for event in self._watcher.event_gen(): # type: ignore[attr-defined]
|
|
132
|
-
if event is None:
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
filename = event[3] # type: ignore[index]
|
|
136
|
-
|
|
137
|
-
self._callback(filename) # type: ignore[misc]
|
|
138
|
-
|
|
139
|
-
else:
|
|
140
|
-
|
|
141
|
-
class InotifyReloader:
|
|
142
|
-
def __init__(
|
|
143
|
-
self,
|
|
144
|
-
extra_files: Iterable[str] | None = None,
|
|
145
|
-
callback: Callable[[str], None] | None = None,
|
|
146
|
-
) -> None:
|
|
147
|
-
raise ImportError(
|
|
148
|
-
"You must have the inotify module installed to use the inotify reloader"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
preferred_reloader = InotifyReloader if has_inotify else Reloader
|
|
153
|
-
|
|
154
|
-
reloader_engines = {
|
|
155
|
-
"auto": preferred_reloader,
|
|
156
|
-
"poll": Reloader,
|
|
157
|
-
"inotify": InotifyReloader,
|
|
158
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|