Search code examples
pythonrecursionparent-child

How to merge Firefox bookmarks exported as json (dictionaries within dictionaries structure)


I am trying to make a new dictionary that is made from an existing dictionary that it has been creating from a function which tries to merge two dictionaries that derive from json files from backup firefox bookmarks.

1st json bookmark dictionary:

json1={'guid': 'root________', 'title': '', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697130233008000, 'id': 1, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'placesRoot', 'children': [{'guid': 'menu________', 'title': 'menu', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697130233008000, 'id': 2, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'bookmarksMenuFolder', 'children': [{'guid': '9GEAbdFPVBqv', 'title': 'Getting started - mypy 1.5.1 documentation', 'index': 0, 'dateAdded': 1696274666207000, 'lastModified': 1696274666207000, 'id': 16, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://mypy.readthedocs.io/en/stable/getting_started.html'}, {'guid': 'PDKXoMPpSKZ9', 'title': 'testFolder2', 'index': 1, 'dateAdded': 1697130042452000, 'lastModified': 1697130183178000, 'id': 18, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'jbP4ff424REs', 'title': 'Secure Coding with Python', 'index': 0, 'dateAdded': 1697130058445000, 'lastModified': 1697130058445000, 'id': 19, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://devopedia.org/secure-coding-with-python'}, {'guid': 'bZSAKQe67MEP', 'title': 'testSubFolder', 'index': 1, 'dateAdded': 1697130074677000, 'lastModified': 1697130183178000, 'id': 20, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': '0U5O4Rw6M3M5', 'title': 'Typer', 'index': 0, 'dateAdded': 1697130183178000, 'lastModified': 1697130183178000, 'id': 21, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://typer.tiangolo.com/'}]}]}, {'guid': '-j27AP1Cwt0O', 'title': 'testFolder1', 'index': 2, 'dateAdded': 1697130021758000, 'lastModified': 1697130233008000, 'id': 17, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'Wb3-R2DDT8Ip', 'title': 'Welcome to Click — Click Documentation (8.1.x)', 'index': 0, 'dateAdded': 1697130230240000, 'lastModified': 1697130230240000, 'id': 22, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://click.palletsprojects.com/en/8.1.x/'}]}, {'guid': 'VDTmkniLNlvN', 'title': 'Mozilla Firefox', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1697129949344000, 'id': 7, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'vUwrKuzYfywC', 'title': 'Get Help', 'index': 0, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 8, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/products/firefox', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/products/firefox'}, {'guid': 'mKpEl6U5Pppr', 'title': 'Customize Firefox', 'index': 1, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 9, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize'}, {'guid': 'Rw167-bbT1fR', 'title': 'Get Involved', 'index': 2, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 10, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/contribute/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/contribute/'}, {'guid': 'stHPEtkREVvD', 'title': 'About Us', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 11, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/about/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/about/'}]}]}, {'guid': 'toolbar_____', 'title': 'toolbar', 'index': 1, 'dateAdded': 1688927106926000, 'lastModified': 1696274298931000, 'id': 3, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'toolbarFolder', 'children': [{'guid': '690YbVlf5eS_', 'title': 'Getting Started', 'index': 0, 'dateAdded': 1688927107484000, 'lastModified': 1688927107484000, 'id': 12, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/firefox/central/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/firefox/central/'}, {'guid': 'YJ_gjoZ6Wwj1', 'title': 'json — JSON encoder and decoder — Python 3.11.5 documentation', 'index': 1, 'dateAdded': 1696274298931000, 'lastModified': 1696274298931000, 'id': 13, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/json.html'}]}, {'guid': 'unfiled_____', 'title': 'unfiled', 'index': 3, 'dateAdded': 1688927106926000, 'lastModified': 1688927107332000, 'id': 5, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'unfiledBookmarksFolder'}, {'guid': 'mobile______', 'title': 'mobile', 'index': 4, 'dateAdded': 1688927106942000, 'lastModified': 1688927107332000, 'id': 6, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'mobileFolder'}]}

2nd json bookmark dictionary:

json2={'guid': 'root________', 'title': '', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697131416648000, 'id': 1, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'placesRoot', 'children': [{'guid': 'menu________', 'title': 'menu', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697131416648000, 'id': 2, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'bookmarksMenuFolder', 'children': [{'guid': '9GEAbdFPVBqv', 'title': 'Getting started - mypy 1.5.1 documentation', 'index': 0, 'dateAdded': 1696274666207000, 'lastModified': 1696274666207000, 'id': 16, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://mypy.readthedocs.io/en/stable/getting_started.html'}, {'guid': 'FNxknWay_xr8', 'title': "Command-line Applications — The Hitchhiker's Guide to Python", 'index': 1, 'dateAdded': 1697130502023000, 'lastModified': 1697130502023000, 'id': 23, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python-guide.org/scenarios/cli/'}, {'guid': 'PDKXoMPpSKZ9', 'title': 'testFolder2', 'index': 2, 'dateAdded': 1697130042452000, 'lastModified': 1697131416648000, 'id': 18, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'bZSAKQe67MEP', 'title': 'testSubFolder', 'index': 0, 'dateAdded': 1697130074677000, 'lastModified': 1697131416648000, 'id': 20, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'm6MBObvLXgt6', 'title': 'The Python Fire Guide - Python Fire', 'index': 0, 'dateAdded': 1697130663668000, 'lastModified': 1697130663668000, 'id': 28, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://google.github.io/python-fire/guide/'}, {'guid': 'fl2vHRLT-RJY', 'title': 'Typer', 'index': 1, 'dateAdded': 1697131416648000, 'lastModified': 1697131416648000, 'id': 30, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://typer.tiangolo.com/'}]}, {'guid': 'Z2khP-DX2nJU', 'title': 'testsubF2', 'index': 1, 'dateAdded': 1697130537074000, 'lastModified': 1697130642695000, 'id': 26, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'ZHUGVs2ZYUiA', 'title': 'argparse — Parser for command-line options, arguments and sub-commands — Python 3.12.0 documentation', 'index': 0, 'dateAdded': 1697130642695000, 'lastModified': 1697130642695000, 'id': 27, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/argparse.html'}]}, {'guid': '3RP_KOI4Pq0q', 'title': 'plac · PyPI', 'index': 2, 'dateAdded': 1697130513781000, 'lastModified': 1697130513781000, 'id': 24, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://pypi.org/project/plac/'}]}, {'guid': '-j27AP1Cwt0O', 'title': 'testFolder1', 'index': 3, 'dateAdded': 1697130021758000, 'lastModified': 1697130520562000, 'id': 17, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'Wb3-R2DDT8Ip', 'title': 'Welcome to Click — Click Documentation (8.1.x)', 'index': 0, 'dateAdded': 1697130230240000, 'lastModified': 1697130230240000, 'id': 22, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://click.palletsprojects.com/en/8.1.x/'}, {'guid': 'zuT4_jp_Rj5l', 'title': 'cliff – Command Line Interface Formulation Framework — cliff 4.3.1.dev12 documentation', 'index': 1, 'dateAdded': 1697130520562000, 'lastModified': 1697130520562000, 'id': 25, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.openstack.org/cliff/latest/'}]}, {'guid': 'LjrKYDavnU7w', 'title': 'Generating Command-Line Interfaces (CLI) with Fire in Python', 'index': 4, 'dateAdded': 1697130696550000, 'lastModified': 1697130696550000, 'id': 29, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://stackabuse.com/generating-command-line-interfaces-cli-with-fire-in-python/'}, {'guid': 'VDTmkniLNlvN', 'title': 'Mozilla Firefox', 'index': 5, 'dateAdded': 1688927107386000, 'lastModified': 1697129949344000, 'id': 7, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'vUwrKuzYfywC', 'title': 'Get Help', 'index': 0, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 8, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/products/firefox', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/products/firefox'}, {'guid': 'mKpEl6U5Pppr', 'title': 'Customize Firefox', 'index': 1, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 9, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize'}, {'guid': 'Rw167-bbT1fR', 'title': 'Get Involved', 'index': 2, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 10, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/contribute/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/contribute/'}, {'guid': 'stHPEtkREVvD', 'title': 'About Us', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 11, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/about/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/about/'}]}]}, {'guid': 'toolbar_____', 'title': 'toolbar', 'index': 1, 'dateAdded': 1688927106926000, 'lastModified': 1696274298931000, 'id': 3, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'toolbarFolder', 'children': [{'guid': '690YbVlf5eS_', 'title': 'Getting Started', 'index': 0, 'dateAdded': 1688927107484000, 'lastModified': 1688927107484000, 'id': 12, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/firefox/central/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/firefox/central/'}, {'guid': 'YJ_gjoZ6Wwj1', 'title': 'json — JSON encoder and decoder — Python 3.11.5 documentation', 'index': 1, 'dateAdded': 1696274298931000, 'lastModified': 1696274298931000, 'id': 13, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/json.html'}]}, {'guid': 'unfiled_____', 'title': 'unfiled', 'index': 3, 'dateAdded': 1688927106926000, 'lastModified': 1688927107332000, 'id': 5, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'unfiledBookmarksFolder'}, {'guid': 'mobile______', 'title': 'mobile', 'index': 4, 'dateAdded': 1688927106942000, 'lastModified': 1688927107332000, 'id': 6, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'mobileFolder'}]}

My idea was to simplify the merging of these two dictionaries by doing:

def has_url(diction):
    if "uri" in diction:
        return True
    else:
        return False


def merge_dicts(dictmain, dict2):
    return {**dict2, **dictmain}



def get_bm_path(bookmarks):
    urls = {}

    def bm_path(x, name=""):
        if type(x) is dict:
            name = name + x.get("guid") + "/"
            if has_url(x):
                urls[name] = [f"uri_{x.get('guid')}", dict((k, x[k]) for k in x.keys() if k not in "children")]

            else:
                urls[name] = [f"folder_{x.get('guid')}", dict((k, x[k]) for k in x.keys() if k not in "children")]
            bm_path(x=x.get("children"), name=name)
        elif type(x) is list:
            for i, a in enumerate(x):
                bm_path(a, name=name)

    bm_path(bookmarks)
    return urls

Then by running:

merged_dict = merge_dicts(get_bm_path(json1), get_bm_path(json2))

I get a merged dictionary with keys that resemble folders/files structure so as not to lose the track of the parent/child structure of bookmarks while merging.

And then I need to make the function to derive from that merged dictionary the actual json structure that firefox uses.

The minimal (maybe) reproducible example:

main dictionary:

{'main_folder/': {'id': 'main_folder', 'ad': 'what'}, 'main_folder/subfolder1/': {'id': 'subfolder1', 'ad': 'what'}, 'main_folder/subfolder1/9GEAbdFPVBqv/': {'id': '9GEAbdFPVBqv', 'ad': 'what1'}, 
'main_folder/subfolder1/eaXY8H5Y1cJ_/': {'id': 'eaXY8H5Y1cJ_', 'ad': 'what2'},
'main_folder/subfolder1/eaXY8H5Y1cJ_/9p2UFp7-qcEt/': {'id': '9p2UFp7-qcEt', 'ad': 'what3'},
'main_folder/subfolder1/fijaCypbmbU1/': {'id': 'fijaCypbmbU1', 'ad': 'what4'}, 
'main_folder/subfolder2/': {'id': 'subfolder2', 'ad': 'what7'}} 

The resulting dictionary should be something like that:

{'id': 'main_folder', 'ad': 'what', 
    'children':[
     {'id': 'subfolder1', 'ad': 'what', 
        'children':[
         {'id': '9GEAbdFPVBqv', 'ad': 'what1'},
         {'id': 'eaXY8H5Y1cJ_', 'ad': 'what2',
             'children': [{'id': '9p2UFp7-qcEt', 'ad': 'what3'}]},
         {'id': 'fijaCypbmbU1', 'ad': 'what4'}
         ]
      },
      {'id': 'subfolder2', 'ad': 'what7'}
      ]
}

The resulting dictionary makes the tree like structure for each element (sub-folder) in a parent/child dictionary/list. I can't find a way to reduce each "layer" of files to subfolders recursively.


Solution

  • To be able to merge these easily, one realization needs to be had ~ every dict in the entire structure is a dict that may have a children key. Since that key is a "maybe" we can derive that at the very least, every dict is just a dict, and we should treat them identically.

    A very good way to do this is through the use of recursion. We can input the entire structure and have it feed child dictionaries back into itself so, everything goes through the exact same process.

    The code works as such:

    The current data dict to be merged is passed simultaneously with the targ dict to be merged to. If the targ dict is empty, it is immediately updated with the data dict contents. Then all of the children of the data dict are iterated. On each iteration a check is made to find a child on targ with a matching guid. Matches are returned as the index of the match within the children list. If no match is found the index is defaulted to -1. This is because, if no match is found we are going to append an empty dict to the end of the list. Finally the current child of data['children'] and the dict at the index of targ['children'] are passed back into the function and the process starts over.

    It's just a bunch of "do my children guids match your children guids?" and if not, make as many new dictionaries as necessary to accommodate the new children. If there is a matching child send the matches through to see if their children match. On and on until there is no more data.

    Triggering the update line is the equivalent of saying. "I didn't have this child but, now I do.", with the only exception being the very first call of r_merge. In that case it's like saying "I didn't have anything, and now I am identical to all of this first bookmark object". That also means that after the very first update nothing really happens until you get to the 2nd bookmark object. It just runs through children lists that perfectly match.

    SOLUTION
    #merge data to targ by eid recursing on r_key
    def r_merge(data:dict, targ:dict, eid:str, r_key:str) -> None:
        #guard clause
        if not data.get(eid): raise ValueError
        
        #update if empty
        if not targ: targ.update(data)
            
        #process children
        for entry in data.get(r_key, []):
            #find match index else default to -1
            i = next((i for i, ent in enumerate(targ[r_key]) if ent[eid] == entry[eid]), -1)
            
            #no match, prime with empty dict
            if i<0: targ[r_key].append(dict())
            
            #recursive merge
            r_merge(entry, targ[r_key][i], eid, r_key)
    
    USAGE
    import json
    
    json1={'guid': 'root________', 'title': '', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697130233008000, 'id': 1, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'placesRoot', 'children': [{'guid': 'menu________', 'title': 'menu', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697130233008000, 'id': 2, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'bookmarksMenuFolder', 'children': [{'guid': '9GEAbdFPVBqv', 'title': 'Getting started - mypy 1.5.1 documentation', 'index': 0, 'dateAdded': 1696274666207000, 'lastModified': 1696274666207000, 'id': 16, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://mypy.readthedocs.io/en/stable/getting_started.html'}, {'guid': 'PDKXoMPpSKZ9', 'title': 'testFolder2', 'index': 1, 'dateAdded': 1697130042452000, 'lastModified': 1697130183178000, 'id': 18, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'jbP4ff424REs', 'title': 'Secure Coding with Python', 'index': 0, 'dateAdded': 1697130058445000, 'lastModified': 1697130058445000, 'id': 19, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://devopedia.org/secure-coding-with-python'}, {'guid': 'bZSAKQe67MEP', 'title': 'testSubFolder', 'index': 1, 'dateAdded': 1697130074677000, 'lastModified': 1697130183178000, 'id': 20, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': '0U5O4Rw6M3M5', 'title': 'Typer', 'index': 0, 'dateAdded': 1697130183178000, 'lastModified': 1697130183178000, 'id': 21, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://typer.tiangolo.com/'}]}]}, {'guid': '-j27AP1Cwt0O', 'title': 'testFolder1', 'index': 2, 'dateAdded': 1697130021758000, 'lastModified': 1697130233008000, 'id': 17, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'Wb3-R2DDT8Ip', 'title': 'Welcome to Click — Click Documentation (8.1.x)', 'index': 0, 'dateAdded': 1697130230240000, 'lastModified': 1697130230240000, 'id': 22, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://click.palletsprojects.com/en/8.1.x/'}]}, {'guid': 'VDTmkniLNlvN', 'title': 'Mozilla Firefox', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1697129949344000, 'id': 7, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'vUwrKuzYfywC', 'title': 'Get Help', 'index': 0, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 8, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/products/firefox', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/products/firefox'}, {'guid': 'mKpEl6U5Pppr', 'title': 'Customize Firefox', 'index': 1, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 9, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize'}, {'guid': 'Rw167-bbT1fR', 'title': 'Get Involved', 'index': 2, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 10, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/contribute/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/contribute/'}, {'guid': 'stHPEtkREVvD', 'title': 'About Us', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 11, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/about/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/about/'}]}]}, {'guid': 'toolbar_____', 'title': 'toolbar', 'index': 1, 'dateAdded': 1688927106926000, 'lastModified': 1696274298931000, 'id': 3, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'toolbarFolder', 'children': [{'guid': '690YbVlf5eS_', 'title': 'Getting Started', 'index': 0, 'dateAdded': 1688927107484000, 'lastModified': 1688927107484000, 'id': 12, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/firefox/central/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/firefox/central/'}, {'guid': 'YJ_gjoZ6Wwj1', 'title': 'json — JSON encoder and decoder — Python 3.11.5 documentation', 'index': 1, 'dateAdded': 1696274298931000, 'lastModified': 1696274298931000, 'id': 13, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/json.html'}]}, {'guid': 'unfiled_____', 'title': 'unfiled', 'index': 3, 'dateAdded': 1688927106926000, 'lastModified': 1688927107332000, 'id': 5, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'unfiledBookmarksFolder'}, {'guid': 'mobile______', 'title': 'mobile', 'index': 4, 'dateAdded': 1688927106942000, 'lastModified': 1688927107332000, 'id': 6, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'mobileFolder'}]}
    json2={'guid': 'root________', 'title': '', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697131416648000, 'id': 1, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'placesRoot', 'children': [{'guid': 'menu________', 'title': 'menu', 'index': 0, 'dateAdded': 1688927106926000, 'lastModified': 1697131416648000, 'id': 2, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'bookmarksMenuFolder', 'children': [{'guid': '9GEAbdFPVBqv', 'title': 'Getting started - mypy 1.5.1 documentation', 'index': 0, 'dateAdded': 1696274666207000, 'lastModified': 1696274666207000, 'id': 16, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://mypy.readthedocs.io/en/stable/getting_started.html'}, {'guid': 'FNxknWay_xr8', 'title': "Command-line Applications — The Hitchhiker's Guide to Python", 'index': 1, 'dateAdded': 1697130502023000, 'lastModified': 1697130502023000, 'id': 23, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python-guide.org/scenarios/cli/'}, {'guid': 'PDKXoMPpSKZ9', 'title': 'testFolder2', 'index': 2, 'dateAdded': 1697130042452000, 'lastModified': 1697131416648000, 'id': 18, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'bZSAKQe67MEP', 'title': 'testSubFolder', 'index': 0, 'dateAdded': 1697130074677000, 'lastModified': 1697131416648000, 'id': 20, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'm6MBObvLXgt6', 'title': 'The Python Fire Guide - Python Fire', 'index': 0, 'dateAdded': 1697130663668000, 'lastModified': 1697130663668000, 'id': 28, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://google.github.io/python-fire/guide/'}, {'guid': 'fl2vHRLT-RJY', 'title': 'Typer', 'index': 1, 'dateAdded': 1697131416648000, 'lastModified': 1697131416648000, 'id': 30, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://typer.tiangolo.com/'}]}, {'guid': 'Z2khP-DX2nJU', 'title': 'testsubF2', 'index': 1, 'dateAdded': 1697130537074000, 'lastModified': 1697130642695000, 'id': 26, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'ZHUGVs2ZYUiA', 'title': 'argparse — Parser for command-line options, arguments and sub-commands — Python 3.12.0 documentation', 'index': 0, 'dateAdded': 1697130642695000, 'lastModified': 1697130642695000, 'id': 27, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/argparse.html'}]}, {'guid': '3RP_KOI4Pq0q', 'title': 'plac · PyPI', 'index': 2, 'dateAdded': 1697130513781000, 'lastModified': 1697130513781000, 'id': 24, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://pypi.org/project/plac/'}]}, {'guid': '-j27AP1Cwt0O', 'title': 'testFolder1', 'index': 3, 'dateAdded': 1697130021758000, 'lastModified': 1697130520562000, 'id': 17, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'Wb3-R2DDT8Ip', 'title': 'Welcome to Click — Click Documentation (8.1.x)', 'index': 0, 'dateAdded': 1697130230240000, 'lastModified': 1697130230240000, 'id': 22, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://click.palletsprojects.com/en/8.1.x/'}, {'guid': 'zuT4_jp_Rj5l', 'title': 'cliff – Command Line Interface Formulation Framework — cliff 4.3.1.dev12 documentation', 'index': 1, 'dateAdded': 1697130520562000, 'lastModified': 1697130520562000, 'id': 25, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.openstack.org/cliff/latest/'}]}, {'guid': 'LjrKYDavnU7w', 'title': 'Generating Command-Line Interfaces (CLI) with Fire in Python', 'index': 4, 'dateAdded': 1697130696550000, 'lastModified': 1697130696550000, 'id': 29, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://stackabuse.com/generating-command-line-interfaces-cli-with-fire-in-python/'}, {'guid': 'VDTmkniLNlvN', 'title': 'Mozilla Firefox', 'index': 5, 'dateAdded': 1688927107386000, 'lastModified': 1697129949344000, 'id': 7, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'children': [{'guid': 'vUwrKuzYfywC', 'title': 'Get Help', 'index': 0, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 8, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/products/firefox', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/products/firefox'}, {'guid': 'mKpEl6U5Pppr', 'title': 'Customize Firefox', 'index': 1, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 9, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize', 'type': 'text/x-moz-place', 'uri': 'https://support.mozilla.org/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize'}, {'guid': 'Rw167-bbT1fR', 'title': 'Get Involved', 'index': 2, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 10, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/contribute/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/contribute/'}, {'guid': 'stHPEtkREVvD', 'title': 'About Us', 'index': 3, 'dateAdded': 1688927107386000, 'lastModified': 1688927107386000, 'id': 11, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/about/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/about/'}]}]}, {'guid': 'toolbar_____', 'title': 'toolbar', 'index': 1, 'dateAdded': 1688927106926000, 'lastModified': 1696274298931000, 'id': 3, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'toolbarFolder', 'children': [{'guid': '690YbVlf5eS_', 'title': 'Getting Started', 'index': 0, 'dateAdded': 1688927107484000, 'lastModified': 1688927107484000, 'id': 12, 'typeCode': 1, 'iconUri': 'fake-favicon-uri:https://www.mozilla.org/firefox/central/', 'type': 'text/x-moz-place', 'uri': 'https://www.mozilla.org/firefox/central/'}, {'guid': 'YJ_gjoZ6Wwj1', 'title': 'json — JSON encoder and decoder — Python 3.11.5 documentation', 'index': 1, 'dateAdded': 1696274298931000, 'lastModified': 1696274298931000, 'id': 13, 'typeCode': 1, 'type': 'text/x-moz-place', 'uri': 'https://docs.python.org/3/library/json.html'}]}, {'guid': 'unfiled_____', 'title': 'unfiled', 'index': 3, 'dateAdded': 1688927106926000, 'lastModified': 1688927107332000, 'id': 5, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'unfiledBookmarksFolder'}, {'guid': 'mobile______', 'title': 'mobile', 'index': 4, 'dateAdded': 1688927106942000, 'lastModified': 1688927107332000, 'id': 6, 'typeCode': 2, 'type': 'text/x-moz-place-container', 'root': 'mobileFolder'}]} 
    
    results = dict()          #for storing results
    datas   = (json1, json2)  #gather dicts for iteration
    
    for data in datas:
        r_merge(data, results, 'guid', 'children') #recursive merge
        
    #print results        
    print(json.dumps(results, indent=4))