# encoding: utf-8
# Copyright (C) 2017 John Törnblom
#
# This file is part of pyxtuml.
#
# pyxtuml is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.
#
# pyxtuml 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with pyxtuml. If not, see <http://www.gnu.org/licenses/>.
'''
Perform various xtUML meta operations, e.g. create new metamodels and
metaclasses, relate instances and perform navigations and queries.
'''

import logging
import collections

import xtuml

from functools import partial

try:
    from future_builtins import filter, zip
except ImportError:
    pass

logger = logging.getLogger(__name__)

class MetaException(Exception):
    '''
    Base class for all exceptions thrown by the xtuml.meta module.
    '''

class DeleteException(MetaException):
    '''
    An exception that may be thrown during delete operations.
    '''

class RelateException(MetaException):
    '''
    An exception that may be thrown during relate operations.
    '''
    def __init__(self, from_instance, to_instance, rel_id, phrase):
        msg = '%s or %s already related across %s' % (from_instance,
                                                      to_instance,
                                                      rel_id)
        MetaException.__init__(self, msg)

class UnrelateException(MetaException):
    '''
    An exception that may be thrown during unrelate operations.
    '''
    def __init__(self, from_instance, to_instance, rel_id, phrase):
        msg = '%s and %s not related across %s' % (from_instance,
                                                   to_instance,
                                                   rel_id)
        MetaException.__init__(self, msg)

class UnknownLinkException(MetaException):
    '''
    An exception that may be thrown when a link is not found.
    '''
    def __init__(self, from_kind, to_kind, rel_id, phrase):
        if phrase:
            msg = "%s->%s[%s, %s]" % (from_kind, to_kind, repr(rel_id), repr(phrase))
        else:
            msg = "%s->%s[%s]" % (from_kind, to_kind, repr(rel_id))

        MetaException.__init__(self, 'Unknown link ' + msg)

class MetaModelException(MetaException):
    '''
    Base class for exceptions thrown by the MetaModel class.
    '''

class UnknownClassException(MetaModelException):
    '''
    An exception that may be thrown when a metaclass is not found.
    '''
    def __init__(self, kind):
        MetaModelException.__init__(self, 'Unknown class %s' % kind)

def _is_null(instance, name):
    '''
    Determine if an attribute of an *instance* with a specific *name*
    is null.
    '''
    if name in instance.__dict__:
        value = instance.__dict__[name]
    else:
        value = getattr(instance, name)

    if value:
        return False

    elif value is None:
        return True

    name = name.upper()
    metaclass = get_metaclass(instance)
    for attr_name, attr_ty in metaclass.attributes:
        if attr_name.upper() != name:
            continue

        attr_ty = attr_ty.upper()
        if attr_ty == 'UNIQUE_ID':
            # UUID(int=0) is reserved for null
            return value == 0

        elif attr_ty == 'STRING':
            # empty string is reserved for null
            return len(value) == 0

        else:
            #null-values for integer, boolean and real are not supported
            return False

def apply_query_operators(iterable, ops):
    '''
    Apply a series of query operators to a sequence of instances, e.g.
    where_eq(), order_by() or filter functions.
    '''
    for op in ops:
        if isinstance(op, WhereEqual):
            iterable = op(iterable)

        elif isinstance(op, OrderBy):
            iterable = op(iterable)

        elif isinstance(op, dict):
            iterable = WhereEqual(op)(iterable)

        else:
            iterable = filter(op, iterable)

    return iterable

class Association(object):
    '''
    An association connects two metaclasses to each other via two directed
    links.
    '''
    rel_id = None
    source_keys = None
    source_link = None
    target_keys = None
    target_link = None

    def __init__(self, rel_id,
                 source_keys, source_link,
                 target_keys, target_link):
        self.rel_id = rel_id
        self.source_link = source_link
        self.target_link = target_link
        self.source_keys = source_keys
        self.target_keys = target_keys

    @property
    def is_reflexive(self):
        return self.source_link.kind == self.target_link.kind

    def batch_relate(self):
        source_class = self.source_link.to_metaclass
        target_class = self.target_link.to_metaclass
        key_map = tuple(zip(self.source_keys, self.target_keys))

        for inst1 in source_class.storage:
            kwargs = dict()
            skip_instance = False

            for ref_key, primary_key in key_map:
                if _is_null(inst1, ref_key):
                    skip_instance = True
                    break

                kwargs[primary_key] = getattr(inst1, ref_key)

            if skip_instance:
                continue

            for inst2 in target_class.query(kwargs):
                self.source_link.connect(inst2, inst1, check=False)
                self.target_link.connect(inst1, inst2, check=False)

    def formalize(self):
        '''
        Formalize the association and expose referential attributes
        on instances.
        '''
        source_class = self.source_link.to_metaclass
        target_class = self.target_link.to_metaclass

        source_class.referential_attributes |= set(self.source_keys)
        target_class.identifying_attributes |= set(self.target_keys)

        def fget(inst, ref_name, alt_prop):
            other_inst = self.target_link.navigate_one(inst)
            if other_inst is None and alt_prop:
                return alt_prop.fget(inst)

            return getattr(other_inst, ref_name, None)

        def fset(inst, value, name, ref_name, alt_prop):
            kind = get_metaclass(inst).kind
            raise MetaException('%s.%s is a referential attribute '\
                                'and cannot be assigned directly'% (kind, name))

            #other_inst = self.target_link.navigate_one(inst)
            #if other_inst is None and alt_prop:
            #    return alt_prop.fset(inst, value)
            #
            #elif other_inst:
            #    return setattr(other_inst, ref_name, value)

        for ref_key, primary_key in zip(self.source_keys, self.target_keys):
            prop = getattr(source_class.clazz, ref_key, None)
            prop = property(partial(fget, ref_name=primary_key, alt_prop=prop),
                            partial(fset, name=ref_key, ref_name=primary_key, alt_prop=prop))
            setattr(source_class.clazz, ref_key, prop)

class Link(dict):
    '''
    A link connects one metaclass to another in a single direction.

    *rel_id* is used to specify the association between the to metaclasses,
    and a *key map* is used to define the mapping from a primary key in the
    *from_metaclass* to a foreign key in the *to_metaclass*. The *phrase* is
    used to specify the direction when the association is reflexive.

    In addition, links also specify cardinality constraints via the *many*
    and *conditional* attributes.
    '''
    from_metaclass = None
    rel_id = None
    to_metaclass = None
    phrase = None
    key_map = None
    conditional = None
    many = None

    def __init__(self, from_metaclass, rel_id, to_metaclass, phrase='',
                 conditional=False, many=False):
        if isinstance(rel_id, int):
            rel_id = 'R%d' % rel_id

        self.from_metaclass = from_metaclass
        self.rel_id = rel_id
        self.to_metaclass = to_metaclass
        self.key_map = dict()
        self.phrase = phrase
        self.conditional = conditional
        self.many = many

    @property
    def cardinality(self):
        '''
        Obtain the cardinality string.

        Example: '1C' for a conditional link with a single instance [0..1]
                 'MC' for a link with any number of instances [0..*]
                 'M'  for a more than one instance [1..*]
                 'M'  for a link with exactly one instance [1]
        '''
        if self.many:
            s = 'M'
        else:
            s = '1'

        if self.conditional:
            s += 'C'

        return s

    @property
    def kind(self):
        '''
        Obtain the resulting kind when the link is navigated.
        '''
        return self.to_metaclass.kind

    def connect(self, instance, another_instance, check=True):
        '''
        Connect an *instance* to *another_instance*.

        Optionally, disable any cardinality *check* that would prevent the two
        instances from being connected.
        '''
        if instance not in self:
            self[instance] = xtuml.OrderedSet()

        if another_instance in self[instance]:
            return True

        if self[instance] and not self.many and check:
            return False

        self[instance].add(another_instance)
        return True

    def disconnect(self, instance, another_instance):
        '''
        Disconnect an *instance* from *another_instance*.
        '''
        if instance not in self:
            return False

        if another_instance not in self[instance]:
            return False

        self[instance].remove(another_instance)
        return True

    def navigate(self, instance):
        '''
        Navigate from *instance* across the link.
        '''
        if instance in self:
            return self[instance]
        else:
            return set()

    def navigate_one(self, instance):
        '''
        Navigate from *instance* across the link.
        '''
        return next(iter(self.navigate(instance)), None)

    def compute_lookup_key(self, from_instance):
        '''
        Compute the lookup key for an instance, i.e. a foreign key that
        can be used to identify an instance at the end of the link.
        '''
        kwargs = dict()
        for attr, other_attr in self.key_map.items():
            if _is_null(from_instance, attr):
                return None

            if attr in from_instance.__dict__:
                kwargs[other_attr] = from_instance.__dict__[attr]
            else:
                kwargs[other_attr] = getattr(from_instance, attr)

        return frozenset(tuple(kwargs.items()))

    def compute_index_key(self, to_instance):
        '''
        Compute the index key that can be used to identify an instance
        on the link.
        '''
        kwargs = dict()
        for attr in self.key_map.values():
            if _is_null(to_instance, attr):
                return None

            if attr in to_instance.__dict__:
                kwargs[attr] = to_instance.__dict__[attr]
            else:
                kwargs[attr] = getattr(to_instance, attr)

        return frozenset(tuple(kwargs.items()))

class QuerySet(xtuml.OrderedSet):
    '''
    An ordered set which holds instances that match queries.
    '''
    @property
    def first(self):
        '''
        Obtain the first element in the set.
        '''
        if len(self):
            return next(iter(self))

    @property
    def last(self):
        '''
        Obtain the last element in the set.
        '''
        if len(self):
            return next(reversed(self))

class Class(object):
    '''
    A class that all instances created by a metaclass inherits from.

    **Note**: Accesses to attributes, e.g. getattr/setattr, on these objects
    are case insensitive.
    '''
    def __add__(self, other):
        assert isinstance(other, Class)
        return QuerySet([self, other])

    def __sub__(self, other):
        assert isinstance(other, Class)
        if self == other: return QuerySet()
        else: return QuerySet([self])

    def __getattr__(self, name):
        uname = name.upper()
        for attr, _ in get_metaclass(self).attributes:
            if attr.upper() != uname :
                continue

            if attr in self.__dict__:
                return self.__dict__[attr]
            else:
                return object.__getattribute__(self, attr)

        return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        uname = name.upper()
        for attr, _ in get_metaclass(self).attributes:
            if attr.upper() != uname :
                continue

            if attr in self.__dict__:
                self.__dict__[attr] = value
            else:
                return object.__setattr__(self, attr, value)

        self.__dict__[name] = value

    def __delattr__(self, name):
        uname = name.upper()
        for name in self.__dict__:
            if uname == name.upper():
                break

        del self.__dict__[name]

    def __str__(self):
        values = list()
        for attr, ty in get_metaclass(self).attributes:
            value = getattr(self, attr)
            value = xtuml.serialize_value(value, ty)
            values.append('%s=%s' % (attr, value))

        return '%s(%s)' % (self.__class__.__name__, ', '.join(values))

