<commit_before>"""A library with base classes and metaclasses that let define
numeric sets."""

import inspect
import traceback

from . import helpers

# Make an AST with the generator expression
# =========================================

# Python have the ast module. This library have the ast.parse(source) funtion
# that parse the source into an AST node, where source is an string. See the
# example:
# >>> y = (x*y for (x, y) in range(2))
# To reuse the ast.parse funtion I need to extract the 'x*y' string from the
# generator object.

def _binary_left_operator(template):
    def decorator(function):
        def operator(self, other):
            result = ExpressionString()
            if hasattr(other, '_expression'):
                result._expression = template % \
                    (self._expression, other._expression)
            else:
                result._expression = template % \
                    (self._expression, repr(other))
            return result
        return operator
    return decorator

def _binary_right_operator(template):
    def decorator(function):
        def operator(self, other):
            result = ExpressionString()
            if hasattr(other, '_expression'):
                result._expression = template % \
                    (other._expression, self._expression)
            else:
                result._expression = template % \
                    (repr(other), self._expression)
            return result
        return operator
    return decorator

def _unary_operator(template):
    def decorator(function):
        def operator(self):
            result = ExpressionString()
            result._expression = template % self._expression
            return result
        return operator
    return decorator

# !!!: maybe exists an better way to implement this behaviour
class ExpressionString:
    """All magick methods make an string."""
    def __init__(self, name=None):
        if name is not None:
            self._expression = name

    def __repr__(self):
        return self._expression

    @_binary_left_operator('%s+(%s)')
    def __add__(self, other):
        pass

    @_binary_left_operator('%s&(%s)')
    def __and__(self, other):
        pass

    @_binary_left_operator('%s/(%s)')
    def __div__(self, other):
        pass

    @_binary_left_operator('%s==(%s)')
    def __eq__(self, other):
        pass

    @_binary_left_operator('%s//(%s)')
    def __floordiv__(self, other):
        pass

    @_binary_left_operator('%s>=(%s)')
    def __ge__(self, other):
        pass

    @_binary_left_operator('%s>(%s)')
    def __gt__(self, other):
        pass

    @_binary_left_operator('%s<=(%s)')
    def __le__(self, other):
        pass

    @_binary_left_operator('%s<<(%s)')
    def __lshift__(self, other):
        pass

    @_binary_left_operator('%s<(%s)')
    def __lt__(self, other):
        pass

    @_binary_left_operator('%s@(%s)')
    def __matmul__(self, other):
        pass

    @_binary_left_operator('%s%%(%s)')
    def __mod__(self, other):
        pass

    @_binary_left_operator('%s*(%s)')
    def __mul__(self, other):
        pass

    @_binary_left_operator('%s!=(%s)')
    def __ne__(self, other):
        pass

    @_binary_left_operator('%s|(%s)')
    def __or__(self, other):
        pass

    @_binary_left_operator('%s**(%s)')
    def __pow__(self, other):
        pass

    @_binary_left_operator('%s>>(%s)')
    def __rshift__(self, other):
        pass

    @_binary_left_operator('%s-(%s)')
    def __sub__(self, other):
        pass

    @_binary_left_operator('%s/(%s)')
    def __truediv__(self, other):
        pass

    @_binary_left_operator('%s^(%s)')
    def __xor__(self, other):
        pass

    # --------------------------------

    @_binary_right_operator('(%s)+%s')
    def __radd__(self, other):
        pass

    @_binary_right_operator('(%s)&%s')
    def __rand__(self, other):
        pass

    @_binary_right_operator('(%s)/%s')
    def __rdiv__(self, other):
        pass

    @_binary_right_operator('(%s)//%s')
    def __rfloordiv__(self, other):
        pass

    @_binary_right_operator('(%s)<<%s')
    def __rlshift__(self, other):
        pass

    @_binary_right_operator('(%s)@%s')
    def __rmatmul__(self, other):
        pass

    @_binary_right_operator('(%s)%%%s')
    def __rmod__(self, other):
        pass

    @_binary_right_operator('(%s)*%s')
    def __rmul__(self, other):
        pass

    @_binary_right_operator('(%s)|%s')
    def __ror__(self, other):
        pass

    @_binary_right_operator('(%s)**%s')
    def __rpow__(self, other):
        pass

    @_binary_right_operator('(%s)>>%s')
    def __rrshift__(self, other):
        pass

    @_binary_right_operator('(%s)-%s')
    def __rsub__(self, other):
        pass

    @_binary_right_operator('(%s)/%s')
    def __rtruediv__(self, other):
        pass

    @_binary_right_operator('(%s)^%s')
    def __rxor__(self, other):
        pass

    # --------------------------------

    @_unary_operator('~(%s)')
    def __invert__(self):
        pass

    @_unary_operator('-(%s)')
    def __neg__(self):
        pass

    @_unary_operator('+(%s)')
    def __pos__(self):
        pass

# Make a vector
# =============

# Supose that I wish to define a vector with the below sintax:
# >>> v = Any**3
# The upper expression mean that `v` is a 3-vector with Any numbers. The
# VectorMeta metaclass add this feature to the `Any` class through
# the `VectorMeta.__pow__()` method.

class VectorMeta(type):
    """Add `NumericType**N` API interface for to make an N-vector with
    components of `NumericType` type.
    """
    def __new__(cls, name, bases, namespace):
        return super().__new__(cls, name, bases, namespace)

    def __pow__(cls, exponent):
        """Make an N-vector of `cls` type with N = `exponent`."""
        assert type(exponent) is int
        assert exponent > 0

        return cls([0 for i in range(0, exponent)])

# Make a function with the generator expression
# =============================================

# I want to define a function that do type checking of arguments and values
# returned. All those with generator-expressions. E.g:

# >>> double = Any(2*x for x in Any)
# >>> double(4)
# 8

# How I can implement this thing?

# The argument_sender() coroutine-object will be store the values passed
# with the .send() method. I will use this values as function arguments later.
# Then yield the value like a producer.

def argument_sender():
    value = None
    while True:
        # If I not yield the value, the following line return the None value.
        # This value can cause some problems inside the generator expression.
        value = yield value
        yield value

# Second, to build the generator-expression, Python3 need that the
# expression-list in the for statement in the generator-expression have the
# .__iter__() method. This method should return an iterable. The IterableMeta
# metaclass do that.

class IterableMeta(type):
    """Make a class that is an iterable object."""
    def __new__(cls, name, bases, namespace):
        return super().__new__(cls, name, bases, namespace)

    def __iter__(cls):
        # !!!: the iter method return the argument_sender() coroutine-object
        sender = argument_sender()
        next(sender)
        return sender

# Join IterableMeta and VectorMeta
class IterableAndVectorMeta(IterableMeta, VectorMeta):
    pass

# Variables with the .__name__ property
# =====================================

class NamedObject:
    """Make an object with the __name__ property."""
    def __init__(self):
        self._name = self._get_name()

    def _get_name(self):
        """Find the name of the instance of the current class.
        Then store it in the .__name__ attribute."""
        *_, text = traceback.extract_stack()[-5]
        if text:
            name, *_ = text.split('=')
            return name.strip()

    def _get_outer_globals(self, frame):
        """Yield all global variables in the higher (calling) frames.
        """
        while frame:
            yield frame.f_globals
            frame = frame.f_back

    # NOTE: I define the __name__ property as a method because I need to store
    # the name after object creation. The @helpers.cached_property decorator
    # call the __name__ method and then transform itself into a property.
    @helpers.cached_property
    def __name__(self):
        """Find the name of the instance of the current class.
        Then store it in the .__name__ attribute."""
        # NOTE: If you use this class in the interactive IDLE shell, the
        # `helpers.get_name()` function return `None`. So, I find the name of
        # the var in the global namespace of each frame.
        if self._name is None:
            global_variables = self._get_outer_globals(inspect.currentframe())
            for glob in global_variables:
                for name, value in glob.items():
                    if value is self:
                        return name
        else:
            name = self._name
            del self._name
            return name

class CalledObject:
    def __init__(self, generator, name, args):
        self._generator = generator
        self._expression = name + repr(args).replace(',)', ')')

    def __iter__(self):
        return self._generator.__iter__

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

    def close(self):
        return self._generator.close()

    def send(self, *args, **kwds):
        return self._generator.send(*args, **kwds)

    def throw(self):
        return self._generator.throw()

    @helpers.cached_property
    def gi_running(self):
        return self._generator.gi_running

    def __repr__(self):
        return self._expression

# Third, I use the coroutine-object returned by the __iter__() method to pass
# values in each iteration of the generator expression. Such values act like
# funtions parameters. The generator-expression is an consumer.

class CallableObject(NamedObject):
    def __init__(self, generator):
        super().__init__()
        self._generator = generator
        self._send = self._generator.gi_frame.f_locals['.0'].send

    def _make_expression(self):
        """Send an ExpressionString() object to the generator. Then
        return the object with the "_expression" property."""
        # CAVEAT: everything the first var name in gi_code.co_varnames is "0.0"
        # This is the name of the iterator used in the first *for* statement
        # in the generator.
        expr_obj = self(*(ExpressionString(name)
            for name in self._generator.gi_code.co_varnames[1:]))
        return next(expr_obj)

    @helpers.cached_property
    def _expression(self):
        obj = self._make_expression()
        return obj._expression if hasattr(obj, '_expression') \
        else self.__name__

    def __call__(self, *args):
        """Simulate a function call."""
        # send arguments to the internal coroutine
        self._send(args[0]) if len(args) is 1 else self._send(args)

        # avance the generator and return their value
        return CalledObject(self._generator, self.__name__, args)

# Make a matrix
# =============

# Now supose that I wish to define a matrix with the sintax:
# >> A = Any**3*4
# The `A` object is a matrix with 3 rows and 4 columns. The
# `MatrixType.__mul__()` method set such feature to the object created
# with the `Any**3` expression. In te example `Any` is an subclass of
# `MatrixType`.

class MatrixType(CallableObject, metaclass=IterableAndVectorMeta):
    """Add `NumericType**M*N` API interface to make an MxN matrix with
    components of `NumericType`type.
    """
    def __init__(self, argument):
        if type(argument) is not list:
            super().__init__(argument)
        self._array = argument

    def __getitem__(self, arg):
        return self._array.__getitem__(arg)

    def __mul__(self, multiplicant):
        assert type(multiplicant) is int
        assert multiplicant > 0

        return MatrixType([self._array for j in range(0, multiplicant)])

