Exemple #1
0
    def __init__(self):
        # NOTE: the rx type hints don't actually work https://github.com/ReactiveX/RxPY/issues/514
        self.door_states = Subject()  # Subject[DoorState]
        self.current_door_states: Dict[str, DoorState] = {}
        self._save_event(
            self.door_states,
            self.current_door_states,
            lambda x: x.door_name,
        )

        self.door_health = Subject()  # Subject[DoorHealth]
        self.current_door_health: Dict[str, ttm.DoorHealth] = {}
        self._save_event(
            self.door_health,
            self.current_door_health,
            lambda x: x.id_,
        )

        self.lift_states = Subject()  # Subject[LiftState]
        self.current_lift_states: Dict[str, LiftState] = {}
        self._save_event(
            self.lift_states,
            self.current_lift_states,
            lambda x: x.lift_name,
        )

        self.lift_health = Subject()  # Subject[LiftHealth]
        self.current_lift_health: Dict[str, ttm.LiftHealth] = {}
        self._save_event(
            self.lift_health,
            self.current_lift_health,
            lambda x: x.id_,
        )

        self.dispenser_states = Subject()  # Subject[DispenserState]
        self.current_dispenser_states: Dict[str, DispenserState] = {}
        self._save_event(
            self.dispenser_states,
            self.current_dispenser_states,
            lambda x: x.guid,
        )

        self.dispenser_health = Subject()  # Subject[DispenserHealth]
        self.current_dispenser_health: Dict[str, ttm.DispenserHealth] = {}
        self._save_event(
            self.dispenser_health,
            self.current_dispenser_health,
            lambda x: x.id_,
        )

        self.ingestor_states = Subject()  # Subject[IngestorState]
        self.current_ingestor_states: Dict[str, IngestorState] = {}
        self._save_event(
            self.ingestor_states,
            self.current_ingestor_states,
            lambda x: x.guid,
        )

        self.ingestor_health = Subject()  # Subject[IngestorHealth]
        self.current_ingestor_health: Dict[str, ttm.IngestorHealth] = {}
        self._save_event(
            self.ingestor_health,
            self.current_ingestor_health,
            lambda x: x.id_,
        )

        self.fleet_states = Subject()  # Subject[FleetState]
        self.current_fleet_states: Dict[str, FleetState] = {}
        self._save_event(
            self.fleet_states,
            self.current_fleet_states,
            lambda x: x.name,
        )

        self.robot_health = Subject()  # Subject[RobotHealth]
        self.current_robot_health: Dict[str, ttm.RobotHealth] = {}
        self._save_event(
            self.robot_health,
            self.current_robot_health,
            lambda x: x.id_,
        )

        self.task_summaries = Subject()  # Subject[TaskSummary]
        self.current_task_summaries: Dict[str, TaskSummary] = {}
        self._init_task_summaries()

        # BehaviorSubject[Optional[RmfBuildingMap]]
        self.rmf_building_map = BehaviorSubject(None)

        # BehaviorSubject[Optional[Dict]], a dict containing fields same as RmfBuildingMap,
        # but with the image data changed to an url.
        self.building_map = BehaviorSubject(None)
