Chrononaut

Chrononaut is a simple package to provide versioning, change tracking, and record locking for applications using Flask-SQLAlchemy. It currently supports Postgres as a database backend.

Getting started

Getting started with Chrononaut is a simple two step process. First, replace your FlaskSQLAlchemy database object with a Chrononaut VersionedSQLAlchemy database connection:

from flask_sqlalchemy import SQLAlchemy
from chrononaut import VersionedSQLAlchemy

# A standard, FlaskSQLAlchemy database connection without support
# for automatic version tracking
db = SQLAlchemy(app)

# A Chrononaut database connection with automated versioning
# for any models with a `Versioned` mixin
db = VersionedSQLAlchemy(app)

After that, simply add the Versioned mixin object to your standard Flask-SQLAlchemy models:

# A simple User model with versioning to support tracking of, e.g.,
# email and name changes.
class User(db.Model, Versioned):
    __tablename__ = 'appuser'
    __chrononaut_untracked__ = ['login_count']
    __chrononaut_hidden__ = ['password']

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=False)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.Text())
    ...
    login_count = db.Column(db.Integer())

This creates an appuser_history table that provides prior record values, along with JSON change_info and a changed microsecond-level timestamp.

Using model history

Chrononaut automatically generates a history table for each model into which you mixin Versioned. This history table facilitates:

# See if the user has changed their email
# since they first signed up
user = User.query.first()
original_user_info = user.versions()[0]
if user.email == original_user_info.email:
    print('User email matches!')
else:
    print('The user has updated their email!')

Trying to access fields that are untracked or hidden raises an exception:

print(original_user_info.password)     # Raises a HiddenAttributeError
print(original_user_info.login_count)  # Raises an UntrackedAttributeError

For more information on fetching specific version records see Versioned.versions().

Fine-grained versioning

By default, Chrononaut will automatically version every column in a model.

In the above example, we do not want to retain past user passwords in our history table, so we add password to the model’s __chrononaut_hidden__ property. Changes to a user’s password will now result in a new model version and creation of a history record, but the automatically generated appuser_history table will not have a password field and will only note that a hidden column was changed in its change_info JSON column.

Similarly, Chrononaut’s __chrononaut_untracked__ property allows us to specify that we do not want to track a field at all. This is useful for changes that are regularly incremented, toggled, or otherwise changed but do not need to be tracked. A good example would be a starred property on an object or other UI state that might be persisted to the database between application sessions.

Migrations

Chrononaut automatically generates a SQLAlchemy model (and corresponding table) for each Versioned mixin. By default, this table is named tablename_history where tablename is the name of the table for the model. A custom table name may be specified by using the __chrononaut_tablename__ property in the model.

In order to use Chrononaut, it’s important to keep your *_history tables in sync with your main tables. We recommend using Alembic for migrations which should automatically generate the *_history tables when you first add the Versioned mixins and subsequent updates to your models.

More details

More in-depth information on Chrononaut’s API is available below: