pyrevit.coreutils

Usage

from pyrevit import coreutils
coreutils.cleanup_string('some string')

Documentation

Misc Helper functions for pyRevit.

class pyrevit.coreutils.FileWatcher(filepath)

Simple file version watcher.

This is a simple utility class to look for changes in a file based on its timestamp.

Example

>>> watcher = FileWatcher('/path/to/file.ext')
>>> watcher.has_changed
True
has_changed

Compare current file timestamp to the cached timestamp.

update_tstamp()

Update the cached timestamp for later comparison.

class pyrevit.coreutils.SafeDict

Dictionary that does not fail on any key.

This is a dictionary subclass to help with string formatting with unknown key values.

Example

>>> string = '{target} {attr} is {color}.'
>>> safedict = SafeDict({'target': 'Apple',
...                      'attr':   'Color'})
>>> string.format(safedict)  # will not fail with missing 'color' key
'Apple Color is {color}.'
class pyrevit.coreutils.ScriptFileParser(file_address)

Parse python script to extract variables and docstrings.

Primarily designed to assist pyRevit in determining script configurations but can work for any python script.

Example

>>> finder = ScriptFileParser('/path/to/coreutils/__init__.py')
>>> finder.docstring()
... "Misc Helper functions for pyRevit."
>>> finder.extract_param('SomeValue', [])
[]
extract_param(param_name, default_value=None)

Find variable and extract its value.

Parameters:
  • param_name (str) – variable name
  • default_value (any) – default value to be returned if variable does not exist
Returns:

value of the variable or None

Return type:

any

get_docstring()

Get global docstring.

class pyrevit.coreutils.Timer

Timer class using python native time module.

Example

>>> timer = Timer()
>>> timer.get_time()
12
get_time()

Get Elapsed Time.

restart()

Restart Timer.

pyrevit.coreutils.calculate_dir_hash(dir_path, dir_filter, file_filter)

Create a unique hash to represent state of directory.

Parameters:
  • dir_path (str) – target directory
  • dir_filter (str) – exclude directories matching this regex
  • file_filter (str) – exclude files matching this regex
Returns:

hash value as string

Return type:

str

Example

>>> calculate_dir_hash(source_path, '\.extension', '\.json')
"1a885a0cae99f53d6088b9f7cee3bf4d"
pyrevit.coreutils.check_internet_connection(timeout=1000)

Check if internet connection is available.

Pings a few well-known websites to check if internet connection is present.

Parameters:timeout (int) – timeout in milliseconds
Returns:True if internet connection is present.
Return type:bool
pyrevit.coreutils.cleanup_filename(file_name)

Cleanup file name from special characters.

Parameters:file_name (str) – file name
Returns:cleaned up file name
Return type:str

Example

>>> cleanup_filename('Myfile-(3).txt')
"Myfile3.txt"
pyrevit.coreutils.cleanup_string(input_str)

Replace special characters in string with another string.

This function was created to help cleanup pyRevit command unique names from any special characters so C# class names can be created based on those unique names.

coreutils.SPECIAL_CHARS is the conversion table for this function.

Parameters:input_str (str) – input string to be cleaned

Example

>>> src_str = 'TEST@Some*<value>'
>>> cleanup_string(src_str)
"TESTATSomeSTARvalue"
pyrevit.coreutils.create_ext_command_attrs()

Create dotnet attributes for Revit extenrnal commads.

This method is used in creating custom dotnet types for pyRevit commands and compiling them into a DLL assembly. Current implementation sets RegenerationOption.Manual and TransactionMode.Manual

Returns:list of CustomAttributeBuilder for RegenerationOption and TransactionMode attributes.
Return type:list
pyrevit.coreutils.create_type(modulebuilder, type_class, class_name, custom_attr_list, *args)

Create a dotnet type for a pyRevit command.

See baseclasses.cs code for the template pyRevit command dotnet type and its constructor default arguments that must be provided here.

Parameters:
  • modulebuilder (ModuleBuilder) – dotnet module builder
  • type_class (type) – source dotnet type for the command
  • class_name (str) – name for the new type
  • custom_attr_list (list) – list of dotnet attributes for the type
  • *args – list of arguments to be used with type constructor
Returns:

returns created dotnet type

Return type:

type

Example

>>> asm_builder = AppDomain.CurrentDomain.DefineDynamicAssembly(
... win_asm_name, AssemblyBuilderAccess.RunAndSave, filepath
... )
>>> module_builder = asm_builder.DefineDynamicModule(
... ext_asm_file_name, ext_asm_full_file_name
... )
>>> create_type(
... module_builder,
... PyRevitCommand,
... "PyRevitSomeCommandUniqueName",
... coreutils.create_ext_command_attrs(),
... [scriptpath, atlscriptpath, searchpath, helpurl, name,
... bundle, extension, uniquename, False, False])
<type PyRevitSomeCommandUniqueName>
pyrevit.coreutils.current_date()

Return formatted current date.

Current implementation uses %Y-%m-%d to format date.

Returns:formatted current date.
Return type:str

Example

>>> current_date()
'2018-01-03'
pyrevit.coreutils.current_time()

Return formatted current time.

Current implementation uses %H:%M:%S to format time.

Returns:formatted current time.
Return type:str

Example

>>> current_time()
'07:50:53'
pyrevit.coreutils.decrement_str(input_str, step)

Decrement identifier.

Parameters:
  • input_str (str) – identifier e.g. A310a
  • step (int) – number of steps to change the identifier
Returns:

modified identifier

Return type:

str

Example

>>> decrement_str('A310a')
'A309z'
pyrevit.coreutils.dletter_to_unc(dletter_path)

Convert drive letter path into UNC path of that drive.

Parameters:dletter_path (str) – drive letter path
Returns:UNC path
Return type:str

Example

>>> # assuming J: is mapped to //filestore/server/jdrive
>>> dletter_to_unc('J:/somefile.txt')
'//filestore/server/jdrive/somefile.txt'
pyrevit.coreutils.exract_range(formatted_str, max_range=500)

Extract range from formatted string.

String must be formatted as below A103 No range A103-A106 A103 to A106 A103:A106 A103 to A106 A103,A105a A103 and A105a A103;A105a A103 and A105a

Parameters:formatted_str (str) – string specifying range
Returns:list of names in the specified range
Return type:list

Example

>>> exract_range('A103:A106')
['A103', 'A104', 'A105', 'A106']
>>> exract_range('S203-S206')
['S203', 'S204', 'S205', 'S206']
>>> exract_range('M00A,M00B')
['M00A', 'M00B']
pyrevit.coreutils.filter_null_items(src_list)

Remove None items in the given list.

Parameters:src_list (list) – list of any items
Returns:cleaned list
Return type:list
pyrevit.coreutils.find_loaded_asm(asm_info, by_partial_name=False, by_location=False)

Find loaded assembly based on name, partial name, or location.

Parameters:
  • asm_info (str) – name or location of the assembly
  • by_partial_name (bool) – returns all assemblies that has the asm_info
  • by_location (bool) – returns all assemblies matching location
Returns:

List of all loaded assemblies matching the provided info If only one assembly has been found, it returns the assembly. None will be returned if assembly is not loaded.

Return type:

list

pyrevit.coreutils.find_type_by_name(assembly, type_name)

Find type by name in assembly.

Parameters:
  • assembly (Assembly) – assembly to find the type in
  • type_name (str) – type name
Returns:

returns the type if found.

Raises:

PyRevitException if type not found.

pyrevit.coreutils.fully_remove_dir(dir_path)

Remove directory recursively.

Parameters:dir_path (str) – directory path
pyrevit.coreutils.get_all_subclasses(parent_classes)

Return all subclasses of a python class.

Parameters:parent_classes (list) – list of python classes
Returns:list of python subclasses
Return type:list
pyrevit.coreutils.get_file_name(file_path)

Return file basename of the given file.

Parameters:file_path (str) – file path
pyrevit.coreutils.get_mapped_drives_dict()

Return a dictionary of currently mapped network drives.

pyrevit.coreutils.get_revit_instance_count()

Return number of open host app instances.

Returns:number of open host app instances.
Return type:int
pyrevit.coreutils.get_str_hash(source_str)

Calculate hash value of given string.

Current implementation uses hashlib.md5() hash function.

Parameters:source_str (str) – source str
Returns:hash value as string
Return type:str
pyrevit.coreutils.get_sub_folders(search_folder)

Get a list of all subfolders directly inside provided folder.

Parameters:search_folder (str) – folder path
Returns:list of subfolder names
Return type:list
pyrevit.coreutils.increment_str(input_str, step)

Incremenet identifier.

Parameters:
  • input_str (str) – identifier e.g. A310a
  • step (int) – number of steps to change the identifier
Returns:

modified identifier

Return type:

str

Example

>>> increment_str('A319z')
'A320a'
pyrevit.coreutils.inspect_calling_scope_global_var(variable_name)

Trace back the stack to find the variable in the caller global stack.

Parameters:variable_name (str) – variable name to look up in caller global scope
pyrevit.coreutils.inspect_calling_scope_local_var(variable_name)

Trace back the stack to find the variable in the caller local stack.

PyRevitLoader defines __revit__ in builtins and __window__ in locals. Thus, modules have access to __revit__ but not to __window__. This function is used to find __window__ in the caller stack.

Parameters:variable_name (str) – variable name to look up in caller local scope
pyrevit.coreutils.is_blank(input_string)

Check if input string is blank (multiple white spaces is blank).

Parameters:input_string (str) – input string
Returns:True if string is blank
Return type:bool

Example

>>> is_blank('   ')
True
pyrevit.coreutils.is_url_valid(url_string)

Check if given URL is in valid format.

Parameters:url_string (str) – URL string
Returns:True if URL is in valid format
Return type:bool

Example

>>> is_url_valid('https://www.google.com')
True
pyrevit.coreutils.join_strings(str_list, separator=';')

Join strings using provided separator.

Parameters:
  • str_list (list) – list of string values
  • separator (str) – single separator character, defaults to DEFAULT_SEPARATOR
Returns:

joined string

Return type:

str

pyrevit.coreutils.load_asm(asm_name)

Load assembly by name into current domain.

Parameters:asm_name (str) – assembly name
Returns:returns the loaded assembly, None if not loaded.
pyrevit.coreutils.load_asm_file(asm_file)

Load assembly by file into current domain.

Parameters:asm_file (str) – assembly file path
Returns:returns the loaded assembly, None if not loaded.
pyrevit.coreutils.make_canonical_name(*args)

Join arguments with dot creating a unique id.

Parameters:*args – Variable length argument list of type str
Returns:dot separated unique name
Return type:str

Example

>>> make_canonical_name('somename', 'someid', 'txt')
"somename.someid.txt"
pyrevit.coreutils.open_folder_in_explorer(folder_path)

Open given folder in Windows Explorer.

Parameters:folder_path (str) – directory path
pyrevit.coreutils.prepare_html_str(input_string)

Reformat html string and prepare for pyRevit output window.

pyRevit output window renders html content. But this means that < and > characters in outputs from python (e.g. <class at xxx>) will be treated as html tags. To avoid this, all <> characters that are defining html content need to be replaced with special phrases. pyRevit output later translates these phrases back in to < and >. That is how pyRevit ditinquishes between <> printed from python and <> that define html.

Parameters:input_string (str) – input html string

Example

>>> prepare_html_str('<p>Some text</p>')
"&clt;p&cgt;Some text&clt;/p&cgt;"
pyrevit.coreutils.random_alpha()

Return a random alpha value (between 0 and 1.00).

pyrevit.coreutils.random_color()

Return a random color channel value (between 0 and 255).

pyrevit.coreutils.random_hex_color()

Return a random color in hex format.

Example

>>> random_hex_color()
'#FF0000'
pyrevit.coreutils.random_rgb_color()

Return a random color in rgb format.

Example

>>> random_rgb_color()
'rgb(255, 0, 0)'
pyrevit.coreutils.random_rgba_color()

Return a random color in rgba format.

Example

>>> random_rgba_color()
'rgba(255, 0, 0, 0.5)'
pyrevit.coreutils.read_source_file(source_file_path)

Read text file and return contents.

Parameters:source_file_path (str) – target file path
Returns:file contents
Return type:str
Raises:PyRevitException on read error
pyrevit.coreutils.reformat_string(orig_str, orig_format, new_format)

Reformat a string into a new format.

Extracts information from a string based on a given pattern, and recreates a new string based on the given new pattern.

Parameters:
  • orig_str (str) – Original string to be reformatted
  • orig_format (str) – Pattern of the original str (data to be extracted)
  • new_format (str) – New pattern (how to recompose the data)
Returns:

Reformatted string

Return type:

str

Example

>>> reformat_string('150 - FLOOR/CEILING - WD - 1 HR - FLOOR ASSEMBLY',
                    '{section} - {loc} - {mat} - {rating} - {name}',
                    '{section}:{mat}:{rating} - {name} ({loc})'))
'150:WD:1 HR - FLOOR ASSEMBLY (FLOOR/CEILING)'
pyrevit.coreutils.reverse_dict(input_dict)

Reverse the key, value pairs.

Parameters:input_dict (dict) – source ordered dict
Returns:reversed dictionary
Return type:defaultdict

Example

>>> reverse_dict({1: 2, 3: 4})
defaultdict(<type 'list'>, {2: [1], 4: [3]})
pyrevit.coreutils.reverse_html(input_html)

Reformat codified pyRevit output html string back to normal html.

pyRevit output window renders html content. But this means that < and > characters in outputs from python (e.g. <class at xxx>) will be treated as html tags. To avoid this, all <> characters that are defining html content need to be replaced with special phrases. pyRevit output later translates these phrases back in to < and >. That is how pyRevit ditinquishes between <> printed from python and <> that define html.

Parameters:input_html (str) – input codified html string

Example

>>> prepare_html_str('&clt;p&cgt;Some text&clt;/p&cgt;')
"<p>Some text</p>"
pyrevit.coreutils.run_process(proc, cwd='')

Run shell process silently.

Parameters:
  • proc (str) – process executive name
  • cwd (str) – current working directory
Exmaple:
>>> run_process('notepad.exe', 'c:/')
pyrevit.coreutils.timestamp()

Return timestamp for current time.

Returns:timestamp in string format
Return type:str

Example

>>> timestamp()
'01003075032506808'
pyrevit.coreutils.touch(fname, times=None)

Update the timestamp on the given file.

Parameters:
  • fname (str) – target file path
  • times (int) – number of times to touch the file
pyrevit.coreutils.unc_to_dletter(unc_path)

Convert UNC path into drive letter path.

Parameters:unc_path (str) – UNC path
Returns:drive letter path
Return type:str

Example

>>> # assuming J: is mapped to //filestore/server/jdrive
>>> unc_to_dletter('//filestore/server/jdrive/somefile.txt')
'J:/somefile.txt'
pyrevit.coreutils.verify_directory(folder)

Check if the folder exists and if not create the folder.

Parameters:folder (str) – path of folder to verify
Returns:path of verified folder, equals to provided folder
Return type:str
Raises:OSError on folder creation error.

Implementation

"""Misc Helper functions for pyRevit."""

import os
import os.path as op
import re
import ast
import hashlib
import time
import datetime
import shutil
import random
import stat
from collections import defaultdict

from pyrevit import HOST_APP, PyRevitException
from pyrevit.compat import safe_strtype
from pyrevit import framework
from pyrevit import api


# pylama:ignore=D105
DEFAULT_SEPARATOR = ';'


class Timer:
    """Timer class using python native time module.

    Example:
        >>> timer = Timer()
        >>> timer.get_time()
        12
    """

    def __init__(self):
        """Initialize and Start Timer."""
        self.start = time.time()

    def restart(self):
        """Restart Timer."""
        self.start = time.time()

    def get_time(self):
        """Get Elapsed Time."""
        return time.time() - self.start


class ScriptFileParser:
    """Parse python script to extract variables and docstrings.

    Primarily designed to assist pyRevit in determining script configurations
    but can work for any python script.

    Example:
        >>> finder = ScriptFileParser('/path/to/coreutils/__init__.py')
        >>> finder.docstring()
        ... "Misc Helper functions for pyRevit."
        >>> finder.extract_param('SomeValue', [])
        []
    """

    def __init__(self, file_address):
        """Initialize and read provided python script.

        Args:
            file_address (str): python script file path
        """
        self.file_addr = file_address
        with open(file_address, 'r') as f:
            self.ast_tree = ast.parse(f.read())

    def get_docstring(self):
        """Get global docstring."""
        doc_str = ast.get_docstring(self.ast_tree)
        if doc_str:
            return doc_str.decode('utf-8')
        return None

    def extract_param(self, param_name, default_value=None):
        """Find variable and extract its value.

        Args:
            param_name (str): variable name
            default_value (any):
                default value to be returned if variable does not exist

        Returns:
            any: value of the variable or :obj:`None`
        """
        try:
            for child in ast.iter_child_nodes(self.ast_tree):
                if hasattr(child, 'targets'):
                    for target in child.targets:
                        if hasattr(target, 'id') and target.id == param_name:
                            param_value = ast.literal_eval(child.value)
                            if isinstance(param_value, str):
                                param_value = param_value.decode('utf-8')
                            return param_value
        except Exception as err:
            raise PyRevitException('Error parsing parameter: {} '
                                   'in script file for : {} | {}'
                                   .format(param_name, self.file_addr, err))

        return default_value


class FileWatcher(object):
    """Simple file version watcher.

    This is a simple utility class to look for changes in a file based on
    its timestamp.

    Example:
        >>> watcher = FileWatcher('/path/to/file.ext')
        >>> watcher.has_changed
        True
    """

    def __init__(self, filepath):
        """Initialize and read timestamp of provided file.

        Args:
            filepath (str): file path
        """
        self._cached_stamp = 0
        self._filepath = filepath
        self.update_tstamp()

    def update_tstamp(self):
        """Update the cached timestamp for later comparison."""
        self._cached_stamp = os.stat(self._filepath).st_mtime

    @property
    def has_changed(self):
        """Compare current file timestamp to the cached timestamp."""
        return os.stat(self._filepath).st_mtime != self._cached_stamp


class SafeDict(dict):
    """Dictionary that does not fail on any key.

    This is a dictionary subclass to help with string formatting with unknown
    key values.

    Example:
        >>> string = '{target} {attr} is {color}.'
        >>> safedict = SafeDict({'target': 'Apple',
        ...                      'attr':   'Color'})
        >>> string.format(safedict)  # will not fail with missing 'color' key
        'Apple Color is {color}.'
    """

    def __missing__(self, key):
        return '{' + key + '}'


def get_all_subclasses(parent_classes):
    """Return all subclasses of a python class.

    Args:
        parent_classes (list): list of python classes

    Returns:
        list: list of python subclasses
    """
    sub_classes = []
    # if super-class, get a list of sub-classes.
    # Otherwise use component_class to create objects.
    for parent_class in parent_classes:
        try:
            derived_classes = parent_class.__subclasses__()
            if len(derived_classes) == 0:
                sub_classes.append(parent_class)
            else:
                sub_classes.extend(derived_classes)
        except AttributeError:
            sub_classes.append(parent_class)
    return sub_classes


def get_sub_folders(search_folder):
    """Get a list of all subfolders directly inside provided folder.

    Args:
        search_folder (str): folder path

    Returns:
        list: list of subfolder names
    """
    sub_folders = []
    for f in os.listdir(search_folder):
        if op.isdir(op.join(search_folder, f)):
            sub_folders.append(f)
    return sub_folders


def verify_directory(folder):
    """Check if the folder exists and if not create the folder.

    Args:
        folder (str): path of folder to verify

    Returns:
        str: path of verified folder, equals to provided folder

    Raises:
        OSError on folder creation error.
    """
    if not op.exists(folder):
        try:
            os.makedirs(folder)
        except OSError as err:
            raise err
    return folder


def join_strings(str_list, separator=DEFAULT_SEPARATOR):
    """Join strings using provided separator.

    Args:
        str_list (list): list of string values
        separator (str): single separator character,
            defaults to DEFAULT_SEPARATOR

    Returns:
        str: joined string
    """
    if str_list:
        return separator.join(str_list)
    return ''


# character replacement list for cleaning up file names
SPECIAL_CHARS = {' ': '',
                 '~': '',
                 '!': 'EXCLAM',
                 '@': 'AT',
                 '#': 'SHARP',
                 '$': 'DOLLAR',
                 '%': 'PERCENT',
                 '^': '',
                 '&': 'AND',
                 '*': 'STAR',
                 '+': 'PLUS',
                 ';': '', ':': '', ',': '', '\"': '',
                 '{': '', '}': '', '[': '', ']': '', '\(': '', '\)': '',
                 '-': 'MINUS',
                 '=': 'EQUALS',
                 '<': '', '>': '',
                 '?': 'QMARK',
                 '.': 'DOT',
                 '_': 'UNDERS',
                 '|': 'VERT',
                 '\/': '', '\\': ''}


def cleanup_string(input_str):
    """Replace special characters in string with another string.

    This function was created to help cleanup pyRevit command unique names from
    any special characters so C# class names can be created based on those
    unique names.

    ``coreutils.SPECIAL_CHARS`` is the conversion table for this function.

    Args:
        input_str (str): input string to be cleaned

    Example:
        >>> src_str = 'TEST@Some*<value>'
        >>> cleanup_string(src_str)
        "TESTATSomeSTARvalue"
    """
    # remove spaces and special characters from strings
    for char, repl in SPECIAL_CHARS.items():
        input_str = input_str.replace(char, repl)

    return input_str


def get_revit_instance_count():
    """Return number of open host app instances.

    Returns:
        int: number of open host app instances.
    """
    return len(list(framework.Process.GetProcessesByName(HOST_APP.proc_name)))


def run_process(proc, cwd=''):
    """Run shell process silently.

    Args:
        proc (str): process executive name
        cwd (str): current working directory

    Exmaple:
        >>> run_process('notepad.exe', 'c:/')
    """
    import subprocess
    return subprocess.Popen(proc,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                            cwd=cwd, shell=True)


def inspect_calling_scope_local_var(variable_name):
    """Trace back the stack to find the variable in the caller local stack.

    PyRevitLoader defines __revit__ in builtins and __window__ in locals.
    Thus, modules have access to __revit__ but not to __window__.
    This function is used to find __window__ in the caller stack.

    Args:
        variable_name (str): variable name to look up in caller local scope
    """
    import inspect

    frame = inspect.stack()[1][0]
    while variable_name not in frame.f_locals:
        frame = frame.f_back
        if frame is None:
            return None
    return frame.f_locals[variable_name]


def inspect_calling_scope_global_var(variable_name):
    """Trace back the stack to find the variable in the caller global stack.

    Args:
        variable_name (str): variable name to look up in caller global scope
    """
    import inspect

    frame = inspect.stack()[1][0]
    while variable_name not in frame.f_globals:
        frame = frame.f_back
        if frame is None:
            return None
    return frame.f_locals[variable_name]


def find_loaded_asm(asm_info, by_partial_name=False, by_location=False):
    """Find loaded assembly based on name, partial name, or location.

    Args:
        asm_info (str): name or location of the assembly
        by_partial_name (bool): returns all assemblies that has the asm_info
        by_location (bool): returns all assemblies matching location

    Returns:
        list: List of all loaded assemblies matching the provided info
        If only one assembly has been found, it returns the assembly.
        :obj:`None` will be returned if assembly is not loaded.
    """
    loaded_asm_list = []
    for loaded_assembly in framework.AppDomain.CurrentDomain.GetAssemblies():
        if by_partial_name:
            if asm_info.lower() in \
                    safe_strtype(loaded_assembly.GetName().Name).lower():
                loaded_asm_list.append(loaded_assembly)
        elif by_location:
            try:
                if op.normpath(loaded_assembly.Location) == \
                        op.normpath(asm_info):
                    loaded_asm_list.append(loaded_assembly)
            except Exception:
                continue
        elif asm_info.lower() == \
                safe_strtype(loaded_assembly.GetName().Name).lower():
            loaded_asm_list.append(loaded_assembly)

    return loaded_asm_list


def load_asm(asm_name):
    """Load assembly by name into current domain.

    Args:
        asm_name (str): assembly name

    Returns:
        returns the loaded assembly, None if not loaded.
    """
    return framework.AppDomain.CurrentDomain.Load(asm_name)


def load_asm_file(asm_file):
    """Load assembly by file into current domain.

    Args:
        asm_file (str): assembly file path

    Returns:
        returns the loaded assembly, None if not loaded.
    """
    try:
        return framework.Assembly.LoadFrom(asm_file)
    except Exception:
        return None


def find_type_by_name(assembly, type_name):
    """Find type by name in assembly.

    Args:
        assembly (:obj:`Assembly`): assembly to find the type in
        type_name (str): type name

    Returns:
        returns the type if found.

    Raises:
        :obj:`PyRevitException` if type not found.
    """
    base_class = assembly.GetType(type_name)
    if base_class is not None:
        return base_class
    else:
        raise PyRevitException('Can not find base class type: {}'
                               .format(type_name))


def make_canonical_name(*args):
    """Join arguments with dot creating a unique id.

    Args:
        *args: Variable length argument list of type :obj:`str`

    Returns:
        str: dot separated unique name

    Example:
        >>> make_canonical_name('somename', 'someid', 'txt')
        "somename.someid.txt"
    """
    return '.'.join(args)


def get_file_name(file_path):
    """Return file basename of the given file.

    Args:
        file_path (str): file path
    """
    return op.splitext(op.basename(file_path))[0]


def get_str_hash(source_str):
    """Calculate hash value of given string.

    Current implementation uses :func:`hashlib.md5` hash function.

    Args:
        source_str (str): source str

    Returns:
        str: hash value as string
    """
    return hashlib.md5(source_str.encode('utf-8', 'ignore')).hexdigest()


def calculate_dir_hash(dir_path, dir_filter, file_filter):
    r"""Create a unique hash to represent state of directory.

    Args:
        dir_path (str): target directory
        dir_filter (str): exclude directories matching this regex
        file_filter (str): exclude files matching this regex

    Returns:
        str: hash value as string

    Example:
        >>> calculate_dir_hash(source_path, '\.extension', '\.json')
        "1a885a0cae99f53d6088b9f7cee3bf4d"
    """
    mtime_sum = 0
    for root, dirs, files in os.walk(dir_path):
        if re.search(dir_filter, op.basename(root), flags=re.IGNORECASE):
            mtime_sum += op.getmtime(root)
            for filename in files:
                if re.search(file_filter, filename, flags=re.IGNORECASE):
                    modtime = op.getmtime(op.join(root, filename))
                    mtime_sum += modtime
    return get_str_hash(safe_strtype(mtime_sum))


def prepare_html_str(input_string):
    """Reformat html string and prepare for pyRevit output window.

    pyRevit output window renders html content. But this means that < and >
    characters in outputs from python (e.g. <class at xxx>) will be treated
    as html tags. To avoid this, all <> characters that are defining
    html content need to be replaced with special phrases. pyRevit output
    later translates these phrases back in to < and >. That is how pyRevit
    ditinquishes between <> printed from python and <> that define html.

    Args:
        input_string (str): input html string

    Example:
        >>> prepare_html_str('<p>Some text</p>')
        "&clt;p&cgt;Some text&clt;/p&cgt;"
    """
    return input_string.replace('<', '&clt;').replace('>', '&cgt;')


def reverse_html(input_html):
    """Reformat codified pyRevit output html string back to normal html.

    pyRevit output window renders html content. But this means that < and >
    characters in outputs from python (e.g. <class at xxx>) will be treated
    as html tags. To avoid this, all <> characters that are defining
    html content need to be replaced with special phrases. pyRevit output
    later translates these phrases back in to < and >. That is how pyRevit
    ditinquishes between <> printed from python and <> that define html.

    Args:
        input_html (str): input codified html string

    Example:
        >>> prepare_html_str('&clt;p&cgt;Some text&clt;/p&cgt;')
        "<p>Some text</p>"
    """
    return input_html.replace('&clt;', '<').replace('&cgt;', '>')


