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 pkgutil
  2from importlib import import_module
  3
  4from plain import signals
  5from plain.exceptions import ImproperlyConfigured
  6from plain.runtime import settings
  7from plain.utils.connection import BaseConnectionHandler, ConnectionProxy
  8from plain.utils.functional import cached_property
  9from plain.utils.module_loading import import_string
 10
 11DEFAULT_DB_ALIAS = "default"
 12PLAIN_VERSION_PICKLE_KEY = "_plain_version"
 13
 14
 15class Error(Exception):
 16    pass
 17
 18
 19class InterfaceError(Error):
 20    pass
 21
 22
 23class DatabaseError(Error):
 24    pass
 25
 26
 27class DataError(DatabaseError):
 28    pass
 29
 30
 31class OperationalError(DatabaseError):
 32    pass
 33
 34
 35class IntegrityError(DatabaseError):
 36    pass
 37
 38
 39class InternalError(DatabaseError):
 40    pass
 41
 42
 43class ProgrammingError(DatabaseError):
 44    pass
 45
 46
 47class NotSupportedError(DatabaseError):
 48    pass
 49
 50
 51class DatabaseErrorWrapper:
 52    """
 53    Context manager and decorator that reraises backend-specific database
 54    exceptions using Plain's common wrappers.
 55    """
 56
 57    def __init__(self, wrapper):
 58        """
 59        wrapper is a database wrapper.
 60
 61        It must have a Database attribute defining PEP-249 exceptions.
 62        """
 63        self.wrapper = wrapper
 64
 65    def __enter__(self):
 66        pass
 67
 68    def __exit__(self, exc_type, exc_value, traceback):
 69        if exc_type is None:
 70            return
 71        for plain_exc_type in (
 72            DataError,
 73            OperationalError,
 74            IntegrityError,
 75            InternalError,
 76            ProgrammingError,
 77            NotSupportedError,
 78            DatabaseError,
 79            InterfaceError,
 80            Error,
 81        ):
 82            db_exc_type = getattr(self.wrapper.Database, plain_exc_type.__name__)
 83            if issubclass(exc_type, db_exc_type):
 84                plain_exc_value = plain_exc_type(*exc_value.args)
 85                # Only set the 'errors_occurred' flag for errors that may make
 86                # the connection unusable.
 87                if plain_exc_type not in (DataError, IntegrityError):
 88                    self.wrapper.errors_occurred = True
 89                raise plain_exc_value.with_traceback(traceback) from exc_value
 90
 91    def __call__(self, func):
 92        # Note that we are intentionally not using @wraps here for performance
 93        # reasons. Refs #21109.
 94        def inner(*args, **kwargs):
 95            with self:
 96                return func(*args, **kwargs)
 97
 98        return inner
 99
