Module anchorage.bookmarks

Anchorage bookmark processing

Expand source code
# SPDX-FileCopyrightText: © 2021 Antonio López Rivera <antonlopezr99@gmail.com>
# SPDX-License-Identifier: GPL-3.0-only

"""
Anchorage bookmark processing
-----------------------------
"""


import os
import re
import sys
import shutil
import datetime
import rapidjson
from tqdm import tqdm

from alexandria.paths import root
from alexandria.shell import print_color

from anchorage.anchor_infrs.infrastructure import init, read_config
from anchorage.anchor_utils.aesthetic import colors, str_log_progress
from anchorage.anchor_utils.system import operating_system
from anchorage.anchor_utils.shell import suppress_stdout
from anchorage.anchor_utils.regex import expr_check
from anchorage.anchor_utils.file_conversion import JSONLZ4_to_JSON


def loc(overwrite=False):
    if overwrite:
        init(True)
    return read_config()


def path(browser, overwrite=False):
    """
    :return: Path of browser of choice Bookmarks file
    """
    if browser == "firefox":
        o_sys = operating_system()
        dir_path = loc(overwrite)[browser][o_sys]
        most_recent = datetime.datetime(1970, 1, 1)
        for filename in os.listdir(dir_path):
            file_date = datetime.datetime.strptime(filename[10:20], "%Y-%m-%d")
            if file_date > most_recent:
                most_recent = file_date
                bk_path = dir_path + "/" + filename
    else:
        o_sys = operating_system()
        bk_path = loc(overwrite)[browser][o_sys]

    if o_sys == "windows":
        preamble = ("\\".join(str(os.getenv("APPDATA")).split("\\")[:-1]) + "\\")
        bk_path = (preamble + bk_path).replace("/", "\\")
    return bk_path


def load(path):
    """
    :param path: Path to Bookmark JSON or JSONLZ4 file
    :return: Parsed JSON file
    """
    if path.split('\\')[-1].find('json') != -1:                    # Check if the JSON extension is present in path
        ext = path[len(path)-path[::-1].find('.'):].lower()        # If so obtain full extension
        if ext == "json":                                          # If full extension = JSON load normally
            return rapidjson.load(open(path, encoding="utf8"))
        elif ext == "jsonlz4":                                     # IF full extension = JSONLZ4 create bookmark
            d = rapidjson.loads(JSONLZ4_to_JSON(path))              # dictionary appropriately
            bm_list = d['children'][0]['children']
            bm_dirs = [d['name'] for d in bm_list]
            bm_dics = [d for d in bm_list]
            return {'roots': dict(zip(bm_dirs, bm_dics))}
    else:                                                          # If no extension is present, the file is assumed
        return rapidjson.load(open(path, encoding="utf8"))         # to be a JSON file (the case for Chromium browsers)


def export(path, dest=root()):
    """
    :param path: Bookmark JSON path
    :param dest: Destination directory INCLUDING THE FILENAME
    :return: Export bookmark JSON file to the project's root directory
    """
    date = datetime.datetime.today().strftime('%Y_%m_%d')
    filename = f"bookmarks_{date}" + (".jsonzl4" if ".jsonlz4" in path else ".json")
    shutil.copyfile(path, os.path.join(dest, filename))
    return dest