# def check_internet_connection():
#     client = framework.WebClient()
#     try:
#         client.OpenRead("http://www.google.com")
#         return True
#     except:
#         return False
#


# def check_internet_connection():
    # import urllib2
    #
    # def internet_on():
    #     try:
    #         urllib2.urlopen('http://216.58.192.142', timeout=1)
    #         return True
    #     except urllib2.URLError as err:
    #         return False


def check_internet_connection(timeout=1000):
    """Check if internet connection is available.

    Pings a few well-known websites to check if internet connection is present.

    Args:
        timeout (int): timeout in milliseconds

    Returns:
        bool: True if internet connection is present.
    """
    def can_access(url_to_open):
        try:
            client = framework.WebRequest.Create(url_to_open)
            client.Method = "HEAD"
            client.Timeout = timeout
            client.Proxy = framework.WebProxy.GetDefaultProxy()
            response = client.GetResponse()
            response.GetResponseStream()
            return True
        except Exception:
                return False

    for url in ["http://google.com/",
                "http://github.com/",
                "http://bitbucket.com/"]:
        if can_access(url):
            return url

    return False


def touch(fname, times=None):
    """Update the timestamp on the given file.

    Args:
        fname (str): target file path
        times (int): number of times to touch the file
    """
    with open(fname, 'a'):
        os.utime(fname, times)


def read_source_file(source_file_path):
    """Read text file and return contents.

    Args:
        source_file_path (str): target file path

    Returns:
        str: file contents

    Raises:
        :obj:`PyRevitException` on read error
    """
    try:
        with open(source_file_path, 'r') as code_file:
            return code_file.read()
    except Exception as read_err:
        raise PyRevitException('Error reading source file: {} | {}'
                               .format(source_file_path, read_err))


def create_ext_command_attrs():
    """Create dotnet attributes for Revit extenrnal commads.

    This method is used in creating custom dotnet types for pyRevit commands
    and compiling them into a DLL assembly. Current implementation sets
    ``RegenerationOption.Manual`` and ``TransactionMode.Manual``

    Returns:
        list: list of :obj:`CustomAttributeBuilder` for
        :obj:`RegenerationOption` and :obj:`TransactionMode` attributes.
    """
    regen_const_info = \
        framework.clr.GetClrType(api.Attributes.RegenerationAttribute) \
        .GetConstructor(
               framework.Array[framework.Type](
                   (api.Attributes.RegenerationOption,)
                   )
               )

    regen_attr_builder = \
        framework.CustomAttributeBuilder(
            regen_const_info,
            framework.Array[object](
                (api.Attributes.RegenerationOption.Manual,)
                )
            )

    # add TransactionAttribute to framework.Type
    trans_constructor_info = \
        framework.clr.GetClrType(api.Attributes.TransactionAttribute) \
        .GetConstructor(
               framework.Array[framework.Type](
                   (api.Attributes.TransactionMode,)
                   )
               )

    trans_attrib_builder = \
        framework.CustomAttributeBuilder(
            trans_constructor_info,
            framework.Array[object](
                (api.Attributes.TransactionMode.Manual,)
                )
            )

    return [regen_attr_builder, trans_attrib_builder]


def create_type(modulebuilder,
                type_class, class_name, custom_attr_list, *args):
    """Create a dotnet type for a pyRevit command.

    See ``baseclasses.cs`` code for the template pyRevit command dotnet type
    and its constructor default arguments that must be provided here.

    Args:
        modulebuilder (:obj:`ModuleBuilder`): dotnet module builder
        type_class (type): source dotnet type for the command
        class_name (str): name for the new type
        custom_attr_list (:obj:`list`): list of dotnet attributes for the type
        *args: list of arguments to be used with type constructor

    Returns:
        type: returns created dotnet type

    Example:
        >>> asm_builder = AppDomain.CurrentDomain.DefineDynamicAssembly(
        ... win_asm_name, AssemblyBuilderAccess.RunAndSave, filepath
        ... )
        >>> module_builder = asm_builder.DefineDynamicModule(
        ... ext_asm_file_name, ext_asm_full_file_name
        ... )
        >>> create_type(
        ... module_builder,
        ... PyRevitCommand,
        ... "PyRevitSomeCommandUniqueName",
        ... coreutils.create_ext_command_attrs(),
        ... [scriptpath, atlscriptpath, searchpath, helpurl, name,
        ... bundle, extension, uniquename, False, False])
        <type PyRevitSomeCommandUniqueName>
    """
    # create type builder
    type_builder = \
        modulebuilder.DefineType(
            class_name,
            framework.TypeAttributes.Class | framework.TypeAttributes.Public,
            type_class
            )

    for custom_attr in custom_attr_list:
        type_builder.SetCustomAttribute(custom_attr)

    # prepare a list of input param types to find the matching constructor
    type_list = []
    param_list = []
    for param in args:
        if type(param) == str \
                or type(param) == int:
            type_list.append(type(param))
            param_list.append(param)

    # call base constructor
    ci = type_class.GetConstructor(framework.Array[framework.Type](type_list))
    # create class constructor builder
    const_builder = \
        type_builder.DefineConstructor(framework.MethodAttributes.Public,
                                       framework.CallingConventions.Standard,
                                       framework.Array[framework.Type](()))
    # add constructor parameters to stack
    gen = const_builder.GetILGenerator()
    gen.Emit(framework.OpCodes.Ldarg_0)  # Load "this" onto eval stack

    # add constructor input params to the stack
    for param_type, param in zip(type_list, param_list):
        if param_type == str:
            gen.Emit(framework.OpCodes.Ldstr, param)
        elif param_type == int:
            gen.Emit(framework.OpCodes.Ldc_I4, param)

    # call base constructor (consumes "this" and the created stack)
    gen.Emit(framework.OpCodes.Call, ci)
    # Fill some space - this is how it is generated for equivalent C# code
    gen.Emit(framework.OpCodes.Nop)
    gen.Emit(framework.OpCodes.Nop)
    gen.Emit(framework.OpCodes.Nop)
    gen.Emit(framework.OpCodes.Ret)
    type_builder.CreateType()