100
101def load_backend(backend_name):
102    """
103    Return a database backend's "base" module given a fully qualified database
104    backend name, or raise an error if it doesn't exist.
105    """
106    try:
107        return import_module("%s.base" % backend_name)
108    except ImportError as e_user:
109        # The database backend wasn't found. Display a helpful error message
110        # listing all built-in database backends.
111        import plain.models.backends
112
113        builtin_backends = [
114            name
115            for _, name, ispkg in pkgutil.iter_modules(plain.models.backends.__path__)
116            if ispkg and name not in {"base", "dummy"}
117        ]
118        if backend_name not in [
119            "plain.models.backends.%s" % b for b in builtin_backends
120        ]:
121            backend_reprs = map(repr, sorted(builtin_backends))
122            raise ImproperlyConfigured(
123                "{!r} isn't an available database backend or couldn't be "
124                "imported. Check the above exception. To use one of the "
125                "built-in backends, use 'plain.models.backends.XXX', where XXX "
126                "is one of:\n"
127                "    {}".format(backend_name, ", ".join(backend_reprs))
128            ) from e_user
129        else:
130            # If there's some other error, this must be an error in Plain
131            raise
132
133
134class ConnectionHandler(BaseConnectionHandler):
135    settings_name = "DATABASES"
136
137    def configure_settings(self, databases):
138        databases = super().configure_settings(databases)
139        if databases == {}:
140            databases[DEFAULT_DB_ALIAS] = {"ENGINE": "plain.models.backends.dummy"}
141        elif DEFAULT_DB_ALIAS not in databases:
142            raise ImproperlyConfigured(
143                f"You must define a '{DEFAULT_DB_ALIAS}' database."
144            )
145        elif databases[DEFAULT_DB_ALIAS] == {}:
146            databases[DEFAULT_DB_ALIAS]["ENGINE"] = "plain.models.backends.dummy"
147
148        # Configure default settings.
149        for conn in databases.values():
150            conn.setdefault("AUTOCOMMIT", True)
151            conn.setdefault("ENGINE", "plain.models.backends.dummy")
152            if conn["ENGINE"] == "plain.models.backends." or not conn["ENGINE"]:
153                conn["ENGINE"] = "plain.models.backends.dummy"
154            conn.setdefault("CONN_MAX_AGE", 0)
155            conn.setdefault("CONN_HEALTH_CHECKS", False)
156            conn.setdefault("OPTIONS", {})
157            conn.setdefault("TIME_ZONE", None)
158            for setting in ["NAME", "USER", "PASSWORD", "HOST", "PORT"]:
159                conn.setdefault(setting, "")
160
161            test_settings = conn.setdefault("TEST", {})
162            default_test_settings = [
163                ("CHARSET", None),
164                ("COLLATION", None),
165                ("MIGRATE", True),
166                ("MIRROR", None),
167                ("NAME", None),
168            ]
169            for key, value in default_test_settings:
170                test_settings.setdefault(key, value)
171        return databases
172
173    @property
174    def databases(self):
175        # Maintained for backward compatibility as some 3rd party packages have
176        # made use of this private API in the past. It is no longer used within
177        # Plain itself.
178        return self.settings
179
180    def create_connection(self, alias):
181        db = self.settings[alias]
182        backend = load_backend(db["ENGINE"])
183        return backend.DatabaseWrapper(db, alias)
184
185
186class ConnectionRouter:
187    def __init__(self, routers=None):
188        """
189        If routers is not specified, default to settings.DATABASE_ROUTERS.
190        """
191        self._routers = routers
192
193    @cached_property
194    def routers(self):
195        if self._routers is None:
196            self._routers = settings.DATABASE_ROUTERS
197        routers = []
198        for r in self._routers:
199            if isinstance(r, str):
200                router = import_string(r)()
201            else:
202                router = r
203            routers.append(router)
204        return routers
205
206    def _router_func(action):
207        def _route_db(self, model, **hints):
208            chosen_db = None
209            for router in self.routers:
210                try:
211                    method = getattr(router, action)
212                except AttributeError:
213                    # If the router doesn't have a method, skip to the next one.
214                    pass
215                else:
216                    chosen_db = method(model, **hints)
217                    if chosen_db:
218                        return chosen_db
219            instance = hints.get("instance")
220            if instance is not None and instance._state.db:
221                return instance._state.db
222            return DEFAULT_DB_ALIAS
223
224        return _route_db
225
226    db_for_read = _router_func("db_for_read")
227    db_for_write = _router_func("db_for_write")
228
229    def allow_relation(self, obj1, obj2, **hints):
230        for router in self.routers:
231            try:
232                method = router.allow_relation
233            except AttributeError:
234                # If the router doesn't have a method, skip to the next one.
235                pass
236            else:
237                allow = method(obj1, obj2, **hints)
238                if allow is not None:
239                    return allow
240        return obj1._state.db == obj2._state.db
241
242    def allow_migrate(self, db, package_label, **hints):
243        for router in self.routers:
244            try:
245                method = router.allow_migrate
246            except AttributeError:
247                # If the router doesn't have a method, skip to the next one.
248                continue
249
250            allow = method(db, package_label, **hints)
251
252            if allow is not None:
253                return allow
254        return True
255
256    def allow_migrate_model(self, db, model):
257        return self.allow_migrate(
258            db,
259            model._meta.package_label,
260            model_name=model._meta.model_name,
261            model=model,
262        )
263
264    def get_migratable_models(self, package_config, db, include_auto_created=False):
265        """Return app models allowed to be migrated on provided db."""
266        models = package_config.get_models(include_auto_created=include_auto_created)
267        return [model for model in models if self.allow_migrate_model(db, model)]
268
269
270connections = ConnectionHandler()
271
272router = ConnectionRouter()
273
274# For backwards compatibility. Prefer connections['default'] instead.
275connection = ConnectionProxy(connections, DEFAULT_DB_ALIAS)
276
277
278# Register an event to reset saved queries when a Plain request is started.
279def reset_queries(**kwargs):
280    for conn in connections.all(initialized_only=True):
281        conn.queries_log.clear()
282
283
284signals.request_started.connect(reset_queries)
285
286
287# Register an event to reset transaction state and close connections past
288# their lifetime.
289def close_old_connections(**kwargs):
290    for conn in connections.all(initialized_only=True):
291        conn.close_if_unusable_or_obsolete()
292
293
294signals.request_started.connect(close_old_connections)
295signals.request_finished.connect(close_old_connections)