Exemple #2
0
class Client(threading.Thread):
    connection: websockets.WebSocketClientProtocol

    def __init__(self,
                 project: Optional[str] = None,
                 account: Optional[str] = None,
                 try_pick_up=False,
                 parent_experiment=None,
                 silent=False):
        self.connected = BehaviorSubject(False)
        self.project = project
        self.account = account
        self.parent_experiment = parent_experiment
        self.silent = silent

        self.host = os.environ.get('DEEPKIT_HOST', '127.0.0.1')
        self.socket_path = os.environ.get('DEEPKIT_SOCKET', None)
        self.ssl = os.environ.get('DEEPKIT_SSL', '0') is '1'
        self.port = int(os.environ.get('DEEPKIT_PORT', '8960'))

        self.job_token = None
        self.job_id = None

        if try_pick_up:
            # is set by Deepkit cli
            self.job_token = os.environ.get('DEEPKIT_JOB_ACCESSTOKEN', None)
            self.job_id = os.environ.get('DEEPKIT_JOB_ID', None)

        # is set by deepkit.login()
        self.token = os.environ.get('DEEPKIT_ACCESSTOKEN', None)

        self.result_status = None

        self.message_id = 0
        self.callbacks: Dict[int, asyncio.Future] = {}
        self.subscriber: Dict[int, any] = {}
        self.stopping = False
        self.queue = []
        self.controllers = {}
        self.patches = {}
        self.offline = False
        self.connections = 0
        self.lock = threading.Lock()
        threading.Thread.__init__(self)
        self.daemon = True
        self.loop = asyncio.new_event_loop()
        self.start()

    def is_connected(self):
        return self.connected.value

    def run(self):
        self.connecting = self.loop.create_future()
        self.loop.run_forever()

    def connect(self):
        asyncio.run_coroutine_threadsafe(self._connect(), self.loop)

    def connect_anon(self):
        asyncio.run_coroutine_threadsafe(self._connect_anon(),
                                         self.loop).result()

    def shutdown(self):
        if self.offline: return
        promise = asyncio.run_coroutine_threadsafe(self.stop_and_sync(),
                                                   self.loop)
        promise.result()
        if not self.connection.closed:
            raise Exception('Connection still active')
        self.loop.stop()

    async def stop_and_sync(self):
        self.stopping = True

        if deepkit.utils.in_self_execution() or self.result_status:
            # only when we are in self execution do we set status, time stamps etc
            # otherwise the CLI is doing that and the server. Or when
            # the experiment set result_state explicitly.

            # done = 150, //when all tasks are done
            # aborted = 200, //when at least one task aborted
            # failed = 250, //when at least one task failed
            # crashed = 300, //when at least one task crashed
            self.patches['status'] = 150
            self.patches['ended'] = datetime.datetime.utcnow()
            self.patches['tasks.main.ended'] = datetime.datetime.utcnow()

            # done = 500,
            # aborted = 550,
            # failed = 600,
            # crashed = 650,
            self.patches['tasks.main.status'] = 500
            self.patches[
                'tasks.main.instances.0.ended'] = datetime.datetime.utcnow()

            # done = 500,
            # aborted = 550,
            # failed = 600,
            # crashed = 650,
            self.patches['tasks.main.instances.0.status'] = 500

            if hasattr(sys, 'last_value'):
                if isinstance(sys.last_value, KeyboardInterrupt):
                    self.patches['status'] = 200
                    self.patches['tasks.main.status'] = 550
                    self.patches['tasks.main.instances.0.status'] = 550
                else:
                    self.patches['status'] = 300
                    self.patches['tasks.main.status'] = 650
                    self.patches['tasks.main.instances.0.status'] = 650

            if self.result_status:
                self.patches['status'] = self.result_status.value

        while len(self.patches) > 0 or len(self.queue) > 0:
            await asyncio.sleep(0.15)

        await self.connection.close()

    def register_controller(self, name: str, controller):
        return asyncio.run_coroutine_threadsafe(
            self._register_controller(name, controller), self.loop)

    async def _register_controller(self, name: str, controller):
        self.controllers[name] = controller

        async def handle_peer_message(message, done):
            if message['type'] == 'error':
                done()
                del self.controllers[name]
                raise Exception('Register controller error: ' +
                                message['error'])

            if message['type'] == 'ack':
                pass

            if message['type'] == 'peerController/message':
                data = message['data']

                if not hasattr(controller, data['action']):
                    error = f"Requested action {message['action']} not available in {name}"
                    print(error, file=sys.stderr)
                    await self._message(
                        {
                            'name': 'peerController/message',
                            'controllerName': name,
                            'clientId': message['clientId'],
                            'data': {
                                'type': 'error',
                                'id': data['id'],
                                'stack': None,
                                'entityName': '@error:default',
                                'error': error
                            }
                        },
                        no_response=True)

                if data['name'] == 'actionTypes':
                    parameters = []

                    i = 0
                    for arg in inspect.getfullargspec(
                            getattr(controller, data['action'])).args:
                        parameters.append({
                            'type': 'any',
                            'name': '#' + str(i)
                        })
                        i += 1

                    await self._message(
                        {
                            'name': 'peerController/message',
                            'controllerName': name,
                            'clientId': message['clientId'],
                            'data': {
                                'type': 'actionTypes/result',
                                'id': data['id'],
                                'parameters': parameters,
                                'returnType': {
                                    'type': 'any',
                                    'name': 'result'
                                }
                            }
                        },
                        no_response=True)

                if data['name'] == 'action':
                    try:
                        res = await getattr(controller,
                                            data['action'])(*data['args'])

                        await self._message(
                            {
                                'name': 'peerController/message',
                                'controllerName': name,
                                'clientId': message['clientId'],
                                'data': {
                                    'type': 'next/json',
                                    'id': data['id'],
                                    'encoding': {
                                        'name': 'r',
                                        'type': 'any'
                                    },
                                    'next': res,
                                }
                            },
                            no_response=True)
                    except Exception as e:
                        await self._message(
                            {
                                'name': 'peerController/message',
                                'controllerName': name,
                                'clientId': message['clientId'],
                                'data': {
                                    'type': 'error',
                                    'id': data['id'],
                                    'stack': None,
                                    'entityName': '@error:default',
                                    'error': str(e)
                                }
                            },
                            no_response=True)

        def subscriber(message, on_done):
            self.loop.create_task(handle_peer_message(message, on_done))

        await self._subscribe(
            {
                'name': 'peerController/register',
                'controllerName': name,
            }, subscriber)

        class Controller:
            def __init__(self, client):
                self.client = client

            def stop(self):
                self.client._message(
                    {
                        'name': 'peerController/unregister',
                        'controllerName': name,
                    },
                    no_response=True)

        return Controller(self)

    async def _action(self,
                      controller: str,
                      action: str,
                      args=None,
                      lock=True,
                      allow_in_shutdown=False):
        if args is None:
            args = []

        if lock: await self.connecting
        if self.offline: return
        if self.stopping and not allow_in_shutdown:
            raise Exception('In shutdown: actions disallowed')

        if not controller: raise Exception('No controller given')
        if not action: raise Exception('No action given')

        # print('> action', action, threading.current_thread().name)
        res = await self._message(
            {
                'name': 'action',
                'controller': controller,
                'action': action,
                'args': args,
                'timeout': 60
            },
            lock=lock)

        # print('< action', action)

        if res['type'] == 'next/json':
            return res['next'] if 'next' in res else None

        if res['type'] == 'error':
            print(res, file=sys.stderr)
            raise ApiError('API Error: ' + str(res['error']))

        raise ApiError(f"Invalid action type '{res['type']}'. Not implemented")

    def app_action_threadsafe(self, action: str, args=None) -> Future:
        if args is None: args = []
        return asyncio.run_coroutine_threadsafe(
            self._action('app', action, args), self.loop)

    async def job_action(self, action: str, args=None):
        return await self._action('job', action, args)

    def job_action_threadsafe(self, action: str, args=None) -> Future:
        """
        This method is non-blocking and every try to block-wait for an answers means
        script execution stops when connection is broken (offline training entirely impossible).
        So, we just schedule the call and return a Future, which the user can subscribe to.
        """
        if args is None: args = []
        return asyncio.run_coroutine_threadsafe(
            self._action('job', action, args), self.loop)

    async def _subscribe(self, message, subscriber):
        await self.connecting

        self.message_id += 1
        message['id'] = self.message_id

        message_id = self.message_id

        def on_done():
            del self.subscriber[message_id]

        def on_incoming_message(incoming_message):
            subscriber(incoming_message, on_done)

        self.subscriber[self.message_id] = on_incoming_message
        self.queue.append(message)

    def _create_message(self,
                        message: dict,
                        lock=True,
                        no_response=False) -> dict:
        self.message_id += 1
        message['id'] = self.message_id
        if not no_response:
            self.callbacks[self.message_id] = self.loop.create_future()

        return message

    async def _message(self, message, lock=True, no_response=False):
        if lock: await self.connecting

        message = self._create_message(message, no_response=no_response)
        self.queue.append(message)

        if no_response:
            return

        return await self.callbacks[self.message_id]

    def patch(self, path: str, value: any):
        if self.offline: return
        if self.stopping: return

        self.patches[path] = value

    async def send_messages(self, connection):
        while not connection.closed:
            try:
                q = self.queue[:]
                for m in q:
                    try:
                        j = json.dumps(m, default=json_converter)
                    except TypeError as e:
                        print('Could not send message since JSON error',
                              e,
                              m,
                              file=sys.stderr)
                        continue
                    await connection.send(j)
                    self.queue.remove(m)
            except Exception as e:
                print("Failed sending, exit send_messages", file=sys.stderr)
                raise e

            if len(self.patches) > 0:
                # we have to send first all messages/actions out
                # before sending patches, as most of the time
                # patches are based on previously created entities,
                # so we need to make sure those entities are created
                # first before sending any patches.
                # print('patches', self.patches)
                try:
                    send = self.patches.copy()
                    await connection.send(
                        json.dumps(
                            {
                                'name': 'action',
                                'controller': 'job',
                                'action': 'patchJob',
                                'args': [send],
                                'timeout': 60
                            },
                            default=json_converter))

                    for i in send.keys():
                        if self.patches[i] == send[i]:
                            del self.patches[i]
                except websockets.exceptions.ConnectionClosed:
                    return
                except ApiError:
                    print("Patching failed. Syncing job data disabled.",
                          file=sys.stderr)
                    return

            await asyncio.sleep(0.5)

    async def handle_messages(self, connection):
        while not connection.closed:
            try:
                res = json.loads(await connection.recv())
            except websockets.exceptions.ConnectionClosedError:
                # we need reconnect
                break
            except websockets.exceptions.ConnectionClosedOK:
                # we closed on purpose, so no reconnect necessary
                return

            if res and 'id' in res:
                if res['id'] in self.subscriber:
                    self.subscriber[res['id']](res)

                if res['id'] in self.callbacks:
                    self.callbacks[res['id']].set_result(res)
                    del self.callbacks[res['id']]

        if not self.stopping:
            self.log("Deepkit: lost connection. reconnect ...")
            self.connecting = self.loop.create_future()
            self.connected.on_next(False)
            self.loop.create_task(self._connect())

    async def _connected(self, id: str, token: str):
        try:
            if self.socket_path:
                self.connection = await websockets.unix_connect(
                    self.socket_path)
            else:
                ws = 'wss' if self.ssl else 'ws'
                url = f"{ws}://{self.host}:{self.port}"
                self.connection = await websockets.connect(url)
        except Exception as e:
            # try again later
            self.log('Unable to connect', e)
            await asyncio.sleep(1)
            self.loop.create_task(self._connect())
            return

        self.loop.create_task(self.handle_messages(self.connection))
        # we don't use send_messages() since this would send all queue/patches
        # which would lead to permission issues when we're not first authenticated

        if token:
            message = self._create_message(
                {
                    'name': 'authenticate',
                    'token': {
                        'id': 'job',
                        'token': token,
                        'job': id
                    }
                },
                lock=False)

            await self.connection.send(
                json.dumps(message, default=json_converter))

            res = await self.callbacks[message['id']]
            if not res['result'] or res['result'] is not True:
                raise Exception('Job token invalid')

        self.loop.create_task(self.send_messages(self.connection))

        self.connecting.set_result(True)
        if self.connections > 0:
            self.log("Deepkit: Reconnected.")

        self.connected.on_next(True)
        self.connections += 1

    async def _connect_anon(self):
        ws = 'wss' if self.ssl else 'ws'
        url = f"{ws}://{self.host}:{self.port}"
        self.connection = await websockets.connect(url)
        self.loop.create_task(self.handle_messages(self.connection))
        self.loop.create_task(self.send_messages(self.connection))

        self.connecting.set_result(True)
        self.connected.on_next(True)
        self.connections += 1

    async def _connect(self):
        # we want to restart with a empty queue, so authentication happens always first
        queue_copy = self.queue[:]
        self.queue = []

        if self.job_token:
            await self._connected(self.job_id, self.job_token)
            return

        try:
            link: Optional[FolderLink] = None

            user_token = self.token
            account_name = 'none'

            if not user_token:
                config = get_home_config()
                # when no user_token is given (via deepkit.login() for example)
                # we need to find the host, port, token from the user config in ~/.deepkit/config
                if not self.account and not self.project:
                    # find both, start with
                    link = config.get_folder_link_of_directory(sys.path[0])
                    account_config = config.get_account_for_id(link.accountId)
                elif self.account and not self.project:
                    account_config = config.get_account_for_name(self.account)
                else:
                    # default to first account configured
                    account_config = config.get_first_account()

                account_name = account_config.name
                self.host = account_config.host
                self.port = account_config.port
                self.ssl = account_config.ssl
                user_token = account_config.token

            ws = 'wss' if self.ssl else 'ws'
            try:
                url = f"{ws}://{self.host}:{self.port}"
                self.connection = await websockets.connect(url)
            except Exception as e:
                self.offline = True
                print(
                    f"Deepkit: App not started or server not reachable. Monitoring disabled. {e}",
                    file=sys.stderr)
                self.connecting.set_result(False)
                return

            self.loop.create_task(self.handle_messages(self.connection))
            self.loop.create_task(self.send_messages(self.connection))

            res = await self._message(
                {
                    'name': 'authenticate',
                    'token': {
                        'id': 'user',
                        'token': user_token
                    }
                },
                lock=False)
            if not res['result']:
                raise Exception('Login invalid')

            project_name = ''
            if link:
                project_name = link.name
                projectId = link.projectId
            else:
                if not self.project:
                    raise Exception(
                        'No project defined. Please use project="project-name" '
                        'to specify which project to use.')

                project = await self._action('app',
                                             'getProjectForPublicName',
                                             [self.project],
                                             lock=False)

                if not project:
                    raise Exception(
                        f'No project found for name {self.project}. Make sure it exists before using it. '
                        f'Do you use the correct account? (used {account_name})'
                    )
                project_name = project['name']
                projectId = project['id']

            job = await self._action('app',
                                     'createJob',
                                     [projectId, self.parent_experiment],
                                     lock=False)

            prefix = "Sub experiment" if self.parent_experiment else "Experiment"
            self.log(
                f"{prefix} #{job['number']} created in project {project_name} using account {account_name}"
            )

            deepkit.globals.loaded_job_config = job['config']['config']
            self.job_token = await self._action('app',
                                                'getJobAccessToken',
                                                [job['id']],
                                                lock=False)
            self.job_id = job['id']

            # todo, implement re-authentication, so we don't have to drop the active connection
            await self.connection.close()
            await self._connected(self.job_id, self.job_token)
        except Exception as e:
            self.connecting.set_exception(e)

        self.queue = queue_copy + self.queue

    def log(self, *message: str):
        if not self.silent: print(*message)
    lambda state, value: state.money < value,
    'else':
    lambda state, value: True,
}


