Skip to content
/ revy Public

A toolkit for building revision control systems.

License

Notifications You must be signed in to change notification settings

ertgl/revy

Repository files navigation

A toolkit for building revision control systems.


⚠️ Currently only Django support is available.


Features

With this library, you can produce various revision control systems for your data. When Revy is used without any customization, it simply gives you a revision history.

All the models in the package are swappable. So, you are free to expand the logic by writing your own models instead of using the default ones.

Idiomatic and Declarative Syntax

Write the code as you think. When you enter a Revy context, it tracks all the changes automatically.

from revy import Context


with (
  Context.via_actor(None),
  Context.via_revision_description("Detected by the system."),
):
    comment.is_marked_as_spam = True
    comment.save()

Context management is done by the Stackholm project. The implementation is both space and time efficient, also sync / async compatible. Context (or stack) operations have O(1) time complexity, and they are zero-copy. (Except the methods prefixed by via_. They are O(n) in the worst case, where n is the number of checkpoint datas to be set, which constantly equals to 1. That makes them amortized O(1).)

Generic Actors

Actors can be any instance of any model.

@Context()
def view(request):
    if request.META.get('HTTP_X_PERFORM_AS_ORGANIZATION'):
        Context.set_actor(request.organization)
    else:
        Context.set_actor(request.user)

Multiple Actors In One Revision

Revy can be used in collaborative systems too. Multiple actors can be involved together in one revision.

from revy import Context as C


def deserialize(data):
    ...


def view(request):
    with C.via_actor(request.user):
        # <-- The actor is `request.user` in this block.
        C.set_revision_description(
            request.META.get('HTTP_X_COMMIT_MESSAGE'),
        )
        instance = deserialize(request.data)
        local_amount = instance.amount * instance.exchange_rate
        if instance.local_amount != local_amount:
            with (
                C.via_actor(None),
                C.via_attribute_delta_description(
                    'Corrected by the system.',
                ),
            ):
                # <-- The actor is `None` in this block.
                instance.local_amount = local_amount
            # <-- The actor here is `request.user` again.
        instance.save()

Disabling Automatic Delta Generation Temporarily

When you need it, you can write code like below to disable / re-enable the tracking.

@Context()
def some_task():
    Context.disable()
    ...
    Context.enable()
@Context()
def some_task():
    with Context.as_disabled():
        ... # <-- Context is disabled in this block.
    # <-- Here context is re-enabled.

Foreign Key Deletion Handler

Revy has drop in replacements for Django's foreign key deletion handlers.

If there is no active context or the existing context is disabled during the deletion, fallbacks to the corresponding Django deletion handler.

Supported handler types:

  • CASCADE
  • SET
  • SET_NULL
  • SET_DEFAULT
from revy.contrib.django.models import CASCADE

class Post(Model):
    author = ForeignKey(
        User,
        on_delete=CASCADE,
    )
with (
    Context.via_object_delta_description(
        "The account has been closed at the owner's request.",
    ),
    Context.via_deletion_description(
        "Deleted because the owner's account has been closed.",
    ),
):
    user.delete()

Getting Snapshots of Object Deltas

Revy provides an ORM function called as ObjectSnapshot. It helps to reconstruct model instances from deltas which are relative to the object deltas, using only one query.

For instance, the following example can be considered as a rollback operation.

from django.contrib.contenttypes.models import ContentType
from revy.contrib.django.models import (
    ObjectDelta,
    ObjectSnapshot,
)


DELETED_USER_ID = 1


object_delta = ObjectDelta.objects.filter(
    action=ObjectDelta.ACTION_DELETE,
    content_type=ContentType.objects.get_for_model(User),
    content_id=DELETED_USER_ID,
).annotate(
    snapshot=ObjectSnapshot(User)
).order_by(
    '-pk',
).first()

# object_delta.snapshot is an instance of User class.
# And it is ready to be saved again, with the same ID if you don't change it.
object_delta.snapshot.save()

Installation

Revy is available on PyPI. It can be installed and upgraded using pip:

pip install revy

Setup

Add 'revy.contrib.django' to INSTALLED_APPS in your Django project.

INSTALLED_APPS = [
  'revy.contrib.django',
]

License

See the LICENSE file.

About

A toolkit for building revision control systems.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published