# Backwards compatibility with older versions of pyxtuml
BaseObject = Class

class MetaClass(object):
    '''
    A metaclass contain metadata for instances, e.g. what attributes are
    available, what thier types are, and so on.

    In addition, each metaclass also handle allocations of instances.
    '''
    metamodel = None
    kind = None
    attributes = None
    referential_attributes = None
    identifying_attributes = None
    links = None
    indices = None
    clazz = None
    storage = None

    def __init__(self, kind, metamodel=None):
        self.metamodel = metamodel
        self.kind = kind
        self.attributes = list()
        self.referential_attributes = set()
        self.identifying_attributes = set()
        self.indices = dict()
        self.links = dict()
        self.storage = list()
        self.clazz = type(kind, (Class,), dict(__metaclass__=self))

    def __call__(self, *args, **kwargs):
        '''
        Create and return a new instance using the metaclass constructor.
        '''
        return self.new(*args, **kwargs)

    @property
    def attribute_names(self):
        '''
        Obtain an ordered list of all attribute names.
        '''
        return [name for name, _ in self.attributes]

    def attribute_type(self, attribute_name):
        '''
        Obtain the type of an attribute.
        '''
        attribute_name = attribute_name.upper()
        for name, ty in self.attributes:
            if name.upper() == attribute_name:
                return ty

    def add_link(self, metaclass, rel_id, phrase, conditional, many):
        '''
        Add a new link from *self* to *metaclass*.
        '''
        link = Link(self, rel_id, metaclass, phrase, conditional, many)
        key = (metaclass.kind.upper(), rel_id, phrase)
        self.links[key] = link

        return link

    def append_attribute(self, name, type_name):
        '''
        Append an attribute with a given *name* and *type name* at the end of
        the list of attributes.
        '''
        attr = (name, type_name)
        self.attributes.append(attr)

    def insert_attribute(self, index, name, type_name):
        '''
        Insert an attribute with a given *name* and *type name* at some *index*
        in the list of attributes.
        '''
        attr = (name, type_name)
        self.attributes.insert(index, attr)

    def delete_attribute(self, name):
        '''
        Delete an attribute with a given *name* from the list of attributes.
        '''
        for idx, attr in enumerate(self.attributes):
            attr_name, _ = attr
            if attr_name == name:
                del self.attributes[idx]
                return

    def default_value(self, type_name):
        '''
        Obtain the default value for some *type name*.
        '''
        uname = type_name.upper()
        if   uname == 'BOOLEAN':
            return False

        elif uname == 'INTEGER':
            return 0

        elif uname == 'REAL':
            return 0.0

        elif uname == 'STRING':
            return ''

        elif uname == 'UNIQUE_ID':
            if self.metamodel:
                return next(self.metamodel.id_generator)
            else:
                return None
        else:
            raise MetaException("Unknown type named '%s'" % type_name)

    def new(self, *args, **kwargs):
        '''
        Create and return a new instance.
        '''
        inst = self.clazz()
        self.storage.append(inst)

        # set all attributes with an initial default value
        referential_attributes = dict()
        for name, ty in self.attributes:
            if name not in self.referential_attributes:
                value = self.default_value(ty)
                setattr(inst, name, value)

        # set all positional arguments
        for attr, value in zip(self.attributes, args):
            name, ty = attr
            if name not in self.referential_attributes:
                setattr(inst, name, value)
            else:
                referential_attributes[name] = value

        # set all named arguments
        for name, value in kwargs.items():
            if name not in self.referential_attributes:
                setattr(inst, name, value)
            else:
                referential_attributes[name] = value

        if not referential_attributes:
            return inst

        # batch relate referential attributes
        for link in self.links.values():
            if set(link.key_map.values()) - set(referential_attributes.keys()):
                continue

            kwargs = dict()
            for key, value in link.key_map.items():
                kwargs[key] = referential_attributes[value]

            if not kwargs:
                continue

            for other_inst in link.to_metaclass.query(kwargs):
                relate(other_inst, inst, link.rel_id, link.phrase)

        for name, value in referential_attributes.items():
            if getattr(inst, name) != value:
                logger.warning('unable to assign %s to %s', name, inst)

        return inst

    def clone(self, instance):
        '''
        Create a shallow clone of an *instance*.

        **Note:** the clone and the original instance **does not** have to be
        part of the same metaclass.
        '''
        args = list()
        for name, _ in get_metaclass(instance).attributes:
            value = getattr(instance, name)
            args.append(value)

        return self.new(*args)

    def delete(self, instance, disconnect=True):
        '''
        Delete an *instance* from the instance pool and optionally *disconnect*
        it from any links it might be connected to. If the *instance* is not
        part of the metaclass, a *MetaException* is thrown.
        '''
        if instance in self.storage:
            self.storage.remove(instance)
        else:
            raise DeleteException("Instance not found in the instance pool")

        if not disconnect:
            return

        for link in self.links.values():
            if instance not in link:
                continue

            for other in link[instance]:
                unrelate(instance, other, link.rel_id, link.phrase)

    def select_one(self, *args):
        '''
        Select a single instance from the instance pool. Query operators such as
        where_eq(), order_by() or filter functions may be passed as optional
        arguments.
        '''
        s = apply_query_operators(self.storage, args)
        return next(iter(s), None)

    def select_many(self, *args):
        '''
        Select several instances from the instance pool. Query operators such as
        where_eq(), order_by() or filter functions may be passed as optional
        arguments.
        '''
        s = apply_query_operators(self.storage, args)
        if isinstance(s, QuerySet):
            return s
        else:
            return QuerySet(s)

    def _find_assoc_links(self, kind, rel_id, phrase=''):
        key = (kind.upper(), rel_id, phrase)
        for link in self.links.values():
            if link.rel_id != rel_id or link.phrase != phrase:
                continue

            metaclass = self.metamodel.find_metaclass(link.kind)
            if key in metaclass.links:
                return link, metaclass.links[key]

        raise UnknownLinkException(self.kind, kind, rel_id, phrase)

    def navigate(self, inst, kind, rel_id, phrase=''):
        '''
        Navigate across a link with some *rel_id* and *phrase* that yields
        instances of some *kind*.
        '''
        key = (kind.upper(), rel_id, phrase)
        if key in self.links:
            link = self.links[key]
            return link.navigate(inst)

        link1, link2 = self._find_assoc_links(kind, rel_id, phrase)
        inst_set = xtuml.OrderedSet()
        for inst in link1.navigate(inst):
            inst_set |= link2.navigate(inst)

        return inst_set

    def query(self, dictonary_of_values):
        '''
        Query the instance pool for instances with attributes that match a given
        *dictonary of values*.
        '''
        return WhereEqual(dictonary_of_values)(self.storage)

class NavChain(object):
    '''
    A navigation chain initializes a navigation from one or more instances.
    Navigation may be syntactically cascaded in several ways:

       res = NavChain(inst).nav('X', 'R100', 'phrase').nav('Y', 101)

    or using an OAL/RSL inspired syntax:

       res = NavChain(inst).X[100, 'phrase'].Y[101](lamda x: <filter expression>)
    '''

    def __init__(self, handle):
        if handle is None:
            handle = []

        elif isinstance(handle, Class):
            handle = [handle]

        elif not isinstance(handle, collections.Iterable):
            raise MetaException("Unable to navigate across '%s'" % type(handle))

        self.handle = handle
        self._kind = None

    def nav(self, kind, relid, phrase=''):
        self.handle = NavChain._nav(self.handle, kind, relid, phrase)
        return self

    @staticmethod
    def _nav(handle, kind, rel_id, phrase):
        if isinstance(rel_id, int):
            rel_id = 'R%d' % rel_id

        for inst in iter(handle):
            metaclass = get_metaclass(inst)
            for result in metaclass.navigate(inst, kind, rel_id, phrase):
                yield result

    def __getattr__(self, kind):
        '''
        The navigation chain specified a *kind*, e.g.
        >>> chain.X
        '''
        self._kind = kind
        return self

    def __getitem__(self, args):
        '''
        The navigation chain specified a link, e.g. the rel_id and phrase, e.g.
        >>> chain[100, 'phrase']
        '''
        if not isinstance(args, tuple):
            args = (args, '')

        relid, phrase = args

        return self.nav(self._kind, relid, phrase)

    def __call__(self, *args):
        '''
        The navigation chain is invoked. Query operators such as where_eq(),
        order_by() or filter functions may be passed as optional arguments, e.g.

        >>> chain(lambda selected: selected.Name == 'test')
        '''
        handle = self.handle or list()
        handle = apply_query_operators(handle, args)
        if isinstance(handle, QuerySet):
            return handle
        else:
            return QuerySet(handle)

class NavOneChain(NavChain):
    '''
    A navigation chain that yeilds an instance, or None.
    '''
    def __call__(self, *args):
        handle = self.handle or list()
        handle = apply_query_operators(handle, args)
        return next(iter(handle), None)

def navigate_one(instance):
    '''
    Initialize a navigation from one *instance* to another across a one-to-one
    association.

    The resulting query will return an instance or None.

    Usage example:

    >>> from xtuml import navigate_one as one
    >>> m = xtuml.load_metamodel('db.sql')
    >>> inst = m.select_any('My_Modeled_Class')
    >>> other_inst = one(inst).Some_Other_Class[4]()

    The syntax is somewhat similar to the action language used in BridgePoint.
    The same semantics would be expressed in BridgePoint as::

        select any inst from instances of My_Modeled_Class;
        select one other_inst related by inst->Some_Other_Class[R4];

    **Note:** If the navigated association is reflexive, a phrase must be
    provided, e.g.

    >>> other_inst = one(inst).Some_Other_Class[4, 'some phrase']()
    '''
    return navigate_any(instance)

def navigate_any(instance_or_set):
    '''
    Initialize a navigation from an instance, or a set of instances, to
    associated instances across a one-to-many or many-to-many association.

    The resulting query will return an instance or None.
    '''
    return NavOneChain(instance_or_set)

def navigate_many(instance_or_set):
    '''
    Initialize a navigation from an instance, or a set of instances, to
    associated instances across a one-to-many or many-to-many association.

    The resulting query will return a set of instances.
    '''
    return NavChain(instance_or_set)