@dataclass
class State:
    case: int = 0
    customer: int = 0
    employer: int = 0
    money: int = 0


store = BehaviorSubject(
    State(settings['initial_case_index'], settings['initial_customer'],
          settings['initial_employer'], settings['initial_money']))


def store_on_next(state):
    print(settings['money_template'].format(state.money))
    check_state(state)
    case = cases[state.case]
    show_case_description(case)
    next_state = get_next_state(case, state)

    if next_state is None or next_state.case == -1:
        print(settings['customer_template'].format(next_state.customer))
        print(settings['employer_template'].format(next_state.employer))
        print(settings['money_template'].format(next_state.money))
        store.on_completed()
Exemple #4
0
 def __init__(self):
     self.rx_sensor_data_subject = Subject()
     self.rx_sensor_settings_subject = BehaviorSubject(SensorSettings())
Exemple #5
0
class Client(threading.Thread):
    connection: websockets.WebSocketClientProtocol

    def __init__(self, config_path: str):
        self.connected = BehaviorSubject(False)
        self.config_path = config_path
        self.loop = asyncio.new_event_loop()
        self.host = os.environ.get('DEEPKIT_HOST', '127.0.0.1')
        self.port = int(os.environ.get('DEEPKIT_PORT', '8960'))
        self.token = os.environ.get('DEEPKIT_JOB_ACCESSTOKEN', None)
        self.job_id = os.environ.get('DEEPKIT_JOB_ID', None)
        self.message_id = 0
        self.account = 'localhost'
        self.callbacks: Dict[int, asyncio.Future] = {}
        self.subscriber: Dict[int, any] = {}
        self.stopping = False
        self.queue = []
        self.controllers = {}
        self.patches = {}
        self.offline = False
        self.connections = 0
        self.lock = threading.Lock()
        threading.Thread.__init__(self)
        self.daemon = True
        self.loop = asyncio.new_event_loop()
        self.start()

    def run(self):
        self.connecting = self.loop.create_future()
        self.loop.create_task(self._connect())
        self.loop.run_forever()

    def shutdown(self):
        if self.offline: return
        promise = asyncio.run_coroutine_threadsafe(self.stop_and_sync(), self.loop)
        promise.result()
        if not self.connection.closed:
            raise Exception('Connection still active')
        self.loop.stop()

    async def stop_and_sync(self):
        self.stopping = True

        # todo, assign correct status depending on python exit code

        # done = 150, //when all tasks are done
        # aborted = 200, //when at least one task aborted
        # failed = 250, //when at least one task failed
        # crashed = 300, //when at least one task crashed
        self.patches['status'] = 150
        self.patches['ended'] = time.time() * 1000

        self.patches['tasks.main.ended'] = time.time() * 1000

        # done = 500,
        # aborted = 550,
        # failed = 600,
        # crashed = 650,
        self.patches['tasks.main.status'] = 500

        self.patches['tasks.main.instances.0.ended'] = time.time() * 1000
        # done = 500,
        # aborted = 550,
        # failed = 600,
        # crashed = 650,
        self.patches['tasks.main.instances.0.status'] = 500

        while len(self.patches) > 0 or len(self.queue) > 0:
            await asyncio.sleep(0.15)

        await self.connection.close()

    async def register_controller(self, name: str, controller):
        self.controllers[name] = controller

        async def subscriber(message, done):
            if message['type'] == 'error':
                done()
                del self.controllers[name]
                raise Exception('Register controller error: ' + message['error'])

            if message['type'] == 'ack':
                pass

            if message['type'] == 'peerController/message':
                data = message['data']
                if not hasattr(controller, data['action']):
                    error = f"Requested action {message['action']} not available in {name}"
                    print(error, file=sys.stderr)
                    await self._message({
                        'name': 'peerController/message',
                        'controllerName': name,
                        'replyId': message['replyId'],
                        'data': {'type': 'error', 'id': 0, 'stack': None, 'entityName': '@error:default',
                                 'error': error}
                    }, no_response=True)

                if data['name'] == 'actionTypes':
                    parameters = []
                    for arg in inspect.getfullargspec(getattr(controller, data['action'])).args:
                        parameters.append({
                            'type': 'Any',
                            'array': False,
                            'partial': False
                        })

                    await self._message({
                        'name': 'peerController/message',
                        'controllerName': name,
                        'replyId': message['replyId'],
                        'data': {
                            'type': 'actionTypes/result',
                            'id': 0,
                            'parameters': parameters,
                            'returnType': {'partial': False, 'type': 'Any', 'array': False}
                        }
                    }, no_response=True)

                if data['name'] == 'action':
                    try:
                        res = getattr(controller, data['action'])(*data['args'])

                        await self._message({
                            'name': 'peerController/message',
                            'controllerName': name,
                            'replyId': message['replyId'],
                            'data': {
                                'type': 'next/json',
                                'id': message['id'],
                                'next': res,
                            }
                        }, no_response=True)
                    except Exception as e:
                        await self._message({
                            'name': 'peerController/message',
                            'controllerName': name,
                            'replyId': message['replyId'],
                            'data': {'type': 'error', 'id': 0, 'stack': None, 'entityName': '@error:default',
                                     'error': str(e)}
                        }, no_response=True)

        await self._subscribe({
            'name': 'peerController/register',
            'controllerName': name,
        }, subscriber)

        class Controller:
            def __init__(self, client):
                self.client = client

            def stop(self):
                self.client._message({
                    'name': 'peerController/unregister',
                    'controllerName': name,
                })

        return Controller(self)

    async def _action(self, controller: str, action: str, args: List, lock=True):
        if lock: await self.connecting
        if self.offline: return
        if self.stopping: raise Exception('In shutdown: actions disallowed')
        res = await self._message({
            'name': 'action',
            'controller': controller,
            'action': action,
            'args': args,
            'timeout': 60
        }, lock=lock)
        if res['type'] == 'next/json':
            return res['next']

        if res['type'] == 'error':
            print(res, file=sys.stderr)
            raise Exception('API Error: ' + res['error'])

        raise Exception(f"Invalid action type '{res['type']}'. Not implemented")

    def job_action(self, action: str, args: List):
        return asyncio.run_coroutine_threadsafe(self._action('job', action, args), self.loop)

    async def _subscribe(self, message, subscriber):
        await self.connecting

        self.message_id += 1
        message['id'] = self.message_id

        message_id = self.message_id

        def on_done():
            del self.subscriber[message_id]

        async def on_incoming_message(incoming_message):
            await subscriber(incoming_message, on_done)

        self.subscriber[self.message_id] = on_incoming_message
        self.queue.append(message)

    async def _message(self, message, lock=True, no_response=False):
        if lock: await self.connecting

        self.message_id += 1
        message['id'] = self.message_id
        if not no_response:
            self.callbacks[self.message_id] = self.loop.create_future()

        self.queue.append(message)

        if no_response:
            return

        return await self.callbacks[self.message_id]

    def patch(self, path: str, value: any):
        if self.offline: return
        if self.stopping: raise Exception('In shutdown: patches disallowed')

        self.patches[path] = value

    async def send_messages(self, connection):
        while not connection.closed:
            try:
                q = self.queue[:]
                for m in q:
                    await connection.send(json.dumps(m))
                    self.queue.remove(m)
            except Exception:
                return

            if len(self.patches) > 0:
                try:
                    send = self.patches.copy()
                    await connection.send(json.dumps({
                        'name': 'action',
                        'controller': 'job',
                        'action': 'patchJob',
                        'args': [
                            send
                        ],
                        'timeout': 60
                    }))

                    for i in send.keys():
                        if self.patches[i] == send[i]:
                            del self.patches[i]

                except websockets.exceptions.ConnectionClosed:
                    return

            await asyncio.sleep(0.25)

    async def handle_messages(self, connection):
        while not connection.closed:
            try:
                res = json.loads(await connection.recv())
            except websockets.exceptions.ConnectionClosedError:
                # we need reconnect
                break
            except websockets.exceptions.ConnectionClosedOK:
                # we closed on purpose, so no reconnect necessary
                return

            if res and 'id' in res:
                if res['id'] in self.subscriber:
                    await self.subscriber[res['id']](res)

                if res['id'] in self.callbacks:
                    self.callbacks[res['id']].set_result(res)
                    del self.callbacks[res['id']]

        if not self.stopping:
            print("Deepkit: lost connection. reconnect ...")
            self.connecting = self.loop.create_future()
            self.connected.on_next(False)
            self.loop.create_task(self._connect())

    async def _connect_job(self, host: str, port: int, id: str, token: str):
        try:
            self.connection = await websockets.connect(f"ws://{host}:{port}")
        except Exception:
            # try again later
            await asyncio.sleep(1)
            self.loop.create_task(self._connect())
            return

        self.loop.create_task(self.handle_messages(self.connection))
        self.loop.create_task(self.send_messages(self.connection))

        res = await self._message({
            'name': 'authenticate',
            'token': {
                'id': 'job',
                'token': token,
                'job': id
            }
        }, lock=False)

        if not res['result'] or res['result'] is not True:
            raise Exception('Job token invalid')

        # load job controller
        await self._message({
            'name': 'action',
            'controller': 'job',
            'action': ''
        }, lock=False)

        self.connecting.set_result(True)
        if self.connections > 0:
            print("Deepkit: Reconnected.")

        self.connected.on_next(True)
        self.connections += 1

    async def _connect(self):
        # we want to restart with a empty queue, so authentication happens always first
        queue_copy = self.queue[:]
        self.queue = []

        if self.token:
            await self._connect_job(self.host, self.port, self.job_id, self.token)
        else:
            account_config = self.get_account_config(self.account)
            self.host = account_config['host']
            self.port = account_config['port']

            try:
                self.connection = await websockets.connect(f"ws://{self.host}:{self.port}")
            except Exception as e:
                self.offline = True
                print(f"Deepkit: App not started or server not reachable. Monitoring disabled. {e}")
                self.connecting.set_result(False)
                return

            self.loop.create_task(self.handle_messages(self.connection))
            self.loop.create_task(self.send_messages(self.connection))
            res = await self._message({
                'name': 'authenticate',
                'token': {
                    'id': 'user',
                    'token': account_config['token']
                }
            }, lock=False)
            if not res['result']:
                raise Exception('Login invalid')

            link = self.get_folder_link()

            deepkit_config_yaml = None
            if self.config_path and os.path.exists(self.config_path):
                deepkit_config_yaml = open(self.config_path, 'r', encoding='utf-8').read()

            job = await self._action('app', 'createJob', [link['projectId'], self.config_path, deepkit_config_yaml],
                                     lock=False)
            deepkit.globals.loaded_job = job
            self.token = await self._action('app', 'getJobAccessToken', [job['id']], lock=False)
            self.job_id = job['id']
            await self.connection.close()
            await self._connect_job(self.host, self.port, self.job_id, self.token)

        self.queue = queue_copy + self.queue

    def get_account_config(self, name: str) -> Dict:
        with open(os.path.expanduser('~') + '/.deepkit/config', 'r') as h:
            config = json.load(h)
            for account in config['accounts']:
                if account['name'] == name:
                    return account

        raise Exception(f"No account for {name} found.")

    def get_folder_link(self) -> Dict:
        with open(os.path.expanduser('~') + '/.deepkit/config', 'r') as h:
            config = json.load(h)
            for link in config['folderLinks']:
                if is_in_directory(sys.path[0], link['path']):
                    return link

        raise Exception(f"No project link for {sys.path[0]} found.")
 def __init__(self, *args, **kwargs):
     super().__init__(*args)
     self.subject = BehaviorSubject(self.checkState())
     self.stateChanged.connect(self.subject.on_next)
     self.subject.subscribe(self.update_ui)