# The below class is used to represent inequalities and equalities.
# Not to be confused with "inequation" and "equation".

class BinaryRelation:
    """Represents a binary relation between two quantities or
    expressions. For example: `x <= 2`."""
    def __init__(self, left, operator, right):
        self.left = left
        self.operator = operator
        self.right = right

    def __repr__(self):
        return 'BinaryRelation(%s %s %s)' % \
            (self.left, self.operator, self.right)

    def __eq__(self, other):
        if self.left == other.left \
        and self.operator == other.operator \
        and self.right == other.right:
            return True
        else:
            return False
<commit_msg>first try of refactoring of hte ExpressionString<commit_after>"""A library with base classes and metaclasses that let define
numeric sets."""

import inspect
import traceback

from . import helpers

# Make an AST with the generator expression
# =========================================

# Python have the ast module. This library have the ast.parse(source) funtion
# that parse the source into an AST node, where source is an string. See the
# example:
# >>> y = (x*y for (x, y) in range(2))
# To reuse the ast.parse funtion I need to extract the 'x*y' string from the
# generator object.

def _binary_left_operator(template):
    def decorator(function):
        def operator(self, other):
            result = ExpressionString()
            if hasattr(other, '_expression'):
                result._expression = template % \
                    (self._expression, other._expression)
            else:
                result._expression = template % \
                    (self._expression, repr(other))
            return result
        return operator
    return decorator

def _binary_right_operator(template):
    def decorator(function):
        def operator(self, other):
            result = ExpressionString()
            if hasattr(other, '_expression'):
                result._expression = template % \
                    (other._expression, self._expression)
            else:
                result._expression = template % \
                    (repr(other), self._expression)
            return result
        return operator
    return decorator

def _unary_operator(template):
    def decorator(function):
        def operator(self):
            result = ExpressionString()
            result._expression = template % self._expression
            return result
        return operator
    return decorator

# !!!: maybe exists an better way to implement this behaviour
class ExpressionString:
    """All magick methods make an string."""
    def __init__(self, name=None):
        if name is not None:
            self._expression = name

    def __repr__(self):
        return self._expression

    @_binary_left_operator('%s+(%s)')
    def __add__(self, other):
        pass

    @_binary_left_operator('%s&(%s)')
    def __and__(self, other):
        pass

    @_binary_left_operator('%s/(%s)')
    def __div__(self, other):
        pass

    @_binary_left_operator('%s==(%s)')
    def __eq__(self, other):
        pass

    @_binary_left_operator('%s//(%s)')
    def __floordiv__(self, other):
        pass

    @_binary_left_operator('%s>=(%s)')
    def __ge__(self, other):
        pass

    @_binary_left_operator('%s>(%s)')
    def __gt__(self, other):
        pass

    @_binary_left_operator('%s<=(%s)')
    def __le__(self, other):
        pass

    @_binary_left_operator('%s<<(%s)')
    def __lshift__(self, other):
        pass

    @_binary_left_operator('%s<(%s)')
    def __lt__(self, other):
        pass

    @_binary_left_operator('%s@(%s)')
    def __matmul__(self, other):
        pass

    @_binary_left_operator('%s%%(%s)')
    def __mod__(self, other):
        pass

    @_binary_left_operator('%s*(%s)')
    def __mul__(self, other):
        pass

    @_binary_left_operator('%s!=(%s)')
    def __ne__(self, other):
        pass

    @_binary_left_operator('%s|(%s)')
    def __or__(self, other):
        pass

    @_binary_left_operator('%s**(%s)')
    def __pow__(self, other):
        pass

    @_binary_left_operator('%s>>(%s)')
    def __rshift__(self, other):
        pass

    @_binary_left_operator('%s-(%s)')
    def __sub__(self, other):
        pass

    @_binary_left_operator('%s/(%s)')
    def __truediv__(self, other):
        pass

    @_binary_left_operator('%s^(%s)')
    def __xor__(self, other):
        pass

    # --------------------------------

    @_binary_right_operator('(%s)+%s')
    def __radd__(self, other):
        pass

    @_binary_right_operator('(%s)&%s')
    def __rand__(self, other):
        pass

    @_binary_right_operator('(%s)/%s')
    def __rdiv__(self, other):
        pass

    @_binary_right_operator('(%s)//%s')
    def __rfloordiv__(self, other):
        pass

    @_binary_right_operator('(%s)<<%s')
    def __rlshift__(self, other):
        pass

    @_binary_right_operator('(%s)@%s')
    def __rmatmul__(self, other):
        pass

    @_binary_right_operator('(%s)%%%s')
    def __rmod__(self, other):
        pass

    @_binary_right_operator('(%s)*%s')
    def __rmul__(self, other):
        pass

    @_binary_right_operator('(%s)|%s')
    def __ror__(self, other):
        pass

    @_binary_right_operator('(%s)**%s')
    def __rpow__(self, other):
        pass

    @_binary_right_operator('(%s)>>%s')
    def __rrshift__(self, other):
        pass

    @_binary_right_operator('(%s)-%s')
    def __rsub__(self, other):
        pass

    @_binary_right_operator('(%s)/%s')
    def __rtruediv__(self, other):
        pass

    @_binary_right_operator('(%s)^%s')
    def __rxor__(self, other):
        pass

    # --------------------------------

    @_unary_operator('~(%s)')
    def __invert__(self):
        pass

    @_unary_operator('-(%s)')
    def __neg__(self):
        pass

    @_unary_operator('+(%s)')
    def __pos__(self):
        pass

# _BINARY_LEFT_OPERATOR = [
#     ('__add__', '%s+(%s)'),
#     ('__and__', '%s&(%s)'),
#     ('__div__', '%s/(%s)'),
#     ('__eq__', '%s==(%s)'),
#     ('__floordiv__', '%s//(%s)'),
#     ('__ge__', '%s>=(%s)'),
#     ('__gt__', '%s>(%s)'),
#     ('__le__', '%s<=(%s)'),
#     ('__lshift__', '%s<<(%s)'),
#     ('__lt__', '%s<(%s)'),
#     ('__matmul__', '%s@(%s)'),
#     ('__mod__', '%s%%(%s)'),
#     ('__mul__', '%s*(%s)'),
#     ('__ne__', '%s!=(%s)'),
#     ('__or__', '%s|(%s)'),
#     ('__pow__', '%s**(%s)'),
#     ('__rshift__', '%s>>(%s)'),
#     ('__sub__', '%s-(%s)'),
#     ('__truediv__', '%s/(%s)'),
#     ('__xor__', '%s^(%s)'),
# ]

# _BINARY_RIGHT_OPERATOR = [
#     ('__radd__', '(%s)+%s'),
#     ('__rand__', '(%s)&%s'),
#     ('__rdiv__', '(%s)/%s'),
#     ('__rfloordiv__', '(%s)//%s'),
#     ('__rlshift__', '(%s)<<%s'),
#     ('__rmatmul__', '(%s)@%s'),
#     ('__rmod__', '(%s)%%%s'),
#     ('__rmul__', '(%s)*%s'),
#     ('__ror__', '(%s)|%s'),
#     ('__rpow__', '(%s)**%s'),
#     ('__rrshift__', '(%s)>>%s'),
#     ('__rsub__', '(%s)-%s'),
#     ('__rtruediv__', '(%s)/%s'),
#     ('__rxor__', '(%s)^%s'),
# ]

# _UNARY_OPERATOR = [
#     ('__invert__', '~(%s)'),
#     ('__neg__', '-(%s)'),
#     ('__pos__', '+(%s)'),
# ]

# def _binary_left_operator(function, template):
#     def operator(self, other):
#         result = ExpressionString()
#         if hasattr(other, '_expression'):
#             result._expression = template % \
#                 (self._expression, other._expression)
#         else:
#             result._expression = template % \
#                 (self._expression, repr(other))
#         return result
#     return operator

# def _binary_right_operator(function, template):
#     def operator(self, other):
#         result = ExpressionString()
#         if hasattr(other, '_expression'):
#             result._expression = template % \
#                 (other._expression, self._expression)
#         else:
#             result._expression = template % \
#                 (repr(other), self._expression)
#         return result
#     return operator

# def _unary_operator(function, template):
#     def operator(self):
#         result = ExpressionString()
#         result._expression = template % self._expression
#         return result
#     return operator

# class ExpressionMeta(type):
#     def __new__(cls, name, bases, namespace):
#         namespace.update({function: template for function, template in
#                           _BINARY_LEFT_OPERATOR})
#         namespace.update({function: template for function, template in
#                           _BINARY_RIGHT_OPERATOR})
#         namespace.update({function: template for function, template in
#                           _UNARY_OPERATOR})
#         new_cls = super().__new__(cls, name, bases, namespace)
#         # for function, template in _BINARY_LEFT_OPERATOR:
#         #     setattr(new_cls, function, _binary_left_operator(function, template))
#         # for function, template in _BINARY_RIGHT_OPERATOR:
#         #     setattr(new_cls, function, _binary_right_operator(function, template))
#         # for function, template in _UNARY_OPERATOR:
#         #     setattr(new_cls, function, _unary_operator(function, template))
#         return new_cls

# class ExpressionString(metaclass=ExpressionMeta):
#     """All magick methods make an string."""
#     def __init__(self, name=None):
#         if name is not None:
#             self._expression = name

#     def __repr__(self):
#         return self._expression

# Make a vector
# =============

# Supose that I wish to define a vector with the below sintax:
# >>> v = Any**3
# The upper expression mean that `v` is a 3-vector with Any numbers. The
# VectorMeta metaclass add this feature to the `Any` class through
# the `VectorMeta.__pow__()` method.

class VectorMeta(type):
    """Add `NumericType**N` API interface for to make an N-vector with
    components of `NumericType` type.
    """
    def __new__(cls, name, bases, namespace):
        return super().__new__(cls, name, bases, namespace)

    def __pow__(cls, exponent):
        """Make an N-vector of `cls` type with N = `exponent`."""
        assert type(exponent) is int
        assert exponent > 0

        return cls([0 for i in range(0, exponent)])

# Make a function with the generator expression
# =============================================

# I want to define a function that do type checking of arguments and values
# returned. All those with generator-expressions. E.g:

# >>> double = Any(2*x for x in Any)
# >>> double(4)
# 8

# How I can implement this thing?

# The argument_sender() coroutine-object will be store the values passed
# with the .send() method. I will use this values as function arguments later.
# Then yield the value like a producer.

