class Handler(object):
    """
    Class representing a reactive flag handler.
    """
    _HANDLERS = {}
    _CONSUMED_FLAGS = set()

    @classmethod
    def get(cls, action, suffix=None):
        """
        Get or register a handler for the given action.

        :param func action: Callback that is called when invoking the Handler
        :param func suffix: Optional suffix for the handler's ID
        """
        action_id = _action_id(action, suffix)
        if action_id not in cls._HANDLERS:
            if LOG_OPTS['register']:
                hookenv.log('Registering reactive handler for %s' % _short_action_id(action, suffix),
                            level=hookenv.DEBUG)
            cls._HANDLERS[action_id] = cls(action, suffix)
        return cls._HANDLERS[action_id]

    @classmethod
    def get_handlers(cls):
        """
        Get all registered handlers.
        """
        return cls._HANDLERS.values()

    @classmethod
    def clear(cls):
        """
        Clear all registered handlers.
        """
        cls._HANDLERS = {}

    def __init__(self, action, suffix=None):
        """
        Create a new Handler.

        :param func action: Callback that is called when invoking the Handler
        :param func suffix: Optional suffix for the handler's ID
        """
        self._action_id = _short_action_id(action, suffix)
        self._action = action
        self._args = []
        self._predicates = []
        self._post_callbacks = []
        self._flags = set()

    def id(self):
        return self._action_id

    def add_args(self, args):
        """
        Add arguments to be passed to the action when invoked.

        :param args: Any sequence or iterable, which will be lazily evaluated
            to provide args.  Subsequent calls to :meth:`add_args` can be used
            to add additional arguments.
        """
        self._args.append(args)

    @property
    def has_args(self):
        """
        Whether or not this Handler has had any args added via :meth:`add_args`.
        """
        return len(self._args) > 0

    def add_predicate(self, predicate):
        """
        Add a new predicate callback to this handler.
        """
        _predicate = predicate
        if isinstance(predicate, partial):
            _predicate = 'partial(%s, %s, %s)' % (predicate.func, predicate.args, predicate.keywords)
        if LOG_OPTS['register']:
            hookenv.log('  Adding predicate for %s: %s' % (self.id(), _predicate), level=hookenv.DEBUG)
        self._predicates.append(predicate)

    def add_post_callback(self, callback):
        """
        Add a callback to be run after the action is invoked.
        """
        self._post_callbacks.append(callback)

    def test(self):
        """
        Check the predicate(s) and return True if this handler should be invoked.
        """
        if self._flags and not FlagWatch.watch(self._action_id, self._flags):
            return False
        return all(predicate() for predicate in self._predicates)

    def _get_args(self):
        """
        Lazily evaluate the args.
        """
        if not hasattr(self, '_args_evaled'):
            # cache the args in case handler is re-invoked due to flags change
            self._args_evaled = list(chain.from_iterable(self._args))
        return self._args_evaled

    def invoke(self):
        """
        Invoke this handler.
        """
        args = self._get_args()
        self._action(*args)
        for callback in self._post_callbacks:
            callback()

    def register_flags(self, flags):
        """
        Register flags as being relevant to this handler.

        Relevant flags will be used to determine if the handler should
        be re-invoked due to changes in the set of active flags.  If this
        handler has already been invoked during this :func:`dispatch` run
        and none of its relevant flags have been set or removed since then,
        then the handler will be skipped.

        This is also used for linting and composition purposes, to determine
        if a layer has unhandled flags.
        """
        self._CONSUMED_FLAGS.update(flags)
        self._flags.update(flags) def _action_id(action, suffix=None):
    if hasattr(action, '_action_id'):
        return action._action_id
    parts = [
        action.__code__.co_filename,
        action.__code__.co_firstlineno,
        action.__code__.co_name,
    ]
    if suffix is not None:
        parts.append(suffix)
    return ':'.join(map(str, parts)) def _short_action_id(action, suffix=None):
    if hasattr(action, '_short_action_id'):
        return action._short_action_id
    filepath = os.path.relpath(action.__code__.co_filename, hookenv.charm_dir())
    parts = [
        filepath,
        action.__code__.co_firstlineno,
        action.__code__.co_name,
    ]
    if suffix is not None:
        parts.append(suffix)
    return ':'.join(map(str, parts)) @cmdline.subcommand()
def get_flags():
    """
    Return a list of all flags which are set.
    """
    flags = unitdata.kv().getrange('reactive.states.', strip=True) or {}
    return sorted(flags.keys()) def endpoint_from_name(endpoint_name):
    """The object used for interacting with the named relations, or None.
    """
    if endpoint_name is None:
        return None
    factory = relation_factory(endpoint_name)
    if factory:
        return factory.from_name(endpoint_name) def endpoint_from_flag(flag):
    """The object used for interacting with relations tied to a flag, or None.
    """
    relation_name = None
    value = _get_flag_value(flag)
    if isinstance(value, dict) and 'relation' in value:
        # old-style RelationBase
        relation_name = value['relation']
    elif flag.startswith('endpoint.'):
        # new-style Endpoint
        relation_name = flag.split('.')[1]
    elif '.' in flag:
        # might be an unprefixed new-style Endpoint
        relation_name = flag.split('.')[0]
        if relation_name not in hookenv.relation_types():
            return None
    if relation_name:
        factory = relation_factory(relation_name)
        if factory:
            return factory.from_flag(flag)
    return None class Endpoint(RelationFactory):
    """
    New base class for creating interface layers.

    This class is intended to create drop-in, backwards-compatible replacements
    for interface layers previously written using the old
    :class:`~charms.reactive.relations.RelationBase` base class.  With the
    advantages of: having commonly used internal flags managed automatically,
    providing a cleaner, more easily understood pattern for interacting with
    relation data, and being able to use ``@when`` rather than ``@hook`` so
    that interface layers are more similar to charm layers and to remove one
    of the biggest barriers to upgrading from a non-reactive version of a
    charm to a reactive version.

    Four flags are automatically managed for each endpoint. Endpoint handlers
    can react to these flags using the :class:`~charms.reactive.decorators`.

      * ``endpoint.{endpoint_name}.joined`` is set when the endpoint is
        :meth:`joined`: when the first remote unit from any relationship
        connected to this endpoint joins. It is cleared when the last unit
        from all relationships connected to this endpoint departs.
      * ``endpoint.{endpoint_name}.changed`` when any relation data has
        changed. It isn't automatically cleared.
      * ``endpoint.{endpoint_name}.changed.{field}`` when a specific field
        has changed. It isn't automatically cleared.
      * ``endpoint.{endpoint_name}.departed`` when a remote unit is leaving.
         It isn't automatically cleared.

    For the flags that are not automatically cleared, it is up to the interface
    author to clear the flag when it is "handled". The following diagram shows
    how these flags relate. In summary, the ``joined`` flag represents the state
    of the relationship and will be automatically cleared when all units are
    gone. ``changed`` and ``departed`` represents relationship events and have to
    be cleared manually by the handler.

    .. image:: _static/endpoints-workflow.svg

    These flags should only be used by the decorators of the endpoint handlers.
    While it is possible to use them with any decorators in any layer, these
    flags should be considered internal, private implementation details. It is
    the interface layers responsibility to manage and document the public flags
    that make up part of its API.

    Endpoint handlers can iterate over the list of joined relations for an
    endpoint via the :attr:`~charms.reactive.endpoints.Endpoint.relations`
    collection.
    """

    _endpoints = {}

    @classmethod
    def from_name(cls, endpoint_name):
        """
        Return an Endpoint subclass instance based on the name of the endpoint.
        """
        return cls._endpoints.get(endpoint_name)

    @classmethod
    def from_flag(cls, flag):
        """
        Return an Endpoint subclass instance based on the given flag.

        The instance that is returned depends on the endpoint name embedded
        in the flag.  Flags should be of the form ``endpoint.{name}.extra...``,
        though for legacy purposes, the ``endpoint.`` prefix can be omitted.
        The ``{name}}`` portion will be passed to
        :meth:`~charms.reactive.endpoints.Endpoint.from_name`.

        If the flag is not set, an appropriate Endpoint subclass cannot be
        found, or the flag name can't be parsed, ``None`` will be returned.
        """
        if not is_flag_set(flag) or '.' not in flag:
            return None
        parts = flag.split('.')
        if parts[0] == 'endpoint':
            return cls.from_name(parts[1])
        else:
            # some older handlers might not use the 'endpoint' prefix
            return cls.from_name(parts[0])

    @classmethod
    def _startup(cls):
        """
        Create Endpoint instances and manage automatic flags.
        """
        for endpoint_name in sorted(hookenv.relation_types()):
            # populate context based on attached relations
            relf = relation_factory(endpoint_name)
            if not relf or not issubclass(relf, cls):
                continue

            rids = sorted(hookenv.relation_ids(endpoint_name))
            # ensure that relation IDs have the endpoint name prefix, in case
            # juju decides to drop it at some point
            rids = ['{}:{}'.format(endpoint_name, rid) if ':' not in rid
                    else rid for rid in rids]
            endpoint = relf(endpoint_name, rids)
            cls._endpoints[endpoint_name] = endpoint
            endpoint.register_triggers()
            endpoint._manage_departed()
            endpoint._manage_flags()
            for relation in endpoint.relations:
                hookenv.atexit(relation._flush_data)

    def __init__(self, endpoint_name, relation_ids=None):
        self._endpoint_name = endpoint_name
        self._relations = KeyList(map(Relation, relation_ids or []),
                                  key_attr='relation_id')
        self._all_joined_units = None
        self._all_departed_units = None

    def register_triggers(self):
        """
        Called once and only once for each named instance of this endpoint,
        before the endpoint's automatic flags are updated.

        This gives the endpoint implementation a chance to register triggers
        that will honor changes to the automatically managed flags.
        """
        pass

    @property
    def endpoint_name(self):
        """
        Relation name of this endpoint.
        """
        return self._endpoint_name

    @property
    def relations(self):
        """
        Collection of :class:`Relation` instances that are established for
        this :class:`Endpoint`.

        This is a :class:`KeyList`, so it can be iterated and indexed as a list,
        or you can look up relations by their ID.  For example::

            rel0 = endpoint.relations[0]
            assert rel0 is endpoint.relations[rel0.relation_id]
            assert all(rel is endpoint.relations[rel.relation_id]
                       for rel in endpoint.relations)
            print(', '.join(endpoint.relations.keys()))
        """
        return self._relations

    @property
    def is_joined(self):
        """
        Whether this endpoint has remote applications attached to it.
        """
        return len(self.all_units) > 0

    @property
    def joined(self):
        """
        .. deprecated:: 0.6.3
           Use :attr:`is_joined` instead
        """
        return self.is_joined

    def expand_name(self, flag):
        """
        Complete a flag for this endpoint by expanding the endpoint name.

        If the flag does not already contain ``{endpoint_name}``, it will be
        prefixed with ``endpoint.{endpoint_name}.``. Then, any occurance of
        ``{endpoint_name}`` will be replaced with ``self.endpoint_name``.
        """
        if '{endpoint_name}' not in flag:
            flag = 'endpoint.{endpoint_name}.' + flag
        # use replace rather than format to prevent any other braces or braced
        # strings from being touched
        return flag.replace('{endpoint_name}', self.endpoint_name)

    def _manage_departed(self):
        hook_name = hookenv.hook_name()
        rel_hook = hook_name.startswith(self.endpoint_name + '-relation-')
        departed_hook = rel_hook and hook_name.endswith('-departed')
        if not departed_hook:
            return
        relation = self.relations[hookenv.relation_id()]
        unit = RelatedUnit(relation, hookenv.remote_unit())
        self.all_departed_units.append(unit)
        if not relation.joined_units:
            del self.relations[relation.relation_id]

    def _manage_flags(self):
        """
        Manage automatic relation flags.

        Internal use only.
        """
        already_joined = is_flag_set(self.expand_name('joined'))
        hook_name = hookenv.hook_name()
        rel_hook = hook_name.startswith(self.endpoint_name + '-relation-')
        departed_hook = rel_hook and hook_name.endswith('-departed')

        toggle_flag(self.expand_name('joined'), self.is_joined)

        if departed_hook:
            set_flag(self.expand_name('departed'))
        elif self.is_joined:
            clear_flag(self.expand_name('departed'))

        if already_joined and not rel_hook:
            # skip checking relation data outside hooks for this relation
            # to save on API calls to the controller (unless we didn't have
            # the joined flag before, since then we might migrating to Endpoints)
            return

        for unit in self.all_units:
            for key, value in unit.received.items():
                data_key = 'endpoint.{}.{}.{}.{}'.format(self.endpoint_name,
                                                         unit.relation.relation_id,
                                                         unit.unit_name,
                                                         key)
                if data_changed(data_key, value):
                    set_flag(self.expand_name('changed'))
                    set_flag(self.expand_name('changed.{}'.format(key)))
        self.manage_flags()

    def manage_flags(self):
        """
        Method that subclasses can override to perform any flag management
        needed during startup.

        This will be called automatically after the framework-managed automatic
        flags have been updated.
        """
        pass

    @property
    def all_units(self):
        """
        .. deprecated:: 0.6.1
           Use :attr:`all_joined_units` instead
        """
        return self.all_joined_units

    @property
    def all_joined_units(self):
        """
        A list view of all the units of all relations attached to this
        :class:`~charms.reactive.endpoints.Endpoint`.

        This is actually a
        :class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units
        will be in order by relation ID and then unit name, and you can access a
        merged view of all the units' data as a single mapping.  You should be
        very careful when using the merged data collections, however, and
        consider carefully what will happen when the endpoint has multiple
        relations and multiple remote units on each.  It is probably better to
        iterate over each unit and handle its data individually.  See
        :class:`~charms.reactive.endpoints.CombinedUnitsView` for an
        explanation of how the merged data collections work.

        Note that, because a given application might be related multiple times
        on a given endpoint, units may show up in this collection more than
        once.
        """
        if self._all_joined_units is None:
            units = chain.from_iterable(rel.units for rel in self.relations)
            self._all_joined_units = CombinedUnitsView(units)
        return self._all_joined_units

    @property
    def all_departed_units(self):
        """
        Collection of all units that were previously part of any relation on
        this endpoint but which have since departed.

        This collection is persistent and mutable.  The departed units will
        be kept until they are explicitly removed, to allow for reasonable
        cleanup of units that have left.

        Example: You need to run a command each time a unit departs the relation.

        .. code-block:: python

            @when('endpoint.{endpoint_name}.departed')
            def handle_departed_unit(self):
                for name, unit in self.all_departed_units.items():
                    # run the command to remove `unit` from the cluster
                    #  ..
                self.all_departed_units.clear()
                clear_flag(self.expand_name('departed'))

        Once a unit is departed, it will no longer show up in
        :attr:`all_joined_units`.  Note that units are considered departed as
        soon as the departed hook is entered, which differs slightly from how
        the Juju primitives behave (departing units are still returned from
        ``related-units`` until after the departed hook is complete).

        This collection is a :class:`KeyList`, so can be used as a mapping to
        look up units by their unit name, or iterated or accessed by index.
        """
        if self._all_departed_units is None:
            self._all_departed_units = CachedKeyList.load(
                'reactive.endpoints.departed.{}'.format(self.endpoint_name),
                RelatedUnit._deserialize,
                'unit_name')
        return self._all_departed_units def _hook(hook_patterns):
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'hooks' and any_hook(*hook_patterns) def _restricted_hook(hook_name):
    current_hook = hookenv.hook_name()
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'restricted' and current_hook == hook_name def _when_all(flags):
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'other' and all_flags_set(*flags) def _when_any(flags):
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'other' and any_flags_set(*flags) def _when_none(flags):
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'other' and not any_flags_set(*flags) def _when_not_all(flags):
    dispatch_phase = unitdata.kv().get('reactive.dispatch.phase')
    return dispatch_phase == 'other' and not all_flags_set(*flags) def any_file_changed(filenames, hash_type='md5'):
    """
    Check if any of the given files have changed since the last time this
    was called.

    :param list filenames: Names of files to check. Accepts callables returning
        the filename.
    :param str hash_type: Algorithm to use to check the files.
    """
    changed = False
    for filename in filenames:
        if callable(filename):
            filename = str(filename())
        else:
            filename = str(filename)
        old_hash = unitdata.kv().get('reactive.files_changed.%s' % filename)
        new_hash = host.file_hash(filename, hash_type=hash_type)
        if old_hash != new_hash:
            unitdata.kv().set('reactive.files_changed.%s' % filename, new_hash)
            changed = True  # mark as changed, but keep updating hashes
    return changed def was_invoked(invocation_id):
    """
    Returns whether the given ID has been invoked before, as per :func:`mark_invoked`.

    This is useful for ensuring that a given block only runs one time::

        def foo():
            if was_invoked('foo'):
                return
            do_something()
            mark_invoked('foo')

    This is also available as a decorator at
    :func:`~charms.reactive.decorators.only_once`.
    """
    return unitdata.kv().get('reactive.invoked.%s' % invocation_id, False) def mark_invoked(invocation_id):
    """
    Mark the given ID as having been invoked, for use with :func:`was_invoked`.
    """
    unitdata.kv().set('reactive.invoked.%s' % invocation_id, True)