def navigate_subtype(supertype, rel_id):
    '''
    Perform a navigation from *supertype* to its subtype across *rel_id*. The
    navigated association must be modeled as a subtype-supertype association.

    The return value will an instance or None.
    '''
    if not supertype:
        return

    if isinstance(rel_id, int):
        rel_id = 'R%d' % rel_id

    metaclass = get_metaclass(supertype)
    for kind, rel_id_candidate, _ in metaclass.links:
        if rel_id != rel_id_candidate:
            continue

        subtype = navigate_one(supertype).nav(kind, rel_id)()
        if subtype:
            return subtype

class WhereEqual(dict):
    '''
    Helper class to create a dictonary of values for queries using
    python keyword arguments to *where_eq()*
    '''
    def __call__(self, s):
        items = collections.deque(self.items())
        for inst in iter(s):
            for name, value in iter(items):
                if getattr(inst, name) != value:
                    break
            else:
                yield inst

def where_eq(**kwargs):
    '''
    Return a where-clause that filters out instances based on named
    keywords.

    Usage example:

    >>> from xtuml import where_eq as where
    >>> m = xtuml.load_metamodel('db.sql')
    >>> inst = m.select_any('My_Modeled_Class', where(My_Number=5))
    '''
    return WhereEqual(kwargs)

def sort_reflexive(set_of_instances, rel_id, phrase):
    '''
    Sort a *set of instances* in the order they appear across a conditional and
    reflexive association. The first instance in the resulting ordered set is
    **not** associated to an instance across the given *phrase*.
    '''
    if not isinstance(set_of_instances, QuerySet):
        raise MetaException('The collection to sort must be a QuerySet')

    if not set_of_instances.first:
        return QuerySet()

    if isinstance(rel_id, int):
        rel_id = 'R%d' % rel_id

    # Figure out the phrase in the other direction
    metaclass = get_metaclass(set_of_instances.first)
    for link in metaclass.links.values():
        if link.to_metaclass != metaclass:
            continue

        if link.rel_id != rel_id:
            continue

        if link.phrase == phrase:
            continue

        other_phrase = link.phrase
        break
    else:
        raise UnknownLinkException(metaclass.kind, metaclass.kind, rel_id, phrase)

    first_filt = lambda sel: not navigate_one(sel).nav(metaclass.kind, rel_id, phrase)()
    first_instances = list(filter(first_filt, set_of_instances))
    if not first_instances:
        #the instance sequence is recursive, start anywhere
        first_instances = [set_of_instances.first]

    def sequence_generator():
        for first in first_instances:
            inst = first
            while inst:
                if inst in set_of_instances:
                    yield inst
                inst = navigate_one(inst).nav(metaclass.kind, rel_id, other_phrase)()
                if inst is first:
                    break

    return QuerySet(sequence_generator())

def _find_link(inst1, inst2, rel_id, phrase):
    '''
    Find links that correspond to the given arguments.
    '''
    metaclass1 = get_metaclass(inst1)
    metaclass2 = get_metaclass(inst2)

    if isinstance(rel_id, int):
        rel_id = 'R%d' % rel_id

    for ass in metaclass1.metamodel.associations:
        if ass.rel_id != rel_id:
            continue

        if (ass.source_link.from_metaclass.kind == metaclass1.kind and
            ass.source_link.to_metaclass.kind == metaclass2.kind and
            ass.source_link.phrase == phrase):
            return inst1, inst2, ass

        if (ass.target_link.from_metaclass.kind == metaclass1.kind and
            ass.target_link.to_metaclass.kind == metaclass2.kind and
            ass.target_link.phrase == phrase):
            return inst2, inst1, ass

    raise UnknownLinkException(metaclass1.kind, metaclass2.kind, rel_id, phrase)

def relate(from_instance, to_instance, rel_id, phrase=''):
    '''
    Relate *from_instance* to *to_instance* across *rel_id*. For reflexive
    association, a *phrase* indicating the direction must also be provided.

    The two instances are related to each other by copying the identifying
    attributes from the instance on the TO side of a association to the instance
    n the FROM side. Updated values which affect existing associations are
    propagated. A set of all affected instances will be returned.
    '''
    if None in [from_instance, to_instance]:
        return False

    inst1, inst2, ass = _find_link(from_instance, to_instance, rel_id, phrase)
    if not ass.source_link.connect(inst1, inst2):
        raise RelateException(from_instance, to_instance, rel_id, phrase)

    if not ass.target_link.connect(inst2, inst1):
        raise RelateException(from_instance, to_instance, rel_id, phrase)

    return True

def unrelate(from_instance, to_instance, rel_id, phrase=''):
    '''
    Unrelate *from_instance* from *to_instance* across *rel_id*. For reflexive
    associations, a *phrase* indicating the direction must also be provided.

    The two instances are unrelated from each other by reseting the identifying
    attributes on the FROM side of the association. Updated values which affect
    existing associations are propagated. A set of all affected instances will
    be returned.
    '''
    if None in [from_instance, to_instance]:
        return False

    inst1, inst2, ass = _find_link(from_instance, to_instance, rel_id, phrase)
    if not ass.source_link.disconnect(inst1, inst2):
        raise UnrelateException(from_instance, to_instance, rel_id, phrase)

    if not ass.target_link.disconnect(inst2, inst1):
        raise UnrelateException(from_instance, to_instance, rel_id, phrase)

    return True

def get_metaclass(class_or_instance):
    '''
    Get the metaclass for a *class_or_instance*.
    '''
    if isinstance(class_or_instance, Class):
        return class_or_instance.__metaclass__

    elif issubclass(class_or_instance, Class):
        return class_or_instance.__metaclass__

    raise MetaException("the provided argument is not an xtuml class or instance")

def get_metamodel(class_or_instance):
    '''
    Get the metamodel in which a *class_or_instance* was created.
    '''
    return get_metaclass(class_or_instance).metamodel

def delete(instance, disconnect=True):
    '''
    Delete an *instance* from its metaclass instance pool and optionally
    *disconnect* it from any links it might be connected to.
    '''
    if not isinstance(instance, Class):
        raise DeleteException("the provided argument is not an xtuml instance")

    return get_metaclass(instance).delete(instance, disconnect)

def cardinality(instance_or_set):
    '''
    Get the cardinality of an *instance_or_set*.
    '''
    if not instance_or_set:
        return 0

    if isinstance(instance_or_set, Class):
        return 1

    return len(instance_or_set)

class OrderBy(list):
    '''
    Helper class to create a tuple of key values for sorting an
    instance set.
    '''
    reverse = False

    def __init__(self, attrs, reverse=False):
        list.__init__(self, attrs)
        self.reverse = reverse

    def __call__(self, s):
        key = lambda el: [getattr(el, name) for name in self]
        return sorted(s, key=key, reverse=self.reverse)

def order_by(*attrs):
    '''
    Return a query ordering operator that will order an instance
    set based on attribute names passed. When ordering on multiple
    attributes is specified, the set will be sorted by the first
    attribute and then within each value of this, by the second
    attribute and so on.

    Usage example:

    >>> from xtuml import order_by
    >>> m = xtuml.load_metamodel('db.sql')
    >>> inst = m.select_many('My_Class', order_by('Name', 'Number'))
    '''
    return OrderBy(attrs, reverse=False)

def reverse_order_by(*attrs):
    '''
    Return a reversed query ordering operator with the same behavior as
    order_by() but reversed order.

    Usage example:

    >>> from xtuml import reverse_order_by
    >>> m = xtuml.load_metamodel('db.sql')
    >>> inst = m.select_many('My_Class', reverse_order_by('Name', 'Number'))
    '''
    return OrderBy(attrs, reverse=True)

class MetaModel(object):
    '''
    A metamodel contains metaclasses with associations between them.

    **Note:** All identifiers, e.g. attributes, association ids, key letters
    (the kind or name of a class), are case **insensitive**.
    '''
    metaclasses = None
    associations = None
    id_generator = None

    def __init__(self, id_generator=None):
        '''
        Create a new, empty metamodel. Optionally, specify an id generator
        used to obtain unique identifiers.
        '''
        if id_generator is None:
            id_generator = xtuml.UUIDGenerator()

        self.metaclasses = dict()
        self.associations = list()
        self.id_generator = id_generator

    @property
    def instances(self):
        '''
        Obtain a sequence of all instances in the metamodel.
        '''
        for metaclass in self.metaclasses.values():
            for inst in metaclass.storage:
                yield inst

    def define_class(self, kind, attributes, doc=''):
        '''
        Define a new class in the metamodel, and return its metaclass.
        '''
        ukind = kind.upper()
        if ukind in self.metaclasses:
            raise MetaModelException('A class with the name %s is already defined' % kind)

        metaclass = MetaClass(kind, self)
        for name, ty in attributes:
            metaclass.append_attribute(name, ty)

        self.metaclasses[ukind] = metaclass

        return metaclass

    def find_class(self, kind):
        '''
        Find a class of some *kind* in the metamodel.
        '''
        return self.find_metaclass(kind).clazz

    def find_metaclass(self, kind):
        '''
        Find a metaclass of some *kind* in the metamodel.
        '''
        ukind = kind.upper()
        if ukind in self.metaclasses:
            return self.metaclasses[ukind]
        else:
            raise UnknownClassException(kind)

    def new(self, kind, *args, **kwargs):
        '''
        Create and return a new instance in the metamodel of some *kind*.

        Optionally, initial attribute values may be assigned to the new instance
        by passing them as positional or keyword arguments. Positional arguments
        are assigned in the order in which they appear in the metaclass.
        '''
        metaclass = self.find_metaclass(kind)
        return metaclass.new(*args, **kwargs)

    def clone(self, instance):
        '''
        Create a shallow clone of an *instance*.

        **Note:** the clone and the original instance **does not** have to be
        part of the same metaclass.
        '''
        metaclass = get_metaclass(instance)
        metaclass = self.find_metaclass(metaclass.kind)
        return metaclass.clone(instance)

    def define_association(self, rel_id, source_kind, source_keys, source_many,
                           source_conditional, source_phrase, target_kind,
                           target_keys, target_many, target_conditional,
                           target_phrase):
        '''
        Define and return an association from one kind of class (the source
        kind) to some other kind of class (the target kind).
        '''
        if isinstance(rel_id, int):
            rel_id = 'R%d' % rel_id

        source_metaclass = self.find_metaclass(source_kind)
        target_metaclass = self.find_metaclass(target_kind)

        source_link = target_metaclass.add_link(source_metaclass, rel_id,
                                                many=source_many,
                                                phrase=target_phrase,
                                                conditional=source_conditional)

        target_link = source_metaclass.add_link(target_metaclass, rel_id,
                                                many=target_many,
                                                phrase=source_phrase,
                                                conditional=target_conditional)

        ass = Association(rel_id,
                          source_keys, source_link,
                          target_keys, target_link)

        source_link.key_map = dict(zip(source_keys, target_keys))
        target_link.key_map = dict(zip(target_keys, source_keys))

        self.associations.append(ass)

        return ass

    def define_unique_identifier(self, kind, name, *named_attributes):
        '''
        Define a unique identifier for some *kind* of class based on its
        *named attributes*.
        '''
        if not named_attributes:
            return

        if isinstance(name, int):
            name = 'I%d' % name

        metaclass = self.find_metaclass(kind)
        metaclass.indices[name] = tuple(named_attributes)
        metaclass.identifying_attributes |= set(named_attributes)

    def select_many(self, kind, *args):
        '''
        Query the metamodel for a set of instances of some *kind*. Query
        operators such as where_eq(), order_by() or filter functions may be
        passed as optional arguments.

        Usage example:

        >>> m = xtuml.load_metamodel('db.sql')
        >>> inst_set = m.select_many('My_Class', lambda sel: sel.number > 5)
        '''
        metaclass = self.find_metaclass(kind)
        return metaclass.select_many(*args)

    def select_one(self, kind, *args):
        '''
        Query the metamodel for a single instance of some *kind*. Query
        operators such as where_eq(), order_by() or filter functions may be
        passed as optional arguments.

        Usage example:

        >>> m = xtuml.load_metamodel('db.sql')
        >>> inst = m.select_one('My_Class', lambda sel: sel.name == 'Test')
        '''
        metaclass = self.find_metaclass(kind)
        return metaclass.select_one(*args)

    # Backwards compatibility with older versions of pyxtuml
    select_any = select_one

    def is_consistent(self):
        '''
        Check the metamodel for integrity violations.
        '''
        if xtuml.check_association_integrity(self):
            return False

        return xtuml.check_uniqueness_constraint(self) == 0


