Co-authored-by: Wenonah Zhang <wenhzha@microsoft.com>
Co-authored-by: wenhzha <53274673+wenhzha@users.noreply.github.com>
This commit is contained in:
David Justo 2020-12-03 11:26:29 -08:00 коммит произвёл GitHub
Родитель d95c56dae2
Коммит 46a050508f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
39 изменённых файлов: 1791 добавлений и 17 удалений

Просмотреть файл

@ -3,14 +3,20 @@
Exposes the different API components intended for public consumption
"""
from .orchestrator import Orchestrator
from .entity import Entity
from .models.utils.entity_utils import EntityId
from .models.DurableOrchestrationClient import DurableOrchestrationClient
from .models.DurableOrchestrationContext import DurableOrchestrationContext
from .models.DurableEntityContext import DurableEntityContext
from .models.RetryOptions import RetryOptions
from .models.TokenSource import ManagedIdentityTokenSource
__all__ = [
'Orchestrator',
'Entity',
'EntityId',
'DurableOrchestrationClient',
'DurableEntityContext',
'DurableOrchestrationContext',
'ManagedIdentityTokenSource',
'RetryOptions'

Просмотреть файл

@ -0,0 +1,119 @@
from .models import DurableEntityContext
from .models.entities import OperationResult, EntityState
from datetime import datetime
from typing import Callable, Any, List, Dict
class InternalEntityException(Exception):
pass
class Entity:
"""Durable Entity Class.
Responsible for executing the user-defined entity function.
"""
def __init__(self, entity_func: Callable[[DurableEntityContext], None]):
"""Create a new entity for the user-defined entity.
Responsible for executing the user-defined entity function
Parameters
----------
entity_func: Callable[[DurableEntityContext], Generator[Any, Any, Any]]
The user defined entity function
"""
self.fn: Callable[[DurableEntityContext], None] = entity_func
def handle(self, context: DurableEntityContext, batch: List[Dict[str, Any]]) -> str:
"""Handle the execution of the user-defined entity function.
Loops over the batch, which serves to specify inputs to the entity,
and collects results and generates a final state, which are returned.
Parameters
----------
context: DurableEntityContext
The entity context of the entity, which the user interacts with as their Durable API
Returns
-------
str
A JSON-formatted string representing the output state, results, and exceptions for the
entity execution.
"""
response = EntityState(results=[], signals=[])
for operation_data in batch:
result: Any = None
is_error: bool = False
start_time: datetime = datetime.now()
try:
# populate context
operation = operation_data["name"]
if operation is None:
raise InternalEntityException("Durable Functions Internal Error: Entity operation was missing a name field")
context._operation = operation
context._input = operation_data["input"]
self.fn(context)
result = context._result
except InternalEntityException as e:
raise e
except Exception as e:
is_error = True
result = str(e)
duration: int = self._elapsed_milliseconds_since(start_time)
operation_result = OperationResult(
is_error=is_error,
duration=duration,
result=result
)
response.results.append(operation_result)
response.state = context._state
response.entity_exists = context._exists
return response.to_json_string()
@classmethod
def create(cls, fn: Callable[[DurableEntityContext], None]) -> Callable[[Any], str]:
"""Create an instance of the entity class.
Parameters
----------
fn (Callable[[DurableEntityContext], None]): [description]
Returns
-------
Callable[[Any], str]
Handle function of the newly created entity client
"""
def handle(context) -> str:
# It is not clear when the context JSON would be found
# inside a "body"-key, but this pattern matches the
# orchestrator implementation, so we keep it for safety.
context_body = getattr(context, "body", None)
if context_body is None:
context_body = context
ctx, batch = DurableEntityContext.from_json(context_body)
return Entity(fn).handle(ctx, batch)
return handle
def _elapsed_milliseconds_since(self, start_time: datetime) -> int:
"""Calculate the elapsed time, in milliseconds, from the start_time to the present.
Parameters
----------
start_time: datetime
The timestamp of when the entity began processing a batched request.
Returns
-------
int
The time, in millseconds, from start_time to now
"""
end_time = datetime.now()
time_diff = end_time - start_time
elapsed_time = int(time_diff.total_seconds() * 1000)
return elapsed_time

Просмотреть файл

@ -0,0 +1,200 @@
from typing import Optional, Any, Dict, Tuple, List, Callable
from azure.functions._durable_functions import _deserialize_custom_object
import json
class DurableEntityContext:
"""Context of the durable entity context.
Describes the API used to specify durable entity user code.
"""
def __init__(self,
name: str,
key: str,
exists: bool,
state: Any):
"""Context of the durable entity context.
Describes the API used to specify durable entity user code.
Parameters
----------
name: str
The name of the Durable Entity
key: str
The key of the Durable Entity
exists: bool
Flag to determine if the entity exists
state: Any
The internal state of the Durable Entity
"""
self._entity_name: str = name
self._entity_key: str = key
self._exists: bool = exists
self._is_newly_constructed: bool = False
self._state: Any = state
self._input: Any = None
self._operation: Optional[str] = None
self._result: Any = None
@property
def entity_name(self) -> str:
"""Get the name of the Entity.
Returns
-------
str
The name of the entity
"""
return self._entity_name
@property
def entity_key(self) -> str:
"""Get the Entity key.
Returns
-------
str
The entity key
"""
return self._entity_key
@property
def operation_name(self) -> Optional[str]:
"""Get the current operation name.
Returns
-------
Optional[str]
The current operation name
"""
if self._operation is None:
raise Exception("Entity operation is unassigned")
return self._operation
@property
def is_newly_constructed(self) -> bool:
"""Determine if the Entity was newly constructed.
Returns
-------
bool
True if the Entity was newly constructed. False otherwise.
"""
# This is not updated at the moment, as its semantics are unclear
return self._is_newly_constructed
@classmethod
def from_json(cls, json_str: str) -> Tuple['DurableEntityContext', List[Dict[str, Any]]]:
"""Instantiate a DurableEntityContext from a JSON-formatted string.
Parameters
----------
json_string: str
A JSON-formatted string, returned by the durable-extension,
which represents the entity context
Returns
-------
DurableEntityContext
The DurableEntityContext originated from the input string
"""
json_dict = json.loads(json_str)
json_dict["name"] = json_dict["self"]["name"]
json_dict["key"] = json_dict["self"]["key"]
json_dict.pop("self")
serialized_state = json_dict["state"]
if serialized_state is not None:
json_dict["state"] = from_json_util(serialized_state)
batch = json_dict.pop("batch")
return cls(**json_dict), batch
def set_state(self, state: Any) -> None:
"""Set the state of the entity.
Parameter
---------
state: Any
The new state of the entity
"""
self._exists = True
# should only serialize the state at the end of the batch
self._state = state
def get_state(self, initializer: Optional[Callable[[], Any]] = None) -> Any:
"""Get the current state of this entity.
Parameters
----------
initializer: Optional[Callable[[], Any]]
A 0-argument function to provide an initial state. Defaults to None.
Returns
-------
Any
The current state of the entity
"""
state = self._state
if state is not None:
return state
elif initializer:
if not callable(initializer):
raise Exception("initializer argument needs to be a callable function")
state = initializer()
return state
def get_input(self) -> Any:
"""Get the input for this operation.
Returns
-------
Any
The input for the current operation
"""
input_ = None
req_input = self._input
req_input = json.loads(req_input)
input_ = None if req_input is None else from_json_util(req_input)
return input_
def set_result(self, result: Any) -> None:
"""Set the result (return value) of the entity.
Paramaters
----------
result: Any
The result / return value for the entity
"""
self._exists = True
self._result = result
def destruct_on_exit(self) -> None:
"""Delete this entity after the operation completes."""
self._exists = False
self._state = None
def from_json_util(self, json_str: str) -> Any:
"""Load an arbitrary datatype from its JSON representation.
The Out-of-proc SDK has a special JSON encoding strategy
to enable arbitrary datatypes to be serialized. This utility
loads a JSON with the assumption that it follows that encoding
method.
Parameters
----------
json_str: str
A JSON-formatted string, from durable-extension
Returns
-------
Any:
The original datatype that was serialized
"""
return json.loads(json_str, object_hook=_deserialize_custom_object)

Просмотреть файл

@ -13,6 +13,7 @@ from .RpcManagementOptions import RpcManagementOptions
from .OrchestrationRuntimeStatus import OrchestrationRuntimeStatus
from ..models.DurableOrchestrationBindings import DurableOrchestrationBindings
from .utils.http_utils import get_async_request, post_async_request, delete_async_request
from .utils.entity_utils import EntityId
from azure.functions._durable_functions import _serialize_custom_object
@ -353,7 +354,6 @@ class DurableOrchestrationClient:
PurgeHistoryResult
The results of the request to purge history
"""
# TODO: do we really want folks to us this without specifying all the args?
options = RpcManagementOptions(created_time_from=created_time_from,
created_time_to=created_time_to,
runtime_status=runtime_status)
@ -457,6 +457,57 @@ class DurableOrchestrationClient:
else:
return self.create_check_status_response(request, instance_id)
async def signal_entity(self, entityId: EntityId, operation_name: str,
operation_input: Optional[Any] = None,
task_hub_name: Optional[str] = None,
connection_name: Optional[str] = None) -> None:
"""Signals an entity to perform an operation.
Parameters
----------
entityId : EntityId
The EntityId of the targeted entity to perform operation.
operation_name: str
The name of the operation.
operation_input: Optional[Any]
The content for the operation.
task_hub_name: Optional[str]
The task hub name of the target entity.
connection_name: Optional[str]
The name of the connection string associated with [task_hub_name].
Raises
------
Exception:
When the signal entity call failed with an unexpected status code
Returns
-------
None
"""
options = RpcManagementOptions(operation_name=operation_name,
connection_name=connection_name,
task_hub_name=task_hub_name,
entity_Id=entityId)
request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
response = await self._post_async_request(
request_url,
json.dumps(operation_input) if operation_input else None)
switch_statement = {
202: lambda: None # signal accepted
}
has_error_message = switch_statement.get(
response[0],
lambda: f"The operation failed with an unexpected status code {response[0]}")
error_message = has_error_message()
if error_message:
raise Exception(error_message)
@staticmethod
def _create_http_response(
status_code: int, body: Union[str, Any]) -> func.HttpResponse:

Просмотреть файл

@ -9,9 +9,11 @@ from .history import HistoryEvent, HistoryEventType
from .actions import Action
from ..models.Task import Task
from ..models.TokenSource import TokenSource
from .utils.entity_utils import EntityId
from ..tasks import call_activity_task, task_all, task_any, call_activity_with_retry_task, \
wait_for_external_event_task, continue_as_new, new_uuid, call_http, create_timer_task, \
call_sub_orchestrator_task, call_sub_orchestrator_with_retry_task
call_sub_orchestrator_task, call_sub_orchestrator_with_retry_task, call_entity_task, \
signal_entity_task
from azure.functions._durable_functions import _deserialize_custom_object
@ -34,7 +36,6 @@ class DurableOrchestrationContext:
self._new_uuid_counter: int = 0
self._sub_orchestrator_counter: int = 0
self._continue_as_new_flag: bool = False
# TODO: waiting on the `continue_as_new` intellisense until that's implemented
self.decision_started_event: HistoryEvent = \
[e_ for e_ in self.histories
if e_.event_type == HistoryEventType.ORCHESTRATOR_STARTED][0]
@ -359,6 +360,46 @@ class DurableOrchestrationContext:
"""
return self._function_context
def call_entity(self, entityId: EntityId,
operationName: str, operationInput: Optional[Any] = None):
"""Get the result of Durable Entity operation given some input.
Parameters
----------
entityId: EntityId
The ID of the entity to call
operationName: str
The operation to execute
operationInput: Optional[Any]
The input for tne operation, defaults to None.
Returns
-------
Task
A Task of the entity call
"""
return call_entity_task(self.histories, entityId, operationName, operationInput)
def signal_entity(self, entityId: EntityId,
operationName: str, operationInput: Optional[Any] = None):
"""Send a signal operation to Durable Entity given some input.
Parameters
----------
entityId: EntityId
The ID of the entity to call
operationName: str
The operation to execute
operationInput: Optional[Any]
The input for tne operation, defaults to None.
Returns
-------
Task
A Task of the entity signal
"""
return signal_entity_task(self, self.histories, entityId, operationName, operationInput)
@property
def will_continue_as_new(self) -> bool:
"""Return true if continue_as_new was called."""

Просмотреть файл

@ -4,6 +4,8 @@ from typing import Any, List, Optional
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
from azure.durable_functions.models.OrchestrationRuntimeStatus import OrchestrationRuntimeStatus
from .utils.entity_utils import EntityId
class RpcManagementOptions:
"""Class used to collect the options for getting orchestration status."""
@ -12,7 +14,9 @@ class RpcManagementOptions:
connection_name: str = None, show_history: bool = None,
show_history_output: bool = None, created_time_from: datetime = None,
created_time_to: datetime = None,
runtime_status: List[OrchestrationRuntimeStatus] = None, show_input: bool = None):
runtime_status: List[OrchestrationRuntimeStatus] = None, show_input: bool = None,
operation_name: str = None,
entity_Id: EntityId = None):
self._instance_id = instance_id
self._task_hub_name = task_hub_name
self._connection_name = connection_name
@ -22,6 +26,8 @@ class RpcManagementOptions:
self._created_time_to = created_time_to
self._runtime_status = runtime_status
self._show_input = show_input
self.operation_name = operation_name
self.entity_Id = entity_Id
@staticmethod
def _add_arg(query: List[str], name: str, value: Any):
@ -55,7 +61,10 @@ class RpcManagementOptions:
if base_url is None:
raise ValueError("orchestration bindings has not RPC base url")
url = f"{base_url}instances/{self._instance_id if self._instance_id else ''}"
if self.entity_Id:
url = f'{base_url}{EntityId.get_entity_id_url_path(self.entity_Id)}'
else:
url = f"{base_url}instances/{self._instance_id if self._instance_id else ''}"
query: List[str] = []
@ -66,6 +75,7 @@ class RpcManagementOptions:
self._add_arg(query, 'showHistoryOutput', self._show_history_output)
self._add_date_arg(query, 'createdTimeFrom', self._created_time_from)
self._add_date_arg(query, 'createdTimeTo', self._created_time_to)
self._add_arg(query, 'op', self.operation_name)
if self._runtime_status is not None and len(self._runtime_status) > 0:
runtime_status = ",".join(r.value for r in self._runtime_status)
self._add_arg(query, 'runtimeStatus', runtime_status)

Просмотреть файл

@ -10,10 +10,12 @@ from .Task import Task
from .TaskSet import TaskSet
from .DurableHttpRequest import DurableHttpRequest
from .TokenSource import ManagedIdentityTokenSource
from .DurableEntityContext import DurableEntityContext
__all__ = [
'DurableOrchestrationBindings',
'DurableOrchestrationClient',
'DurableEntityContext',
'DurableOrchestrationContext',
'DurableHttpRequest',
'ManagedIdentityTokenSource',

Просмотреть файл

@ -11,4 +11,6 @@ class ActionType(IntEnum):
CONTINUE_AS_NEW: int = 4
CREATE_TIMER: int = 5
WAIT_FOR_EXTERNAL_EVENT: int = 6
CALL_ENTITY = 7
CALL_HTTP: int = 8
SIGNAL_ENTITY: int = 9

Просмотреть файл

@ -0,0 +1,46 @@
from typing import Any, Dict
from .Action import Action
from .ActionType import ActionType
from ..utils.json_utils import add_attrib
from json import dumps
from azure.functions._durable_functions import _serialize_custom_object
from ..utils.entity_utils import EntityId
class CallEntityAction(Action):
"""Defines the structure of the Call Entity object.
Provides the information needed by the durable extension to be able to call an activity
"""
def __init__(self, entity_id: EntityId, operation: str, input_=None):
self.entity_id: EntityId = entity_id
# Validating that EntityId exists before trying to parse its instanceId
if not self.entity_id:
raise ValueError("entity_id cannot be empty")
self.instance_id: str = EntityId.get_scheduler_id(entity_id)
self.operation: str = operation
self.input_: str = dumps(input_, default=_serialize_custom_object)
@property
def action_type(self) -> int:
"""Get the type of action this class represents."""
return ActionType.CALL_ENTITY
def to_json(self) -> Dict[str, Any]:
"""Convert object into a json dictionary.
Returns
-------
Dict[str, Any]
The instance of the class converted into a json dictionary
"""
json_dict: Dict[str, Any] = {}
add_attrib(json_dict, self, "action_type", "actionType")
add_attrib(json_dict, self, 'instance_id', 'instanceId')
add_attrib(json_dict, self, 'operation', 'operation')
add_attrib(json_dict, self, 'input_', 'input')
return json_dict

Просмотреть файл

@ -0,0 +1,47 @@
from typing import Any, Dict
from .Action import Action
from .ActionType import ActionType
from ..utils.json_utils import add_attrib
from json import dumps
from azure.functions._durable_functions import _serialize_custom_object
from ..utils.entity_utils import EntityId
class SignalEntityAction(Action):
"""Defines the structure of the Signal Entity object.
Provides the information needed by the durable extension to be able to signal an entity
"""
def __init__(self, entity_id: EntityId, operation: str, input_=None):
self.entity_id: EntityId = entity_id
# Validating that EntityId exists before trying to parse its instanceId
if not self.entity_id:
raise ValueError("entity_id cannot be empty")
self.instance_id: str = EntityId.get_scheduler_id(entity_id)
self.operation: str = operation
self.input_: str = dumps(input_, default=_serialize_custom_object)
@property
def action_type(self) -> int:
"""Get the type of action this class represents."""
return ActionType.SIGNAL_ENTITY
def to_json(self) -> Dict[str, Any]:
"""Convert object into a json dictionary.
Returns
-------
Dict[str, Any]
The instance of the class converted into a json dictionary
"""
json_dict: Dict[str, Any] = {}
add_attrib(json_dict, self, "action_type", "actionType")
add_attrib(json_dict, self, 'instance_id', 'instanceId')
add_attrib(json_dict, self, 'operation', 'operation')
add_attrib(json_dict, self, 'input_', 'input')
return json_dict

Просмотреть файл

@ -0,0 +1,74 @@
from typing import List, Optional, Dict, Any
from .Signal import Signal
from azure.functions._durable_functions import _serialize_custom_object
from .OperationResult import OperationResult
import json
class EntityState:
"""Entity State.
Used to communicate the state of the entity back to the durable extension
"""
def __init__(self,
results: List[OperationResult],
signals: List[Signal],
entity_exists: bool = False,
state: Optional[str] = None):
self.entity_exists = entity_exists
self.state = state
self._results = results
self._signals = signals
@property
def results(self) -> List[OperationResult]:
"""Get list of results of the entity.
Returns
-------
List[OperationResult]:
The results of the entity
"""
return self._results
@property
def signals(self) -> List[Signal]:
"""Get list of signals to the entity.
Returns
-------
List[Signal]:
The signals of the entity
"""
return self._signals
def to_json(self) -> Dict[str, Any]:
"""Convert object into a json dictionary.
Returns
-------
Dict[str, Any]
The instance of the class converted into a json dictionary
"""
json_dict: Dict[str, Any] = {}
# Serialize the OperationResult list
serialized_results = list(map(lambda x: x.to_json(), self.results))
json_dict["entityExists"] = self.entity_exists
json_dict["entityState"] = json.dumps(self.state, default=_serialize_custom_object)
json_dict["results"] = serialized_results
json_dict["signals"] = self.signals
return json_dict
def to_json_string(self) -> str:
"""Convert object into a json string.
Returns
-------
str
The instance of the object in json string format
"""
# TODO: Same implementation as in Orchestrator.py, we should refactor to shared a base
json_dict = self.to_json()
return json.dumps(json_dict)

Просмотреть файл

@ -0,0 +1,76 @@
from typing import Optional, Dict, Any
from azure.functions._durable_functions import _serialize_custom_object
import json
class OperationResult:
"""OperationResult.
The result of an Entity operation.
"""
def __init__(self,
is_error: bool,
duration: int,
result: Optional[str] = None):
"""Instantiate an OperationResult.
Parameters
----------
is_error: bool
Whether or not the operation resulted in an exception.
duration: int
How long the operation took, in milliseconds.
result: Optional[str]
The operation result. Defaults to None.
"""
self._is_error: bool = is_error
self._duration: int = duration
self._result: Optional[str] = result
@property
def is_error(self) -> bool:
"""Determine if the operation resulted in an error.
Returns
-------
bool
True if the operation resulted in error. Otherwise False.
"""
return self._is_error
@property
def duration(self) -> int:
"""Get the duration of this operation.
Returns
-------
int:
The duration of this operation, in milliseconds
"""
return self._duration
@property
def result(self) -> Any:
"""Get the operation's result.
Returns
-------
Any
The operation's result
"""
return self._result
def to_json(self) -> Dict[str, Any]:
"""Represent OperationResult as a JSON-serializable Dict.
Returns
-------
Dict[str, Any]
A JSON-serializable Dict of the OperationResult
"""
to_json: Dict[str, Any] = {}
to_json["isError"] = self.is_error
to_json["duration"] = self.duration
to_json["result"] = json.dumps(self.result, default=_serialize_custom_object)
return to_json

Просмотреть файл

@ -0,0 +1,53 @@
from typing import List, Optional, Any
from ..utils.entity_utils import EntityId
import json
class RequestMessage:
"""RequestMessage.
Specifies a request to an entity.
"""
def __init__(self,
id_: str,
name: Optional[str] = None,
signal: Optional[bool] = None,
input_: Optional[str] = None,
arg: Optional[Any] = None,
parent: Optional[str] = None,
lockset: Optional[List[EntityId]] = None,
pos: Optional[int] = None,
**kwargs):
# TODO: this class has too many optionals, may speak to
# over-caution, but it mimics the JS class. Investigate if
# these many Optionals are necessary.
self.id = id_
self.name = name
self.signal = signal
self.input = input_
self.arg = arg
self.parent = parent
self.lockset = lockset
self.pos = pos
@classmethod
def from_json(cls, json_str: str) -> 'RequestMessage':
"""Instantiate a RequestMessage object from the durable-extension provided JSON data.
Parameters
----------
json_str: str
A durable-extension provided json-formatted string representation of
a RequestMessage
Returns
-------
RequestMessage:
A RequestMessage object from the json_str parameter
"""
# We replace the `id` key for `id_` to avoid clashes with reserved
# identifiers in Python
json_dict = json.loads(json_str)
json_dict["id_"] = json_dict.pop("id")
return cls(**json_dict)

Просмотреть файл

@ -0,0 +1,38 @@
from typing import Dict, Any
class ResponseMessage:
"""ResponseMessage.
Specifies the response of an entity, as processed by the durable-extension.
"""
def __init__(self, result: str):
"""Instantiate a ResponseMessage.
Specifies the response of an entity, as processed by the durable-extension.
Parameters
----------
result: str
The result provided by the entity
"""
self.result = result
# TODO: JS has an additional exceptionType field, but does not use it
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> 'ResponseMessage':
"""Instantiate a ResponseMessage from a dict of the JSON-response by the extension.
Parameters
----------
d: Dict[str, Any]
The dictionary parsed from the JSON-response by the durable-extension
Returns
-------
ResponseMessage:
The ResponseMessage built from the provided dictionary
"""
result = cls(d["result"])
return result

Просмотреть файл

@ -0,0 +1,62 @@
from ..utils.entity_utils import EntityId
class Signal:
"""An EntitySignal.
Describes a signal call to a Durable Entity.
"""
def __init__(self,
target: EntityId,
name: str,
input_: str):
"""Instantiate an EntitySignal.
Instantiate a signal call to a Durable Entity.
Parameters
----------
target: EntityId
The target of signal
name: str
The name of the signal
input_: str
The signal's input
"""
self._target = target
self._name = name
self._input = input_
@property
def target(self) -> EntityId:
"""Get the Signal's target entity.
Returns
-------
EntityId
EntityId of the target
"""
return self._target
@property
def name(self) -> str:
"""Get the Signal's name.
Returns
-------
str
The Signal's name
"""
return self._name
@property
def input(self) -> str:
"""Get the Signal's input.
Returns
-------
str
The Signal's input
"""
return self._input

Просмотреть файл

@ -0,0 +1,17 @@
"""Utility classes used by the Durable Function python library for dealing with entities.
_Internal Only_
"""
from .RequestMessage import RequestMessage
from .OperationResult import OperationResult
from .EntityState import EntityState
from .Signal import Signal
__all__ = [
'RequestMessage',
'OperationResult',
'Signal',
'EntityState'
]

Просмотреть файл

@ -0,0 +1,91 @@
class EntityId:
"""EntityId.
It identifies an entity by its name and its key.
"""
def __init__(self, name: str, key: str):
"""Instantiate an EntityId object.
Identifies an entity by its name and its key.
Parameters
----------
name: str
The entity name
key: str
The entity key
Raises
------
ValueError: If the entity name or key are the empty string
"""
if name == "":
raise ValueError("Entity name cannot be empty")
if key == "":
raise ValueError("Entity key cannot be empty")
self.name: str = name
self.key: str = key
@staticmethod
def get_scheduler_id(entity_id: 'EntityId') -> str:
"""Produce a SchedulerId from an EntityId.
Parameters
----------
entity_id: EntityId
An EntityId object
Returns
-------
str:
A SchedulerId representation of the input EntityId
"""
return f"@{entity_id.name.lower()}@{entity_id.key}"
@staticmethod
def get_entity_id(scheduler_id: str) -> 'EntityId':
"""Return an EntityId from a SchedulerId string.
Parameters
----------
scheduler_id: str
The SchedulerId in which to base the returned EntityId
Raises
------
ValueError:
When the SchedulerId string does not have the expected format
Returns
-------
EntityId:
An EntityId object based on the SchedulerId string
"""
sched_id_truncated = scheduler_id[1:] # we drop the starting `@`
components = sched_id_truncated.split("@")
if len(components) != 2:
raise ValueError("Unexpected format in SchedulerId")
[name, key] = components
return EntityId(name, key)
@staticmethod
def get_entity_id_url_path(entity_id: 'EntityId') -> str:
"""Print the the entity url path.
Returns
-------
str:
A url path of the EntityId
"""
return f'entities/{entity_id.name}/{entity_id.key}'
def __str__(self) -> str:
"""Print the string representation of this EntityId.
Returns
-------
str:
A SchedulerId-based string representation of the EntityId
"""
return EntityId.get_scheduler_id(entity_id=self)

Просмотреть файл

@ -11,12 +11,16 @@ from .continue_as_new import continue_as_new
from .new_uuid import new_uuid
from .call_http import call_http
from .create_timer import create_timer_task
from .call_entity import call_entity_task
from .signal_entity import signal_entity_task
__all__ = [
'call_activity_task',
'call_activity_with_retry_task',
'call_sub_orchestrator_task',
'call_sub_orchestrator_with_retry_task',
'call_entity_task',
'signal_entity_task',
'call_http',
'continue_as_new',
'new_uuid',

Просмотреть файл

@ -0,0 +1,83 @@
from typing import List, Any, Optional
from ..models.Task import (
Task)
from ..models.actions.CallEntityAction import CallEntityAction
from ..models.history import HistoryEvent, HistoryEventType
from .task_utilities import set_processed, parse_history_event, find_event
from ..models.utils.entity_utils import EntityId
from ..models.entities.RequestMessage import RequestMessage
from ..models.entities.ResponseMessage import ResponseMessage
import json
def call_entity_task(
state: List[HistoryEvent],
entity_id: EntityId,
operation_name: str = "",
input_: Optional[Any] = None):
"""Determine the status of a call-entity task.
It the task hasn't been scheduled, it returns a Task to schedule. If the task completed,
we return a completed Task, to process its result.
Parameters
----------
state: List[HistoryEvent]
The list of history events to search over to determine the
current state of the callEntity Task.
entity_id: EntityId
An identifier for the entity to call.
operation_name: str
The name of the operation the entity needs to execute.
input_: Any
The JSON-serializable input to pass to the activity function.
Returns
-------
Task
A Durable Task that completes when the called entity completes or fails.
"""
new_action = CallEntityAction(entity_id, operation_name, input_)
scheduler_id = EntityId.get_scheduler_id(entity_id=entity_id)
hist_type = HistoryEventType.EVENT_SENT
extra_constraints = {
"InstanceId": scheduler_id,
"Name": "op"
}
event_sent = find_event(state, hist_type, extra_constraints)
event_raised = None
if event_sent:
event_input = None
if hasattr(event_sent, "Input"):
event_input = RequestMessage.from_json(event_sent.Input)
hist_type = HistoryEventType.EVENT_RAISED
extra_constraints = {
"Name": event_input.id
}
event_raised = find_event(state, hist_type, extra_constraints)
# TODO: does it make sense to have an event_sent but no `Input` attribute ?
# If not, we should raise an exception here
set_processed([event_sent, event_raised])
if event_raised is not None:
response = parse_history_event(event_raised)
response = ResponseMessage.from_dict(response)
# TODO: json.loads inside parse_history_event is not recursive
# investigate if response.result is used elsewhere,
# which probably requires another deserialization
result = json.loads(response.result)
return Task(
is_completed=True,
is_faulted=False,
action=new_action,
result=result,
timestamp=event_raised.timestamp,
id_=event_raised.Name) # event_raised.TaskScheduledId
# TODO: this may be missing exception handling, as is JS
return Task(is_completed=False, is_faulted=False, action=new_action)

Просмотреть файл

@ -0,0 +1,45 @@
from typing import List, Any, Optional
from ..models.actions.SignalEntityAction import SignalEntityAction
from ..models.history import HistoryEvent, HistoryEventType
from .task_utilities import set_processed, find_event
from ..models.utils.entity_utils import EntityId
def signal_entity_task(
context,
state: List[HistoryEvent],
entity_id: EntityId,
operation_name: str = "",
input_: Optional[Any] = None):
"""Signal a entity operation.
It the action hasn't been scheduled, it appends the action.
If the action has been scheduled, no ops.
Parameters
----------
state: List[HistoryEvent]
The list of history events to search over to determine the
current state of the callEntity Task.
entity_id: EntityId
An identifier for the entity to call.
operation_name: str
The name of the operation the entity needs to execute.
input_: Any
The JSON-serializable input to pass to the activity function.
"""
new_action = SignalEntityAction(entity_id, operation_name, input_)
scheduler_id = EntityId.get_scheduler_id(entity_id=entity_id)
hist_type = HistoryEventType.EVENT_SENT
extra_constraints = {
"InstanceId": scheduler_id,
"Name": "op"
}
event_sent = find_event(state, hist_type, extra_constraints)
set_processed([event_sent])
context.actions.append([new_action])
if event_sent:
return

Просмотреть файл

@ -1,8 +1,8 @@
import json
from ..models.history import HistoryEventType, HistoryEvent
from azure.functions._durable_functions import _deserialize_custom_object
from dateutil import parser
from typing import List, Optional
from datetime import datetime
from typing import List, Optional, Dict, Any
from ..models.actions.Action import Action
from ..models.Task import Task
@ -22,15 +22,65 @@ def parse_history_event(directive_result):
# We provide the ability to deserialize custom objects, because the output of this
# will be passed directly to the orchestrator as the output of some activity
if event_type == HistoryEventType.EVENT_RAISED:
return json.loads(directive_result.Input, object_hook=_deserialize_custom_object)
if event_type == HistoryEventType.SUB_ORCHESTRATION_INSTANCE_COMPLETED:
return json.loads(directive_result.Result, object_hook=_deserialize_custom_object)
if event_type == HistoryEventType.TASK_COMPLETED:
return json.loads(directive_result.Result, object_hook=_deserialize_custom_object)
if event_type == HistoryEventType.EVENT_RAISED:
# TODO: Investigate why the payload is in "Input" instead of "Result"
return json.loads(directive_result.Input, object_hook=_deserialize_custom_object)
return None
def find_event(state: List[HistoryEvent], event_type: HistoryEventType,
extra_constraints: Dict[str, Any]) -> Optional[HistoryEvent]:
"""Find event in the histories array as per some constraints.
Parameters
----------
state: List[HistoryEvent]
The list of events so far in the orchestaration
event_type: HistoryEventType
The type of the event we're looking for
extra_constraints: Dict[str, Any]
A dictionary of key-value pairs where the key is a property of the
sought-after event, and value are its expected contents.
Returns
-------
Optional[HistoryEvent]
The event being searched-for, if found. Else, None.
"""
def satisfies_contraints(e: HistoryEvent) -> bool:
"""Determine if an event matches our search criteria.
Parameters
----------
e: HistoryEvent
An event from the state array
Returns
-------
bool
True if the event matches our constraints. Else, False.
"""
for attr, val in extra_constraints.items():
if hasattr(e, attr) and getattr(e, attr) == val:
continue
else:
return False
return True
tasks = [e for e in state
if e.event_type == event_type
and satisfies_contraints(e) and not e.is_processed]
if len(tasks) == 0:
return None
return tasks[0]
def find_event_raised(state, name):
"""Find if the event with the given event name is raised.
@ -251,8 +301,9 @@ def find_sub_orchestration(
err = "Tried to lookup suborchestration in history but had not name to reference it."
raise ValueError(err)
# TODO: The HistoryEvent does not necessarily have an name or an instance_id
# We should create sub-classes of these types like JS does
# TODO: The HistoryEvent does not necessarily have a name or an instance_id
# We should create sub-classes of these types like JS does, to ensure their
# precense.
err_message: str = ""
if not(event.Name == name):
mid_message = "a function name of {} instead of the provided function name of {}."

Просмотреть файл

@ -0,0 +1 @@
venv

132
samples/counter_entity/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,132 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that dont work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Azure Functions artifacts
bin
obj
appsettings.json
.python_packages
# pycharm
.idea

Просмотреть файл

@ -0,0 +1,35 @@
import logging
import json
import azure.functions as func
import azure.durable_functions as df
def entity_function(context: df.DurableEntityContext):
"""A Counter Durable Entity.
A simple example of a Durable Entity that implements
a simple counter.
Parameters
----------
context (df.DurableEntityContext):
The Durable Entity context, which exports an API
for implementing durable entities.
"""
current_value = context.get_state(lambda: 0)
operation = context.operation_name
if operation == "add":
amount = context.get_input()
current_value += amount
elif operation == "reset":
current_value = 0
elif operation == "get":
pass
context.set_state(current_value)
context.set_result(current_value)
main = df.Entity.create(entity_function)

Просмотреть файл

@ -0,0 +1,10 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "entityTrigger",
"direction": "in"
}
]
}