from functools import wraps, partial
from inspect import getfile, isclass, signature
from pathlib import Path
from charmhelpers.core import hookenv
from charms.reactive.bus import Handler
from charms.reactive.bus import _action_id
from charms.reactive.bus import _short_action_id
from charms.reactive.flags import get_flags
from charms.reactive.relations import endpoint_from_name
from charms.reactive.relations import endpoint_from_flag
from charms.reactive.endpoints import Endpoint
from charms.reactive.helpers import _hook
from charms.reactive.helpers import _restricted_hook
from charms.reactive.helpers import _when_all
from charms.reactive.helpers import _when_any
from charms.reactive.helpers import _when_none
from charms.reactive.helpers import _when_not_all
from charms.reactive.helpers import any_file_changed
from charms.reactive.helpers import was_invoked
from charms.reactive.helpers import mark_invoked
            # take care to not pass in multiple copies of self.
            if not handler.has_args:
                handler.add_args(map(endpoint_from_name, [endpoint_name]))
        elif has_params and legacy_args:
            # Handlers should all move to not taking any params and getting
            # the Endpoint instances from the context, but during the
            # transition, we need to provide for handlers expecting args.
            handler.add_args(filter(None, map(endpoint_from_flag, flags)))
        handler.register_flags(flags)
    return action