def argument_sender():
    value = None
    while True:
        # If I not yield the value, the following line return the None value.
        # This value can cause some problems inside the generator expression.
        value = yield value
        yield value

# Second, to build the generator-expression, Python3 need that the
# expression-list in the for statement in the generator-expression have the
# .__iter__() method. This method should return an iterable. The IterableMeta
# metaclass do that.

class IterableMeta(type):
    """Make a class that is an iterable object."""
    def __new__(cls, name, bases, namespace):
        return super().__new__(cls, name, bases, namespace)

    def __iter__(cls):
        # !!!: the iter method return the argument_sender() coroutine-object
        sender = argument_sender()
        next(sender)
        return sender

# Join IterableMeta and VectorMeta
class IterableAndVectorMeta(IterableMeta, VectorMeta):
    pass

# Variables with the .__name__ property
# =====================================

class NamedObject:
    """Make an object with the __name__ property."""
    def __init__(self):
        self._name = self._get_name()

    def _get_name(self):
        """Find the name of the instance of the current class.
        Then store it in the .__name__ attribute."""
        *_, text = traceback.extract_stack()[-5]
        if text:
            name, *_ = text.split('=')
            return name.strip()

    def _get_outer_globals(self, frame):
        """Yield all global variables in the higher (calling) frames.
        """
        while frame:
            yield frame.f_globals
            frame = frame.f_back

    # NOTE: I define the __name__ property as a method because I need to store
    # the name after object creation. The @helpers.cached_property decorator
    # call the __name__ method and then transform itself into a property.
    @helpers.cached_property
    def __name__(self):
        """Find the name of the instance of the current class.
        Then store it in the .__name__ attribute."""
        # NOTE: If you use this class in the interactive IDLE shell, the
        # `helpers.get_name()` function return `None`. So, I find the name of
        # the var in the global namespace of each frame.
        if self._name is None:
            global_variables = self._get_outer_globals(inspect.currentframe())
            for glob in global_variables:
                for name, value in glob.items():
                    if value is self:
                        return name
        else:
            name = self._name
            del self._name
            return name

class CalledObject:
    def __init__(self, generator, name, args):
        self._generator = generator
        self._expression = name + repr(args).replace(',)', ')')

    def __iter__(self):
        return self._generator.__iter__

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

    def close(self):
        return self._generator.close()

    def send(self, *args, **kwds):
        return self._generator.send(*args, **kwds)

    def throw(self):
        return self._generator.throw()

    @helpers.cached_property
    def gi_running(self):
        return self._generator.gi_running

    def __repr__(self):
        return self._expression

# Third, I use the coroutine-object returned by the __iter__() method to pass
# values in each iteration of the generator expression. Such values act like
# funtions parameters. The generator-expression is an consumer.

class CallableObject(NamedObject):
    def __init__(self, generator):
        super().__init__()
        self._generator = generator
        self._send = self._generator.gi_frame.f_locals['.0'].send

    def _make_expression(self):
        """Send an ExpressionString() object to the generator. Then
        return the object with the "_expression" property."""
        # CAVEAT: everything the first var name in gi_code.co_varnames is "0.0"
        # This is the name of the iterator used in the first *for* statement
        # in the generator.
        expr_obj = self(*(ExpressionString(name)
            for name in self._generator.gi_code.co_varnames[1:]))
        return next(expr_obj)

    @helpers.cached_property
    def _expression(self):
        obj = self._make_expression()
        return obj._expression if hasattr(obj, '_expression') \
        else self.__name__

    def __call__(self, *args):
        """Simulate a function call."""
        # send arguments to the internal coroutine
        self._send(args[0]) if len(args) is 1 else self._send(args)

        # avance the generator and return their value
        return CalledObject(self._generator, self.__name__, args)

# Make a matrix
# =============

# Now supose that I wish to define a matrix with the sintax:
# >> A = Any**3*4
# The `A` object is a matrix with 3 rows and 4 columns. The
# `MatrixType.__mul__()` method set such feature to the object created
# with the `Any**3` expression. In te example `Any` is an subclass of
# `MatrixType`.

class MatrixType(CallableObject, metaclass=IterableAndVectorMeta):
    """Add `NumericType**M*N` API interface to make an MxN matrix with
    components of `NumericType`type.
    """
    def __init__(self, argument):
        if type(argument) is not list:
            super().__init__(argument)
        self._array = argument

    def __getitem__(self, arg):
        return self._array.__getitem__(arg)

    def __mul__(self, multiplicant):
        assert type(multiplicant) is int
        assert multiplicant > 0

        return MatrixType([self._array for j in range(0, multiplicant)])

# The below class is used to represent inequalities and equalities.
# Not to be confused with "inequation" and "equation".

class BinaryRelation:
    """Represents a binary relation between two quantities or
    expressions. For example: `x <= 2`."""
    def __init__(self, left, operator, right):
        self.left = left
        self.operator = operator
        self.right = right

    def __repr__(self):
        return 'BinaryRelation(%s %s %s)' % \
            (self.left, self.operator, self.right)

    def __eq__(self, other):
        if self.left == other.left \
        and self.operator == other.operator \
        and self.right == other.right:
            return True
        else:
            return False

class EqualOperator:
    def __add__(self, other):
        return other<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# Copyright 2012-2013 Rooter Analysis S.L.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.contrib.auth.models import User

from celery import task

from moocng.courses.models import KnowledgeQuantum
from moocng.mongodb import get_db

@task
def on_activity_created_task(activity_created, unit_activity, course_activity):
    db = get_db()
    kq = KnowledgeQuantum.objects.get(id=activity_created['kq_id'])
    kq_type = kq.kq_type()

    # KQ
    data = {
        'viewed': 1
    }
    if kq_type == 'Video':
        data['passed'] = 1
    stats_kq = db.get_collection('stats_kq')
    stats_kq.update(
        {'kq_id': activity_created['kq_id']},
        {'$inc': data},
        safe=True
    )

    # UNIT
    data = {}
    if unit_activity == 1:  # First activity of the unit
        data['started'] = 1
    elif kq.unit.knowledgequantum_set.count() == unit_activity:
        data['completed'] = 1
    # TODO passed
    if data.keys():
        stats_unit = db.get_collection('stats_unit')
        stats_unit.update(
            {'unit_id': kq.unit.id},
            {'$inc': data},
            safe=True
        )

    # COURSE
    course_kqs = KnowledgeQuantum.objects.filter(unit__course__id=activity_created['course_id']).count()
    data = {}
    if course_activity == 1:  # First activity of the course
        data['started'] = 1
    elif course_kqs == course_activity:
        data['completed'] = 1
    # TODO passed
    if data.keys():
        stats_course = db.get_collection('stats_course')
        stats_course.update(
            {'course_id': activity_created['course_id']},
            {'$inc': data},
            safe=True
        )

def update_stats(submitted, data):
    db = get_db()

    # KQ
    # TODO passed
    stats_kq = db.get_collection('stats_kq')
    stats_kq.update(
        {'kq_id': submitted['kq_id']},
        {'$inc': data},
        safe=True
    )

    # UNIT
    data = {}
    # TODO passed
    if data.keys():
        stats_unit = db.get_collection('stats_unit')
        stats_unit.update(
            {'unit_id': submitted['unit_id']},
            {'$inc': data},
            safe=True
        )

    # COURSE
    data = {}
    # TODO passed
    if data.keys():
        stats_course = db.get_collection('stats_course')
        stats_course.update(
            {'course_id': submitted['course_id']},
            {'$inc': data},
            safe=True
        )

def update_kq_mark(db, kq, user, new_mark_kq=None, new_mark_normalized_kq=None):
    from moocng.courses.marks import calculate_kq_mark
    if not new_mark_kq or not new_mark_normalized_kq:
        from moocng.courses.marks_old import calculate_kq_mark_old
        calculate_kq_mark_old(kq, user)
        new_mark_kq, new_mark_normalized_kq, use_in_total = calculate_kq_mark(kq, user)
        if not use_in_total:
            return False
    data_kq = {}
    data_kq['user_id'] = user.pk
    data_kq['course_id'] = kq.unit.course.pk
    data_kq['unit_id'] = kq.unit.pk
    data_kq['kq_id'] = kq.pk

    marks_kq = db.get_collection('marks_kq')
    mark_kq_item = marks_kq.find_one(data_kq)
    if mark_kq_item:
        updated_kq_mark = (new_mark_kq != mark_kq_item['mark'] or
                           new_mark_normalized_kq != mark_kq_item.get('relative_mark', None))
        if updated_kq_mark:
            marks_kq.update(
                data_kq,
                {'$set': {'mark': new_mark_kq,
                          'relative_mark': new_mark_normalized_kq}},
                safe=True
            )
    else:
        updated_kq_mark = True
        data_kq['mark'] = new_mark_kq
        marks_kq.insert(data_kq)
    return updated_kq_mark

def update_unit_mark(db, unit, user, new_mark_unit=None, new_mark_normalized_unit=None):
    from moocng.courses.marks import calculate_unit_mark
    if not new_mark_unit or not new_mark_normalized_unit:
        new_mark_unit, new_mark_normalized_unit, use_unit_in_total = calculate_unit_mark(unit, user)
        if not use_unit_in_total:
            return False
    data_unit = {}
    data_unit['user_id'] = user.pk
    data_unit['course_id'] = unit.course.pk
    data_unit['unit_id'] = unit.pk

    marks_unit = db.get_collection('marks_unit')
    mark_unit_item = marks_unit.find_one(data_unit)
    if mark_unit_item:
        updated_unit_mark = (new_mark_unit != mark_unit_item['mark'] or
                             new_mark_normalized_unit != mark_unit_item['relative_mark'])
        if updated_unit_mark:
            marks_unit.update(
                data_unit,
                {'$set': {'mark': new_mark_unit,
                          'relative_mark': new_mark_normalized_unit}},
                safe=True
            )
    else:
        updated_unit_mark = True
        data_unit['mark'] = new_mark_unit
        data_unit['relative_mark'] = new_mark_normalized_unit
        marks_unit.insert(data_unit)
    return updated_unit_mark

def update_course_mark(db, course, user, new_mark_course=None):
    from moocng.courses.marks import calculate_course_mark
    if not new_mark_course:
        new_mark_course, units_info = calculate_course_mark(course, user)
    data_course = {}
    data_course['user_id'] = user.pk
    data_course['course_id'] = course.pk
    marks_course = db.get_collection('marks_course')
    mark_course_item = marks_course.find_one(data_course)
    if mark_course_item:
        updated_course_mark = new_mark_course != mark_course_item['mark']
        if updated_course_mark:
            marks_course.update(
                data_course,
                {'$set': {'mark': new_mark_course}},
                safe=True
            )
    else:
        updated_course_mark = True
        data_course['mark'] = new_mark_course
        marks_course.insert(data_course)
    return updated_course_mark

