async def test_cycle(assert_run, event_loop): with event_loop.assert_cleanup(): xs = stream.empty() | pipe.cycle() | pipe.timeout(1) await assert_run(xs, [], asyncio.TimeoutError()) with event_loop.assert_cleanup(): xs = stream.empty() | add_resource.pipe( 1) | pipe.cycle() | pipe.timeout(1) await assert_run(xs, [], asyncio.TimeoutError()) with event_loop.assert_cleanup(): xs = stream.just(1) | add_resource.pipe(1) | pipe.cycle() await assert_run(xs[:5], [1] * 5) assert event_loop.steps == [1] * 5
async def run(self) -> None: """ The main task, that reads data from the sensors and pushes it onto the event_bus. """ # Generate the UUIDs of new sensors sensor_stream = stream.chain( stream.iterate( iterate_safely(f"{self.__topic}/get", f"{self.__topic}/status_update")), stream.iterate(event_bus.subscribe(f"{self.__topic}/add_host")), ) | pipe.flatmap( lambda item: stream.chain( (stream.call(event_bus.call, f"{self.__topic}/get_config", item ) | catch.pipe(TopicNotRegisteredError)), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{item}/update")), ) | pipe.until(lambda config: config is None) | pipe.map(lambda config: config if self._is_config_valid( self.__node_id, config) else None) | pipe.map(self._create_transport) | pipe.switchmap(lambda transport: stream.empty() if transport is None else stream.iterate(transport.stream_data())) | pipe.action(lambda data: event_bus.publish("wamp/publish", data) )) await sensor_stream
def stream_data(self, config: dict[str, Any]) -> AsyncGenerator[DataEvent, None]: """ Stream the data from the sensor. Parameters ---------- config: dict A dictionary containing the sensor configuration. Returns ------- AsyncGenerator The asynchronous stream. """ data_stream = (stream.chain( stream.just(config), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{self.__uuid}/update"))) | pipe.action(lambda _: logging.getLogger(__name__).info( "Got new configuration for: %s", self) if config is not None else logging.getLogger(__name__). info("Removed configuration for: %s", self)) | pipe.map(self._parse_config) | pipe.switchmap(lambda conf: stream.empty( ) if conf is None or not conf["enabled"] else (self._configure_and_stream(conf)))) return data_stream
def _configure_and_stream( self, config: dict[str, Any] | None) -> AsyncGenerator[DataEvent, None]: if config is None: return stream.empty() try: # Run all config steps in order (concat) and one at a time (task_limit=1). Drop the output. There is # nothing to compare them to (filter => false), then read all sensors of the bricklet and process them in # parallel (flatten). config_stream = stream.chain( stream.iterate(config["on_connect"]) | pipe.starmap(lambda func, timeout: stream.just(func()) | pipe .timeout(timeout)) | pipe.concat(task_limit=1) | pipe.filter(lambda result: False), stream.iterate(config["config"].items()) | pipe.starmap(self._parse_callback_configuration) | pipe.starmap(self._set_callback_configuration) | pipe.flatten() | pipe.map(lambda args: self._read_sensor(config["uuid"], *args)) | pipe.flatten(), ) return config_stream except NotConnectedError: # Do not log it raise except Exception: self._logger.exception("This should not happen.") raise
def _stream_data(self, transport): config_stream = (stream.chain( stream.call( call_safely, f"{self.__database_topic}/get_config", f"{self.__database_topic}/status_update", transport.uuid, ), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{transport.uuid}/add")), ) | pipe.map(lambda config: self._create_device( transport, config)) | pipe.starmap(lambda config, device: stream.empty( ) if device is None else device.stream_data(config)) | pipe.switch() | context.pipe( transport, on_enter=lambda: logging.getLogger(__name__).info( "Connected to %s at %s (%s).", transport.name, transport.uri, transport.label), on_exit=lambda: logging.getLogger(__name__).info( "Disconnected from %s at %s (%s).", transport. name, transport.uri, transport.label), )) return config_stream
async def catch( source: AsyncIterable[Any], exc_class: Type[BaseException], on_exc: Callable[[BaseException], Stream] = None) -> AsyncGenerator[Any, None]: """ Catch an exception and then switch to the next stream `on_exc` or gracefully terminate, when no stream is given. Parameters ---------- source: AsyncIterable exc_class: BaseException type The exception to catch on_exc: Callable A function, that takes an exception and returns a Stream. Yields ------- Any The results from the source stream or the `on_exc` stream. """ try: async with streamcontext(source) as streamer: async for item in streamer: yield item except exc_class as exc: if on_exc is not None: async with on_exc(exc).stream() as streamer: async for item in streamer: yield item else: yield stream.empty()
def stream_data(self) -> AsyncGenerator[DataEvent, None]: """ Generate the initial configuration of the sensor, configure it, and finally stream the data from the sensor. If there is a configuration update, reconfigure the sensor and start streaming again. Returns ------- AsyncGenerator of DataEvent The data from the device """ # Generates the first configuration # Query the database and if it does not have a config for the sensor, wait until there is one data_stream = ( stream.chain( stream.call( call_safely, "db_labnode_sensors/get_config", "db_labnode_sensors/status_update", self.__uuid ) | pipe.takewhile(lambda config: config is not None), stream.iterate(event_bus.subscribe(f"nodes/by_uuid/{self.__uuid}/update")), ) | pipe.action( lambda config: logging.getLogger(__name__).info( "Got new configuration for: %s", self._device, ) ) | pipe.map(self._create_config) | pipe.switchmap( lambda config: stream.empty() if config is None or not config["enabled"] else (self._configure_and_stream(config)) ) ) return data_stream
async def test_cycle(assert_run, event_loop): with event_loop.assert_cleanup(): xs = stream.empty() | pipe.cycle() | pipe.timeout(1) await assert_run(xs, [], asyncio.TimeoutError()) with event_loop.assert_cleanup(): xs = ( stream.empty() | add_resource.pipe(1) | pipe.cycle() | pipe.timeout(1) ) await assert_run(xs, [], asyncio.TimeoutError()) with event_loop.assert_cleanup(): xs = stream.just(1) | add_resource.pipe(1) | pipe.cycle() await assert_run(xs[:5], [1]*5) assert event_loop.steps == [1]*5
async def execute(self) -> Tuple[Optional[int], Stream]: if self.executable_commands: source_action = cast(CLISource, self.executable_commands[0].action) count, flow = await source_action.source() for command in self.executable_commands[1:]: flow_action = cast(CLIFlow, command.action) flow = await flow_action.flow(flow) return count, flow else: return 0, stream.empty()
async def _set_callback_configuration( self, sid: int, unit: str, topic: str, config: AdvancedCallbackConfiguration): try: await self.device.set_callback_configuration(sid, *config) except AssertionError: self._logger.error( "Invalid configuration for %s: sid=%i, config=%s", self.device, sid, config) return stream.empty() remote_callback_config: AdvancedCallbackConfiguration remote_callback_config = await self.device.get_callback_configuration( sid) if remote_callback_config.period == 0: self._logger.warning( "Callback configuration configuration for %s: sid=%i, config=%s failed. Source disabled.", self.device, sid, config, ) return stream.empty() return stream.just((sid, unit, topic, remote_callback_config))
async def test_async_gen() -> None: async with stream.empty().stream() as empty: async for _ in await force_gen(empty): pass with pytest.raises(Exception): async with stream.throw(Exception(";)")).stream() as err: async for _ in await force_gen(err): pass async with stream.iterate(range(0, 100)).stream() as elems: assert [x async for x in await force_gen(elems)] == list(range(0, 100))
def stream_data(self) -> AsyncGenerator[DataEvent, None]: """ Generate the initial configuration of the sensor, configure it, and finally stream the data from the sensor. If there is a configuration update, reconfigure the sensor and start streaming again. Returns ------- AsyncGenerator of DataEvent The data from the device """ # Generates the first configuration # Query the database and if it does not have a config for the sensor, wait until there is one data_stream = stream.chain( stream.just(self), stream.iterate( event_bus.subscribe( f"nodes/tinkerforge/{self.device.uid}/remove"))[:1] | pipe.map(lambda x: None), ) | pipe.switchmap( lambda sensor: stream.empty() if sensor is None else (self._stream_config_updates(sensor) | pipe.switchmap(lambda config: stream.chain( stream.just(config), stream.iterate( event_bus.subscribe( f"nodes/by_uuid/{config['uuid']}/remove"))[:1] | pipe.map(lambda x: None), )) | pipe.action(lambda config: logging.getLogger(__name__).info( "Got new configuration for: %s", sensor.device, )) | pipe.map(self._create_config) | pipe.switchmap(lambda config: stream.empty() if config is None or not config["enabled"] else (self._configure_and_stream(config))))) return data_stream
async def arg_stream(ic, user, arg): if type(arg) in [int, str, float]: return stream.just({'type': type(arg).__name__, 'val': arg}) elif isinstance(arg, pathlib.Path): path = misc.normpath(ic, arg) return read_field(ic, user, path) elif type(arg) is dict and 'fname' in arg: return display_fn(ic, arg) else: logger.warning(f"{ic.user}@{ic.path}: argument {arg}: unknow type") return stream.empty()
def on_error(self, exc: BaseException) -> AsyncGenerator[None, None]: """ The function to call in case of an execution during streaming. Parameters ---------- exc: BaseException The exception, that was raised Returns ------- AsyncGenerator Am empty stream, that terminates without generating a value. """ logging.getLogger(__name__).error( "Error while while reading %s. Terminating device. Error: %s", self, exc) return stream.empty()
def _configure_and_stream( self, config: dict[str, Any]) -> AsyncGenerator[DataEvent, None]: if config is None: return stream.empty() # Run all config steps in order (concat) and one at a time (task_limit=1). Drop the output. There is nothing to # compare them to (filter => false), then read the device. config_stream = stream.chain( stream.iterate(config["on_connect"]) | pipe.starmap(lambda func, timeout: stream.just(func()) | pipe. timeout(timeout)) | pipe.concat(task_limit=1) | pipe.filter(lambda result: False), self._read_device(config) | pipe.map(lambda item: DataEvent(sender=config["uuid"], topic=config["topic"], value=item, sid=0, unit=config["unit"])) | finally_action.pipe( stream.call(self._clean_up, config["on_disconnect"])), ) | catch.pipe(TypeError, on_exc=self.on_error) return config_stream
async def test_empty(assert_run): xs = stream.empty() await assert_run(xs, [])