import re

from capybara.compat import cmp
from capybara.helpers import declension, desc, failure_message
from capybara.utils import cached_property

class Result(object):
    """
    A :class:`Result` represents a collection of :class:`Element` objects on the page. It is
    possible to interact with this collection similar to a List because it implements
    ``__getitem__`` and offers the following container methods through delegation:

    * ``__len__``
    * ``__nonzero__`` (Python 2)

    Args:
        elements (List[Element]): The initial list of elements found by the query.
        query (SelectorQuery): The query used to find elements.
    """

    def __init__(self, elements, query):
        self._elements = elements

        self._result_cache = []
        self._result_iter = (node for node in elements
                             if query.matches_filters(node))

        self.query = query

    def __getitem__(self, key):
        max_index = key.stop if isinstance(key, slice) else key

        if max_index is None or max_index < 0:
            return self._full_results[key]
        else:
            self._cache_at_least(max_index + 1)
            return self._result_cache[key]

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

    def __nonzero__(self):
        return self._cache_at_least(1)

    def __iter__(self):
        for node in self._result_cache:
            yield node
        for node in self._result_iter:
            self._result_cache.append(node)
            yield node

    @property
    def compare_count(self):
        """
        Returns how the result count compares to the query options.

        The return value is negative if too few results were found, zero if enough were found, and
        positive if too many were found.

        Returns:
            int: -1, 0, or 1.
        """

        if self.query.options["count"] is not None:
            count_opt = int(self.query.options["count"])
            self._cache_at_least(count_opt + 1)
            return cmp(len(self._result_cache), count_opt)

        if self.query.options["minimum"] is not None:
            min_opt = int(self.query.options["minimum"])
            if not self._cache_at_least(min_opt):
                return -1

        if self.query.options["maximum"] is not None:
            max_opt = int(self.query.options["maximum"])
            if self._cache_at_least(max_opt + 1):
                return 1

        if self.query.options["between"] is not None:
            between = self.query.options["between"]
            min_opt, max_opt = between[0], between[-1]
            if not self._cache_at_least(min_opt):
                return -1
            if self._cache_at_least(max_opt + 1):
                return 1
            return 0

        return 0

    @property
    def matches_count(self):
        """ bool: Whether the result count matches the query options. """
        return self.compare_count == 0

    @property
    def failure_message(self):
        """ str: A message describing the query failure. """

        message = failure_message(self.query.description, self.query.options)

        if len(self) > 0:
            message += ", found {count} {matches}: {results}".format(
                count=len(self),
                matches=declension("match", "matches", len(self)),
                results=", ".join([desc(node.text) for node in self]))
        else:
            message += " but there were no matches"

        if self._rest:
            elements = ", ".join([desc(element.text) for element in self._rest])
            message += (". Also found {}, which matched the selector"
                        " but not all filters.".format(elements))

        return message

    @property
    def negative_failure_message(self):
        return re.sub(r"(to find)", r"not \1", self.failure_message)

    def _cache_at_least(self, size):
        """
        Attempts to fill the result cache with at least the given number of results.

        Returns:
            bool: Whether the cache contains at least the given size.
        """

        try:
            while len(self._result_cache) < size:
                self._result_cache.append(next(self._result_iter))
            return True
        except StopIteration:
            return False

    @cached_property
    def _full_results(self):
        return list(iter(self))

    @cached_property
    def _rest(self):
        return list(set(self._elements) - set(self._full_results))


import re
import time

from capybara.compat import PY2, str_
from capybara.utils import decode_bytes, isbytes, isregex, isstring, encode_string

def declension(singular, plural, count):
    """
    Returns the appropriate word variation for the given quantity.

    Args:
        singular (str): The singular variation.
        plural (str): The plural variation.
        count (int): The count.

    Returns:
        str: The appropriate variation for the quantity.
    """

    return singular if count == 1 else plural

def desc(value):
    """ str: A normalized representation for a user-provided value. """

    def normalize_strings(value):
        if isinstance(value, list):
            value = [normalize_strings(e) for e in value]

        if isinstance(value, dict):
            value = {normalize_strings(k): normalize_strings(v) for k, v in iter(value.items())}

        if isregex(value):
            value = value.pattern

        if isbytes(value):
            value = decode_bytes(value)

        if PY2:
            if isstring(value):
                # In Python 2, strings (``unicode`` objects) represent as ``u'...'``, so ensure
                # the string is encoded (as a ``str`` object) for cleaner representation.
                value = encode_string(value)

        return value

    value = normalize_strings(value)

    return repr(value)

def expects_none(options):
    """
    Returns whether the given query options expect a possible count of zero.

    Args:
        options (Dict[str, int | Iterable[int]]): A dictionary of query options.

    Returns:
        bool: Whether a possible count of zero is expected.
    """

    if any(options.get(key) is not None for key in ["count", "maximum", "minimum", "between"]):
        return matches_count(0, options)
    else:
        return False

def failure_message(description, options):
    """
    Returns a expectation failure message for the given query description.

    Args:
        description (str): A description of the failed query.
        options (Dict[str, Any]): The query options.

    Returns:
        str: A message describing the failure.
    """

    message = "expected to find {}".format(description)

    if options["count"] is not None:
        message += " {count} {times}".format(
            count=options["count"],
            times=declension("time", "times", options["count"]))
    elif options["between"] is not None:
        between = options["between"]
        if between:
            first, last = between[0], between[-1]
        else:
            first, last = None, None

        message += " between {first} and {last} times".format(
            first=first,
            last=last)
    elif options["maximum"] is not None:
        message += " at most {maximum} {times}".format(
            maximum=options["maximum"],
            times=declension("time", "times", options["maximum"]))
    elif options["minimum"] is not None:
        message += " at least {minimum} {times}".format(
            minimum=options["minimum"],
            times=declension("time", "times", options["minimum"]))

    return message

def matches_count(count, options):
    """
    Returns whether the given count matches the given query options.

    If no quantity options are specified, any count is considered acceptable.

    Args:
        count (int): The count to be validated.
        options (Dict[str, int | Iterable[int]]): A dictionary of query options.

    Returns:
        bool: Whether the count matches the options.
    """

    if options.get("count") is not None:
        return count == int(options["count"])
    if options.get("maximum") is not None and int(options["maximum"]) < count:
        return False
    if options.get("minimum") is not None and int(options["minimum"]) > count:
        return False
    if options.get("between") is not None and count not in options["between"]:
        return False
    return True

def monotonic():
    """ float: Returns the current time, using a monotonic clock where available. """
    if hasattr(time, "monotonic"):
        return time.monotonic()
    else:
        return time.time()

def normalize_text(value):
    """
    Normalizes the given value to a string of text with extra whitespace removed.

    Byte sequences are decoded. ``None`` is converted to an empty string. Everything else
    is simply cast to a string.

    Args:
        value (Any): The data to normalize.

    Returns:
        str: The normalized text.
    """

    if value is None:
        return ""

    text = decode_bytes(value) if isbytes(value) else str_(value)

    return normalize_whitespace(text)

def normalize_whitespace(text):
    """
    Returns the given text with outer whitespace removed and inner whitespace collapsed.

    Args:
        text (str): The text to normalize.

    Returns:
        str: The normalized text.
    """

    return re.sub(r"\s+", " ", text, flags=re.UNICODE).strip()

class Timer:
    """
    Args:
        expire_in (int): The number of seconds from now in which this timer should expire.
    """

    def __init__(self, expire_in):
        self._start = monotonic()
        self._expire_in = expire_in

    @property
    def expired(self):
        """ bool: Whether this timer has expired. """
        return monotonic() - self._start >= self._expire_in

    @property
    def stalled(self):
        """ bool: Whether this timer appears to have stalled. """
        return monotonic() == self._start

def toregex(text, exact=False):
    """
    Returns a compiled regular expression for the given text.

    Args:
        text (str | RegexObject): The text to match.
        exact (bool, optional): Whether the generated regular expression should match exact
            strings. Defaults to False.

    Returns:
        RegexObject: A compiled regular expression that will match the text.
    """

    if isregex(text):
        return text

    escaped = re.escape(normalize_text(text))
    if exact:
        escaped = r"\A{}\Z".format(escaped)

    return re.compile(escaped)


from capybara.compat import urlparse
from capybara.helpers import desc
from capybara.utils import isregex, normalize_url