class bookmarks:

    def __init__(self, bookmark_dict,
                 drop_duplicate_urls=True,
                 drop_local_files=True,
                 drop_dirs=None,
                 drop_names=None,
                 drop_urls=None,
                 drop_dirs_subs=None,
                 drop_urls_subs=None,
                 drop_names_subs=None,
                 drop_dirs_regex=None,
                 drop_urls_regex=None,
                 drop_names_regex=None,
                 ):
        """
        Prepare bookmarks for archiving.

        Instance attributes:

            self.links: dictionary of - Name: Dict - pairs, where

                Dict: {"url": url,
                       "tags": [tag_1, tag_2, ..., tag_n]}

            self.tags: list containing the ordered names of the parent folders of each link.

        :param bookmark_dict: Bookmark JSON file.
        :param drop_local_files: Remove local file bookmarks from bookmark list.
        :param drop_duplicate_urls: Remove duplicated URLs.
        :param drop_dirs: Directories from which no bookmarks are to be archived.
        """

        content = bookmark_dict['roots']

        self.bookmarks = {}
        self.tags = []
        self.n_dirs = 0

        # Conduct iteration
        for directory in content:
            self.search_dict(content[directory])        # Iteration
            self.tags = []                              # "Navigate" back to root
            self.n_dirs += 1                            # Keep track of the number of directories

        # Drop duplicate URLs
        if drop_duplicate_urls:
            self.drop_duplicate_urls()

        # Filters
        if drop_local_files:                                                    # Local files
            self.drop_local_files()
        if drop_dirs:                                                           # String    - Directories
            self.drop_string(target=drop_dirs, what='dir')
        if drop_names:                                                          # String    - Names
            self.drop_string(target=drop_names, what='name')
        if drop_urls:                                                           # String    - URLs
            self.drop_string(target=drop_dirs, what='url')
        if drop_dirs_subs:                                                      # Substring - Directories
            self.drop_substring(target=drop_dirs_subs, what='dir')
        if drop_names_subs:                                                     # Substring - Names
            self.drop_substring(target=drop_names_subs, what='name')
        if drop_urls_subs:                                                      # Substring - URLs
            self.drop_substring(target=drop_urls_subs, what='url')
        if drop_dirs_regex:                                                     # Regex     - Directories
            self.drop_regex(regex=drop_dirs_regex, what='dir')
        if drop_names_regex:                                                    # Regex     - Names
            self.drop_regex(regex=drop_names_regex, what='name')
        if drop_urls_regex:                                                     # Regex     - URLs
            self.drop_regex(regex=drop_urls_regex, what='url')

    def search_dict(self, dictionary):
        """
        Reduce depth-n bookmark dictionary to depth-1 dictionary of -Name: Dict - pairs, where

            Dict: {"url": url,
                   "tags": tag_list}

        and tag_list is a list containing the ordered names of the parent folders of each link.

        :param dictionary: Bookmark dictionary from which - Name: [link, tags] - pairs are to be extracted.
        :return: Depth-1 dictionary of - Name: [link, tags] - pairs.
        """
        if "children" in dictionary.keys():
            self.tags.append(dictionary['name'])            # Append directory name to tag list
            self.n_dirs += 1                                # Keep track of the number of directories
            self.search_children(dictionary['children'])    # Search through children
            self.tag_backtrack(dictionary['name'])          # After search is over, "navigate" back to parent directory

        elif 'url' in dictionary.keys():
            # Conduct a regex search for the name of the bookmark among all those previously found.
            #
            # Avoids a name conflict caused by the nth duplicate with n>1 due to the first
            # duplicate having already a different name to the first one, as
            #
            #       name of first duplicate = <name> ::anchorage name duplicate:: 2
            #
            n_rep = len(list(filter(re.compile(f'{re.escape(dictionary["name"])}').match,
                                    list(self.bookmarks.keys()))
                             ))
            #   1. re.escape
            #          Avoid issues with bookmarks with special regex sequences in their name
            #   2. re.compile(<bookmark name>
            #          Match any string with the entire bookmark name in it

            if n_rep > 0:
                key = dictionary['name'] + f" ::anchorage name duplicate:: {n_rep+1}"
            else:
                key = dictionary['name']

            self.bookmarks[key] = {'url': dictionary['url'],
                                   'tags': self.tags.copy()}
        else:
            pass        # Account for possibly empty bookmark folders

    def search_children(self, children):
        """
        Iterate over children.

        :param children: List entries in bookmark dictionary, which are by custom named "children" in
                         bookmark dictionaries.
        """
        for child in children:
            self.search_dict(child)

    def tag_backtrack(self, tag):
        """
        Remove tracked tags deeper than input tag.
            For a series of nested dictionaries (D) which may contain "children" lists (c)

                D
                |_ c[dir_name1]             tags = [dir_name1]
                   |_ D
                   |  |_ c[dirname_2]       tags = [dir_name1, dir_name2]
                   |     |_ D
                   |_ D                  !! tags = [dir_name1]

            It is necessary to be able to "navigate" to the current parent directory after
            iterating through any subdirectories.

            The "path" to each link is kept in an ordered list. To "navigate" back after
            iterating through any previously subdirectories, all elements in the tag list
            after the specified (parent) tag are eliminated.
        """
        try:
            inv_index = self.tags[::-1].index(tag)
            index = len(self.tags) - 1 - inv_index
            self.tags = self.tags[:index]
        except ValueError:
            self.tags = [tag]

    def drop_duplicate_urls(self):
        links = []
        for key, value in self.bookmarks.copy().items():
            if value['url'] in links:
                del self.bookmarks[key]
            else:
                links.append(value['url'])

    def drop_local_files(self):
        for key, value in self.bookmarks.copy().items():
            if "http" not in value["url"][:4]:
                del self.bookmarks[key]

    def drop_string(self, target, what):
        if isinstance(target, str):
            target = [target]

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        for key, value in self.bookmarks.copy().items():
            if list(set(target) & set(list([key] if target_key == 'name' else value[target_key]))):
                del self.bookmarks[key]

    def drop_substring(self, target, what):
        if isinstance(target, str):
            target = [target]

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        i = 0
        for key, value in self.bookmarks.copy().items():
            rm = False
            while not rm:
                for string in key if target_key == 'name' else value[target_key]:
                    for substring in target:
                        if substring in string:
                            rm = True
                break
            if rm:
                del self.bookmarks[key]

    def drop_regex(self, regex, what):

        # Check regex formula for correctness
        expr_check(regex)

        r = re.compile(regex)

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        for key, value in self.bookmarks.copy().items():
            rm = False
            while not rm:
                for string in key if target_key == 'name' else value[target_key]:
                    if r.match(string):
                        rm = True
                break
            if rm:
                del self.bookmarks[key]

    def loop(self, f,
             loglevel=0,
             pb_label=None,
             pb_leave=True,
             pb_width=110,
             ):
        """
        :param f: Function - To be run on each entry of the bookmark dictionary.
        :param loglevel: Python standard library logging-style log levels.
            - 0 : Full log output.
            - 20: Visualize progress with tqdm progress bar.
            - 50: Suppress all output.
        :param pb_label: Str - Progress bar label.
        :param pb_leave: Boolean - False to remove progress bar from screen after completion.
        :param pb_width: N - Width in char of the progress bar.
        :return: List with all [key, value] pairs for which `f` execution resulted in an error.
        """

        e = {}
        t0 = datetime.datetime.now()
        step = 1
        total = len(self.bookmarks)

        if loglevel == 20:                                          # Create tqdm progress bar if specified
            pgr = tqdm(self.bookmarks.items(),
                       ncols=pb_width,
                       position=0,
                       leave=pb_leave,
                       desc=pb_label,
                       bar_format="{l_bar}"
                                  "%s{bar}"
                                  "%s| {n_fmt}/{total_fmt} "
                                  "[{elapsed}<%s{remaining}%s,"
                                  " {rate_fmt}{postfix}]" % (colors.fgYellow, colors.reset,
                                                             colors.fgRed, colors.reset)
                       )

        for key, value in self.bookmarks.items():
            if loglevel == 0:
                # Time estimate
                tn          = datetime.datetime.now()
                t_elapsed   = tn - t0
                t_remaining = step/max(t_elapsed.total_seconds(), 10**-6)*(total-step)

                print(str_log_progress(f"{step}/{total}", t_elapsed.total_seconds(), t_remaining))
                step += 1

            try:                                        # Attempt to run provided function on dictionary
                if loglevel in [20, 50]:
                    with suppress_stdout():             # Suppress function output if so specified
                        f(key, value)
                else:
                    f(key, value)
            except BaseException as exception:          # Error: add entry to error list
                e[key] = {'url': value['url'],
                          'Error message': str(exception)}

            if loglevel == 0:
                print("")
            if loglevel == 20:                          # Update progress bar if in use
                pgr.update()

        if loglevel == 20:
            print("\r" + " "*pb_width, end="\r")            # Clean console from debris left by tqdm

        return e

    def __repr__(self):
        """
        :return: Name and URL of all bookmarks, as well as the total number of bookmarks
                 and directories in the collection.
        """
        links = []
        for key, value in self.bookmarks.items():
            links.append([key, value["url"]])

        lstr = "\n".join(" ".join(name_link) for name_link in links)
        info = f'\n\nFound: {len(self.bookmarks)} links and {self.n_dirs} directories.'

        return lstr + info

