<commit_before>#!/usr/bin/env python

__author__     = "Jan Kogut"
__copyright__  = "Jan Kogut"
__license__    = "MIT"
__version__    = "0.0.1"
__maintainer__ = "Jan Kogut"
__status__     = "Beta"

import json
from optparse import OptionParser
from kazoo.client import KazooClient

## START of config section
##################################################

class OurConfig(object):
    ''' Our config section '''

    pass

cfg = OurConfig()

cfg.zkServers  = 'zoo1.dmz:2181,zoo2.dmz:2181,zoo3.dmz:2181'
cfg.aPath      = '/ansible-test'

#################################################
## END of config section

def oParser():
    '''
    Commandline options parsing function.

    Return dict (parsed options).
    '''

    parser = OptionParser(usage="usage: %prog [opts] <args>",
                          version="%prog 0.0.1")
    parser.add_option("-A", nargs = 1,
                      help="add hostname: <groupname1:newhostname1,var1:value1,var2:value2,var3:value3>")
    parser.add_option("-D", nargs = 1,
                      help="delete hostname or groupname recursively: <groupname1:newhostname1> or <groupname1>")
    parser.add_option("-U", nargs = 1,
                      help="update hostname with comma separated hostvars: <groupname1:newhostname1,var1:newvalue1,var2:newvalue2>")
    parser.add_option("-S", nargs = 1,
                      help="show hostvars for a given hostname or groupname: <groupname1:newhostname1> or <groupname1>")
    parser.add_option("-I", nargs = 1,
                      help="inventory mode: dumps inventory in json format from zookeeper")

    (opts, args) = parser.parse_args()

    if (opts.A or opts.D or opts.U or opts.I or opts.S) == None:

        parser.print_help()
        exit(-1)

    return {'addMode':opts.A, 'deleteMode':opts.D, 'updateMode':opts.U,
            'showMode':opts.S, 'inventoryMode':opts.I}

def zkStartRo():
    '''
    Start a zookeeper client connection in read-only mode.

    Return zookeeper read-only connection object.
    '''

    zk = KazooClient(hosts=cfg.zkServers, read_only = True)
    zk.start()

    return zk

def zkStartRw():
    '''
    Start a zookeeper client connection in read-write mode.

    Return zookeeper read-write connection object.
    '''

    zk = KazooClient(hosts=cfg.zkServers)
    zk.start()

    return zk

def splitZnodeVarString(znodeVarString):
    '''
    Parse string for commandline opts: <-A|-U>.

    Return dict.
    '''

    ## spliting example string into dictionary:
    ## example string: groupname:hostname1,var1:val1,var2:val2,var3:val3
    ## desired dict  : {"groupname":{"hostname1":{"var1":"val1", "var2":"val2", "var3":"val3"}}}

    varList = znodeVarString.split(',')
    varDict = {}

    for var in varList[1:]:
        varDict[var.split(':')[0]] = var.split(':')[1]

    groupName, hostName = varList[0].split(':')[0], varList[0].split(':')[1]

    return { groupName : { hostName : varDict }}

def splitZnodeString(znodeString):
    '''
    Splits znodeString into groupName, hostName, groupPath, hostPath.

    Return list of tuples.
    '''

    ## spliting example string into list of tuples or tuple :
    ## example string: groupname:hostname1
    ## example output: [("groupname","/ansible_zk/groups/groupname"),("hostname1","/ansible_zk/groups/groupname")]

    if ':' in znodeString:
       groupName = znodeString.split(':')[0]
       hostName  = znodeString.split(':')[1]
       groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)
       hostPath  = "{0}/{1}".format(groupPath, hostName)

       return [(groupName, groupPath),(hostName, hostPath)]

    else:
       groupName = znodeString
       groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)

       return [(groupName, groupPath)]

def addZnode(znodeDict):
    '''
    Add new znode with hostvars.

    Return a string (ADDED    ==> host: <hostname> to group: <groupname>).
    '''

    zk = zkStartRw()

    groupName = znodeDict.keys()[0]
    hostName  = znodeDict[groupName].keys()[0]
    groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)
    hostPath  = "{0}/{1}".format(groupPath, hostName)

    if zk.exists(hostPath):
        zk.stop()

        return "ERROR  ==> host: {0} in group {1} exist !!!".format(hostName, groupName)

    if zk.exists(groupPath):
        zk.create(hostPath)
    else:
        zk.ensure_path(hostPath)

    for key in znodeDict[groupName][hostName]:
        varPath = "{0}/{1}".format(hostPath, key)
        varVal  = znodeDict[groupName][hostName][key]
        zk.create(varPath, varVal)

    zk.stop()

    return "ADDED   ==> host: {0} to group: {1}".format(hostName, groupName)

def deleteZnodeRecur(znodeStringSplited):
    '''
    Delete znode with hostvars for a given list of tuple <groupname:hostname>.

    Return a string (DELETED||ERROR  ==> host: <hostname> in group: <groupname>||group: <groupname>).
    '''

    zk = zkStartRw()

    if len(znodeStringSplited) > 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]
        hostName  = znodeStringSplited[1][0]
        hostPath  = znodeStringSplited[1][1]

        if zk.exists(hostPath) is None:
            zk.stop()
            return "ERROR  ==> could not delete host: {0} that does not exist !!!".format(hostName)

        if len(zk.get_children(groupPath)) == 1:
            zk.delete(groupPath, recursive=True)
        else:
            zk.delete(hostPath, recursive=True)
            zk.stop()
            return "DELETED ==> host: {0} in group: {1}".format(hostName, groupName)

    elif len(znodeStringSplited) == 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]

        if zk.exists(groupPath) is None:
            zk.stop()
            return "ERROR  ==> could not delete group: {0} that does not exist !!!".format(groupName)

        else:
            zk.delete(groupPath, recursive=True)
    else:
        return "ERROR with processing znodeStrings !!!"

    zk.stop()
    return "DELETED ==> group: {0}".format(groupName)

def updateZnode(znodeDict):
    '''
    Update znode with hostvars.

    Return a string (UPDATED   ==> host: <hostname> to group: <groupname>).
    '''

    zk = zkStartRw()

    groupName   = znodeDict.keys()[0]
    hostName    = znodeDict[groupName].keys()[0]
    groupPath   = "{0}/groups/{1}".format(cfg.aPath, groupName)
    hostPath    = "{0}/{1}".format(groupPath, hostName)
    hostVarList = zk.get_children(hostPath)

    if zk.exists(hostPath) is None:
        zk.stop()
        return "ERROR  ==> host: {0} in group {1} does not exist !!!".format(hostName, groupName)

    for hostVar in hostVarList:
        if zk.exists("{0}/{1}".format(hostPath, hostVar)) is None:
            zk.stop()
            return "ERROR  ==> hostvar: {0} for host {1} in group {2} does not exist !!!".format(hostVar, hostName, groupName)

    for key in znodeDict[groupName][hostName]:
        varPath = "{0}/{1}".format(hostPath, key)
        varVal  = znodeDict[groupName][hostName][key]
        if
        zk.create(varPath, varVal)

    zk.stop()
    return "ADDED   ==> host: {0} to group: {1}".format(hostName, groupName)

def hostVarsShow(znodeStringSplited):
    '''
    Show hostvars for a given <groupname:hostname> or <groupname>.

    Return dict or string (in case of ERROR).
    '''

    zk = zkStartRo()

    if len(znodeStringSplited) > 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]
        hostName  = znodeStringSplited[1][0]
        hostPath  = znodeStringSplited[1][1]

        if zk.exists(hostPath) is None:
            zk.stop()
            return "ERROR  ==> no such hostname: {0} in group {1} !!!".format(hostName, groupName)

        else:
            hostVarList = zk.get_children(hostPath)
            valDict     = {}

            for var in hostVarList:
                valDict[var] = zk.get('{0}/{1}'.format(hostPath, var))[0]

    elif len(znodeStringSplited) == 1:
        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]

        if zk.exists(groupPath) is None:
            zk.stop()
            return "ERROR  ==> no such groupname: {0} !!!".format(groupName)

        else:
            hostList = zk.get_children(groupPath)
            valDict  = {}

            for host in hostList:
                tmpHostPath    = '{0}/{1}'.format(groupPath, host)
                valDict[host]  = zk.get_children('{0}'.format(tmpHostPath))

            for host in valDict.keys():
                varDict = {}

                for var in valDict[host]:
                    tmpHostPath   = '{0}/{1}'.format(groupPath, host)
                    varDict[var]  = zk.get('{0}/{1}'.format(tmpHostPath, var))[0]
                    valDict[host] = varDict
    else:
        return "ERROR with processing znodeStrings !!!"

    zk.stop()
    return valDict

def inventoryDump():
    '''
    Inventory dump for a given list of zookeeper servers and ansible-keeper path.

    Return dict.
    '''

    zk = zkStartRo()

    groupList = zk.get_children("{}/groups".format(cfg.aPath))
    groupDict = {}

    for group in groupList:
        path     = "{0}/groups/{1}".format(cfg.aPath, group)
        children = zk.get_children(path)
        groupDict[group] = children

    zk.stop()
    return groupDict

def ansibleInventoryDump():
    '''
    Ansible compliant inventory dump for a given list of zookeeper servers and ansible-keeper path.

    Return dict.
    '''

    zk = zkStartRo()

    groupList = zk.get_children("{}/groups".format(cfg.aPath))
    groupDict = {}

    for group in groupList:
        path     = "{0}/groups/{1}".format(cfg.aPath, group)
        children = zk.get_children(path)
        tmpDict  = {}
        tmpDict['hosts'] = children
        tmpDict['vars']  = {"a":"b"}
        groupDict[group] = tmpDict

    zk.stop()
    return groupDict

def main():
    '''
    Main logic
    '''

    if oParser()['inventoryMode'] == 'true':
        print json.dumps(inventoryDump())

    if oParser()['inventoryMode'] == 'ansible':
        print json.dumps(ansibleInventoryDump())

    if oParser()['addMode'] is not None:
        znodeDict = splitZnodeVarString(oParser()['addMode'])
        print addZnode(znodeDict)

    if oParser()['deleteMode'] is not None:
        znodeStringSplited = splitZnodeString(oParser()['deleteMode'])
        print deleteZnodeRecur(znodeStringSplited)

    if oParser()['showMode'] is not None:
        znodeStringSplited = splitZnodeString(oParser()['showMode'])
        print json.dumps(hostVarsShow(znodeStringSplited))

if __name__ == "__main__":
    main()
<commit_msg>updateZnode: commented out function which is still under construction<commit_after>#!/usr/bin/env python

__author__     = "Jan Kogut"
__copyright__  = "Jan Kogut"
__license__    = "MIT"
__version__    = "0.0.1"
__maintainer__ = "Jan Kogut"
__status__     = "Beta"

import json
from optparse import OptionParser
from kazoo.client import KazooClient

## START of config section
##################################################

class OurConfig(object):
    ''' Our config section '''

    pass

cfg = OurConfig()

cfg.zkServers  = 'zoo1.dmz:2181,zoo2.dmz:2181,zoo3.dmz:2181'
cfg.aPath      = '/ansible-test'

#################################################
## END of config section

def oParser():
    '''
    Commandline options parsing function.

    Return dict (parsed options).
    '''

    parser = OptionParser(usage="usage: %prog [opts] <args>",
                          version="%prog 0.0.1")
    parser.add_option("-A", nargs = 1,
                      help="add hostname: <groupname1:newhostname1,var1:value1,var2:value2,var3:value3>")
    parser.add_option("-D", nargs = 1,
                      help="delete hostname or groupname recursively: <groupname1:newhostname1> or <groupname1>")
    parser.add_option("-U", nargs = 1,
                      help="update hostname with comma separated hostvars: <groupname1:newhostname1,var1:newvalue1,var2:newvalue2>")
    parser.add_option("-S", nargs = 1,
                      help="show hostvars for a given hostname or groupname: <groupname1:newhostname1> or <groupname1>")
    parser.add_option("-I", nargs = 1,
                      help="inventory mode: dumps inventory in json format from zookeeper")

    (opts, args) = parser.parse_args()

    if (opts.A or opts.D or opts.U or opts.I or opts.S) == None:

        parser.print_help()
        exit(-1)

    return {'addMode':opts.A, 'deleteMode':opts.D, 'updateMode':opts.U,
            'showMode':opts.S, 'inventoryMode':opts.I}

def zkStartRo():
    '''
    Start a zookeeper client connection in read-only mode.

    Return zookeeper read-only connection object.
    '''

    zk = KazooClient(hosts=cfg.zkServers, read_only = True)
    zk.start()

    return zk

def zkStartRw():
    '''
    Start a zookeeper client connection in read-write mode.

    Return zookeeper read-write connection object.
    '''

    zk = KazooClient(hosts=cfg.zkServers)
    zk.start()

    return zk

def splitZnodeVarString(znodeVarString):
    '''
    Parse string for commandline opts: <-A|-U>.

    Return dict.
    '''

    ## spliting example string into dictionary:
    ## example string: groupname:hostname1,var1:val1,var2:val2,var3:val3
    ## desired dict  : {"groupname":{"hostname1":{"var1":"val1", "var2":"val2", "var3":"val3"}}}

    varList = znodeVarString.split(',')
    varDict = {}

    for var in varList[1:]:
        varDict[var.split(':')[0]] = var.split(':')[1]

    groupName, hostName = varList[0].split(':')[0], varList[0].split(':')[1]

    return { groupName : { hostName : varDict }}

def splitZnodeString(znodeString):
    '''
    Splits znodeString into groupName, hostName, groupPath, hostPath.

    Return list of tuples.
    '''

    ## spliting example string into list of tuples or tuple :
    ## example string: groupname:hostname1
    ## example output: [("groupname","/ansible_zk/groups/groupname"),("hostname1","/ansible_zk/groups/groupname")]

    if ':' in znodeString:
       groupName = znodeString.split(':')[0]
       hostName  = znodeString.split(':')[1]
       groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)
       hostPath  = "{0}/{1}".format(groupPath, hostName)

       return [(groupName, groupPath),(hostName, hostPath)]

    else:
       groupName = znodeString
       groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)

       return [(groupName, groupPath)]

def addZnode(znodeDict):
    '''
    Add new znode with hostvars.

    Return a string (ADDED    ==> host: <hostname> to group: <groupname>).
    '''

    zk = zkStartRw()

    groupName = znodeDict.keys()[0]
    hostName  = znodeDict[groupName].keys()[0]
    groupPath = "{0}/groups/{1}".format(cfg.aPath, groupName)
    hostPath  = "{0}/{1}".format(groupPath, hostName)

    if zk.exists(hostPath):
        zk.stop()

        return "ERROR  ==> host: {0} in group {1} exist !!!".format(hostName, groupName)

    if zk.exists(groupPath):
        zk.create(hostPath)
    else:
        zk.ensure_path(hostPath)

    for key in znodeDict[groupName][hostName]:
        varPath = "{0}/{1}".format(hostPath, key)
        varVal  = znodeDict[groupName][hostName][key]
        zk.create(varPath, varVal)

    zk.stop()

    return "ADDED   ==> host: {0} to group: {1}".format(hostName, groupName)

def deleteZnodeRecur(znodeStringSplited):
    '''
    Delete znode with hostvars for a given list of tuple <groupname:hostname>.

    Return a string (DELETED||ERROR  ==> host: <hostname> in group: <groupname>||group: <groupname>).
    '''

    zk = zkStartRw()

    if len(znodeStringSplited) > 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]
        hostName  = znodeStringSplited[1][0]
        hostPath  = znodeStringSplited[1][1]

        if zk.exists(hostPath) is None:
            zk.stop()
            return "ERROR  ==> could not delete host: {0} that does not exist !!!".format(hostName)

        if len(zk.get_children(groupPath)) == 1:
            zk.delete(groupPath, recursive=True)
        else:
            zk.delete(hostPath, recursive=True)
            zk.stop()
            return "DELETED ==> host: {0} in group: {1}".format(hostName, groupName)

    elif len(znodeStringSplited) == 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]

        if zk.exists(groupPath) is None:
            zk.stop()
            return "ERROR  ==> could not delete group: {0} that does not exist !!!".format(groupName)

        else:
            zk.delete(groupPath, recursive=True)
    else:
        return "ERROR with processing znodeStrings !!!"

    zk.stop()
    return "DELETED ==> group: {0}".format(groupName)

# def updateZnode(znodeDict):
#     '''
#     Update znode with hostvars.

#     Return a string (UPDATED   ==> host: <hostname> to group: <groupname>).
#     '''

#     zk = zkStartRw()

#     groupName   = znodeDict.keys()[0]
#     hostName    = znodeDict[groupName].keys()[0]
#     groupPath   = "{0}/groups/{1}".format(cfg.aPath, groupName)
#     hostPath    = "{0}/{1}".format(groupPath, hostName)
#     hostVarList = zk.get_children(hostPath)

#     if zk.exists(hostPath) is None:
#         zk.stop()
#         return "ERROR  ==> host: {0} in group {1} does not exist !!!".format(hostName, groupName)

#     for hostVar in hostVarList:
#         if zk.exists("{0}/{1}".format(hostPath, hostVar)) is None:
#             zk.stop()
#             return "ERROR  ==> hostvar: {0} for host {1} in group {2} does not exist !!!".format(hostVar, hostName, groupName)

#     for key in znodeDict[groupName][hostName]:
#         varPath = "{0}/{1}".format(hostPath, key)
#         varVal  = znodeDict[groupName][hostName][key]
#         if CONDITION: #FIXME!!!
#            zk.create(varPath, varVal)

#     zk.stop()
#     return "ADDED   ==> host: {0} to group: {1}".format(hostName, groupName)

def hostVarsShow(znodeStringSplited):
    '''
    Show hostvars for a given <groupname:hostname> or <groupname>.

    Return dict or string (in case of ERROR).
    '''

    zk = zkStartRo()

    if len(znodeStringSplited) > 1:

        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]
        hostName  = znodeStringSplited[1][0]
        hostPath  = znodeStringSplited[1][1]

        if zk.exists(hostPath) is None:
            zk.stop()
            return "ERROR  ==> no such hostname: {0} in group {1} !!!".format(hostName, groupName)

        else:
            hostVarList = zk.get_children(hostPath)
            valDict     = {}

            for var in hostVarList:
                valDict[var] = zk.get('{0}/{1}'.format(hostPath, var))[0]

    elif len(znodeStringSplited) == 1:
        groupName = znodeStringSplited[0][0]
        groupPath = znodeStringSplited[0][1]

        if zk.exists(groupPath) is None:
            zk.stop()
            return "ERROR  ==> no such groupname: {0} !!!".format(groupName)

        else:
            hostList = zk.get_children(groupPath)
            valDict  = {}

            for host in hostList:
                tmpHostPath    = '{0}/{1}'.format(groupPath, host)
                valDict[host]  = zk.get_children('{0}'.format(tmpHostPath))

            for host in valDict.keys():
                varDict = {}

                for var in valDict[host]:
                    tmpHostPath   = '{0}/{1}'.format(groupPath, host)
                    varDict[var]  = zk.get('{0}/{1}'.format(tmpHostPath, var))[0]
                    valDict[host] = varDict
    else:
        return "ERROR with processing znodeStrings !!!"

    zk.stop()
    return valDict

def inventoryDump():
    '''
    Inventory dump for a given list of zookeeper servers and ansible-keeper path.

    Return dict.
    '''

    zk = zkStartRo()

    groupList = zk.get_children("{}/groups".format(cfg.aPath))
    groupDict = {}

    for group in groupList:
        path     = "{0}/groups/{1}".format(cfg.aPath, group)
        children = zk.get_children(path)
        groupDict[group] = children

    zk.stop()
    return groupDict

def ansibleInventoryDump():
    '''
    Ansible compliant inventory dump for a given list of zookeeper servers and ansible-keeper path.

    Return dict.
    '''

    zk = zkStartRo()

    groupList = zk.get_children("{}/groups".format(cfg.aPath))
    groupDict = {}

    for group in groupList:
        path     = "{0}/groups/{1}".format(cfg.aPath, group)
        children = zk.get_children(path)
        tmpDict  = {}
        tmpDict['hosts'] = children
        tmpDict['vars']  = {"a":"b"}
        groupDict[group] = tmpDict

    zk.stop()
    return groupDict

def main():
    '''
    Main logic
    '''

    if oParser()['inventoryMode'] == 'true':
        print json.dumps(inventoryDump())

    if oParser()['inventoryMode'] == 'ansible':
        print json.dumps(ansibleInventoryDump())

    if oParser()['addMode'] is not None:
        znodeDict = splitZnodeVarString(oParser()['addMode'])
        print addZnode(znodeDict)

    if oParser()['deleteMode'] is not None:
        znodeStringSplited = splitZnodeString(oParser()['deleteMode'])
        print deleteZnodeRecur(znodeStringSplited)

    if oParser()['showMode'] is not None:
        znodeStringSplited = splitZnodeString(oParser()['showMode'])
        print json.dumps(hostVarsShow(znodeStringSplited))

if __name__ == "__main__":
    main()
<|endoftext|>


<commit_before>#
# Copyright (C) 2012 - 2015 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-import,import-error,invalid-name
r"""Public APIs of anyconfig module.

.. versionadded:: 0.4.99
   Most keyword arguments passed to APIs are now position independent.

.. versionadded:: 0.4.99
   Added ac_namedtuple parameter to \*load and \*dump APIs.

.. versionchanged:: 0.3
   Replaced `forced_type` optional argument of some public APIs with
   `ac_parser` to allow skip of config parser search by passing parser object
   previously found and instantiated.

   Also removed some optional arguments, `ignore_missing`, `merge` and
   `marker`, from definitions of some public APIs as these may not be changed
   from default in common use cases.

.. versionchanged:: 0.2
   Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
   :func:`load` and :func:`dump` can process a file/file-like object or a list
   of file/file-like objects instead of a file path or a list of file paths.

.. versionadded:: 0.2
   Export factory method (create) of anyconfig.mergeabledict.MergeableDict
"""
from __future__ import absolute_import

import os.path

from anyconfig.globals import LOGGER
import anyconfig.backends
import anyconfig.backend.json
import anyconfig.compat
import anyconfig.mergeabledict
import anyconfig.parser
import anyconfig.template
import anyconfig.utils

# Import some global constants will be re-exported:
from anyconfig.mergeabledict import (
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
    PATH_SEPS, get, set_, convert_to,  # flake8: noqa
    create_from as to_container
)
from anyconfig.schema import validate, gen_schema
from anyconfig.utils import is_path

# Re-export and aliases:
list_types = anyconfig.backends.list_types  # flake8: noqa
container = anyconfig.mergeabledict.MergeableDict

def _is_paths(maybe_paths):
    """
    Does given object `maybe_paths` consist of path or path pattern strings?
    """
    if anyconfig.utils.is_iterable(maybe_paths):
        return not getattr(maybe_paths, "read", False)

    return False  # Not an iterable at least.

def _maybe_validated(cnf, schema, format_checker=None, **options):
    """
    :param cnf: Configuration object :: container
    :param schema: JSON schema object :: container
    :param format_checker: A format property checker object of which class is
        inherited from jsonschema.FormatChecker, it's default if None given.
    :param options: Keyword options such as:

        - ac_namedtuple: Convert result to nested namedtuple object if True

    :return: Given `cnf` as it is if validation succeeds else None
    """
    if schema is None:
        valid = True
    else:
        (valid, msg) = validate(cnf, schema, format_checker=format_checker,
                                safe=True)
        if msg:
            LOGGER.warning(msg)

    if valid:
        return convert_to(cnf, True) if options.get("ac_namedtuple") else cnf

    return None

def set_loglevel(level):
    """
    :param level: Log level, e.g. logging.INFO and logging.WARN.
    """
    LOGGER.setLevel(level)

def find_loader(path_or_stream, parser_or_type=None, is_path_=None):
    """
    Find out config parser object appropriate to load from a file of given path
    or file/file-like object.

    :param path_or_stream: Configuration file path or file / file-like object
    :param parser_or_type: Forced configuration parser type or parser object
    :param is_path_: True if given `path_or_stream` is a file path

    :return: Config parser instance or None
    """
    if anyconfig.backends.is_parser(parser_or_type):
        return parser_or_type

    (psr, err) = anyconfig.backends.find_parser(path_or_stream, parser_or_type,
                                                is_path_=is_path_)
    if psr is None:
        LOGGER.error(err)
        return None

    LOGGER.debug("Using config parser: %r [%s]", psr, psr.type())
    return psr()  # TBD: Passing initialization arguments.

def _load_schema(**options):
    """
    :param options: Optional keyword arguments such as

        - ac_template: Assume configuration file may be a template file and try
          to compile it AAR if True
        - ac_context: A dict presents context to instantiate template
        - ac_schema: JSON schema file path to validate given config file
    """
    ac_schema = options.get("ac_schema", None)
    if ac_schema is not None:
        # Try to detect the appropriate as it may be different from the
        # original config file's format, perhaps.
        options["ac_parser"] = None
        options["ac_schema"] = None  # Avoid infinite loop.
        LOGGER.info("Loading schema: %s", ac_schema)
        return load(ac_schema, **options)

    return None

def single_load(path_or_stream, ac_parser=None, ac_template=False,
                ac_context=None, **options):
    """
    Load single config file.

    :param path_or_stream: Configuration file path or file / file-like object
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options: Optional keyword arguments such as:

        - Common options:

          - ac_namedtuple: Convert result to nested namedtuple object if True
          - ac_schema: JSON schema file path to validate given config file

        - Common backend options:

          - ignore_missing: Ignore and just return empty result if given file
            (``path_or_stream``) does not exist.

        - Backend specific options such as {"indent": 2} for JSON backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    is_path_ = is_path(path_or_stream)
    if is_path_:
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
        filepath = path_or_stream
    else:
        filepath = anyconfig.utils.get_path_from_stream(path_or_stream)

    psr = find_loader(path_or_stream, ac_parser, is_path_)
    if psr is None:
        return None

    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
                          **options)
    options["ac_schema"] = None  # It's not needed now.

    LOGGER.info("Loading: %s", filepath)
    if ac_template and filepath is not None:
        try:
            LOGGER.debug("Compiling: %s", filepath)
            content = anyconfig.template.render(filepath, ac_context)
            cnf = psr.loads(content, **options)
            return _maybe_validated(cnf, schema, **options)

        except Exception as exc:
            LOGGER.warning("Failed to compile %s, fallback to no template "
                           "mode, exc=%r", path_or_stream, exc)

    cnf = psr.load(path_or_stream, **options)
    return _maybe_validated(cnf, schema, **options)

def multi_load(paths, ac_parser=None, ac_template=False, ac_context=None,
               **options):
    """
    Load multiple config files.

    The first argument `paths` may be a list of config file paths or
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
    the dir /etc/foo/conf.d/, the followings give same results::

      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
                  "/etc/foo/conf.d/c.yml", ])

      multi_load("/etc/foo/conf.d/*.yml")

    :param paths: List of config file paths or a glob pattern to list paths, or
        a list of file/file-like objects
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options: Optional keyword arguments:

        - Common options:

          - ac_merge (merge): Specify strategy of how to merge results loaded
            from multiple configuration files. See the doc of mergeabledict
            module for more details of strategies. The default is MS_DICTS.

          - ac_marker (marker): Globbing marker to detect paths patterns.
          - ac_namedtuple: Convert result to nested namedtuple object if True
          - ac_schema: JSON schema file path to validate given config file

        - Common backend options:

          - ignore_missing: Ignore and just return empty result if given file
            (``path_or_stream``) does not exist.

        - Backend specific options such as {"indent": 2} for JSON backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
                          **options)
    options["ac_schema"] = None  # It's not needed now.

    cnf = to_container(ac_context, **options)
    same_type = anyconfig.utils.are_same_file_types(paths)

    if is_path(paths) and marker in paths:
        paths = anyconfig.utils.sglob(paths)

    for path in paths:
        opts = options.copy()
        opts["ac_namedtuple"] = False  # Disabled within this loop.
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
        if is_path(path) and marker in path:
            cups = multi_load(path, ac_parser=ac_parser,
                              ac_template=ac_template, ac_context=cnf, **opts)
        else:
            if same_type:
                ac_parser = find_loader(path, ac_parser, is_path(path))
            cups = single_load(path, ac_parser=ac_parser,
                               ac_template=ac_template, ac_context=cnf, **opts)

        if cups:
            cnf.update(cups)

    return _maybe_validated(cnf, schema, **options)

def load(path_specs, ac_parser=None, ac_template=False, ac_context=None,
         **options):
    r"""
    Load single or multiple config files or multiple config files specified in
    given paths pattern.

    :param path_specs: Configuration file path or paths or its pattern such as
        r'/a/b/\*.json' or a list of files/file-like objects
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options:
        Optional keyword arguments. See also the description of `options` in
        `multi_load` function.

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    marker = options.setdefault("ac_marker", options.get("marker", '*'))

    if is_path(path_specs) and marker in path_specs or _is_paths(path_specs):
        return multi_load(path_specs, ac_parser=ac_parser,
                          ac_template=ac_template, ac_context=ac_context,
                          **options)
    else:
        return single_load(path_specs, ac_parser=ac_parser,
                           ac_template=ac_template, ac_context=ac_context,
                           **options)

def loads(content, ac_parser=None, ac_template=False, ac_context=None,
          **options):
    """
    :param content: Configuration file's content
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: Context dict to instantiate template
    :param options:
        Optional keyword arguments. See also the description of `options` in
        `single_load` function.

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if ac_parser is None:
        LOGGER.warning("No type or parser was given. Try to parse...")
        return anyconfig.parser.parse(content)

    psr = find_loader(None, ac_parser)
    if psr is None:
        return anyconfig.parser.parse(content)

    schema = None
    ac_schema = options.get("ac_schema", None)
    if ac_schema is not None:
        options["ac_schema"] = None
        schema = loads(ac_schema, ac_parser=psr, ac_template=ac_template,
                       ac_context=ac_context, **options)

    if ac_template:
        try:
            LOGGER.debug("Compiling")
            content = anyconfig.template.render_s(content, ac_context)
        except Exception as exc:
            LOGGER.warning("Failed to compile and fallback to no template "
                           "mode: '%s', exc=%r", content[:50] + '...', exc)

    cnf = psr.loads(content, **options)
    return _maybe_validated(cnf, schema, **options)

def _find_dumper(path_or_stream, ac_parser=None):
    """
    Find configuration parser to dump data.

    :param path_or_stream: Output file path or file / file-like object
    :param ac_parser: Forced parser type or parser object

    :return: Parser-inherited class object
    """
    psr = find_loader(path_or_stream, ac_parser)

    if psr is None or not getattr(psr, "dump", False):
        LOGGER.warning("Dump method not implemented. Fallback to json.Parser")
        psr = anyconfig.backend.json.Parser()

    return psr

def dump(data, path_or_stream, ac_parser=None, **options):
    """
    Save `data` as `path_or_stream`.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param path_or_stream: Output file path or file / file-like object
    :param ac_parser: Forced parser type or parser object
    :param options: Keyword options such ash:

        - ac_namedtuple: Convert result to nested namedtuple object if True
        - other backend specific optional arguments, e.g. {"indent": 2} for
          JSON loader/dumper backend
    """
    dumper = _find_dumper(path_or_stream, ac_parser)
    LOGGER.info("Dumping: %s",
                anyconfig.utils.get_path_from_stream(path_or_stream))
    if options.get("ac_namedtuple", False):
        data = to_container(data, **options)
    dumper.dump(data, path_or_stream, **options)

def dumps(data, ac_parser=None, **options):
    """
    Return string representation of `data` in forced type format.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param ac_parser: Forced parser type or parser object
    :param options: see :func:`dump`

    :return: Backend-specific string representation for the given data
    """
    if options.get("ac_namedtuple", False):
        data = to_container(data, **options)
    return _find_dumper(None, ac_parser).dumps(data, **options)

# vim:sw=4:ts=4:et:
<commit_msg>refactor: use keyword argument to_namedtuple explicitly in .api._maybe_validated<commit_after>#
# Copyright (C) 2012 - 2015 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-import,import-error,invalid-name
r"""Public APIs of anyconfig module.

.. versionadded:: 0.4.99
   Most keyword arguments passed to APIs are now position independent.

.. versionadded:: 0.4.99
   Added ac_namedtuple parameter to \*load and \*dump APIs.

.. versionchanged:: 0.3
   Replaced `forced_type` optional argument of some public APIs with
   `ac_parser` to allow skip of config parser search by passing parser object
   previously found and instantiated.

   Also removed some optional arguments, `ignore_missing`, `merge` and
   `marker`, from definitions of some public APIs as these may not be changed
   from default in common use cases.

.. versionchanged:: 0.2
   Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
   :func:`load` and :func:`dump` can process a file/file-like object or a list
   of file/file-like objects instead of a file path or a list of file paths.

.. versionadded:: 0.2
   Export factory method (create) of anyconfig.mergeabledict.MergeableDict
"""
from __future__ import absolute_import

import os.path

from anyconfig.globals import LOGGER
import anyconfig.backends
import anyconfig.backend.json
import anyconfig.compat
import anyconfig.mergeabledict
import anyconfig.parser
import anyconfig.template
import anyconfig.utils

# Import some global constants will be re-exported:
from anyconfig.mergeabledict import (
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
    PATH_SEPS, get, set_, convert_to,  # flake8: noqa
    create_from as to_container
)
from anyconfig.schema import validate, gen_schema
from anyconfig.utils import is_path

# Re-export and aliases:
list_types = anyconfig.backends.list_types  # flake8: noqa
container = anyconfig.mergeabledict.MergeableDict

def _is_paths(maybe_paths):
    """
    Does given object `maybe_paths` consist of path or path pattern strings?
    """
    if anyconfig.utils.is_iterable(maybe_paths):
        return not getattr(maybe_paths, "read", False)

    return False  # Not an iterable at least.

def _maybe_validated(cnf, schema, format_checker=None, **options):
    """
    :param cnf: Configuration object :: container
    :param schema: JSON schema object :: container
    :param format_checker: A format property checker object of which class is
        inherited from jsonschema.FormatChecker, it's default if None given.
    :param options: Keyword options such as:

        - ac_namedtuple: Convert result to nested namedtuple object if True

    :return: Given `cnf` as it is if validation succeeds else None
    """
    if schema is None:
        valid = True
    else:
        (valid, msg) = validate(cnf, schema, format_checker=format_checker,
                                safe=True)
        if msg:
            LOGGER.warning(msg)

    if valid:
        if options.get("ac_namedtuple", False):
            return convert_to(cnf, to_namedtuple=True)
        else:
            return cnf

    return None

def set_loglevel(level):
    """
    :param level: Log level, e.g. logging.INFO and logging.WARN.
    """
    LOGGER.setLevel(level)

def find_loader(path_or_stream, parser_or_type=None, is_path_=None):
    """
    Find out config parser object appropriate to load from a file of given path
    or file/file-like object.

    :param path_or_stream: Configuration file path or file / file-like object
    :param parser_or_type: Forced configuration parser type or parser object
    :param is_path_: True if given `path_or_stream` is a file path

    :return: Config parser instance or None
    """
    if anyconfig.backends.is_parser(parser_or_type):
        return parser_or_type

    (psr, err) = anyconfig.backends.find_parser(path_or_stream, parser_or_type,
                                                is_path_=is_path_)
    if psr is None:
        LOGGER.error(err)
        return None

    LOGGER.debug("Using config parser: %r [%s]", psr, psr.type())
    return psr()  # TBD: Passing initialization arguments.

def _load_schema(**options):
    """
    :param options: Optional keyword arguments such as

        - ac_template: Assume configuration file may be a template file and try
          to compile it AAR if True
        - ac_context: A dict presents context to instantiate template
        - ac_schema: JSON schema file path to validate given config file
    """
    ac_schema = options.get("ac_schema", None)
    if ac_schema is not None:
        # Try to detect the appropriate as it may be different from the
        # original config file's format, perhaps.
        options["ac_parser"] = None
        options["ac_schema"] = None  # Avoid infinite loop.
        LOGGER.info("Loading schema: %s", ac_schema)
        return load(ac_schema, **options)

    return None

def single_load(path_or_stream, ac_parser=None, ac_template=False,
                ac_context=None, **options):
    """
    Load single config file.

    :param path_or_stream: Configuration file path or file / file-like object
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options: Optional keyword arguments such as:

        - Common options:

          - ac_namedtuple: Convert result to nested namedtuple object if True
          - ac_schema: JSON schema file path to validate given config file

        - Common backend options:

          - ignore_missing: Ignore and just return empty result if given file
            (``path_or_stream``) does not exist.

        - Backend specific options such as {"indent": 2} for JSON backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    is_path_ = is_path(path_or_stream)
    if is_path_:
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
        filepath = path_or_stream
    else:
        filepath = anyconfig.utils.get_path_from_stream(path_or_stream)

    psr = find_loader(path_or_stream, ac_parser, is_path_)
    if psr is None:
        return None

    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
                          **options)
    options["ac_schema"] = None  # It's not needed now.

    LOGGER.info("Loading: %s", filepath)
    if ac_template and filepath is not None:
        try:
            LOGGER.debug("Compiling: %s", filepath)
            content = anyconfig.template.render(filepath, ac_context)
            cnf = psr.loads(content, **options)
            return _maybe_validated(cnf, schema, **options)

        except Exception as exc:
            LOGGER.warning("Failed to compile %s, fallback to no template "
                           "mode, exc=%r", path_or_stream, exc)

    cnf = psr.load(path_or_stream, **options)
    return _maybe_validated(cnf, schema, **options)

def multi_load(paths, ac_parser=None, ac_template=False, ac_context=None,
               **options):
    """
    Load multiple config files.

    The first argument `paths` may be a list of config file paths or
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
    the dir /etc/foo/conf.d/, the followings give same results::

      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
                  "/etc/foo/conf.d/c.yml", ])

      multi_load("/etc/foo/conf.d/*.yml")

    :param paths: List of config file paths or a glob pattern to list paths, or
        a list of file/file-like objects
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options: Optional keyword arguments:

        - Common options:

          - ac_merge (merge): Specify strategy of how to merge results loaded
            from multiple configuration files. See the doc of mergeabledict
            module for more details of strategies. The default is MS_DICTS.

          - ac_marker (marker): Globbing marker to detect paths patterns.
          - ac_namedtuple: Convert result to nested namedtuple object if True
          - ac_schema: JSON schema file path to validate given config file

        - Common backend options:

          - ignore_missing: Ignore and just return empty result if given file
            (``path_or_stream``) does not exist.

        - Backend specific options such as {"indent": 2} for JSON backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
                          **options)
    options["ac_schema"] = None  # It's not needed now.

    cnf = to_container(ac_context, **options)
    same_type = anyconfig.utils.are_same_file_types(paths)

    if is_path(paths) and marker in paths:
        paths = anyconfig.utils.sglob(paths)

    for path in paths:
        opts = options.copy()
        opts["ac_namedtuple"] = False  # Disabled within this loop.
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
        if is_path(path) and marker in path:
            cups = multi_load(path, ac_parser=ac_parser,
                              ac_template=ac_template, ac_context=cnf, **opts)
        else:
            if same_type:
                ac_parser = find_loader(path, ac_parser, is_path(path))
            cups = single_load(path, ac_parser=ac_parser,
                               ac_template=ac_template, ac_context=cnf, **opts)

        if cups:
            cnf.update(cups)

    return _maybe_validated(cnf, schema, **options)

def load(path_specs, ac_parser=None, ac_template=False, ac_context=None,
         **options):
    r"""
    Load single or multiple config files or multiple config files specified in
    given paths pattern.

    :param path_specs: Configuration file path or paths or its pattern such as
        r'/a/b/\*.json' or a list of files/file-like objects
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param options:
        Optional keyword arguments. See also the description of `options` in
        `multi_load` function.

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    marker = options.setdefault("ac_marker", options.get("marker", '*'))

    if is_path(path_specs) and marker in path_specs or _is_paths(path_specs):
        return multi_load(path_specs, ac_parser=ac_parser,
                          ac_template=ac_template, ac_context=ac_context,
                          **options)
    else:
        return single_load(path_specs, ac_parser=ac_parser,
                           ac_template=ac_template, ac_context=ac_context,
                           **options)

def loads(content, ac_parser=None, ac_template=False, ac_context=None,
          **options):
    """
    :param content: Configuration file's content
    :param ac_parser: Forced parser type or parser object
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: Context dict to instantiate template
    :param options:
        Optional keyword arguments. See also the description of `options` in
        `single_load` function.

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if ac_parser is None:
        LOGGER.warning("No type or parser was given. Try to parse...")
        return anyconfig.parser.parse(content)

    psr = find_loader(None, ac_parser)
    if psr is None:
        return anyconfig.parser.parse(content)

    schema = None
    ac_schema = options.get("ac_schema", None)
    if ac_schema is not None:
        options["ac_schema"] = None
        schema = loads(ac_schema, ac_parser=psr, ac_template=ac_template,
                       ac_context=ac_context, **options)

    if ac_template:
        try:
            LOGGER.debug("Compiling")
            content = anyconfig.template.render_s(content, ac_context)
        except Exception as exc:
            LOGGER.warning("Failed to compile and fallback to no template "
                           "mode: '%s', exc=%r", content[:50] + '...', exc)

    cnf = psr.loads(content, **options)
    return _maybe_validated(cnf, schema, **options)

def _find_dumper(path_or_stream, ac_parser=None):
    """
    Find configuration parser to dump data.

    :param path_or_stream: Output file path or file / file-like object
    :param ac_parser: Forced parser type or parser object

    :return: Parser-inherited class object
    """
    psr = find_loader(path_or_stream, ac_parser)

    if psr is None or not getattr(psr, "dump", False):
        LOGGER.warning("Dump method not implemented. Fallback to json.Parser")
        psr = anyconfig.backend.json.Parser()

    return psr

def dump(data, path_or_stream, ac_parser=None, **options):
    """
    Save `data` as `path_or_stream`.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param path_or_stream: Output file path or file / file-like object
    :param ac_parser: Forced parser type or parser object
    :param options: Keyword options such ash:

        - ac_namedtuple: Convert result to nested namedtuple object if True
        - other backend specific optional arguments, e.g. {"indent": 2} for
          JSON loader/dumper backend
    """
    dumper = _find_dumper(path_or_stream, ac_parser)
    LOGGER.info("Dumping: %s",
                anyconfig.utils.get_path_from_stream(path_or_stream))
    if options.get("ac_namedtuple", False):
        data = to_container(data, **options)
    dumper.dump(data, path_or_stream, **options)

def dumps(data, ac_parser=None, **options):
    """
    Return string representation of `data` in forced type format.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param ac_parser: Forced parser type or parser object
    :param options: see :func:`dump`

    :return: Backend-specific string representation for the given data
    """
    if options.get("ac_namedtuple", False):
        data = to_container(data, **options)
    return _find_dumper(None, ac_parser).dumps(data, **options)

# vim:sw=4:ts=4:et:
<|endoftext|>


<commit_before>#
# Copyright (C) 2012 - 2015 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-import,import-error,invalid-name
"""Public APIs of anyconfig module.

.. versionchanged:: 0.2
   Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
   :func:`load` and :func:`dump` can process a file/file-like object or a list
   offile/file-like objects instead of a file path or a list of file paths.

.. versionadded:: 0.2
   Export factory method (create) of anyconfig.mergeabledict.MergeableDict
"""
from __future__ import absolute_import

import os.path

from anyconfig.globals import LOGGER
import anyconfig.backends
import anyconfig.backend.json
import anyconfig.compat
import anyconfig.mergeabledict
import anyconfig.parser
import anyconfig.template
import anyconfig.utils

# Import some global constants will be re-exported:
from anyconfig.mergeabledict import (
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
    get, set_  # flake8: noqa
)
from anyconfig.parser import PATH_SEPS
from anyconfig.schema import validate, gen_schema
from anyconfig.utils import get_path_from_stream

# Re-export and aliases:
list_types = anyconfig.backends.list_types  # flake8: noqa
container = anyconfig.mergeabledict.MergeableDict
to_container = container.create

def _is_path(path_or_stream):
    """
    Is given object `path_or_stream` a file path?
    """
    return isinstance(path_or_stream, anyconfig.compat.STR_TYPES)

def _validate(cnf, schema, format_checker=None):
    """
    Wrapper function for anycnf.schema.vaildate.

    :param cnf: Configuration object :: container
    :param schema: JSON schema object :: container
    :param format_checker: A format property checker object of which class is
        inherited from jsonschema.FormatChecker, it's default if None given.

    :return: True if validation suceeds or jsonschema module is not available.
    """
    (ret, msg) = validate(cnf, schema, format_checker, True)
    if msg:
        LOGGER.warn(msg)

    return ret

def set_loglevel(level):
    """
    :param level: Log level, e.g. logging.INFO and logging.WARN.
    """
    LOGGER.setLevel(level)

def find_loader(path_or_stream, forced_type=None):
    """
    Find out config parser object appropriate to load from a file of given path
    or file/file-like object.

    :param path_or_stream: Configuration file path or file / file-like object
    :param forced_type: Forced configuration parser type

    :return: Config parser instance or None
    """
    if forced_type is not None:
        cparser = anyconfig.backends.find_by_type(forced_type)
        if not cparser:
            LOGGER.error("No parser found for given type: %s", forced_type)
            return None
    else:
        if not _is_path(path_or_stream):
            path_or_stream = get_path_from_stream(path_or_stream)
            if path_or_stream is None:
                return None  # No way to detect filename.

        cparser = anyconfig.backends.find_by_file(path_or_stream)
        if not cparser:
            LOGGER.error("No parser found for given file: %s", path_or_stream)
            return None

    LOGGER.debug("Using config parser of type: %s", cparser.type())
    return cparser()

def single_load(path_or_stream, forced_type=None, ignore_missing=False,
                ac_template=False, ac_context=None, ac_schema=None,
                **kwargs):
    """
    Load single config file.

    :param path_or_stream: Configuration file path or file / file-like object
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore and just return empty result if given file
        (``path_or_stream``) does not exist
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if _is_path(path_or_stream):
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
        filepath = path_or_stream
    else:
        filepath = get_path_from_stream(path_or_stream)

    cparser = find_loader(path_or_stream, forced_type)
    if cparser is None:
        return None

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        LOGGER.info("Loading schema: %s", ac_schema)
        schema = load(ac_schema, forced_type=forced_type,
                      ignore_missing=ignore_missing, ac_template=ac_template,
                      ac_context=ac_context, **kwargs)

    LOGGER.info("Loading: %s", filepath)
    if ac_template and filepath is not None:
        try:
            LOGGER.debug("Compiling: %s", filepath)
            content = anyconfig.template.render(filepath, ac_context)
            cnf = cparser.loads(content, ignore_missing=ignore_missing,
                                **kwargs)
            if ac_schema is not None:
                if not _validate(cnf, schema, format_checker):
                    return None

            return cnf

        except Exception as exc:
            LOGGER.debug("Exc=%s", str(exc))
            LOGGER.warn("Failed to compile %s, fallback to no template "
                        "mode", path_or_stream)

    cnf = cparser.load(path_or_stream, ignore_missing=ignore_missing, **kwargs)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            return None

    return cnf

def multi_load(paths, forced_type=None, ignore_missing=False,
               merge=MS_DICTS, marker='*', ac_template=False, ac_context=None,
               ac_schema=None, **kwargs):
    """
    Load multiple config files.

    The first argument `paths` may be a list of config file paths or
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
    the dir /etc/foo/conf.d/, the followings give same results::

      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
                  "/etc/foo/conf.d/c.yml", ])

      multi_load("/etc/foo/conf.d/*.yml")

    :param paths: List of config file paths or a glob pattern to list paths, or
        a list of file/file-like objects
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param merge: Strategy to merge config results of multiple config files
        loaded. see also: anyconfig.mergeabledict.MergeableDict.update()
    :param marker: Globbing markerer to detect paths patterns
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if merge not in MERGE_STRATEGIES:
        raise ValueError("Invalid merge strategy: " + merge)

    if not paths:
        return container()

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        schema = load(ac_schema, forced_type=forced_type,
                      ignore_missing=ignore_missing, merge=merge,
                      marker=marker, ac_template=ac_template,
                      ac_context=ac_context, **kwargs)

    cnf = to_container(ac_context) if ac_context else container()

    if _is_path(paths) and marker in paths:
        paths = anyconfig.utils.sglob(paths)

    for path in paths:
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
        if _is_path(path) and marker in path:
            conf_updates = multi_load(path, forced_type=forced_type,
                                      ignore_missing=ignore_missing,
                                      merge=merge, marker=marker,
                                      ac_template=ac_template,
                                      ac_context=cnf, **kwargs)
        else:
            conf_updates = single_load(path, forced_type=forced_type,
                                       ignore_missing=ignore_missing,
                                       ac_template=ac_template,
                                       ac_context=cnf, **kwargs)

        cnf.update(conf_updates, merge)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            return None

    return cnf

def load(path_specs, forced_type=None, ignore_missing=False,
         merge=MS_DICTS, marker='*', ac_template=False, ac_context=None,
         ac_schema=None, **kwargs):
    """
    Load single or multiple config files or multiple config files specified in
    given paths pattern.

    :param path_specs: Configuration file path or paths or its pattern such as
        '/a/b/*.json' or a list of files/file-like objects
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param merge: Merging strategy to use
    :param marker: Globbing marker to detect paths patterns
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    is_glob_path = _is_path(path_specs) and marker in path_specs
    if is_glob_path or anyconfig.utils.is_iterable(path_specs):
        return multi_load(path_specs, forced_type=forced_type,
                          ignore_missing=ignore_missing, merge=merge,
                          marker=marker, ac_template=ac_template,
                          ac_context=ac_context, ac_schema=ac_schema, **kwargs)
    else:
        return single_load(path_specs, forced_type=forced_type,
                           ignore_missing=ignore_missing,
                           ac_template=ac_template, ac_context=ac_context,
                           ac_schema=ac_schema, **kwargs)

def loads(content, forced_type=None, ac_template=False, ac_context=None,
          ac_schema=None, **kwargs):
    """
    :param content: Configuration file's content
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: Context dict to instantiate template
    :param ac_schema: JSON schema content to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if forced_type is None:
        LOGGER.warn("No config type was given. Try to parse...")
        return anyconfig.parser.parse(content)

    cparser = find_loader(None, forced_type)
    if cparser is None:
        return anyconfig.parser.parse(content)

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        schema = loads(ac_schema, forced_type, ac_template, ac_context,
                       **kwargs)

    if ac_template:
        try:
            LOGGER.debug("Compiling")
            content = anyconfig.template.render_s(content, ac_context)
        except Exception as exc:
            LOGGER.debug("Exc=%s", str(exc))
            LOGGER.warn("Failed to compile and fallback to no template "
                        "mode: '%s'", content[:50] + '...')

    cnf = cparser.loads(content, **kwargs)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            LOGGER.warn("Validation failed: schema=%s", schema)
            return None

    return cnf

def _find_dumper(path_or_stream, forced_type=None):
    """
    Find configuration parser to dump data.

    :param path_or_stream: Output file path or file / file-like object
    :param forced_type: Forced configuration parser type

    :return: Parser-inherited class object
    """
    cparser = find_loader(path_or_stream, forced_type)

    if cparser is None or not getattr(cparser, "dump", False):
        LOGGER.warn("Dump method not implemented. Fallback to json.Parser")
        cparser = anyconfig.backend.json.Parser()

    return cparser

def dump(data, path_or_stream, forced_type=None, **kwargs):
    """
    Save `data` as `path_or_stream`.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param path_or_stream: Output file path or file / file-like object
    :param forced_type: Forced configuration parser type
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend
    """
    dumper = _find_dumper(path_or_stream, forced_type)

    if _is_path(path_or_stream):
        LOGGER.info("Dumping: %s", path_or_stream)
    else:
        LOGGER.info("Dumping: %s", get_path_from_stream(path_or_stream))

    dumper.dump(data, path_or_stream, **kwargs)

def dumps(data, forced_type, **kwargs):
    """
    Return string representation of `data` in forced type format.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param forced_type: Forced configuration parser type
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Backend-specific string representation for the given data
    """
    return _find_dumper(None, forced_type).dumps(data, **kwargs)

# vim:sw=4:ts=4:et:
<commit_msg>call anyconfig.backends.find_parser from anyconfig.api.find_loader and utilize is_path_ arg if useful<commit_after>#
# Copyright (C) 2012 - 2015 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-import,import-error,invalid-name
"""Public APIs of anyconfig module.

.. versionchanged:: 0.2
   Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
   :func:`load` and :func:`dump` can process a file/file-like object or a list
   offile/file-like objects instead of a file path or a list of file paths.

.. versionadded:: 0.2
   Export factory method (create) of anyconfig.mergeabledict.MergeableDict
"""
from __future__ import absolute_import

import os.path

from anyconfig.globals import LOGGER
import anyconfig.backends
import anyconfig.backend.json
import anyconfig.compat
import anyconfig.mergeabledict
import anyconfig.parser
import anyconfig.template
import anyconfig.utils

# Import some global constants will be re-exported:
from anyconfig.mergeabledict import (
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
    get, set_  # flake8: noqa
)
from anyconfig.parser import PATH_SEPS
from anyconfig.schema import validate, gen_schema
from anyconfig.utils import get_path_from_stream

# Re-export and aliases:
list_types = anyconfig.backends.list_types  # flake8: noqa
container = anyconfig.mergeabledict.MergeableDict
to_container = container.create

def _is_path(path_or_stream):
    """
    Is given object `path_or_stream` a file path?
    """
    return isinstance(path_or_stream, anyconfig.compat.STR_TYPES)

def _validate(cnf, schema, format_checker=None):
    """
    Wrapper function for anycnf.schema.vaildate.

    :param cnf: Configuration object :: container
    :param schema: JSON schema object :: container
    :param format_checker: A format property checker object of which class is
        inherited from jsonschema.FormatChecker, it's default if None given.

    :return: True if validation suceeds or jsonschema module is not available.
    """
    (ret, msg) = validate(cnf, schema, format_checker, True)
    if msg:
        LOGGER.warn(msg)

    return ret

def set_loglevel(level):
    """
    :param level: Log level, e.g. logging.INFO and logging.WARN.
    """
    LOGGER.setLevel(level)

def find_loader(path_or_stream, forced_type=None, is_path_=None):
    """
    Find out config parser object appropriate to load from a file of given path
    or file/file-like object.

    :param path_or_stream: Configuration file path or file / file-like object
    :param forced_type: Forced configuration parser type
    :param is_path_: True if given `path_or_stream` is a file path

    :return: Config parser instance or None
    """
    (psr, err) = anyconfig.backends.find_parser(path_or_stream, forced_type,
                                                is_path_=is_path_)
    if psr is None:
        LOGGER.error(err)
        return None

    LOGGER.debug("Using config parser of type: %s", psr.type())
    return psr()  # TBD: Passing initialization arguments.