class CurrentPathQuery(object):
    """
    Queries the current path.

    Args:
        expected_path (str | RegexObject): The expected path.
        url (bool, optional): Whether the complete URL should match. Defaults to False.
        only_path (bool, optional): Whether only the path (excluding the query string) should
            match. Defaults to False.
    """

    def __init__(self, expected_path, url=False, only_path=False):
        if url and only_path:
            raise RuntimeError("the url and only_path arguments cannot both be true")

        self.expected_path = expected_path
        self.actual_path = None
        self.url = url
        self.only_path = only_path

    def resolves_for(self, session):
        """
        Returns whether this query resolves for the given session.

        Args:
            session (Session): The session for which this query should be executed.

        Returns:
            bool: Whether this query resolves.
        """

        if self.url:
            self.actual_path = session.current_url
        else:
            result = urlparse(session.current_url)

            if self.only_path:
                self.actual_path = result.path
            else:
                request_uri = result.path
                if result.query:
                    request_uri += "?{0}".format(result.query)

                self.actual_path = request_uri

        if isregex(self.expected_path):
            return self.expected_path.search(self.actual_path)
        else:
            return normalize_url(self.actual_path) == normalize_url(self.expected_path)

    @property
    def failure_message(self):
        """ str: A description of this query's failure. """
        return self._build_message()

    @property
    def negative_failure_message(self):
        """ str: A description of this query's negative failure. """
        return self._build_message(" not")

    def _build_message(self, negated=""):
        verb = "match" if isregex(self.expected_path) else "equal"

        return "expected {actual}{negated} to {verb} {expected}".format(
            actual=desc(self.actual_path),
            negated=negated,
            verb=verb,
            expected=desc(self.expected_path))


class Window(object):
    """
    The Window class represents a browser window.

    You can get an instance of the class by calling either of:

    * :attr:`windows`
    * :attr:`current_window`
    * :meth:`window_opened_by`
    * :meth:`switch_to_window`

    Note that some drivers (e.g. Selenium) support getting size of/resizing/closing only current
    window. So if you invoke such method for:

    * window that is current, Capybara will make 2 Selenium method invocations (get handle of
      current window + get size/resize/close).
    * window that is not current, Capybara will make 4 Selenium method invocations (get handle of
      current window + switch to given handle + get size/resize/close + switch to original handle)

    Args:
        session (Session): Session that this window belongs to.
        handle (object): An object that uniquely identifies the window within the session.
    """

    def __init__(self, session, handle):
        self.session = session
        self.driver = session.driver
        self.handle = handle
        self._contexts = []

    def __repr__(self):
        return "<capybara.window.Window handle={}>".format(repr(self.handle))

    def __eq__(self, other):
        return (
            type(self) == type(other) and
            self.session == other.session and
            self.handle == other.handle)

    def __hash__(self):
        return hash(self.session) ^ hash(self.handle)

    def __enter__(self):
        context = self.session.window(self)
        self._contexts.append(context)
        return context.__enter__()

    def __exit__(self, *args):
        context = self._contexts.pop()
        context.__exit__(*args)

    @property
    def closed(self):
        """ bool: Whether the window is closed. """
        return not self.exists

    @property
    def current(self):
        """ bool: Whether this window is the window in which commands are being executed. """
        try:
            return self.driver.current_window_handle == self.handle
        except self.driver.no_such_window_error:
            return False

    @property
    def exists(self):
        """ bool: Whether the window is not closed. """
        return self.handle in self.driver.window_handles

    @property
    def size(self):
        """ List[int, int]: The window size. """
        return self.driver.window_size(self.handle)

    def close(self):
        """
        Close window.

        If this method was called for the window that is current, then after calling this method
        future invocations of other Capybara methods should raise
        ``session.driver.no_such_window_error`` until another window is switched to.

        If this method was called for a window that is not current, then after calling this method
        the current window should remain the same as it was before calling this method.
        """

        self.driver.close_window(self.handle)

    def maximize(self):
        """
        Maximize the window.

        If this method was called for a window that is not current, then after calling this method
        the current window should remain the same as it was before calling this method.
        """

        self.driver.maximize_window(self.handle)

    def fullscreen(self):
        """
        Fullscreen the window.
        """

        self.driver.fullscreen_window(self.handle)

    def resize_to(self, width, height):
        """
        Resizes the window to the given dimensions.

        If this method was called for a window that is not current, then after calling this method
        the current window should remain the same as it was before calling this method.

        Args:
            width (int): The new window width in pixels.
            height (int): The new window height in pixels.
        """

        self.driver.resize_window_to(self.handle, width, height)


from contextlib import closing
from threading import Thread
from time import sleep

import capybara
from capybara.compat import URLError, urlopen
from capybara.helpers import Timer
from capybara.utils import (
    Counter,
    cached_property,
    decode_bytes,
    encode_string,
    find_available_port)

class Server(object):
    """
    Serves a WSGI-compliant app for Capybara to test.

    Args:
        app (object): The WSGI-compliant app to serve.
        port (int, optional): The port on which the server should be available.
            Defaults to :data:`capybara.server_port`, or the last port used by the
            given app, or a random available port.
        host (str, optional): The host on which the server should be available.
            Defaults to :data:`capybara.server_host`.
    """

    _ports = {}

    def __init__(self, app, port=None, host=None):
        self.app = app

        self.host = (
            host or
            capybara.server_host)

        self.port = (
            port or
            capybara.server_port or
            type(self)._ports.get(self.port_key, None) or
            find_available_port())

        self.server_thread = None

    @property
    def error(self):
        return self.middleware.error

    def reset_error(self):
        self.middleware.error = None

    @property
    def port_key(self):
        return str(id(self.app))

    @cached_property
    def middleware(self):
        return Middleware(self.app)

    def wait_for_pending_requests(self):
        timer = Timer(60)
        while self.has_pending_requests:
            if timer.expired:
                raise RuntimeError("Requests did not finish in 60 seconds")
            sleep(0.01)

    def boot(self):
        """
        Boots a server for the app, if it isn't already booted.

        Returns:
            Server: This server.
        """

        if not self.responsive:
            # Remember the port so we can reuse it if we try to serve this same app again.
            type(self)._ports[self.port_key] = self.port

            init_func = capybara.servers[capybara.server_name]
            init_args = (self.middleware, self.port, self.host)

            self.server_thread = Thread(target=init_func, args=init_args)

            # Inform Python that it shouldn't wait for this thread to terminate before
            # exiting. (It will still be appropriately terminated when the process exits.)
            self.server_thread.daemon = True

            self.server_thread.start()

            # Make sure the server actually starts and becomes responsive.
            timer = Timer(60)
            while not self.responsive:
                if timer.expired:
                    raise RuntimeError("WSGI application timed out during boot")
                self.server_thread.join(0.1)

        return self

    @property
    def responsive(self):
        """ bool: Whether the server for this app is up and responsive. """

        if self.server_thread and self.server_thread.join(0):
            return False

        try:
            # Try to fetch the endpoint added by the middleware.
            identify_url = "http://{0}:{1}/__identify__".format(self.host, self.port)

            with closing(urlopen(identify_url)) as response:
                body, status_code = response.read(), response.getcode()

                if str(status_code)[0] == "2" or str(status_code)[0] == "3":
                    return decode_bytes(body) == str(id(self.app))
        except URLError:
            pass

        return False

    @property
    def has_pending_requests(self):
        return self.middleware.has_pending_requests

class Middleware(object):
    def __init__(self, app):
        self.app = app
        self.counter = Counter()
        self.error = None

    def __call__(self, environ, start_response):
        if environ["PATH_INFO"] == "/__identify__":
            return self.identify(environ, start_response)
        else:
            with self.counter:
                try:
                    return self.app(environ, start_response)
                except Exception as e:
                    self.error = e
                    raise

    def identify(self, environ, start_response):
        start_response("200 OK", [("Content-Type", "text/plain")])
        return [encode_string(str(id(self.app)))]

    @property
    def has_pending_requests(self):
        return self.counter.value > 0


#!/usr/bin/env python

#    Copyright 2016 - 2019 Alexey Stepanov aka penguinolog
#    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.

"""Property with class-wide getter."""

__all__ = ("AdvancedProperty",)

import typing