Functions

def export(path, dest='/opt/hostedtoolcache/Python/3.7.13/x64/bin')

:param path: Bookmark JSON path :param dest: Destination directory INCLUDING THE FILENAME :return: Export bookmark JSON file to the project's root directory

Expand source code
def export(path, dest=root()):
    """
    :param path: Bookmark JSON path
    :param dest: Destination directory INCLUDING THE FILENAME
    :return: Export bookmark JSON file to the project's root directory
    """
    date = datetime.datetime.today().strftime('%Y_%m_%d')
    filename = f"bookmarks_{date}" + (".jsonzl4" if ".jsonlz4" in path else ".json")
    shutil.copyfile(path, os.path.join(dest, filename))
    return dest
def load(path)

:param path: Path to Bookmark JSON or JSONLZ4 file :return: Parsed JSON file

Expand source code
def load(path):
    """
    :param path: Path to Bookmark JSON or JSONLZ4 file
    :return: Parsed JSON file
    """
    if path.split('\\')[-1].find('json') != -1:                    # Check if the JSON extension is present in path
        ext = path[len(path)-path[::-1].find('.'):].lower()        # If so obtain full extension
        if ext == "json":                                          # If full extension = JSON load normally
            return rapidjson.load(open(path, encoding="utf8"))
        elif ext == "jsonlz4":                                     # IF full extension = JSONLZ4 create bookmark
            d = rapidjson.loads(JSONLZ4_to_JSON(path))              # dictionary appropriately
            bm_list = d['children'][0]['children']
            bm_dirs = [d['name'] for d in bm_list]
            bm_dics = [d for d in bm_list]
            return {'roots': dict(zip(bm_dirs, bm_dics))}
    else:                                                          # If no extension is present, the file is assumed
        return rapidjson.load(open(path, encoding="utf8"))         # to be a JSON file (the case for Chromium browsers)