Exemple #7
0
class LobbyConnection:
    """
    GUARANTEES:
    - After calling connect, connection will leave disconnected state.
    """
    def __init__(self, host, port):
        self.socket = QtNetwork.QTcpSocket()
        self.socket.stateChanged.connect(self._on_socket_state_change)
        self.socket.readyRead.connect(self.read)
        self.socket.error.connect(self._on_error)
        self.socket.setSocketOption(QtNetwork.QTcpSocket.KeepAliveOption, 1)

        self._host = host
        self._port = port
        self.block_size = 0

        self._obs_state = BehaviorSubject(ConnectionState.DISCONNECTED)
        self.obs_state = self._obs_state.pipe(ops.distinct_until_changed())
        self.message_stream = Subject()

    def _socket_state(self, s):
        qas = QtNetwork.QAbstractSocket
        if s in [qas.ConnectedState, qas.ClosingState]:
            return ConnectionState.CONNECTED
        elif s in [qas.BoundState, qas.HostLookupState, qas.ConnectingState]:
            return ConnectionState.CONNECTING
        else:
            return ConnectionState.DISCONNECTED

    @property
    def state(self):
        return self._obs_state.value

    def _on_socket_state_change(self, state):
        s = self._socket_state(state)
        if s is ConnectionState.DISCONNECTED:
            self.block_size = 0
        self._obs_state.on_next(s)

    # Conflict with PySide2 signals!
    # Idempotent
    def connect_(self):
        if self.state is not ConnectionState.DISCONNECTED:
            return
        self.socket.connectToHost(self._host, self._port)

    def disconnect_(self):
        self.socket.disconnectFromHost()

    def read(self):
        ins = QtCore.QDataStream(self.socket)
        ins.setVersion(QtCore.QDataStream.Qt_4_2)

        while not ins.atEnd():
            if self.block_size == 0:
                if self.socket.bytesAvailable() < 4:
                    return
                self.block_size = ins.readUInt32()
            if self.socket.bytesAvailable() < self.block_size:
                return
            data = ins.readQString()
            self.block_size = 0
            self.message_stream.on_next(data)

    def write(self, data):
        block = QtCore.QByteArray()
        out = QtCore.QDataStream(block, QtCore.QIODevice.ReadWrite)
        out.setVersion(QtCore.QDataStream.Qt_4_2)
        out.writeUInt32(2 * len(data) + 4)
        out.writeQString(data)
        if self.socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
            self.socket.write(block)

    def _on_error(self):
        self.disconnect_()
