Plain is headed towards 1.0! Subscribe for development updates →

plain.models

Model your data and store it in a database.

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


class User(models.Model):
    email = models.EmailField(unique=True)
    password = PasswordField()
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Create, update, and delete instances of your models:

from .models import User


# Create a new user
user = User.objects.create(
    email="[email protected]",
    password="password",
)

# Update a user
user.email = "[email protected]"
user.save()

# Delete a user
user.delete()

# Query for users
staff_users = User.objects.filter(is_staff=True)

Installation

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.models",
]

To connect to a database, you can provide a DATABASE_URL environment variable.

DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Or you can manually define the DATABASES setting.

# app/settings.py
DATABASES = {
    "default": {
        "ENGINE": "plain.models.backends.postgresql",
        "NAME": "dbname",
        "USER": "user",
        "PASSWORD": "password",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

Multiple backends are supported, including Postgres, MySQL, and SQLite.

Querying

Migrations

Migration docs

Fields

Field docs

Validation

Indexes and constraints

Managers

Forms

 1import functools
 2from collections import namedtuple
 3
 4
 5def make_model_tuple(model):
 6    """
 7    Take a model or a string of the form "package_label.ModelName" and return a
 8    corresponding ("package_label", "modelname") tuple. If a tuple is passed in,
 9    assume it's a valid model tuple already and return it unchanged.
10    """
11    try:
12        if isinstance(model, tuple):
13            model_tuple = model
14        elif isinstance(model, str):
15            package_label, model_name = model.split(".")
16            model_tuple = package_label, model_name.lower()
17        else:
18            model_tuple = model._meta.package_label, model._meta.model_name
19        assert len(model_tuple) == 2
20        return model_tuple
21    except (ValueError, AssertionError):
22        raise ValueError(
23            "Invalid model reference '%s'. String model references "
24            "must be of the form 'package_label.ModelName'." % model
25        )
26
27
28def resolve_callables(mapping):
29    """
30    Generate key/value pairs for the given mapping where the values are
31    evaluated if they're callable.
32    """
33    for k, v in mapping.items():
34        yield k, v() if callable(v) else v
35
36
37def unpickle_named_row(names, values):
38    return create_namedtuple_class(*names)(*values)
39
40
41@functools.lru_cache
42def create_namedtuple_class(*names):
43    # Cache type() with @lru_cache since it's too slow to be called for every
44    # QuerySet evaluation.
45    def __reduce__(self):
46        return unpickle_named_row, (names, tuple(self))
47
48    return type(
49        "Row",
50        (namedtuple("Row", names),),
51        {"__reduce__": __reduce__, "__slots__": ()},
52    )
53
54
55class AltersData:
56    """
57    Make subclasses preserve the alters_data attribute on overridden methods.
58    """
59
60    def __init_subclass__(cls, **kwargs):
61        for fn_name, fn in vars(cls).items():
62            if callable(fn) and not hasattr(fn, "alters_data"):
63                for base in cls.__bases__:
64                    if base_fn := getattr(base, fn_name, None):
65                        if hasattr(base_fn, "alters_data"):
66                            fn.alters_data = base_fn.alters_data
67                        break
68
69        super().__init_subclass__(**kwargs)