from aiohttp import web

from hyperion.fetch import ApiError
from hyperion.fetch.twitter import fetch_twitter
from .util import str_json_response

async def api_twitter(request):
    """
    Gets the twitter feed from a given handle.
    :return: The feed in json format.
    """
    handle = request.match_info.get('handle', None)
    if handle is None:
        raise web.HTTPNotFound(body="Not found.")

    try:
        posts = await fetch_twitter(handle)
    except ApiError as e:
        raise web.HTTPInternalServerError(body=e.status)
    return str_json_response(posts)


# -*- coding: utf-8 -*-
"""
   zeronimo.messaging
   ~~~~~~~~~~~~~~~~~~

   :copyright: (c) 2013-2017 by Heungsub Lee
   :license: BSD, see LICENSE for more details.

"""
from __future__ import absolute_import

from collections import namedtuple
try:
    import cPickle as pickle
except ImportError:
    import pickle

from zeronimo.helpers import eintr_retry_zmq, make_repr

__all__ = ['ACK', 'DONE', 'ITER', 'ACCEPT', 'REJECT', 'RETURN', 'RAISE',
           'YIELD', 'BREAK', 'PACK', 'UNPACK', 'Call', 'Reply', 'send', 'recv',
           'parse', 'parse_call']

# Method masks:
ACK = 0b10000000
DONE = 0b01000000
ITER = 0b00100000

# Methods:
ACCEPT = ACK | 0b01
REJECT = ACK | 0b10
RETURN = DONE | 0b01
RAISE = DONE | 0b10
YIELD = ITER | 0b01
BREAK = ITER | DONE | 0b10

#: The seam between topics (prefixes) and header-payload.
SEAM = b'\xff'

#: The default function to pack message.
PACK = lambda obj: pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)

#: The default function to unpack message.
UNPACK = pickle.loads

class Call(namedtuple('Call', 'name call_id reply_to hints caller_info')):

    __repr__ = lambda x: make_repr(x, keywords=x._fields)

class Reply(namedtuple('Reply', 'method call_id task_id')):

    def __repr__(self):
        method = {
            ACCEPT: 'ACCEPT',
            REJECT: 'REJECT',
            RETURN: 'RETURN',
            RAISE: 'RAISE',
            YIELD: 'YIELD',
            BREAK: 'BREAK',
        }[self.method]
        class M(object):
            def __repr__(self):
                return method
        return make_repr(self, keywords=self._fields, data={'method': M()})

def send(socket, header, payload, topics=(), flags=0):
    """Sends header, payload, and topics through a ZeroMQ socket.

    :param socket: a zmq socket.
    :param header: a list of byte strings which represent a message header.
    :param payload: the serialized byte string of a payload.
    :param topics: a chain of topics.
    :param flags: zmq flags to send messages.

    """
    msgs = []
    msgs.extend(topics)
    msgs.append(SEAM)
    msgs.extend(header)
    msgs.append(payload)
    return eintr_retry_zmq(socket.send_multipart, msgs, flags)

def recv(socket, flags=0, capture=(lambda msgs: None)):
    """Receives header, payload, and topics through a ZeroMQ socket.

    :param socket: a zmq socket.
    :param flags: zmq flags to receive messages.
    :param capture: a function to capture received messages.

    """
    msgs = eintr_retry_zmq(socket.recv_multipart, flags)
    capture(msgs)
    return parse(msgs)

def parse(msgs):
    try:
        seam = msgs.index(SEAM)
    except ValueError:
        raise EOFError('no seam after topics')
    if seam == len(msgs) - 1:
        raise EOFError('neither header nor payload')
    topics = msgs[:seam]
    header = msgs[seam + 1:-1]
    payload = msgs[-1]
    return header, payload, topics

def parse_call(header):
    try:
        name = header[0].decode('utf-8')
        call_id, reply_to = header[1:3]
    except (IndexError, ValueError):
        raise ValueError('too few fields in call header')
    hints = tuple(header[3:-1])
    caller_info = header[-1] or None
    return Call(name, call_id, reply_to, hints, caller_info)


# coding=utf-8
"""
Stop gap build script until I find something better.
"""
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
import sys
import functools
import glob
import json
import os
import socket
import subprocess

from checksumdir import dirhash
from pynt import task
from pyntcontrib import execute, safe_cd
from semantic_version import Version

sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
from build_utils import check_is_aws, skip_if_no_change, execute_with_environment, get_versions, get_packages, execute_get_text

PROJECT_NAME = "find_known_secrets"
SRC = '.'

# generic python
PYTHON = "python"
IS_DJANGO = False
IS_TRAVIS = 'TRAVIS' in os.environ
if IS_TRAVIS:
    PIPENV = ""
else:
    PIPENV = "pipenv run"
GEM_FURY = ""

CURRENT_HASH = None

MAC_LIBS = ":..:"

@task()
@skip_if_no_change("git_secrets")
def git_secrets():
    if check_is_aws():
        # no easy way to install git secrets on ubuntu.
        return
    if IS_TRAVIS:
        # nothing is edited on travis
        return
    try:
        commands = ["git secrets --install", "git secrets --register-aws"]
        for command in commands:
            cp = subprocess.run(command.split(" "),
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                shell=False, check=True)
            for stream in [cp.stdout, cp.stderr]:
                if stream:
                    for line in stream.decode().split("\n"):
                        print("*" + line)
    except subprocess.CalledProcessError as cpe:
        print(cpe)
        installed = False
        for stream in [cpe.stdout, cpe.stderr]:
            if stream:
                for line in stream.decode().split("\n"):
                    print("-" + line)
                    if "commit-msg already exists" in line:
                        print("git secrets installed.")
                        installed = True
                        break
        if not installed:
            raise
    execute(*("git secrets --scan".strip().split(" ")))

@task()
def clean():
    return

@task()
@skip_if_no_change("formatting")
def formatting():
    with safe_cd(SRC):
        if sys.version_info < (3, 6):
            print("Black doesn't work on python 2")
            return
        command = "{0} black {1}".format(PIPENV, PROJECT_NAME).strip()
        print(command)
        result = execute_get_text(command)
        assert result
        changed =[]
        for line in result.split("\n"):
            if "reformatted " in line:
                file = line[len("reformatted "):].strip()
                changed.append(file)
        for change in changed:
            command ="git add {0}".format(change)
            print(command)
            execute(*(command.split(" ")))

@task(clean)
@skip_if_no_change("compile_py")
def compile_py():
    with safe_cd(SRC):
        execute(PYTHON, "-m", "compileall", PROJECT_NAME)

@task(compile_py)
@skip_if_no_change("prospector")
def prospector():
    with safe_cd(SRC):
        command = "{0} prospector {1} --profile {1}_style --pylint-config-file=pylintrc.ini --profile-path=.prospector" \
            .format(PIPENV, PROJECT_NAME).strip().replace("  ", " ")
        print(command)
        execute(*(command
                  .split(" ")))

@task()
@skip_if_no_change("detect_secrets")
def detect_secrets():
    # use
    # blah blah = "foo"     # pragma: whitelist secret
    # to ignore a false posites
    errors_file = "detect-secrets-results.json"

    command = "detect-secrets --scan --base64-limit 4 --exclude .idea|.js|.min.js|.html|.xsd|" \
              "lock.json|synced_folders|.scss|" \
              "lint.txt|{0}".format(errors_file)
    print(command)
    bash_process = subprocess.Popen(command.split(" "),
                                    # shell=True,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE
                                    )
    out, err = bash_process.communicate()  # wait

    with open(errors_file, "w+") as file_handle:
        file_handle.write(out.decode())

    with open(errors_file) as f:
        try:
            data = json.load(f)
            if data["results"]:
                for result in data["results"]:
                    print(result)
                print("detect-secrets has discovered high entropy strings, possibly passwords?")
                exit(-1)
                return
                # raise TypeError("detect-secrets has discovered high entropy strings, possibly passwords?")
        except Exception:
            print("Can't read json")

@task(compile_py, formatting, prospector)
@skip_if_no_change("lint")
def lint():
    with safe_cd(SRC):
        if os.path.isfile("lint.txt"):
            execute("rm", "lint.txt")

    with safe_cd(SRC):
        if IS_DJANGO:
            django_bits = "--load-plugins pylint_django "
        else:
            django_bits = ""

        # command += "{0}--rcfile=pylintrc.ini {1}".format(django_bits, PROJECT_NAME).split(" ")
        command = "{0} pylint {1} --rcfile=pylintrc.ini {2}".format(PIPENV, django_bits, PROJECT_NAME) \
            .strip() \
            .replace("  ", " ")
        print(command)
        command = command.split(" ")

        # keep out of src tree, causes extraneous change detections
        lint_output_file_name = "lint.txt"
        with open(lint_output_file_name, "w") as outfile:
            env = config_pythonpath()
            subprocess.call(command, stdout=outfile, env=env)

        fatal_errors = sum(1 for line in open(lint_output_file_name)
                           if "no-member" in line or \
                           "no-name-in-module" in line or \
                           "import-error" in line)

        if fatal_errors > 0:
            for line in open(lint_output_file_name):
                if "no-member" in line or \
                        "no-name-in-module" in line or \
                        "import-error" in line:
                    print(line)
            print("Fatal lint errors : {0}".format(fatal_errors))
            exit(-1)
            return

        cutoff = 69
        num_lines = sum(1 for line in open(lint_output_file_name)
                        if "*************" not in line
                        and "---------------------" not in line
                        and "Your code has been rated at" not in line)
        if num_lines > cutoff:
            print("Too many lines of lint : {0}".format(num_lines))
            exit(-1)
            return

