# -*- coding: utf-8 -*-
import abc
import logging
import os
import re
import six
from frutils import dict_merge
from frutils.defaults import KEY_LUCI_NAME
from .defaults import *
from .exceptions import NoSuchDictletException
log = logging.getLogger("lucify")
DICTLET_CACHE = {}
[docs]def create_dictlet_finder(dictlet_finder_name, dictlet_finder_config=None):
"""Create a dictlet reader with the provided config.
Args:
dictlet_finder_name (str): the registered name (in setup.py entry point)
dictlet_finder_config (dict): optional configuration for the finder object
Returns:
DictletFinder: the finder object
"""
from .plugins import load_dictlet_finder_extension
if dictlet_finder_config is None:
dictlet_finder_config = {}
dictlet_finder = load_dictlet_finder_extension(
dictlet_finder_name, init_params=dictlet_finder_config
)
return dictlet_finder
[docs]def check_dictlet_content(file_path, regex):
"""Checks whether a dictlet file matches the specified regular expression.
Args:
file_path (str): the path to the dictlet
regex (str, _sre.SRE_Pattern): the regular expression
Returns:
bool: whether there was a match or not
"""
if isinstance(regex, six.string_types):
regex = re.compile(regex)
try:
with open(file_path) as f:
line = f.readline()
while line != "":
match = regex.search(line)
if match:
return True
# Read next line
line = f.readline()
except (Exception) as e:
log.debug(
"Could not check file '{}' for dictlet properties: {}".format(file_path, e)
)
return False
return False
[docs]@six.add_metaclass(abc.ABCMeta)
class DictletFinder(object):
"""Abstract base class for an object that can find and load dictlets.
The task of a DictletFinder is to find all available dictlets for the current environment. A sub-class of this should implement the :meth:`find_available_dictlets` and/or, if applicable, overwrite the :meth:`get_dictlet_details` methods.
If it's not possible to get a list of all available dictlets, it's advisable to return an empty dict for the :meth:`find_available_dictlets` method and implement :meth:`get_dictlet_details`.
"""
def __init__(self):
self.available_dictlets = None
[docs] def init_dictlet_cache(self, name=None):
"""Fills the cache, either with all dictlets, or only the one whose name was specified.
Args:
name (str): if specified, only the dictlet with that name is loaded, otherwise all available
Returns:
dict: a dictionary with either one (requested) dictlet and it's details, or all of them
"""
if name is None:
if self.available_dictlets is None:
self.available_dictlets = {}
dictlet_names = self.get_all_dictlet_names()
for name in dictlet_names:
self.available_dictlets[name] = self.get_dictlet(name)
return self.available_dictlets
else:
if (
self.available_dictlets is None
or name not in self.available_dictlets.keys()
):
details = self.get_dictlet(name)
if not details:
return {}
else:
details = self.available_dictlets[name]
return {name: details}
[docs] @abc.abstractmethod
def get_dictlet(self, name):
"""Loads the dictlet with the specified name/path.
Args:
name (str): the name (or path) of the dictlet
Return:
dict: details for this dictlet, None if not found
"""
pass
[docs] @abc.abstractmethod
def get_all_dictlet_names(self):
"""Returns a list of all available dictlet names.
Returns:
list: all dictlet names
"""
pass
[docs] def get_all_dictlets(self):
"""Returns all available dictlets.
Returns:
dict: a dictionary with the dictlet name as key, and it's url as value
"""
return self.init_dictlet_cache()
[docs] def get_dictlet_details(self, name):
"""Returns the details of the dictlet with the specified name.
The returned details should contain at least a 'type' key, indicating the type of the dictlet (e.g. 'file', 'class'). Which and whether other keys are necessary depends on that type.
Args:
name: the name or url of a dictlet
Returns:
dict: details about this dictlet
"""
cache = self.init_dictlet_cache(name=name)
result = cache.get(name, None)
if not result:
raise NoSuchDictletException(name)
return result
[docs]class LucifierFinder(DictletFinder):
"""Finds all lucifiers.
This lucifier is mainly used to create a command-line interface using the :class:`luci.cli_lucifer.CliLucifier` class.
"""
def __init__(self):
super(LucifierFinder, self).__init__()
# otherwise we have a module load loop
from .plugins import get_plugin_names, extension_details
self.plugin_names_lookup = get_plugin_names
self.plugin_details_lookup = extension_details
[docs] def get_all_dictlet_names(self):
return self.plugin_names_lookup()
[docs] def get_dictlet(self, name):
"""Loads (stevedore-registered) available lucifier class in this Python environment.
Returns:
dict: lucifier details
"""
result = self.plugin_details_lookup.get(name, None)
return result
[docs]class PathDictletFinder(DictletFinder):
"""Can find dictlets using a list of paths.
Args:
paths (list): a list of paths where to look for dictlets
max_folders (int): how many child-folder levels to look for dictlets, this is restricted by default because for performance reasons
"""
CONTENT_MARKER_REGEX = re.compile(
"{}|{}".format(DICTLET_READER_MARKER_STRING, KEY_LUCI_NAME), re.IGNORECASE
)
def __init__(self, paths=None, max_folders=PATH_FINDER_MAX_FOLDER_DEFAULT):
super(PathDictletFinder, self).__init__()
if paths is None:
paths = [os.getcwd()]
self.paths = paths
self.max_level = max_folders
self.dictlet_cache = {}
[docs] def find_available_dictlets_path(
self, base_path, relative_path="", current_result=None, current_level=0
):
"""Checks a path for valid dictlet files.
Args:
base_bath (str): the base path (corresponding to one of the object initial paths)
relative_path (str): relative path (from the base_path) to the folder/file in question
current_result (dict): used for recursive call of this method
current_level (int): used for recursive call of this method
Return:
dict: a list of available dictlets (so far)
"""
if current_result is None:
current_result = {}
if current_level >= self.max_level:
return current_result
if relative_path.startswith(".") and current_level != 0:
return current_result
path = os.path.join(base_path, relative_path)
for f in os.listdir(path):
file_path = os.path.join(path, f)
if os.path.isdir(file_path):
new_relative_path = os.path.join(relative_path, f)
current_result = self.find_available_dictlets_path(
base_path, new_relative_path, current_result, current_level + 1
)
elif os.path.isfile(file_path) and check_dictlet_content(
file_path, PathDictletFinder.CONTENT_MARKER_REGEX
):
rel_path = os.path.join(relative_path, f)
current_result[rel_path] = {
"path": os.path.join(base_path, rel_path),
"type": "file",
}
return current_result
[docs] def find_available_dictlets(self, paths=None):
"""Finds all available dictlets under the specified paths.
Args:
paths (list): the paths to look in. If not specified, the objects default paths will be used
Return:
dict: all dictlets and their details
"""
result = {}
if paths is None:
paths = self.paths
if isinstance(paths, six.string_types):
paths = [paths]
for path in paths:
path_dictlets = None
if path not in self.dictlet_cache.keys():
self.dictlet_cache["path"] = {}
real_path = os.path.realpath(path)
if os.path.isdir(real_path):
path_dictlets = self.find_available_dictlets_path(path)
dict_merge(
self.dictlet_cache["path"], path_dictlets, copy_dct=False
)
elif os.path.isfile(real_path):
path_dictlets = {path: {"path": path, "type": "file"}}
log.debug("Found dictlets in path '{}': {}".format(path, path_dictlets))
else:
path_dictlets = self.dictlet_cache[path]
if path_dictlets:
dict_merge(result, path_dictlets, copy_dct=False)
return result
[docs] def get_all_dictlet_names(self):
return self.find_available_dictlets().keys()
[docs] def get_dictlet(self, name):
result = self.find_available_dictlets(name).get(name, None)
return result
[docs]class FolderFinder(DictletFinder):
"""Simple finder for folders."""
def __init__(self, **kwargs):
super(FolderFinder, self).__init__(**kwargs)
[docs] def get_all_dictlet_names(self):
return []
[docs] def get_dictlet(self, name):
abs_path = os.path.realpath(name)
if os.path.isdir(abs_path):
return {"path": abs_path, "type": "folder"}
return None
[docs]class FolderOrFileFinder(DictletFinder):
"""Simple finder for folders."""
def __init__(self, **kwargs):
super(FolderOrFileFinder, self).__init__(**kwargs)
[docs] def get_all_dictlet_names(self):
return []
[docs] def get_dictlet(self, name):
abs_path = os.path.realpath(name)
if os.path.isdir(abs_path):
return {"path": abs_path, "type": "folder"}
elif os.path.isfile(abs_path):
return {"path": abs_path, "type": "file"}
return None