def open_folder_in_explorer(folder_path):
    """Open given folder in Windows Explorer.

    Args:
        folder_path (str): directory path
    """
    import subprocess
    subprocess.Popen(r'explorer /open,"{}"'
                     .format(os.path.normpath(folder_path)))


def fully_remove_dir(dir_path):
    """Remove directory recursively.

    Args:
        dir_path (str): directory path
    """
    def del_rw(action, name, exc):
        os.chmod(name, stat.S_IWRITE)
        os.remove(name)

    shutil.rmtree(dir_path, onerror=del_rw)


def cleanup_filename(file_name):
    """Cleanup file name from special characters.

    Args:
        file_name (str): file name

    Returns:
        str: cleaned up file name

    Example:
        >>> cleanup_filename('Myfile-(3).txt')
        "Myfile3.txt"
    """
    return re.sub('[^\w_.)( -]', '', file_name)


def _inc_or_dec_string(str_id, shift):
    """Increment or decrement identifier.

    Args:
        str_id (str): identifier e.g. A310a
        shift (int): number of steps to change the identifier

    Returns:
        str: modified identifier

    Example:
        >>> _inc_or_dec_string('A319z')
        'A320a'
    """
    next_str = ""
    index = len(str_id) - 1
    carry = shift

    while index >= 0:
        if str_id[index].isalpha():
            if str_id[index].islower():
                reset_a = 'a'
                reset_z = 'z'
            else:
                reset_a = 'A'
                reset_z = 'Z'

            curr_digit = (ord(str_id[index]) + carry)
            if curr_digit < ord(reset_a):
                curr_digit = ord(reset_z) - ((ord(reset_a) - curr_digit) - 1)
                carry = shift
            elif curr_digit > ord(reset_z):
                curr_digit = ord(reset_a) + ((curr_digit - ord(reset_z)) - 1)
                carry = shift
            else:
                carry = 0

            curr_digit = chr(curr_digit)
            next_str += curr_digit

        elif str_id[index].isdigit():

            curr_digit = int(str_id[index]) + carry
            if curr_digit > 9:
                curr_digit = 0 + ((curr_digit - 9)-1)
                carry = shift
            elif curr_digit < 0:
                curr_digit = 9 - ((0 - curr_digit)-1)
                carry = shift
            else:
                carry = 0
            next_str += safe_strtype(curr_digit)

        else:
            next_str += str_id[index]

        index -= 1

    return next_str[::-1]


def increment_str(input_str, step):
    """Incremenet identifier.

    Args:
        input_str (str): identifier e.g. A310a
        step (int): number of steps to change the identifier

    Returns:
        str: modified identifier

    Example:
        >>> increment_str('A319z')
        'A320a'
    """
    return _inc_or_dec_string(input_str, abs(step))


def decrement_str(input_str, step):
    """Decrement identifier.

    Args:
        input_str (str): identifier e.g. A310a
        step (int): number of steps to change the identifier

    Returns:
        str: modified identifier

    Example:
        >>> decrement_str('A310a')
        'A309z'
    """
    return _inc_or_dec_string(input_str, -abs(step))


def filter_null_items(src_list):
    """Remove None items in the given list.

    Args:
        src_list (:obj:`list`): list of any items

    Returns:
        :obj:`list`: cleaned list
    """
    return list(filter(bool, src_list))


def reverse_dict(input_dict):
    """Reverse the key, value pairs.

    Args:
        input_dict (:obj:`dict`): source ordered dict

    Returns:
        :obj:`defaultdict`: reversed dictionary

    Example:
        >>> reverse_dict({1: 2, 3: 4})
        defaultdict(<type 'list'>, {2: [1], 4: [3]})
    """
    output_dict = defaultdict(list)
    for key, value in input_dict.items():
        output_dict[value].append(key)
    return output_dict


def timestamp():
    """Return timestamp for current time.

    Returns:
        str: timestamp in string format

    Example:
        >>> timestamp()
        '01003075032506808'
    """
    return datetime.datetime.now().strftime("%m%j%H%M%S%f")


def current_time():
    """Return formatted current time.

    Current implementation uses %H:%M:%S to format time.

    Returns:
        str: formatted current time.

    Example:
        >>> current_time()
        '07:50:53'
    """
    return datetime.datetime.now().strftime("%H:%M:%S")


def current_date():
    """Return formatted current date.

    Current implementation uses %Y-%m-%d to format date.

    Returns:
        str: formatted current date.

    Example:
        >>> current_date()
        '2018-01-03'
    """
    return datetime.datetime.now().strftime("%Y-%m-%d")


def is_blank(input_string):
    """Check if input string is blank (multiple white spaces is blank).

    Args:
        input_string (str): input string

    Returns:
        bool: True if string is blank

    Example:
        >>> is_blank('   ')
        True
    """
    if input_string and input_string.strip():
        return False
    return True