class AdvancedProperty(property):
    """Property with class-wide getter.

    This property allows implementation of read-only getter for classes
    in additional to normal set of getter/setter/deleter on instance.
    Implements almost full @property interface,
    except __doc__ due to class-wide nature.

    .. versionadded:: 2.1.0 Inherit property

    .. note:: If class-wide setter/deleter required: use normal property in metaclass.

    Usage examples:

    >>> class LikeNormalProperty:
    ...     def __init__(self):
    ...         self.val = 42
    ...     @AdvancedProperty
    ...     def prop(self):
    ...         return self.val
    ...     @prop.setter
    ...     def prop(self, new_val):
    ...         self.val = new_val
    ...     @prop.deleter
    ...     def prop(self):
    ...         self.val = 0

    >>> test_instance = LikeNormalProperty()

    >>> test_instance.prop
    42

    >>> test_instance.prop=43
    >>> test_instance.prop
    43

    >>> del test_instance.prop
    >>> test_instance.prop
    0

    # But access from class is restricted instead of normal property return.
    >>> LikeNormalProperty.prop
    Traceback (most recent call last):
    ...
    AttributeError

    >>> class ExtendedProperty:
    ...     def __init__(self):
    ...         self.val = 21
    ...     @AdvancedProperty
    ...     def prop(self):
    ...         return self.val
    ...     @prop.setter
    ...     def prop(self, new_val):
    ...         self.val = new_val
    ...     @prop.deleter
    ...     def prop(self):
    ...         self.val = -1
    ...     @prop.cgetter
    ...     def prop(cls):
    ...         return 'self.val'

    >>> test_instance = ExtendedProperty()

    >>> test_instance.prop
    21

    >>> test_instance.prop=20
    >>> test_instance.prop
    20

    >>> del test_instance.prop
    >>> test_instance.prop
    -1

    # Class getter is set and differs from normal getter
    >>> ExtendedProperty.prop
    'self.val'

    >>> class ClassProperty:
    ...     def _getter(cls):
    ...         return cls
    ...     prop = AdvancedProperty(fcget=_getter)  # special case

    >>> ClassProperty.prop is ClassProperty
    True

    >>> ClassProperty().prop is ClassProperty  # class wide property is used
    True
    """

    def __init__(
        self,
        fget: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None,
        fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]] = None,
        fdel: typing.Optional[typing.Callable[[typing.Any], None]] = None,
        fcget: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None,
    ) -> None:
        """Advanced property main entry point.

        :param fget: normal getter.
        :type fget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]
        :param fset: normal setter.
        :type fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]
        :param fdel: normal deleter.
        :type fdel: typing.Optional[typing.Callable[[typing.Any, ], None]]
        :param fcget: class getter. Used as normal, if normal is None.
        :type fcget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]

        .. note:: doc argument is not supported due to class wide getter usage.
        """
        super(AdvancedProperty, self).__init__(fget=fget, fset=fset, fdel=fdel)

        self.__fcget: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = fcget

    def __get__(self, instance: typing.Any, owner: typing.Any = None) -> typing.Any:
        """Get descriptor.

        :param instance: Owner class instance. Filled only if instance created, else None.
        :type instance: typing.Optional[owner]
        :param owner: Owner class for property.
        :return: getter call result if getter presents
        :rtype: typing.Any
        :raises AttributeError: Getter is not available
        """
        if owner is not None and (instance is None or self.fget is None):
            if self.__fcget is None:
                raise AttributeError()
            return self.__fcget(owner)
        return super(AdvancedProperty, self).__get__(instance, owner)

    @property
    def fcget(self) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]:
        """Class wide getter instance.

        :return: Class wide getter instance
        :rtype: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]
        """
        return self.__fcget

    def cgetter(self, fcget: typing.Optional[typing.Callable[[typing.Any], typing.Any]]) -> "AdvancedProperty":
        """Descriptor to change the class wide getter on a property.

        :param fcget: new class-wide getter.
        :type fcget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]
        :return: AdvancedProperty
        :rtype: AdvancedProperty
        """
        self.__fcget = fcget
        return self

if __name__ == "__main__":  # pragma: no cover
    import doctest

    doctest.testmod(verbose=True)


#!/usr/bin/env python

#    Copyright 2017 - 2019 Alexey Stepanov aka penguinolog
#    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.

"""Separate class and instance methods with the same name."""

__all__ = ("SeparateClassMethod",)

import functools
import typing