def loc(overwrite=False)
Expand source code
def loc(overwrite=False):
    if overwrite:
        init(True)
    return read_config()
def path(browser, overwrite=False)

:return: Path of browser of choice Bookmarks file

Expand source code
def path(browser, overwrite=False):
    """
    :return: Path of browser of choice Bookmarks file
    """
    if browser == "firefox":
        o_sys = operating_system()
        dir_path = loc(overwrite)[browser][o_sys]
        most_recent = datetime.datetime(1970, 1, 1)
        for filename in os.listdir(dir_path):
            file_date = datetime.datetime.strptime(filename[10:20], "%Y-%m-%d")
            if file_date > most_recent:
                most_recent = file_date
                bk_path = dir_path + "/" + filename
    else:
        o_sys = operating_system()
        bk_path = loc(overwrite)[browser][o_sys]

    if o_sys == "windows":
        preamble = ("\\".join(str(os.getenv("APPDATA")).split("\\")[:-1]) + "\\")
        bk_path = (preamble + bk_path).replace("/", "\\")
    return bk_path

Classes

class bookmarks (bookmark_dict, drop_duplicate_urls=True, drop_local_files=True, drop_dirs=None, drop_names=None, drop_urls=None, drop_dirs_subs=None, drop_urls_subs=None, drop_names_subs=None, drop_dirs_regex=None, drop_urls_regex=None, drop_names_regex=None)

Prepare bookmarks for archiving.

Instance attributes:

self.links: dictionary of - Name: Dict - pairs, where

    Dict: {"url": url,
           "tags": [tag_1, tag_2, ..., tag_n]}

self.tags: list containing the ordered names of the parent folders of each link.

:param bookmark_dict: Bookmark JSON file. :param drop_local_files: Remove local file bookmarks from bookmark list. :param drop_duplicate_urls: Remove duplicated URLs. :param drop_dirs: Directories from which no bookmarks are to be archived.

