How to record activities

The W3C defines activities as follows: An activity is something that occurs over a period of time and acts upon or with entities; it may include consuming, processing, transforming, modifying, relocating, using, or generating entities.

In opposition to entities and agents, you do not define activities in your configuration. You need to manually specify an activity for each function or method whose execution should be recorded. In addition, recording the execution of Djangos class-based views is done differently, too.

Getting the instance of the ProvenanceGenerator

To be able to track the correct flow of multiple executing activities, each activity has to provide data for the same ProvenanceGenerator instance. This is only possible, because the ProvenanceGenerator implements the Singleton pattern. Using this pattern, there can always be only one instance of a ProvenanceGenerator existing. Each activity will then contribute to the same ProvenanceGenerator instance.

You can get an instance of the ProvenanceGenerator using the class method get() at the top of the module where you want to record an activity:

from django_prov.generator import ProvenanceGenerator
generator_instance = ProvenanceGenerator.get()

How to record functions

To record each execution of a function, you need to wrap it with the ProvenanceGenerator.activity() decorator:

@generator_instance.activity(name="my_activity_name")
def return_a_borrowing(request, pk=None):
    ...

You can specify a useful name for your function. If you don’t specify a name, the ProvenanceGenerator will record the original function name in the provenance documents.

If a function is calling another function that got decorated too, the ProvenanceGenerator is able to correctly relate them using a Communication

How to record classes

For ordinary Python classes, apply the activity() decorator to a class to record construction and public methods:

The following examples use plain Python versions of the library workflows of the example application. Borrowings use the status codes: O (ordered), B (borrowed), and R (returned).

@generator_instance.activity(name="Borrowing")
class Borrowing:
    def __init__(self):
        self.status = "O"

    def start_borrowing(self):
        self.status = "B"
        return self.status

The resulting activities will be named Borrowing.__init__ and Borrowing.start_borrowing. The decorator returns the original class, preserving isinstance checks and inheritance. Public inherited methods, static methods, and class methods are included. Private methods, properties, and special methods (except __init__()) are not recorded. Parent classes are not modified.

If you don’t want to trace all methods of a class. you can still decorate individual methods instead. An explicit method decorator will be recorded over the class decorator and is not wrapped a second time:

class Borrowing:
    def __init__(self):
        self.status = "B"

    @generator_instance.activity(name="return_borrowing", fields=["status"])
    def return_borrowing(self):
        self.status = "R"
        return self.status

Recording selected instance states

The optional fields argument records the listed attributes as entity snapshots before and after function or class / dataclass calls. Without fields, the decorator records the activity only. With fields=[], records will only contain the object’s type.

An existing state is linked to the activity through usage. A changed state is a new entity, linked to its previous version through derivation and to the activity through generation. Unchanged states will be reused within the current document, without creating additional generation relations. Construction records only the initialized state of the specified fields. If the values of recorded fields are bigger than the specified output size, they will be cut short.

The first function parameter supplies the instance, including when passed by keyword. If you specify fields that don’t exist, the generator will raise an Exception. Be careful, because attribute getters may run, which can result in additional runtime. Objects that get decorated with the activity() don’t need to be added to the explicit Django model configuration.

from dataclasses import dataclass

@generator_instance.activity(fields=["status"])
@dataclass
class Borrowing:
    status: str = "O"

    def start_borrowing(self):
        self.status = "B"

    def return_borrowing(self):
        self.status = "R"

@generator_instance.activity(name="borrow_and_return")
def borrow_and_return():
    borrowing = Borrowing()
    borrowing.start_borrowing()
    borrowing.return_borrowing()

borrow_and_return()

The example shows, how you can use the activity() decorator.

Place the decorator above @dataclass so it sees the generated constructor. The outermost executed activty will export and reset the latest Provenance document. You can wrap a workflow in another activity to record several calls and their versions together. Direct attribute assignments outside decorated calls will not be intercepted.

How to record Django class-based views

For Django class-based views, it is proposed to decorate dispatch() with Django’s method_decorator. Whole-class decoration is intended for ordinary Python classes. Django models will be recorded using the existing configuration and connected signals:

@method_decorator(generator.activity(name="django_activity",), name='dispatch')
class BorrowStartView(UpdateView):
    model = Borrowing
    template_name = 'order_form.html'

Since you usually don’t access the dispatch() method in your code, you need to wrap the ProvenanceGenerator.activity() in a method_decorator(). Using this decorator, you define the name of the method you want to decorate, in this case it is name=dispatch.

It is still possible to manually decorate other methods, but django-prov works best when decorating the dispatch() method.

See also

Information on the method flowchart of Djangos class-based views can be found here: Django - Base Views