class SeparateClassMethod:
    """Separate class method and instance methods with the same name.

    Usage examples:

    >>> class WithNormalMethod:  # no wrapper should be used
    ...     def __init__(self):
    ...         self.val=42
    ...     @SeparateClassMethod
    ...     def set_val(self, new_val):
    ...         self.val=new_val

    >>> tst_obj = WithNormalMethod()
    >>> tst_obj.val
    42

    >>> tst_obj.set_val(21)
    >>> tst_obj.val
    21

    >>> WithNormalMethod.set_val()  # It's not defined
    Traceback (most recent call last):
    ...
    AttributeError

    >>> class WithSeparateMethod:
    ...     val = 33
    ...     def __init__(self):
    ...         self.val=42
    ...     @SeparateClassMethod
    ...     def set_val(self, new_val):
    ...         self.val=new_val
    ...     @set_val.class_method
    ...     def set_val(cls, new_val):
    ...         cls.val=new_val

    >>> tst_obj = WithSeparateMethod()
    >>> WithSeparateMethod.val
    33

    >>> tst_obj.val
    42

    >>> tst_obj.set_val(21)  # normal method
    >>> tst_obj.val
    21

    >>> WithSeparateMethod.val
    33

    >>> WithSeparateMethod.set_val(44)  # class method (not instance)
    >>> tst_obj.val
    21
    >>> WithSeparateMethod.val
    44

    >>> class WithClassMethod:  # @classmethod should be used
    ...     def _func(cls):
    ...         return cls
    ...     meth = SeparateClassMethod(cmeth=_func)

    >>> WithClassMethod.meth() is WithClassMethod
    True

    >>> WithClassMethod().meth() is WithClassMethod
    True
    """

    __slots__ = ("__instance_method", "__class_method", "__owner", "__name")

    def __init__(
        self,
        imeth: typing.Optional[typing.Callable[..., typing.Any]] = None,
        cmeth: typing.Optional[typing.Callable[..., typing.Any]] = None,
    ) -> None:
        """Separate class method and instance methods.

        :param imeth: Instance method
        :type imeth: typing.Optional[typing.Callable]
        :param cmeth: Class method
        :type cmeth: typing.Optional[typing.Callable]
        """
        self.__instance_method: typing.Optional[typing.Callable[..., typing.Any]] = imeth
        self.__class_method: typing.Optional[typing.Callable[..., typing.Any]] = cmeth
        self.__owner: typing.Optional[typing.Any] = None
        self.__name: str = ""

    def __set_name__(self, owner: typing.Any, name: str) -> None:
        """Set __name__ and __objclass__ property."""
        self.__owner = owner
        self.__name = name

    def __get__(self, instance: typing.Optional[typing.Any], owner: typing.Any) -> typing.Callable[..., typing.Any]:
        """Get descriptor.

        :return: class method or instance method depends on call behavior
        :rtype: typing.Callable
        :raises AttributeError: Not implemented getter for class method and called class context.
        """
        if instance is None or self.__instance_method is None:
            if self.__class_method is None:
                raise AttributeError()

            @functools.wraps(self.__class_method)
            def class_method(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
                """Bound class method."""
                return self.__class_method(owner, *args, **kwargs)  # type: ignore

            return class_method

        @functools.wraps(self.__instance_method)
        def instance_method(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
            """Bound instance method."""
            return self.__instance_method(instance, *args, **kwargs)  # type: ignore

        return instance_method

    @property
    def __objclass__(self) -> typing.Any:  # pragma: no cover
        """Read-only owner."""
        return self.__owner

    @property
    def __name__(self) -> str:  # pragma: no cover
        """Read-only name."""
        return self.__name

    def instance_method(self, imeth: typing.Optional[typing.Callable[..., typing.Any]]) -> "SeparateClassMethod":
        """Descriptor to change instance method.

        :param imeth: New instance method.
        :type imeth: typing.Optional[typing.Callable]
        :return: SeparateClassMethod
        :rtype: SeparateClassMethod
        """
        self.__instance_method = imeth
        return self

    def class_method(self, cmeth: typing.Optional[typing.Callable[..., typing.Any]]) -> "SeparateClassMethod":
        """Descriptor to change class method.

        :param cmeth: New class method.
        :type cmeth: typing.Optional[typing.Callable]
        :return: SeparateClassMethod
        :rtype: SeparateClassMethod
        """
        self.__class_method = cmeth
        return self

    @property
    def imeth(self) -> typing.Optional[typing.Callable[..., typing.Any]]:
        """Instance method instance.

        :rtype: typing.Optional[typing.Callable]
        """
        return self.__instance_method

    @property
    def cmeth(self) -> typing.Optional[typing.Callable[..., typing.Any]]:
        """Class method instance.

        :rtype: typing.Optional[typing.Callable]
        """
        return self.__class_method

if __name__ == "__main__":  # pragma: no cover
    import doctest

    doctest.testmod(verbose=True)


#!/usr/bin/env python

#    Copyright 2016 - 2019 Alexey Stepanov aka penguinolog
#    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.

"""Property with logging on successful get/set/delete or failure."""

__all__ = ("LogOnAccess",)

import logging
import sys
import traceback
import typing
import warnings

_LOGGER: logging.Logger = logging.getLogger(__name__)

class LogOnAccess(property):
    """Property with logging on successful get/set/delete or failure.

    .. versionadded:: 2.1.0
    .. versionchanged:: 2.2.0 Re-use logger from instance, if possible.
    """

    def __init__(
        self,
        fget: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None,
        fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]] = None,
        fdel: typing.Optional[typing.Callable[[typing.Any], None]] = None,
        doc: typing.Optional[str] = None,
        *,
        # Extended settings start
        logger: typing.Optional[typing.Union[logging.Logger, str]] = None,
        log_object_repr: bool = True,
        log_level: int = logging.DEBUG,
        exc_level: int = logging.DEBUG,
        log_success: bool = True,
        log_failure: bool = True,
        log_traceback: bool = True,
        override_name: typing.Optional[str] = None,
    ) -> None:
        """Advanced property main entry point.

        :param fget: normal getter.
        :type fget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]
        :param fset: normal setter.
        :type fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]
        :param fdel: normal deleter.
        :type fdel: typing.Optional[typing.Callable[[typing.Any, ], None]]
        :param doc: docstring override
        :type doc: typing.Optional[str]
        :param logger: logger instance or name to use as override
        :type logger: typing.Optional[typing.Union[logging.Logger, str]]
        :param log_object_repr: use `repr` over object to describe owner if True else owner class name and id
        :type log_object_repr: bool
        :param log_level: log level for successful operations
        :type log_level: int
        :param exc_level: log level for exceptions
        :type exc_level: int
        :param log_success: log successful operations
        :type log_success: bool
        :param log_failure: log exceptions
        :type log_failure: bool
        :param log_traceback: Log traceback on exceptions
        :type log_traceback: bool
        :param override_name: override property name if not None else use getter/setter/deleter name
        :type override_name: typing.Optional[str]

        Usage examples:

        >>> import logging
        >>> import io

        >>> log = io.StringIO()
        >>> logging.basicConfig(level=logging.DEBUG, stream=log)

        >>> class Test:
        ...     def __init__(self, val = 'ok'):
        ...         self.val = val
        ...     def __repr__(self):
        ...         return f'{self.__class__.__name__}(val={self.val})'
        ...     @LogOnAccess
        ...     def ok(self):
        ...         return self.val
        ...     @ok.setter
        ...     def ok(self, val):
        ...         self.val = val
        ...     @ok.deleter
        ...     def ok(self):
        ...         self.val = ''
        ...     @LogOnAccess
        ...     def fail_get(self):
        ...         raise RuntimeError()
        ...     @LogOnAccess
        ...     def fail_set_del(self):
        ...         return self.val
        ...     @fail_set_del.setter
        ...     def fail_set_del(self, value):
        ...         raise ValueError(value)
        ...     @fail_set_del.deleter
        ...     def fail_set_del(self):
        ...         raise RuntimeError()

        >>> test = Test()
        >>> test.ok
        'ok'
        >>> test.ok = 'OK'
        >>> del test.ok
        >>> test.ok = 'fail_get'

        >>> test.fail_get
        Traceback (most recent call last):
        ...
        RuntimeError

        >>> test.ok = 'fail_set_del'
        >>> test.fail_set_del
        'fail_set_del'

        >>> test.fail_set_del = 'fail'
        Traceback (most recent call last):
        ...
        ValueError: fail

        >>> del test.fail_set_del
        Traceback (most recent call last):
        ...
        RuntimeError

        >>> test.fail_set_del
        'fail_set_del'

        >>> logs = log.getvalue().splitlines()
        >>> logs[0] == "DEBUG:log_on_access:Test(val=ok).ok -> 'ok'"
        True
        >>> logs[1] == "DEBUG:log_on_access:Test(val=ok).ok = 'OK'"
        True
        >>> logs[2] == "DEBUG:log_on_access:del Test(val=OK).ok"
        True
        >>> logs[3] == "DEBUG:log_on_access:Test(val=).ok = 'fail_get'"
        True
        >>> logs[4:6]
        ['DEBUG:log_on_access:Failed Test(val=fail_get).fail_get', 'Traceback (most recent call last):']
        >>> logs[14] == "DEBUG:log_on_access:Test(val=fail_get).ok = 'fail_set_del'"
        True
        >>> logs[16] == "DEBUG:log_on_access:Failed Test(val=fail_set_del).fail_set_del = 'fail'"
        True
        >>> logs[17] == 'Traceback (most recent call last):'
        True
        >>> logs[26] == 'DEBUG:log_on_access:Test(val=fail_set_del): failed to delete fail_set_del'
        True
        >>> logs[27] == 'Traceback (most recent call last):'
        True
        """
        warnings.warn("LogOnAccess has been ported to logwrap with extended repr logic.", PendingDeprecationWarning)
        super(LogOnAccess, self).__init__(fget=fget, fset=fset, fdel=fdel, doc=doc)

        if logger is None or isinstance(logger, logging.Logger):
            self.__logger: typing.Optional[logging.Logger] = logger
        else:
            self.__logger = logging.getLogger(logger)

        self.__log_object_repr: bool = log_object_repr
        self.__log_level: int = log_level
        self.__exc_level: int = exc_level
        self.__log_success: bool = log_success
        self.__log_failure: bool = log_failure
        self.__log_traceback: bool = log_traceback
        self.__override_name: typing.Optional[str] = override_name

    @property
    def __traceback(self) -> str:
        """Get outer traceback text for logging."""
        if not self.log_traceback:
            return ""
        exc_info = sys.exc_info()
        stack = traceback.extract_stack()
        exc_tb = traceback.extract_tb(exc_info[2])
        full_tb = stack[:1] + exc_tb  # cut decorator and build full traceback
        exc_line: typing.List[str] = traceback.format_exception_only(*exc_info[:2])
        # Make standard traceback string
        tb_text = "\nTraceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line)
        return tb_text

    def __get_obj_source(self, instance: typing.Any, owner: typing.Optional[type] = None) -> str:
        """Get object repr block."""
        if self.log_object_repr:
            return f"{instance!r}"
        return f"<{owner.__name__ if owner is not None else instance.__class__.__name__}() at 0x{id(instance):X}>"

    def _get_logger_for_instance(self, instance: typing.Any) -> logging.Logger:
        """Get logger for log calls.

        :param instance: Owner class instance. Filled only if instance created, else None.
        :type instance: typing.Optional[owner]
        :return: logger instance
        :rtype: logging.Logger
        """
        if self.logger is not None:  # pylint: disable=no-else-return
            return self.logger
        elif hasattr(instance, "logger") and isinstance(instance.logger, logging.Logger):
            return instance.logger
        elif hasattr(instance, "log") and isinstance(instance.log, logging.Logger):
            return instance.log
        return _LOGGER

    def __get__(self, instance: typing.Any, owner: typing.Optional[type] = None) -> typing.Any:
        """Get descriptor.

        :param instance: Owner class instance. Filled only if instance created, else None.
        :type instance: typing.Optional[owner]
        :param owner: Owner class for property.
        :return: getter call result if getter presents
        :rtype: typing.Any
        :raises AttributeError: Getter is not available
        :raises Exception: Something goes wrong
        """
        if instance is None or self.fget is None:
            raise AttributeError()

        source: str = self.__get_obj_source(instance, owner)
        logger: logging.Logger = self._get_logger_for_instance(instance)

        try:
            result = super(LogOnAccess, self).__get__(instance, owner)
            if self.log_success:
                logger.log(self.log_level, f"{source}.{self.__name__} -> {result!r}")
            return result
        except Exception:
            if self.log_failure:
                logger.log(self.exc_level, f"Failed: {source}.{self.__name__}{self.__traceback}", exc_info=False)
            raise

    def __set__(self, instance: typing.Any, value: typing.Any) -> None:
        """Set descriptor.

        :param instance: Owner class instance. Filled only if instance created, else None.
        :type instance: typing.Optional
        :param value: Value for setter
        :raises AttributeError: Setter is not available
        :raises Exception: Something goes wrong
        """
        if self.fset is None:
            raise AttributeError()

        source: str = self.__get_obj_source(instance)
        logger: logging.Logger = self._get_logger_for_instance(instance)

        try:
            super(LogOnAccess, self).__set__(instance, value)
            if self.log_success:
                logger.log(self.log_level, f"{source}.{self.__name__} = {value!r}")
        except Exception:
            if self.log_failure:
                logger.log(
                    self.exc_level, f"Failed: {source}.{self.__name__} = {value!r}{self.__traceback}", exc_info=False
                )
            raise

    def __delete__(self, instance: typing.Any) -> None:
        """Delete descriptor.

        :param instance: Owner class instance. Filled only if instance created, else None.
        :type instance: typing.Optional
        :raises AttributeError: Deleter is not available
        :raises Exception: Something goes wrong
        """
        if self.fdel is None:
            raise AttributeError()

        source: str = self.__get_obj_source(instance)
        logger: logging.Logger = self._get_logger_for_instance(instance)

        try:
            super(LogOnAccess, self).__delete__(instance)
            if self.log_success:
                logger.log(self.log_level, f"del {source}.{self.__name__}")
        except Exception:
            if self.log_failure:
                logger.log(self.exc_level, f"{source}: Failed: del {self.__name__}{self.__traceback}", exc_info=False)
            raise

    @property
    def logger(self) -> typing.Optional[logging.Logger]:
        """Logger instance to use as override."""
        return self.__logger

    @logger.setter
    def logger(self, logger: typing.Union[logging.Logger, str, None]) -> None:
        """Logger instance to use as override."""
        if logger is None or isinstance(logger, logging.Logger):
            self.__logger = logger
        else:
            self.__logger = logging.getLogger(logger)

    @property
    def log_object_repr(self) -> bool:
        """Use `repr` over object to describe owner if True else owner class name and id."""
        return self.__log_object_repr

    @log_object_repr.setter
    def log_object_repr(self, value: bool) -> None:
        """Use `repr` over object to describe owner if True else owner class name and id."""
        self.__log_object_repr = value

    @property
    def log_level(self) -> int:
        """Log level for successful operations."""
        return self.__log_level

    @log_level.setter
    def log_level(self, value: int) -> None:
        """Log level for successful operations."""
        self.__log_level = value

    @property
    def exc_level(self) -> int:
        """Log level for exceptions."""
        return self.__exc_level

    @exc_level.setter
    def exc_level(self, value: int) -> None:
        """Log level for exceptions."""
        self.__exc_level = value

    @property
    def log_success(self) -> bool:
        """Log successful operations."""
        return self.__log_success

    @log_success.setter
    def log_success(self, value: bool) -> None:
        """Log successful operations."""
        self.__log_success = value

    @property
    def log_failure(self) -> bool:
        """Log exceptions."""
        return self.__log_failure

    @log_failure.setter
    def log_failure(self, value: bool) -> None:
        """Log exceptions."""
        self.__log_failure = value

    @property
    def log_traceback(self) -> bool:
        """Log traceback on exceptions."""
        return self.__log_traceback

    @log_traceback.setter
    def log_traceback(self, value: bool) -> None:
        """Log traceback on exceptions."""
        self.__log_traceback = value

    @property
    def override_name(self) -> typing.Optional[str]:
        """Override property name if not None else use getter/setter/deleter name."""
        return self.__override_name

    @override_name.setter
    def override_name(self, name: typing.Optional[str]) -> None:
        """Override property name if not None else use getter/setter/deleter name."""
        self.__override_name = name

    @property
    def __name__(self) -> str:
        """Name getter."""
        return (
            self.override_name or self.fget.__name__
            if self.fget
            else self.fset.__name__
            if self.fset
            else self.fdel.__name__
            if self.fdel
            else ""
        )

