1+ import asyncio
12import inspect
23import logging
34import signal
45import uuid
5- from collections .abc import Callable
6+ from collections .abc import AsyncIterator , Callable
67from importlib import import_module
78from multiprocessing import Pool , set_start_method
9+ from multiprocessing .connection import Connection , Pipe
810from multiprocessing .pool import Pool as PoolClass
911from typing import Any , ParamSpec , TypeVar
1012
1517)
1618from opentelemetry .context import attach
1719from opentelemetry .propagate import get_global_textmap
18- from pydantic import TypeAdapter
1920
2021from blueapi .config import ApplicationConfig
21- from blueapi .service .interface import setup , teardown
22+ from blueapi .core .bluesky_types import DataEvent
23+ from blueapi .service import interface
24+ from blueapi .service .interface import SubHandles , setup , teardown
2225from blueapi .service .model import EnvironmentResponse
26+ from blueapi .worker .event import ProgressEvent , WorkerEvent
2327
2428# The default multiprocessing start method is fork
2529set_start_method ("spawn" , force = True )
@@ -145,11 +149,57 @@ def run(
145149 kwargs ,
146150 )
147151
152+ def event_pipe (self ):
153+ return EventPipe (self )
154+
148155 @property
149156 def state (self ) -> EnvironmentResponse :
150157 return self ._state
151158
152159
160+ class EventStream :
161+ def __init__ (self , rx : Connection ):
162+ self ._rx = rx
163+
164+ def __aiter__ (self ) -> AsyncIterator [WorkerEvent | DataEvent | ProgressEvent ]:
165+ return self
166+
167+ async def __anext__ (self ) -> WorkerEvent | DataEvent | ProgressEvent :
168+ data_available = asyncio .Event ()
169+ asyncio .get_event_loop ().add_reader (self ._rx .fileno (), data_available .set )
170+ try :
171+ while not self ._rx .poll ():
172+ await data_available .wait ()
173+ data_available .clear ()
174+ return self ._rx .recv ()
175+ except BrokenPipeError :
176+ raise StopAsyncIteration () from None
177+ finally :
178+ asyncio .get_event_loop ().remove_reader (self ._rx .fileno ())
179+
180+
181+ class EventPipe :
182+ runner : WorkerDispatcher
183+ handles : list [tuple [SubHandles , Connection ]]
184+
185+ def __init__ (self , runner : WorkerDispatcher ):
186+ self .runner = runner
187+ self .handles = []
188+
189+ def __enter__ (self ) -> EventStream :
190+ tx , rx = Pipe ()
191+ hnd = self .runner .run (interface .pipe_events , tx )
192+ LOGGER .debug ("Subscribing new event pipe: %s" , hnd )
193+ self .handles .append ((hnd , tx ))
194+ return EventStream (rx )
195+
196+ def __exit__ (self , * exc ):
197+ hnd , conn = self .handles .pop ()
198+ LOGGER .debug ("Unsubscribing event pipe: %s" , hnd )
199+ conn .close ()
200+ self .runner .run (interface .unpipe_events , hnd )
201+
202+
153203class InvalidRunnerStateError (Exception ):
154204 def __init__ (self , message ):
155205 super ().__init__ (message )
@@ -173,15 +223,7 @@ def import_and_run_function(
173223 func : Callable [..., T ] = _validate_function (
174224 mod .__dict__ .get (function_name , None ), function_name
175225 )
176- value = func (* args , ** kwargs )
177- return _valid_return (value , expected_type )
178-
179-
180- def _valid_return (value : Any , expected_type : type [T ] | None = None ) -> T :
181- if expected_type is None :
182- return value
183- else :
184- return TypeAdapter (expected_type ).validate_python (value )
226+ return func (* args , ** kwargs )
185227
186228
187229def _validate_function (func : Any , function_name : str ) -> Callable :
0 commit comments