<gh_stars>0
from typing import Callable
import itertools as it
import warnings
import asyncio

from hautomate.util.async_ import Asyncable
from hautomate.context import Context
from hautomate.enums import IntentState
from hautomate.check import Cooldown
from hautomate.app import App


_intent_id = it.count()


class Intent(Asyncable):
    """
    Represents a work item to be done.

    Intents are the core building block of Hautomate. They are callable
    items similar to asyncio's Task, in that they hold an internal state
    and represent work that will be done in the future.
    """
    def __init__(self, event: str, fn: Callable, *, checks: list=None, limit: int=-1):
        super().__init__(fn)

        try:
            existing_checks = fn.__checks__
        except AttributeError:
            existing_checks = []
        finally:
            all_checks = [*existing_checks, *(checks or [])]
            checks = [c for c in all_checks if not isinstance(c, Cooldown)]
            cooldown = next((c for c in all_checks if isinstance(c, Cooldown)), None)

        self._id = next(_intent_id)
        self.event = event.upper()
        self.checks = checks
        self.cooldown = cooldown
        self.limit = limit
        self._app = None
        self._state = IntentState.initialized

        # internal statistics
        self.runs = 0
        self.last_ran = None

        # binding to an app
        if hasattr(self.func, '__self__') and isinstance(self.func.__self__, App):
            self._bind(self.func)

    def _bind(self, method: Callable) -> None:
        """
        Replace a class's function with a bound method.
        """
        self.func = method
        self._app = method.__self__
        self._app.intents.append(self)
        self._state = IntentState.ready

    def cancel(self):
        """
        Permanently cancel the Intent.

        A cancelled Intent will not fire.
        """
        self._state = IntentState.cancelled

    def pause(self):
        """
        Set the Intent to paused.

        A paused Intent will not fire.
        """
        if self._state == IntentState.cancelled:
            warnings.warn('intent is cancelled and cannot be paused!', RuntimeWarning)
            return

        self._state = IntentState.paused

    def unpause(self):
        """
        Set the Intent to ready.
        """
        if self._state == IntentState.cancelled:
            warnings.warn('intent is cancelled and cannot be unpaused!', RuntimeWarning)
            return

        self._state = IntentState.ready

    async def can_run(self, ctx: Context, *a, **kw) -> bool:
        """
        Determine if the intent can run.
        """
        if self._state in (IntentState.paused, IntentState.cancelled):
            return False

        if self.runs >= self.limit > 0:
            return False

        if not await self._all_checks_pass(ctx):
            return False

        return True

    async def _all_checks_pass(self, ctx: Context) -> bool:
        """
        Determine if this Intent passes its checks.

        The list of checks is scheduled concurrently, but evaluated
        eagerly. This allows the Intent to fail fast in case of a long
        line of checks. Only once all checks have passed, the cooldown
        is evaluated - which keeps the cooldown from being evaluated if
        the Intent isn't meant to run in the first place.
        """
        pending = [check(ctx) for check in self.checks]

        # this is basically asyncio.as_completed, but with the ability to
        # cancel any checks that are still running in the background.
        while pending:
            done, pending = await asyncio.wait(pending, return_when='FIRST_COMPLETED')

            if not all((f.result() for f in done)):
                {p.cancel() for p in pending}
                return False

        if self.cooldown is not None and not await self.cooldown(ctx):
            return False

        return True

    async def __runner__(self, ctx: Context, *a, **kw):
        """
        Akin to __call__, but used in an async context and tracks
        statistics on the Intent itself.
        """
        # break race condition where more than 1 copy of an Intent can
        # qualify all check during the same iteration of the event loop,
        # but only one should actually run.
        if self.runs + 1 > self.limit > 0:
            return

        self.runs += 1
        self.last_ran = ctx.hauto.now
        return await super().__call__(ctx, *a, loop=ctx.hauto.loop, **kw)

    __call__ = __runner__

    def __repr__(self):
        fn_name = (
            getattr(self.func, '__qualname__', None)
            or getattr(self.func, '__name__', None)
            or str(self.func)
        )

        if self._app is not None:
            name = fn_name
        else:
            name = f'unbound {fn_name}'

        return f'<Intent {name}>'

from typing import Callable
import itertools as it
import warnings
import asyncio

from hautomate.util.async_ import Asyncable
from hautomate.context import Context
from hautomate.enums import IntentState
from hautomate.check import Cooldown
from hautomate.app import App


_intent_id = it.count()