if __name__ == "__main__":  # pragma: no cover
    import doctest

    doctest.testmod(verbose=True)


#    Copyright 2016 - 2019 Alexey Stepanov aka penguinolog
#    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.

"""Advanced descriptors for special cases."""

from __future__ import print_function

import ast
import collections
import distutils.errors
import os.path
import shutil
import sys
from distutils.command import build_ext

import setuptools

try:
    from Cython.Build import cythonize
except ImportError:
    cythonize = None

with open(os.path.join(os.path.dirname(__file__), "advanced_descriptors", "__init__.py")) as f:
    SOURCE = f.read()

with open("requirements.txt") as f:
    REQUIRED = f.read().splitlines()

with open("README.rst") as f:
    LONG_DESCRIPTION = f.read()

def _extension(modpath):
    """Make setuptools.Extension."""
    return setuptools.Extension(modpath, [modpath.replace(".", "/") + ".py"])

REQUIRES_OPTIMIZATION = [
    _extension("advanced_descriptors.separate_class_method"),
    _extension("advanced_descriptors.advanced_property"),
    _extension("advanced_descriptors.log_on_access"),
]

if "win32" != sys.platform:
    REQUIRES_OPTIMIZATION.append(_extension("advanced_descriptors.__init__"))

EXT_MODULES = (
    cythonize(
        REQUIRES_OPTIMIZATION,
        compiler_directives=dict(
            always_allow_keywords=True, binding=True, embedsignature=True, overflowcheck=True, language_level=3
        ),
    )
    if cythonize is not None
    else []
)

class BuildFailed(Exception):
    """For install clear scripts."""

class AllowFailRepair(build_ext.build_ext):
    """This class allows C extension building to fail and repairs init."""

    def run(self):
        """Run.

        :raises BuildFailed: extension build failed and need to skip cython part.
        """
        try:
            build_ext.build_ext.run(self)

            # Copy __init__.py back to repair package.
            build_dir = os.path.abspath(self.build_lib)
            root_dir = os.path.abspath(os.path.join(__file__, ".."))
            target_dir = build_dir if not self.inplace else root_dir

            src_file = os.path.join("advanced_descriptors", "__init__.py")

            src = os.path.join(root_dir, src_file)
            dst = os.path.join(target_dir, src_file)

            if src != dst:
                shutil.copyfile(src, dst)
        except (
            distutils.errors.DistutilsPlatformError,
            FileNotFoundError,
        ):
            raise BuildFailed()

    def build_extension(self, ext):
        """build_extension.

        :raises BuildFailed: extension build failed and need to skip cython part.
        """
        try:
            build_ext.build_ext.build_extension(self, ext)
        except (
            distutils.errors.CCompilerError,
            distutils.errors.DistutilsExecError,
            distutils.errors.DistutilsPlatformError,
            ValueError,
        ):
            raise BuildFailed()

# noinspection PyUnresolvedReferences
def get_simple_vars_from_src(src):
    """Get simple (string/number/boolean and None) assigned values from source.

    :param src: Source code
    :type src: str
    :returns: OrderedDict with keys, values = variable names, values
    :rtype: typing.Dict[
                str,
                typing.Union[
                    str, bytes,
                    int, float, complex,
                    list, set, dict, tuple,
                    None,
                ]
            ]

    Limitations: Only defined from scratch variables.
    Not supported by design:
        * Imports
        * Executable code, including string formatting and comprehensions.

    Examples:

    >>> string_sample = "a = '1'"
    >>> get_simple_vars_from_src(string_sample)
    OrderedDict([('a', '1')])

    >>> int_sample = "b = 1"
    >>> get_simple_vars_from_src(int_sample)
    OrderedDict([('b', 1)])

    >>> list_sample = "c = [u'1', b'1', 1, 1.0, 1j, None]"
    >>> result = get_simple_vars_from_src(list_sample)
    >>> result == collections.OrderedDict(
    ...     [('c', [u'1', b'1', 1, 1.0, 1j, None])]
    ... )
    True

    >>> iterable_sample = "d = ([1], {1: 1}, {1})"
    >>> get_simple_vars_from_src(iterable_sample)
    OrderedDict([('d', ([1], {1: 1}, {1}))])

    >>> multiple_assign = "e = f = g = 1"
    >>> get_simple_vars_from_src(multiple_assign)
    OrderedDict([('e', 1), ('f', 1), ('g', 1)])
    """
    ast_data = (ast.Str, ast.Num, ast.List, ast.Set, ast.Dict, ast.Tuple, ast.Bytes, ast.NameConstant)

    tree = ast.parse(src)

    result = collections.OrderedDict()

    for node in ast.iter_child_nodes(tree):
        if not isinstance(node, ast.Assign):  # We parse assigns only
            continue
        try:
            if isinstance(node.value, ast_data):
                value = ast.literal_eval(node.value)
            else:
                continue
        except ValueError:
            continue
        for tgt in node.targets:
            if isinstance(tgt, ast.Name) and isinstance(tgt.ctx, ast.Store):
                result[tgt.id] = value
    return result

VARIABLES = get_simple_vars_from_src(SOURCE)

CLASSIFIERS = [
    "Development Status :: 5 - Production/Stable",
    "Intended Audience :: Developers",
    "Topic :: Software Development :: Libraries :: Python Modules",
    "License :: OSI Approved :: Apache Software License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.6",
    "Programming Language :: Python :: 3.7",
    "Programming Language :: Python :: Implementation :: CPython",
]

KEYWORDS = ["descriptor", "property", "classmethod", "development"]

setup_args = dict(
    name="Advanced-Descriptors",
    author=VARIABLES["__author__"],
    author_email=VARIABLES["__author_email__"],
    maintainer=", ".join(
        "{name} <{email}>".format(name=name, email=email) for name, email in VARIABLES["__maintainers__"].items()
    ),
    url=VARIABLES["__url__"],
    license=VARIABLES["__license__"],
    description=VARIABLES["__description__"],
    long_description=LONG_DESCRIPTION,
    classifiers=CLASSIFIERS,
    keywords=KEYWORDS,
    python_requires=">=3.6",
    # While setuptools cannot deal with pre-installed incompatible versions,
    # setting a lower bound is not harmful - it makes error messages cleaner. DO
    # NOT set an upper bound on setuptools, as that will lead to uninstallable
    # situations as progressive releases of projects are done.
    # Blacklist setuptools 34.0.0-34.3.2 due to https://github.com/pypa/setuptools/issues/951
    # Blacklist setuptools 36.2.0 due to https://github.com/pypa/setuptools/issues/1086
    setup_requires=[
        "setuptools >= 21.0.0,!=24.0.0,"
        "!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,"
        "!=36.2.0",
        "setuptools_scm",
    ],
    use_scm_version=True,
    install_requires=REQUIRED,
    package_data={"advanced_descriptors": ["py.typed"]},
)
if cythonize is not None:
    setup_args["ext_modules"] = EXT_MODULES
    setup_args["cmdclass"] = dict(build_ext=AllowFailRepair)

try:
    setuptools.setup(**setup_args)
except BuildFailed:
    print("*" * 80 + "\n" "* Build Failed!\n" "* Use clear scripts version.\n" "*" * 80 + "\n")
    del setup_args["ext_modules"]
    del setup_args["cmdclass"]
    setuptools.setup(**setup_args)