Просмотреть файл

@ -0,0 +1,35 @@
# This function is not intended to be invoked directly. Instead it will be
# triggered by an HTTP starter function.
# Before running this sample, please:
# - create a Durable activity function (default name is "Hello")
# - create a Durable HTTP starter function
# - add azure-functions-durable to requirements.txt
# - run pip install -r requirements.txt
import logging
import json
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
"""This function provides the a simple implementation of an orchestrator
that signals and then calls a counter Durable Entity.
Parameters
----------
context: DurableOrchestrationContext
This context has the past history and the durable orchestration API
Returns
-------
state
The state after applying the operation on the Durable Entity
"""
entityId = df.EntityId("Counter", "myCounter")
context.signal_entity(entityId, "add", 3)
state = yield context.call_entity(entityId, "get")
return state
main = df.Orchestrator.create(orchestrator_function)

Просмотреть файл

@ -0,0 +1,11 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}

Просмотреть файл

@ -0,0 +1,30 @@
import logging
from azure.durable_functions import DurableOrchestrationClient
import azure.functions as func
async def main(req: func.HttpRequest, starter: str, message):
"""This function starts up the orchestrator from an HTTP endpoint
starter: str
A JSON-formatted string describing the orchestration context
message:
An azure functions http output binding, it enables us to establish
an http response.
Parameters
----------
req: func.HttpRequest
An HTTP Request object, it can be used to parse URL
parameters.
"""
function_name = req.route_params.get('functionName')
logging.info(starter)
client = DurableOrchestrationClient(starter)
instance_id = await client.start_new(function_name)
response = client.create_check_status_response(req, instance_id)
message.set(response)