def when(*desired_flags):
    """
    Alias for :func:`~charms.reactive.decorators.when_all`.
    """
    return when_all(*desired_flags)


def when_all(*desired_flags):
    """
    Register the decorated function to run when all of ``desired_flags`` are active.

    Note that handlers whose conditions match are triggered at least once per
    hook invocation.

    For backwards compatibility, this decorator can pass arguments, but it is
    recommended to use argument-less handlers.  See
    `the summary <#charms-reactive-decorators>`_ for more information.
    """
    return partial(_when_decorator, _when_all, desired_flags, legacy_args=True)


def when_any(*desired_flags):
    """
    Register the decorated function to run when any of ``desired_flags`` are active.

    This decorator will never cause arguments to be passed to the handler.

    Note that handlers whose conditions match are triggered at least once per
    hook invocation.
    """
    return partial(_when_decorator, _when_any, desired_flags, legacy_args=False)


def when_not(*desired_flags):
    """
    Alias for :func:`~charms.reactive.decorators.when_none`.
    """
    return when_none(*desired_flags)


def when_none(*desired_flags):
    """
    Register the decorated function to run when none of ``desired_flags`` are
    active.

    This decorator will never cause arguments to be passed to the handler.

    Note that handlers whose conditions match are triggered at least once per
    hook invocation.
    """
    return partial(_when_decorator, _when_none, desired_flags, legacy_args=False)


