plain.dev
A single command that runs everything you need for local development.
The plain.dev
package can be installed from PyPI, and does not need to be added to INSTALLED_PACKAGES
.
plain dev
The plain dev
command does several things:
- Sets
PLAIN_CSRF_TRUSTED_ORIGINS
to localhost by default - Runs
plain preflight
to check for any issues - Executes any pending model migrations
- Starts
gunicorn
with--reload
- Runs
plain tailwind compile --watch
, ifplain.tailwind
is installed - Any custom process defined in
pyproject.toml
attool.plain.dev.run
- Necessary services (ex. Postgres) defined in
pyproject.toml
attool.plain.dev.services
Services
Use services to define databases or other processes that your app needs to be functional. The services will be started automatically in plain dev
, but also in plain pre-commit
(so preflight and tests have a database).
Ultimately, how you run your development database is up to you. But a recommended starting point is to use Docker:
# pyproject.toml
[tool.plain.dev.services]
postgres = {cmd = "docker run --name app-postgres --rm -p 54321:5432 -v $(pwd)/.plain/dev/pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=postgres postgres:15 postgres"}
Custom processes
Unlike services, custom processes are only run during plain dev
. This is a good place to run something like ngrok or a Plain worker, which you might need to use your local site, but don't need running for executing tests, for example.
# pyproject.toml
[tool.plain.dev.run]
ngrok = {command = "ngrok http $PORT"}
plain dev services
Starts your services by themselves.
plain pre-commit
A built-in pre-commit hook that can be installed with plain pre-commit --install
.
Runs:
- Custom commands defined in
pyproject.toml
attool.plain.pre-commit.run
plain code check
, ifplain.code
is installeduv lock --locked
, if using uvplain preflight --database default
plain migrate --check
plain makemigrations --dry-run --check
plain compile
plain test
VS Code debugging
Since plain dev
runs multiple processes at once, the regular pdb debuggers don't quite work.
Instead, we include microsoft/debugpy and an attach
function to make it even easier to use VS Code's debugger.
First, import and run the debug.attach()
function:
class HomeView(TemplateView):
template_name = "home.html"
def get_template_context(self):
context = super().get_template_context()
# Make sure the debugger is attached (will need to be if runserver reloads)
from plain.dev import debug; debug.attach()
# Add a breakpoint (or use the gutter in VS Code to add one)
breakpoint()
return context
When you load the page, you'll see "Waiting for debugger to attach...".
You can then run the VS Code debugger and attach to an existing Python process, at localhost:5678.
1import platform
2import shutil
3import subprocess
4import sys
5import urllib.request
6
7import click
8
9
10class MkcertManager:
11 def __init__(self):
12 self.mkcert_bin = None
13
14 def setup_mkcert(self, install_path):
15 """Set up mkcert by checking if it's installed or downloading the binary and installing the local CA."""
16 if mkcert_path := shutil.which("mkcert"):
17 # mkcert is already installed somewhere
18 self.mkcert_bin = mkcert_path
19 else:
20 self.mkcert_bin = install_path / "mkcert"
21 install_path.mkdir(parents=True, exist_ok=True)
22 if not self.mkcert_bin.exists():
23 system = platform.system()
24 arch = platform.machine()
25
26 # Map platform.machine() to mkcert's expected architecture strings
27 arch_map = {
28 "x86_64": "amd64",
29 "amd64": "amd64",
30 "AMD64": "amd64",
31 "arm64": "arm64",
32 "aarch64": "arm64",
33 }
34 arch = arch_map.get(
35 arch.lower(), "amd64"
36 ) # Default to amd64 if unknown
37
38 if system == "Darwin":
39 os_name = "darwin"
40 elif system == "Linux":
41 os_name = "linux"
42 elif system == "Windows":
43 os_name = "windows"
44 else:
45 click.secho("Unsupported OS", fg="red")
46 sys.exit(1)
47
48 mkcert_url = f"https://dl.filippo.io/mkcert/latest?for={os_name}/{arch}"
49 click.secho(f"Downloading mkcert from {mkcert_url}...", bold=True)
50 urllib.request.urlretrieve(mkcert_url, self.mkcert_bin)
51 self.mkcert_bin.chmod(0o755)
52 self.mkcert_bin = str(self.mkcert_bin) # Convert Path object to string
53
54 if not self.is_mkcert_ca_installed():
55 click.secho(
56 "Installing mkcert local CA. You may be prompted for your password.",
57 bold=True,
58 )
59 subprocess.run([self.mkcert_bin, "-install"], check=True)
60
61 def is_mkcert_ca_installed(self):
62 """Check if mkcert local CA is already installed using mkcert -check."""
63 try:
64 result = subprocess.run([self.mkcert_bin, "-check"], capture_output=True)
65 output = result.stdout.decode() + result.stderr.decode()
66 if "The local CA is not installed" in output:
67 return False
68 return True
69 except Exception as e:
70 click.secho(f"Error checking mkcert CA installation: {e}", fg="red")
71 return False
72
73 def generate_certs(self, domain, storage_path):
74 cert_path = storage_path / f"{domain}-cert.pem"
75 key_path = storage_path / f"{domain}-key.pem"
76
77 if cert_path.exists() and key_path.exists():
78 return cert_path, key_path
79
80 storage_path.mkdir(parents=True, exist_ok=True)
81
82 # Generate SSL certificates using mkcert
83 click.secho(f"Generating SSL certificates for {domain}...", bold=True)
84 subprocess.run(
85 [
86 self.mkcert_bin,
87 "-cert-file",
88 str(cert_path),
89 "-key-file",
90 str(key_path),
91 domain,
92 ],
93 check=True,
94 )
95
96 return cert_path, key_path