def single_load(path_or_stream, forced_type=None, ignore_missing=False,
                ac_template=False, ac_context=None, ac_schema=None,
                **kwargs):
    """
    Load single config file.

    :param path_or_stream: Configuration file path or file / file-like object
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore and just return empty result if given file
        (``path_or_stream``) does not exist
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    is_path_ = _is_path(path_or_stream)
    if is_path_:
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
        filepath = path_or_stream
    else:
        filepath = get_path_from_stream(path_or_stream)

    cparser = find_loader(path_or_stream, forced_type, is_path_)
    if cparser is None:
        return None

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        LOGGER.info("Loading schema: %s", ac_schema)
        schema = load(ac_schema, forced_type=forced_type,
                      ignore_missing=ignore_missing, ac_template=ac_template,
                      ac_context=ac_context, **kwargs)

    LOGGER.info("Loading: %s", filepath)
    if ac_template and filepath is not None:
        try:
            LOGGER.debug("Compiling: %s", filepath)
            content = anyconfig.template.render(filepath, ac_context)
            cnf = cparser.loads(content, ignore_missing=ignore_missing,
                                **kwargs)
            if ac_schema is not None:
                if not _validate(cnf, schema, format_checker):
                    return None

            return cnf

        except Exception as exc:
            LOGGER.debug("Exc=%s", str(exc))
            LOGGER.warn("Failed to compile %s, fallback to no template "
                        "mode", path_or_stream)

    cnf = cparser.load(path_or_stream, ignore_missing=ignore_missing, **kwargs)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            return None

    return cnf

def multi_load(paths, forced_type=None, ignore_missing=False,
               merge=MS_DICTS, marker='*', ac_template=False, ac_context=None,
               ac_schema=None, **kwargs):
    """
    Load multiple config files.

    The first argument `paths` may be a list of config file paths or
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
    the dir /etc/foo/conf.d/, the followings give same results::

      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
                  "/etc/foo/conf.d/c.yml", ])

      multi_load("/etc/foo/conf.d/*.yml")

    :param paths: List of config file paths or a glob pattern to list paths, or
        a list of file/file-like objects
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param merge: Strategy to merge config results of multiple config files
        loaded. see also: anyconfig.mergeabledict.MergeableDict.update()
    :param marker: Globbing markerer to detect paths patterns
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if merge not in MERGE_STRATEGIES:
        raise ValueError("Invalid merge strategy: " + merge)

    if not paths:
        return container()

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        schema = load(ac_schema, forced_type=forced_type,
                      ignore_missing=ignore_missing, merge=merge,
                      marker=marker, ac_template=ac_template,
                      ac_context=ac_context, **kwargs)

    cnf = to_container(ac_context) if ac_context else container()

    if _is_path(paths) and marker in paths:
        paths = anyconfig.utils.sglob(paths)

    for path in paths:
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
        if _is_path(path) and marker in path:
            conf_updates = multi_load(path, forced_type=forced_type,
                                      ignore_missing=ignore_missing,
                                      merge=merge, marker=marker,
                                      ac_template=ac_template,
                                      ac_context=cnf, **kwargs)
        else:
            conf_updates = single_load(path, forced_type=forced_type,
                                       ignore_missing=ignore_missing,
                                       ac_template=ac_template,
                                       ac_context=cnf, **kwargs)

        cnf.update(conf_updates, merge)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            return None

    return cnf