Просмотреть файл

@ -0,0 +1,27 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"route": "orchestrators/{functionName}",
"methods": [
"post",
"get"
]
},
{
"direction": "out",
"name": "message",
"type": "http"
},
{
"name": "starter",
"type": "durableClient",
"direction": "in",
"datatype": "string"
}
]
}

Просмотреть файл

@ -0,0 +1,35 @@
# Durable Entities - Sample
This sample exemplifies how to go about using the [Durable Entities](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-entities?tabs=csharp) construct in Python Durable Functions.
## Usage Instructions
### Create a `local.settings.json` file in this directory
This file stores app settings, connection strings, and other settings used by local development tools. Learn more about it [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file).
For this sample, you will only need an `AzureWebJobsStorage` connection string, which you can obtain from the Azure portal.
With you connection string, your `local.settings.json` file should look as follows, with `<your connection string>` replaced with the connection string you obtained from the Azure portal:
```json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "<your connection string>",
"FUNCTIONS_WORKER_RUNTIME": "python"
}
}
```
### Run the Sample
To try this sample, run `func host start` in this directory. If all the system requirements have been met, and
after some initialization logs, you should see something like the following:
```bash
Http Functions:
DurableTrigger: [POST,GET] http://localhost:7071/api/orchestrators/{functionName}
```
This indicates that your `DurableTrigger` function can be reached via a `GET` or `POST` request to that URL. `DurableTrigger` starts the function-chaning orchestrator whose name is passed as a parameter to the URL. So, to start the orchestrator, which is named `DurableOrchestration`, make a GET request to `http://127.0.0.1:7071/api/orchestrators/DurableOrchestration`.
And that's it! You should see a JSON response with five URLs to monitor the status of the orchestration.