@task()
@skip_if_no_change("nose_tests")
def nose_tests():
    if IS_DJANGO:
        command = "{0} manage.py test -v 2".format(PYTHON)
        # We'd expect this to be MAC or a build server.
        my_env = config_pythonpath()
        execute_with_environment(command, env=my_env)
    else:
        my_env = config_pythonpath()
        command = "{0} {1} -m nose {2}".format(PIPENV, PYTHON, "test").strip()
        print(command)
        execute_with_environment(command, env=my_env)

def config_pythonpath():
    if check_is_aws():
        env = "DEV"
    else:
        env = "MAC"
    my_env = {'ENV': env}
    for key, value in os.environ.items():
        my_env[key] = value
    my_env["PYTHONPATH"] = my_env.get("PYTHONPATH",
                                      "") + MAC_LIBS
    print(my_env["PYTHONPATH"])
    return my_env

@task()
def coverage():
    print("Coverage tests always re-run")
    with safe_cd(SRC):
        my_env = config_pythonpath()
        command = "{0} py.test {1} --cov={2} --cov-report html:coverage --cov-fail-under 60  --verbose".format(
            PIPENV,
            "test", PROJECT_NAME)
        print(command)
        execute_with_environment(command, my_env)

@task()
@skip_if_no_change("docs")
def docs():
    with safe_cd(SRC):
        with safe_cd("docs"):
            my_env = config_pythonpath()
            execute_with_environment("pipenv run make html", env=my_env)

@task()
def pip_check():
    print("pip_check always reruns")
    with safe_cd(SRC):
        execute("pipenv", "check")

@task()
def compile_mark_down():
    with safe_cd(SRC):
        if IS_TRAVIS:
            command = "pandoc --from=markdown --to=rst --output=README.rst README.md".strip().split(
                " ")
        else:
            command = "{0} pandoc --from=markdown --to=rst --output=README.rst README.md".format(PIPENV).strip().split(
                " ")
        execute(*(command))

@task()
@skip_if_no_change("mypy")
def mypy():
    if sys.version_info < (3, 4):
        print("Mypy doesn't work on python < 3.4")
        return
    mypy_file = "mypy_errors.txt"
    if os.path.isfile(mypy_file):
        execute("rm", mypy_file)
    command = "{0} mypy {1} --ignore-missing-imports --strict".format(PIPENV, PROJECT_NAME).strip()
    bash_process = subprocess.Popen(command.split(" "),
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE
                                    )
    out, err = bash_process.communicate()  # wait

    with open(mypy_file, "w+") as lint_file:
        lines = out.decode().split("\n")
        for line in lines:
            if "test.py" in line:
                continue
            if "tests.py" in line:
                continue
            if "/test_" in line:
                continue
            if "/tests_" in line:
                continue
            else:
                lint_file.writelines([line + "\n"])

    num_lines = sum(1 for line in open(mypy_file) if line.strip())
    max_lines = 2
    if num_lines > max_lines:
        print("Too many lines of mypy : {0}, max {1}".format(num_lines, max_lines))
        exit(-1)
        return

@task()
def pin_dependencies():
    with safe_cd(SRC):
        execute(*("{0} pipenv_to_requirements".format(PIPENV).strip().split(" ")))

@task()
@skip_if_no_change("vulture", expect_files="dead_code.txt")
def dead_code():
    """
    This also finds code you are working on today!
    """
    with safe_cd(SRC):
        if IS_TRAVIS:
            command = "{0} vulture {1}".format(PYTHON, PROJECT_NAME).strip().split()
        else:
            command = "{0} vulture {1}".format(PIPENV, PROJECT_NAME).strip().split()

        output_file_name = "dead_code.txt"
        with open(output_file_name, "w") as outfile:
            env = config_pythonpath()
            subprocess.call(command, stdout=outfile, env=env)

        cutoff = 20
        num_lines = sum(1 for line in open(output_file_name) if line)
        if num_lines > cutoff:
            print("Too many lines of dead code : {0}, max {1}".format(num_lines, cutoff))
            exit(-1)

@task()
def jiggle_version():
    command = "{0} jiggle_version --project={1} --source={2}".format(PIPENV, PROJECT_NAME, "").strip()
    execute(*(command.split(" ")))

@task()
def check_setup_py():
    with safe_cd(SRC):
        if IS_TRAVIS:
            execute(PYTHON, *("setup.py check -r -s".split(" ")))
        else:
            execute(*("{0} {1} setup.py check -r -s".format(PIPENV, PYTHON).strip().split(" ")))

@task(formatting, mypy, detect_secrets, git_secrets, check_setup_py, nose_tests, coverage, compile_py, dead_code, lint,
      compile_mark_down, pin_dependencies, jiggle_version)
@skip_if_no_change("package")
def package():
    with safe_cd(SRC):
        for folder in ["build", "dist", PROJECT_NAME + ".egg-info"]:
            execute("rm", "-rf", folder)

    with safe_cd(SRC):
        execute(PYTHON, "setup.py", "sdist", "--formats=gztar,zip")

# FAST. FATAL ERRORS. DON'T CHANGE THINGS THAT CHECK IN
@task(mypy, detect_secrets, git_secrets, check_setup_py, compile_py, dead_code)
@skip_if_no_change("package")
def pre_commit_hook():
    # Don't format or update version
    # Don't do slow stuff- discourages frequent check in
    # Run checks that are likely to have FATAL errors, not just sloppy coding.
    pass

# Don't break the build, but don't change source tree either.
@task(mypy, detect_secrets, git_secrets, nose_tests, coverage, check_setup_py, compile_py, dead_code)
@skip_if_no_change("package")
def pre_push_hook():
    # Don't format or update version
    # Don't do slow stuff- discourages frequent check in
    # Run checks that are likely to have FATAL errors, not just sloppy coding.
    pass

@task(package)
def gemfury():
    """
    Upload to gemfury
    """
    # fury login
    #fury push dist/*.gz --as=YOUR_ACCT
    #fury push dist/*.whl --as=YOUR_ACCT

    cp = subprocess.run(("fury login --as={0}".format(GEM_FURY).split(" ")),
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                        shell=False, check=True)
    print(cp.stdout)

    about = {}
    with open(os.path.join(SRC, PROJECT_NAME, "__version__.py")) as f:
        exec(f.read(), about)
    version = Version(about["__version__"])
    print("Have version : " + str(version))
    print("Preparing to upload")

    if version not in get_versions():
        for kind in ["gz", "whl"]:
            try:
                files = glob.glob("{0}dist/*.{1}".format(SRC.replace(".", ""), kind))
                for file_name in files:
                    cp = subprocess.run(("fury push {0} --as={1}".format(file_name, GEM_FURY).split(" ")),
                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                        shell=False, check=True)
                    print("result of fury push")
                    for stream in [cp.stdout, cp.stderr]:
                        if stream:
                            for line in stream.decode().split("\n"):
                                print(line)

            except subprocess.CalledProcessError as cpe:
                print("result of fury push- got error")
                for stream in [cp.stdout, cp.stderr]:
                    if stream:
                        for line in stream.decode().split("\n"):
                            print(line)
                print(cpe)
                raise

@task()
def echo(*args, **kwargs):
    print(args)
    print(kwargs)

# Default task (if specified) is run when no task is specified in the command line
# make sure you define the variable __DEFAULT__ after the task is defined
# A good convention is to define it at the end of the module
# __DEFAULT__ is an optional member

__DEFAULT__ = echo


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author: Chris Ward <cward@redhat.com>

'''
'''

import datetime
import dateutil
import functools
import getpass
import logging
import os
import pytz
import re
import shutil

from babel.support import Translations
import confuse
import jinja2
from premailer import Premailer
import yaml

from comdev import __app__

try:
    import pandas as pd
except ImportError:
    log.warn('INSTALL PANDAS')

re_email = re.compile('^["\']?(([^<]*?)["\']? ?<)?([^>]+?)>?$')
re_emails = re.compile('["\']?(([^<]*?)["\']? ?<)?([^>]+)>?')

# # Configuration with confuse
def load_config(app_name, path=None):
    '''
    '''
    if path:
        config = os.path.expanduser(os.path.abspath(path))
        # CONFIG - overide default location of the config.yaml file
        os.environ['{}DIR'.format(app_name.upper())] = config
    config = confuse.Configuration(app_name)
    return config

# # JINJA2
def parse_emails(values):
    '''
    Take a string or list of strings and try to extract all the emails
    '''
    emails = []
    if isinstance(values, str):
        values = [values]
    # now we know we have a list of strings
    for value in values:
        matches = re_emails.findall(value)
        emails.extend([match[2] for match in matches])
    return emails

def parse_email(string):
    '''
    '''
    string = string.strip()
    result = re_email.match(string)
    _, name, email = result.groups()
    return email

def render_template(jinja2_env, path, params=None, inline_css=False, locale='en_US',
                    path_locale=None):
    '''
    '''
    params = params or {}
    if isinstance(jinja2_env, str) and os.path.exists(expand_path(jinja2_env)):
        jinja2_env = get_jinja2_env(jinja2_env)

    path_locale = expand_path(path_locale) if path_locale else path_locale
    if locale and path_locale:
        translations = Translations.load(path_locale, [locale])
        jinja2_env.install_gettext_translations(translations, newstyle=True)

    params['_page'] = path
    template = jinja2_env.get_template(path)
    content = template.render(**params)

    if os.path.basename(path).split('.')[-1].lower() == 'html':
        if inline_css:
            log.debug('... Inlining CSS')
            content = Premailer(
                content, remove_classes=True,
                cssutils_logging_level=logging.ERROR,
                preserve_inline_attachments=False).transform()

    return content

def ext_url(path, path_build=None, static=False, lang=None):
    if lang:
        matcher = re.search('(.*)(\.(\w\w))?\.(html)$', path)
        if matcher:
            prefix, _, cur_lang, ext = matcher.groups()
        else:
            log.error('Unable to detect language from url: {}'.format(path))
            return path

        # FIXME: this should respect GLOBAL default for default lang
        if lang == 'en':
            path = '.'.join([prefix, ext])
        else:
            path = '.'.join([prefix, lang, ext])

    if path_build:
        if path[0] == '/':
            path = path.lstrip('/')  # path.join only joins if no leading '/'
            path = os.path.join(path_build, path)
        else:
            path = os.path.join(path_build, path)
    return path

