import logging
import threading
import enum
import concurrent.futures

all = {}
lock = threading.Lock()
logger = logging.getLogger("conversations")


class Conversation(object):
    # States should typically transition as per the listed order
    # STICKER may hijack the order if the user sends a new sticker
    #   and cancels the previous chain.
    # LABEL may repeat if user replies negatively to confirmation
    class State(enum.Enum):  # TODO Add pending confirmation state
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker

    @property
    def labels(self):
        return self._labels

    @labels.setter
    def labels(self, labels):
        self._labels = labels

    # Returns True if state is None and self.state is IDLE
    #                 state is None and future is done
    #                 state is None and there is no future
    #                 state matches and future is done
    #                 state matches and there is no future
    def state_complete(self, state=None):
        if state and self.state != state:
            return False

        if self._future:
            return self._future.done()

        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE

    def reset_state(self):
        with self._lock:
            self.state = Conversation.State.IDLE
            if self._future:
                self._future.cancel()
            self._future = None

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future

    # Change state to typical previous state, unless otherwise specified
    # If specifying future, state must also be specified
    def rollback_state(self, state=None, future=None):
        if future and not state:
            raise ValueError()

        with self._lock:
            if state:
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                self.state = state

            if future:
                self._future = future

            if state is None and future is None:
                if self._future:
                    self._future.cancel()

                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None

                if self.state != Conversation.State.IDLE:
                    self.state = Conversation.State(self.state.value - 1)

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return

        # Block until previous state's action is complete
        elif self._future:
            while not self._future.done():
                pass

        # Enforce state transition order
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert (self.state == Conversation.State.STICKER or
                        self.state == Conversation.State.LABEL)
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)

        logger = logging.getLogger("conversation." + str(self.user.id))
        logger.debug("Transiting from " + str(self.state) +
                     " to " + str(new_state))

        self.__change_state(new_state, future)

    def update_future(self, new_future, force=False):
        if force:
            with self._lock:
                if self._future:
                    self._future.cancel()
                self._future = new_future

        if not force:
            if self._future:
                while not self._future.done():
                    pass
            with self._lock:
                self._future = new_future

    # timeout in seconds
    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError("Conversation has no future.")
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


def get_or_create(telegram_user, get_only=False, chat=None):
    conversation = None
    user_id = telegram_user.id

    if not (get_only or chat):
        raise ValueError("Pass in chat or get_only=True")

    with lock:
        logger.debug("Acquired conversations lock")
        if user_id in all:
            logger.debug("User " + str(user_id) + " found")
            conversation = all[user_id]
        elif not get_only:
            logger.debug("Creating new conversation")
            conversation = Conversation(telegram_user, chat)
            all[user_id] = conversation

    return conversation

f0cd6fc8497ed515e59addf9cad236b79b5ba76c
<mask token>


class Conversation(object):


    class State(enum.Enum):
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker
    <mask token>
    <mask token>

    def state_complete(self, state=None):
        if state and self.state != state:
            return False
        if self._future:
            return self._future.done()
        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE
    <mask token>

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future
    <mask token>

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return
        elif self._future:
            while not self._future.done():
                pass
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert self.state == Conversation.State.STICKER or self.state == Conversation.State.LABEL
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)
        logger = logging.getLogger('conversation.' + str(self.user.id))
        logger.debug('Transiting from ' + str(self.state) + ' to ' + str(
            new_state))
        self.__change_state(new_state, future)
    <mask token>

    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError('Conversation has no future.')
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


<mask token>

<mask token>


class Conversation(object):


    class State(enum.Enum):
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker
    <mask token>
    <mask token>

    def state_complete(self, state=None):
        if state and self.state != state:
            return False
        if self._future:
            return self._future.done()
        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE

    def reset_state(self):
        with self._lock:
            self.state = Conversation.State.IDLE
            if self._future:
                self._future.cancel()
            self._future = None

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future

    def rollback_state(self, state=None, future=None):
        if future and not state:
            raise ValueError()
        with self._lock:
            if state:
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                self.state = state
            if future:
                self._future = future
            if state is None and future is None:
                if self._future:
                    self._future.cancel()
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                if self.state != Conversation.State.IDLE:
                    self.state = Conversation.State(self.state.value - 1)

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return
        elif self._future:
            while not self._future.done():
                pass
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert self.state == Conversation.State.STICKER or self.state == Conversation.State.LABEL
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)
        logger = logging.getLogger('conversation.' + str(self.user.id))
        logger.debug('Transiting from ' + str(self.state) + ' to ' + str(
            new_state))
        self.__change_state(new_state, future)

    def update_future(self, new_future, force=False):
        if force:
            with self._lock:
                if self._future:
                    self._future.cancel()
                self._future = new_future
        if not force:
            if self._future:
                while not self._future.done():
                    pass
            with self._lock:
                self._future = new_future

    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError('Conversation has no future.')
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


