A toolkit for building revision control systems.
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.
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).)
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)
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()
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.
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()
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()
Revy is available on PyPI. It can be installed and upgraded using pip:
pip install revy
Add 'revy.contrib.django'
to INSTALLED_APPS
in your Django project.
INSTALLED_APPS = [
'revy.contrib.django',
]
See the LICENSE file.