Models

A clearskies model.

Overview

To be useable, a model class needs four things:

  1. The name of the id column
  2. A backend
  3. A destination name (equivalent to a table name for SQL backends)
  4. Columns

In more detail:

Id Column Name

clearskies assumes that all models have a column that uniquely identifies each record. This id column is provided where appropriate in the lifecycle of the model save process to help connect and find related records. It’s defined as a simple class attribute called id_column_name. There MUST be a column with the same name in the column definitions. A simple approach to take is to use the Uuid column as an id column. This will automatically provide a random UUID when the record is first created. If you are using auto-incrementing integers, you can simply use an Int column type and define the column as auto-incrementing in your database.

Backend

Every model needs a backend, which is an object that extends clearskies.Backend and is attached to the backend attribute of the model class. clearskies comes with a variety of backends in the clearskies.backends module that you can use, and you can also define your own or import more from additional packages.

Destination Name

The destination name is the equivalent of a table name in other frameworks, but the name is more generic to reflect the fact that clearskies is intended to work with a variety of backends - not just SQL databases. The exact meaning of the destination name depends on the backend: for a cursor backend it is in fact used as the table name when fetching/storing records. For the API backend it is frequently appended to a base URL to reach the corect endpoint.

This is provided by a class function call destination_name. The base model class declares a generic method for this which takes the class name, converts it from title case to snake case, and makes it plural. Hence, a model class called User will have a default destination name of users and a model class of OrderProduct will have a default destination name of order_products. Of course, this system isn’t pefect: your backend may have a different convention or you may have one of the many words in the english language that are exceptions to the grammatical rules of making words plural. In this case you can simply extend the method and change it according to your needs, e.g.:

from typing import Self
import clearskies

class Fish(clearskies.Model):
    @classmethod
    def destination_name(cls: type[Self]) -> str:
        return "fish"

Columns

Finally, columns are defined by attaching attributes to your model class that extend clearskies.Column. A variety are provided by default in the clearskies.columns module, and you can always create more or import them from other packages.

Fetching From the Di Container

In order to use a model in your application you need to retrieve it from the dependency injection system. Like everything, you can do this by either the name or with type hinting. Models do have a special rule for injection-via-name: like all classes their dependency injection name is made by converting the class name from title case to snake case, but they are also available via the pluralized name. Here’s a quick example of all three approaches for dependency injection:

import clearskies

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()

    id = clearskies.columns.Uuid()
    name = clearskies.columns.String()

def my_application(user, users, by_type_hint: User):
    return {
        "all_are_user_models": isinstance(user, User) and isinstance(users, User) and isinstance(by_type_hint, User)
    }

cli = clearskies.contexts.Cli(my_application, classes=[User])
cli()

Note that the User model class was provided in the classes list sent to the context: that’s important as it informs the dependency injection system that this is a class we want to provide. It’s common (but not required) to put all models for a clearskies application in their own separate python module and then provide those to the depedency injection system via the modules argument to the context. So you may have a directory structure like this:

├── app/
│   └── models/
│       ├── __init__.py
│       ├── category.py
│       ├── order.py
│       ├── product.py
│       ├── status.py
│       └── user.py
└── api.py

Where __init__.py imports all the models:

from app.models.category import Category
from app.models.order import Order
from app.models.proudct import Product
from app.models.status import Status
from app.models.user import User

__all__ = ["Category", "Order", "Product", "Status", "User"]

Then in your main application you can just import the whole models module into your context:

import app.models

cli = clearskies.contexts.cli(SomeApplication, modules=[app.models])

Adding Dependencies

The base model class extends clearskies.di.InjectableProperties which means that you can inject dependencies into your model using the di.inject classes. Here’s an example that demonstrates dependency injection for models:

import datetime
import clearskies

class SomeClass:
    # Since this will be built by the DI system directly, we can declare dependencies in the __init__
    def __init__(self, some_date):
        self.some_date = some_date

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()

    utcnow = clearskies.di.inject.Utcnow()
    some_class = clearskies.di.inject.ByClass(SomeClass)

    id = clearskies.columns.Uuid()
    name = clearskies.columns.String()

    def some_date_in_the_past(self):
        return self.some_class.some_date < self.utcnow

def my_application(user):
    return user.some_date_in_the_past()

cli = clearskies.contexts.Cli(
    my_application,
    classes=[User],
    bindings={
        "some_date": datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1),
    }
)
cli()

Table of contents