Exemple #8
0
class GenericRantList(urwid.WidgetWrap):
    def __init__(self, rants_subscriptable):

        self.widget = None
        self.rants_subscriptable = rants_subscriptable
        self.rants = []
        self.update = BehaviorSubject(self)
        self._subscriptions = []

        self.create()
        super().__init__(self.widget)

    def __del__(self):
        self.update.dispose()
        for subscription in self._subscritpions:
            subscription.unsubscribe()

    def create_list_box(self):

        elements = []
        for i, rant in enumerate(self.rants):
            if i > 0:
                elements.append(urwid.Divider(u'\u2500'))

            element = RantListElementWidget(rant)
            elements.append(element)

        list_box = urwid.ListBox(urwid.SimpleListWalker(elements))

        return list_box

    def create(self):
        self.widget = urwid.Frame(self.create_list_box())
        self._subscribe_rant_list()

    def _subscribe_rant_list(self):
        def action(new_value):
            self.rants = new_value

            simple_list_walker = self.widget.contents['body'][0].body
            i = 0
            j = 0

            # update existent
            while i < len(simple_list_walker) and j < len(new_value):
                element = simple_list_walker[i]
                rant = new_value[j]
                if type(element) is not RantListElementWidget:
                    i += 1
                else:
                    element.update_rant(rant)
                    i += 1
                    j += 1

            # append new ones
            while j < len(new_value):
                rant = new_value[j]
                if i > 0:
                    simple_list_walker.append(urwid.Divider(u'\u2500'))
                    i += 1
                simple_list_walker.contents.append(RantListElementWidget(rant))
                i += 1
                j += 1

            # delete excedent
            simple_list_walker.contents[:i]

            self.update.on_next(self)

        self._subscriptions.append(self.rants_subscriptable.subscribe(action))
Exemple #9
0
 def __init__(self):
     self.game_msg = MockMessage()
     self.player_msg = MockMessage()
     self.login_msg = MockLoginMessage()
     self.obs_connection_state = BehaviorSubject(
         ConnectionState.DISCONNECTED)
class DevRantService():
    def __init__(self, *args, **kwargs):

        self.base_url = "https://devrant.com/api"

        self.base_params = {
            'app': 3,
        }

        self.rants = BehaviorSubject([])
        self.error = Subject()
        self.me = BehaviorSubject(None)
        self.auth_token = BehaviorSubject(None)
        self.is_logged = BehaviorSubject(False)

        self._subscribe_to_auth_token()
        self._load_cache()

    def __del__(self):
        self.rants.dispose()
        self.error.dispose()
        self.me.dispose()
        self.auth_token.dispose()
        self.is_logged.dispose()

    def _get_params(self, **kwargs):
        params = dict(self.base_params, **kwargs)
        return urllib.parse.urlencode(params)

    def _load_cache(self, filename='.cache.json'):
        try:
            fh = open(filename, 'r')
        except FileNotFoundError:
            self._write_cache(filename)
            self._load_cache(filename)
        else:
            try:
                cache_data = json.load(fh)
            except json.decoder.JSONDecodeError:
                pass
            else:
                cached_auth_token = cache_data.get('auth_token')

                if cached_auth_token is not None:
                    cached_auth_token = AuthToken(**cached_auth_token)
                    self.auth_token.on_next(cached_auth_token)
            fh.close()

    def _write_cache(self, filename='.cache.json'):

        cache_data = {}

        try:
            fh = open(filename, 'r')
            cache_data = json.load(fh)
        except FileNotFoundError:
            pass
        except json.JSONDecodeError:
            pass
        else:
            fh.close()

        fh = open(filename, 'w')
        if self.auth_token.value is not None:
            cache_data['auth_token'] = self.auth_token.value.__dict__()

        json.dump(cache_data, fh)
        fh.close()

    def _delete_cache(self, filename='.cache.json'):
        fh = open(filename, 'w')
        json.dump({}, fh)
        fh.close()

    def _subscribe_to_auth_token(self):
        def action(value):
            # change is_logged value
            if self.is_logged.value and value.user_id is None:
                self.is_logged.on_next(False)
            elif not self.is_logged.value and value and value.user_id:
                self.is_logged.on_next(True)

            # save new auth_token
            self._write_cache()

        self._auth_token_subscription = self.auth_token.subscribe(action)
        return self

    async def get_rants(self, limit=10):
        param_url = self._get_params(sort='recent',
                                     limit=limit,
                                     skip=len(self.rants.value))

        response = requests.get(self.base_url + '/devrant/rants' + '?' +
                                param_url)

        if response.status_code == 200:
            new_rants = json.loads(response.text)['rants']
            new_rants = [Rant(rant) for rant in new_rants]
            all_rants = list(
                merge(self.rants.value,
                      new_rants,
                      key=lambda x: x.id,
                      reverse=True))
            self.rants.on_next(all_rants)
        else:
            self.error.on_next({
                'code': 1,
                'message': 'Unexpected status code',
                'response': response
            })

        return self

    async def get_new_rants(self, skip=0, limit=10):

        if len(self.rants.value) == 0:
            return await self.get_rants()

        newest_actual_rant = self.rants.value[0]

        param_url = self._get_params(sort='recent', limit=limit, skip=skip)
        response = requests.get(self.base_url + '/devrant/rants' + '?' +
                                param_url)

        if response.status_code == 200:
            new_rants = json.loads(response.text)['rants']
            new_rants = [Rant(rant) for rant in new_rants]
            new_rants.sort(key=lambda x: x.id, reverse=True)
            if new_rants[-1].id > newest_actual_rant.id:
                await self.get_new_rants(skip + limit, limit)
                newest_actual_rant = self.rants.value[0]

            new_rants = [
                rant for rant in new_rants if rant.id > newest_actual_rant.id
            ]

            all_rants = list(
                merge(new_rants,
                      self.rants.value,
                      key=lambda x: x.id,
                      reverse=True))

            self.rants.on_next(all_rants)
        else:
            self.error.on_next({
                'code': 1,
                'message': 'Unexpected status code',
                'response': response
            })

        return self

    async def post_rant(self, draft_rant):

        headers = {
            "Host": "devrant.com",
            "Connection": "keep-alive",
            "Accept": "application/json",
            "Origin": "https://devrant.com",
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Referer": "https://devrant.com/feed",
        }

        form_data = {
            **self.base_params, 'rant': draft_rant.text,
            'token_id': self.auth_token.value.id,
            'token_key': self.auth_token.value.key,
            'user_id': self.auth_token.value.user_id,
            'tags': ', '.join(draft_rant.tags),
            'type': 1
        }

        draft_rant.response = requests.post(self.base_url + '/devrant/rants',
                                            headers=headers,
                                            data=form_data)

        draft_rant.state.on_next(DraftState.Sent)

        if draft_rant.response.status_code == 200:
            success = json.loads(draft_rant.response.text).get('success')
            if success:
                await self.get_new_rants()
                draft_rant.state.on_next(DraftState.Published)
            else:
                draft_rant.state.on_next(DraftState.Rejected)
                self.error.on_next({
                    'code': 2,
                    'message': 'Posted rant error',
                    'response': draft_rant.response
                })
        elif draft_rant.response.status_code == 400:
            draft_rant.state.on_next(DraftState.Rejected)
            self.error.on_next({
                'code': 2,
                'message': 'Posted rant error',
                'response': draft_rant.response
            })
        else:
            draft_rant.state.on_next(DraftState.Rejected)
            self.error.on_next({
                'code': 1,
                'message': 'Unexpected status code',
                'response': draft_rant.response
            })

    async def login(self, username, password):

        headers = {
            "Host": "devrant.com",
            "Connection": "keep-alive",
            "Accept": "application/json",
            "Origin": "https://devrant.com",
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Referer": "https://devrant.com/",
        }
        response = requests.post(self.base_url + '/users/auth-token',
                                 headers=headers,
                                 data={
                                     **self.base_params,
                                     "username": username,
                                     "password": password,
                                 })

        if response.status_code == 200:
            data = response.json()
            if data.get('success'):
                auth_token = AuthToken(**data.get('auth_token'))
                self.auth_token.on_next(auth_token)
                # await self.get_about_me()
            else:
                self.error.on_next({'message': data.get('error')})

    def logout(self):
        self._delete_cache()
        self.auth_token.on_next(AuthToken())

    async def get_about_me(self):

        param_url = self._get_params(token_id=self.auth_token.value.id,
                                     token_key=self.auth_token.value.key)
        response = requests.get(
            self.base_url + '/users/{}'.format(self.auth_token.value.user_id) +
            '?' + param_url)
        return response