import logging
import os

import requests

logger = logging.getLogger('slackapi')

class SlackAPI(object):
    """
    Yet Another Slack Client
    """
    url = 'https://slack.com/api/{method}'

    def __init__(self, token=None, auth_test=False, verify=True, lazy=False):
        """
        Instantiation an instance of the Slack API

        Args:
            token: {str} (required) API token, read from SLACK_TOKEN env var
            auth_test: {bool} verify this token
            verify: {bool} verify all API calls return with a True 'ok'
            lazy: {bool} Don't populate properties until called
        """

        try:
            self.token = token if token else os.environ['SLACK_TOKEN']
        except KeyError:
            raise ValueError('If not providing a token, must set SLACK_TOKEN envvar')
        if auth_test:
            response = self.auth_test()
            if not response['ok']:
                raise ValueError('Authentication Failed with response: {}'.format(response))
        self.verify = verify

        # Attributes backing properties
        self._channels = []
        self._users = []

        if not lazy:
            _ = self.channels
            _ = self.users

    def _call_api(self, method, params=None):
        """
        Low-level method to call the Slack API.

        Args:
            method: {str} method name to call
            params: {dict} GET parameters
                The token will always be added
        """
        url = self.url.format(method=method)
        if not params:
            params = {'token': self.token}
        else:
            params['token'] = self.token
        logger.debug('Send request to %s', url)
        response = requests.get(url, params=params).json()
        if self.verify:
            if not response['ok']:
                msg = 'For {url} API returned this bad response {response}'
                raise Exception(msg.format(url=url, response=response))
        return response

    @property
    def channels(self):
        """
        List of channels of this slack team
        """
        if not self._channels:
            self._channels = self._call_api('channels.list')['channels']
        return self._channels

    @property
    def users(self):
        """
        List of users of this slack team
        """
        if not self._users:
            self._users = self._call_api('users.list')['members']
        return self._users

    # API Methods
    def auth_test(self):
        """
        Call auth.test
        """
        return self._call_api('auth.test')

    def rtm_start(self):
        """
        Call rtm.start
        """
        return self._call_api('rtm.start')

    # Translation
    def channel_from_name(self, name):
        """
        Return the channel dict given by human-readable {name}
        """
        try:
            channel = [channel for channel in self.channels
                       if channel['name'] == name][0]
        except IndexError:
            raise ValueError('Unknown channel for name: "{}"'.format(name))
        return channel

    def user_from_id(self, user_id):
        try:
            user = [user for user in self.users
                    if user['id'] == user_id][0]
        except IndexError:
            raise ValueError('Unknown user for id: "{}"'.format(user_id))
        return user

    def channel_from_id(self, channel_id):
        try:
            channel = [channel for channel in self.channels
                       if channel['id'] == channel_id][0]
        except IndexError:
            raise ValueError('Unknown channel for id: "{}"'.format(channel_id))
        else:
            return channel

    def reload_channels(self):
        self._channels = None


import json

from autobahn.twisted.websocket import (WebSocketClientProtocol,
                                        WebSocketClientFactory, connectWS)
import channels
from twisted.internet import reactor, ssl
from twisted.internet.ssl import ClientContextFactory

from .slack import SlackAPI

def pack(message):
    """
    Serialize the message into something for sending
    across the wire.

    Args:
        message: {dict} dictionary that full specifies a slack message
    """
    return json.dumps(message).encode('utf8')

def unpack(text):
    """
    Decode & Load

    Args:
        text: {str} JSON blob
    """
    return json.loads(text.decode('utf8'))

class SlackClientProtocol(WebSocketClientProtocol):
    """
    This defines how messages from Slack are passed onto
    Channels and how a specific channel is passed to Slack
    """
    def __init__(self, *args, **kwargs):
        super(SlackClientProtocol, self).__init__(*args, **kwargs)
        self._message_id = 0

    @property
    def message_id(self):
        """
        Return an auto-incrementing integer
        """
        self._message_id += 1
        return self._message_id

    def make_message(self, text, channel):
        """
        High-level function for creating messages. Return packed bytes.

        Args:
            text: {str}
            channel: {str} Either name or ID
        """
        try:
            channel_id = self.slack.channel_from_name(channel)['id']
        except ValueError:
            channel_id = channel
        return pack({
            'text': text,
            'type': 'message',
            'channel': channel_id,
            'id': self.message_id,
        })

    def translate(self, message):
        """
        Translate machine identifiers into human-readable
        """
        # translate user
        try:
            user_id = message.pop('user')
            user = self.slack.user_from_id(user_id)
            message[u'user'] = user['name']
        except (KeyError, IndexError, ValueError):
            pass
        # translate channel
        try:
            if type(message['channel']) == str:
                channel_id = message.pop('channel')
            else:
                channel_id = message.pop('channel')['id']
                self.slack.reload_channels()
            channel = self.slack.channel_from_id(channel_id)
            message[u'channel'] = channel['name']
        except (KeyError, IndexError, ValueError):
            pass
        return message

    def onOpen(self):
        """
        Store this protocol instance in the factory and wave hello.
        """
        self.factory.protocols.append(self)

    def onMessage(self, payload, isBinary):
        """
        Send the payload onto the {slack.[payload['type]'} channel.
        The message is transalated from IDs to human-readable identifiers.

        Note: The slack API only sends JSON, isBinary will always be false.
        """
        msg = self.translate(unpack(payload))
        if 'type' in msg:
            channel_name = 'slack.{}'.format(msg['type'])
            print('Sending on {}'.format(channel_name))
            channels.Channel(channel_name).send({'text': pack(msg)})

    def sendSlack(self, message):
        """
        Send message to Slack
        """
        channel = message.get('channel', 'general')
        self.sendMessage(self.make_message(message['text'], channel))

class SlackClientFactory(WebSocketClientFactory):

    def run(self):
        if self.isSecure:
            contextFactory = ssl.ClientContextFactory()
        else:
            contextFactory = None

        self.connector = connectWS(self, contextFactory)
        self.read_channel()
        reactor.run(installSignalHandlers=True)

    def read_channel(self):
        """
        Get available messages and send through to the protocol
        """
        channel, message = self.protocol.channel_layer.receive_many([u'slack.send'], block=False)
        delay = 0.1
        if channel:
            self.protocols[0].sendSlack(message)
        reactor.callLater(delay, self.read_channel)

class Client(object):
    """
    Main client to instantiate the SlackAPI and client factory
    """

    def __init__(self, channel_layer, token, channel_name=u'slack.send'):
        """
        Args:
            channel_layer: channel layer on which this client will communicate to Django
            token: {str} Slack token
            channel_name: {str} channel name to send messages that will come back to slack
        """
        self.channel_layer = channel_layer
        self.token = token
        # TODO: Surface this channel_name in the CLI args
        self.channel_name = channel_name

    def run(self):
        """
        Main interface. Instantiate the SlackAPI, connect to RTM
        and start the client.
        """
        slack = SlackAPI(token=self.token)
        rtm = slack.rtm_start()

        factory = SlackClientFactory(rtm['url'])
        # Attach attributes
        factory.protocol = SlackClientProtocol
        factory.protocol.slack = slack
        factory.protocol.channel_layer = self.channel_layer
        factory.channel_name = self.channel_name

        # Here we go
        factory.run()


import argparse
import importlib
import os
import sys

from .client import Client

class CLI(object):
    """
    Main command-line interface
    """

    def __init__(self):
        description = 'Slack bridge to a channels-based Django app'
        self.parser = argparse.ArgumentParser(
            description=description
        )
        self.parser.add_argument(
            '-t',
            '--token',
            dest='token',
            help='Slack API token',
            default=os.environ.get('DJANGOBOT_TOKEN', None),
        )
        self.parser.add_argument(
            'channel_layer',
            help='ASGI channel layer instance to listen on given as path.to.module:layer',
        )

    @classmethod
    def entry(cls):
        """
        External starting point
        """
        cls().run(sys.argv[1:])

    def run(self, args):
        """
        Pass in raw arguments, instantiate Slack API and begin client.
        """
        args = self.parser.parse_args(args)
        if not args.token:
            raise ValueError('Supply the slack token through --token or setting DJANGOBOT_TOKEN')

        # Import the channel layer
        sys.path.insert(0, ".")
        module_path, object_path = args.channel_layer.split(':', 1)
        channel_layer = importlib.import_module(module_path)
        for part in object_path.split('.'):
            channel_layer = getattr(channel_layer, part)

        # Boot up the client
        Client(
            channel_layer=channel_layer,
            token=args.token,
        ).run()


from operator import attrgetter
from pyangbind.lib.yangtypes import RestrictedPrecisionDecimalType
from pyangbind.lib.yangtypes import RestrictedClassType
from pyangbind.lib.yangtypes import TypedListType
from pyangbind.lib.yangtypes import YANGBool
from pyangbind.lib.yangtypes import YANGListType
from pyangbind.lib.yangtypes import YANGDynClass
from pyangbind.lib.yangtypes import ReferenceType
from pyangbind.lib.base import PybindBase
from decimal import Decimal
from bitarray import bitarray
import six