def when_not_all(*desired_flags):
    """
    Register the decorated function to run when one or more of the
    ``desired_flags`` are not active.

    This decorator will never cause arguments to be passed to the handler.

    Note that handlers whose conditions match are triggered at least once per
    hook invocation.
    """
    return partial(_when_decorator, _when_not_all, desired_flags, legacy_args=False)


def when_file_changed(*filenames, **kwargs):
    """
    Register the decorated function to run when one or more files have changed.

    :param list filenames: The names of one or more files to check for changes
        (a callable returning the name is also accepted).
    :param str hash_type: The type of hash to use for determining if a file has
        changed.  Defaults to 'md5'.  Must be given as a kwarg.
    """
    def _register(action):
        handler = Handler.get(action)
        handler.add_predicate(partial(any_file_changed, filenames, **kwargs))
        return action
    return _register


def not_unless(*desired_flags):
    """
    Assert that the decorated function can only be called if the desired_flags
    are active.

    Note that, unlike :func:`when`, this does **not** trigger the decorated
    function if the flags match.  It **only** raises an exception if the
    function is called when the flags do not match.

    This is primarily for informational purposes and as a guard clause.
    """
    def _decorator(func):
        action_id = _action_id(func)
        short_action_id = _short_action_id(func)

        @wraps(func)
        def _wrapped(*args, **kwargs):
            active_flags = get_flags()
            missing_flags = [flag for flag in desired_flags if flag not in active_flags]
            if missing_flags:
                hookenv.log('%s called before flag%s: %s' % (
                    short_action_id,
                    's' if len(missing_flags) > 1 else '',
                    ', '.join(missing_flags)), hookenv.WARNING)
            return func(*args, **kwargs)
        _wrapped._action_id = action_id

        _wrapped._short_action_id = short_action_id