def rel_url(path, static=False, lang=None):
    path = expand_path(path)
    #if path[0] == '/' and static:
    #    path = path.lstrip('/')  # path.join only joins if no leading '/'
    return path

def get_jinja2_env(path_templates, rel_paths=False, path_build=None,
                   path_locale=None, path_static=None):
    path_templates = expand_path(path_templates)
    path_build = expand_path(path_build) if path_build else path_build

    if path_build and path_static:
        build_clean(path_build)
        static_copy(path_build, path_static)

    _loader = jinja2.FileSystemLoader(path_templates)

    # FIXME: make this a kwarg 'extensions'?
    extensions = ['jinja2.ext.i18n', 'jinja2.ext.with_']
    env = jinja2.Environment(
        loader=_loader,
        autoescape=jinja2.select_autoescape(disabled_extensions=('txt',),
                                            default_for_string=True,
                                            default=True),
        extensions=extensions)

    # add the ext to the jinja environment
    if rel_paths:
        # env.filters['url'] = rel_url
        # env.filters['static'] = functools.partial(rel_url, static=True)
        env.filters['url'] = functools.partial(ext_url, static=False)
        env.filters['static'] = functools.partial(ext_url, static=True)
    else:
        env.filters['url'] = functools.partial(ext_url, static=False,
                                               path_build=path_build)
        env.filters['static'] = functools.partial(ext_url, static=True,
                                                  path_build=path_build)
    return env

def build_clean(path_build):
    if os.path.exists(path_build):
        log.debug('Cleaning up old builds in {}'.format(path_build))
        shutil.rmtree(path_build)
    os.makedirs(path_build)

def static_copy(path_build, path_static):
    # Copy out all the static files to root of output directory
    for _path in os.listdir(path_static):
        from_path = os.path.join(path_static, _path)
        to_path = os.path.join(path_build, _path)
        if os.path.exists(to_path):
            log.warning('"static" path already exists: {}'.format(to_path))
        else:
            log.debug('Copying "static" to build dir: {}'.format(to_path))
            copy(from_path, to_path)

# # Object manipulation
def as_list(obj):
    '''
    '''
    if isinstance(obj, str):
        return [x.strip() for x in obj.split(',')]
    elif isinstance(obj, pd.Series):
        return obj.apply(lambda x: [y.strip() for y in (x or '').split(',')])
    else:
        return list(obj)

def list2str(values, separator=";"):
    if not isinstance(values, (list, tuple)):
        return values
    values = ['' if not x or x != x else x for x in values]
    separator = separator + ' '
    try:
        values_csv = separator.join(values)
    except TypeError:
        # ATTACHMENT field types have ordereddicts in them...
        values_csv = str(values)
    return values_csv

# # Datetime
def dt_normalize(dt, iso=False, local_tz=False, utc_overide=False):
    '''
    '''
    dt = pd.to_datetime(dt, utc=True).to_pydatetime()
    if local_tz:
        try:
            dt = dt.astimezone(LOCAL_TZ)
        except ValueError:
            # ValueError: NaTType does not support astimezone
            log.debug('Failed to convert {} to local timezone'.format(dt))
            return dt

    # the source date is adjusted for local time but loaded as utc...
    # need to adjust local tz (to make utc) and then reset tz to utc
    dt = dt.replace(tzinfo=LOCAL_TZ) if utc_overide else dt

    # return a standard full iso string form
    dt = dt.strftime('%Y-%m-%dT%H:%M:%S.%f%z') if iso else dt
    return dt

def today(iso=True):
    d = datetime.date.today()
    if iso:
        return d.isoformat()
    else:
        return d

def ts_now():
    return pd.to_datetime(today(False))

def now(iso=True):
    dt = datetime.datetime.now()
    if iso:
        return dt.isoformat()
    else:
        return dt

def this_month(iso=True):
    dt = today(False)
    dt = datetime.date(dt.year, dt.month, 1)
    if iso:
        return dt.strftime('%B')
    else:
        return dt

def last_month(iso=True):
    dt = this_month(False) + dateutil.relativedelta.relativedelta(months=-1)
    if iso:
        return dt.strftime('%B')
    else:
        return dt

def last_month_year(iso=True):
    dt = last_month(False)
    if iso:
        return dt.strftime('%Y')
    else:
        return dt

# # Path manipulation
def expand_path(path):
    path = path.strip()
    return os.path.abspath(os.path.expanduser(path))

def set_path(dirname, parent_path=None, makedirs=True, env_var=None):
    '''
    '''
    if parent_path:
        path = os.path.join(parent_path, dirname)
    else:
        path = dirname
    path = os.path.expanduser(path)
    if makedirs and not os.path.exists(path):
        os.makedirs(path)
    if env_var:
        os.environ[env_var] = path
    return path

def saveas(title, content, ext, subdir=None):
    path = sluggify(title, ext, subdir)
    dumps(content, path)
    return path

def sluggify(string, ext=None, subdir=None, keep_characters=(' ', '.', '-')):
    slug = "".join(c for c in string
                   if c.isalnum() or c in keep_characters).rstrip()
    slug = "-".join(slug.split())
    # append an extension to the slug (eg, file extension)
    ext = '.' + ext.lstrip('.') if ext else ''
    slug = '{}{}'.format(slug, ext) if ext else slug
    slug = re.sub('\-+', '-', slug)
    # prepend a path if provided
    if subdir:
        subdir = expand_path(subdir)
        mkdirs(subdir)
        slug = os.path.join(subdir, slug)
    return slug

# # I/O
def dumps(content, path):
    '''
    '''
    path = expand_path(path)
    log.info('Dumping content to {}'.format(path))
    # create the directory struct where the file will live first
    dest_path = os.path.dirname(path)
    if not os.path.exists(dest_path):
        os.makedirs(dest_path)
    with open(path, 'w') as out:
        out.writelines(content)

def mkdirs(path):
    path = os.path.dirname(path)
    if not os.path.exists(path):
        log.debug('Creating path: {}'.format(path))
        os.makedirs(path)
    else:
        log.debug('mkdirs failed. Path exists: {}'.format(path))

def copy(src, dst):
    if os.path.isdir(src):
        shutil.copytree(src, dst)
    else:
        shutil.copy2(src, dst)

def touch(path):
    with open(path, 'a'):
        os.utime(path)

def load_yaml(path, transpose=True):
    '''
    '''
    path = os.path.abspath(os.path.expanduser(path))
    if not os.path.exists(path):
        raise IOError('{} does not exist'.format(path))
    with open(path) as stream:
        data = yaml.load(stream, yaml.Loader)
    if transpose:
        df = pd.DataFrame(data).T.copy()
    else:
        df = pd.DataFrame(data).copy()
    return df

def dump_yaml(data, path=None):
    '''
    '''
    path = os.path.abspath(os.path.expanduser(path)) if path else None
    if path:
        if not os.path.exists(path):
            raise IOError('{} does not exist'.format(path))
        with open(path, 'w') as outfile:
            yaml.dump(data, outfile, default_flow_style=False)
    else:
        content = yaml.dump(data, default_flow_style=False)
    return content

def load_pickle(path, name=None):
    log.debug('Loading pickle at {}'.format(path))
    df = pd.read_pickle(path)
    df.name = name if name else df.name
    return df

def save_pickle(df, path):
    log.debug(' . Saving pickle file {}'.format(path))
    return df.to_pickle(path)

logging.basicConfig(format='%(message)s')
log = logging.getLogger(__app__)

USER = getpass.getuser()
UTC_TZ = pytz.timezone('utc')
# FIXME: Why LOCAL_TZ == UTC? confusing...
LOCAL_TZ = pytz.timezone('utc')
# if config['local_timezone'].exists():
#    LOCAL_TZ = pytz.timezone(config['local_timezone'].get())
# else:
#    LOCAL_TZ = pytz.timezone('utc')

if __name__ == '__main__':
    pass


# -*- coding: utf-8 -*-
"""
   zeronimo.application
   ~~~~~~~~~~~~~~~~~~~~

   A tools for application which a worker wraps.

   :copyright: (c) 2013-2017 by Heungsub Lee
   :license: BSD, see LICENSE for more details.

"""
from __future__ import absolute_import

import functools
import inspect

import six

from zeronimo.helpers import FALSE_RETURNER

__all__ = ['NULL_RPC_SPEC', 'rpc', 'rpc_spec_table', 'get_rpc_spec']

RPC_SPEC_ATTR = '__zeronimo__'

class RPCSpec(object):

    __slots__ = ('name', 'pass_call', 'reject_if')

    def __init__(self, name=None, pass_call=False, reject_if=FALSE_RETURNER):
        self.name = name
        self.pass_call = pass_call
        self.reject_if = reject_if

def register_reject_if(rpc_spec, reject_if):
    rpc_spec.reject_if = reject_if
    return reject_if

#: The :class:`RPCSpec` with default values.
NULL_RPC_SPEC = RPCSpec()

def _rpc(f, name=None, pass_call=False, reject_if=None):
    if name is None:
        name = f.__name__
    rpc_spec = RPCSpec(name, pass_call)
    setattr(f, RPC_SPEC_ATTR, rpc_spec)
    f.reject_if = functools.partial(register_reject_if, rpc_spec)
    if reject_if is not None:
        f.reject_if(reject_if)
    return f

def rpc(f=None, **kwargs):
    """Marks a method as RPC."""
    if f is not None:
        if isinstance(f, six.string_types):
            if 'name' in kwargs:
                raise ValueError('name option duplicated')
            kwargs['name'] = f
        else:
            return rpc(**kwargs)(f)
    return functools.partial(_rpc, **kwargs)

def rpc_spec_table(app):
    """Collects methods which are speced as RPC."""
    table = {}
    for attr, value in inspect.getmembers(app):
        rpc_spec = get_rpc_spec(value, default=None)
        if rpc_spec is None:
            continue
        table[rpc_spec.name] = (value, rpc_spec)
    return table

def get_rpc_spec(f, default=NULL_RPC_SPEC):
    """Gets an RPC spec from a method."""
    return getattr(f, RPC_SPEC_ATTR, default)


from functools import partial
from json import dumps
from typing import Optional

import aiohttp_cors as aiohttp_cors
from aiohttp import web
from aiohttp.web import json_response, middleware

from hyperion.util import is_uk_postcode

str_dumps = partial(dumps, default=str)
str_json_response = partial(json_response, dumps=str_dumps)