def update_mark(submitted, data):
    from courses.marks import calculate_kq_mark, calculate_unit_mark, calculate_course_mark
    updated_kq_mark = updated_unit_mark = updated_course_mark = False
    kq = KnowledgeQuantum.objects.get(pk=submitted['kq_id'])
    user = User.objects.get(pk=submitted['user_id'])
    mark_kq, mark_normalized_kq, use_kq_in_total = calculate_kq_mark(kq, user)
    mark_unit, mark_normalized_unit, use_unit_in_total = calculate_unit_mark(kq.unit, user)
    mark_course, units_info = calculate_course_mark(kq.unit.course, user)
    if not use_kq_in_total:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)

    db = get_db()

    # KQ
    updated_kq_mark = update_kq_mark(db, kq, user, mark_kq, mark_normalized_kq)

    # UNIT
    if not updated_kq_mark or not use_unit_in_total:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)

    updated_unit_mark = update_unit_mark(db, kq.unit, user, mark_unit, mark_normalized_unit)

    # COURSE
    if not updated_unit_mark:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)
    updated_course_mark = update_course_mark(db, kq.unit.course, user, mark_course)
    return (updated_kq_mark, updated_unit_mark, updated_course_mark)

@task
def on_answer_created_task(answer_created):
    update_mark(answer_created, {'submitted': 1})
    update_stats(answer_created, {'submitted': 1})

@task
def on_answer_updated_task(answer_updated):
    update_stats(answer_updated, {})

@task
def on_peerreviewsubmission_created_task(submission_created):
    data = {
        'course_id': submission_created['course'],
        'unit_id': submission_created['unit'],
        'kq_id': submission_created['kq'],
    }
    update_stats(data, {'submitted': 1})

@task
def on_peerreviewreview_created_task(review_created, user_reviews):
    data = {
        'course_id': review_created['course'],
        'unit_id': review_created['unit'],
        'kq_id': review_created['kq'],
    }

    inc_reviewers = 0
    if user_reviews == 1:  # First review of this guy
        inc_reviewers = 1
    increment = {
        'reviews': 1,
        'reviewers': inc_reviewers
    }

    update_stats(data, increment)
<commit_msg>Details<commit_after># -*- coding: utf-8 -*-
# Copyright 2012-2013 Rooter Analysis S.L.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.contrib.auth.models import User

from celery import task

from moocng.courses.models import KnowledgeQuantum
from moocng.mongodb import get_db

@task
def on_activity_created_task(activity_created, unit_activity, course_activity):
    db = get_db()
    kq = KnowledgeQuantum.objects.get(id=activity_created['kq_id'])
    kq_type = kq.kq_type()

    # KQ
    data = {
        'viewed': 1
    }
    if kq_type == 'Video':
        data['passed'] = 1
    stats_kq = db.get_collection('stats_kq')
    stats_kq.update(
        {'kq_id': activity_created['kq_id']},
        {'$inc': data},
        safe=True
    )

    # UNIT
    data = {}
    if unit_activity == 1:  # First activity of the unit
        data['started'] = 1
    elif kq.unit.knowledgequantum_set.count() == unit_activity:
        data['completed'] = 1
    # TODO passed
    if data.keys():
        stats_unit = db.get_collection('stats_unit')
        stats_unit.update(
            {'unit_id': kq.unit.id},
            {'$inc': data},
            safe=True
        )

    # COURSE
    course_kqs = KnowledgeQuantum.objects.filter(unit__course__id=activity_created['course_id']).count()
    data = {}
    if course_activity == 1:  # First activity of the course
        data['started'] = 1
    elif course_kqs == course_activity:
        data['completed'] = 1
    # TODO passed
    if data.keys():
        stats_course = db.get_collection('stats_course')
        stats_course.update(
            {'course_id': activity_created['course_id']},
            {'$inc': data},
            safe=True
        )

def update_stats(submitted, data):
    db = get_db()

    # KQ
    # TODO passed
    stats_kq = db.get_collection('stats_kq')
    stats_kq.update(
        {'kq_id': submitted['kq_id']},
        {'$inc': data},
        safe=True
    )

    # UNIT
    data = {}
    # TODO passed
    if data.keys():
        stats_unit = db.get_collection('stats_unit')
        stats_unit.update(
            {'unit_id': submitted['unit_id']},
            {'$inc': data},
            safe=True
        )

    # COURSE
    data = {}
    # TODO passed
    if data.keys():
        stats_course = db.get_collection('stats_course')
        stats_course.update(
            {'course_id': submitted['course_id']},
            {'$inc': data},
            safe=True
        )

def update_kq_mark(db, kq, user, new_mark_kq=None, new_mark_normalized_kq=None):
    from moocng.courses.marks import calculate_kq_mark
    if not new_mark_kq or not new_mark_normalized_kq:
        from moocng.courses.marks_old import calculate_kq_mark_old
        calculate_kq_mark_old(kq, user)
        new_mark_kq, new_mark_normalized_kq, use_in_total = calculate_kq_mark(kq, user)
        if not use_in_total:
            return False
    data_kq = {}
    data_kq['user_id'] = user.pk
    data_kq['course_id'] = kq.unit.course.pk
    data_kq['unit_id'] = kq.unit.pk
    data_kq['kq_id'] = kq.pk

    marks_kq = db.get_collection('marks_kq')
    mark_kq_item = marks_kq.find_one(data_kq)
    if mark_kq_item:
        updated_kq_mark = (new_mark_kq != mark_kq_item['mark'] or
                           new_mark_normalized_kq != mark_kq_item['relative_mark'])
        if updated_kq_mark:
            marks_kq.update(
                data_kq,
                {'$set': {'mark': new_mark_kq,
                          'relative_mark': new_mark_normalized_kq}},
                safe=True
            )
    else:
        updated_kq_mark = True
        data_kq['mark'] = new_mark_kq
        data_kq['relative_mark'] = new_mark_normalized_kq
        marks_kq.insert(data_kq)
    return updated_kq_mark

def update_unit_mark(db, unit, user, new_mark_unit=None, new_mark_normalized_unit=None):
    from moocng.courses.marks import calculate_unit_mark
    if not new_mark_unit or not new_mark_normalized_unit:
        new_mark_unit, new_mark_normalized_unit, use_unit_in_total = calculate_unit_mark(unit, user)
        if not use_unit_in_total:
            return False
    data_unit = {}
    data_unit['user_id'] = user.pk
    data_unit['course_id'] = unit.course.pk
    data_unit['unit_id'] = unit.pk

    marks_unit = db.get_collection('marks_unit')
    mark_unit_item = marks_unit.find_one(data_unit)
    if mark_unit_item:
        updated_unit_mark = (new_mark_unit != mark_unit_item['mark'] or
                             new_mark_normalized_unit != mark_unit_item['relative_mark'])
        if updated_unit_mark:
            marks_unit.update(
                data_unit,
                {'$set': {'mark': new_mark_unit,
                          'relative_mark': new_mark_normalized_unit}},
                safe=True
            )
    else:
        updated_unit_mark = True
        data_unit['mark'] = new_mark_unit
        data_unit['relative_mark'] = new_mark_normalized_unit
        marks_unit.insert(data_unit)
    return updated_unit_mark

def update_course_mark(db, course, user, new_mark_course=None):
    from moocng.courses.marks import calculate_course_mark
    if not new_mark_course:
        new_mark_course, units_info = calculate_course_mark(course, user)
    data_course = {}
    data_course['user_id'] = user.pk
    data_course['course_id'] = course.pk
    marks_course = db.get_collection('marks_course')
    mark_course_item = marks_course.find_one(data_course)
    if mark_course_item:
        updated_course_mark = new_mark_course != mark_course_item['mark']
        if updated_course_mark:
            marks_course.update(
                data_course,
                {'$set': {'mark': new_mark_course}},
                safe=True
            )
    else:
        updated_course_mark = True
        data_course['mark'] = new_mark_course
        marks_course.insert(data_course)
    return updated_course_mark

def update_mark(submitted):
    from courses.marks import calculate_kq_mark, calculate_unit_mark, calculate_course_mark
    updated_kq_mark = updated_unit_mark = updated_course_mark = False
    kq = KnowledgeQuantum.objects.get(pk=submitted['kq_id'])
    user = User.objects.get(pk=submitted['user_id'])
    mark_kq, mark_normalized_kq, use_kq_in_total = calculate_kq_mark(kq, user)
    mark_unit, mark_normalized_unit, use_unit_in_total = calculate_unit_mark(kq.unit, user)
    mark_course, units_info = calculate_course_mark(kq.unit.course, user)
    if not use_kq_in_total:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)

    db = get_db()

    # KQ
    updated_kq_mark = update_kq_mark(db, kq, user, mark_kq, mark_normalized_kq)

    # UNIT
    if not updated_kq_mark or not use_unit_in_total:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)

    updated_unit_mark = update_unit_mark(db, kq.unit, user, mark_unit, mark_normalized_unit)

    # COURSE
    if not updated_unit_mark:
        return (updated_kq_mark, updated_unit_mark, updated_course_mark)
    updated_course_mark = update_course_mark(db, kq.unit.course, user, mark_course)
    return (updated_kq_mark, updated_unit_mark, updated_course_mark)

@task
def on_answer_created_task(answer_created):
    update_mark(answer_created)
    update_stats(answer_created, {'submitted': 1})

@task
def on_answer_updated_task(answer_updated):
    update_mark(answer_updated)
    update_stats(answer_updated, {})

@task
def on_peerreviewsubmission_created_task(submission_created):
    data = {
        'course_id': submission_created['course'],
        'unit_id': submission_created['unit'],
        'kq_id': submission_created['kq'],
    }
    update_mark(data)
    update_stats(data, {'submitted': 1})

@task
def on_peerreviewreview_created_task(review_created, user_reviews):
    data = {
        'course_id': review_created['course'],
        'unit_id': review_created['unit'],
        'kq_id': review_created['kq'],
    }

    inc_reviewers = 0
    if user_reviews == 1:  # First review of this guy
        inc_reviewers = 1
    increment = {
        'reviews': 1,
        'reviewers': inc_reviewers
    }
    update_mark(data)
    update_stats(data, increment)
