Skip to main content

django-relations-Iterator

Provides utilities for iterating over django model instances hierarchy. Provides easy out-of-the-box way to clone django instances.

Reasoning and solution for use case with cloning - https://hackernoon.com/the-smart-way-to-clone-django-instances

Example:

Simple instances tree clone

#models.py
from django.conf import settings
from django.db import models


class Meeting(models.Model):
    title = models.CharField(max_length=200)
    time = models.DateTimeField(null=True, blank=True)
    participants = models.ManyToManyField(settings.AUTH_USER_MODEL, through='Participation', blank=True)


class Participation(models.Model):
    meeting = models.ForeignKey('Meeting', on_delete=models.CASCADE, related_name='participations')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='participations')


class Invitation(models.Model):
    STATUS_SENT = 'sent'
    STATUS_ACCEPTED = 'accepted'
    STATUS_DECLINED = 'declined'
    STATUS_CHOICES = (
        (STATUS_SENT, STATUS_SENT),
        (STATUS_ACCEPTED, STATUS_ACCEPTED),
        (STATUS_DECLINED, STATUS_DECLINED),
    )

    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_SENT)
    participation = models.ForeignKey('Participation', on_delete=models.CASCADE, related_name='invitations')


class Comment(models.Model):
    meeting = models.ForeignKey('Meeting', on_delete=models.CASCADE, related_name='comments')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    description = models.TextField(max_length=3000)
#clone.py
from relations_iterator import clone, CloneVisitor
from .models import Meeting

# because of config, tree will ignore comments, but will consider all participations and invitations
CLONE_STRUCTURE = {
    'participations': {
        'invitations': {}
    }
}

        
meeting = Meeting.objects.last()
clone(meeting, CLONE_STRUCTURE, CloneVisitor())

Customizing cloning process

# Example: you want to set title for cloned Meeting as {original_title}-COPY 
# and set time of the instance to None
class CustomCloneVisitor(CloneVisitor):
    @singledispatchmethod
    def customize(self, instance):
        pass

    @customize.register
    def _(self, instance: Meeting):
        instance.title = f'{instance.title}-COPY'
        instance.time = None

Installation

pip install django-relations-iterator

Features

Instance tree

from relations_iterator import ConfigurableRelationTree

Collects all related instances from model hierarchy accordingly to the provided config

from pprint import pprint
from django.contrib.auth.models import User
from relations_iterator import ConfigurableRelationTree
from tests.meetings.models import Meeting, Participation, Invitation, Comment 


tom = User.objects.create(username='Tom')
jerry = User.objects.create(username='Jerry')
meeting = Meeting.objects.create(title='dinner')
tom_participation = Participation.objects.create(user_id=tom.id, meeting_id=meeting.id)
jerry_participation = Participation.objects.create(user_id=jerry.id, meeting_id=meeting.id)
Invitation.objects.create(user_id=jerry.id, meeting_id=meeting.id)
Comment.objects.create(user_id=jerry.id, meeting_id=meeting.id)


config = {
    'participations': {
        'invitations': {}
    }
}

meeting = Meeting.objects.last()
tree = ConfigurableRelationTree(root=instance, structure=config)
pprint(tree.tree)
# Output:
{
    <TreeNode for Meeting: Meeting object (1)>: {
        <ManyToOneRel: meetings.participation>: {
            <TreeNode for Participation: Participation object (1)>: {
                <ManyToOneRel: meetings.invitation>: {
                    <TreeNode for Invitation: Invitation object (1)>: {}
                }
            },
            <TreeNode for Participation: Participation object (2)>: {
                <ManyToOneRel: meetings.invitation>: {}
            }
        }
    }
}

For provided config tree will build himself only with participations and invitations relations and will ignore any other relations.

Tree iterator

from relations_iterator import RelationTreeIterator

Iterates over provided tree and yieds nodes one by one

For example above it will look like

pprint(list(node for node in RelationTreeIterator(tree)))
# Output
[<TreeNode for Meeting: Meeting object (1)>,
 <TreeNode for Participation: Participation object (1)>,
 <TreeNode for Invitation: Invitation object (1)>,
 <TreeNode for Participation: Participation object (2)>]

Abstract Visitor iterator

from relations_iterator import AbstractVisitor

Provides abstract class, with interface to implement visitor pattern. You must implement .visit(node) method, to complete implementation