@middleware
async def normalize_postcode_middleware(request, handler):
    """
    If there is a postcode in the url it validates and normalizes it.
    """
    postcode: Optional[str] = request.match_info.get('postcode', None)

    if postcode is None or postcode == "random":
        return await handler(request)
    elif not is_uk_postcode(postcode):
        raise web.HTTPNotFound(text="Invalid Postcode")

    postcode_processed = postcode.upper().replace(" ", "")
    if postcode_processed == postcode:
        return await handler(request)
    else:
        url_name = request.match_info.route.name
        url = request.app.router[url_name]
        params = dict(request.match_info)
        params['postcode'] = postcode_processed
        raise web.HTTPMovedPermanently(str(url.url_for(**params)))

def enable_cross_origin(app):
    cors = aiohttp_cors.setup(app, defaults={
        "*": aiohttp_cors.ResourceOptions(
            allow_credentials=True,
            expose_headers="*",
            allow_headers="*",
        )
    })
    for route in list(app.router.routes()):
        cors.add(route)


# -*- coding: utf-8 -*-
"""
   zeronimo.helpers
   ~~~~~~~~~~~~~~~~

   Helper functions.

   :copyright: (c) 2013-2017 by Heungsub Lee
   :license: BSD, see LICENSE for more details.

"""
from errno import EINTR

import zmq

__all__ = ['FALSE_RETURNER', 'class_name', 'make_repr', 'socket_type_name',
           'repr_socket', 'eintr_retry', 'eintr_retry_zmq']

#: A function which always returns ``False``.  It is used for default of
#: `Worker.reject_if` and `Fanout.drop_if`.
FALSE_RETURNER = lambda *a, **k: False

def class_name(obj):
    """Returns the class name of the object."""
    return type(obj).__name__

def _repr_attr(obj, attr, data=None, reprs=None):
    val = getattr(obj, attr)
    if data is not None:
        val = data.get(attr, val)
    if reprs is None:
        repr_f = repr
    else:
        repr_f = reprs.get(attr, repr)
    return repr_f(val)

def make_repr(obj, params=None, keywords=None, data=None, name=None,
              reprs=None):
    """Generates a string of object initialization code style.  It is useful
    for custom __repr__ methods::

       class Example(object):

           def __init__(self, param, keyword=None):
               self.param = param
               self.keyword = keyword

           def __repr__(self):
               return make_repr(self, ['param'], ['keyword'])

    See the representation of example object::

       >>> Example('hello', keyword='world')
       Example('hello', keyword='world')
    """
    opts = []
    if params is not None:
        opts.append(', '.join(
            _repr_attr(obj, attr, data, reprs) for attr in params))
    if keywords is not None:
        opts.append(', '.join(
            '%s=%s' % (attr, _repr_attr(obj, attr, data, reprs))
            for attr in keywords))
    if name is None:
        name = class_name(obj)
    return '%s(%s)' % (name, ', '.join(opts))

_socket_type_names = {}
for name in ('PAIR PUB SUB REQ REP DEALER ROUTER PULL PUSH XPUB XSUB '
             'STREAM').split():
    try:
        socket_type = getattr(zmq, name)
    except AttributeError:
        continue
    assert socket_type not in _socket_type_names
    _socket_type_names[socket_type] = name

def socket_type_name(socket_type):
    """Gets the ZeroMQ socket type name."""
    return _socket_type_names[socket_type]

def repr_socket(socket):
    try:
        return '%s[%d]' % (socket_type_name(socket.type), socket.fd)
    except zmq.ZMQError as exc:
        return 'ERR[%d]' % exc.errno

def eintr_retry(exc_type, f, *args, **kwargs):
    """Calls a function.  If an error of the given exception type with
    interrupted system call (EINTR) occurs calls the function again.
    """
    while True:
        try:
            return f(*args, **kwargs)
        except exc_type as exc:
            if exc.errno != EINTR:
                raise
        else:
            break

def eintr_retry_zmq(f, *args, **kwargs):
    """The specialization of :func:`eintr_retry` by :exc:`zmq.ZMQError`."""
    return eintr_retry(zmq.ZMQError, f, *args, **kwargs)


# encoding: utf-8
# Copyright (C) 2017 John Törnblom
#
# This file is part of pyxtuml.
#
# pyxtuml is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.
#
# pyxtuml 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with pyxtuml. If not, see <http://www.gnu.org/licenses/>.
import collections
import uuid

class IdGenerator(object):
    '''
    Base class for generating unique identifiers.
    '''

    readfunc = None

    def __init__(self):
        '''
        Initialize an id generator with a start value.
        '''
        self._current = self.readfunc()

    def peek(self):
        '''
        Peek at the current value without progressing to the next one.
        '''
        return self._current

    def next(self):
        '''
        Progress to the next identifier, and return the current one.
        '''
        val = self._current
        self._current = self.readfunc()
        return val

    def __iter__(self):
        return self

    def __next__(self):
        return self.next()

class UUIDGenerator(IdGenerator):
    '''
    A uuid-based id generator. 128-bit unique numbers are generated
    randomly each time a new id is requested.
    '''
    def readfunc(self):
        return uuid.uuid4().int

class IntegerGenerator(IdGenerator):
    '''
    An integer-based id generator. Integers are generated sequentially,
    starting from the number one.

    Generally, the uuid-based id generator shall be used. In some cases such as
    testing however, having deterministic unique ids may be benifitial.

    Usage example:

    >>> l = xtuml.ModelLoader()
    >>> l.filename_input("schema.sql")
    >>> l.filename_input("data.sql")
    >>> m = l.build_metamodel(xtuml.IntegerGenerator())
    '''

    _current = 0
    def readfunc(self):
        return self._current + 1

class Visitor(object):
    '''
    A visitor may be used to visit tree nodes walked by a walker.
    '''

    def enter(self, node):
        '''
        Tries to invoke a method matching the pattern *enter_<type name>*, where
        <type name> is the name of the type of the *node*.
        '''
        name = 'enter_' + node.__class__.__name__
        fn = getattr(self, name, self.default_enter)
        fn(node)

    def leave(self, node):
        '''
        Tries to invoke a method matching the pattern *leave_<type name>*, where
        <type name> is the name of the type of the *node*.
        '''
        name = 'leave_' + node.__class__.__name__
        fn = getattr(self, name, self.default_leave)
        fn(node)

    def default_enter(self, node):
        '''
        The default behaviour when entering a *node* if no other action is
        defined by a subclass is to do nothing.
        '''
        pass

    def default_leave(self, node):
        '''
        The default behaviour when leaving a *node* if no other action is
        defined by a subclass is to do nothing.
        '''
        pass

class OrderedSet(collections.MutableSet):
    '''
    Set that remembers original insertion order.
    '''
    # Originally posted on http://code.activestate.com/recipes/576694
    # by Raymond Hettinger.

    def __init__(self, iterable=None):
        self.end = end = []
        end += [None, end, end]         # sentinel node for doubly linked list
        self.map = {}                   # key --> [key, prev, next]
        if iterable is not None:
            self |= iterable

    def add(self, key):
        if key not in self.map:
            end = self.end
            curr = end[1]
            curr[2] = end[1] = self.map[key] = [key, curr, end]

    def discard(self, key):
        if key in self.map:
            key, prev, next_ = self.map.pop(key)
            prev[2] = next_
            next_[1] = prev

    def pop(self, last=True):
        if not self:
            raise KeyError('set is empty')

        if last:
            key = self.end[1][0]
        else:
            key = self.end[2][0]

        self.discard(key)
        return key

    def __len__(self):
        return len(self.map)

    def __contains__(self, key):
        return key in self.map

    def __iter__(self):
        end = self.end
        curr = end[2]
        while curr is not end:
            yield curr[0]
            curr = curr[2]

    def __reversed__(self):
        end = self.end
        curr = end[1]
        while curr is not end:
            yield curr[0]
            curr = curr[1]

    def __repr__(self):
        if not self:
            return '%s()' % (self.__class__.__name__,)
        return '%s(%r)' % (self.__class__.__name__, list(self))

    def __eq__(self, other):
        if not isinstance(other, OrderedSet):
            return self == OrderedSet(iter(other))

        if not len(self) == len(other):
            return False

        return list(self) == list(other)

class Walker(object):
    '''
    A walker may be used to walk a tree.
    '''
    def __init__(self):
        #: A list of *visitors* to notify when visiting nodes.
        self.visitors = list()

    def accept(self, node, **kwargs):
        '''
        Invoke the visitors before and after decending down the tree.
        The walker will also try to invoke a method matching the pattern
        *accept_<type name>*, where <type name> is the name of the accepted
        *node*.
        '''
        if node is None:
            return

        for v in self.visitors:
            v.enter(node)

        name = 'accept_' + node.__class__.__name__
        fn = getattr(self, name, self.default_accept)
        r = fn(node, **kwargs)

        for v in self.visitors:
            v.leave(node)

        return r

    def default_accept(self, node, **kwargs):
        '''
        The default accept behaviour is to decend into the iterable member
        *node.children* (if available).
        '''
        if not hasattr(node, 'children'):
            return

        for child in node.children:
            self.accept(child, **kwargs)

class NodePrintVisitor(Visitor):
    '''
    A visitor that prints a tree-like structure to stdout.
    '''

    def __init__(self):
        self._lvl = 0
        self._suppressed = list()

    def default_enter(self, node):
        text = self.render(node)
        if text is None:
            self._suppressed.append(node)
        else:
            print('%s%s' % ("  " * self._lvl, text))
            self._lvl += 1

    def default_leave(self, node):
        if self._suppressed and self._suppressed[-1] == node:
            self._suppressed.pop()
        else:
            self._lvl -= 1

    def render(self, node):
        '''
        Try to invoke a method matching the pattern *render_<type name>*, where
        <type name> is the name of the rendering *node*.
        '''
        name = 'render_' + type(node).__name__
        fn = getattr(self, name, self.default_render)
        return fn(node)

    def default_render(self, node):
        '''
        The default behaviour when rendering a *node* if no other rendering
        method is defined by a subclass is to render the class name.
        '''
        return type(node).__name__


#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2017 John Törnblom
#
# This file is part of pyxtuml.
#
# pyxtuml is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.
#
# pyxtuml 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with pyxtuml. If not, see <http://www.gnu.org/licenses/>.

import sys
import xtuml

from xtuml import navigate_many as many
from xtuml import navigate_subtype as subtype

from bridgepoint import ooaofooa