Expand source code
class bookmarks:

    def __init__(self, bookmark_dict,
                 drop_duplicate_urls=True,
                 drop_local_files=True,
                 drop_dirs=None,
                 drop_names=None,
                 drop_urls=None,
                 drop_dirs_subs=None,
                 drop_urls_subs=None,
                 drop_names_subs=None,
                 drop_dirs_regex=None,
                 drop_urls_regex=None,
                 drop_names_regex=None,
                 ):
        """
        Prepare bookmarks for archiving.

        Instance attributes:

            self.links: dictionary of - Name: Dict - pairs, where

                Dict: {"url": url,
                       "tags": [tag_1, tag_2, ..., tag_n]}

            self.tags: list containing the ordered names of the parent folders of each link.

        :param bookmark_dict: Bookmark JSON file.
        :param drop_local_files: Remove local file bookmarks from bookmark list.
        :param drop_duplicate_urls: Remove duplicated URLs.
        :param drop_dirs: Directories from which no bookmarks are to be archived.
        """

        content = bookmark_dict['roots']

        self.bookmarks = {}
        self.tags = []
        self.n_dirs = 0

        # Conduct iteration
        for directory in content:
            self.search_dict(content[directory])        # Iteration
            self.tags = []                              # "Navigate" back to root
            self.n_dirs += 1                            # Keep track of the number of directories

        # Drop duplicate URLs
        if drop_duplicate_urls:
            self.drop_duplicate_urls()

        # Filters
        if drop_local_files:                                                    # Local files
            self.drop_local_files()
        if drop_dirs:                                                           # String    - Directories
            self.drop_string(target=drop_dirs, what='dir')
        if drop_names:                                                          # String    - Names
            self.drop_string(target=drop_names, what='name')
        if drop_urls:                                                           # String    - URLs
            self.drop_string(target=drop_dirs, what='url')
        if drop_dirs_subs:                                                      # Substring - Directories
            self.drop_substring(target=drop_dirs_subs, what='dir')
        if drop_names_subs:                                                     # Substring - Names
            self.drop_substring(target=drop_names_subs, what='name')
        if drop_urls_subs:                                                      # Substring - URLs
            self.drop_substring(target=drop_urls_subs, what='url')
        if drop_dirs_regex:                                                     # Regex     - Directories
            self.drop_regex(regex=drop_dirs_regex, what='dir')
        if drop_names_regex:                                                    # Regex     - Names
            self.drop_regex(regex=drop_names_regex, what='name')
        if drop_urls_regex:                                                     # Regex     - URLs
            self.drop_regex(regex=drop_urls_regex, what='url')

    def search_dict(self, dictionary):
        """
        Reduce depth-n bookmark dictionary to depth-1 dictionary of -Name: Dict - pairs, where

            Dict: {"url": url,
                   "tags": tag_list}

        and tag_list is a list containing the ordered names of the parent folders of each link.

        :param dictionary: Bookmark dictionary from which - Name: [link, tags] - pairs are to be extracted.
        :return: Depth-1 dictionary of - Name: [link, tags] - pairs.
        """
        if "children" in dictionary.keys():
            self.tags.append(dictionary['name'])            # Append directory name to tag list
            self.n_dirs += 1                                # Keep track of the number of directories
            self.search_children(dictionary['children'])    # Search through children
            self.tag_backtrack(dictionary['name'])          # After search is over, "navigate" back to parent directory

        elif 'url' in dictionary.keys():
            # Conduct a regex search for the name of the bookmark among all those previously found.
            #
            # Avoids a name conflict caused by the nth duplicate with n>1 due to the first
            # duplicate having already a different name to the first one, as
            #
            #       name of first duplicate = <name> ::anchorage name duplicate:: 2
            #
            n_rep = len(list(filter(re.compile(f'{re.escape(dictionary["name"])}').match,
                                    list(self.bookmarks.keys()))
                             ))
            #   1. re.escape
            #          Avoid issues with bookmarks with special regex sequences in their name
            #   2. re.compile(<bookmark name>
            #          Match any string with the entire bookmark name in it

            if n_rep > 0:
                key = dictionary['name'] + f" ::anchorage name duplicate:: {n_rep+1}"
            else:
                key = dictionary['name']

            self.bookmarks[key] = {'url': dictionary['url'],
                                   'tags': self.tags.copy()}
        else:
            pass        # Account for possibly empty bookmark folders

    def search_children(self, children):
        """
        Iterate over children.

        :param children: List entries in bookmark dictionary, which are by custom named "children" in
                         bookmark dictionaries.
        """
        for child in children:
            self.search_dict(child)

    def tag_backtrack(self, tag):
        """
        Remove tracked tags deeper than input tag.
            For a series of nested dictionaries (D) which may contain "children" lists (c)

                D
                |_ c[dir_name1]             tags = [dir_name1]
                   |_ D
                   |  |_ c[dirname_2]       tags = [dir_name1, dir_name2]
                   |     |_ D
                   |_ D                  !! tags = [dir_name1]

            It is necessary to be able to "navigate" to the current parent directory after
            iterating through any subdirectories.

            The "path" to each link is kept in an ordered list. To "navigate" back after
            iterating through any previously subdirectories, all elements in the tag list
            after the specified (parent) tag are eliminated.
        """
        try:
            inv_index = self.tags[::-1].index(tag)
            index = len(self.tags) - 1 - inv_index
            self.tags = self.tags[:index]
        except ValueError:
            self.tags = [tag]

    def drop_duplicate_urls(self):
        links = []
        for key, value in self.bookmarks.copy().items():
            if value['url'] in links:
                del self.bookmarks[key]
            else:
                links.append(value['url'])

    def drop_local_files(self):
        for key, value in self.bookmarks.copy().items():
            if "http" not in value["url"][:4]:
                del self.bookmarks[key]

    def drop_string(self, target, what):
        if isinstance(target, str):
            target = [target]

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        for key, value in self.bookmarks.copy().items():
            if list(set(target) & set(list([key] if target_key == 'name' else value[target_key]))):
                del self.bookmarks[key]

    def drop_substring(self, target, what):
        if isinstance(target, str):
            target = [target]

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        i = 0
        for key, value in self.bookmarks.copy().items():
            rm = False
            while not rm:
                for string in key if target_key == 'name' else value[target_key]:
                    for substring in target:
                        if substring in string:
                            rm = True
                break
            if rm:
                del self.bookmarks[key]

    def drop_regex(self, regex, what):

        # Check regex formula for correctness
        expr_check(regex)

        r = re.compile(regex)

        # What
        if what == 'dir':
            target_key = 'tags'
        elif what == 'name':
            target_key = 'name'
        elif what == 'url':
            target_key = 'url'
        else:
            print_color(f'Wrong filter target: {what}', 'red')
            sys.exit()

        for key, value in self.bookmarks.copy().items():
            rm = False
            while not rm:
                for string in key if target_key == 'name' else value[target_key]:
                    if r.match(string):
                        rm = True
                break
            if rm:
                del self.bookmarks[key]

    def loop(self, f,
             loglevel=0,
             pb_label=None,
             pb_leave=True,
             pb_width=110,
             ):
        """
        :param f: Function - To be run on each entry of the bookmark dictionary.
        :param loglevel: Python standard library logging-style log levels.
            - 0 : Full log output.
            - 20: Visualize progress with tqdm progress bar.
            - 50: Suppress all output.
        :param pb_label: Str - Progress bar label.
        :param pb_leave: Boolean - False to remove progress bar from screen after completion.
        :param pb_width: N - Width in char of the progress bar.
        :return: List with all [key, value] pairs for which `f` execution resulted in an error.
        """

        e = {}
        t0 = datetime.datetime.now()
        step = 1
        total = len(self.bookmarks)

        if loglevel == 20:                                          # Create tqdm progress bar if specified
            pgr = tqdm(self.bookmarks.items(),
                       ncols=pb_width,
                       position=0,
                       leave=pb_leave,
                       desc=pb_label,
                       bar_format="{l_bar}"
                                  "%s{bar}"
                                  "%s| {n_fmt}/{total_fmt} "
                                  "[{elapsed}<%s{remaining}%s,"
                                  " {rate_fmt}{postfix}]" % (colors.fgYellow, colors.reset,
                                                             colors.fgRed, colors.reset)
                       )

        for key, value in self.bookmarks.items():
            if loglevel == 0:
                # Time estimate
                tn          = datetime.datetime.now()
                t_elapsed   = tn - t0
                t_remaining = step/max(t_elapsed.total_seconds(), 10**-6)*(total-step)

                print(str_log_progress(f"{step}/{total}", t_elapsed.total_seconds(), t_remaining))
                step += 1

            try:                                        # Attempt to run provided function on dictionary
                if loglevel in [20, 50]:
                    with suppress_stdout():             # Suppress function output if so specified
                        f(key, value)
                else:
                    f(key, value)
            except BaseException as exception:          # Error: add entry to error list
                e[key] = {'url': value['url'],
                          'Error message': str(exception)}

            if loglevel == 0:
                print("")
            if loglevel == 20:                          # Update progress bar if in use
                pgr.update()

        if loglevel == 20:
            print("\r" + " "*pb_width, end="\r")            # Clean console from debris left by tqdm

        return e

    def __repr__(self):
        """
        :return: Name and URL of all bookmarks, as well as the total number of bookmarks
                 and directories in the collection.
        """
        links = []
        for key, value in self.bookmarks.items():
            links.append([key, value["url"]])

        lstr = "\n".join(" ".join(name_link) for name_link in links)
        info = f'\n\nFound: {len(self.bookmarks)} links and {self.n_dirs} directories.'

        return lstr + info

