3737 TypeVar ,
3838 overload ,
3939)
40- from weakref import WeakSet
4140import weakref
4241from fastapi import APIRouter , FastAPI , HTTPException , Request , Body , BackgroundTasks
4342from pydantic import BaseModel , create_model
4443
44+ from labthings_fastapi .message_broker import Message
45+
4546
4647from .middleware .url_for import URLFor
4748from .base_descriptor import (
6869)
6970from .thing_description import type_to_dataschema
7071from .thing_description ._model import ActionAffordance , ActionOp , Form , LinkElement
71- from .utilities import labthings_data
7272
7373
7474if TYPE_CHECKING :
@@ -247,6 +247,20 @@ def response(self) -> InvocationModel:
247247 log = self .log ,
248248 )
249249
250+ def _publish_status (self ) -> None :
251+ """Publish a status change event to any observers.
252+
253+ This should be called after each change to ``self._status``
254+ """
255+ self .thing ._thing_server_interface .publish (
256+ Message (
257+ thing = self .thing .name ,
258+ affordance = self .action .name , # type: ignore[attr-defined]
259+ message_type = "action" ,
260+ payload = self ._status .value ,
261+ )
262+ )
263+
250264 def run (self ) -> None :
251265 """Run the action and track progress.
252266
@@ -282,7 +296,7 @@ def run(self) -> None:
282296 add_thing_log_destination (self .id , self ._log )
283297 with invocation_contexts .set_invocation_id (self .id ):
284298 try :
285- action . emit_changed_event ( self .thing , self . _status . value )
299+ self ._publish_status ( )
286300
287301 thing = self .thing
288302 kwargs = model_to_dict (self .input )
@@ -298,21 +312,21 @@ def run(self) -> None:
298312 with self ._status_lock :
299313 self ._status = InvocationStatus .RUNNING
300314 self ._start_time = datetime .datetime .now ()
301- action . emit_changed_event ( self .thing , self . _status . value )
315+ self ._publish_status ( )
302316
303317 # Actually run the action
304318 ret = action .func (thing , ** kwargs , ** self .dependencies )
305319
306320 with self ._status_lock :
307321 self ._return_value = ret
308322 self ._status = InvocationStatus .COMPLETED
309- action . emit_changed_event ( self .thing , self . _status . value )
323+ self ._publish_status ( )
310324
311325 except InvocationCancelledError :
312326 logger .info (f"Invocation { self .id } was cancelled." )
313327 with self ._status_lock :
314328 self ._status = InvocationStatus .CANCELLED
315- action . emit_changed_event ( self .thing , self . _status . value )
329+ self ._publish_status ( )
316330 except Exception as e : # skipcq: PYL-W0703
317331 # First log
318332 if isinstance (e , InvocationError ):
@@ -332,7 +346,7 @@ def run(self) -> None:
332346 with self ._status_lock :
333347 self ._status = InvocationStatus .ERROR
334348 self ._exception = e
335- action . emit_changed_event ( self .thing , self . _status . value )
349+ self ._publish_status ( )
336350 finally :
337351 with self ._status_lock :
338352 self ._end_time = datetime .datetime .now ()
@@ -810,70 +824,13 @@ def instance_get(self, obj: OwnerT) -> Callable[ActionParams, ActionReturn]:
810824 """
811825
812826 @wraps (self .func )
813- def wrapped (* args : Any , ** kwargs : Any ) -> Any : # noqa: DOC
827+ def wrapped (* args : Any , ** kwargs : Any ) -> Any : # noqa: DOC101, DOC103, DOC201
814828 """Acquire the lock then run `func` with supplied arguments."""
815829 with self .context_for_func (obj ):
816830 return self .func (* args , ** kwargs )
817831
818832 return partial (wrapped , obj )
819833
820- def _observers_set (self , obj : Thing ) -> WeakSet :
821- """Return a set used to notify changes.
822-
823- Note that we need to supply the `~lt.Thing` we are looking at, as in
824- general there may be more than one object of the same type, and
825- descriptor instances are shared between all instances of their class.
826-
827- :param obj: The `~lt.Thing` on which the action is being observed.
828-
829- :return: a weak set of callables to notify on changes to the action.
830- This is used by websocket endpoints.
831- """
832- ld = labthings_data (obj )
833- if self .name not in ld .action_observers :
834- ld .action_observers [self .name ] = WeakSet ()
835- return ld .action_observers [self .name ]
836-
837- def emit_changed_event (self , obj : Thing , status : str ) -> None :
838- """Notify subscribers that the action status has changed.
839-
840- This function is run from within the `.Invocation` thread that
841- is created when an action is called. It must be run from a thread
842- as it is communicating with the event loop via an `asyncio` blocking
843- portal. Async code must not use the blocking portal as it can deadlock
844- the event loop.
845-
846- :param obj: The `~lt.Thing` on which the action is being observed.
847- :param status: The status of the action, to be sent to observers.
848- """
849- obj ._thing_server_interface .start_async_task_soon (
850- self .emit_changed_event_async ,
851- obj ,
852- status ,
853- )
854-
855- async def emit_changed_event_async (self , obj : Thing , value : Any ) -> None :
856- """Notify subscribers that the action status has changed.
857-
858- This is an async function that must be run in the `anyio` event loop.
859- It will send messages to each observer to notify them that something
860- has changed.
861-
862- :param obj: The `~lt.Thing` on which the action is defined.
863- `.ActionDescriptor` objects are unique to the class, but there may
864- be more than one `~lt.Thing` attached to a server with the same class.
865- We use ``obj`` to look up the observers of the current `~lt.Thing`.
866- :param value: The action status to communicate to the observers.
867- """
868- action_name = self .name
869- for observer in self ._observers_set (obj ):
870- await observer .send (
871- {
872- "messageType" : "actionStatus" ,
873- "data" : {"action name" : action_name , "status" : value },
874- }
875- )
876-
877834 def add_to_fastapi (self , app : FastAPI , thing : Thing ) -> None :
878835 """Add this action to a FastAPI app, bound to a particular Thing.
879836
0 commit comments