2 votes

Comment agréger plusieurs objets avec les mêmes valeurs dans certains champs ?

J'ai ce modèle :

class Connection(models.Model):
    CONNECTION_CHOICES = [
        ('REST-API', 'REST-API'),
        ('SSH', 'SSH'),
        ('SFTP', 'SFTP'),
        ('SOAP-API', 'SOAP-API'),
    ]
    endpoint = models.CharField(max_length=240, blank=True, null=True)
    port = models.IntegerField(blank=True, null=True)
    connection_type = models.CharField(max_length=240, choices=CONNECTION_CHOICES)
    source_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='source-tool+')
    target_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='target-tool+')

    def __str__(self):
        return self.source_tool.name + " à " + self.target_tool.name 

    def get_absolute_url(self):
        return reverse('tools:connection-detail', kwargs={'pk': self.pk})

Dans une vue, j'essaie de combiner des objets, où source_tool et target_tool sont les mêmes, mais le connection_type diffère.

Actuellement, j'ai cette vue :

def api_map_view(request):
    json = {}
    nodes = []
    links = []
    connections = Connection.objects.all()

    for connection in connections:
        if {'name': connection.source_tool.name, 'id': connection.source_tool.id} not in nodes:
            nodes.append({'name': connection.source_tool.name, 'id': connection.source_tool.id})
        if {'name': connection.target_tool.name, 'id': connection.target_tool.id} not in nodes:
            nodes.append({'name': connection.target_tool.name, 'id': connection.target_tool.id})
        if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
            links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
        else:
            links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})
    json['nodes'] = nodes
    json['links'] = links
    print(json)
    return JsonResponse(data=json)

Cela renvoie, par exemple

{
   'nodes':
           [
               {'name': 'Ansible', 'id': 1 },
               {'name': 'Terraform', 'id': 2},
               {'name': 'Foreman', 'id': 3}
           ],
   'links':
           [
               {'source': 1, 'target': 2, 'type': 'SSH'},
               {'source': 2, 'target': 3, 'type': 'REST-API'}
               {'source': 1, 'target': 2, 'type': 'REST-API'}
           ]
}

Mon cas d'utilisation est que je veux modifier les connexions pour ne pas obtenir 2 entrées différentes dans la liste pour la même connexion, qui ne diffère que dans le type. Au lieu du JSON ci-dessus, je veux obtenir ceci :

{
   'nodes':
           [
               {'name': 'Ansible', 'id': 1 },
               {'name': 'Terraform', 'id': 2},
               {'name': 'Foreman', 'id': 3}
           ],
   'links':
           [
               {'source': 1, 'target': 2, 'type': 'SSH/REST-API'},
               {'source': 2, 'target': 3, 'type': 'REST-API'}
           ]
}

Actuellement, je ne suis pas en mesure de créer une requête ou de modifier la liste de dictionnaires pour trouver l'entrée où source et target sont identiques à l'entrée actuelle (en itérant à travers la liste), et de modifier le champ type.

J'utilise Django 3.1 avec Python 3.8.

Cordialement

0voto

aneroid Points 2268

Le problème se produit dans ces lignes :

if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
    links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
else:
    links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})

Vous vérifiez si {'source': X, 'target': Y} est dans links, mais pour la première occurrence, vous ajoutez {'source': X, 'target': Y, 'type': Z1} à links. Ainsi, l'élément ajouté ne sera jamais Vrai pour dans les liens car il a une clé supplémentaire 'type'.

D'autre part, vous ne pouvez pas vérifier directement si {'source': X, 'target': Y, 'type': Z1} est dans les liens car alors il ne correspondra pas au cas où 'type': Z2.

Pour contourner ce problème, faites l'un des suivants :


1. (préféré) utilisez un dictionnaire avec les clés comme un namedtuple ou simplement un tuple de source et target. Puisque les tuples et les namedtuples sont hashables, ils peuvent être utilisés comme clés de dictionnaire.

import collections  # en haut

liens = {}  # liens est maintenant un dict, pas une liste
SourceTarget = collections.namedtuple('SourceTarget', 'source target')
# >>> SourceTarget('X', 'Y')  # pour montrer comment ils fonctionnent
# SourceTarget(source='X', target='Y')

À utiliser comme suit :

if (connection.source_tool.id, connection.target_tool.id) in links:  # les tuples peuvent correspondre aux namedtuples
    links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] += '/' + connection.connection_type
else:
    links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] = connection.connection_type

Et à la fin, où vous voulez les convertir en liste d'objets/dictionnaires :

json['links'] = [{'source': st.source, 'target': st.target, 'type': type_}
                 for st, type_ in links.items()]
                # J'ai utilisé `type_` pour éviter tout conflit avec le propre `type()` de Python

2. (variation de 1) Vous devez toujours utiliser un tuple ou un namedtuple comme clés de votre dictionnaire, mais ensuite utilisez un defaultdict avec list pour continuer à ajouter les types de connexion.

Vous n'aurez pas besoin de la partie if/else et vous pourrez simplement faire :

import collections

links = collections.defaultdict(list)
...

# en utilisant des tuples comme clé au lieu de namedtuples...
links[(connection.source_tool.id, connection.target_tool.id)].append(connection.connection_type)

Cela créera soit de nouvelles entrées pour chaque (source, cible) avec ce type en tant que seule valeur dans une liste, soit il ajoutera à cette liste. Aucune vérification if n'est nécessaire.

Et pour le convertir en votre objet json :

json['links'] = [{'source': st[0], 'target': st[1], 'type': '/'.join(types)}
                 for st, types in links.items()]

Au fait, puisque vous êtes sur Python 3.8, vous pouvez utiliser les expressions d'assignation alias l'opérateur 'morse', pour réduire la répétition et rendre le code plus concis.

En utilisant l'option 1 comme exemple, cela rendra les premières parties de votre bloc if beaucoup plus claires, car il devient évident que vous ajoutez la chose si elle n'existe pas ; sans avoir besoin de lire toute la longue ligne.

if (src := {'name': connection.source_tool.name, 'id': connection.source_tool.id}) not in nodes:
    nodes.append(src)
if (trg := {'name': connection.target_tool.name, 'id': connection.target_tool.id}) not in nodes:
    nodes.append(trg)
if (st := (connection.source_tool.id, connection.target_tool.id)) in links:
    # j'ai utilisé un tuple pour mettre à jour l'élément NT _existant_
    links[st] += '/' + connection.connection_type
else:
    # mais il faut utiliser des namedtuples lors de l'ajout de nouveaux éléments
    links[SourceTarget(*st)] = connection.connection_type

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X