Plain is headed towards 1.0! Subscribe for development updates →

Runtime

Leverage user-settings at runtime.

Settings

Single-file

All of your settings go in app/settings.py. That's how you do it!

The file itself is not much different than how Django does it, but the location, and a strong recommendation to only use the one file makes a big difference.

Environment variables

It seems pretty well-accepted these days that storing settings in env vars is a good idea (12factor.net).

Your settings file should be looking at the environment for secrets or other values that might change between environments. For example:

# app/settings.py
STRIPE_SECRET_KEY = environ["STRIPE_SECRET_KEY"]

Local development

In local development, you should use .env files to set these values. The .env should then be in your .gitignore!

It would seem like .env.dev would be a good idea, but there's a chicken-and-egg problem with that. You would then have to prefix most (or all) of your local commands with PLAIN_ENV=dev or otherwise configure your environment to do that for you. Generally speaking, a production .env shouldn't be committed in your repo anyway, so using .env for local development is ok. The downside to this is that it's harder to share your local settings with others, but these also often contain real secrets which shouldn't be committed to your repo either! More advanced .env sharing patterns are currently beyond the scope of Plain...

Production

TODO

Minimum required settings

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = environ["SECRET_KEY"]

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = environ.get("DEBUG", "false").lower() in ("true", "1", "yes")

MIDDLEWARE = [
    "plain.sessions.middleware.SessionMiddleware",
    "plain.auth.middleware.AuthenticationMiddleware",
]

if DEBUG:
    INSTALLED_PACKAGES += [
        "plain.dev",
    ]
    MIDDLEWARE += [
        "plain.dev.RequestsMiddleware",
    ]

TIME_ZONE = "America/Chicago"
 1import importlib.metadata
 2import sys
 3from importlib.metadata import entry_points
 4from pathlib import Path
 5
 6from .user_settings import Settings
 7
 8try:
 9    __version__ = importlib.metadata.version("plain")
10except importlib.metadata.PackageNotFoundError:
11    __version__ = "dev"
12
13
14# Made available without setup or settings
15APP_PATH = Path.cwd() / "app"
16
17# from plain.runtime import settings
18settings = Settings()
19
20
21class AppPathNotFound(RuntimeError):
22    pass
23
24
25def setup():
26    """
27    Configure the settings (this happens as a side effect of accessing the
28    first setting), configure logging and populate the app registry.
29    """
30
31    # Packages can hook into the setup process through an entrypoint.
32    for entry_point in entry_points().select(group="plain.setup"):
33        entry_point.load()()
34
35    from plain.logs import configure_logging
36    from plain.packages import packages
37
38    if not APP_PATH.exists():
39        raise AppPathNotFound(
40            "No app directory found. Are you sure you're in a Plain project?"
41        )
42
43    # Automatically put the project dir on the Python path
44    # which doesn't otherwise happen when you run `plain` commands.
45    # This makes "app.<module>" imports and relative imports work.
46    if APP_PATH.parent not in sys.path:
47        sys.path.insert(0, APP_PATH.parent.as_posix())
48
49    configure_logging(settings.LOGGING)
50
51    packages.populate(settings.INSTALLED_PACKAGES)
52
53
54__all__ = [
55    "setup",
56    "settings",
57    "APP_PATH",
58    "__version__",
59]