Просмотреть файл

@ -0,0 +1,3 @@
{
"version": "2.0"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python"
}
}

Просмотреть файл

@ -0,0 +1,2 @@
azure-functions
#azure-functions-durable>=1.0.0b6

Просмотреть файл

@ -495,7 +495,7 @@ async def test_wait_or_response_check_status_response(binding_string):
@pytest.mark.asyncio
async def test_wait_or_response_check_status_response(binding_string):
async def test_wait_or_response_null_request(binding_string):
status = dict(createdTime=TEST_CREATED_TIME,
lastUpdatedTime=TEST_LAST_UPDATED_TIME,
runtimeStatus="Running")

Просмотреть файл

@ -1,19 +1,36 @@
import json
from typing import Callable, Iterator, Any, Dict
from typing import Callable, Iterator, Any, Dict, List
from jsonschema import validate
from azure.durable_functions.models import DurableOrchestrationContext
from azure.durable_functions.models import DurableOrchestrationContext, DurableEntityContext
from azure.durable_functions.orchestrator import Orchestrator
from azure.durable_functions.entity import Entity
from .schemas.OrchetrationStateSchema import schema
def assert_orchestration_state_equals(expected, result):
"""Ensure that the observable OrchestratorState matches the expected result.
"""
assert_attribute_equal(expected, result, "isDone")
assert_actions_are_equal(expected, result)
assert_attribute_equal(expected, result, "output")
assert_attribute_equal(expected, result, "error")
assert_attribute_equal(expected, result, "customStatus")
def assert_entity_state_equals(expected, result):
"""Ensure the that the observable EntityState json matches the expected result.
"""
assert_attribute_equal(expected, result,"entityExists")
assert "results" in result
observed_results = result["results"]
expected_results = expected["results"]
assert_results_are_equal(expected_results, observed_results)
assert_attribute_equal(expected, result, "entityState")
assert_attribute_equal(expected, result, "signals")
def assert_results_are_equal(expected: Dict[str, Any], result: Dict[str, Any]) -> bool:
assert_attribute_equal(expected, result, "result")
assert_attribute_equal(expected, result, "isError")
def assert_attribute_equal(expected, result, attribute):
if attribute in expected:
@ -50,6 +67,33 @@ def get_orchestration_state_result(
result = json.loads(result_of_handle)
return result
def get_entity_state_result(
context_builder: DurableEntityContext,
user_code: Callable[[DurableEntityContext], Any],
) -> Dict[str, Any]:
"""Simulate the result of running the entity function with the provided context and batch.
Parameters
----------
context_builder: DurableEntityContext
A mocked entity context
user_code: Callable[[DurableEntityContext], Any]
A function implementing an entity
Returns:
-------
Dict[str, Any]:
JSON-response of the entity
"""
# The durable-extension automatically wraps the data within a 'self' key
context_as_string = context_builder.to_json_string()
entity = Entity(user_code)
context, batch = DurableEntityContext.from_json(context_as_string)
result_of_handle = entity.handle(context, batch)
result = json.loads(result_of_handle)
return result
def get_orchestration_property(
context_builder,
activity_func: Callable[[DurableOrchestrationContext], Iterator[Any]],

Просмотреть файл

@ -0,0 +1,217 @@
from .orchestrator_test_utils \
import assert_orchestration_state_equals, get_orchestration_state_result, assert_valid_schema, \
get_entity_state_result, assert_entity_state_equals
from tests.test_utils.ContextBuilder import ContextBuilder
from tests.test_utils.EntityContextBuilder import EntityContextBuilder
from azure.durable_functions.models.OrchestratorState import OrchestratorState
from azure.durable_functions.models.entities.EntityState import EntityState, OperationResult
from azure.durable_functions.models.actions.CallEntityAction \
import CallEntityAction
from azure.durable_functions.models.actions.SignalEntityAction \
import SignalEntityAction
from tests.test_utils.testClasses import SerializableClass
import azure.durable_functions as df
from typing import Any, Dict, List
import json
def generator_function_call_entity(context):
outputs = []
entityId = df.EntityId("Counter", "myCounter")
x = yield context.call_entity(entityId, "add", 3)
outputs.append(x)
return outputs
def generator_function_signal_entity(context):
outputs = []
entityId = df.EntityId("Counter", "myCounter")
context.signal_entity(entityId, "add", 3)
x = yield context.call_entity(entityId, "get")
outputs.append(x)
return outputs
def counter_entity_function(context):
"""A Counter Durable Entity.
A simple example of a Durable Entity that implements
a simple counter.
"""
current_value = context.get_state(lambda: 0)
operation = context.operation_name
if operation == "add":
amount = context.get_input()
current_value += amount
elif operation == "reset":
current_value = 0
elif operation == "get":
pass
result = f"The state is now: {current_value}"
context.set_state(current_value)
context.set_result(result)
def test_entity_signal_then_call():
"""Tests that a simple counter entity outputs the correct value
after a sequence of operations. Mostly just a sanity check.
"""
# Create input batch
batch = []
add_to_batch(batch, name="add", input_=3)
add_to_batch(batch, name="get")
context_builder = EntityContextBuilder(batch=batch)
# Run the entity, get observed result
result = get_entity_state_result(
context_builder,
counter_entity_function,
)
# Construct expected result
expected_state = entity_base_expected_state()
apply_operation(expected_state, result="The state is now: 3", state=3)
expected = expected_state.to_json()
# Ensure expectation matches observed behavior
#assert_valid_schema(result)
assert_entity_state_equals(expected, result)
def apply_operation(entity_state: EntityState, result: Any, state: Any, is_error: bool = False):
"""Apply the effects of an operation to the expected entity state object
Parameters
----------
entity_state: EntityState
The expected entity state object
result: Any
The result of the latest operation
state: Any
The state right after the latest operation
is_error: bool
Whether or not the operation resulted in an exception. Defaults to False
"""
entity_state.state = state
# We cannot control duration, so default it to zero and avoid checking for it
# in later asserts
duration = 0
operation_result = OperationResult(
is_error=is_error,
duration=duration,
result=result
)
entity_state._results.append(operation_result)
def add_to_batch(batch: List[Dict[str, Any]], name: str, input_: Any=None):
"""Add new work item to the batch of entity operations.
Parameters
----------
batch: List[Dict[str, Any]]
Current list of json-serialized entity work items
name: str
Name of the entity operation to be performed
input_: Optional[Any]:
Input to the operation. Defaults to None.
Returns
--------
List[Dict[str, str]]:
Batch of json-serialized entity work items
"""
# It is key to serialize the input twice, as this is
# the extension behavior
packet = {
"name": name,
"input": json.dumps(json.dumps(input_))
}
batch.append(packet)
return batch
def entity_base_expected_state() -> EntityState:
"""Get a base entity state.
Returns
-------
EntityState:
An EntityState with no results, no signals, a None state, and entity_exists set to True.
"""
return EntityState(results=[], signals=[], entity_exists=True, state=None)
def add_call_entity_action_for_entity(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
state.actions.append([action])
def base_expected_state(output=None) -> OrchestratorState:
return OrchestratorState(is_done=False, actions=[], output=output)
def add_call_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
state.actions.append([action])
def add_signal_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
action = SignalEntityAction(entity_id=id_, operation=op, input_=input_)
state.actions.append([action])
def add_call_entity_completed_events(
context_builder: ContextBuilder, op: str, instance_id=str, input_=None):
context_builder.add_event_sent_event(instance_id)
context_builder.add_orchestrator_completed_event()
context_builder.add_orchestrator_started_event()
context_builder.add_event_raised_event(name="0000", id_=0, input_=input_, is_entity=True)
def test_call_entity_sent():
context_builder = ContextBuilder('test_simple_function')
entityId = df.EntityId("Counter", "myCounter")
result = get_orchestration_state_result(
context_builder, generator_function_call_entity)
expected_state = base_expected_state()
add_call_entity_action(expected_state, entityId, "add", 3)
expected = expected_state.to_json()
#assert_valid_schema(result)
assert_orchestration_state_equals(expected, result)
def test_signal_entity_sent():
context_builder = ContextBuilder('test_simple_function')
entityId = df.EntityId("Counter", "myCounter")
result = get_orchestration_state_result(
context_builder, generator_function_signal_entity)
expected_state = base_expected_state()
add_signal_entity_action(expected_state, entityId, "add", 3)
add_call_entity_action(expected_state, entityId, "get", None)
expected = expected_state.to_json()
#assert_valid_schema(result)
assert_orchestration_state_equals(expected, result)
def test_call_entity_raised():
entityId = df.EntityId("Counter", "myCounter")
context_builder = ContextBuilder('test_simple_function')
add_call_entity_completed_events(context_builder, "add", df.EntityId.get_scheduler_id(entityId), 3)
result = get_orchestration_state_result(
context_builder, generator_function_call_entity)
expected_state = base_expected_state(
[3]
)
add_call_entity_action(expected_state, entityId, "add", 3)
expected_state._is_done = True
expected = expected_state.to_json()
#assert_valid_schema(result)
assert_orchestration_state_equals(expected, result)

Просмотреть файл

@ -63,6 +63,13 @@ class ContextBuilder:
event.TaskScheduledId = id_
self.history_events.append(event)
def add_event_sent_event(self, instance_id):
event = self.get_base_event(HistoryEventType.EVENT_SENT)
event.InstanceId = instance_id
event.Name = "op"
event.Input = json.dumps({ "id": "0000" }) # usually provided by the extension
self.history_events.append(event)
def add_task_scheduled_event(
self, name: str, id_: int, version: str = '', input_=None):
event = self.get_base_event(HistoryEventType.TASK_SCHEDULED, id_=id_)
@ -109,10 +116,13 @@ class ContextBuilder:
event.Input = input_
self.history_events.append(event)
def add_event_raised_event(self, name: str, id_: int, input_=None, timestamp=None):
def add_event_raised_event(self, name:str, id_: int, input_=None, timestamp=None, is_entity=False):
event = self.get_base_event(HistoryEventType.EVENT_RAISED, id_=id_, timestamp=timestamp)
event.Name = name
event.Input = input_
if is_entity:
event.Input = json.dumps({ "result": json.dumps(input_) })
else:
event.Input = input_
# event.timestamp = timestamp
self.history_events.append(event)

Просмотреть файл

@ -0,0 +1,57 @@
import json
from typing import Any, List, Dict, Any
class EntityContextBuilder():
"""Mock class for an EntityContext object, includes a batch field for convenience
"""
def __init__(self,
name: str = "",
key: str = "",
exists: bool = True,
state: Any = None,
batch: List[Dict[str, Any]] = []):
"""Construct an EntityContextBuilder
Parameters
----------
name: str:
The name of the entity. Defaults to the empty string.
key: str
The key of the entity. Defaults to the empty string.
exists: bool
Boolean representing if the entity exists, defaults to True.
state: Any
The state of the entity, defaults ot None.
batch: List[Dict[str, Any]]
The upcoming batch of operations for the entity to perform.
Note that the batch is not technically a part of the entity context
and so it is here only for convenience. Defaults to the empty list.
"""
self.name = name
self.key = key
self.exists = exists
self.state = state
self.batch = batch
def to_json_string(self) -> str:
"""Generate a string-representation of the Entity input payload.
The payload matches the current durable-extension entity-communication
schema.
Returns
-------
str:
A JSON-formatted string for an EntityContext to load via `from_json`
"""
context_json = {
"self": {
"name": self.name,
"key": self.key
},
"state": self.state,
"exists": self.exists,
"batch": self.batch
}
json_string = json.dumps(context_json)
return json_string

Просмотреть файл

@ -24,7 +24,7 @@ def convert_history_event_to_json_dict(
add_attrib(json_dict, history_event, 'FireAt')
add_attrib(json_dict, history_event, 'TimerId')
add_attrib(json_dict, history_event, 'Name')
add_attrib(json_dict, history_event, 'InstanceId')
add_json_attrib(json_dict, history_event,
'orchestration_instance', 'OrchestrationInstance')
return json_dict