Methods

def drop_duplicate_urls(self)
Expand source code
def drop_duplicate_urls(self):
    links = []
    for key, value in self.bookmarks.copy().items():
        if value['url'] in links:
            del self.bookmarks[key]
        else:
            links.append(value['url'])
def drop_local_files(self)
Expand source code
def drop_local_files(self):
    for key, value in self.bookmarks.copy().items():
        if "http" not in value["url"][:4]:
            del self.bookmarks[key]
def drop_regex(self, regex, what)
Expand source code
def drop_regex(self, regex, what):

    # Check regex formula for correctness
    expr_check(regex)

    r = re.compile(regex)

    # What
    if what == 'dir':
        target_key = 'tags'
    elif what == 'name':
        target_key = 'name'
    elif what == 'url':
        target_key = 'url'
    else:
        print_color(f'Wrong filter target: {what}', 'red')
        sys.exit()

    for key, value in self.bookmarks.copy().items():
        rm = False
        while not rm:
            for string in key if target_key == 'name' else value[target_key]:
                if r.match(string):
                    rm = True
            break
        if rm:
            del self.bookmarks[key]
def drop_string(self, target, what)
Expand source code
def drop_string(self, target, what):
    if isinstance(target, str):
        target = [target]

    # What
    if what == 'dir':
        target_key = 'tags'
    elif what == 'name':
        target_key = 'name'
    elif what == 'url':
        target_key = 'url'
    else:
        print_color(f'Wrong filter target: {what}', 'red')
        sys.exit()

    for key, value in self.bookmarks.copy().items():
        if list(set(target) & set(list([key] if target_key == 'name' else value[target_key]))):
            del self.bookmarks[key]