Exemple #11
0
def create_store(initial_state: Optional[ReduxRootState] = None) -> ReduxRootStore:  # pylint: disable=too-many-locals
    """ Constructs a new store that can handle feature modules.

        Args:
            initial_state: optional initial state of the store, will typically be the empty dict

        Returns:
            An implementation of the store
    """

    # current reducer
    reducer: Reducer = identity_reducer

    def replace_reducer(new_reducer: Reducer) -> None:
        """ Callback that replaces the current reducer

            Args:
                new_reducer: the new reducer

        """
        nonlocal reducer
        reducer = new_reducer

    # subject used to dispatch actions
    actions = Subject()

    # the shared action observable
    actions_ = actions.pipe(op.share())

    _dispatch = actions.on_next

    # our current state
    state = BehaviorSubject(initial_state if initial_state else {})

    # shutdown trigger
    done_ = Subject()

    # The set of known modules, to avoid cycles and duplicate registration
    modules: MutableMapping[str, ReduxFeatureModule] = {}

    # Sequence of added modules
    module_subject = Subject()

    # Subscribe to the resolved modules
    module_ = module_subject.pipe(op.distinct(select_id), op.share())

    # Build the reducers
    reducer_ = module_.pipe(
        op.filter(has_reducer),
        op.scan(reduce_reducers, {}),
        op.map(combine_reducers),
        op.map(replace_reducer),
    )

    # Build the epic
    epic_ = module_.pipe(
        op.map(select_epic),
        op.filter(bool),
        op.map(normalize_epic)
    )

    # Root epic that combines all of the incoming epics
    def root_epic(
        action_: Observable, state_: Observable
    ) -> Observable:
        """ Implementation of the root epic. If listens for new epics
            to come in and automatically subscribes.

            Args:
                action_: the action observable
                state_: the state observable

            Returns
                The observable of resulting actions
        """
        return epic_.pipe(
            op.flat_map(run_epic(action_, state_)),
            op.map(_dispatch)
        )

    # notifications about new feature states
    new_module_ = module_.pipe(
        op.map(select_id),
        op.map(create_action(INIT_ACTION)),
        op.map(_dispatch),
    )

    def _add_feature_module(module: ReduxFeatureModule):
        """ Registers a new feature module

            Args:
                module: the new feature module

        """
        module_id = select_id(module)
        if not module_id in modules:
            modules[module_id] = module
            for dep in select_dependencies(module):
                _add_feature_module(dep)
            module_subject.on_next(module)

    # all state
    internal_ = merge(root_epic(actions_, state), reducer_, new_module_).pipe(
        op.ignore_elements()
    )

    def _as_observable() -> Observable:
        """ Returns the state as an observable

            Returns:
                the observable
        """
        return state

    def _on_completed() -> None:
        """ Triggers the done event """
        done_.on_next(None)

    merge(actions_, internal_).pipe(
        op.map(lambda action: reducer(state.value, action)),
        op.take_until(done_),
    ).subscribe(state, logger.error)

    return ReduxRootStore(
        _as_observable, _dispatch, _add_feature_module, _dispatch, _on_completed
    )
 def subject_factory(scheduler):
     return BehaviorSubject(initial_value)
Exemple #13
0
 def action1(scheduler, state=None):
     subject[0] = BehaviorSubject(100)
Exemple #14
0
class BlenderContext(Context):
    window_size: RV[Dimension] = rv.new_view()

    batch: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s))

    buffer: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s))

    def __init__(self,
                 toolkit: BlenderToolkit,
                 look_and_feel: Optional[LookAndFeel] = None,
                 font_options: Optional[FontOptions] = None,
                 window_manager: Optional[WindowManager] = None,
                 error_handler: Optional[ErrorHandler] = None) -> None:
        super().__init__(toolkit, look_and_feel, font_options, window_manager,
                         error_handler)

        resolution = Dimension(bge.render.getWindowWidth(),
                               bge.render.getWindowHeight())

        self._resolution = BehaviorSubject(resolution)

        # noinspection PyTypeChecker
        self.window_size = self._resolution.pipe(ops.distinct_until_changed())

        self._shader = cast(GPUShader, gpu.shader.from_builtin("2D_IMAGE"))

        if use_viewport_render:
            # noinspection PyArgumentList
            self._draw_handler = SpaceView3D.draw_handler_add(
                self.process, (), "WINDOW", "POST_PIXEL")
        else:
            self._draw_handler = None

            bge.logic.getCurrentScene().post_draw.append(self.process)

        # noinspection PyTypeChecker
        self._texture = Buffer(bgl.GL_INT, 1)

        bgl.glGenTextures(1, self.texture)

    @property
    def shader(self) -> GPUShader:
        return self._shader

    @property
    def texture(self) -> Buffer:
        return self._texture

    def create_batch(self, size: Dimension) -> GPUBatch:
        if size is None:
            raise ValueError("Argument 'size' is required.")

        points = Bounds(0, 0, size.width, size.height).points

        vertices = tuple(map(lambda p: p.tuple, map(self.translate, points)))
        coords = ((0, 0), (1, 0), (1, 1), (0, 1))

        indices = {"pos": vertices, "texCoord": coords}

        return batch_for_shader(self.shader, "TRI_FAN", indices)

    def translate(self, point: Point) -> Point:
        if point is None:
            raise ValueError("Argument 'point' is required.")

        return point.copy(y=self.window_size.height - point.y)

    # noinspection PyTypeChecker
    def process_draw(self) -> None:
        width = bge.render.getWindowWidth()
        height = bge.render.getWindowHeight()

        self._resolution.on_next(Dimension(width, height))

        super().process_draw()

        data = self.surface.get_data()

        source = bgl.Buffer(bgl.GL_BYTE, width * height * 4, data)

        bgl.glEnable(bgl.GL_BLEND)
        bgl.glActiveTexture(bgl.GL_TEXTURE0)

        # noinspection PyUnresolvedReferences
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])

        bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_SRGB_ALPHA, width,
                         height, 0, bgl.GL_BGRA, bgl.GL_UNSIGNED_BYTE, source)

        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER,
                            bgl.GL_NEAREST)
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER,
                            bgl.GL_NEAREST)

        self.shader.bind()
        self.shader.uniform_int("image", 0)

        self.batch.draw(self.shader)

        bgl.glDeleteBuffers(1, source)

    def dispose(self) -> None:
        if self._draw_handler:
            # noinspection PyArgumentList
            SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
        else:
            bge.logic.getCurrentScene().post_draw.remove(self.process)

        bgl.glDeleteTextures(1, self.texture)

        # noinspection PyTypeChecker
        bgl.glDeleteBuffers(1, self.texture)

        super().dispose()
 def __init__(self):
     self.note_repository = NoteRepository()
     # Create notes field as a behavior subject with note from the business logic as an initial value
     # Your code here
     self.note_behavior_subject = BehaviorSubject(
         self.note_repository.get_all_notes())
