plain.dev 0.5.0__tar.gz → 0.6.0__tar.gz
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_dev-0.5.0 → plain_dev-0.6.0}/PKG-INFO +1 -2
- plain_dev-0.6.0/plain/dev/cli.py +349 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/precommit/cli.py +3 -7
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/services.py +1 -5
- {plain_dev-0.5.0 → plain_dev-0.6.0}/pyproject.toml +1 -3
- plain_dev-0.5.0/plain/dev/cli.py +0 -164
- {plain_dev-0.5.0 → plain_dev-0.6.0}/LICENSE +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/README.md +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/README.md +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/__init__.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/config.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/contribute/__init__.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/contribute/cli.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/db/__init__.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/db/cli.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/db/container.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/debug.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/default_settings.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/pid.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/precommit/__init__.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/requests.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/templates/dev/requests.html +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/urls.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/utils.py +0 -0
- {plain_dev-0.5.0 → plain_dev-0.6.0}/plain/dev/views.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: plain.dev
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: Local development tools for Plain.
|
5
5
|
Home-page: https://plainframework.com
|
6
6
|
License: BSD-3-Clause
|
@@ -18,7 +18,6 @@ Requires-Dist: honcho (>=1.1.0,<2.0.0)
|
|
18
18
|
Requires-Dist: plain (<1.0.0)
|
19
19
|
Requires-Dist: psycopg[binary] (>=3.2.2,<4.0.0)
|
20
20
|
Requires-Dist: requests (>=2.0.0)
|
21
|
-
Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
|
22
21
|
Project-URL: Documentation, https://plainframework.com/docs/
|
23
22
|
Project-URL: Repository, https://github.com/dropseed/plain
|
24
23
|
Description-Content-Type: text/markdown
|
@@ -0,0 +1,349 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import platform
|
4
|
+
import shutil
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import urllib.request
|
8
|
+
from importlib.util import find_spec
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
import click
|
12
|
+
import tomllib
|
13
|
+
from honcho.manager import Manager as HonchoManager
|
14
|
+
|
15
|
+
from plain.runtime import APP_PATH, settings
|
16
|
+
|
17
|
+
from .db import cli as db_cli
|
18
|
+
from .pid import Pid
|
19
|
+
from .services import Services
|
20
|
+
from .utils import has_pyproject_toml, plainpackage_installed
|
21
|
+
|
22
|
+
|
23
|
+
@click.group(invoke_without_command=True)
|
24
|
+
@click.pass_context
|
25
|
+
@click.option(
|
26
|
+
"--port",
|
27
|
+
"-p",
|
28
|
+
default=8443,
|
29
|
+
type=int,
|
30
|
+
help="Port to run the web server on",
|
31
|
+
envvar="PORT",
|
32
|
+
)
|
33
|
+
def cli(ctx, port):
|
34
|
+
"""Start local development"""
|
35
|
+
|
36
|
+
if ctx.invoked_subcommand:
|
37
|
+
return
|
38
|
+
|
39
|
+
returncode = Dev(port=port).run()
|
40
|
+
if returncode:
|
41
|
+
sys.exit(returncode)
|
42
|
+
|
43
|
+
|
44
|
+
@cli.command()
|
45
|
+
def services():
|
46
|
+
"""Start additional services defined in pyproject.toml"""
|
47
|
+
Services().run()
|
48
|
+
|
49
|
+
|
50
|
+
class Dev:
|
51
|
+
def __init__(self, *, port):
|
52
|
+
self.manager = HonchoManager()
|
53
|
+
self.port = port
|
54
|
+
self.plain_env = {
|
55
|
+
**os.environ,
|
56
|
+
"PYTHONUNBUFFERED": "true",
|
57
|
+
}
|
58
|
+
self.custom_process_env = {
|
59
|
+
**self.plain_env,
|
60
|
+
"PORT": str(self.port),
|
61
|
+
"PYTHONPATH": os.path.join(APP_PATH.parent, "app"),
|
62
|
+
}
|
63
|
+
self.project_name = os.path.basename(os.getcwd())
|
64
|
+
self.domain = f"{self.project_name}.localhost"
|
65
|
+
|
66
|
+
# Paths for mkcert and certificates
|
67
|
+
self.mkcert_dir = Path.home() / ".plain" / "dev"
|
68
|
+
self.mkcert_bin = self.mkcert_dir / "mkcert"
|
69
|
+
self.certs_dir = (
|
70
|
+
Path(settings.PLAIN_TEMP_PATH) / "dev" / "certs"
|
71
|
+
) # Local project directory for certs
|
72
|
+
|
73
|
+
# Define certificate and key paths with clear filenames
|
74
|
+
self.cert_path = self.certs_dir / f"{self.domain}-cert.pem"
|
75
|
+
self.key_path = self.certs_dir / f"{self.domain}-key.pem"
|
76
|
+
|
77
|
+
def run(self):
|
78
|
+
pid = Pid()
|
79
|
+
pid.write()
|
80
|
+
|
81
|
+
try:
|
82
|
+
self.setup_mkcert()
|
83
|
+
self.generate_certs()
|
84
|
+
self.modify_hosts_file()
|
85
|
+
self.add_csrf_trusted_origins()
|
86
|
+
self.add_allowed_hosts()
|
87
|
+
self.run_preflight()
|
88
|
+
self.add_gunicorn()
|
89
|
+
self.add_tailwind()
|
90
|
+
self.add_pyproject_run()
|
91
|
+
self.add_services()
|
92
|
+
|
93
|
+
# Output the clickable link before starting the manager loop
|
94
|
+
url = f"https://{self.domain}:{self.port}/"
|
95
|
+
click.secho(
|
96
|
+
f"\nYour application is running at: {click.style(url, fg='green', underline=True)}\n",
|
97
|
+
bold=True,
|
98
|
+
)
|
99
|
+
|
100
|
+
self.manager.loop()
|
101
|
+
|
102
|
+
return self.manager.returncode
|
103
|
+
finally:
|
104
|
+
pid.rm()
|
105
|
+
|
106
|
+
def setup_mkcert(self):
|
107
|
+
"""Set up mkcert by checking if it's installed or downloading the binary and installing the local CA."""
|
108
|
+
if mkcert_path := shutil.which("mkcert"):
|
109
|
+
# mkcert is already installed somewhere
|
110
|
+
self.mkcert_bin = mkcert_path
|
111
|
+
else:
|
112
|
+
self.mkcert_dir.mkdir(parents=True, exist_ok=True)
|
113
|
+
if not self.mkcert_bin.exists():
|
114
|
+
system = platform.system()
|
115
|
+
arch = platform.machine()
|
116
|
+
|
117
|
+
# Map platform.machine() to mkcert's expected architecture strings
|
118
|
+
arch_map = {
|
119
|
+
"x86_64": "amd64",
|
120
|
+
"amd64": "amd64",
|
121
|
+
"AMD64": "amd64",
|
122
|
+
"arm64": "arm64",
|
123
|
+
"aarch64": "arm64",
|
124
|
+
}
|
125
|
+
arch = arch_map.get(
|
126
|
+
arch.lower(), "amd64"
|
127
|
+
) # Default to amd64 if unknown
|
128
|
+
|
129
|
+
if system == "Darwin":
|
130
|
+
os_name = "darwin"
|
131
|
+
elif system == "Linux":
|
132
|
+
os_name = "linux"
|
133
|
+
elif system == "Windows":
|
134
|
+
os_name = "windows"
|
135
|
+
else:
|
136
|
+
click.secho("Unsupported OS", fg="red")
|
137
|
+
sys.exit(1)
|
138
|
+
|
139
|
+
mkcert_url = f"https://dl.filippo.io/mkcert/latest?for={os_name}/{arch}"
|
140
|
+
click.secho(f"Downloading mkcert from {mkcert_url}...", bold=True)
|
141
|
+
urllib.request.urlretrieve(mkcert_url, self.mkcert_bin)
|
142
|
+
self.mkcert_bin.chmod(0o755)
|
143
|
+
self.mkcert_bin = str(self.mkcert_bin) # Convert Path object to string
|
144
|
+
|
145
|
+
if not self.is_mkcert_ca_installed():
|
146
|
+
click.secho(
|
147
|
+
"Installing mkcert local CA. You may be prompted for your password.",
|
148
|
+
bold=True,
|
149
|
+
)
|
150
|
+
subprocess.run([self.mkcert_bin, "-install"], check=True)
|
151
|
+
|
152
|
+
def is_mkcert_ca_installed(self):
|
153
|
+
"""Check if mkcert local CA is already installed using mkcert -check."""
|
154
|
+
try:
|
155
|
+
result = subprocess.run([self.mkcert_bin, "-check"], capture_output=True)
|
156
|
+
output = result.stdout.decode() + result.stderr.decode()
|
157
|
+
if "The local CA is not installed" in output:
|
158
|
+
return False
|
159
|
+
return True
|
160
|
+
except Exception as e:
|
161
|
+
click.secho(f"Error checking mkcert CA installation: {e}", fg="red")
|
162
|
+
return False
|
163
|
+
|
164
|
+
def generate_certs(self):
|
165
|
+
if self.cert_path.exists() and self.key_path.exists():
|
166
|
+
return
|
167
|
+
|
168
|
+
self.certs_dir.mkdir(parents=True, exist_ok=True)
|
169
|
+
|
170
|
+
# Generate SSL certificates using mkcert
|
171
|
+
click.secho(f"Generating SSL certificates for {self.domain}...", bold=True)
|
172
|
+
subprocess.run(
|
173
|
+
[
|
174
|
+
self.mkcert_bin,
|
175
|
+
"-cert-file",
|
176
|
+
str(self.cert_path),
|
177
|
+
"-key-file",
|
178
|
+
str(self.key_path),
|
179
|
+
self.domain,
|
180
|
+
],
|
181
|
+
check=True,
|
182
|
+
)
|
183
|
+
|
184
|
+
def modify_hosts_file(self):
|
185
|
+
"""Modify the hosts file to map the custom domain to 127.0.0.1."""
|
186
|
+
entry_identifier = "# Added by plain"
|
187
|
+
hosts_entry = f"127.0.0.1 {self.domain} {entry_identifier}"
|
188
|
+
|
189
|
+
if platform.system() == "Windows":
|
190
|
+
hosts_path = Path(r"C:\Windows\System32\drivers\etc\hosts")
|
191
|
+
try:
|
192
|
+
with hosts_path.open("r") as f:
|
193
|
+
content = f.read()
|
194
|
+
|
195
|
+
if hosts_entry in content:
|
196
|
+
return # Entry already exists; no action needed
|
197
|
+
|
198
|
+
# Entry does not exist; add it
|
199
|
+
with hosts_path.open("a") as f:
|
200
|
+
f.write(f"{hosts_entry}\n")
|
201
|
+
click.secho(f"Added {self.domain} to {hosts_path}", bold=True)
|
202
|
+
except PermissionError:
|
203
|
+
click.secho(
|
204
|
+
"Permission denied while modifying hosts file. Please run the script as an administrator.",
|
205
|
+
fg="red",
|
206
|
+
)
|
207
|
+
sys.exit(1)
|
208
|
+
else:
|
209
|
+
# For macOS and Linux
|
210
|
+
hosts_path = Path("/etc/hosts")
|
211
|
+
try:
|
212
|
+
with hosts_path.open("r") as f:
|
213
|
+
content = f.read()
|
214
|
+
|
215
|
+
if hosts_entry in content:
|
216
|
+
return # Entry already exists; no action needed
|
217
|
+
|
218
|
+
# Entry does not exist; append it using sudo
|
219
|
+
click.secho(
|
220
|
+
"Modifying /etc/hosts file. You may be prompted for your password.",
|
221
|
+
bold=True,
|
222
|
+
)
|
223
|
+
cmd = f"echo '{hosts_entry}' | sudo tee -a {hosts_path} >/dev/null"
|
224
|
+
subprocess.run(cmd, shell=True, check=True)
|
225
|
+
click.secho(f"Added {self.domain} to {hosts_path}", bold=True)
|
226
|
+
except PermissionError:
|
227
|
+
click.secho(
|
228
|
+
"Permission denied while accessing hosts file.",
|
229
|
+
fg="red",
|
230
|
+
)
|
231
|
+
sys.exit(1)
|
232
|
+
except subprocess.CalledProcessError:
|
233
|
+
click.secho(
|
234
|
+
"Failed to modify hosts file. Please ensure you have sudo privileges.",
|
235
|
+
fg="red",
|
236
|
+
)
|
237
|
+
sys.exit(1)
|
238
|
+
|
239
|
+
def add_csrf_trusted_origins(self):
|
240
|
+
csrf_trusted_origins = json.dumps(
|
241
|
+
[
|
242
|
+
f"https://{self.domain}:{self.port}",
|
243
|
+
]
|
244
|
+
)
|
245
|
+
|
246
|
+
click.secho(
|
247
|
+
f"Automatically set PLAIN_CSRF_TRUSTED_ORIGINS={click.style(csrf_trusted_origins, underline=True)}",
|
248
|
+
bold=True,
|
249
|
+
)
|
250
|
+
|
251
|
+
# Set environment variables
|
252
|
+
self.plain_env["PLAIN_CSRF_TRUSTED_ORIGINS"] = csrf_trusted_origins
|
253
|
+
self.custom_process_env["PLAIN_CSRF_TRUSTED_ORIGINS"] = csrf_trusted_origins
|
254
|
+
|
255
|
+
def add_allowed_hosts(self):
|
256
|
+
allowed_hosts = json.dumps([self.domain])
|
257
|
+
|
258
|
+
click.secho(
|
259
|
+
f"Automatically set PLAIN_ALLOWED_HOSTS={click.style(allowed_hosts, underline=True)}",
|
260
|
+
bold=True,
|
261
|
+
)
|
262
|
+
|
263
|
+
# Set environment variables
|
264
|
+
self.plain_env["PLAIN_ALLOWED_HOSTS"] = allowed_hosts
|
265
|
+
self.custom_process_env["PLAIN_ALLOWED_HOSTS"] = allowed_hosts
|
266
|
+
|
267
|
+
def run_preflight(self):
|
268
|
+
if subprocess.run(["plain", "preflight"], env=self.plain_env).returncode:
|
269
|
+
click.secho("Preflight check failed!", fg="red")
|
270
|
+
sys.exit(1)
|
271
|
+
|
272
|
+
def add_gunicorn(self):
|
273
|
+
plain_db_installed = find_spec("plain.models") is not None
|
274
|
+
|
275
|
+
# Watch .env files for reload
|
276
|
+
extra_watch_files = []
|
277
|
+
for f in os.listdir(APP_PATH.parent):
|
278
|
+
if f.startswith(".env"):
|
279
|
+
extra_watch_files.append(f)
|
280
|
+
|
281
|
+
reload_extra = " ".join(f"--reload-extra-file {f}" for f in extra_watch_files)
|
282
|
+
gunicorn_cmd = [
|
283
|
+
"gunicorn",
|
284
|
+
"--bind",
|
285
|
+
f"{self.domain}:{self.port}",
|
286
|
+
"--certfile",
|
287
|
+
str(self.cert_path),
|
288
|
+
"--keyfile",
|
289
|
+
str(self.key_path),
|
290
|
+
"--reload",
|
291
|
+
"plain.wsgi:app",
|
292
|
+
"--timeout",
|
293
|
+
"60",
|
294
|
+
"--access-logfile",
|
295
|
+
"-",
|
296
|
+
"--error-logfile",
|
297
|
+
"-",
|
298
|
+
*reload_extra.split(),
|
299
|
+
"--access-logformat",
|
300
|
+
"'\"%(r)s\" status=%(s)s length=%(b)s dur=%(M)sms'",
|
301
|
+
]
|
302
|
+
gunicorn = " ".join(gunicorn_cmd)
|
303
|
+
|
304
|
+
if plain_db_installed:
|
305
|
+
runserver_cmd = f"plain models db-wait && plain migrate && {gunicorn}"
|
306
|
+
else:
|
307
|
+
runserver_cmd = gunicorn
|
308
|
+
|
309
|
+
if "WEB_CONCURRENCY" not in self.plain_env:
|
310
|
+
# Default to two workers to prevent lockups
|
311
|
+
self.plain_env["WEB_CONCURRENCY"] = "2"
|
312
|
+
|
313
|
+
self.manager.add_process("plain", runserver_cmd, env=self.plain_env)
|
314
|
+
|
315
|
+
def add_tailwind(self):
|
316
|
+
if not plainpackage_installed("tailwind"):
|
317
|
+
return
|
318
|
+
|
319
|
+
self.manager.add_process("tailwind", "plain tailwind compile --watch")
|
320
|
+
|
321
|
+
def add_pyproject_run(self):
|
322
|
+
if not has_pyproject_toml(APP_PATH.parent):
|
323
|
+
return
|
324
|
+
|
325
|
+
with open(Path(APP_PATH.parent, "pyproject.toml"), "rb") as f:
|
326
|
+
pyproject = tomllib.load(f)
|
327
|
+
|
328
|
+
run_commands = (
|
329
|
+
pyproject.get("tool", {}).get("plain", {}).get("dev", {}).get("run", {})
|
330
|
+
)
|
331
|
+
for name, data in run_commands.items():
|
332
|
+
env = {
|
333
|
+
**self.custom_process_env,
|
334
|
+
**data.get("env", {}),
|
335
|
+
}
|
336
|
+
self.manager.add_process(name, data["cmd"], env=env)
|
337
|
+
|
338
|
+
def add_services(self):
|
339
|
+
services = Services.get_services(APP_PATH.parent)
|
340
|
+
for name, data in services.items():
|
341
|
+
env = {
|
342
|
+
**os.environ,
|
343
|
+
"PYTHONUNBUFFERED": "true",
|
344
|
+
**data.get("env", {}),
|
345
|
+
}
|
346
|
+
self.manager.add_process(name, data["cmd"], env=env)
|
347
|
+
|
348
|
+
|
349
|
+
cli.add_command(db_cli)
|
@@ -4,17 +4,13 @@ import sys
|
|
4
4
|
from importlib.util import find_spec
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
|
+
import click
|
8
|
+
import tomllib
|
9
|
+
|
7
10
|
from plain.cli.print import print_event
|
8
11
|
|
9
12
|
from ..services import Services
|
10
13
|
|
11
|
-
try:
|
12
|
-
import tomllib
|
13
|
-
except ModuleNotFoundError:
|
14
|
-
import tomli as tomllib
|
15
|
-
|
16
|
-
import click
|
17
|
-
|
18
14
|
|
19
15
|
def install_git_hook():
|
20
16
|
hook_path = os.path.join(".git", "hooks", "pre-commit")
|
@@ -5,6 +5,7 @@ from importlib.util import find_spec
|
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
7
|
import click
|
8
|
+
import tomllib
|
8
9
|
from honcho.manager import Manager as HonchoManager
|
9
10
|
|
10
11
|
from plain.runtime import APP_PATH
|
@@ -12,11 +13,6 @@ from plain.runtime import APP_PATH
|
|
12
13
|
from .pid import Pid
|
13
14
|
from .utils import has_pyproject_toml
|
14
15
|
|
15
|
-
try:
|
16
|
-
import tomllib
|
17
|
-
except ModuleNotFoundError:
|
18
|
-
import tomli as tomllib
|
19
|
-
|
20
16
|
|
21
17
|
class Services:
|
22
18
|
@staticmethod
|
@@ -5,7 +5,7 @@ packages = [
|
|
5
5
|
{ include = "plain" },
|
6
6
|
]
|
7
7
|
|
8
|
-
version = "0.
|
8
|
+
version = "0.6.0"
|
9
9
|
description = "Local development tools for Plain."
|
10
10
|
authors = ["Dave Gaeddert <dave.gaeddert@dropseed.dev>"]
|
11
11
|
license = "BSD-3-Clause"
|
@@ -32,8 +32,6 @@ debugpy = "^1.6.3"
|
|
32
32
|
# For local runserver
|
33
33
|
gunicorn = ">20"
|
34
34
|
|
35
|
-
tomli = {version = "^2.0.1", python = "<3.11"}
|
36
|
-
|
37
35
|
# db
|
38
36
|
requests = ">=2.0.0"
|
39
37
|
psycopg = {version = "^3.2.2", extras = ["binary"]}
|
plain_dev-0.5.0/plain/dev/cli.py
DELETED
@@ -1,164 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
import subprocess
|
4
|
-
import sys
|
5
|
-
from importlib.util import find_spec
|
6
|
-
from pathlib import Path
|
7
|
-
|
8
|
-
import click
|
9
|
-
from honcho.manager import Manager as HonchoManager
|
10
|
-
|
11
|
-
from plain.runtime import APP_PATH
|
12
|
-
|
13
|
-
from .db import cli as db_cli
|
14
|
-
from .pid import Pid
|
15
|
-
from .services import Services
|
16
|
-
from .utils import has_pyproject_toml, plainpackage_installed
|
17
|
-
|
18
|
-
try:
|
19
|
-
import tomllib
|
20
|
-
except ModuleNotFoundError:
|
21
|
-
import tomli as tomllib
|
22
|
-
|
23
|
-
|
24
|
-
@click.group(invoke_without_command=True)
|
25
|
-
@click.pass_context
|
26
|
-
@click.option(
|
27
|
-
"--port",
|
28
|
-
"-p",
|
29
|
-
default=8000,
|
30
|
-
type=int,
|
31
|
-
help="Port to run the web server on",
|
32
|
-
envvar="PORT",
|
33
|
-
)
|
34
|
-
def cli(ctx, port):
|
35
|
-
"""Start local development"""
|
36
|
-
|
37
|
-
if ctx.invoked_subcommand:
|
38
|
-
return
|
39
|
-
|
40
|
-
returncode = Dev(port=port).run()
|
41
|
-
if returncode:
|
42
|
-
sys.exit(returncode)
|
43
|
-
|
44
|
-
|
45
|
-
@cli.command()
|
46
|
-
def services():
|
47
|
-
"""Start additional services defined in pyproject.toml"""
|
48
|
-
Services().run()
|
49
|
-
|
50
|
-
|
51
|
-
class Dev:
|
52
|
-
def __init__(self, *, port):
|
53
|
-
self.manager = HonchoManager()
|
54
|
-
self.port = port
|
55
|
-
self.plain_env = {
|
56
|
-
**os.environ,
|
57
|
-
"PYTHONUNBUFFERED": "true",
|
58
|
-
}
|
59
|
-
self.custom_process_env = {
|
60
|
-
**self.plain_env,
|
61
|
-
"PORT": str(self.port),
|
62
|
-
"PYTHONPATH": os.path.join(APP_PATH.parent, "app"),
|
63
|
-
}
|
64
|
-
|
65
|
-
def run(self):
|
66
|
-
pid = Pid()
|
67
|
-
pid.write()
|
68
|
-
|
69
|
-
try:
|
70
|
-
self.add_csrf_trusted_origins()
|
71
|
-
self.run_preflight()
|
72
|
-
self.add_gunicorn()
|
73
|
-
self.add_tailwind()
|
74
|
-
self.add_pyproject_run()
|
75
|
-
self.add_services()
|
76
|
-
|
77
|
-
self.manager.loop()
|
78
|
-
|
79
|
-
return self.manager.returncode
|
80
|
-
finally:
|
81
|
-
pid.rm()
|
82
|
-
|
83
|
-
def add_csrf_trusted_origins(self):
|
84
|
-
if "PLAIN_CSRF_TRUSTED_ORIGINS" in os.environ:
|
85
|
-
return
|
86
|
-
|
87
|
-
csrf_trusted_origins = json.dumps(
|
88
|
-
[f"http://localhost:{self.port}", f"http://127.0.0.1:{self.port}"]
|
89
|
-
)
|
90
|
-
|
91
|
-
click.secho(
|
92
|
-
f"Automatically set PLAIN_CSRF_TRUSTED_ORIGINS={click.style(csrf_trusted_origins, underline=True)}",
|
93
|
-
bold=True,
|
94
|
-
)
|
95
|
-
|
96
|
-
# Set BASE_URL for plain and custom processes
|
97
|
-
self.plain_env["PLAIN_CSRF_TRUSTED_ORIGINS"] = csrf_trusted_origins
|
98
|
-
self.custom_process_env["PLAIN_CSRF_TRUSTED_ORIGINS"] = csrf_trusted_origins
|
99
|
-
|
100
|
-
def run_preflight(self):
|
101
|
-
if subprocess.run(["plain", "preflight"], env=self.plain_env).returncode:
|
102
|
-
click.secho("Preflight check failed!", fg="red")
|
103
|
-
sys.exit(1)
|
104
|
-
|
105
|
-
def add_gunicorn(self):
|
106
|
-
plain_db_installed = find_spec("plain.models") is not None
|
107
|
-
|
108
|
-
# TODO not necessarily watching the right .env...
|
109
|
-
# could return path from env.load?
|
110
|
-
extra_watch_files = []
|
111
|
-
for f in os.listdir(APP_PATH.parent):
|
112
|
-
if f.startswith(".env"):
|
113
|
-
# Will include some extra, but good enough for now
|
114
|
-
extra_watch_files.append(f)
|
115
|
-
|
116
|
-
reload_extra = " ".join(f"--reload-extra-file {f}" for f in extra_watch_files)
|
117
|
-
gunicorn = f"gunicorn --bind 127.0.0.1:{self.port} --reload plain.wsgi:app --timeout 60 --access-logfile - --error-logfile - {reload_extra} --access-logformat '\"%(r)s\" status=%(s)s length=%(b)s dur=%(M)sms'"
|
118
|
-
|
119
|
-
if plain_db_installed:
|
120
|
-
runserver_cmd = f"plain models db-wait && plain migrate && {gunicorn}"
|
121
|
-
else:
|
122
|
-
runserver_cmd = gunicorn
|
123
|
-
|
124
|
-
if "WEB_CONCURRENCY" not in self.plain_env:
|
125
|
-
# Default to two workers so request log etc are less
|
126
|
-
# likely to get locked up
|
127
|
-
self.plain_env["WEB_CONCURRENCY"] = "2"
|
128
|
-
|
129
|
-
self.manager.add_process("plain", runserver_cmd, env=self.plain_env)
|
130
|
-
|
131
|
-
def add_tailwind(self):
|
132
|
-
if not plainpackage_installed("tailwind"):
|
133
|
-
return
|
134
|
-
|
135
|
-
self.manager.add_process("tailwind", "plain tailwind compile --watch")
|
136
|
-
|
137
|
-
def add_pyproject_run(self):
|
138
|
-
if not has_pyproject_toml(APP_PATH.parent):
|
139
|
-
return
|
140
|
-
|
141
|
-
with open(Path(APP_PATH.parent, "pyproject.toml"), "rb") as f:
|
142
|
-
pyproject = tomllib.load(f)
|
143
|
-
|
144
|
-
for name, data in (
|
145
|
-
pyproject.get("tool", {}).get("plain", {}).get("dev", {}).get("run", {})
|
146
|
-
).items():
|
147
|
-
env = {
|
148
|
-
**self.custom_process_env,
|
149
|
-
**data.get("env", {}),
|
150
|
-
}
|
151
|
-
self.manager.add_process(name, data["cmd"], env=env)
|
152
|
-
|
153
|
-
def add_services(self):
|
154
|
-
services = Services.get_services(APP_PATH.parent)
|
155
|
-
for name, data in services.items():
|
156
|
-
env = {
|
157
|
-
**os.environ,
|
158
|
-
"PYTHONUNBUFFERED": "true",
|
159
|
-
**data.get("env", {}),
|
160
|
-
}
|
161
|
-
self.manager.add_process(name, data["cmd"], env=env)
|
162
|
-
|
163
|
-
|
164
|
-
cli.add_command(db_cli)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|