55from __future__ import annotations
66
77import math
8+ import uuid
89from abc import ABC , abstractmethod
910from datetime import datetime , timedelta
1011from typing import Any , Callable , Generator , Generic , Optional , TypeVar , Union
@@ -178,13 +179,13 @@ def continue_as_new(self, new_input: Any, *, save_events: bool = False) -> None:
178179 pass
179180
180181 @abstractmethod
181- def signal_entity (self , entity_id : str , operation_name : str , * ,
182+ def signal_entity (self , entity_id : Union [ str , 'EntityInstanceId' ] , operation_name : str , * ,
182183 input : Optional [Any ] = None ) -> Task :
183184 """Signal an entity with an operation.
184185
185186 Parameters
186187 ----------
187- entity_id : str
188+ entity_id : Union[ str, EntityInstanceId]
188189 The ID of the entity to signal.
189190 operation_name : str
190191 The name of the operation to perform.
@@ -199,14 +200,14 @@ def signal_entity(self, entity_id: str, operation_name: str, *,
199200 pass
200201
201202 @abstractmethod
202- def call_entity (self , entity_id : str , operation_name : str , * ,
203+ def call_entity (self , entity_id : Union [ str , 'EntityInstanceId' ] , operation_name : str , * ,
203204 input : Optional [TInput ] = None ,
204205 retry_policy : Optional [RetryPolicy ] = None ) -> Task [TOutput ]:
205206 """Call an entity operation and wait for the result.
206207
207208 Parameters
208209 ----------
209- entity_id : str
210+ entity_id : Union[ str, EntityInstanceId]
210211 The ID of the entity to call.
211212 operation_name : str
212213 The name of the operation to perform.
@@ -513,12 +514,48 @@ def task_id(self) -> int:
513514 return self ._task_id
514515
515516
517+ @dataclass
518+ class EntityInstanceId :
519+ """Represents the ID of a durable entity instance."""
520+ name : str
521+ key : str
522+
523+ def __str__ (self ) -> str :
524+ """Return the string representation in the format: name@key"""
525+ return f"{ self .name } @{ self .key } "
526+
527+ @classmethod
528+ def from_string (cls , instance_id : str ) -> 'EntityInstanceId' :
529+ """Parse an entity instance ID from string format (name@key)."""
530+ if '@' not in instance_id :
531+ raise ValueError (f"Invalid entity instance ID format: { instance_id } . Expected format: name@key" )
532+
533+ parts = instance_id .split ('@' , 1 )
534+ if len (parts ) != 2 or not parts [0 ] or not parts [1 ]:
535+ raise ValueError (f"Invalid entity instance ID format: { instance_id } . Expected format: name@key" )
536+
537+ return cls (name = parts [0 ], key = parts [1 ])
538+
539+
540+ class EntityOperationFailedException (Exception ):
541+ """Exception raised when an entity operation fails."""
542+
543+ def __init__ (self , entity_id : EntityInstanceId , operation_name : str , failure_details : FailureDetails ):
544+ self .entity_id = entity_id
545+ self .operation_name = operation_name
546+ self .failure_details = failure_details
547+ super ().__init__ (f"Operation '{ operation_name } ' on entity '{ entity_id } ' failed: { failure_details .message } " )
548+
549+
516550class EntityContext :
551+ """Context for entity operations, providing access to state and scheduling capabilities."""
552+
517553 def __init__ (self , instance_id : str , operation_name : str , is_new_entity : bool = False ):
518554 self ._instance_id = instance_id
519555 self ._operation_name = operation_name
520556 self ._is_new_entity = is_new_entity
521557 self ._state : Optional [Any ] = None
558+ self ._entity_instance_id = EntityInstanceId .from_string (instance_id )
522559
523560 @property
524561 def instance_id (self ) -> str :
@@ -531,6 +568,17 @@ def instance_id(self) -> str:
531568 """
532569 return self ._instance_id
533570
571+ @property
572+ def entity_id (self ) -> EntityInstanceId :
573+ """Get the structured entity instance ID.
574+
575+ Returns
576+ -------
577+ EntityInstanceId
578+ The structured entity instance ID.
579+ """
580+ return self ._entity_instance_id
581+
534582 @property
535583 def operation_name (self ) -> str :
536584 """Get the name of the operation being performed on the entity.
@@ -578,6 +626,64 @@ def set_state(self, state: Any) -> None:
578626 """
579627 self ._state = state
580628
629+ def signal_entity (self , entity_id : Union [str , EntityInstanceId ], operation_name : str , * ,
630+ input : Optional [Any ] = None ) -> None :
631+ """Signal another entity with an operation (fire-and-forget).
632+
633+ Parameters
634+ ----------
635+ entity_id : Union[str, EntityInstanceId]
636+ The ID of the entity to signal.
637+ operation_name : str
638+ The name of the operation to perform.
639+ input : Optional[Any]
640+ The JSON-serializable input to pass to the entity operation.
641+ """
642+ # Store the signal for later processing during entity execution
643+ if not hasattr (self , '_signals' ):
644+ self ._signals = []
645+
646+ entity_id_str = str (entity_id ) if isinstance (entity_id , EntityInstanceId ) else entity_id
647+ self ._signals .append ({
648+ 'entity_id' : entity_id_str ,
649+ 'operation_name' : operation_name ,
650+ 'input' : input
651+ })
652+
653+ def start_new_orchestration (self , orchestrator : Union [Orchestrator [TInput , TOutput ], str ], * ,
654+ input : Optional [TInput ] = None ,
655+ instance_id : Optional [str ] = None ) -> str :
656+ """Start a new orchestration from within an entity operation.
657+
658+ Parameters
659+ ----------
660+ orchestrator : Union[Orchestrator[TInput, TOutput], str]
661+ The orchestrator function or name to start.
662+ input : Optional[TInput]
663+ The JSON-serializable input to pass to the orchestration.
664+ instance_id : Optional[str]
665+ The instance ID for the new orchestration. If not provided, a random UUID will be used.
666+
667+ Returns
668+ -------
669+ str
670+ The instance ID of the new orchestration.
671+ """
672+ # Store the orchestration start request for later processing
673+ if not hasattr (self , '_orchestrations' ):
674+ self ._orchestrations = []
675+
676+ orchestrator_name = orchestrator if isinstance (orchestrator , str ) else get_name (orchestrator )
677+ new_instance_id = instance_id or str (uuid .uuid4 ())
678+
679+ self ._orchestrations .append ({
680+ 'name' : orchestrator_name ,
681+ 'input' : input ,
682+ 'instance_id' : new_instance_id
683+ })
684+
685+ return new_instance_id
686+
581687
582688# Orchestrators are generators that yield tasks and receive/return any type
583689Orchestrator = Callable [[OrchestrationContext , TInput ], Union [Generator [Task , Any , Any ], TOutput ]]
0 commit comments