# Path: py_linq/core.py
# class Key(object):
#     def __init__(self, key, **kwargs):
#         """
#         Constructor for Key class. Autogenerates key properties in object
#         given dict or kwargs
#         :param key: dict of name-values
#         :param kwargs: optional keyword arguments
#         :return: void
#         """
#         key = key if key is not None else kwargs
#         self.__dict__.update(key)
# 
#     def __repr__(self):
#         return self.__dict__.__repr__()
# 
# class OrderingDirection(object):
#     def __init__(self, key, reverse):
#         """
#         A container to hold the lambda key and sorting direction
#         :param key: lambda function
#         :param reverse: boolean. True for reverse sort
#         """
#         self.key = key
#         self.descending = reverse
# 
# class RepeatableIterable(object):
#     def __init__(self, data):
#         """
#         Constructor. Pretty straight forward except for the is_generator check. This is so we
#         can detect when a function that generates data is passed as a datasource. In this case we
#         need to exhaust the values in the function generator
#         """
#         if data is None:
#             data = []
#         if not hasattr(data, "__iter__"):
#             raise TypeError(
#                 u"RepeatableIterable must be instantiated with an iterable object"
#             )
#         is_generator = hasattr(data, "gi_running") or isinstance(data, io.TextIOBase)
#         self._data = data if not is_generator else [i for i in data]
#         self._len = None
#         self.cycle = itertools.cycle(self._data)
# 
#     def __len__(self):
#         if self._len is None:
#             self._len = sum(1 for item in self._data)
#         return self._len
# 
#     def __iter__(self):
#         i = 0
#         while i < len(self):
#             yield next(self.cycle)
#             i += 1
# 
#     def __next__(self):
#         return self.next()
# 
#     def next(self):
#         return next(self.cycle)
# 
# Path: py_linq/decorators.py
# def deprecated(reason):
#     """
#     This is a decorator which can be used to mark function as deprecated. It
#     will result in a warning being emitted when the function is used.
#     :param reason: a reason as a string
#     :return: decorated function
#     """
# 
#     def deprecated_decorator(func):
#         @functools.wraps(func)
#         def new_func(*args, **kwargs):
#             warnings.simplefilter("always", DeprecationWarning)
#             warnings.warn(
#                 u"{0} is deprecated. {1}".format(func.__name__, reason),
#                 category=DeprecationWarning,
#                 stacklevel=2,
#             )
#             return func(*args, **kwargs)
# 
#         return new_func
# 
#     return deprecated_decorator
# 
# Path: py_linq/exceptions.py
# class NoElementsError(Exception):
#     pass
# 
# class NoMatchingElement(Exception):
#     pass
# 
# class NullArgumentError(Exception):
#     pass
# 
# class MoreThanOneMatchingElement(Exception):
#     pass


import itertools
import json
import io
from queue import LifoQueue
from six import string_types
    from itertools import imap as map
    from itertools import ifilter as filter
    from itertools import izip as zip
from builtins import range
from .core import Key, OrderingDirection, RepeatableIterable
from .decorators import deprecated
from .exceptions import (
    NoElementsError,
    NoMatchingElement,
    NullArgumentError,
    MoreThanOneMatchingElement,
)
    def last_or_default(self, func=None):
        """
        Return the last element in a collection or None if the collection is empty
        :func: predicate as a lambda expression used to filter collection
        :return: data element as object or None if transformed data contains no
         elements
        """
        if func is not None:
            return self.where(func).reverse().first_or_default()
        return self.reverse().first_or_default()

    def order_by(self, key):
        """
        Returns new Enumerable sorted in ascending order by given key
        :param key: key to sort by as lambda expression
        :return: new Enumerable object
        """
        if key is None:
            raise NullArgumentError(u"No key for sorting given")
        kf = [OrderingDirection(key, reverse=False)]
        return SortedEnumerable(Enumerable(iter(self)), key_funcs=kf)

    def order_by_descending(self, key):
        """
        Returns new Enumerable sorted in descending order by given key
        :param key: key to sort by as lambda expression
        :return: new Enumerable object
        """
        if key is None:
            raise NullArgumentError(u"No key for sorting given")