def drop_substring(self, target, what)
Expand source code
def drop_substring(self, target, what):
    if isinstance(target, str):
        target = [target]

    # What
    if what == 'dir':
        target_key = 'tags'
    elif what == 'name':
        target_key = 'name'
    elif what == 'url':
        target_key = 'url'
    else:
        print_color(f'Wrong filter target: {what}', 'red')
        sys.exit()

    i = 0
    for key, value in self.bookmarks.copy().items():
        rm = False
        while not rm:
            for string in key if target_key == 'name' else value[target_key]:
                for substring in target:
                    if substring in string:
                        rm = True
            break
        if rm:
            del self.bookmarks[key]
def loop(self, f, loglevel=0, pb_label=None, pb_leave=True, pb_width=110)

:param f: Function - To be run on each entry of the bookmark dictionary. :param loglevel: Python standard library logging-style log levels. - 0 : Full log output. - 20: Visualize progress with tqdm progress bar. - 50: Suppress all output. :param pb_label: Str - Progress bar label. :param pb_leave: Boolean - False to remove progress bar from screen after completion. :param pb_width: N - Width in char of the progress bar. :return: List with all [key, value] pairs for which f execution resulted in an error.

Expand source code
def loop(self, f,
         loglevel=0,
         pb_label=None,
         pb_leave=True,
         pb_width=110,
         ):
    """
    :param f: Function - To be run on each entry of the bookmark dictionary.
    :param loglevel: Python standard library logging-style log levels.
        - 0 : Full log output.
        - 20: Visualize progress with tqdm progress bar.
        - 50: Suppress all output.
    :param pb_label: Str - Progress bar label.
    :param pb_leave: Boolean - False to remove progress bar from screen after completion.
    :param pb_width: N - Width in char of the progress bar.
    :return: List with all [key, value] pairs for which `f` execution resulted in an error.
    """

    e = {}
    t0 = datetime.datetime.now()
    step = 1
    total = len(self.bookmarks)

    if loglevel == 20:                                          # Create tqdm progress bar if specified
        pgr = tqdm(self.bookmarks.items(),
                   ncols=pb_width,
                   position=0,
                   leave=pb_leave,
                   desc=pb_label,
                   bar_format="{l_bar}"
                              "%s{bar}"
                              "%s| {n_fmt}/{total_fmt} "
                              "[{elapsed}<%s{remaining}%s,"
                              " {rate_fmt}{postfix}]" % (colors.fgYellow, colors.reset,
                                                         colors.fgRed, colors.reset)
                   )

    for key, value in self.bookmarks.items():
        if loglevel == 0:
            # Time estimate
            tn          = datetime.datetime.now()
            t_elapsed   = tn - t0
            t_remaining = step/max(t_elapsed.total_seconds(), 10**-6)*(total-step)

            print(str_log_progress(f"{step}/{total}", t_elapsed.total_seconds(), t_remaining))
            step += 1

        try:                                        # Attempt to run provided function on dictionary
            if loglevel in [20, 50]:
                with suppress_stdout():             # Suppress function output if so specified
                    f(key, value)
            else:
                f(key, value)
        except BaseException as exception:          # Error: add entry to error list
            e[key] = {'url': value['url'],
                      'Error message': str(exception)}

        if loglevel == 0:
            print("")
        if loglevel == 20:                          # Update progress bar if in use
            pgr.update()

    if loglevel == 20:
        print("\r" + " "*pb_width, end="\r")            # Clean console from debris left by tqdm

    return e