class Intent(Asyncable):
    """
    Represents a work item to be done.

    Intents are the core building block of Hautomate. They are callable
    items similar to asyncio's Task, in that they hold an internal state
    and represent work that will be done in the future.
    """
    def __init__(self, event: str, fn: Callable, *, checks: list=None, limit: int=-1):
        super().__init__(fn)

        try:
            existing_checks = fn.__checks__
        except AttributeError:
            existing_checks = []
        finally:
            all_checks = [*existing_checks, *(checks or [])]
            checks = [c for c in all_checks if not isinstance(c, Cooldown)]
            cooldown = next((c for c in all_checks if isinstance(c, Cooldown)), None)

        self._id = next(_intent_id)
        self.event = event.upper()
        self.checks = checks
        self.cooldown = cooldown
        self.limit = limit
        self._app = None
        self._state = IntentState.initialized

        # internal statistics
        self.runs = 0
        self.last_ran = None

        # binding to an app
        if hasattr(self.func, '__self__') and isinstance(self.func.__self__, App):
            self._bind(self.func)

    def _bind(self, method: Callable) -> None:
        """
        Replace a class's function with a bound method.
        """
        self.func = method
        self._app = method.__self__
        self._app.intents.append(self)
        self._state = IntentState.ready

    def cancel(self):
        """
        Permanently cancel the Intent.

        A cancelled Intent will not fire.
        """
        self._state = IntentState.cancelled

    def pause(self):
        """
        Set the Intent to paused.

        A paused Intent will not fire.
        """
        if self._state == IntentState.cancelled:
            warnings.warn('intent is cancelled and cannot be paused!', RuntimeWarning)
            return

        self._state = IntentState.paused

    def unpause(self):
        """
        Set the Intent to ready.
        """
        if self._state == IntentState.cancelled:
            warnings.warn('intent is cancelled and cannot be unpaused!', RuntimeWarning)
            return

        self._state = IntentState.ready

    async def can_run(self, ctx: Context, *a, **kw) -> bool:
        """
        Determine if the intent can run.
        """
        if self._state in (IntentState.paused, IntentState.cancelled):
            return False

        if self.runs >= self.limit > 0:
            return False

        if not await self._all_checks_pass(ctx):
            return False

        return True

    async def _all_checks_pass(self, ctx: Context) -> bool:
        """
        Determine if this Intent passes its checks.

        The list of checks is scheduled concurrently, but evaluated
        eagerly. This allows the Intent to fail fast in case of a long
        line of checks. Only once all checks have passed, the cooldown
        is evaluated - which keeps the cooldown from being evaluated if
        the Intent isn't meant to run in the first place.
        """
        pending = [check(ctx) for check in self.checks]

        # this is basically asyncio.as_completed, but with the ability to
        # cancel any checks that are still running in the background.
        while pending:
            done, pending = await asyncio.wait(pending, return_when='FIRST_COMPLETED')

            if not all((f.result() for f in done)):
                {p.cancel() for p in pending}
                return False

        if self.cooldown is not None and not await self.cooldown(ctx):
            return False

        return True

    async def __runner__(self, ctx: Context, *a, **kw):
        """
        Akin to __call__, but used in an async context and tracks
        statistics on the Intent itself.
        """
        # break race condition where more than 1 copy of an Intent can
        # qualify all check during the same iteration of the event loop,
        # but only one should actually run.
        if self.runs + 1 > self.limit > 0:
            return

        self.runs += 1
        self.last_ran = ctx.hauto.now
        return await super().__call__(ctx, *a, loop=ctx.hauto.loop, **kw)

    __call__ = __runner__

    def __repr__(self):
        fn_name = (
            getattr(self.func, '__qualname__', None)
            or getattr(self.func, '__name__', None)
            or str(self.func)
        )

        if self._app is not None:
            name = fn_name
        else:
            name = f'unbound {fn_name}'

        return f'<Intent {name}>'
Represents a work item to be done.      Intents are the core building block of Hautomate. They are callable     items similar to asyncio's Task, in that they hold an internal state     and represent work that will be done in the future. # internal statistics # binding to an app Replace a class's function with a bound method. Permanently cancel the Intent.          A cancelled Intent will not fire. Set the Intent to paused.          A paused Intent will not fire. Set the Intent to ready. Determine if the intent can run. Determine if this Intent passes its checks.          The list of checks is scheduled concurrently, but evaluated         eagerly. This allows the Intent to fail fast in case of a long         line of checks. Only once all checks have passed, the cooldown         is evaluated - which keeps the cooldown from being evaluated if         the Intent isn't meant to run in the first place. # this is basically asyncio.as_completed, but with the ability to # cancel any checks that are still running in the background. Akin to __call__, but used in an async context and tracks         statistics on the Intent itself. # break race condition where more than 1 copy of an Intent can # qualify all check during the same iteration of the event loop, # but only one should actually run.