class TabViewModel:
    """Informação global da tab"""
    def __init__(self, share_x, max_plots, name):
        self.sharexBehavior = BehaviorSubject(share_x)
        self.maxPlotsBehavior = BehaviorSubject(max_plots)
        self.nameBehavior = BehaviorSubject(name)

    def dispose(self):
        self.sharexBehavior.dispose()
        self.maxPlotsBehavior.dispose()
        self.nameBehavior.dispose()

    def _get_sharex_(self):
        return self.sharexBehavior.value

    def _set_sharex_(self, s):
        self.sharexBehavior.on_next(s)

    sharex = property(_get_sharex_, _set_sharex_)

    def _get_maxPlots_(self):
        return self.maxPlotsBehavior.value

    def _set_maxPlots_(self, m):
        self.maxPlotsBehavior.on_next(m)

    maxPlots = property(_get_maxPlots_, _set_maxPlots_)

    def _get_name_(self):
        return self.nameBehavior.value

    def _set_name_(self, value):
        self.nameBehavior.on_next(value)

    name = property(_get_name_, _set_name_)
Exemple #17
0
class Client(threading.Thread):
    connection: websockets.WebSocketClientProtocol

    def __init__(self, options: ContextOptions):
        self.connected = BehaviorSubject(False)
        self.options: ContextOptions = options

        self.loop = asyncio.new_event_loop()
        self.host = os.environ.get('DEEPKIT_HOST', '127.0.0.1')
        self.port = int(os.environ.get('DEEPKIT_PORT', '8960'))
        self.token = os.environ.get('DEEPKIT_JOB_ACCESSTOKEN', None)
        self.job_id = os.environ.get('DEEPKIT_JOB_ID', None)
        self.message_id = 0
        self.account = 'localhost'
        self.callbacks: Dict[int, asyncio.Future] = {}
        self.subscriber: Dict[int, any] = {}
        self.stopping = False
        self.queue = []
        self.controllers = {}
        self.patches = {}
        self.offline = False
        self.connections = 0
        self.lock = threading.Lock()
        threading.Thread.__init__(self)
        self.daemon = True
        self.loop = asyncio.new_event_loop()
        self.start()

    def run(self):
        self.connecting = self.loop.create_future()
        self.loop.create_task(self._connect())
        self.loop.run_forever()

    def shutdown(self):
        if self.offline: return
        promise = asyncio.run_coroutine_threadsafe(self.stop_and_sync(),
                                                   self.loop)
        promise.result()
        if not self.connection.closed:
            raise Exception('Connection still active')
        self.loop.stop()

    async def stop_and_sync(self):
        self.stopping = True

        # done = 150, //when all tasks are done
        # aborted = 200, //when at least one task aborted
        # failed = 250, //when at least one task failed
        # crashed = 300, //when at least one task crashed
        self.patches['status'] = 150
        self.patches['ended'] = datetime.utcnow().isoformat()
        self.patches['tasks.main.ended'] = datetime.utcnow().isoformat()

        # done = 500,
        # aborted = 550,
        # failed = 600,
        # crashed = 650,
        self.patches['tasks.main.status'] = 500
        self.patches['tasks.main.instances.0.ended'] = datetime.utcnow(
        ).isoformat()

        # done = 500,
        # aborted = 550,
        # failed = 600,
        # crashed = 650,
        self.patches['tasks.main.instances.0.status'] = 500

        if hasattr(sys, 'last_value'):
            if isinstance(sys.last_value, KeyboardInterrupt):
                self.patches['status'] = 200
                self.patches['tasks.main.status'] = 550
                self.patches['tasks.main.instances.0.status'] = 550
            else:
                self.patches['status'] = 300
                self.patches['tasks.main.status'] = 650
                self.patches['tasks.main.instances.0.status'] = 650

        while len(self.patches) > 0 or len(self.queue) > 0:
            await asyncio.sleep(0.15)

        await self.connection.close()

    async def register_controller(self, name: str, controller):
        self.controllers[name] = controller

        async def subscriber(message, done):
            if message['type'] == 'error':
                done()
                del self.controllers[name]
                raise Exception('Register controller error: ' +
                                message['error'])

            if message['type'] == 'ack':
                pass

            if message['type'] == 'peerController/message':
                data = message['data']

                if not hasattr(controller, data['action']):
                    error = f"Requested action {message['action']} not available in {name}"
                    print(error, file=sys.stderr)
                    await self._message(
                        {
                            'name': 'peerController/message',
                            'controllerName': name,
                            'clientId': message['clientId'],
                            'data': {
                                'type': 'error',
                                'id': data['id'],
                                'stack': None,
                                'entityName': '@error:default',
                                'error': error
                            }
                        },
                        no_response=True)

                if data['name'] == 'actionTypes':
                    parameters = []

                    i = 0
                    for arg in inspect.getfullargspec(
                            getattr(controller, data['action'])).args:
                        parameters.append({
                            'type': 'any',
                            'name': '#' + str(i)
                        })
                        i += 1

                    await self._message(
                        {
                            'name': 'peerController/message',
                            'controllerName': name,
                            'clientId': message['clientId'],
                            'data': {
                                'type': 'actionTypes/result',
                                'id': data['id'],
                                'parameters': parameters,
                                'returnType': {
                                    'type': 'any',
                                    'name': 'result'
                                }
                            }
                        },
                        no_response=True)

                if data['name'] == 'action':
                    try:
                        res = getattr(controller,
                                      data['action'])(*data['args'])

                        await self._message(
                            {
                                'name': 'peerController/message',
                                'controllerName': name,
                                'clientId': message['clientId'],
                                'data': {
                                    'type': 'next/json',
                                    'id': data['id'],
                                    'encoding': {
                                        'name': 'r',
                                        'type': 'any'
                                    },
                                    'next': res,
                                }
                            },
                            no_response=True)
                    except Exception as e:
                        await self._message(
                            {
                                'name': 'peerController/message',
                                'controllerName': name,
                                'clientId': message['clientId'],
                                'data': {
                                    'type': 'error',
                                    'id': data['id'],
                                    'stack': None,
                                    'entityName': '@error:default',
                                    'error': str(e)
                                }
                            },
                            no_response=True)

        await self._subscribe(
            {
                'name': 'peerController/register',
                'controllerName': name,
            }, subscriber)

        class Controller:
            def __init__(self, client):
                self.client = client

            def stop(self):
                self.client._message({
                    'name': 'peerController/unregister',
                    'controllerName': name,
                })

        return Controller(self)

    async def _action(self,
                      controller: str,
                      action: str,
                      args: List,
                      lock=True,
                      allow_in_shutdown=False):
        if lock: await self.connecting
        if self.offline: return
        if self.stopping and not allow_in_shutdown:
            raise Exception('In shutdown: actions disallowed')

        if not controller: raise Exception('No controller given')
        if not action: raise Exception('No action given')

        res = await self._message(
            {
                'name': 'action',
                'controller': controller,
                'action': action,
                'args': args,
                'timeout': 60
            },
            lock=lock)

        if res['type'] == 'next/json':
            return res['next'] if 'next' in res else None

        if res['type'] == 'error':
            print(res, file=sys.stderr)
            raise ApiError('API Error: ' + str(res['error']))

        raise ApiError(f"Invalid action type '{res['type']}'. Not implemented")

    def job_action(self, action: str, args: List):
        return asyncio.run_coroutine_threadsafe(
            self._action('job', action, args), self.loop)

    async def _subscribe(self, message, subscriber):
        await self.connecting

        self.message_id += 1
        message['id'] = self.message_id

        message_id = self.message_id

        def on_done():
            del self.subscriber[message_id]

        async def on_incoming_message(incoming_message):
            await subscriber(incoming_message, on_done)

        self.subscriber[self.message_id] = on_incoming_message
        self.queue.append(message)

    async def _message(self, message, lock=True, no_response=False):
        if lock: await self.connecting

        self.message_id += 1
        message['id'] = self.message_id
        if not no_response:
            self.callbacks[self.message_id] = self.loop.create_future()

        self.queue.append(message)

        if no_response:
            return

        return await self.callbacks[self.message_id]

    def patch(self, path: str, value: any):
        if self.offline: return
        if self.stopping: return

        self.patches[path] = value

    async def send_messages(self, connection):
        while not connection.closed:
            try:
                q = self.queue[:]
                for m in q:
                    await connection.send(json.dumps(m))
                    self.queue.remove(m)
            except Exception as e:
                print("Failed sending, exit send_messages")
                raise e

            if len(self.patches) > 0:
                # we have to send first all messages/actions out
                # before sending patches, as most of the time
                # patches are based on previously created entities,
                # so we need to make sure those entities are created
                # first before sending any patches.
                try:
                    send = self.patches.copy()
                    await connection.send(
                        json.dumps({
                            'name': 'action',
                            'controller': 'job',
                            'action': 'patchJob',
                            'args': [send],
                            'timeout': 60
                        }))

                    for i in send.keys():
                        if self.patches[i] == send[i]:
                            del self.patches[i]
                except websockets.exceptions.ConnectionClosed:
                    return
                except ApiError:
                    print("Patching failed. Syncing job data disabled.",
                          file=sys.stderr)
                    return

            await asyncio.sleep(0.2)

    async def handle_messages(self, connection):
        while not connection.closed:
            try:
                res = json.loads(await connection.recv())
            except websockets.exceptions.ConnectionClosedError:
                # we need reconnect
                break
            except websockets.exceptions.ConnectionClosedOK:
                # we closed on purpose, so no reconnect necessary
                return

            if res and 'id' in res:
                if res['id'] in self.subscriber:
                    await self.subscriber[res['id']](res)

                if res['id'] in self.callbacks:
                    self.callbacks[res['id']].set_result(res)
                    del self.callbacks[res['id']]

        if not self.stopping:
            print("Deepkit: lost connection. reconnect ...")
            self.connecting = self.loop.create_future()
            self.connected.on_next(False)
            self.loop.create_task(self._connect())

    async def _connect_job(self, host: str, port: int, id: str, token: str):
        try:
            self.connection = await websockets.connect(f"ws://{host}:{port}")
        except Exception:
            # try again later
            await asyncio.sleep(1)
            self.loop.create_task(self._connect())
            return

        self.loop.create_task(self.handle_messages(self.connection))
        self.loop.create_task(self.send_messages(self.connection))

        res = await self._message(
            {
                'name': 'authenticate',
                'token': {
                    'id': 'job',
                    'token': token,
                    'job': id
                }
            },
            lock=False)

        if not res['result'] or res['result'] is not True:
            raise Exception('Job token invalid')

        self.connecting.set_result(True)
        if self.connections > 0:
            print("Deepkit: Reconnected.")

        self.connected.on_next(True)
        self.connections += 1

    async def _connect(self):
        # we want to restart with a empty queue, so authentication happens always first
        queue_copy = self.queue[:]
        self.queue = []

        if self.token:
            await self._connect_job(self.host, self.port, self.job_id,
                                    self.token)
        else:
            config = get_home_config()
            link: Optional[FolderLink] = None
            if self.options.account:
                account_config = config.get_account_for_name(
                    self.options.account)
            else:
                link = config.get_folder_link_of_directory(sys.path[0])
                account_config = config.get_account_for_id(link.accountId)

            self.host = account_config.host
            self.port = account_config.port
            ws = 'wss' if account_config.ssl else 'ws'

            try:
                self.connection = await websockets.connect(
                    f"{ws}://{self.host}:{self.port}")
            except Exception as e:
                self.offline = True
                print(
                    f"Deepkit: App not started or server not reachable. Monitoring disabled. {e}"
                )
                self.connecting.set_result(False)
                return

            self.loop.create_task(self.handle_messages(self.connection))
            self.loop.create_task(self.send_messages(self.connection))
            res = await self._message(
                {
                    'name': 'authenticate',
                    'token': {
                        'id': 'user',
                        'token': account_config.token
                    }
                },
                lock=False)
            if not res['result']:
                raise Exception('Login invalid')

            if link:
                projectId = link.projectId
            else:
                if not self.options.project:
                    raise Exception(
                        'No project defined. Please use ContextOptions(project="project-name") '
                        'to specify which project to use.')

                project = await self._action('app',
                                             'getProjectForPublicName',
                                             [self.options.project],
                                             lock=False)
                if not project:
                    raise Exception(
                        f'No project found for name {self.options.project}. '
                        f'Do you use the correct account? (used {account_config.name})'
                    )

                projectId = project['id']

            job = await self._action('app',
                                     'createJob', [projectId],
                                     lock=False)

            deepkit.globals.loaded_job = job
            self.token = await self._action('app',
                                            'getJobAccessToken', [job['id']],
                                            lock=False)
            self.job_id = job['id']

            # todo, implement re-authentication, so we don't have to drop the active connection
            await self.connection.close()
            await self._connect_job(self.host, self.port, self.job_id,
                                    self.token)

        self.queue = queue_copy + self.queue
 def __init__(self, share_x, max_plots, name):
     self.sharexBehavior = BehaviorSubject(share_x)
     self.maxPlotsBehavior = BehaviorSubject(max_plots)
     self.nameBehavior = BehaviorSubject(name)
 def __init__(self, *args, **kwargs):
     super().__init__(*args, **kwargs)
     self.subject = BehaviorSubject(self.currentData())
     self.currentIndexChanged.connect(
         lambda: self.subject.on_next(self.currentText()))
Exemple #20
0
subject.on_next(4)
subject.on_completed()

# ReplaySubject会缓存所有值,如果指定参数的话只会缓存最近的几个值
print('--------ReplaySubject---------')
subject = ReplaySubject()
subject.on_next(1)
subject.subscribe(lambda i: print(i))
subject.on_next(2)
subject.on_next(3)
subject.on_next(4)
subject.on_completed()

# BehaviorSubject会缓存上次发射的值,除非Observable已经关闭
print('--------BehaviorSubject---------')
subject = BehaviorSubject(0)
subject.on_next(1)
subject.on_next(2)
subject.subscribe(lambda i: print(i))
subject.on_next(3)
subject.on_next(4)
subject.on_completed()

# AsyncSubject会缓存上次发射的值,而且仅会在Observable关闭后开始发射
print('--------AsyncSubject---------')
subject = AsyncSubject()
subject.on_next(1)
subject.on_next(2)
subject.subscribe(lambda i: print(i))
subject.on_next(3)
subject.on_next(4)