<|endoftext|>


<commit_before># Created: 06.2020
# Copyright (c) 2020, Matthew Broadway
# License: MIT License
import math
from typing import Iterable, TYPE_CHECKING, Sequence

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from matplotlib.lines import Line2D
from matplotlib.patches import Circle, PathPatch
from matplotlib.path import Path
from matplotlib.textpath import TextPath

from ezdxf.addons.drawing.backend import Backend, prepare_string_for_rendering
from ezdxf.addons.drawing.properties import Properties
from ezdxf.addons.drawing.text import FontMeasurements
from ezdxf.addons.drawing.type_hints import Color
from ezdxf.math import Vector, Matrix44
from ezdxf.render import Command

if TYPE_CHECKING:
    from ezdxf.eztypes import Layout

# matplotlib docs: https://matplotlib.org/index.html

# line style:
# https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linestyle
# https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html

# line width:
# https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linewidth
# points unit (pt), 1pt = 1/72 inch, 1pt = 0.3527mm
POINTS = 1.0 / 0.3527  # mm -> points
CURVE4x3 = (Path.CURVE4, Path.CURVE4, Path.CURVE4)

class MatplotlibBackend(Backend):
    def __init__(self, ax: plt.Axes,
                 *,
                 adjust_figure: bool = True,
                 point_size: float = 2.0,
                 point_size_relative: bool = True,
                 font: FontProperties = FontProperties(),
                 ):
        super().__init__()
        self.ax = ax
        self._adjust_figure = adjust_figure

        # like set_axis_off, except that the face_color can still be set
        self.ax.xaxis.set_visible(False)
        self.ax.yaxis.set_visible(False)
        for s in self.ax.spines.values():
            s.set_visible(False)

        self.ax.autoscale(False)
        self.ax.set_aspect('equal', 'datalim')
        self._current_z = 0
        self.point_size = point_size
        self.point_size_relative = point_size_relative
        self.font = font
        self._font_measurements = _get_font_measurements(font)

    def _get_z(self) -> int:
        z = self._current_z
        self._current_z += 1
        return z

    def set_background(self, color: Color):
        self.ax.set_facecolor(color)

    def draw_line(self, start: Vector, end: Vector, properties: Properties):
        self.ax.add_line(
            Line2D(
                (start.x, end.x), (start.y, end.y),
                linewidth=properties.lineweight * POINTS,
                color=properties.color,
                zorder=self._get_z()
            ))

    def draw_path(self, path, properties: Properties):
        vertices, codes = _get_path_patch_data(path)
        patch = PathPatch(
            Path(vertices, codes),
            linewidth=properties.lineweight * POINTS,
            color=properties.color,
            fill=bool(properties.filling),
            zorder=self._get_z()
        )
        self.ax.add_patch(patch)

    def draw_point(self, pos: Vector, properties: Properties):
        color = properties.color
        if self.point_size_relative:
            self.ax.scatter([pos.x], [pos.y], s=self.point_size, c=color,
                            zorder=self._get_z())
        else:
            self.ax.add_patch(Circle((pos.x, pos.y), radius=self.point_size,
                                     facecolor=color, edgecolor=None,
                                     zorder=self._get_z()))

    def draw_filled_polygon(self, points: Iterable[Vector],
                            properties: Properties):
        self.ax.fill(*zip(*((p.x, p.y) for p in points)),
                     color=properties.color, zorder=self._get_z())

    def draw_text(self, text: str, transform: Matrix44, properties: Properties,
                  cap_height: float):
        if not text.strip():
            return  # no point rendering empty strings
        text = prepare_string_for_rendering(text, self.current_entity.dxftype())
        scale = cap_height / self._font_measurements.cap_height
        path = _text_path(text, self.font)
        transformed_path = _transform_path(path,
                                           Matrix44.scale(scale) @ transform)
        self.ax.add_patch(
            PathPatch(transformed_path, facecolor=properties.color, linewidth=0,
                      zorder=self._get_z()))

    def get_font_measurements(self, cap_height: float,
                              font: str = None) -> FontMeasurements:
        return self._font_measurements.scale_from_baseline(
            desired_cap_height=cap_height)

    def get_text_line_width(self, text: str, cap_height: float,
                            font: str = None) -> float:
        if not text.strip():
            return 0
        dxftype = self.current_entity.dxftype() if self.current_entity else 'TEXT'
        text = prepare_string_for_rendering(text, dxftype)
        path = _text_path(text, self.font)
        scale = cap_height / self._font_measurements.cap_height
        return max(x for x, y in path.vertices) * scale

    def clear(self):
        self.ax.clear()

    def finalize(self):
        super().finalize()
        self.ax.autoscale(True)
        if self._adjust_figure:
            minx, maxx = self.ax.get_xlim()
            miny, maxy = self.ax.get_ylim()
            data_width, data_height = maxx - minx, maxy - miny
            if not math.isclose(data_width, 0):
                width, height = plt.figaspect(data_height / data_width)
                self.ax.get_figure().set_size_inches(width, height,
                                                     forward=True)

def _transform_path(path: Path, transform: Matrix44) -> Path:
    vertices = transform.transform_vertices(
        [Vector(x, y) for x, y in path.vertices])
    return Path([(v.x, v.y) for v in vertices], path.codes)

def _text_path(text: str, font: FontProperties) -> TextPath:
    return TextPath((0, 0), text, size=1, prop=font)

def _get_font_measurements(
        font: FontProperties = FontProperties()) -> "FontMeasurements":
    upper_x = _text_path('X', font).vertices[:, 1].tolist()
    lower_x = _text_path('x', font).vertices[:, 1].tolist()
    lower_p = _text_path('p', font).vertices[:, 1].tolist()
    return FontMeasurements(
        baseline=min(lower_x),
        cap_top=max(upper_x),
        x_top=max(lower_x),
        bottom=min(lower_p)
    )

def _get_path_patch_data(path):
    codes = [Path.MOVETO]
    vertices = [path.start]
    for cmd in path:
        if cmd.type == Command.LINE_TO:
            codes.append(Path.LINETO)
            vertices.append(cmd.end)
        elif cmd.type == Command.CURVE_TO:
            codes.extend(CURVE4x3)
            vertices.extend((cmd.ctrl1, cmd.ctrl2, cmd.end))
        else:
            raise ValueError(f'Invalid command: {cmd.type}')
    return [(p.x, p.y) for p in vertices], codes

def qsave(layout: 'Layout', filename: str, *,
          axis: Sequence[float] = (0, 0, 1, 1),
          dpi: int = 300) -> None:
    """ Quick and simplified render export by matplotlib.

    Args:
        layout: modelspace or paperspace layout to export
        filename: export filename, file extension determines the format e.g.
            "image.png" to save in PNG format.
        axis: matplotlib axis
        dpi: image resolution

    .. versionadded:: 0.14

    """
    from .properties import RenderContext
    from .frontend import Frontend
    fig: plt.Figure = plt.figure()
    ax: plt.Axes = fig.add_axes(axis)
    ctx = RenderContext(layout.doc)
    out = MatplotlibBackend(ax)
    Frontend(ctx, out).draw_layout(layout, finalize=True)
    fig.savefig(filename, dpi=dpi)
    plt.close(fig)
<commit_msg>fix qsave() signature<commit_after># Created: 06.2020
# Copyright (c) 2020, Matthew Broadway
# License: MIT License
import math
from typing import Iterable, TYPE_CHECKING, Sequence

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from matplotlib.lines import Line2D
from matplotlib.patches import Circle, PathPatch
from matplotlib.path import Path
from matplotlib.textpath import TextPath

from ezdxf.addons.drawing.backend import Backend, prepare_string_for_rendering
from ezdxf.addons.drawing.properties import Properties
from ezdxf.addons.drawing.text import FontMeasurements
from ezdxf.addons.drawing.type_hints import Color
from ezdxf.math import Vector, Matrix44
from ezdxf.render import Command

if TYPE_CHECKING:
    from ezdxf.eztypes import Layout

# matplotlib docs: https://matplotlib.org/index.html

# line style:
# https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linestyle
# https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html

# line width:
# https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linewidth
# points unit (pt), 1pt = 1/72 inch, 1pt = 0.3527mm
POINTS = 1.0 / 0.3527  # mm -> points
CURVE4x3 = (Path.CURVE4, Path.CURVE4, Path.CURVE4)

class MatplotlibBackend(Backend):
    def __init__(self, ax: plt.Axes,
                 *,
                 adjust_figure: bool = True,
                 point_size: float = 2.0,
                 point_size_relative: bool = True,
                 font: FontProperties = FontProperties(),
                 ):
        super().__init__()
        self.ax = ax
        self._adjust_figure = adjust_figure

        # like set_axis_off, except that the face_color can still be set
        self.ax.xaxis.set_visible(False)
        self.ax.yaxis.set_visible(False)
        for s in self.ax.spines.values():
            s.set_visible(False)

        self.ax.autoscale(False)
        self.ax.set_aspect('equal', 'datalim')
        self._current_z = 0
        self.point_size = point_size
        self.point_size_relative = point_size_relative
        self.font = font
        self._font_measurements = _get_font_measurements(font)

    def _get_z(self) -> int:
        z = self._current_z
        self._current_z += 1
        return z

    def set_background(self, color: Color):
        self.ax.set_facecolor(color)

    def draw_line(self, start: Vector, end: Vector, properties: Properties):
        self.ax.add_line(
            Line2D(
                (start.x, end.x), (start.y, end.y),
                linewidth=properties.lineweight * POINTS,
                color=properties.color,
                zorder=self._get_z()
            ))

    def draw_path(self, path, properties: Properties):
        vertices, codes = _get_path_patch_data(path)
        patch = PathPatch(
            Path(vertices, codes),
            linewidth=properties.lineweight * POINTS,
            color=properties.color,
            fill=bool(properties.filling),
            zorder=self._get_z()
        )
        self.ax.add_patch(patch)

    def draw_point(self, pos: Vector, properties: Properties):
        color = properties.color
        if self.point_size_relative:
            self.ax.scatter([pos.x], [pos.y], s=self.point_size, c=color,
                            zorder=self._get_z())
        else:
            self.ax.add_patch(Circle((pos.x, pos.y), radius=self.point_size,
                                     facecolor=color, edgecolor=None,
                                     zorder=self._get_z()))

    def draw_filled_polygon(self, points: Iterable[Vector],
                            properties: Properties):
        self.ax.fill(*zip(*((p.x, p.y) for p in points)),
                     color=properties.color, zorder=self._get_z())

    def draw_text(self, text: str, transform: Matrix44, properties: Properties,
                  cap_height: float):
        if not text.strip():
            return  # no point rendering empty strings
        text = prepare_string_for_rendering(text, self.current_entity.dxftype())
        scale = cap_height / self._font_measurements.cap_height
        path = _text_path(text, self.font)
        transformed_path = _transform_path(path,
                                           Matrix44.scale(scale) @ transform)
        self.ax.add_patch(
            PathPatch(transformed_path, facecolor=properties.color, linewidth=0,
                      zorder=self._get_z()))

    def get_font_measurements(self, cap_height: float,
                              font: str = None) -> FontMeasurements:
        return self._font_measurements.scale_from_baseline(
            desired_cap_height=cap_height)

    def get_text_line_width(self, text: str, cap_height: float,
                            font: str = None) -> float:
        if not text.strip():
            return 0
        dxftype = self.current_entity.dxftype() if self.current_entity else 'TEXT'
        text = prepare_string_for_rendering(text, dxftype)
        path = _text_path(text, self.font)
        scale = cap_height / self._font_measurements.cap_height
        return max(x for x, y in path.vertices) * scale

    def clear(self):
        self.ax.clear()

    def finalize(self):
        super().finalize()
        self.ax.autoscale(True)
        if self._adjust_figure:
            minx, maxx = self.ax.get_xlim()
            miny, maxy = self.ax.get_ylim()
            data_width, data_height = maxx - minx, maxy - miny
            if not math.isclose(data_width, 0):
                width, height = plt.figaspect(data_height / data_width)
                self.ax.get_figure().set_size_inches(width, height,
                                                     forward=True)