<mask token>

<mask token>
all = {}
lock = threading.Lock()
logger = logging.getLogger('conversations')


class Conversation(object):


    class State(enum.Enum):
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker

    @property
    def labels(self):
        return self._labels

    @labels.setter
    def labels(self, labels):
        self._labels = labels

    def state_complete(self, state=None):
        if state and self.state != state:
            return False
        if self._future:
            return self._future.done()
        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE

    def reset_state(self):
        with self._lock:
            self.state = Conversation.State.IDLE
            if self._future:
                self._future.cancel()
            self._future = None

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future

    def rollback_state(self, state=None, future=None):
        if future and not state:
            raise ValueError()
        with self._lock:
            if state:
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                self.state = state
            if future:
                self._future = future
            if state is None and future is None:
                if self._future:
                    self._future.cancel()
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                if self.state != Conversation.State.IDLE:
                    self.state = Conversation.State(self.state.value - 1)

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return
        elif self._future:
            while not self._future.done():
                pass
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert self.state == Conversation.State.STICKER or self.state == Conversation.State.LABEL
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)
        logger = logging.getLogger('conversation.' + str(self.user.id))
        logger.debug('Transiting from ' + str(self.state) + ' to ' + str(
            new_state))
        self.__change_state(new_state, future)

    def update_future(self, new_future, force=False):
        if force:
            with self._lock:
                if self._future:
                    self._future.cancel()
                self._future = new_future
        if not force:
            if self._future:
                while not self._future.done():
                    pass
            with self._lock:
                self._future = new_future

    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError('Conversation has no future.')
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


def get_or_create(telegram_user, get_only=False, chat=None):
    conversation = None
    user_id = telegram_user.id
    if not (get_only or chat):
        raise ValueError('Pass in chat or get_only=True')
    with lock:
        logger.debug('Acquired conversations lock')
        if user_id in all:
            logger.debug('User ' + str(user_id) + ' found')
            conversation = all[user_id]
        elif not get_only:
            logger.debug('Creating new conversation')
            conversation = Conversation(telegram_user, chat)
            all[user_id] = conversation
    return conversation

import logging
import threading
import enum
import concurrent.futures
all = {}
lock = threading.Lock()
logger = logging.getLogger('conversations')


class Conversation(object):


    class State(enum.Enum):
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker

    @property
    def labels(self):
        return self._labels

    @labels.setter
    def labels(self, labels):
        self._labels = labels

    def state_complete(self, state=None):
        if state and self.state != state:
            return False
        if self._future:
            return self._future.done()
        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE

    def reset_state(self):
        with self._lock:
            self.state = Conversation.State.IDLE
            if self._future:
                self._future.cancel()
            self._future = None

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future

    def rollback_state(self, state=None, future=None):
        if future and not state:
            raise ValueError()
        with self._lock:
            if state:
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                self.state = state
            if future:
                self._future = future
            if state is None and future is None:
                if self._future:
                    self._future.cancel()
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                if self.state != Conversation.State.IDLE:
                    self.state = Conversation.State(self.state.value - 1)

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return
        elif self._future:
            while not self._future.done():
                pass
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert self.state == Conversation.State.STICKER or self.state == Conversation.State.LABEL
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)
        logger = logging.getLogger('conversation.' + str(self.user.id))
        logger.debug('Transiting from ' + str(self.state) + ' to ' + str(
            new_state))
        self.__change_state(new_state, future)

    def update_future(self, new_future, force=False):
        if force:
            with self._lock:
                if self._future:
                    self._future.cancel()
                self._future = new_future
        if not force:
            if self._future:
                while not self._future.done():
                    pass
            with self._lock:
                self._future = new_future

    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError('Conversation has no future.')
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