def is_url_valid(url_string):
    """Check if given URL is in valid format.

    Args:
        url_string (str): URL string

    Returns:
        bool: True if URL is in valid format

    Example:
        >>> is_url_valid('https://www.google.com')
        True
    """
    regex = re.compile(
            r'^(?:http|ftp)s?://'                   # http:// or https://
            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
            r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
            r'localhost|'                           # localhost...
            r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'  # ...or ip
            r'(?::\d+)?'                            # optional port
            r'(?:/?|[/?]\S+)$', re.IGNORECASE)

    return regex.match(url_string)


def reformat_string(orig_str, orig_format, new_format):
    """Reformat a string into a new format.

    Extracts information from a string based on a given pattern,
    and recreates a new string based on the given new pattern.

    Args:
        orig_str (str): Original string to be reformatted
        orig_format (str): Pattern of the original str (data to be extracted)
        new_format (str): New pattern (how to recompose the data)

    Returns:
        str: Reformatted string

    Example:
        >>> reformat_string('150 - FLOOR/CEILING - WD - 1 HR - FLOOR ASSEMBLY',
                            '{section} - {loc} - {mat} - {rating} - {name}',
                            '{section}:{mat}:{rating} - {name} ({loc})'))
        '150:WD:1 HR - FLOOR ASSEMBLY (FLOOR/CEILING)'
    """
    # find the tags
    tag_extractor = re.compile('{(.*?)}')
    tags = tag_extractor.findall(orig_format)

    # replace the tags with regex patterns
    # to create a regex pattern that finds values
    tag_replacer = re.compile('{.*?}')
    value_extractor_pattern = tag_replacer.sub('(.+)', orig_format)
    # find all values
    value_extractor = re.compile(value_extractor_pattern)
    values = value_extractor.findall(orig_str)
    if len(values) > 0:
        values = values[0]

    # create a dictionary of tags and values
    reformat_dict = {}
    for k, v in zip(tags, values):
        reformat_dict[k] = v

    # use dictionary to reformat the string into new
    return new_format.format(**reformat_dict)


def get_mapped_drives_dict():
    """Return a dictionary of currently mapped network drives."""
    searcher = framework.ManagementObjectSearcher(
        "root\\CIMV2",
        "SELECT * FROM Win32_MappedLogicalDisk"
        )

    return {x['DeviceID']: x['ProviderName'] for x in searcher.Get()}


def dletter_to_unc(dletter_path):
    """Convert drive letter path into UNC path of that drive.

    Args:
        dletter_path (str): drive letter path

    Returns:
        str: UNC path

    Example:
        >>> # assuming J: is mapped to //filestore/server/jdrive
        >>> dletter_to_unc('J:/somefile.txt')
        '//filestore/server/jdrive/somefile.txt'
    """
    drives = get_mapped_drives_dict()
    dletter = dletter_path[:2]
    for mapped_drive, server_path in drives.items():
        if dletter.lower() == mapped_drive.lower():
            return dletter_path.replace(dletter, server_path)


def unc_to_dletter(unc_path):
    """Convert UNC path into drive letter path.

    Args:
        unc_path (str): UNC path

    Returns:
        str: drive letter path

    Example:
        >>> # assuming J: is mapped to //filestore/server/jdrive
        >>> unc_to_dletter('//filestore/server/jdrive/somefile.txt')
        'J:/somefile.txt'
    """
    drives = get_mapped_drives_dict()
    for mapped_drive, server_path in drives.items():
        if server_path in unc_path:
            return unc_path.replace(server_path, mapped_drive)


def random_color():
    """Return a random color channel value (between 0 and 255)."""
    return random.randint(0, 255)


def random_alpha():
    """Return a random alpha value (between 0 and 1.00)."""
    return round(random.random(), 2)


def random_hex_color():
    """Return a random color in hex format.

    Example:
        >>> random_hex_color()
        '#FF0000'
    """
    return '#%02X%02X%02X' % (random_color(),
                              random_color(),
                              random_color())


def random_rgb_color():
    """Return a random color in rgb format.

    Example:
        >>> random_rgb_color()
        'rgb(255, 0, 0)'
    """
    return 'rgb(%d, %d, %d)' % (random_color(),
                                random_color(),
                                random_color())


def random_rgba_color():
    """Return a random color in rgba format.

    Example:
        >>> random_rgba_color()
        'rgba(255, 0, 0, 0.5)'
    """
    return 'rgba(%d, %d, %d, %.2f)' % (random_color(),
                                       random_color(),
                                       random_color(),
                                       random_alpha())


def exract_range(formatted_str, max_range=500):
    """Extract range from formatted string.

    String must be formatted as below
    A103            No range
    A103-A106       A103 to A106
    A103:A106       A103 to A106
    A103,A105a      A103 and A105a
    A103;A105a      A103 and A105a

    Args:
        formatted_str (str): string specifying range

    Returns:
        list: list of names in the specified range

    Example:
        >>> exract_range('A103:A106')
        ['A103', 'A104', 'A105', 'A106']
        >>> exract_range('S203-S206')
        ['S203', 'S204', 'S205', 'S206']
        >>> exract_range('M00A,M00B')
        ['M00A', 'M00B']
    """
    for rchar, rchartype in {':': 'range', '-': 'range',
                             ',': 'list', ';': 'list'}.items():
        if rchar in formatted_str:
            if rchartype == 'range' \
                    and formatted_str.count(rchar) == 1:
                items = []
                start, end = formatted_str.split(rchar)
                assert len(start) == len(end), \
                    'Range start and end must have same length'
                items.append(start)
                item = increment_str(start, 1)
                safe_counter = 0
                while item != end:
                    items.append(item)
                    item = increment_str(item, 1)
                    safe_counter += 1
                    assert safe_counter < max_range, 'Max range reached.'
                items.append(end)
                return items
            elif rchartype == 'list':
                return [x.strip() for x in formatted_str.split(rchar)]
    return [formatted_str]