def _transform_path(path: Path, transform: Matrix44) -> Path:
    vertices = transform.transform_vertices(
        [Vector(x, y) for x, y in path.vertices])
    return Path([(v.x, v.y) for v in vertices], path.codes)

def _text_path(text: str, font: FontProperties) -> TextPath:
    return TextPath((0, 0), text, size=1, prop=font)

def _get_font_measurements(
        font: FontProperties = FontProperties()) -> "FontMeasurements":
    upper_x = _text_path('X', font).vertices[:, 1].tolist()
    lower_x = _text_path('x', font).vertices[:, 1].tolist()
    lower_p = _text_path('p', font).vertices[:, 1].tolist()
    return FontMeasurements(
        baseline=min(lower_x),
        cap_top=max(upper_x),
        x_top=max(lower_x),
        bottom=min(lower_p)
    )

def _get_path_patch_data(path):
    codes = [Path.MOVETO]
    vertices = [path.start]
    for cmd in path:
        if cmd.type == Command.LINE_TO:
            codes.append(Path.LINETO)
            vertices.append(cmd.end)
        elif cmd.type == Command.CURVE_TO:
            codes.extend(CURVE4x3)
            vertices.extend((cmd.ctrl1, cmd.ctrl2, cmd.end))
        else:
            raise ValueError(f'Invalid command: {cmd.type}')
    return [(p.x, p.y) for p in vertices], codes

def qsave(layout: 'Layout', filename: str, *,
          rect: Sequence[float] = (0, 0, 1, 1),
          dpi: int = 300) -> None:
    """ Quick and simplified render export by matplotlib.

    Args:
        layout: modelspace or paperspace layout to export
        filename: export filename, file extension determines the format e.g.
            "image.png" to save in PNG format.
        rect: the dimensions [left, bottom, width, height] of the axes.
            All quantities are in fractions of figure width and height.
        dpi: image resolution

    .. versionadded:: 0.14

    """
    from .properties import RenderContext
    from .frontend import Frontend
    fig: plt.Figure = plt.figure()
    ax: plt.Axes = fig.add_axes(rect)
    ctx = RenderContext(layout.doc)
    out = MatplotlibBackend(ax)
    Frontend(ctx, out).draw_layout(layout, finalize=True)
    fig.savefig(filename, dpi=dpi)
    plt.close(fig)
<|endoftext|>


<commit_before># -*- coding: utf8 -*-

"""
Lambda function to manange AEM stack offline backups. It uses a SNS topic to help
orchestrate sequence of steps
"""

import os
import boto3
import logging
import json
import datetime

__author__ = 'Andy Wang (andy.wang@shinesolutions.com)'
__copyright__ = 'Shine Solutions'
__license__ = 'Apache License, Version 2.0'

# setting up logger
logger = logging.getLogger(__name__)
logger.setLevel(int(os.getenv('LOG_LEVEL', logging.INFO)))

# AWS resources
ssm = boto3.client('ssm')
ec2 = boto3.client('ec2')
s3 = boto3.client('s3')
dynamodb = boto3.client('dynamodb')

# reading in config info from either s3 or within bundle
bucket = os.getenv('S3_BUCKET')
prefix = os.getenv('S3_PREFIX')
if bucket is not None and prefix is not None:
    config_file = '/tmp/config.json'
    s3.download_file(bucket, '{}/config.json'.format(prefix), config_file)
else:
    logger.info('Unable to locate config.json in S3, searching within bundle')
    config_file = 'config.json'

with open(config_file, 'r') as f:
    content = ''.join(f.readlines()).replace('\n', '')
    logger.debug('config file: ' + content)
    config = json.loads(content)
    task_document_mapping = config['document_mapping']
    offline_snapshot_config = config['offline_snapshot']
    environment = config['environment']

class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime('%Y-%m-%dT%H:%M:%S.000Z')

        return json.JSONEncoder.default(self, obj)

def instance_ids_by_tags(filters):
    response = ec2.describe_instances(
        Filters=filters
    )
    response2 = json.loads(json.dumps(response, cls=MyEncoder))

    instance_ids = []
    for reservation in response2['Reservations']:
        instance_ids += [instance['InstanceId'] for instance in reservation['Instances']]
    return instance_ids

ssm_common_parameters = {
    'OutputS3BucketName': offline_snapshot_config['cmd-output-bucket'],
    'OutputS3KeyPrefix': offline_snapshot_config['cmd-output-prefix'],
    'ServiceRoleArn': offline_snapshot_config['ssm-service-role-arn'],
    'NotificationConfig': {
        'NotificationArn': offline_snapshot_config['sns-topic-arn'],
        'NotificationEvents': [
            'Success'
        ],
        'NotificationType': 'Command'
    }
}

def send_ssm_cmd(cmd_details):
    print('calling ssm commands')
    parameters = ssm_common_parameters.copy()
    parameters.update(cmd_details)
    return json.loads(json.dumps(ssm.send_command(**parameters),
                      cls=MyEncoder))

def manage_aem_service(target_instances, action):
    # boto3 ssm client does not accept multiple filter for Targets
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['manage-service'],
        'TimeoutSeconds': 120,
        'Comment': 'Start or Stop AEM service',
        'Parameters': {
            'action': [action]
        }
    }
    return send_ssm_cmd(details)

def offline_compaction(target_instances):
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['offline-compaction'],
        'TimeoutSeconds': 120,
        'Comment': 'run offline compaction on author instances'
    }
    return send_ssm_cmd(details)

def offline_snapshot(target_instances):
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['offline-snapshot'],
        'TimeoutSeconds': 120,
        'Comment': 'run offline ebs snapshot on targeted instances'
    }
    return send_ssm_cmd(details)

def stack_health_check(stack_prefix):
    """
    Simple AEM stack health check based on the number of author-primary,
    author-standby and publisher instances.
    """

    base_filter = [{
            'Name': 'tag:StackPrefix',
            'Values': [stack_prefix]
    }, {
        'Name': 'instance-state-name',
        'Values': ['running']
    }]

    author_primary_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['author-primary']
        }
    ]

    author_primary_instances = instance_ids_by_tags(author_primary_filter)

    author_standby_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['author-standby']
        }
    ]

    author_standby_instances = instance_ids_by_tags(author_standby_filter)

    publish_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['publish']
        }
    ]

    publish_instances = instance_ids_by_tags(publish_filter)

    if (len(author_primary_instances) == 1 and
       len(author_standby_instances) == 1 and
       len(publish_instances) >= config['offline_snapshot']['min-publish-instances']):
        return {
          'author-primary': author_primary_instances[0],
          'author-standby': author_standby_instances[0],
          'publish': publish_instances[0]
        }

    if len(author_primary_instances) != 1:
        logger.error('Found {} author-primary instances. Unhealthy stack.'.format(len(author_primary_instances)))

    if len(author_standby_instances) != 1:
        logger.error('Found {} author-standby instances. Unhealthy stack.'.format(len(author_standby_instances)))

    if len(publish_instances) < config['offline_snapshot']['min-publish-instances']:
        logger.error('Found {} publish instances. Unhealthy stack.'.format(len(publish_instances)))

    return Exception('Unhealthy Stack')

def start_offline_snapshot(instance_info):
    """
    offline snapshot is a complicated process, requiring a few things happen in the
    right order. This function will kick start the process. The bulk of operations
    will happen in another lambada function.
    """

    details = {
        'InstanceIds': [instance_info['author-standby']],
        'DocumentName': task_document_mapping["manage-service"],
        'TimeoutSeconds': 120,
        'Comment': 'kicking start offline snapshot with stopping AEM service on author-standby',
        'Parameters': {
            'action': ['stop']
        }
    }

    return send_ssm_cmd(details)

def put_state_in_dynamodb(instance_info, command_id):
    """
    schema:
    key: environment(hash) + task(range)
    cmd_id: current state
    instance_ids: map containing authour-primary, author-stand and publish
                  instance ids
    ttl: one day
    """

    # ideally set TTL attribute in CF tempalte
    # dynamodb.update_time_to_live(
    #     TableName=offline_snapshot_config['dynamodb-table'],
    #     TimeToLiveSpecification={
    #         'Enabled': True,
    #         'AttributeName': 'ttl'
    #     }
    # )

    # item ttl is set to 1 day
    ttl = (datetime.datetime.now() -
           datetime.datetime.fromtimestamp(0)).total_seconds()
    ttl += datetime.timedelta(days=1).total_seconds()

    instances = {key: {'S': value} for (key, value) in instance_info.items()}

    dynamodb.put_item(
        TableName=offline_snapshot_config['dynamodb-table'],
        Item={
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            },
            'instance_ids': {
                'M': instances
            },
            'command_state': {
                'M': {
                    command_id: {
                        'S': 'STOP_AUTHOR_STANDBY'
                    }
                }
            },
            'ttl': {
                'N': str(ttl)
            }
        }
    )