class MyWalker(xtuml.Walker):
    '''
    Walk the ooaofooa packageable element tree-like structure
    '''

    def accept_S_SYS(self, inst):
        '''
        A System Model contains top-level packages
        '''
        for child in many(inst).EP_PKG[1401]():
            self.accept(child)

    def accept_PE_PE(self, inst):
        '''
        Packeable Element is the supertype of something packageable
        '''
        self.accept(subtype(inst, 8001))

    def accept_C_C(self, inst):
        '''
        A Component contains packageable elements
        '''
        for child in many(inst).PE_PE[8003]():
            self.accept(child)

    def accept_EP_PKG(self, inst):
        '''
        A Package contains packageable elements
        '''
        for child in many(inst).PE_PE[8000]():
            self.accept(child)

class MyPrinter(xtuml.NodePrintVisitor):

    def render_PE_PE(self, inst):
        '''suppress instances of PE_PE'''
        pass

    def render_O_IOBJ(self, inst):
        '''suppress imported classes'''
        pass

    def render_R_REL(self, inst):
        return 'R%d (Association)' % inst.Numb

    def default_render(self, inst):
        if hasattr(inst, 'Name'):
            return '%s (%s)' % (inst.Name, type(inst).__name__)
        else:
            return type(inst).__name__

if len(sys.argv) < 2:
    print('')
    print('  usage: %s <path to bridgepoint model folder>' % sys.argv[0])
    print('')
    sys.exit(1)

m = ooaofooa.load_metamodel(sys.argv[1:])

w = MyWalker()
w.visitors.append(MyPrinter())

for s_sys in m.select_many('S_SYS'):
    w.accept(s_sys)


import asyncio
from datetime import timedelta, datetime
from typing import List, Optional

from aiobreaker import CircuitBreakerError
from geopy import Point
from geopy.distance import vincenty, geodesic
from peewee import DoesNotExist

from hyperion import logger
from hyperion.fetch import ApiError
from hyperion.fetch.bikeregister import fetch_bikes
from hyperion.fetch.police import fetch_neighbourhood
from hyperion.fetch.postcode import fetch_postcode_from_string, fetch_postcode_random, fetch_postcodes_from_coordinates
from . import CachingError, PostCodeLike, Postcode, Neighbourhood, Bike, Location, Link

async def update_bikes(delta: Optional[timedelta] = None):
    """
    A background task that retrieves bike data.
    :param delta: The amount of time to wait between checks.
    """

    async def update(delta: timedelta):
        logger.info("Fetching bike data.")
        if await should_update_bikes(delta):
            try:
                bike_data = await fetch_bikes()
            except ApiError:
                logger.debug(f"Failed to fetch bikes.")
            except CircuitBreakerError:
                logger.debug(f"Failed to fetch bikes (circuit breaker open).")
            else:
                # save only bikes that aren't in the db
                most_recent_bike = Bike.get_most_recent_bike()
                new_bikes = (
                    Bike.from_dict(bike) for index, bike in enumerate(bike_data)
                    if index > (most_recent_bike.id if most_recent_bike is not None else -1)
                )

                counter = 0
                with Bike._meta.database.atomic():
                    for bike in new_bikes:
                        bike.save()
                        counter += 1
                logger.info(f"Saved {counter} new entries.")
        else:
            logger.info("Bike data up to date.")

    if delta is None:
        await update(timedelta(days=1000))
    else:
        while True:
            await update(delta)
            await asyncio.sleep(delta.total_seconds())

async def should_update_bikes(delta: timedelta):
    """
    Checks the most recently cached bike and returns true if
    it either doesn't exist or
    :return: Whether the cache should be updated.

    todo what if there are no bikes added for a week? ... every request will be triggered.
    """
    bike = Bike.get_most_recent_bike()
    if bike is not None:
        return bike.cached_date < datetime.now() - delta
    else:
        return True

async def get_bikes(postcode: PostCodeLike, kilometers=1) -> Optional[List[Bike]]:
    """
    Gets stolen bikes from the database within a
    certain radius (km) of a given postcode. Selects
    a square from the database and then filters out
    the corners of the square.
    :param postcode: The postcode to look up.
    :param kilometers: The radius (km) of the search.
    :return: The bikes in that radius or None if the postcode doesn't exist.
    """

    try:
        postcode_opt = await get_postcode(postcode)
    except CachingError as e:
        raise e

    if postcode_opt is None:
        return None
    else:
        postcode = postcode_opt

    # create point and distance
    center = Point(postcode.lat, postcode.long)
    distance = geodesic(kilometers=kilometers)

    # calculate edges of a square and retrieve
    lat_end = distance.destination(point=center, bearing=0).latitude
    lat_start = distance.destination(point=center, bearing=180).latitude
    long_start = distance.destination(point=center, bearing=270).longitude
    long_end = distance.destination(point=center, bearing=90).longitude

    bikes_in_area = Bike.select().where(
        lat_start <= Bike.latitude,
        Bike.latitude <= lat_end,
        long_start <= Bike.longitude,
        Bike.longitude <= long_end
    )

    # filter out items in square that aren't within the radius and return
    return [
        bike for bike in bikes_in_area
        if geodesic(Point(bike.latitude, bike.longitude), center).kilometers < kilometers
    ]

async def get_postcode_random() -> Postcode:
    """
    Gets a random postcode object..
    Acts as a middleware between us and the API, caching results.
    :return: The PostCode object else None if the postcode does not exist.
    """
    try:
        postcode = await fetch_postcode_random()
    except (ApiError, CircuitBreakerError):
        raise CachingError(f"Requested postcode is not cached, and can't be retrieved.")

    if postcode is not None:
        postcode.save()
    return postcode

async def get_postcode(postcode_like: PostCodeLike) -> Optional[Postcode]:
    """
    Gets the postcode object for a given postcode string.
    Acts as a middleware between us and the API, caching results.
    :param postcode_like: The either a string postcode or PostCode object.
    :return: The PostCode object else None if the postcode does not exist..
    :raises CachingError: When the postcode is not in cache, and the API is unreachable.
    """
    if isinstance(postcode_like, Postcode):
        return postcode_like

    postcode_like = postcode_like.replace(" ", "").upper()

    try:
        postcode = Postcode.get(Postcode.postcode == postcode_like)
    except DoesNotExist:
        try:
            postcode = await fetch_postcode_from_string(postcode_like)
        except (ApiError, CircuitBreakerError):
            raise CachingError(f"Requested postcode is not cached, and can't be retrieved.")
        if postcode is not None:
            postcode.save()

    return postcode

async def get_postcodes_from_coordinates(lat: float, long: float) -> Optional[List[Postcode]]:
    try:
        postcodes = await fetch_postcodes_from_coordinates(lat, long)
    except (ApiError, CircuitBreakerError):
        raise CachingError(f"Requested postcode is not cached, and can't be retrieved.")
    if postcodes is not None:
        for postcode in postcodes:
            postcode.save()

    return postcodes

async def get_neighbourhood(postcode_like: PostCodeLike) -> Optional[Neighbourhood]:
    """
    Gets a police neighbourhood from the database.
    Acts as a middleware between us and the API, caching results.
    :param postcode_like: The UK postcode to look up.
    :return: The Neighbourhood or None if the postcode does not exist.
    :raises CachingError: If the needed neighbourhood is not in cache, and the fetch isn't responding.

    todo save locations/links
    """
    try:
        postcode = await get_postcode(postcode_like)
    except CachingError as e:
        raise e
    else:
        if postcode is None:
            return None
        elif postcode.neighbourhood is not None:
            return postcode.neighbourhood

    try:
        data = await fetch_neighbourhood(postcode.lat, postcode.long)
    except ApiError as e:
        raise CachingError(f"Neighbourhood not in cache, and could not reach API: {e.status}")

    if data is not None:
        neighbourhood = Neighbourhood.from_dict(data)
        locations = [Location.from_dict(neighbourhood, postcode, location) for location in data["locations"]]
        links = [Link.from_dict(neighbourhood, link) for link in data["links"]]

        with Neighbourhood._meta.database.atomic():
            neighbourhood.save()
            postcode.neighbourhood = neighbourhood
            postcode.save()
            for location in locations:
                location.save()
            for link in links:
                link.save()
    else:
        neighbourhood = None
    return neighbourhood


'''
    ffi.py: wrapper around cffi.FFI

    Copyright (C) 2016 Sundar Nagarajan

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    For details of the GNU General Pulic License version 3, see the
    LICENSE.txt file that accompanied this program
'''
from cffi import FFI

class FFIExt(FFI):
    '''
    FFIExt is an extension of cffi.FFI, adding a few utility methods

    get_cdata(), get_buffer() and get_bytes() all operate on a variable
    list of arguments as a convenience.

    Otherwise, get_cdata() and get_buffer() are equivalent to
    FFI.from_buffer() and FFI.buffer() respectively

    get_bytes() is identical to get_buffer() except that outputs are
    converted to bytes
    '''
    def get_cdata(self, *args):
        '''
        all args-->_cffi_backend.buffer
        Returns-->cdata (if a SINGLE argument was provided)
                  LIST of cdata (if a args was a tuple or list)
        '''
        res = tuple([
            self.from_buffer(x) for x in args
        ])

        if len(res) == 0:
            return None
        elif len(res) == 1:
            return res[0]
        else:
            return res

    def get_buffer(self, *args):
        '''
        all args-->_cffi_backend.CDataOwn
        Must be a pointer or an array
        Returns-->buffer (if a SINGLE argument was provided)
                  LIST of buffer (if a args was a tuple or list)
        '''
        res = tuple([
            self.buffer(x) for x in args
        ])

        if len(res) == 0:
            return None
        elif len(res) == 1:
            return res[0]
        else:
            return res

    def get_bytes(self, *args):
        '''
        all args-->_cffi_backend.CDataOwn
        Must be a pointer or an array
        Returns-->bytes (if a SINGLE argument was provided)
                  LIST of bytes (if a args was a tuple or list)
        '''
        res = tuple([
            bytes(self.buffer(x)) for x in args
        ])

        if len(res) == 0:
            return None
        elif len(res) == 1:
            return res[0]
        else:
            return res

    def get_extension(self):
        return [self.verifier.get_extension()]


from zorg.driver import Driver
from PIL import Image, ImageStat

class LightSensor(Driver):
    """
    This driver makes it possible to use a camera
    as a light sensor by analyzing the lighting
    levels of the image.
    """

    def __init__(self, options, connection):
        super(LightSensor, self).__init__(options, connection)

        self.image_brightness = 0

        self.commands = [
            "get_brightness",
        ]

    def get_brightness(self):
        """
        Return the average brightness of the image.
        """
        # Only download the image if it has changed
        if not self.connection.has_changed():
            return self.image_brightness

        image_path = self.connection.download_image()

        converted_image = Image.open(image_path).convert('L')
        statistics = ImageStat.Stat(converted_image)

        self.image_brightness = statistics.mean[0]
        return self.image_brightness