{"id":23977967,"url":"https://github.com/ertgl/revy","last_synced_at":"2025-04-14T01:55:09.612Z","repository":{"id":37096876,"uuid":"480915542","full_name":"ertgl/revy","owner":"ertgl","description":"Revision control system toolkit for Django models, built with stackholm.","archived":false,"fork":false,"pushed_at":"2025-03-03T08:43:34.000Z","size":201,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-14T01:55:02.912Z","etag":null,"topics":["audit","collaborative","django","history","python","revision"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ertgl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-12T17:48:03.000Z","updated_at":"2025-03-03T08:43:31.000Z","dependencies_parsed_at":"2023-12-22T14:55:00.415Z","dependency_job_id":"116c20a5-73ac-4876-b0eb-2c63b10a0f23","html_url":"https://github.com/ertgl/revy","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Frevy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Frevy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Frevy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Frevy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ertgl","download_url":"https://codeload.github.com/ertgl/revy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248809032,"owners_count":21164895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit","collaborative","django","history","python","revision"],"created_at":"2025-01-07T08:15:49.364Z","updated_at":"2025-04-14T01:55:09.605Z","avatar_url":"https://github.com/ertgl.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"### Revy\n\nA toolkit for building revision control systems around\n[Django](https://www.djangoproject.com/) models.\n\n## Table of Contents\n- [Overview](#overview)\n- [Installation](#installation)\n- [Setup](#setup)\n- [Usage](#usage)\n  - [Polymorphic Actor Types](#polymorphic-actor-types)\n  - [Collaborative Revisions](#collaborative-revisions)\n  - [Foreign Key Deletion Handlers](#foreign-key-deletion-handlers)\n  - [Snapshots and Rollbacks](#snapshots-and-rollbacks)\n  - [Disabling Tracking Temporarily](#disabling-tracking-temporarily)\n- [Glossary](#glossary)\n- [License](#license)\n\n## Overview\n\nRevy is a powerful toolkit designed to build revision control systems for\nDjango models. It provides a flexible framework for automatically tracking\nchanges to model instances, enabling the creation of detailed revision\nhistories. Whether you need to track simple changes or manage complex\ncollaborative revisions, revy offers an easy-to-use, fast and efficient\nsolution.\n\nRevy tracks changes at the object and attribute levels, and stores them in a\ntabular format for easy querying and analysis. It supports polymorphic actor\ntypes, allowing actors to be instances of any model, and allows customizing\nthe database models using Django's swappable models feature. Revy also offers\ndrop-in replacements for Django's foreign key deletion handlers, making it easy\nto track cascading deletions and other related changes. To get snapshots and\nmake rollbacks, revy provides an ORM function named as `ObjectSnapshot`, which\nreconstructs model instances from object deltas with a single query.\n\nThe tracking system is built around the concept of contexts, which provide a\nmanaged scope for tracking revisions, actors, and changes. Contexts can be\ndisabled and re-enabled as needed, allowing developers to control when and how\nchanges are tracked.\n\nBehind the scenes, revy patches Django models to add tracking functionality,\nand uses the [stackholm](https://github.com/ertgl/stackholm/) project to manage\ncontexts as indexed stacks. This allows for zero-copy context operations with\nO(1) time complexity (at worst it is amortized O(1)), so the solution is both\ntime and memory efficient.\n\nThe implementation is synchronous and asynchronous compatible, making it easy\nto integrate with existing Django projects.\n\n## Installation\n\nRevy is available on PyPI. It can be installed using any compatible package\nmanager, such as `pip`:\n\n```shell\npip install revy\n```\n\n## Setup\n\nAdd `'revy.contrib.django'` to `INSTALLED_APPS` in your Django project.\n\n\n```python\nINSTALLED_APPS = [\n  'revy.contrib.django',\n]\n```\n\n## Usage\n\nSimply wrap your code in a context block to enable automatic tracking.\n\n```python\nfrom revy import Context\n\n\nwith (\n  Context.via_actor(None),\n  Context.via_revision_description(\"Detected by the system.\"),\n):\n    # Changes made in this block will be tracked automatically.\n    comment.is_marked_as_spam = True\n    # Saving the instance will create a new revision.\n    comment.save()\n```\n\n### Polymorphic Actor Types\n\nRevy uses polymorphic relationships to associate actors with revisions. This\nmeans that actors can be instances of any model, e.g., user, organization,\nsystem, etc.\n\n```python\n@Context()\ndef view(request):\n    # If the request was made on behalf of an organization,\n    # set the actor as the organization.\n    if (\n      # Check if the request has an HTTP header to perform as an organization.\n      request.META.get('HTTP_X_PERFORM_AS_ORGANIZATION')\n      and request.organization is not None\n    ):\n        Context.set_actor(request.organization)\n    else:\n        Context.set_actor(request.user)\n```\n\n### Collaborative Revisions\n\nRevy supports collaborative systems, allowing multiple actors in a single\nrevision.\n\n```python\nfrom revy import Context as C\n\n\ndef deserialize(data):\n    ...\n\n\ndef view(request):\n    with C.via_actor(request.user):\n        # \u003c-- The actor is `request.user` in this block.\n        C.set_revision_description(\n            request.META.get('HTTP_X_COMMIT_MESSAGE'),\n        )\n        instance = deserialize(request.data)\n        local_amount = instance.amount * instance.exchange_rate\n        if instance.local_amount != local_amount:\n            with (\n                # Assume that `None` is the system.\n                C.via_actor(None),\n                C.via_attribute_delta_description(\n                    'Corrected by the system.',\n                ),\n            ):\n                # \u003c-- The actor is the system in this block.\n                instance.local_amount = local_amount\n            # \u003c-- The actor here is `request.user` again.\n        instance.save()\n```\n\n### Foreign Key Deletion Handlers\n\nRevy provides drop-in replacements for Django's foreign key deletion handlers.\n\nIf no active context exists or if the current context is disabled during\ndeletion, the corresponding Django handler will be used as a fallback.\n\n**Supported handler types:**\n\n- `CASCADE`\n- `SET`\n- `SET_DEFAULT`\n- `SET_NULL`\n\n```python\n# Import `CASCADE` from `revy.contrib.django.models` module\n# instead of `django.db.models` module, to use revy's handler.\nfrom revy.contrib.django.models import CASCADE\n\nclass Post(Model):\n    author = ForeignKey(\n        User,\n        on_delete=CASCADE,\n    )\n```\n\n```python\nwith (\n    Context.via_object_delta_description(\n        \"The account has been closed at the owner's request.\",\n    ),\n    Context.via_deletion_description(\n        \"Deleted because the owner's account has been closed.\",\n    ),\n):\n    user.delete()\n```\n\n### Snapshots and Rollbacks\n\nRevy provides an ORM function named as `ObjectSnapshot` to reconstruct\nmodel instances from object deltas with a single query.\n\nThe following example demonstrates a rollback operation:\n\n```python\nfrom django.contrib.contenttypes.models import ContentType\nfrom revy.contrib.django.models import (\n    ObjectDelta,\n    ObjectSnapshot,\n)\n\n\nDELETED_USER_ID = 1\n\n\nobject_delta = ObjectDelta.objects.filter(\n    action=ObjectDelta.ACTION_DELETE,\n    content_type=ContentType.objects.get_for_model(User),\n    content_id=DELETED_USER_ID,\n).annotate(\n    snapshot=ObjectSnapshot(User)\n).order_by(\n    '-pk',\n).first()\n\n# object_delta.snapshot is an instance of User class.\n# And it is ready to be saved again, with the same ID if you don't change it.\nobject_delta.snapshot.save()\n```\n\n### Disabling Tracking Temporarily\n\nTracking can be disabled and re-enabled as needed.\n\n```python\n@Context()\ndef some_task():\n    Context.disable()\n    ...\n    Context.enable()\n```\n\n```python\n@Context()\ndef some_task():\n    with Context.as_disabled():\n        ... # \u003c-- Context is disabled in this block.\n    # \u003c-- Here context is re-enabled.\n```\n\n## Glossary\n\n- **Actor**: An entity (e.g., user, organization, system, etc.) responsible for\n  making changes.\n- **Attribute delta**: A change made to a model instance's attribute (e.g., set,\n  unset).\n- **Context**: A managed scope that tracks revisions, actors, and changes.\n- **Delta**: Either an object delta or an attribute delta.\n- **Object delta**: A change made to a model instance (e.g., create, update,\n  delete). An object delta can contain multiple attribute deltas.\n- **Revision**: A collection of deltas made by one or more actors.\n- **Snapshot**: A record of an object's state at a specific point in time\n  (e.g., before a change).\n\n## License\n\nThis project is licensed under the\n[MIT License](https://opensource.org/license/mit).\n\nSee the [LICENSE](LICENSE) file for more details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fertgl%2Frevy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fertgl%2Frevy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fertgl%2Frevy/lists"}