azure-functions-durable 1.2.9__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure/durable_functions/__init__.py +81 -81
- azure/durable_functions/constants.py +9 -9
- azure/durable_functions/decorators/__init__.py +3 -3
- azure/durable_functions/decorators/durable_app.py +260 -249
- azure/durable_functions/decorators/metadata.py +109 -109
- azure/durable_functions/entity.py +129 -125
- azure/durable_functions/models/DurableEntityContext.py +201 -201
- azure/durable_functions/models/DurableHttpRequest.py +58 -58
- azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
- azure/durable_functions/models/DurableOrchestrationClient.py +812 -781
- azure/durable_functions/models/DurableOrchestrationContext.py +761 -707
- azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
- azure/durable_functions/models/EntityStateResponse.py +23 -23
- azure/durable_functions/models/FunctionContext.py +7 -7
- azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
- azure/durable_functions/models/OrchestratorState.py +117 -116
- azure/durable_functions/models/PurgeHistoryResult.py +33 -33
- azure/durable_functions/models/ReplaySchema.py +9 -8
- azure/durable_functions/models/RetryOptions.py +69 -69
- azure/durable_functions/models/RpcManagementOptions.py +86 -86
- azure/durable_functions/models/Task.py +540 -426
- azure/durable_functions/models/TaskOrchestrationExecutor.py +352 -336
- azure/durable_functions/models/TokenSource.py +56 -56
- azure/durable_functions/models/__init__.py +26 -24
- azure/durable_functions/models/actions/Action.py +23 -23
- azure/durable_functions/models/actions/ActionType.py +18 -18
- azure/durable_functions/models/actions/CallActivityAction.py +41 -41
- azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
- azure/durable_functions/models/actions/CallEntityAction.py +46 -46
- azure/durable_functions/models/actions/CallHttpAction.py +35 -35
- azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
- azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
- azure/durable_functions/models/actions/CompoundAction.py +35 -35
- azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
- azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
- azure/durable_functions/models/actions/NoOpAction.py +35 -35
- azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
- azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
- azure/durable_functions/models/actions/WhenAllAction.py +14 -14
- azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
- azure/durable_functions/models/actions/__init__.py +24 -24
- azure/durable_functions/models/entities/EntityState.py +74 -74
- azure/durable_functions/models/entities/OperationResult.py +94 -76
- azure/durable_functions/models/entities/RequestMessage.py +53 -53
- azure/durable_functions/models/entities/ResponseMessage.py +48 -48
- azure/durable_functions/models/entities/Signal.py +62 -62
- azure/durable_functions/models/entities/__init__.py +17 -17
- azure/durable_functions/models/history/HistoryEvent.py +92 -92
- azure/durable_functions/models/history/HistoryEventType.py +27 -27
- azure/durable_functions/models/history/__init__.py +8 -8
- azure/durable_functions/models/utils/__init__.py +7 -7
- azure/durable_functions/models/utils/entity_utils.py +103 -91
- azure/durable_functions/models/utils/http_utils.py +80 -69
- azure/durable_functions/models/utils/json_utils.py +96 -56
- azure/durable_functions/orchestrator.py +73 -71
- azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
- azure/durable_functions/testing/__init__.py +6 -0
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +59 -58
- azure_functions_durable-1.3.0.dist-info/RECORD +103 -0
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
- tests/models/test_DecoratorMetadata.py +135 -135
- tests/models/test_Decorators.py +107 -107
- tests/models/test_DurableOrchestrationBindings.py +68 -68
- tests/models/test_DurableOrchestrationClient.py +730 -730
- tests/models/test_DurableOrchestrationContext.py +102 -102
- tests/models/test_DurableOrchestrationStatus.py +59 -59
- tests/models/test_OrchestrationState.py +28 -28
- tests/models/test_RpcManagementOptions.py +79 -79
- tests/models/test_TokenSource.py +10 -10
- tests/orchestrator/models/OrchestrationInstance.py +18 -18
- tests/orchestrator/orchestrator_test_utils.py +130 -130
- tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
- tests/orchestrator/test_call_http.py +235 -176
- tests/orchestrator/test_continue_as_new.py +67 -67
- tests/orchestrator/test_create_timer.py +126 -126
- tests/orchestrator/test_entity.py +397 -395
- tests/orchestrator/test_external_event.py +53 -53
- tests/orchestrator/test_fan_out_fan_in.py +175 -175
- tests/orchestrator/test_is_replaying_flag.py +101 -101
- tests/orchestrator/test_retries.py +308 -308
- tests/orchestrator/test_sequential_orchestrator.py +841 -841
- tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
- tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
- tests/orchestrator/test_serialization.py +30 -30
- tests/orchestrator/test_sub_orchestrator.py +95 -95
- tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
- tests/orchestrator/test_task_any.py +60 -60
- tests/tasks/tasks_test_utils.py +17 -17
- tests/tasks/test_long_timers.py +70 -0
- tests/tasks/test_new_uuid.py +34 -34
- tests/test_utils/ContextBuilder.py +174 -174
- tests/test_utils/EntityContextBuilder.py +56 -56
- tests/test_utils/constants.py +1 -1
- tests/test_utils/json_utils.py +30 -30
- tests/test_utils/testClasses.py +56 -56
- tests/utils/__init__.py +1 -0
- tests/utils/test_entity_utils.py +24 -0
- azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
- azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/top_level.txt +0 -0
tests/tasks/tasks_test_utils.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
def assert_tasks_equal(task1, task2):
|
|
2
|
-
assert task1.is_completed == task2.is_completed
|
|
3
|
-
assert task1.is_faulted == task2.is_faulted
|
|
4
|
-
assert task1.result == task2.result
|
|
5
|
-
assert task1.timestamp == task2.timestamp
|
|
6
|
-
assert task1.id == task2.id
|
|
7
|
-
assert task1.action == task2.action
|
|
8
|
-
assert str(task1.exception) == str(task2.exception)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def assert_taskset_equal(taskset1, taskset2):
|
|
12
|
-
assert taskset1.is_completed == taskset2.is_completed
|
|
13
|
-
assert taskset1.is_faulted == taskset2.is_faulted
|
|
14
|
-
assert taskset1.result == taskset2.result
|
|
15
|
-
assert taskset1.actions == taskset2.actions
|
|
16
|
-
assert taskset1.timestamp == taskset2.timestamp
|
|
17
|
-
assert str(taskset1.exception) == str(taskset2.exception)
|
|
1
|
+
def assert_tasks_equal(task1, task2):
|
|
2
|
+
assert task1.is_completed == task2.is_completed
|
|
3
|
+
assert task1.is_faulted == task2.is_faulted
|
|
4
|
+
assert task1.result == task2.result
|
|
5
|
+
assert task1.timestamp == task2.timestamp
|
|
6
|
+
assert task1.id == task2.id
|
|
7
|
+
assert task1.action == task2.action
|
|
8
|
+
assert str(task1.exception) == str(task2.exception)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def assert_taskset_equal(taskset1, taskset2):
|
|
12
|
+
assert taskset1.is_completed == taskset2.is_completed
|
|
13
|
+
assert taskset1.is_faulted == taskset2.is_faulted
|
|
14
|
+
assert taskset1.result == taskset2.result
|
|
15
|
+
assert taskset1.actions == taskset2.actions
|
|
16
|
+
assert taskset1.timestamp == taskset2.timestamp
|
|
17
|
+
assert str(taskset1.exception) == str(taskset2.exception)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
5
|
+
from azure.durable_functions.models.Task import LongTimerTask, TaskState, TimerTask
|
|
6
|
+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def starting_context_v3():
|
|
11
|
+
context = DurableOrchestrationContext.from_json(
|
|
12
|
+
'{"history":[{"EventType":12,"EventId":-1,"IsPlayed":false,'
|
|
13
|
+
'"Timestamp":"'
|
|
14
|
+
f'{datetime.datetime.now(datetime.timezone.utc).isoformat()}'
|
|
15
|
+
'"}, {"OrchestrationInstance":{'
|
|
16
|
+
'"InstanceId":"48d0f95957504c2fa579e810a390b938", '
|
|
17
|
+
'"ExecutionId":"fd183ee02e4b4fd18c95b773cfb5452b"},"EventType":0,'
|
|
18
|
+
'"ParentInstance":null, '
|
|
19
|
+
'"Name":"DurableOrchestratorTrigger","Version":"","Input":"null",'
|
|
20
|
+
'"Tags":null,"EventId":-1,"IsPlayed":false, '
|
|
21
|
+
'"Timestamp":"'
|
|
22
|
+
f'{datetime.datetime.now(datetime.timezone.utc).isoformat()}'
|
|
23
|
+
'"}],"input":null,'
|
|
24
|
+
'"instanceId":"48d0f95957504c2fa579e810a390b938", '
|
|
25
|
+
'"upperSchemaVersion": 2, '
|
|
26
|
+
'"upperSchemaVersionNew": 3, '
|
|
27
|
+
'"isReplaying":false,"parentInstanceId":null, '
|
|
28
|
+
'"maximumShortTimerDuration":"0.16:00:00", '
|
|
29
|
+
'"longRunningTimerIntervalDuration":"0.08:00:00" } ')
|
|
30
|
+
return context
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_durable_context_creates_correct_timer(starting_context_v3):
|
|
34
|
+
timer = starting_context_v3.create_timer(datetime.datetime.now(datetime.timezone.utc) +
|
|
35
|
+
datetime.timedelta(minutes=30))
|
|
36
|
+
assert isinstance(timer, TimerTask)
|
|
37
|
+
|
|
38
|
+
timer2 = starting_context_v3.create_timer(datetime.datetime.now(datetime.timezone.utc) +
|
|
39
|
+
datetime.timedelta(days=1))
|
|
40
|
+
assert isinstance(timer2, LongTimerTask)
|
|
41
|
+
|
|
42
|
+
def test_long_timer_fires_appropriately(starting_context_v3):
|
|
43
|
+
starting_time = starting_context_v3.current_utc_datetime
|
|
44
|
+
final_fire_time = starting_time + datetime.timedelta(hours=20)
|
|
45
|
+
long_timer_action = CreateTimerAction(final_fire_time)
|
|
46
|
+
long_timer = LongTimerTask(None, long_timer_action, starting_context_v3)
|
|
47
|
+
assert long_timer.action.fire_at == final_fire_time
|
|
48
|
+
assert long_timer.action == long_timer_action
|
|
49
|
+
|
|
50
|
+
# Check the first "inner" timer and simulate firing it
|
|
51
|
+
short_timer = long_timer.pending_tasks.pop()
|
|
52
|
+
assert short_timer.action_repr.fire_at == starting_time + datetime.timedelta(hours=8)
|
|
53
|
+
# This happens when the task is reconstructed during replay, doing it manually for the test
|
|
54
|
+
long_timer._orchestration_context.current_utc_datetime = short_timer.action_repr.fire_at
|
|
55
|
+
short_timer.state = TaskState.SUCCEEDED
|
|
56
|
+
long_timer.try_set_value(short_timer)
|
|
57
|
+
|
|
58
|
+
assert long_timer.state == TaskState.RUNNING
|
|
59
|
+
|
|
60
|
+
# Check the scond "inner" timer and simulate firing it. This one should be set to the final
|
|
61
|
+
# fire time, the remaining time (12 hours) is less than the max long timer duration (16 hours)
|
|
62
|
+
short_timer = long_timer.pending_tasks.pop()
|
|
63
|
+
assert short_timer.action_repr.fire_at == final_fire_time
|
|
64
|
+
long_timer._orchestration_context.current_utc_datetime = short_timer.action_repr.fire_at
|
|
65
|
+
short_timer.state = TaskState.SUCCEEDED
|
|
66
|
+
long_timer.try_set_value(short_timer)
|
|
67
|
+
|
|
68
|
+
# Ensure the LongTimerTask finished
|
|
69
|
+
assert len(long_timer.pending_tasks) == 0
|
|
70
|
+
assert long_timer.state == TaskState.SUCCEEDED
|
tests/tasks/test_new_uuid.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
from uuid import uuid1
|
|
2
|
-
from typing import List, Any, Dict
|
|
3
|
-
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
4
|
-
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def history_list() -> List[Dict[Any, Any]]:
|
|
8
|
-
history = [{'EventType': 12, 'EventId': -1, 'IsPlayed': False,
|
|
9
|
-
'Timestamp': '2019-12-08T23:18:41.3240927Z'}, {
|
|
10
|
-
'OrchestrationInstance': {'InstanceId': '48d0f95957504c2fa579e810a390b938',
|
|
11
|
-
'ExecutionId': 'fd183ee02e4b4fd18c95b773cfb5452b'},
|
|
12
|
-
'EventType': 0, 'ParentInstance': None, 'Name': 'DurableOrchestratorTrigger',
|
|
13
|
-
'Version': '', 'Input': 'null', 'Tags': None, 'EventId': -1, 'IsPlayed': False,
|
|
14
|
-
'Timestamp': '2019-12-08T23:18:39.756132Z'}]
|
|
15
|
-
return history
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_new_uuid():
|
|
19
|
-
instance_id = str(uuid1())
|
|
20
|
-
history = history_list()
|
|
21
|
-
context1 = DurableOrchestrationContext(history, instance_id, False, None)
|
|
22
|
-
|
|
23
|
-
result1a = context1.new_uuid()
|
|
24
|
-
result1b = context1.new_uuid()
|
|
25
|
-
|
|
26
|
-
context2 = DurableOrchestrationContext(history, instance_id, False, None)
|
|
27
|
-
|
|
28
|
-
result2a = context2.new_uuid()
|
|
29
|
-
result2b = context2.new_uuid()
|
|
30
|
-
|
|
31
|
-
assert result1a == result2a
|
|
32
|
-
assert result1b == result2b
|
|
33
|
-
|
|
34
|
-
assert result1a != result1b
|
|
1
|
+
from uuid import uuid1
|
|
2
|
+
from typing import List, Any, Dict
|
|
3
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
4
|
+
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def history_list() -> List[Dict[Any, Any]]:
|
|
8
|
+
history = [{'EventType': 12, 'EventId': -1, 'IsPlayed': False,
|
|
9
|
+
'Timestamp': '2019-12-08T23:18:41.3240927Z'}, {
|
|
10
|
+
'OrchestrationInstance': {'InstanceId': '48d0f95957504c2fa579e810a390b938',
|
|
11
|
+
'ExecutionId': 'fd183ee02e4b4fd18c95b773cfb5452b'},
|
|
12
|
+
'EventType': 0, 'ParentInstance': None, 'Name': 'DurableOrchestratorTrigger',
|
|
13
|
+
'Version': '', 'Input': 'null', 'Tags': None, 'EventId': -1, 'IsPlayed': False,
|
|
14
|
+
'Timestamp': '2019-12-08T23:18:39.756132Z'}]
|
|
15
|
+
return history
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_new_uuid():
|
|
19
|
+
instance_id = str(uuid1())
|
|
20
|
+
history = history_list()
|
|
21
|
+
context1 = DurableOrchestrationContext(history, instance_id, False, None)
|
|
22
|
+
|
|
23
|
+
result1a = context1.new_uuid()
|
|
24
|
+
result1b = context1.new_uuid()
|
|
25
|
+
|
|
26
|
+
context2 = DurableOrchestrationContext(history, instance_id, False, None)
|
|
27
|
+
|
|
28
|
+
result2a = context2.new_uuid()
|
|
29
|
+
result2b = context2.new_uuid()
|
|
30
|
+
|
|
31
|
+
assert result1a == result2a
|
|
32
|
+
assert result1b == result2b
|
|
33
|
+
|
|
34
|
+
assert result1a != result1b
|
|
35
35
|
assert result2a != result2b
|
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
2
|
-
import uuid
|
|
3
|
-
import json
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from typing import List, Dict, Any, Optional
|
|
6
|
-
|
|
7
|
-
from .json_utils import add_attrib, convert_history_event_to_json_dict
|
|
8
|
-
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
9
|
-
from tests.orchestrator.models.OrchestrationInstance \
|
|
10
|
-
import OrchestrationInstance
|
|
11
|
-
from azure.durable_functions.models.history.HistoryEvent import HistoryEvent
|
|
12
|
-
from azure.durable_functions.models.history.HistoryEventType \
|
|
13
|
-
import HistoryEventType
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ContextBuilder:
|
|
17
|
-
def __init__(self, name: str="", increase_time: bool = True, starting_time: Optional[datetime] = None, is_replaying=False, replay_schema: ReplaySchema = ReplaySchema.V1):
|
|
18
|
-
self.increase_time = increase_time
|
|
19
|
-
self.instance_id = uuid.uuid4()
|
|
20
|
-
self.is_replaying: bool = False
|
|
21
|
-
self.input_ = None
|
|
22
|
-
self.parent_instance_id = None
|
|
23
|
-
self.history_events: List[HistoryEvent] = []
|
|
24
|
-
|
|
25
|
-
if starting_time is None:
|
|
26
|
-
starting_time = datetime.now()
|
|
27
|
-
self.current_datetime: datetime = starting_time
|
|
28
|
-
self.upperSchemaVersion = replay_schema.value
|
|
29
|
-
|
|
30
|
-
self.add_orchestrator_started_event()
|
|
31
|
-
self.add_execution_started_event(name, is_played=is_replaying)
|
|
32
|
-
|
|
33
|
-
def get_base_event(
|
|
34
|
-
self, event_type: HistoryEventType, id_: int = -1,
|
|
35
|
-
is_played: bool = False, timestamp=None) -> HistoryEvent:
|
|
36
|
-
if self.increase_time:
|
|
37
|
-
self.current_datetime = self.current_datetime + timedelta(seconds=1)
|
|
38
|
-
if not timestamp:
|
|
39
|
-
timestamp = self.current_datetime
|
|
40
|
-
event = HistoryEvent(EventType=event_type, EventId=id_,
|
|
41
|
-
IsPlayed=is_played,
|
|
42
|
-
Timestamp=timestamp.strftime(DATETIME_STRING_FORMAT))
|
|
43
|
-
|
|
44
|
-
return event
|
|
45
|
-
|
|
46
|
-
def add_orchestrator_started_event(self):
|
|
47
|
-
event = self.get_base_event(HistoryEventType.ORCHESTRATOR_STARTED)
|
|
48
|
-
self.history_events.append(event)
|
|
49
|
-
|
|
50
|
-
def add_orchestrator_completed_event(self):
|
|
51
|
-
event = self.get_base_event(HistoryEventType.ORCHESTRATOR_COMPLETED)
|
|
52
|
-
self.history_events.append(event)
|
|
53
|
-
|
|
54
|
-
def add_sub_orchestrator_started_event(self, name: str, id_, input_=None):
|
|
55
|
-
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_CREATED,
|
|
56
|
-
id_=id_)
|
|
57
|
-
event.Name = name
|
|
58
|
-
event.Input = input_
|
|
59
|
-
self.history_events.append(event)
|
|
60
|
-
|
|
61
|
-
def add_sub_orchestrator_completed_event(self, result, id_):
|
|
62
|
-
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_COMPLETED)
|
|
63
|
-
event.Result = result
|
|
64
|
-
event.TaskScheduledId = id_
|
|
65
|
-
self.history_events.append(event)
|
|
66
|
-
|
|
67
|
-
def add_sub_orchestrator_failed_event(self, id_, reason, details):
|
|
68
|
-
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_FAILED)
|
|
69
|
-
event.Reason = reason
|
|
70
|
-
event.Details = details
|
|
71
|
-
event.TaskScheduledId = id_
|
|
72
|
-
self.history_events.append(event)
|
|
73
|
-
|
|
74
|
-
def add_event_sent_event(self, instance_id, event_id):
|
|
75
|
-
event = self.get_base_event(HistoryEventType.EVENT_SENT)
|
|
76
|
-
event.InstanceId = instance_id
|
|
77
|
-
event._event_id = event_id
|
|
78
|
-
event.Name = "op"
|
|
79
|
-
event.Input = json.dumps({ "id": "0000" }) # usually provided by the extension
|
|
80
|
-
self.history_events.append(event)
|
|
81
|
-
|
|
82
|
-
def add_task_scheduled_event(
|
|
83
|
-
self, name: str, id_: int, version: str = '', input_=None):
|
|
84
|
-
event = self.get_base_event(HistoryEventType.TASK_SCHEDULED, id_=id_)
|
|
85
|
-
event.Name = name
|
|
86
|
-
event.Version = version
|
|
87
|
-
event.Input_ = input_
|
|
88
|
-
self.history_events.append(event)
|
|
89
|
-
|
|
90
|
-
def add_task_completed_event(self, id_: int, result, is_played=False):
|
|
91
|
-
event = self.get_base_event(HistoryEventType.TASK_COMPLETED, is_played=is_played)
|
|
92
|
-
event.Result = result
|
|
93
|
-
event.TaskScheduledId = id_
|
|
94
|
-
self.history_events.append(event)
|
|
95
|
-
|
|
96
|
-
def add_task_failed_event(self, id_: int, reason: str, details: str):
|
|
97
|
-
event = self.get_base_event(HistoryEventType.TASK_FAILED)
|
|
98
|
-
event.Reason = reason
|
|
99
|
-
event.Details = details
|
|
100
|
-
event.TaskScheduledId = id_
|
|
101
|
-
self.history_events.append(event)
|
|
102
|
-
|
|
103
|
-
def add_timer_created_event(self, id_: int, timestamp: str = None):
|
|
104
|
-
fire_at = self.current_datetime.strftime(DATETIME_STRING_FORMAT)
|
|
105
|
-
if timestamp is not None:
|
|
106
|
-
fire_at = timestamp
|
|
107
|
-
event = self.get_base_event(HistoryEventType.TIMER_CREATED, id_=id_)
|
|
108
|
-
event.FireAt = fire_at
|
|
109
|
-
self.history_events.append(event)
|
|
110
|
-
return fire_at
|
|
111
|
-
|
|
112
|
-
def add_timer_fired_event(self, id_: int, fire_at: str, is_played: bool = True):
|
|
113
|
-
event = self.get_base_event(HistoryEventType.TIMER_FIRED, is_played=is_played)
|
|
114
|
-
event.TimerId = id_
|
|
115
|
-
event.FireAt = fire_at
|
|
116
|
-
self.history_events.append(event)
|
|
117
|
-
|
|
118
|
-
def add_execution_started_event(
|
|
119
|
-
self, name: str, version: str = '', input_=None, is_played=True):
|
|
120
|
-
event = self.get_base_event(HistoryEventType.EXECUTION_STARTED, is_played=is_played)
|
|
121
|
-
event.orchestration_instance = OrchestrationInstance()
|
|
122
|
-
self.instance_id = event.orchestration_instance.instance_id
|
|
123
|
-
event.Name = name
|
|
124
|
-
event.Version = version
|
|
125
|
-
event.Input = input_
|
|
126
|
-
self.history_events.append(event)
|
|
127
|
-
|
|
128
|
-
def add_event_raised_event(self, name:str, id_: int, input_=None, timestamp=None, is_entity=False, is_error = False, literal_input=False):
|
|
129
|
-
event = self.get_base_event(HistoryEventType.EVENT_RAISED, id_=id_, timestamp=timestamp)
|
|
130
|
-
event.Name = name
|
|
131
|
-
if is_entity:
|
|
132
|
-
if is_error:
|
|
133
|
-
event.Input = json.dumps({ "result": json.dumps(input_), "exceptionType": "True" })
|
|
134
|
-
else:
|
|
135
|
-
if literal_input:
|
|
136
|
-
event.Input = json.dumps({ "result": input_ })
|
|
137
|
-
else:
|
|
138
|
-
event.Input = json.dumps({ "result": json.dumps(input_) })
|
|
139
|
-
else:
|
|
140
|
-
event.Input = input_
|
|
141
|
-
# event.timestamp = timestamp
|
|
142
|
-
self.history_events.append(event)
|
|
143
|
-
|
|
144
|
-
def to_json(self, **kwargs) -> Dict[str, Any]:
|
|
145
|
-
json_dict = {}
|
|
146
|
-
|
|
147
|
-
add_attrib(json_dict, self, 'instance_id', 'instanceId')
|
|
148
|
-
add_attrib(json_dict, self, 'parent_instance_id', 'parentInstanceId')
|
|
149
|
-
add_attrib(json_dict, self, 'is_replaying', 'isReplaying')
|
|
150
|
-
add_attrib(json_dict, self, 'input_', "input")
|
|
151
|
-
add_attrib(json_dict, self, 'upperSchemaVersion', "upperSchemaVersion")
|
|
152
|
-
|
|
153
|
-
history_list_as_dict = self.get_history_list_as_dict()
|
|
154
|
-
json_dict['history'] = history_list_as_dict
|
|
155
|
-
|
|
156
|
-
if kwargs is not None:
|
|
157
|
-
for key, value in kwargs.items():
|
|
158
|
-
json_dict[key] = value
|
|
159
|
-
|
|
160
|
-
return json_dict
|
|
161
|
-
|
|
162
|
-
def get_history_list_as_dict(self) -> List[Dict[str, Any]]:
|
|
163
|
-
history_list = []
|
|
164
|
-
|
|
165
|
-
for history_event in self.history_events:
|
|
166
|
-
event_as_dict = convert_history_event_to_json_dict(history_event)
|
|
167
|
-
history_list.append(event_as_dict)
|
|
168
|
-
|
|
169
|
-
return history_list
|
|
170
|
-
|
|
171
|
-
def to_json_string(self, **kwargs) -> str:
|
|
172
|
-
json_dict = self.to_json(**kwargs)
|
|
173
|
-
|
|
174
|
-
return json.dumps(json_dict)
|
|
1
|
+
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
2
|
+
import uuid
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
from .json_utils import add_attrib, convert_history_event_to_json_dict
|
|
8
|
+
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
|
|
9
|
+
from tests.orchestrator.models.OrchestrationInstance \
|
|
10
|
+
import OrchestrationInstance
|
|
11
|
+
from azure.durable_functions.models.history.HistoryEvent import HistoryEvent
|
|
12
|
+
from azure.durable_functions.models.history.HistoryEventType \
|
|
13
|
+
import HistoryEventType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContextBuilder:
|
|
17
|
+
def __init__(self, name: str="", increase_time: bool = True, starting_time: Optional[datetime] = None, is_replaying=False, replay_schema: ReplaySchema = ReplaySchema.V1):
|
|
18
|
+
self.increase_time = increase_time
|
|
19
|
+
self.instance_id = uuid.uuid4()
|
|
20
|
+
self.is_replaying: bool = False
|
|
21
|
+
self.input_ = None
|
|
22
|
+
self.parent_instance_id = None
|
|
23
|
+
self.history_events: List[HistoryEvent] = []
|
|
24
|
+
|
|
25
|
+
if starting_time is None:
|
|
26
|
+
starting_time = datetime.now()
|
|
27
|
+
self.current_datetime: datetime = starting_time
|
|
28
|
+
self.upperSchemaVersion = replay_schema.value
|
|
29
|
+
|
|
30
|
+
self.add_orchestrator_started_event()
|
|
31
|
+
self.add_execution_started_event(name, is_played=is_replaying)
|
|
32
|
+
|
|
33
|
+
def get_base_event(
|
|
34
|
+
self, event_type: HistoryEventType, id_: int = -1,
|
|
35
|
+
is_played: bool = False, timestamp=None) -> HistoryEvent:
|
|
36
|
+
if self.increase_time:
|
|
37
|
+
self.current_datetime = self.current_datetime + timedelta(seconds=1)
|
|
38
|
+
if not timestamp:
|
|
39
|
+
timestamp = self.current_datetime
|
|
40
|
+
event = HistoryEvent(EventType=event_type, EventId=id_,
|
|
41
|
+
IsPlayed=is_played,
|
|
42
|
+
Timestamp=timestamp.strftime(DATETIME_STRING_FORMAT))
|
|
43
|
+
|
|
44
|
+
return event
|
|
45
|
+
|
|
46
|
+
def add_orchestrator_started_event(self):
|
|
47
|
+
event = self.get_base_event(HistoryEventType.ORCHESTRATOR_STARTED)
|
|
48
|
+
self.history_events.append(event)
|
|
49
|
+
|
|
50
|
+
def add_orchestrator_completed_event(self):
|
|
51
|
+
event = self.get_base_event(HistoryEventType.ORCHESTRATOR_COMPLETED)
|
|
52
|
+
self.history_events.append(event)
|
|
53
|
+
|
|
54
|
+
def add_sub_orchestrator_started_event(self, name: str, id_, input_=None):
|
|
55
|
+
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_CREATED,
|
|
56
|
+
id_=id_)
|
|
57
|
+
event.Name = name
|
|
58
|
+
event.Input = input_
|
|
59
|
+
self.history_events.append(event)
|
|
60
|
+
|
|
61
|
+
def add_sub_orchestrator_completed_event(self, result, id_):
|
|
62
|
+
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_COMPLETED)
|
|
63
|
+
event.Result = result
|
|
64
|
+
event.TaskScheduledId = id_
|
|
65
|
+
self.history_events.append(event)
|
|
66
|
+
|
|
67
|
+
def add_sub_orchestrator_failed_event(self, id_, reason, details):
|
|
68
|
+
event = self.get_base_event(HistoryEventType.SUB_ORCHESTRATION_INSTANCE_FAILED)
|
|
69
|
+
event.Reason = reason
|
|
70
|
+
event.Details = details
|
|
71
|
+
event.TaskScheduledId = id_
|
|
72
|
+
self.history_events.append(event)
|
|
73
|
+
|
|
74
|
+
def add_event_sent_event(self, instance_id, event_id):
|
|
75
|
+
event = self.get_base_event(HistoryEventType.EVENT_SENT)
|
|
76
|
+
event.InstanceId = instance_id
|
|
77
|
+
event._event_id = event_id
|
|
78
|
+
event.Name = "op"
|
|
79
|
+
event.Input = json.dumps({ "id": "0000" }) # usually provided by the extension
|
|
80
|
+
self.history_events.append(event)
|
|
81
|
+
|
|
82
|
+
def add_task_scheduled_event(
|
|
83
|
+
self, name: str, id_: int, version: str = '', input_=None):
|
|
84
|
+
event = self.get_base_event(HistoryEventType.TASK_SCHEDULED, id_=id_)
|
|
85
|
+
event.Name = name
|
|
86
|
+
event.Version = version
|
|
87
|
+
event.Input_ = input_
|
|
88
|
+
self.history_events.append(event)
|
|
89
|
+
|
|
90
|
+
def add_task_completed_event(self, id_: int, result, is_played=False):
|
|
91
|
+
event = self.get_base_event(HistoryEventType.TASK_COMPLETED, is_played=is_played)
|
|
92
|
+
event.Result = result
|
|
93
|
+
event.TaskScheduledId = id_
|
|
94
|
+
self.history_events.append(event)
|
|
95
|
+
|
|
96
|
+
def add_task_failed_event(self, id_: int, reason: str, details: str):
|
|
97
|
+
event = self.get_base_event(HistoryEventType.TASK_FAILED)
|
|
98
|
+
event.Reason = reason
|
|
99
|
+
event.Details = details
|
|
100
|
+
event.TaskScheduledId = id_
|
|
101
|
+
self.history_events.append(event)
|
|
102
|
+
|
|
103
|
+
def add_timer_created_event(self, id_: int, timestamp: str = None):
|
|
104
|
+
fire_at = self.current_datetime.strftime(DATETIME_STRING_FORMAT)
|
|
105
|
+
if timestamp is not None:
|
|
106
|
+
fire_at = timestamp
|
|
107
|
+
event = self.get_base_event(HistoryEventType.TIMER_CREATED, id_=id_)
|
|
108
|
+
event.FireAt = fire_at
|
|
109
|
+
self.history_events.append(event)
|
|
110
|
+
return fire_at
|
|
111
|
+
|
|
112
|
+
def add_timer_fired_event(self, id_: int, fire_at: str, is_played: bool = True):
|
|
113
|
+
event = self.get_base_event(HistoryEventType.TIMER_FIRED, is_played=is_played)
|
|
114
|
+
event.TimerId = id_
|
|
115
|
+
event.FireAt = fire_at
|
|
116
|
+
self.history_events.append(event)
|
|
117
|
+
|
|
118
|
+
def add_execution_started_event(
|
|
119
|
+
self, name: str, version: str = '', input_=None, is_played=True):
|
|
120
|
+
event = self.get_base_event(HistoryEventType.EXECUTION_STARTED, is_played=is_played)
|
|
121
|
+
event.orchestration_instance = OrchestrationInstance()
|
|
122
|
+
self.instance_id = event.orchestration_instance.instance_id
|
|
123
|
+
event.Name = name
|
|
124
|
+
event.Version = version
|
|
125
|
+
event.Input = input_
|
|
126
|
+
self.history_events.append(event)
|
|
127
|
+
|
|
128
|
+
def add_event_raised_event(self, name:str, id_: int, input_=None, timestamp=None, is_entity=False, is_error = False, literal_input=False):
|
|
129
|
+
event = self.get_base_event(HistoryEventType.EVENT_RAISED, id_=id_, timestamp=timestamp)
|
|
130
|
+
event.Name = name
|
|
131
|
+
if is_entity:
|
|
132
|
+
if is_error:
|
|
133
|
+
event.Input = json.dumps({ "result": json.dumps(input_), "exceptionType": "True" })
|
|
134
|
+
else:
|
|
135
|
+
if literal_input:
|
|
136
|
+
event.Input = json.dumps({ "result": input_ })
|
|
137
|
+
else:
|
|
138
|
+
event.Input = json.dumps({ "result": json.dumps(input_) })
|
|
139
|
+
else:
|
|
140
|
+
event.Input = input_
|
|
141
|
+
# event.timestamp = timestamp
|
|
142
|
+
self.history_events.append(event)
|
|
143
|
+
|
|
144
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
|
145
|
+
json_dict = {}
|
|
146
|
+
|
|
147
|
+
add_attrib(json_dict, self, 'instance_id', 'instanceId')
|
|
148
|
+
add_attrib(json_dict, self, 'parent_instance_id', 'parentInstanceId')
|
|
149
|
+
add_attrib(json_dict, self, 'is_replaying', 'isReplaying')
|
|
150
|
+
add_attrib(json_dict, self, 'input_', "input")
|
|
151
|
+
add_attrib(json_dict, self, 'upperSchemaVersion', "upperSchemaVersion")
|
|
152
|
+
|
|
153
|
+
history_list_as_dict = self.get_history_list_as_dict()
|
|
154
|
+
json_dict['history'] = history_list_as_dict
|
|
155
|
+
|
|
156
|
+
if kwargs is not None:
|
|
157
|
+
for key, value in kwargs.items():
|
|
158
|
+
json_dict[key] = value
|
|
159
|
+
|
|
160
|
+
return json_dict
|
|
161
|
+
|
|
162
|
+
def get_history_list_as_dict(self) -> List[Dict[str, Any]]:
|
|
163
|
+
history_list = []
|
|
164
|
+
|
|
165
|
+
for history_event in self.history_events:
|
|
166
|
+
event_as_dict = convert_history_event_to_json_dict(history_event)
|
|
167
|
+
history_list.append(event_as_dict)
|
|
168
|
+
|
|
169
|
+
return history_list
|
|
170
|
+
|
|
171
|
+
def to_json_string(self, **kwargs) -> str:
|
|
172
|
+
json_dict = self.to_json(**kwargs)
|
|
173
|
+
|
|
174
|
+
return json.dumps(json_dict)
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Any, List, Dict, Any
|
|
3
|
-
|
|
4
|
-
class EntityContextBuilder():
|
|
5
|
-
"""Mock class for an EntityContext object, includes a batch field for convenience
|
|
6
|
-
"""
|
|
7
|
-
def __init__(self,
|
|
8
|
-
name: str = "",
|
|
9
|
-
key: str = "",
|
|
10
|
-
exists: bool = True,
|
|
11
|
-
state: Any = None,
|
|
12
|
-
batch: List[Dict[str, Any]] = []):
|
|
13
|
-
"""Construct an EntityContextBuilder
|
|
14
|
-
|
|
15
|
-
Parameters
|
|
16
|
-
----------
|
|
17
|
-
name: str:
|
|
18
|
-
The name of the entity. Defaults to the empty string.
|
|
19
|
-
key: str
|
|
20
|
-
The key of the entity. Defaults to the empty string.
|
|
21
|
-
exists: bool
|
|
22
|
-
Boolean representing if the entity exists, defaults to True.
|
|
23
|
-
state: Any
|
|
24
|
-
The state of the entity, defaults ot None.
|
|
25
|
-
batch: List[Dict[str, Any]]
|
|
26
|
-
The upcoming batch of operations for the entity to perform.
|
|
27
|
-
Note that the batch is not technically a part of the entity context
|
|
28
|
-
and so it is here only for convenience. Defaults to the empty list.
|
|
29
|
-
"""
|
|
30
|
-
self.name = name
|
|
31
|
-
self.key = key
|
|
32
|
-
self.exists = exists
|
|
33
|
-
self.state = state
|
|
34
|
-
self.batch = batch
|
|
35
|
-
|
|
36
|
-
def to_json_string(self) -> str:
|
|
37
|
-
"""Generate a string-representation of the Entity input payload.
|
|
38
|
-
|
|
39
|
-
The payload matches the current durable-extension entity-communication
|
|
40
|
-
schema.
|
|
41
|
-
|
|
42
|
-
Returns
|
|
43
|
-
-------
|
|
44
|
-
str:
|
|
45
|
-
A JSON-formatted string for an EntityContext to load via `from_json`
|
|
46
|
-
"""
|
|
47
|
-
context_json = {
|
|
48
|
-
"self": {
|
|
49
|
-
"name": self.name,
|
|
50
|
-
"key": self.key
|
|
51
|
-
},
|
|
52
|
-
"state": self.state,
|
|
53
|
-
"exists": self.exists,
|
|
54
|
-
"batch": self.batch
|
|
55
|
-
}
|
|
56
|
-
json_string = json.dumps(context_json)
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, List, Dict, Any
|
|
3
|
+
|
|
4
|
+
class EntityContextBuilder():
|
|
5
|
+
"""Mock class for an EntityContext object, includes a batch field for convenience
|
|
6
|
+
"""
|
|
7
|
+
def __init__(self,
|
|
8
|
+
name: str = "",
|
|
9
|
+
key: str = "",
|
|
10
|
+
exists: bool = True,
|
|
11
|
+
state: Any = None,
|
|
12
|
+
batch: List[Dict[str, Any]] = []):
|
|
13
|
+
"""Construct an EntityContextBuilder
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
name: str:
|
|
18
|
+
The name of the entity. Defaults to the empty string.
|
|
19
|
+
key: str
|
|
20
|
+
The key of the entity. Defaults to the empty string.
|
|
21
|
+
exists: bool
|
|
22
|
+
Boolean representing if the entity exists, defaults to True.
|
|
23
|
+
state: Any
|
|
24
|
+
The state of the entity, defaults ot None.
|
|
25
|
+
batch: List[Dict[str, Any]]
|
|
26
|
+
The upcoming batch of operations for the entity to perform.
|
|
27
|
+
Note that the batch is not technically a part of the entity context
|
|
28
|
+
and so it is here only for convenience. Defaults to the empty list.
|
|
29
|
+
"""
|
|
30
|
+
self.name = name
|
|
31
|
+
self.key = key
|
|
32
|
+
self.exists = exists
|
|
33
|
+
self.state = state
|
|
34
|
+
self.batch = batch
|
|
35
|
+
|
|
36
|
+
def to_json_string(self) -> str:
|
|
37
|
+
"""Generate a string-representation of the Entity input payload.
|
|
38
|
+
|
|
39
|
+
The payload matches the current durable-extension entity-communication
|
|
40
|
+
schema.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
str:
|
|
45
|
+
A JSON-formatted string for an EntityContext to load via `from_json`
|
|
46
|
+
"""
|
|
47
|
+
context_json = {
|
|
48
|
+
"self": {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"key": self.key
|
|
51
|
+
},
|
|
52
|
+
"state": self.state,
|
|
53
|
+
"exists": self.exists,
|
|
54
|
+
"batch": self.batch
|
|
55
|
+
}
|
|
56
|
+
json_string = json.dumps(context_json)
|
|
57
57
|
return json_string
|
tests/test_utils/constants.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
RPC_BASE_URL = "http://127.0.0.1:17071/durabletask/"
|
|
1
|
+
RPC_BASE_URL = "http://127.0.0.1:17071/durabletask/"
|