# PY3 support of some PY2 keywords (needs improved)
if six.PY3:
    import builtins as __builtin__

    long = int
    unicode = str
elif six.PY2:
    import __builtin__

class yc_individual_napalm_star_wars__universe_individual(PybindBase):
    """
  This class was auto-generated by the PythonClass plugin for PYANG
  from YANG module napalm-star-wars - based on the path /universe/individual. Each member element of
  the container is represented as a class variable - with a specific
  YANG type.
  """
    __slots__ = (
        "_pybind_generated_by",
        "_path_helper",
        "_yang_name",
        "_extmethods",
        "__name",
        "__age",
        "__affiliation",
    )

    _yang_name = "individual"

    _pybind_generated_by = "container"

    def __init__(self, *args, **kwargs):

        self._path_helper = False

        self._extmethods = False
        self.__affiliation = YANGDynClass(
            base=RestrictedClassType(
                base_type=unicode,
                restriction_type="dict_key",
                restriction_arg={
                    u"napalm-star-wars:EMPIRE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"EMPIRE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"napalm-star-wars:REBEL_ALLIANCE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"REBEL_ALLIANCE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                },
            ),
            is_leaf=True,
            yang_name="affiliation",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="identityref",
            is_config=True,
        )
        self.__age = YANGDynClass(
            base=RestrictedClassType(
                base_type=RestrictedClassType(
                    base_type=int, restriction_dict={"range": ["0..65535"]}, int_size=16
                ),
                restriction_dict={"range": [u"1..2000"]},
            ),
            is_leaf=True,
            yang_name="age",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="age",
            is_config=True,
        )
        self.__name = YANGDynClass(
            base=unicode,
            is_leaf=True,
            yang_name="name",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            is_keyval=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="string",
            is_config=True,
        )

        load = kwargs.pop("load", None)
        if args:
            if len(args) > 1:
                raise TypeError("cannot create a YANG container with >1 argument")
            all_attr = True
            for e in self._pyangbind_elements:
                if not hasattr(args[0], e):
                    all_attr = False
                    break
            if not all_attr:
                raise ValueError("Supplied object did not have the correct attributes")
            for e in self._pyangbind_elements:
                nobj = getattr(args[0], e)
                if nobj._changed() is False:
                    continue
                setmethod = getattr(self, "_set_%s" % e)
                if load is None:
                    setmethod(getattr(args[0], e))
                else:
                    setmethod(getattr(args[0], e), load=load)

    def _path(self):
        if hasattr(self, "_parent"):
            return self._parent._path() + [self._yang_name]
        else:
            return [u"universe", u"individual"]

    def _get_name(self):
        """
    Getter method for name, mapped from YANG variable /universe/individual/name (string)
    """
        return self.__name

    def _set_name(self, v, load=False):
        """
    Setter method for name, mapped from YANG variable /universe/individual/name (string)
    If this variable is read-only (config: false) in the
    source YANG file, then _set_name is considered as a private
    method. Backends looking to populate this variable should
    do so via calling thisObj._set_name() directly.
    """
        parent = getattr(self, "_parent", None)
        if parent is not None and load is False:
            raise AttributeError(
                "Cannot set keys directly when" + " within an instantiated list"
            )

        if hasattr(v, "_utype"):
            v = v._utype(v)
        try:
            t = YANGDynClass(
                v,
                base=unicode,
                is_leaf=True,
                yang_name="name",
                parent=self,
                path_helper=self._path_helper,
                extmethods=self._extmethods,
                register_paths=True,
                is_keyval=True,
                namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
                defining_module="napalm-star-wars",
                yang_type="string",
                is_config=True,
            )
        except (TypeError, ValueError):
            raise ValueError(
                {
                    "error-string": """name must be of a type compatible with string""",
                    "defined-type": "string",
                    "generated-type": """YANGDynClass(base=unicode, is_leaf=True, yang_name="name", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, is_keyval=True, namespace='https://napalm-yang.readthedocs.io/napalm-star-wars', defining_module='napalm-star-wars', yang_type='string', is_config=True)""",
                }
            )

        self.__name = t
        if hasattr(self, "_set"):
            self._set()

    def _unset_name(self):
        self.__name = YANGDynClass(
            base=unicode,
            is_leaf=True,
            yang_name="name",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            is_keyval=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="string",
            is_config=True,
        )

    def _get_age(self):
        """
    Getter method for age, mapped from YANG variable /universe/individual/age (age)
    """
        return self.__age

    def _set_age(self, v, load=False):
        """
    Setter method for age, mapped from YANG variable /universe/individual/age (age)
    If this variable is read-only (config: false) in the
    source YANG file, then _set_age is considered as a private
    method. Backends looking to populate this variable should
    do so via calling thisObj._set_age() directly.
    """
        if hasattr(v, "_utype"):
            v = v._utype(v)
        try:
            t = YANGDynClass(
                v,
                base=RestrictedClassType(
                    base_type=RestrictedClassType(
                        base_type=int,
                        restriction_dict={"range": ["0..65535"]},
                        int_size=16,
                    ),
                    restriction_dict={"range": [u"1..2000"]},
                ),
                is_leaf=True,
                yang_name="age",
                parent=self,
                path_helper=self._path_helper,
                extmethods=self._extmethods,
                register_paths=True,
                namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
                defining_module="napalm-star-wars",
                yang_type="age",
                is_config=True,
            )
        except (TypeError, ValueError):
            raise ValueError(
                {
                    "error-string": """age must be of a type compatible with age""",
                    "defined-type": "napalm-star-wars:age",
                    "generated-type": """YANGDynClass(base=RestrictedClassType(base_type=RestrictedClassType(base_type=int, restriction_dict={'range': ['0..65535']},int_size=16), restriction_dict={'range': [u'1..2000']}), is_leaf=True, yang_name="age", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, namespace='https://napalm-yang.readthedocs.io/napalm-star-wars', defining_module='napalm-star-wars', yang_type='age', is_config=True)""",
                }
            )

        self.__age = t
        if hasattr(self, "_set"):
            self._set()

    def _unset_age(self):
        self.__age = YANGDynClass(
            base=RestrictedClassType(
                base_type=RestrictedClassType(
                    base_type=int, restriction_dict={"range": ["0..65535"]}, int_size=16
                ),
                restriction_dict={"range": [u"1..2000"]},
            ),
            is_leaf=True,
            yang_name="age",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="age",
            is_config=True,
        )

    def _get_affiliation(self):
        """
    Getter method for affiliation, mapped from YANG variable /universe/individual/affiliation (identityref)
    """
        return self.__affiliation

    def _set_affiliation(self, v, load=False):
        """
    Setter method for affiliation, mapped from YANG variable /universe/individual/affiliation (identityref)
    If this variable is read-only (config: false) in the
    source YANG file, then _set_affiliation is considered as a private
    method. Backends looking to populate this variable should
    do so via calling thisObj._set_affiliation() directly.
    """
        if hasattr(v, "_utype"):
            v = v._utype(v)
        try:
            t = YANGDynClass(
                v,
                base=RestrictedClassType(
                    base_type=unicode,
                    restriction_type="dict_key",
                    restriction_arg={
                        u"napalm-star-wars:EMPIRE": {
                            "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                            "@module": u"napalm-star-wars",
                        },
                        u"EMPIRE": {
                            "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                            "@module": u"napalm-star-wars",
                        },
                        u"napalm-star-wars:REBEL_ALLIANCE": {
                            "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                            "@module": u"napalm-star-wars",
                        },
                        u"REBEL_ALLIANCE": {
                            "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                            "@module": u"napalm-star-wars",
                        },
                    },
                ),
                is_leaf=True,
                yang_name="affiliation",
                parent=self,
                path_helper=self._path_helper,
                extmethods=self._extmethods,
                register_paths=True,
                namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
                defining_module="napalm-star-wars",
                yang_type="identityref",
                is_config=True,
            )
        except (TypeError, ValueError):
            raise ValueError(
                {
                    "error-string": """affiliation must be of a type compatible with identityref""",
                    "defined-type": "napalm-star-wars:identityref",
                    "generated-type": """YANGDynClass(base=RestrictedClassType(base_type=unicode, restriction_type="dict_key", restriction_arg={u'napalm-star-wars:EMPIRE': {'@namespace': u'https://napalm-yang.readthedocs.io/napalm-star-wars', '@module': u'napalm-star-wars'}, u'EMPIRE': {'@namespace': u'https://napalm-yang.readthedocs.io/napalm-star-wars', '@module': u'napalm-star-wars'}, u'napalm-star-wars:REBEL_ALLIANCE': {'@namespace': u'https://napalm-yang.readthedocs.io/napalm-star-wars', '@module': u'napalm-star-wars'}, u'REBEL_ALLIANCE': {'@namespace': u'https://napalm-yang.readthedocs.io/napalm-star-wars', '@module': u'napalm-star-wars'}},), is_leaf=True, yang_name="affiliation", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, namespace='https://napalm-yang.readthedocs.io/napalm-star-wars', defining_module='napalm-star-wars', yang_type='identityref', is_config=True)""",
                }
            )

        self.__affiliation = t
        if hasattr(self, "_set"):
            self._set()

    def _unset_affiliation(self):
        self.__affiliation = YANGDynClass(
            base=RestrictedClassType(
                base_type=unicode,
                restriction_type="dict_key",
                restriction_arg={
                    u"napalm-star-wars:EMPIRE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"EMPIRE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"napalm-star-wars:REBEL_ALLIANCE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                    u"REBEL_ALLIANCE": {
                        "@namespace": u"https://napalm-yang.readthedocs.io/napalm-star-wars",
                        "@module": u"napalm-star-wars",
                    },
                },
            ),
            is_leaf=True,
            yang_name="affiliation",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="identityref",
            is_config=True,
        )

    name = __builtin__.property(_get_name, _set_name)
    age = __builtin__.property(_get_age, _set_age)
    affiliation = __builtin__.property(_get_affiliation, _set_affiliation)

    _pyangbind_elements = {"name": name, "age": age, "affiliation": affiliation}