def get_or_create(telegram_user, get_only=False, chat=None):
    conversation = None
    user_id = telegram_user.id
    if not (get_only or chat):
        raise ValueError('Pass in chat or get_only=True')
    with lock:
        logger.debug('Acquired conversations lock')
        if user_id in all:
            logger.debug('User ' + str(user_id) + ' found')
            conversation = all[user_id]
        elif not get_only:
            logger.debug('Creating new conversation')
            conversation = Conversation(telegram_user, chat)
            all[user_id] = conversation
    return conversation

import logging
import threading
import enum
import concurrent.futures

all = {}
lock = threading.Lock()
logger = logging.getLogger("conversations")


class Conversation(object):
    # States should typically transition as per the listed order
    # STICKER may hijack the order if the user sends a new sticker
    #   and cancels the previous chain.
    # LABEL may repeat if user replies negatively to confirmation
    class State(enum.Enum):  # TODO Add pending confirmation state
        IDLE = 0
        STICKER = 1
        LABEL = 2
        CONFIRMED = 3

    def __init__(self, user, chat):
        self.user = user
        self.chat = chat
        self.state = Conversation.State.IDLE
        self._lock = threading.Lock()
        self._sticker = None
        self._labels = None
        self._future = None

    @property
    def sticker(self):
        return self._sticker

    @sticker.setter
    def sticker(self, sticker):
        self._sticker = sticker

    @property
    def labels(self):
        return self._labels

    @labels.setter
    def labels(self, labels):
        self._labels = labels

    # Returns True if state is None and self.state is IDLE
    #                 state is None and future is done
    #                 state is None and there is no future
    #                 state matches and future is done
    #                 state matches and there is no future
    def state_complete(self, state=None):
        if state and self.state != state:
            return False

        if self._future:
            return self._future.done()

        return True

    def is_idle(self):
        return self.state == Conversation.State.IDLE

    def reset_state(self):
        with self._lock:
            self.state = Conversation.State.IDLE
            if self._future:
                self._future.cancel()
            self._future = None

    def __change_state(self, new_state, future):
        with self._lock:
            self.state = new_state
            if self._future:
                self._future.cancel()
            self._future = future

    # Change state to typical previous state, unless otherwise specified
    # If specifying future, state must also be specified
    def rollback_state(self, state=None, future=None):
        if future and not state:
            raise ValueError()

        with self._lock:
            if state:
                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None
                self.state = state

            if future:
                self._future = future

            if state is None and future is None:
                if self._future:
                    self._future.cancel()

                if self.state == Conversation.State.STICKER:
                    self.sticker = None
                elif self.state == Conversation.State.LABEL:
                    self.labels = None

                if self.state != Conversation.State.IDLE:
                    self.state = Conversation.State(self.state.value - 1)

    def change_state(self, new_state, future=None, force=False):
        if force:
            self.__change_state(new_state, future)
            return

        # Block until previous state's action is complete
        elif self._future:
            while not self._future.done():
                pass

        # Enforce state transition order
        try:
            if new_state == Conversation.State.STICKER:
                assert self.state == Conversation.State.IDLE
            elif new_state == Conversation.State.LABEL:
                assert (self.state == Conversation.State.STICKER or
                        self.state == Conversation.State.LABEL)
            elif new_state == Conversation.State.CONFIRMED:
                assert self.state == Conversation.State.LABEL
        except AssertionError:
            raise ValueError(self.state)

        logger = logging.getLogger("conversation." + str(self.user.id))
        logger.debug("Transiting from " + str(self.state) +
                     " to " + str(new_state))

        self.__change_state(new_state, future)

    def update_future(self, new_future, force=False):
        if force:
            with self._lock:
                if self._future:
                    self._future.cancel()
                self._future = new_future

        if not force:
            if self._future:
                while not self._future.done():
                    pass
            with self._lock:
                self._future = new_future

    # timeout in seconds
    def get_future_result(self, timeout=3):
        if not self._future:
            raise ValueError("Conversation has no future.")
        try:
            return self._future.result(timeout=timeout)
        except concurrent.futures.TimeoutError:
            return None


def get_or_create(telegram_user, get_only=False, chat=None):
    conversation = None
    user_id = telegram_user.id

    if not (get_only or chat):
        raise ValueError("Pass in chat or get_only=True")

    with lock:
        logger.debug("Acquired conversations lock")
        if user_id in all:
            logger.debug("User " + str(user_id) + " found")
            conversation = all[user_id]
        elif not get_only:
            logger.debug("Creating new conversation")
            conversation = Conversation(telegram_user, chat)
            all[user_id] = conversation

    return conversation