Instances cloning feature

from relations_iterator import clone, CloneVisitor

Provides function to clone instances and simple CloneVisitor class, just as explained below in examples section.

clone function accepts 3 arguments:

  • instance - django Model instance, that needs to be cloned
  • config - config dictionary of the structure that needs to be cloned
  • visitor - visitor instance. CloneVisitor can be used directly or you can customize it and pass your own implementation

Config explanation:

Example:

# Config for cloning Meeting instance, we want to clone also participation's and invitations
config = {
    'participations': {  # related name for `Participation` model, that have fk to Meeting
        'invitations': {}  # related name for `Invitation` model, that have fk to `Participation` model
    }
}
# All other relations will be skipped, as they are not listed in config

Examples:

Clone visitor full implementation

from django.db.models import Model
from relations_iterator import TreeNode, AbstractVisitor


class CloneVisitor(AbstractVisitor):
    def visit(self, node: TreeNode):
        node.instance.pk = None
        if node.parent is not None:
            parent_joining_field, instance_joining_field = node.relation.get_joining_fields()[0]
            setattr(
                node.instance,
                instance_joining_field.attname,
                parent_joining_field.value_from_object(node.parent.instance)
            )
        self.customize(node.instance)
        node.instance.save()

    def customize(self, instance: Model) -> None:
        pass

Clone visitor will clone every instance in hierarchy and set proper parent, so it can be used to implement instance hierarchy clone

CLONE_STRUCTURE = {
    'participations': {
        'invitations': {}
    }
}

def clone(instance, config):
    visitor = CloneVisitor()
    tree = ConfigurableRelationTree(root=instance, structure=config)
    for node in RelationTreeIterator(tree=tree):
        visitor.visit(node)

clone(meeting, CLONE_STRUCTURE)

cloned_meeting = Meeting.objects.last()
tree = ConfigurableRelationTree(root=cloned_meeting, structure=CLONE_STRUCTURE)
pprint(tree.tree)
# Output
{
    <TreeNode for Meeting: Meeting object (2)>: {
        <ManyToOneRel: meetings.participation>: {
            <TreeNode for Participation: Participation object (3)>: {
                <ManyToOneRel: meetings.invitation>: {
                    <TreeNode for Invitation: Invitation object (2)>: {}
                }
            },
            <TreeNode for Participation: Participation object (3)>: {
                <ManyToOneRel: meetings.invitation>: {}
            }
        }
    }
}

Path print visitor

Path print visitor will print all parent nodes from root to curent node

class PathPrintVisitor(AbstractVisitor):
    def visit(self, node: TreeNode):
        print(list(reversed(self.get_path(node))))

    def get_path(self, node: TreeNode):
        path = [node]
        if node.parent:
            path.extend(self.get_path(node.parent))
        return path

visitor = PathPrintVisitor()
for node in RelationTreeIterator(tree):
    visitor.visit(node)
# Output
[<TreeNode for Meeting: Meeting object (2)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (3)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (3)>, <TreeNode for Invitation: Invitation object (2)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (4)>]

Metadata

Release files for django-relations-iterator 0.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-relations-iterator 0.0.5
File Size Uploaded
django_relations_iterator-0.0.5.tar.gz 5.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-relations-iterator 0.0.5
File Interpreter ABI Platform
django_relations_iterator-0.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 12.8 kB

Release files / django_relations_iterator-0.0.5.tar.gz

Download URL django_relations_iterator-0.0.5.tar.gz
Size 5.7 kB
Tags Source
SHA-256 checksum
How to use checksums
036e7d058f2782a37471b2b8135e6a2813edd1f1b0d59b543b609b449826c5e8
BLAKE2b-256 checksum
How to use checksums
0f8dbcfed82be149531167506a2d5f5365c8eec8b06020baed255d4de7f83039
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/5.1.0 CPython/3.10.12

Release files / django_relations_iterator-0.0.5-py3-none-any.whl

Download URL django_relations_iterator-0.0.5-py3-none-any.whl
Size 7.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3f04306f77152bf37c0e3218c8bf655ef9a427b8f8f7406d457e76455e87e616
BLAKE2b-256 checksum
How to use checksums
d84e9218ec63f6b95f0021505ecfb05b514785f170c8c2572eded752c30b5a8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/5.1.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.0.5 This release

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page