Search code examples
dictionarygroup-byansiblenestedgroup

Ansible - How to regroup a list of dicts based on dict attribute


I have a list of dicts. For simplicity, it contains Country/State/City/Street details

{
    "addresses": [
        {
            "Name": "North_America",
            "Id": 10001,
            "Type": "Country",
            "Parent_Id": 10000
        },
        {
            "Name": "Maine",
            "Id": 10011,
            "Type": "State",
            "Parent_Id": 10001
        },
        {
            "Name": "Colorado",
            "Id": 10012,
            "Type": "State",
            "Parent_Id": 10001
        },
        {
            "Name": "Texas",
            "Id": 10013,
            "Type": "State",
            "Parent_Id": 10001
        },
        {
            "Name": "Augusta",
            "Id": 10101,
            "Type": "City",
            "Parent_Id": 10011
        },
        {
            "Name": "Portland",
            "Id": 10102,
            "Type": "City",
            "Parent_Id": 10011
        },
        {
            "Name": "Denver",
            "Id": 10103,
            "Type": "City",
            "Parent_Id": 10012
        },
        {
            "Name": "Austin",
            "Id": 10104,
            "Type": "City",
            "Parent_Id": 10013
        },
        {
            "Name": "Houston",
            "Id": 10105,
            "Type": "City",
            "Parent_Id": 10013
        },
        {
            "Name": "First_st",
            "Id": 11001,
            "Type": "Street",
            "Parent_Id": 10101
        },
        {
            "Name": "Second_st",
            "Id": 11002,
            "Type": "Street",
            "Parent_Id": 10101
        },
        {
            "Name": "First_st",
            "Id": 11003,
            "Type": "Street",
            "Parent_Id": 10102
        },
        {
            "Name": "Second_st",
            "Id": 11004,
            "Type": "Street",
            "Parent_Id": 10102
        },
        {
            "Name": "First_st",
            "Id": 11005,
            "Type": "Street",
            "Parent_Id": 10104
        },
        {
            "Name": "Second_st",
            "Id": 11006,
            "Type": "Street",
            "Parent_Id": 10104
        }
    ]
}

I want to restructure the above into a nested data structure using the Parent_Id attribute so that the final product resembles the following

Country State_1 City_1 Street_1 Street_2 City_2 Street_1 Street_2 State_2 City_1 Street_1 Street_2 City_2 Street_1 Street_2

With the data in the above format, if a new street is built, it should be possible to search this data structure, check to see if the Street name exists (in the correct City), and if it does not, add it in.

I've attempted the following

---
- name: Use groupby to create needed structure
  hosts: localhost
  gather_facts: false
  connection: local

  tasks:
    - include_vars:
        file: ../map.json
        name: map
    
    - name: set fact to group
      set_fact:
        grouped: "{{ map.addresses | groupby('Parent_Id') }}"
    
    - name: print grouped
      debug:
        var: grouped

    - name: set fact to re group
      set_fact:
        regrouped: "{{ regrouped | d([]) + [{ 'type': item.1 | map(attribute='Type')|list,
                                              'name': item.1 | map(attribute='Name')|list|flatten }] }}"
      loop: "{{ grouped }}"
    
    - name: print regrouped
      debug:
        var: regrouped

The first print statement returns

    "grouped": [
        [
            10000,
            [
                {
                    "Id": 10001,
                    "Name": "North_America",
                    "Parent_Id": 10000,
                    "Type": "Country"
                }
            ]
        ],
        [
            10001,
            [
                {
                    "Id": 10011,
                    "Name": "Maine",
                    "Parent_Id": 10001,
                    "Type": "State"
                },
                {
                    "Id": 10012,
                    "Name": "Colorado",
                    "Parent_Id": 10001,
                    "Type": "State"
                },
                {
                    "Id": 10013,
                    "Name": "Texas",
                    "Parent_Id": 10001,
                    "Type": "State"
                }
            ]
        ],
        [
            10011,
            [
                {
                    "Id": 10101,
                    "Name": "Augusta",
                    "Parent_Id": 10011,
                    "Type": "City"
                },
                {
                    "Id": 10102,
                    "Name": "Portland",
                    "Parent_Id": 10011,
                    "Type": "City"
                }
            ]
        ],
        ............
        ............
    ]

The second Print isn't any better

I've also attempted piping to multiple groupby but that doesn't work, returning an error saying the second item doesn't exist

i.e.

grouped: "{{ map.addresses | groupby('Parent_Id') | groupby('Id') }}"

Solution

  • The function dict() is the missing part

      grouped: "{{ dict(addresses|groupby('Parent_Id')) }}"
    

    gives what you want

      grouped:
        10000:
        - {Id: 10001, Name: North_America, Parent_Id: 10000, Type: Country}
        10001:
        - {Id: 10011, Name: Maine, Parent_Id: 10001, Type: State}
        - {Id: 10012, Name: Colorado, Parent_Id: 10001, Type: State}
        - {Id: 10013, Name: Texas, Parent_Id: 10001, Type: State}
        10011:
        - {Id: 10101, Name: Augusta, Parent_Id: 10011, Type: City}
        - {Id: 10102, Name: Portland, Parent_Id: 10011, Type: City}
        10012:
        - {Id: 10103, Name: Denver, Parent_Id: 10012, Type: City}
        10013:
        - {Id: 10104, Name: Austin, Parent_Id: 10013, Type: City}
        - {Id: 10105, Name: Houston, Parent_Id: 10013, Type: City}
        10101:
        - {Id: 11001, Name: First_st, Parent_Id: 10101, Type: Street}
        - {Id: 11002, Name: Second_st, Parent_Id: 10101, Type: Street}
        10102:
        - {Id: 11003, Name: First_st, Parent_Id: 10102, Type: Street}
        - {Id: 11004, Name: Second_st, Parent_Id: 10102, Type: Street}
        10104:
        - {Id: 11005, Name: First_st, Parent_Id: 10104, Type: Street}
        - {Id: 11006, Name: Second_st, Parent_Id: 10104, Type: Street}
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        addresses:
          - {Id: 10001, Name: North_America, Parent_Id: 10000, Type: Country}
          - {Id: 10011, Name: Maine, Parent_Id: 10001, Type: State}
          - {Id: 10012, Name: Colorado, Parent_Id: 10001, Type: State}
          - {Id: 10013, Name: Texas, Parent_Id: 10001, Type: State}
          - {Id: 10101, Name: Augusta, Parent_Id: 10011, Type: City}
          - {Id: 10102, Name: Portland, Parent_Id: 10011, Type: City}
          - {Id: 10103, Name: Denver, Parent_Id: 10012, Type: City}
          - {Id: 10104, Name: Austin, Parent_Id: 10013, Type: City}
          - {Id: 10105, Name: Houston, Parent_Id: 10013, Type: City}
          - {Id: 11001, Name: First_st, Parent_Id: 10101, Type: Street}
          - {Id: 11002, Name: Second_st, Parent_Id: 10101, Type: Street}
          - {Id: 11003, Name: First_st, Parent_Id: 10102, Type: Street}
          - {Id: 11004, Name: Second_st, Parent_Id: 10102, Type: Street}
          - {Id: 11005, Name: First_st, Parent_Id: 10104, Type: Street}
          - {Id: 11006, Name: Second_st, Parent_Id: 10104, Type: Street}
    
        grouped: "{{ dict(addresses|groupby('Parent_Id')) }}"
    
      tasks:
    
        - debug:
            var: grouped|to_yaml