# dynamodb is used to host state information
def get_state_from_dynamodb():

    item = dynamodb.get_item(
        TableName=offline_snapshot_config['dynamodb-table'],
        Key={
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        },
        ConsistentRead=True,
        ReturnConsumedCapacity='NONE',
        ProjectionExpression='command_state, instance_ids'
    )

    return item

def update_state_in_dynamodb(command_id, current_state):

    item_update = {
        'TableName': offline_snapshot_config['dynamodb-table'],
        'Key': {
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        },
        'UpdateExpression': 'SET command_state.#cmd_id = :state',
        'ExpressionAttributeNames': {
            '#cmd_id': command_id
        },
        'ExpressionAttributeValues': {
            ':state': {
                'S': current_state
            }
        }
    }

    dynamodb.update_item(**item_update)

def delete_state_from_dynamodb():
    item_key = {
        'TableName': offline_snapshot_config['dynamodb-table'],
        'Key': {
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        }
    }
    dynamodb.delete_item(**item_key)

def sns_message_processor(event, context):

    for record in event['Records']:
        message_text = record['Sns']['Message']
        logger.debug(message_text)
        message = json.loads(message_text.replace('\'', '"'))

        # message that start offline snapshot has a task key
        response = None
        if 'task' in message and message['task'] == 'offline-snapshot':

            instance_info = stack_health_check(message['stack_prefix'])
            if instance_info is None:
                raise Exception("Unhealthy Stack")

            response = start_offline_snapshot(instance_info)
            put_state_in_dynamodb(instance_info, response['Command']['CommandId'])
            return response
        else:
            # extract important piece of information from the message
            cmd_id = message['commandId']
            item = get_state_from_dynamodb()

            state = item['Item']['command_state']['M'][cmd_id]['S']
            author_primary_id = item['Item']['instance_ids']['M']['author-primary']['S']
            author_standby_id = item['Item']['instance_ids']['M']['author-standby']['S']
            publish_id = item['Item']['instance_ids']['M']['publish']['S']

            if state == 'STOP_AUTHOR_STANDBY':
                response = manage_aem_service([author_primary_id, publish_id], 'stop')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'STOP_AUTHOR_PRIMARY')

            elif state == 'STOP_AUTHOR_PRIMARY':
                response = offline_compaction([author_primary_id, author_standby_id])

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'OFFLINE_COMPACTION')

            elif state == 'OFFLINE_COMPACTION':
                response = offline_snapshot([author_primary_id, publish_id])

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'OFFLINE_BACKUP')

            elif state == 'OFFLINE_BACKUP':
                response = manage_aem_service([author_primary_id], 'start')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'START_AUTHOR_PRIMARY')

            elif state == 'START_AUTHOR_PRIMARY':
                response = manage_aem_service([author_standby_id, publish_id], 'start')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'START_AUTHOR_STANDBY')

            elif state == 'START_AUTHOR_STANDBY':
                delete_state_from_dynamodb()
                print('done')

            else:
                raise Exception('Unknown state')

            return response
<commit_msg>add check for command failure<commit_after># -*- coding: utf8 -*-

"""
Lambda function to manage AEM stack offline backups. It uses a SNS topic to help
orchestrate sequence of steps
"""

import os
import boto3
import logging
import json
import datetime

__author__ = 'Andy Wang (andy.wang@shinesolutions.com)'
__copyright__ = 'Shine Solutions'
__license__ = 'Apache License, Version 2.0'

# setting up logger
logger = logging.getLogger(__name__)
logger.setLevel(int(os.getenv('LOG_LEVEL', logging.INFO)))

# AWS resources
ssm = boto3.client('ssm')
ec2 = boto3.client('ec2')
s3 = boto3.client('s3')
dynamodb = boto3.client('dynamodb')

# reading in config info from either s3 or within bundle
bucket = os.getenv('S3_BUCKET')
prefix = os.getenv('S3_PREFIX')
if bucket is not None and prefix is not None:
    config_file = '/tmp/config.json'
    s3.download_file(bucket, '{}/config.json'.format(prefix), config_file)
else:
    logger.info('Unable to locate config.json in S3, searching within bundle')
    config_file = 'config.json'

with open(config_file, 'r') as f:
    content = ''.join(f.readlines()).replace('\n', '')
    logger.debug('config file: ' + content)
    config = json.loads(content)
    task_document_mapping = config['document_mapping']
    offline_snapshot_config = config['offline_snapshot']
    environment = config['environment']

class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime('%Y-%m-%dT%H:%M:%S.000Z')

        return json.JSONEncoder.default(self, obj)

def instance_ids_by_tags(filters):
    response = ec2.describe_instances(
        Filters=filters
    )
    response2 = json.loads(json.dumps(response, cls=MyEncoder))

    instance_ids = []
    for reservation in response2['Reservations']:
        instance_ids += [instance['InstanceId'] for instance in reservation['Instances']]
    return instance_ids

ssm_common_parameters = {
    'OutputS3BucketName': offline_snapshot_config['cmd-output-bucket'],
    'OutputS3KeyPrefix': offline_snapshot_config['cmd-output-prefix'],
    'ServiceRoleArn': offline_snapshot_config['ssm-service-role-arn'],
    'NotificationConfig': {
        'NotificationArn': offline_snapshot_config['sns-topic-arn'],
        'NotificationEvents': [
            'Success',
            'Failed'
        ],
        'NotificationType': 'Command'
    }
}

def send_ssm_cmd(cmd_details):
    print('calling ssm commands')
    parameters = ssm_common_parameters.copy()
    parameters.update(cmd_details)
    return json.loads(json.dumps(ssm.send_command(**parameters),
                      cls=MyEncoder))

def manage_aem_service(target_instances, action):
    # boto3 ssm client does not accept multiple filter for Targets
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['manage-service'],
        'TimeoutSeconds': 120,
        'Comment': 'Start or Stop AEM service',
        'Parameters': {
            'action': [action]
        }
    }
    return send_ssm_cmd(details)

def offline_compaction(target_instances):
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['offline-compaction'],
        'TimeoutSeconds': 120,
        'Comment': 'run offline compaction on author instances'
    }
    return send_ssm_cmd(details)

def offline_snapshot(target_instances):
    details = {
        'InstanceIds': target_instances,
        'DocumentName': task_document_mapping['offline-snapshot'],
        'TimeoutSeconds': 120,
        'Comment': 'run offline ebs snapshot on targeted instances'
    }
    return send_ssm_cmd(details)

def stack_health_check(stack_prefix):
    """
    Simple AEM stack health check based on the number of author-primary,
    author-standby and publisher instances.
    """

    base_filter = [{
            'Name': 'tag:StackPrefix',
            'Values': [stack_prefix]
    }, {
        'Name': 'instance-state-name',
        'Values': ['running']
    }]

    author_primary_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['author-primary']
        }
    ]

    author_primary_instances = instance_ids_by_tags(author_primary_filter)

    author_standby_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['author-standby']
        }
    ]

    author_standby_instances = instance_ids_by_tags(author_standby_filter)

    publish_filter = base_filter + [
        {
            'Name': 'tag:Component',
            'Values': ['publish']
        }
    ]

    publish_instances = instance_ids_by_tags(publish_filter)

    if (len(author_primary_instances) == 1 and
       len(author_standby_instances) == 1 and
       len(publish_instances) >= config['offline_snapshot']['min-publish-instances']):
        return {
          'author-primary': author_primary_instances[0],
          'author-standby': author_standby_instances[0],
          'publish': publish_instances[0]
        }

    if len(author_primary_instances) != 1:
        logger.error('Found {} author-primary instances. Unhealthy stack.'.format(len(author_primary_instances)))

    if len(author_standby_instances) != 1:
        logger.error('Found {} author-standby instances. Unhealthy stack.'.format(len(author_standby_instances)))

    if len(publish_instances) < config['offline_snapshot']['min-publish-instances']:
        logger.error('Found {} publish instances. Unhealthy stack.'.format(len(publish_instances)))

    return Exception('Unhealthy Stack')

def start_offline_snapshot(instance_info):
    """
    offline snapshot is a complicated process, requiring a few things happen in the
    right order. This function will kick start the process. The bulk of operations
    will happen in another lambada function.
    """

    details = {
        'InstanceIds': [instance_info['author-standby']],
        'DocumentName': task_document_mapping["manage-service"],
        'TimeoutSeconds': 120,
        'Comment': 'kicking start offline snapshot with stopping AEM service on author-standby',
        'Parameters': {
            'action': ['stop']
        }
    }

    return send_ssm_cmd(details)

def put_state_in_dynamodb(instance_info, command_id):
    """
    schema:
    key: environment(hash) + task(range)
    cmd_id: current state
    instance_ids: map containing authour-primary, author-stand and publish
                  instance ids
    ttl: one day
    """

    # ideally set TTL attribute in CF tempalte
    # dynamodb.update_time_to_live(
    #     TableName=offline_snapshot_config['dynamodb-table'],
    #     TimeToLiveSpecification={
    #         'Enabled': True,
    #         'AttributeName': 'ttl'
    #     }
    # )

    # item ttl is set to 1 day
    ttl = (datetime.datetime.now() -
           datetime.datetime.fromtimestamp(0)).total_seconds()
    ttl += datetime.timedelta(days=1).total_seconds()

    instances = {key: {'S': value} for (key, value) in instance_info.items()}

    dynamodb.put_item(
        TableName=offline_snapshot_config['dynamodb-table'],
        Item={
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            },
            'instance_ids': {
                'M': instances
            },
            'command_state': {
                'M': {
                    command_id: {
                        'S': 'STOP_AUTHOR_STANDBY'
                    }
                }
            },
            'ttl': {
                'N': str(ttl)
            }
        }
    )

# dynamodb is used to host state information
def get_state_from_dynamodb():

    item = dynamodb.get_item(
        TableName=offline_snapshot_config['dynamodb-table'],
        Key={
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        },
        ConsistentRead=True,
        ReturnConsumedCapacity='NONE',
        ProjectionExpression='command_state, instance_ids'
    )

    return item

def update_state_in_dynamodb(command_id, current_state):

    item_update = {
        'TableName': offline_snapshot_config['dynamodb-table'],
        'Key': {
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        },
        'UpdateExpression': 'SET command_state.#cmd_id = :state',
        'ExpressionAttributeNames': {
            '#cmd_id': command_id
        },
        'ExpressionAttributeValues': {
            ':state': {
                'S': current_state
            }
        }
    }

    dynamodb.update_item(**item_update)

