@@ -222,3 +222,141 @@ def purge_orchestration(self, instance_id: str, recursive: bool = True):
222222 req = pb .PurgeInstancesRequest (instanceId = instance_id , recursive = recursive )
223223 self ._logger .info (f"Purging instance '{ instance_id } '." )
224224 self ._stub .PurgeInstances (req )
225+
226+ def signal_entity (self , entity_id : str , operation_name : str , * ,
227+ input : Optional [Any ] = None ,
228+ request_id : Optional [str ] = None ,
229+ scheduled_time : Optional [datetime ] = None ):
230+ """Signal an entity with an operation.
231+
232+ Parameters
233+ ----------
234+ entity_id : str
235+ The ID of the entity to signal.
236+ operation_name : str
237+ The name of the operation to perform.
238+ input : Optional[Any]
239+ The JSON-serializable input to pass to the entity operation.
240+ request_id : Optional[str]
241+ A unique request ID for the operation. If not provided, a random UUID will be used.
242+ scheduled_time : Optional[datetime]
243+ The time to schedule the operation. If not provided, the operation is scheduled immediately.
244+ """
245+ req = pb .SignalEntityRequest (
246+ instanceId = entity_id ,
247+ name = operation_name ,
248+ input = wrappers_pb2 .StringValue (value = shared .to_json (input )) if input is not None else None ,
249+ requestId = request_id if request_id else uuid .uuid4 ().hex ,
250+ scheduledTime = helpers .new_timestamp (scheduled_time ) if scheduled_time else None )
251+
252+ self ._logger .info (f"Signaling entity '{ entity_id } ' with operation '{ operation_name } '." )
253+ self ._stub .SignalEntity (req )
254+
255+ def get_entity (self , entity_id : str , * , include_state : bool = True ) -> Optional [task .EntityState ]:
256+ """Get the state of an entity.
257+
258+ Parameters
259+ ----------
260+ entity_id : str
261+ The ID of the entity to query.
262+ include_state : bool
263+ Whether to include the entity's state in the response.
264+
265+ Returns
266+ -------
267+ Optional[EntityState]
268+ The entity state if it exists, None otherwise.
269+ """
270+ req = pb .GetEntityRequest (instanceId = entity_id , includeState = include_state )
271+ res : pb .GetEntityResponse = self ._stub .GetEntity (req )
272+
273+ if not res .exists :
274+ return None
275+
276+ entity_metadata = res .entity
277+ return task .EntityState (
278+ instance_id = entity_metadata .instanceId ,
279+ last_modified_time = entity_metadata .lastModifiedTime .ToDatetime (),
280+ backlog_queue_size = entity_metadata .backlogQueueSize ,
281+ locked_by = entity_metadata .lockedBy .value if not helpers .is_empty (entity_metadata .lockedBy ) else None ,
282+ serialized_state = entity_metadata .serializedState .value if not helpers .is_empty (entity_metadata .serializedState ) else None )
283+
284+ def query_entities (self , query : task .EntityQuery ) -> task .EntityQueryResult :
285+ """Query entities based on the provided criteria.
286+
287+ Parameters
288+ ----------
289+ query : EntityQuery
290+ The query criteria for entities.
291+
292+ Returns
293+ -------
294+ EntityQueryResult
295+ The query result containing matching entities and continuation token.
296+ """
297+ # Build the protobuf query
298+ pb_query = pb .EntityQuery (
299+ includeState = query .include_state ,
300+ includeTransient = query .include_transient )
301+
302+ if query .instance_id_starts_with is not None :
303+ pb_query .instanceIdStartsWith = wrappers_pb2 .StringValue (value = query .instance_id_starts_with )
304+ if query .last_modified_from is not None :
305+ pb_query .lastModifiedFrom = helpers .new_timestamp (query .last_modified_from )
306+ if query .last_modified_to is not None :
307+ pb_query .lastModifiedTo = helpers .new_timestamp (query .last_modified_to )
308+ if query .page_size is not None :
309+ pb_query .pageSize = wrappers_pb2 .Int32Value (value = query .page_size )
310+ if query .continuation_token is not None :
311+ pb_query .continuationToken = wrappers_pb2 .StringValue (value = query .continuation_token )
312+
313+ req = pb .QueryEntitiesRequest (query = pb_query )
314+ res : pb .QueryEntitiesResponse = self ._stub .QueryEntities (req )
315+
316+ # Convert response to Python objects
317+ entities = []
318+ for entity_metadata in res .entities :
319+ entities .append (task .EntityState (
320+ instance_id = entity_metadata .instanceId ,
321+ last_modified_time = entity_metadata .lastModifiedTime .ToDatetime (),
322+ backlog_queue_size = entity_metadata .backlogQueueSize ,
323+ locked_by = entity_metadata .lockedBy .value if not helpers .is_empty (entity_metadata .lockedBy ) else None ,
324+ serialized_state = entity_metadata .serializedState .value if not helpers .is_empty (entity_metadata .serializedState ) else None ))
325+
326+ return task .EntityQueryResult (
327+ entities = entities ,
328+ continuation_token = res .continuationToken .value if not helpers .is_empty (res .continuationToken ) else None )
329+
330+ def clean_entity_storage (self , * ,
331+ remove_empty_entities : bool = True ,
332+ release_orphaned_locks : bool = True ,
333+ continuation_token : Optional [str ] = None ) -> tuple [int , int , Optional [str ]]:
334+ """Clean up entity storage by removing empty entities and releasing orphaned locks.
335+
336+ Parameters
337+ ----------
338+ remove_empty_entities : bool
339+ Whether to remove entities that have no state.
340+ release_orphaned_locks : bool
341+ Whether to release locks that are no longer held by active orchestrations.
342+ continuation_token : Optional[str]
343+ A continuation token from a previous cleanup operation.
344+
345+ Returns
346+ -------
347+ tuple[int, int, Optional[str]]
348+ A tuple containing (empty_entities_removed, orphaned_locks_released, continuation_token).
349+ """
350+ req = pb .CleanEntityStorageRequest (
351+ removeEmptyEntities = remove_empty_entities ,
352+ releaseOrphanedLocks = release_orphaned_locks )
353+
354+ if continuation_token is not None :
355+ req .continuationToken = wrappers_pb2 .StringValue (value = continuation_token )
356+
357+ self ._logger .info ("Cleaning entity storage." )
358+ res : pb .CleanEntityStorageResponse = self ._stub .CleanEntityStorage (req )
359+
360+ return (res .emptyEntitiesRemoved ,
361+ res .orphanedLocksReleased ,
362+ res .continuationToken .value if not helpers .is_empty (res .continuationToken ) else None )
0 commit comments