class yc_universe_napalm_star_wars__universe(PybindBase):
    """
  This class was auto-generated by the PythonClass plugin for PYANG
  from YANG module napalm-star-wars - based on the path /universe. Each member element of
  the container is represented as a class variable - with a specific
  YANG type.
  """
    __slots__ = (
        "_pybind_generated_by",
        "_path_helper",
        "_yang_name",
        "_extmethods",
        "__individual",
    )

    _yang_name = "universe"

    _pybind_generated_by = "container"

    def __init__(self, *args, **kwargs):

        self._path_helper = False

        self._extmethods = False
        self.__individual = YANGDynClass(
            base=YANGListType(
                "name",
                yc_individual_napalm_star_wars__universe_individual,
                yang_name="individual",
                parent=self,
                is_container="list",
                user_ordered=False,
                path_helper=self._path_helper,
                yang_keys="name",
                extensions=None,
            ),
            is_container="list",
            yang_name="individual",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            extensions=None,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="list",
            is_config=True,
        )

        load = kwargs.pop("load", None)
        if args:
            if len(args) > 1:
                raise TypeError("cannot create a YANG container with >1 argument")
            all_attr = True
            for e in self._pyangbind_elements:
                if not hasattr(args[0], e):
                    all_attr = False
                    break
            if not all_attr:
                raise ValueError("Supplied object did not have the correct attributes")
            for e in self._pyangbind_elements:
                nobj = getattr(args[0], e)
                if nobj._changed() is False:
                    continue
                setmethod = getattr(self, "_set_%s" % e)
                if load is None:
                    setmethod(getattr(args[0], e))
                else:
                    setmethod(getattr(args[0], e), load=load)

    def _path(self):
        if hasattr(self, "_parent"):
            return self._parent._path() + [self._yang_name]
        else:
            return [u"universe"]

    def _get_individual(self):
        """
    Getter method for individual, mapped from YANG variable /universe/individual (list)
    """
        return self.__individual

    def _set_individual(self, v, load=False):
        """
    Setter method for individual, mapped from YANG variable /universe/individual (list)
    If this variable is read-only (config: false) in the
    source YANG file, then _set_individual is considered as a private
    method. Backends looking to populate this variable should
    do so via calling thisObj._set_individual() directly.
    """
        if hasattr(v, "_utype"):
            v = v._utype(v)
        try:
            t = YANGDynClass(
                v,
                base=YANGListType(
                    "name",
                    yc_individual_napalm_star_wars__universe_individual,
                    yang_name="individual",
                    parent=self,
                    is_container="list",
                    user_ordered=False,
                    path_helper=self._path_helper,
                    yang_keys="name",
                    extensions=None,
                ),
                is_container="list",
                yang_name="individual",
                parent=self,
                path_helper=self._path_helper,
                extmethods=self._extmethods,
                register_paths=True,
                extensions=None,
                namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
                defining_module="napalm-star-wars",
                yang_type="list",
                is_config=True,
            )
        except (TypeError, ValueError):
            raise ValueError(
                {
                    "error-string": """individual must be of a type compatible with list""",
                    "defined-type": "list",
                    "generated-type": """YANGDynClass(base=YANGListType("name",yc_individual_napalm_star_wars__universe_individual, yang_name="individual", parent=self, is_container='list', user_ordered=False, path_helper=self._path_helper, yang_keys='name', extensions=None), is_container='list', yang_name="individual", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, extensions=None, namespace='https://napalm-yang.readthedocs.io/napalm-star-wars', defining_module='napalm-star-wars', yang_type='list', is_config=True)""",
                }
            )

        self.__individual = t
        if hasattr(self, "_set"):
            self._set()

    def _unset_individual(self):
        self.__individual = YANGDynClass(
            base=YANGListType(
                "name",
                yc_individual_napalm_star_wars__universe_individual,
                yang_name="individual",
                parent=self,
                is_container="list",
                user_ordered=False,
                path_helper=self._path_helper,
                yang_keys="name",
                extensions=None,
            ),
            is_container="list",
            yang_name="individual",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            extensions=None,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="list",
            is_config=True,
        )

    individual = __builtin__.property(_get_individual, _set_individual)

    _pyangbind_elements = {"individual": individual}

class napalm_star_wars(PybindBase):
    """
  This class was auto-generated by the PythonClass plugin for PYANG
  from YANG module napalm-star-wars - based on the path /napalm-star-wars. Each member element of
  the container is represented as a class variable - with a specific
  YANG type.
  """
    __slots__ = (
        "_pybind_generated_by",
        "_path_helper",
        "_yang_name",
        "_extmethods",
        "__universe",
    )

    _yang_name = "napalm-star-wars"

    _pybind_generated_by = "container"

    def __init__(self, *args, **kwargs):

        self._path_helper = False

        self._extmethods = False
        self.__universe = YANGDynClass(
            base=yc_universe_napalm_star_wars__universe,
            is_container="container",
            yang_name="universe",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            extensions=None,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="container",
            is_config=True,
        )

        load = kwargs.pop("load", None)
        if args:
            if len(args) > 1:
                raise TypeError("cannot create a YANG container with >1 argument")
            all_attr = True
            for e in self._pyangbind_elements:
                if not hasattr(args[0], e):
                    all_attr = False
                    break
            if not all_attr:
                raise ValueError("Supplied object did not have the correct attributes")
            for e in self._pyangbind_elements:
                nobj = getattr(args[0], e)
                if nobj._changed() is False:
                    continue
                setmethod = getattr(self, "_set_%s" % e)
                if load is None:
                    setmethod(getattr(args[0], e))
                else:
                    setmethod(getattr(args[0], e), load=load)

    def _path(self):
        if hasattr(self, "_parent"):
            return self._parent._path() + [self._yang_name]
        else:
            return []

    def _get_universe(self):
        """
    Getter method for universe, mapped from YANG variable /universe (container)
    """
        return self.__universe

    def _set_universe(self, v, load=False):
        """
    Setter method for universe, mapped from YANG variable /universe (container)
    If this variable is read-only (config: false) in the
    source YANG file, then _set_universe is considered as a private
    method. Backends looking to populate this variable should
    do so via calling thisObj._set_universe() directly.
    """
        if hasattr(v, "_utype"):
            v = v._utype(v)
        try:
            t = YANGDynClass(
                v,
                base=yc_universe_napalm_star_wars__universe,
                is_container="container",
                yang_name="universe",
                parent=self,
                path_helper=self._path_helper,
                extmethods=self._extmethods,
                register_paths=True,
                extensions=None,
                namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
                defining_module="napalm-star-wars",
                yang_type="container",
                is_config=True,
            )
        except (TypeError, ValueError):
            raise ValueError(
                {
                    "error-string": """universe must be of a type compatible with container""",
                    "defined-type": "container",
                    "generated-type": """YANGDynClass(base=yc_universe_napalm_star_wars__universe, is_container='container', yang_name="universe", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, extensions=None, namespace='https://napalm-yang.readthedocs.io/napalm-star-wars', defining_module='napalm-star-wars', yang_type='container', is_config=True)""",
                }
            )

        self.__universe = t
        if hasattr(self, "_set"):
            self._set()

    def _unset_universe(self):
        self.__universe = YANGDynClass(
            base=yc_universe_napalm_star_wars__universe,
            is_container="container",
            yang_name="universe",
            parent=self,
            path_helper=self._path_helper,
            extmethods=self._extmethods,
            register_paths=True,
            extensions=None,
            namespace="https://napalm-yang.readthedocs.io/napalm-star-wars",
            defining_module="napalm-star-wars",
            yang_type="container",
            is_config=True,
        )

    universe = __builtin__.property(_get_universe, _set_universe)

    _pyangbind_elements = {"universe": universe}