def delete_state_from_dynamodb():
    item_key = {
        'TableName': offline_snapshot_config['dynamodb-table'],
        'Key': {
            'environment': {
                'S': environment
            },
            'task': {
                'S': 'offline_snapshot'
            }
        }
    }
    dynamodb.delete_item(**item_key)

def sns_message_processor(event, context):

    for record in event['Records']:
        message_text = record['Sns']['Message']
        logger.debug(message_text)
        message = json.loads(message_text.replace('\'', '"'))

        # message that start offline snapshot has a task key
        response = None
        if 'task' in message and message['task'] == 'offline-snapshot':

            instance_info = stack_health_check(message['stack_prefix'])
            if instance_info is None:
                raise Exception('Unhealthy Stack')

            response = start_offline_snapshot(instance_info)
            put_state_in_dynamodb(instance_info, response['Command']['CommandId'])
            return response
        else:
            cmd_id = message['commandId']

            if message['status'] == 'Failed':
                raise Exception('Command {} failed.'.format(cmd_id))

            # get back the state of this task
            item = get_state_from_dynamodb()
            state = item['Item']['command_state']['M'][cmd_id]['S']
            author_primary_id = item['Item']['instance_ids']['M']['author-primary']['S']
            author_standby_id = item['Item']['instance_ids']['M']['author-standby']['S']
            publish_id = item['Item']['instance_ids']['M']['publish']['S']

            if state == 'STOP_AUTHOR_STANDBY':
                response = manage_aem_service([author_primary_id, publish_id], 'stop')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'STOP_AUTHOR_PRIMARY')

            elif state == 'STOP_AUTHOR_PRIMARY':
                response = offline_compaction([author_primary_id, author_standby_id])

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'OFFLINE_COMPACTION')

            elif state == 'OFFLINE_COMPACTION':
                response = offline_snapshot([author_primary_id, publish_id])

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'OFFLINE_BACKUP')

            elif state == 'OFFLINE_BACKUP':
                response = manage_aem_service([author_primary_id], 'start')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'START_AUTHOR_PRIMARY')

            elif state == 'START_AUTHOR_PRIMARY':
                response = manage_aem_service([author_standby_id, publish_id], 'start')

                update_state_in_dynamodb(response['Command']['CommandId'],
                                         'START_AUTHOR_STANDBY')

            elif state == 'START_AUTHOR_STANDBY':
                delete_state_from_dynamodb()
                print('done')

            else:
                raise Exception('Unknown state')

            return response
<|endoftext|>


<commit_before>import time
import threading

from synapse.common import *
from synapse.eventbus import EventBus

class QueueShutdown(Exception): pass

class BulkQueue(EventBus):
    '''
    A Queue like object which returns lists of items at once.
    ( to minimize round trips in remote queue retrieval )

    Example:

        q = BulkQueue()

        for x in q.get():
            stuff(x)

    Notes:

        * specify fd for a previously "hybernating" queue.

    '''
    def __init__(self, fd=None):
        EventBus.__init__(self)
        self.fd = fd
        self.last = time.time()
        self.lock = threading.Lock()

        self.items = []
        self.event = threading.Event()
        self.onfini( self._onQueFini )

    def _onQueFini(self):
        self.event.set()
        if self.fd != None:
            self.fd.close()

    def abandoned(self, dtime):
        now = time.time()
        return now > (self.last + dtime)

    def hyber(self, fd):
        '''
        Tell the queue to go into a "hybernation" state where
        items are written to fd using msgpack.

        Any subsequent call to get() will "wake" and load the queue.
        '''
        with self.lock:
            self.fd = fd
            [ fd.write(msgenpack(i)) for i in self.items ]
            self.items = None

    def _check_hyber(self, x):
        # check for hybernation
        if self.fd:
            self.fd.write( msgenpack(x) )
            return True

    def prepend(self, x):
        '''
        Prepend and item to the front of the queue.

        Example:

            q.prepend( x )

        Notes:

            * this is currently heavy and should be used judiciously...
            * if the queue is hybernating, it's the same as append

        '''
        # NOTE: this is heavy, use judiciously
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self._check_hyber(x):
                return

            self.items.insert(0,x)
            self.event.set()

    def extend(self, x):
        '''
        Bulk add a list of objects to the queue.

        Example:

            x = [ "foo", "bar", "baz" ]
            q.extend( x )

        '''
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self.fd != None:
                [ self.fd.write(msgenpack(i)) for i in x ]
                return

            self.items.extend(x)
            self.event.set()

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

    def __iter__(self):
        while True:
            items = self.get(timeout=1)
            if items == None:
                return

            for i in items:
                yield i

    def put(self, item):
        '''
        Put an item into the BulkQueue.

        Example:

            q.put( foo )

        '''
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self._check_hyber(item):
                return

            self.items.append(item)
            self.event.set()

    def get(self, timeout=None):
        '''
        Retrieve the next list of items from the BulkQueue.

        Example:

            items = q.get()
            if items == None:
                # the queue is shutdown
                return

            for item in items:
                dostuff(item)

        '''
        self.last = time.time()
        with self.lock:

            # were we hybernated?
            if self.fd:
                # ghetto for now... maybe back them better later
                self.fd.seek(0)
                byts = self.fd.read()

                unpk = msgpack.Unpacker(use_list=False, encoding='utf8')
                unpk.feed(byts)

                self.items = [ x for x in unpk ]

                self.fd.close()
                self.fd = None

            if self.items:
                return self._get_items()

            if self.isfini:
                return None

            # Clear the event so we can wait...
            self.event.clear()

        self.event.wait(timeout=timeout)
        if self.isfini:
            return None

        with self.lock:
            self.last = time.time()
            if not self.items and self.isfini:
                return None
            return self._get_items()

    def peek(self):
        return list(self.items)

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

    def _get_items(self):
        ret = self.items
        self.items = []
        return ret
<commit_msg>added basic Queue object with nicities like iter and fini<commit_after>import time
import threading
import collections

from synapse.common import *
from synapse.eventbus import EventBus

class QueueShutdown(Exception): pass

class Queue(EventBus):
    '''
    A simple custom queue to address python Queue() issues.
    '''
    def __init__(self, items=()):
        EventBus.__init__(self)
        self.deq = collections.deque()
        self.lock = threading.Lock()
        self.event = threading.Event()

        self.onfini( self._onQueFini )

    def get(self, timeout=None):
        '''
        Get the next item from the queue.

        This API will return None on timeout or fini()

        Example:

            item = q.get(timeout=30)

        '''
        while not self.isfini:

            with self.lock:
                if self.deq:
                    return self.deq.popleft()

                self.event.clear()

            self.event.wait(timeout=timeout)
            if not self.event.is_set():
                return None

    def put(self, item):
        '''
        Add an item to the queue and wake the sleeper.

        Example:

            q.put('woot')

        '''
        with self.lock:
            self.deq.append(item)
            self.event.set()

    def __iter__(self):
        return self

    def __next__(self):
        ret =  self.get()
        if ret == None:
            raise StopIteration()

        return ret

    def _onQueFini(self):
        self.event.set()

class BulkQueue(EventBus):
    '''
    A Queue like object which returns lists of items at once.
    ( to minimize round trips in remote queue retrieval )

    Example:

        q = BulkQueue()

        for x in q.get():
            stuff(x)

    Notes:

        * specify fd for a previously "hybernating" queue.

    '''
    def __init__(self, fd=None):
        EventBus.__init__(self)
        self.fd = fd
        self.last = time.time()
        self.lock = threading.Lock()

        self.items = []
        self.event = threading.Event()
        self.onfini( self._onQueFini )

    def _onQueFini(self):
        self.event.set()
        if self.fd != None:
            self.fd.close()

    def abandoned(self, dtime):
        now = time.time()
        return now > (self.last + dtime)

    def hyber(self, fd):
        '''
        Tell the queue to go into a "hybernation" state where
        items are written to fd using msgpack.

        Any subsequent call to get() will "wake" and load the queue.
        '''
        with self.lock:
            self.fd = fd
            [ fd.write(msgenpack(i)) for i in self.items ]
            self.items = None

    def _check_hyber(self, x):
        # check for hybernation
        if self.fd:
            self.fd.write( msgenpack(x) )
            return True

    def prepend(self, x):
        '''
        Prepend and item to the front of the queue.

        Example:

            q.prepend( x )

        Notes:

            * this is currently heavy and should be used judiciously...
            * if the queue is hybernating, it's the same as append

        '''
        # NOTE: this is heavy, use judiciously
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self._check_hyber(x):
                return

            self.items.insert(0,x)
            self.event.set()

    def extend(self, x):
        '''
        Bulk add a list of objects to the queue.

        Example:

            x = [ "foo", "bar", "baz" ]
            q.extend( x )

        '''
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self.fd != None:
                [ self.fd.write(msgenpack(i)) for i in x ]
                return

            self.items.extend(x)
            self.event.set()

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

    def __iter__(self):
        while True:
            items = self.get(timeout=1)
            if items == None:
                return

            for i in items:
                yield i

    def put(self, item):
        '''
        Put an item into the BulkQueue.

        Example:

            q.put( foo )

        '''
        if self.isfini:
            raise QueueShutdown()

        with self.lock:
            if self._check_hyber(item):
                return

            self.items.append(item)
            self.event.set()

    def get(self, timeout=None):
        '''
        Retrieve the next list of items from the BulkQueue.

        Example:

            items = q.get()
            if items == None:
                # the queue is shutdown
                return

            for item in items:
                dostuff(item)

        '''
        self.last = time.time()
        with self.lock:

            # were we hybernated?
            if self.fd:
                # ghetto for now... maybe back them better later
                self.fd.seek(0)
                byts = self.fd.read()

                unpk = msgpack.Unpacker(use_list=False, encoding='utf8')
                unpk.feed(byts)

                self.items = [ x for x in unpk ]

                self.fd.close()
                self.fd = None

            if self.items:
                return self._get_items()

            if self.isfini:
                return None

            # Clear the event so we can wait...
            self.event.clear()

        self.event.wait(timeout=timeout)
        if self.isfini:
            return None

        with self.lock:
            self.last = time.time()
            if not self.items and self.isfini:
                return None
            return self._get_items()

    def peek(self):
        return list(self.items)

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

    def _get_items(self):
        ret = self.items
        self.items = []
        return ret
<|endoftext|>