# Path: py_linq/core.py
# class Key(object):
#     def __init__(self, key, **kwargs):
#         """
#         Constructor for Key class. Autogenerates key properties in object
#         given dict or kwargs
#         :param key: dict of name-values
#         :param kwargs: optional keyword arguments
#         :return: void
#         """
#         key = key if key is not None else kwargs
#         self.__dict__.update(key)
# 
#     def __repr__(self):
#         return self.__dict__.__repr__()
# 
# class OrderingDirection(object):
#     def __init__(self, key, reverse):
#         """
#         A container to hold the lambda key and sorting direction
#         :param key: lambda function
#         :param reverse: boolean. True for reverse sort
#         """
#         self.key = key
#         self.descending = reverse
# 
# class RepeatableIterable(object):
#     def __init__(self, data):
#         """
#         Constructor. Pretty straight forward except for the is_generator check. This is so we
#         can detect when a function that generates data is passed as a datasource. In this case we
#         need to exhaust the values in the function generator
#         """
#         if data is None:
#             data = []
#         if not hasattr(data, "__iter__"):
#             raise TypeError(
#                 u"RepeatableIterable must be instantiated with an iterable object"
#             )
#         is_generator = hasattr(data, "gi_running") or isinstance(data, io.TextIOBase)
#         self._data = data if not is_generator else [i for i in data]
#         self._len = None
#         self.cycle = itertools.cycle(self._data)
# 
#     def __len__(self):
#         if self._len is None:
#             self._len = sum(1 for item in self._data)
#         return self._len
# 
#     def __iter__(self):
#         i = 0
#         while i < len(self):
#             yield next(self.cycle)
#             i += 1
# 
#     def __next__(self):
#         return self.next()
# 
#     def next(self):
#         return next(self.cycle)
# 
# Path: py_linq/decorators.py
# def deprecated(reason):
#     """
#     This is a decorator which can be used to mark function as deprecated. It
#     will result in a warning being emitted when the function is used.
#     :param reason: a reason as a string
#     :return: decorated function
#     """
# 
#     def deprecated_decorator(func):
#         @functools.wraps(func)
#         def new_func(*args, **kwargs):
#             warnings.simplefilter("always", DeprecationWarning)
#             warnings.warn(
#                 u"{0} is deprecated. {1}".format(func.__name__, reason),
#                 category=DeprecationWarning,
#                 stacklevel=2,
#             )
#             return func(*args, **kwargs)
# 
#         return new_func
# 
#     return deprecated_decorator
# 
# Path: py_linq/exceptions.py
# class NoElementsError(Exception):
#     pass
# 
# class NoMatchingElement(Exception):
#     pass
# 
# class NullArgumentError(Exception):
#     pass
# 
# class MoreThanOneMatchingElement(Exception):
#     pass


# Path: py_linq/py_linq.py
import itertools
import json
import io
from queue import LifoQueue
from six import string_types
    from itertools import imap as map
    from itertools import ifilter as filter
    from itertools import izip as zip
from builtins import range
from .core import Key, OrderingDirection, RepeatableIterable
from .decorators import deprecated
from .exceptions import (
    NoElementsError,
    NoMatchingElement,
    NullArgumentError,
    MoreThanOneMatchingElement,
)

    def last_or_default(self, func=None):
        """
        Return the last element in a collection or None if the collection is empty
        :func: predicate as a lambda expression used to filter collection
        :return: data element as object or None if transformed data contains no
         elements
        """
        if func is not None:
            return self.where(func).reverse().first_or_default()
        return self.reverse().first_or_default()

    def order_by(self, key):
        """
        Returns new Enumerable sorted in ascending order by given key
        :param key: key to sort by as lambda expression
        :return: new Enumerable object
        """
        if key is None:
            raise NullArgumentError(u"No key for sorting given")
        kf = [OrderingDirection(key, reverse=False)]
        return SortedEnumerable(Enumerable(iter(self)), key_funcs=kf)

    def order_by_descending(self, key):
        """
        Returns new Enumerable sorted in descending order by given key
        :param key: key to sort by as lambda expression
        :return: new Enumerable object
        """
        if key is None:
            raise NullArgumentError(u"No key for sorting given")

        kf = [OrderingDirection(key, reverse=True)]