def search_children(self, children)

Iterate over children.

:param children: List entries in bookmark dictionary, which are by custom named "children" in bookmark dictionaries.

Expand source code
def search_children(self, children):
    """
    Iterate over children.

    :param children: List entries in bookmark dictionary, which are by custom named "children" in
                     bookmark dictionaries.
    """
    for child in children:
        self.search_dict(child)
def search_dict(self, dictionary)

Reduce depth-n bookmark dictionary to depth-1 dictionary of -Name: Dict - pairs, where

Dict: {"url": url,
       "tags": tag_list}

and tag_list is a list containing the ordered names of the parent folders of each link.

:param dictionary: Bookmark dictionary from which - Name: [link, tags] - pairs are to be extracted. :return: Depth-1 dictionary of - Name: [link, tags] - pairs.

Expand source code
def search_dict(self, dictionary):
    """
    Reduce depth-n bookmark dictionary to depth-1 dictionary of -Name: Dict - pairs, where

        Dict: {"url": url,
               "tags": tag_list}

    and tag_list is a list containing the ordered names of the parent folders of each link.

    :param dictionary: Bookmark dictionary from which - Name: [link, tags] - pairs are to be extracted.
    :return: Depth-1 dictionary of - Name: [link, tags] - pairs.
    """
    if "children" in dictionary.keys():
        self.tags.append(dictionary['name'])            # Append directory name to tag list
        self.n_dirs += 1                                # Keep track of the number of directories
        self.search_children(dictionary['children'])    # Search through children
        self.tag_backtrack(dictionary['name'])          # After search is over, "navigate" back to parent directory

    elif 'url' in dictionary.keys():
        # Conduct a regex search for the name of the bookmark among all those previously found.
        #
        # Avoids a name conflict caused by the nth duplicate with n>1 due to the first
        # duplicate having already a different name to the first one, as
        #
        #       name of first duplicate = <name> ::anchorage name duplicate:: 2
        #
        n_rep = len(list(filter(re.compile(f'{re.escape(dictionary["name"])}').match,
                                list(self.bookmarks.keys()))
                         ))
        #   1. re.escape
        #          Avoid issues with bookmarks with special regex sequences in their name
        #   2. re.compile(<bookmark name>
        #          Match any string with the entire bookmark name in it

        if n_rep > 0:
            key = dictionary['name'] + f" ::anchorage name duplicate:: {n_rep+1}"
        else:
            key = dictionary['name']

        self.bookmarks[key] = {'url': dictionary['url'],
                               'tags': self.tags.copy()}
    else:
        pass        # Account for possibly empty bookmark folders
def tag_backtrack(self, tag)

Remove tracked tags deeper than input tag. For a series of nested dictionaries (D) which may contain "children" lists (c)

    D
    |_ c[dir_name1]             tags = [dir_name1]
       |_ D
       |  |_ c[dirname_2]       tags = [dir_name1, dir_name2]
       |     |_ D
       |_ D                  !! tags = [dir_name1]

It is necessary to be able to "navigate" to the current parent directory after
iterating through any subdirectories.

The "path" to each link is kept in an ordered list. To "navigate" back after
iterating through any previously subdirectories, all elements in the tag list
after the specified (parent) tag are eliminated.
Expand source code
def tag_backtrack(self, tag):
    """
    Remove tracked tags deeper than input tag.
        For a series of nested dictionaries (D) which may contain "children" lists (c)

            D
            |_ c[dir_name1]             tags = [dir_name1]
               |_ D
               |  |_ c[dirname_2]       tags = [dir_name1, dir_name2]
               |     |_ D
               |_ D                  !! tags = [dir_name1]

        It is necessary to be able to "navigate" to the current parent directory after
        iterating through any subdirectories.

        The "path" to each link is kept in an ordered list. To "navigate" back after
        iterating through any previously subdirectories, all elements in the tag list
        after the specified (parent) tag are eliminated.
    """
    try:
        inv_index = self.tags[::-1].index(tag)
        index = len(self.tags) - 1 - inv_index
        self.tags = self.tags[:index]
    except ValueError:
        self.tags = [tag]