def load(path_specs, forced_type=None, ignore_missing=False,
         merge=MS_DICTS, marker='*', ac_template=False, ac_context=None,
         ac_schema=None, **kwargs):
    """
    Load single or multiple config files or multiple config files specified in
    given paths pattern.

    :param path_specs: Configuration file path or paths or its pattern such as
        '/a/b/*.json' or a list of files/file-like objects
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param merge: Merging strategy to use
    :param marker: Globbing marker to detect paths patterns
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: A dict presents context to instantiate template
    :param ac_schema: JSON schema file path to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    is_glob_path = _is_path(path_specs) and marker in path_specs
    if is_glob_path or anyconfig.utils.is_iterable(path_specs):
        return multi_load(path_specs, forced_type=forced_type,
                          ignore_missing=ignore_missing, merge=merge,
                          marker=marker, ac_template=ac_template,
                          ac_context=ac_context, ac_schema=ac_schema, **kwargs)
    else:
        return single_load(path_specs, forced_type=forced_type,
                           ignore_missing=ignore_missing,
                           ac_template=ac_template, ac_context=ac_context,
                           ac_schema=ac_schema, **kwargs)

def loads(content, forced_type=None, ac_template=False, ac_context=None,
          ac_schema=None, **kwargs):
    """
    :param content: Configuration file's content
    :param forced_type: Forced configuration parser type
    :param ignore_missing: Ignore missing config files
    :param ac_template: Assume configuration file may be a template file and
        try to compile it AAR if True
    :param ac_context: Context dict to instantiate template
    :param ac_schema: JSON schema content to validate given config file
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Dict-like object (instance of
        anyconfig.mergeabledict.MergeableDict by default) supports merge
        operations.
    """
    if forced_type is None:
        LOGGER.warn("No config type was given. Try to parse...")
        return anyconfig.parser.parse(content)

    cparser = find_loader(None, forced_type)
    if cparser is None:
        return anyconfig.parser.parse(content)

    if ac_schema is not None:
        kwargs["ac_schema"] = None  # Avoid infinit loop
        format_checker = kwargs.get("format_checker", None)
        schema = loads(ac_schema, forced_type, ac_template, ac_context,
                       **kwargs)

    if ac_template:
        try:
            LOGGER.debug("Compiling")
            content = anyconfig.template.render_s(content, ac_context)
        except Exception as exc:
            LOGGER.debug("Exc=%s", str(exc))
            LOGGER.warn("Failed to compile and fallback to no template "
                        "mode: '%s'", content[:50] + '...')

    cnf = cparser.loads(content, **kwargs)

    if ac_schema is not None:
        if not _validate(cnf, schema, format_checker):
            LOGGER.warn("Validation failed: schema=%s", schema)
            return None

    return cnf

def _find_dumper(path_or_stream, forced_type=None):
    """
    Find configuration parser to dump data.

    :param path_or_stream: Output file path or file / file-like object
    :param forced_type: Forced configuration parser type

    :return: Parser-inherited class object
    """
    cparser = find_loader(path_or_stream, forced_type)

    if cparser is None or not getattr(cparser, "dump", False):
        LOGGER.warn("Dump method not implemented. Fallback to json.Parser")
        cparser = anyconfig.backend.json.Parser()

    return cparser

def dump(data, path_or_stream, forced_type=None, **kwargs):
    """
    Save `data` as `path_or_stream`.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param path_or_stream: Output file path or file / file-like object
    :param forced_type: Forced configuration parser type
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend
    """
    dumper = _find_dumper(path_or_stream, forced_type)

    if _is_path(path_or_stream):
        LOGGER.info("Dumping: %s", path_or_stream)
    else:
        LOGGER.info("Dumping: %s", get_path_from_stream(path_or_stream))

    dumper.dump(data, path_or_stream, **kwargs)

def dumps(data, forced_type, **kwargs):
    """
    Return string representation of `data` in forced type format.

    :param data: Config data object to dump ::
        anyconfig.mergeabledict.MergeableDict by default
    :param forced_type: Forced configuration parser type
    :param kwargs: Backend specific optional arguments, e.g. {"indent": 2} for
        JSON loader/dumper backend

    :return: Backend-specific string representation for the given data
    """
    return _find_dumper(None, forced_type).dumps(data, **kwargs)

# vim:sw=4:ts=4:et:
<|endoftext|>


<commit_before>################################################################################
# Copyright (C) 2015-2016 by Arena of Titans Contributors.
#
# This file is part of Arena of Titans.
#
# Arena of Titans is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Arena of Titans is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Arena of Titans. If not, see <http://www.gnu.org/licenses/>.
################################################################################

import xml.etree.ElementTree as ET  # noqa: S405 (xml from stdlib can be a security risk)

from .board import get_colors_disposition

ET.register_namespace('', 'http://www.w3.org/2000/svg')
ET.register_namespace('cc', 'http://creativecommons.org/ns#')
ET.register_namespace('dc', 'http://purl.org/dc/elements/1.1/')
ET.register_namespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
ET.register_namespace('xlink', 'http://www.w3.org/1999/xlink')

class SvgBoardCreator:
    #: XML namespace. Required to use xpath to search the DOM.
    NS = {'ns': 'http://www.w3.org/2000/svg'}
    _LAST_LINE_CLASS_TEMPLATE = "${{playerIndex == {} ? 'last-line-square' : ''}}"
    _SQUARE_ID_TEMPLATE = 'square-{}-{}'
    _ARRIVAL_SPOT_CLASS_TEMPLATE = 'arrival arrival-{index}'
    _PAWN_CLASS_TEMPLATE = r"${{playerIndexes.indexOf({index}) === -1 ? 'hidden' : ''}} ${{isPawnClickable[{index}] ? 'pointer' : ''}}"  # noqa
    _PAWN_DELETAGE_TEMPLATE = '{attr}.delegate'
    _PAWN_DELETAGE_TEMPLATES = {
        'click': 'pawnClicked({index})',
        'mouseover': 'showPlayerName({index}, $event)',
        'mouseout': 'hidePlayerName()',
    }
    _PLAY_ATTR_TEMPLATE = "moveTo('{}', '{}', '{}')"
    _SQUARE_CLASS_TEMPLATE = "square {} ${{possibleSquares.indexOf('{}') > -1 ? 'highlighted-square' : ''}}"  # noqa
    _TRANSFORM = 'transform'
    _TEMPLATE_LOCATION = 'aot/resources/templates/boards/standard.html'

    def __init__(self, board_description):
        self._xid = 0
        self._yid = 0
        self._number_arms = board_description['number_arms']
        self._colors_disposition = get_colors_disposition(
            board_description['arms_colors'],
            board_description['inner_circle_colors'],
            board_description['number_arms'])
        self._arms_width = board_description['arms_width']
        self._arms_length = board_description['arms_length']

        self._svg, self._board_layer, self._pawn_layer = self._load_template()
        self._create_svg_board()

    def _load_template(self):
        with open(self._TEMPLATE_LOCATION, 'r') as template_file:
            svg = ET.parse(template_file)  # noqa: S314 (ET usage)
            pawn_layer = svg.findall(
                './/ns:g[@id="pawnLayer"]',
                namespaces=self.NS)[0]
            board_layer = svg.findall(
                './/ns:g[@id="boardLayer"]',
                namespaces=self.NS)[0]
            return svg, board_layer, pawn_layer

    def _create_svg_board(self):
        self._setup_board_layer()
        self._setup_arrival_line()
        self._setup_pawn_layer()

    def _get_id(self, x, y):
        return self._SQUARE_ID_TEMPLATE.format(x, y)

    def _set_click_attr(self, element, x, y):
        square_id = self._get_id(x, y)
        click_attr = self._PLAY_ATTR_TEMPLATE.format(square_id, x, y)
        element.set('click.delegate', click_attr)

    def _setup_board_layer(self):
        for y, line in enumerate(self._colors_disposition):
            for x in range(len(line)):
                path = './/*[@id="square-{}-{}"]'.format(x, y)
                square = self._svg.findall(path, namespaces=self.NS)[0]
                self._set_click_attr(square, x, y)
                color = self._colors_disposition[y][x]
                svg_color = color.lower()
                primary_class = svg_color + '-square'
                square_id = self._get_id(x, y)
                class_ = self._SQUARE_CLASS_TEMPLATE.format(primary_class, square_id)
                if self._is_on_last_line(y):
                    class_ += ' ' + self._last_line_class(x)
                square.set('class', class_)

    def _is_on_last_line(self, y):
        return y == 8

    def _last_line_class(self, x):
        index = -1
        if 0 <= x <= 3:
            index = 4
        elif 4 <= x <= 7:
            index = 5
        elif 8 <= x <= 11:
            index = 6
        elif 12 <= x <= 15:
            index = 7
        elif 16 <= x <= 19:
            index = 0
        elif 20 <= x <= 23:
            index = 1
        elif 24 <= x <= 27:
            index = 2
        elif 28 <= x <= 31:
            index = 3

        return self._LAST_LINE_CLASS_TEMPLATE.format(index)

    def _setup_arrival_line(self):
        arrival_layer = self._board_layer.findall(
            './/ns:g[@id="arrivalLayer"]',
            namespaces=self.NS,
        )[0]
        for index, arrival_spot in enumerate(arrival_layer):
            arrival_spot.set('class', self._ARRIVAL_SPOT_CLASS_TEMPLATE.format(index=index))

    def _setup_pawn_layer(self):
        for index, pawn in enumerate(self._pawn_layer):
            pawn.set('class', self._PAWN_CLASS_TEMPLATE.format(index=index))
            for attr, value in self._PAWN_DELETAGE_TEMPLATES.items():
                pawn.set(
                    self._PAWN_DELETAGE_TEMPLATE.format(attr=attr),
                    value.format(index=index),
                )

    @property
    def svg(self):  # pragma: no cover
        return self._svg

    def __str__(self):
        return ET.tostring(self._svg.getroot())\
            .decode('utf-8')\
            .replace('>', '>')

    def __repr__(self):  # pragma: no cover
        return str(self)
<commit_msg>ref(svg): update a function name<commit_after>################################################################################
# Copyright (C) 2015-2016 by Arena of Titans Contributors.
#
# This file is part of Arena of Titans.
#
# Arena of Titans is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Arena of Titans is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Arena of Titans. If not, see <http://www.gnu.org/licenses/>.
################################################################################

import xml.etree.ElementTree as ET  # noqa: S405 (xml from stdlib can be a security risk)

from .board import get_colors_disposition

ET.register_namespace('', 'http://www.w3.org/2000/svg')
ET.register_namespace('cc', 'http://creativecommons.org/ns#')
ET.register_namespace('dc', 'http://purl.org/dc/elements/1.1/')
ET.register_namespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
ET.register_namespace('xlink', 'http://www.w3.org/1999/xlink')

class SvgBoardCreator:
    #: XML namespace. Required to use xpath to search the DOM.
    NS = {'ns': 'http://www.w3.org/2000/svg'}
    _LAST_LINE_CLASS_TEMPLATE = "${{playerIndex == {} ? 'last-line-square' : ''}}"
    _SQUARE_ID_TEMPLATE = 'square-{}-{}'
    _ARRIVAL_SPOT_CLASS_TEMPLATE = 'arrival arrival-{index}'
    _PAWN_CLASS_TEMPLATE = r"${{playerIndexes.indexOf({index}) === -1 ? 'hidden' : ''}} ${{isPawnClickable[{index}] ? 'pointer' : ''}}"  # noqa
    _PAWN_DELETAGE_TEMPLATE = '{attr}.delegate'
    _PAWN_DELETAGE_TEMPLATES = {
        'click': 'pawnClicked({index})',
        'mouseover': 'showPlayerName({index}, $event)',
        'mouseout': 'hidePlayerName()',
    }
    _PLAY_ATTR_TEMPLATE = "handleSquareClicked('{}', '{}', '{}')"
    _SQUARE_CLASS_TEMPLATE = "square {} ${{possibleSquares.indexOf('{}') > -1 ? 'highlighted-square' : ''}}"  # noqa
    _TRANSFORM = 'transform'
    _TEMPLATE_LOCATION = 'aot/resources/templates/boards/standard.html'

    def __init__(self, board_description):
        self._xid = 0
        self._yid = 0
        self._number_arms = board_description['number_arms']
        self._colors_disposition = get_colors_disposition(
            board_description['arms_colors'],
            board_description['inner_circle_colors'],
            board_description['number_arms'])
        self._arms_width = board_description['arms_width']
        self._arms_length = board_description['arms_length']

        self._svg, self._board_layer, self._pawn_layer = self._load_template()
        self._create_svg_board()

    def _load_template(self):
        with open(self._TEMPLATE_LOCATION, 'r') as template_file:
            svg = ET.parse(template_file)  # noqa: S314 (ET usage)
            pawn_layer = svg.findall(
                './/ns:g[@id="pawnLayer"]',
                namespaces=self.NS)[0]
            board_layer = svg.findall(
                './/ns:g[@id="boardLayer"]',
                namespaces=self.NS)[0]
            return svg, board_layer, pawn_layer

    def _create_svg_board(self):
        self._setup_board_layer()
        self._setup_arrival_line()
        self._setup_pawn_layer()

    def _get_id(self, x, y):
        return self._SQUARE_ID_TEMPLATE.format(x, y)

    def _set_click_attr(self, element, x, y):
        square_id = self._get_id(x, y)
        click_attr = self._PLAY_ATTR_TEMPLATE.format(square_id, x, y)
        element.set('click.delegate', click_attr)

    def _setup_board_layer(self):
        for y, line in enumerate(self._colors_disposition):
            for x in range(len(line)):
                path = './/*[@id="square-{}-{}"]'.format(x, y)
                square = self._svg.findall(path, namespaces=self.NS)[0]
                self._set_click_attr(square, x, y)
                color = self._colors_disposition[y][x]
                svg_color = color.lower()
                primary_class = svg_color + '-square'
                square_id = self._get_id(x, y)
                class_ = self._SQUARE_CLASS_TEMPLATE.format(primary_class, square_id)
                if self._is_on_last_line(y):
                    class_ += ' ' + self._last_line_class(x)
                square.set('class', class_)

    def _is_on_last_line(self, y):
        return y == 8

    def _last_line_class(self, x):
        index = -1
        if 0 <= x <= 3:
            index = 4
        elif 4 <= x <= 7:
            index = 5
        elif 8 <= x <= 11:
            index = 6
        elif 12 <= x <= 15:
            index = 7
        elif 16 <= x <= 19:
            index = 0
        elif 20 <= x <= 23:
            index = 1
        elif 24 <= x <= 27:
            index = 2
        elif 28 <= x <= 31:
            index = 3

        return self._LAST_LINE_CLASS_TEMPLATE.format(index)

    def _setup_arrival_line(self):
        arrival_layer = self._board_layer.findall(
            './/ns:g[@id="arrivalLayer"]',
            namespaces=self.NS,
        )[0]
        for index, arrival_spot in enumerate(arrival_layer):
            arrival_spot.set('class', self._ARRIVAL_SPOT_CLASS_TEMPLATE.format(index=index))

    def _setup_pawn_layer(self):
        for index, pawn in enumerate(self._pawn_layer):
            pawn.set('class', self._PAWN_CLASS_TEMPLATE.format(index=index))
            for attr, value in self._PAWN_DELETAGE_TEMPLATES.items():
                pawn.set(
                    self._PAWN_DELETAGE_TEMPLATE.format(attr=attr),
                    value.format(index=index),
                )

    @property
    def svg(self):  # pragma: no cover
        return self._svg

    def __str__(self):
        return ET.tostring(self._svg.getroot())\
            .decode('utf-8')\
            .replace('>', '>')

    def __repr__(self):  # pragma: no cover
        return str(self)
<|endoftext|>