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.
Files changed (104) hide show
  1. azure/durable_functions/__init__.py +81 -81
  2. azure/durable_functions/constants.py +9 -9
  3. azure/durable_functions/decorators/__init__.py +3 -3
  4. azure/durable_functions/decorators/durable_app.py +260 -249
  5. azure/durable_functions/decorators/metadata.py +109 -109
  6. azure/durable_functions/entity.py +129 -125
  7. azure/durable_functions/models/DurableEntityContext.py +201 -201
  8. azure/durable_functions/models/DurableHttpRequest.py +58 -58
  9. azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
  10. azure/durable_functions/models/DurableOrchestrationClient.py +812 -781
  11. azure/durable_functions/models/DurableOrchestrationContext.py +761 -707
  12. azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
  13. azure/durable_functions/models/EntityStateResponse.py +23 -23
  14. azure/durable_functions/models/FunctionContext.py +7 -7
  15. azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
  16. azure/durable_functions/models/OrchestratorState.py +117 -116
  17. azure/durable_functions/models/PurgeHistoryResult.py +33 -33
  18. azure/durable_functions/models/ReplaySchema.py +9 -8
  19. azure/durable_functions/models/RetryOptions.py +69 -69
  20. azure/durable_functions/models/RpcManagementOptions.py +86 -86
  21. azure/durable_functions/models/Task.py +540 -426
  22. azure/durable_functions/models/TaskOrchestrationExecutor.py +352 -336
  23. azure/durable_functions/models/TokenSource.py +56 -56
  24. azure/durable_functions/models/__init__.py +26 -24
  25. azure/durable_functions/models/actions/Action.py +23 -23
  26. azure/durable_functions/models/actions/ActionType.py +18 -18
  27. azure/durable_functions/models/actions/CallActivityAction.py +41 -41
  28. azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
  29. azure/durable_functions/models/actions/CallEntityAction.py +46 -46
  30. azure/durable_functions/models/actions/CallHttpAction.py +35 -35
  31. azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
  32. azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
  33. azure/durable_functions/models/actions/CompoundAction.py +35 -35
  34. azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
  35. azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
  36. azure/durable_functions/models/actions/NoOpAction.py +35 -35
  37. azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
  38. azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
  39. azure/durable_functions/models/actions/WhenAllAction.py +14 -14
  40. azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
  41. azure/durable_functions/models/actions/__init__.py +24 -24
  42. azure/durable_functions/models/entities/EntityState.py +74 -74
  43. azure/durable_functions/models/entities/OperationResult.py +94 -76
  44. azure/durable_functions/models/entities/RequestMessage.py +53 -53
  45. azure/durable_functions/models/entities/ResponseMessage.py +48 -48
  46. azure/durable_functions/models/entities/Signal.py +62 -62
  47. azure/durable_functions/models/entities/__init__.py +17 -17
  48. azure/durable_functions/models/history/HistoryEvent.py +92 -92
  49. azure/durable_functions/models/history/HistoryEventType.py +27 -27
  50. azure/durable_functions/models/history/__init__.py +8 -8
  51. azure/durable_functions/models/utils/__init__.py +7 -7
  52. azure/durable_functions/models/utils/entity_utils.py +103 -91
  53. azure/durable_functions/models/utils/http_utils.py +80 -69
  54. azure/durable_functions/models/utils/json_utils.py +96 -56
  55. azure/durable_functions/orchestrator.py +73 -71
  56. azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
  57. azure/durable_functions/testing/__init__.py +6 -0
  58. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +21 -21
  59. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +59 -58
  60. azure_functions_durable-1.3.0.dist-info/RECORD +103 -0
  61. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
  62. tests/models/test_DecoratorMetadata.py +135 -135
  63. tests/models/test_Decorators.py +107 -107
  64. tests/models/test_DurableOrchestrationBindings.py +68 -68
  65. tests/models/test_DurableOrchestrationClient.py +730 -730
  66. tests/models/test_DurableOrchestrationContext.py +102 -102
  67. tests/models/test_DurableOrchestrationStatus.py +59 -59
  68. tests/models/test_OrchestrationState.py +28 -28
  69. tests/models/test_RpcManagementOptions.py +79 -79
  70. tests/models/test_TokenSource.py +10 -10
  71. tests/orchestrator/models/OrchestrationInstance.py +18 -18
  72. tests/orchestrator/orchestrator_test_utils.py +130 -130
  73. tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
  74. tests/orchestrator/test_call_http.py +235 -176
  75. tests/orchestrator/test_continue_as_new.py +67 -67
  76. tests/orchestrator/test_create_timer.py +126 -126
  77. tests/orchestrator/test_entity.py +397 -395
  78. tests/orchestrator/test_external_event.py +53 -53
  79. tests/orchestrator/test_fan_out_fan_in.py +175 -175
  80. tests/orchestrator/test_is_replaying_flag.py +101 -101
  81. tests/orchestrator/test_retries.py +308 -308
  82. tests/orchestrator/test_sequential_orchestrator.py +841 -841
  83. tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
  84. tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
  85. tests/orchestrator/test_serialization.py +30 -30
  86. tests/orchestrator/test_sub_orchestrator.py +95 -95
  87. tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
  88. tests/orchestrator/test_task_any.py +60 -60
  89. tests/tasks/tasks_test_utils.py +17 -17
  90. tests/tasks/test_long_timers.py +70 -0
  91. tests/tasks/test_new_uuid.py +34 -34
  92. tests/test_utils/ContextBuilder.py +174 -174
  93. tests/test_utils/EntityContextBuilder.py +56 -56
  94. tests/test_utils/constants.py +1 -1
  95. tests/test_utils/json_utils.py +30 -30
  96. tests/test_utils/testClasses.py +56 -56
  97. tests/utils/__init__.py +1 -0
  98. tests/utils/test_entity_utils.py +24 -0
  99. azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
  100. azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
  101. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
  102. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
  103. azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
  104. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,396 +1,398 @@
1
- from azure.durable_functions.models.ReplaySchema import ReplaySchema
2
- from .orchestrator_test_utils \
3
- import assert_orchestration_state_equals, assert_results_are_equal, get_orchestration_state_result, assert_valid_schema, \
4
- get_entity_state_result, assert_entity_state_equals
5
- from tests.test_utils.ContextBuilder import ContextBuilder
6
- from tests.test_utils.EntityContextBuilder import EntityContextBuilder
7
- from azure.durable_functions.models.OrchestratorState import OrchestratorState
8
- from azure.durable_functions.models.entities.EntityState import EntityState, OperationResult
9
- from azure.durable_functions.models.actions.CallEntityAction \
10
- import CallEntityAction
11
- from azure.durable_functions.models.actions.SignalEntityAction \
12
- import SignalEntityAction
13
- from tests.test_utils.testClasses import SerializableClass
14
- import azure.durable_functions as df
15
- from typing import Any, Dict, List
16
- import json
17
- import azure.functions as func
18
-
19
- app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
20
-
21
-
22
- def generator_function_call_entity(context):
23
- outputs = []
24
- entityId = df.EntityId("Counter", "myCounter")
25
- x = yield context.call_entity(entityId, "add", 3)
26
-
27
- outputs.append(x)
28
- return outputs
29
-
30
- def generator_function_catch_entity_exception(context):
31
- entityId = df.EntityId("Counter", "myCounter")
32
- try:
33
- yield context.call_entity(entityId, "add", 3)
34
- return "No exception thrown"
35
- except:
36
- return "Exception thrown"
37
-
38
- def generator_function_signal_entity(context):
39
- outputs = []
40
- entityId = df.EntityId("Counter", "myCounter")
41
- context.signal_entity(entityId, "add", 3)
42
- x = yield context.call_entity(entityId, "get")
43
-
44
- outputs.append(x)
45
- return outputs
46
-
47
- def counter_entity_function(context):
48
- """A Counter Durable Entity.
49
-
50
- A simple example of a Durable Entity that implements
51
- a simple counter.
52
- """
53
-
54
- current_value = context.get_state(lambda: 0)
55
- operation = context.operation_name
56
- if operation == "add":
57
- amount = context.get_input()
58
- current_value += amount
59
- elif operation == "reset":
60
- current_value = 0
61
- elif operation == "get":
62
- pass
63
-
64
- result = f"The state is now: {current_value}"
65
- context.set_state(current_value)
66
- context.set_result(result)
67
-
68
- @app.entity_trigger(context_name="context")
69
- def counter_entity_function_with_pystein(context):
70
- """A Counter Durable Entity.
71
-
72
- A simple example of a Durable Entity that implements
73
- a simple counter.
74
- """
75
-
76
- current_value = context.get_state(lambda: 0)
77
- operation = context.operation_name
78
- if operation == "add":
79
- amount = context.get_input()
80
- current_value += amount
81
- elif operation == "reset":
82
- current_value = 0
83
- elif operation == "get":
84
- pass
85
-
86
- result = f"The state is now: {current_value}"
87
- context.set_state(current_value)
88
- context.set_result(result)
89
-
90
- def counter_entity_function_raises_exception(context):
91
- raise Exception("boom!")
92
-
93
- @app.entity_trigger(context_name="context")
94
- def counter_entity_function_raises_exception_with_pystein(context):
95
- raise Exception("boom!")
96
-
97
- def test_entity_raises_exception():
98
- # Create input batch
99
- batch = []
100
- add_to_batch(batch, name="get")
101
- context_builder = EntityContextBuilder(batch=batch)
102
-
103
- # Run the entity, get observed result
104
- result = get_entity_state_result(
105
- context_builder,
106
- counter_entity_function_raises_exception,
107
- )
108
-
109
- # Construct expected result
110
- expected_state = entity_base_expected_state()
111
- apply_operation(expected_state, result="boom!", state=None, is_error=True)
112
- expected = expected_state.to_json()
113
-
114
- # Ensure expectation matches observed behavior
115
- #assert_valid_schema(result)
116
- assert_entity_state_equals(expected, result)
117
-
118
- def test_entity_raises_exception_with_pystein():
119
- # Create input batch
120
- batch = []
121
- add_to_batch(batch, name="get")
122
- context_builder = EntityContextBuilder(batch=batch)
123
-
124
- # Run the entity, get observed result
125
- result = get_entity_state_result(
126
- context_builder,
127
- counter_entity_function_raises_exception_with_pystein,
128
- uses_pystein=True
129
- )
130
-
131
- # Construct expected result
132
- expected_state = entity_base_expected_state()
133
- apply_operation(expected_state, result="boom!", state=None, is_error=True)
134
- expected = expected_state.to_json()
135
-
136
- # Ensure expectation matches observed behavior
137
- #assert_valid_schema(result)
138
- assert_entity_state_equals(expected, result)
139
-
140
- def test_entity_signal_then_call():
141
- """Tests that a simple counter entity outputs the correct value
142
- after a sequence of operations. Mostly just a sanity check.
143
- """
144
-
145
- # Create input batch
146
- batch = []
147
- add_to_batch(batch, name="add", input_=3)
148
- add_to_batch(batch, name="get")
149
- context_builder = EntityContextBuilder(batch=batch)
150
-
151
- # Run the entity, get observed result
152
- result = get_entity_state_result(
153
- context_builder,
154
- counter_entity_function,
155
- )
156
-
157
- # Construct expected result
158
- expected_state = entity_base_expected_state()
159
- apply_operation(expected_state, result="The state is now: 3", state=3)
160
- expected = expected_state.to_json()
161
-
162
- # Ensure expectation matches observed behavior
163
- #assert_valid_schema(result)
164
- assert_entity_state_equals(expected, result)
165
-
166
- def test_entity_signal_then_call_with_pystein():
167
- """Tests that a simple counter entity outputs the correct value
168
- after a sequence of operations. Mostly just a sanity check.
169
- """
170
-
171
- # Create input batch
172
- batch = []
173
- add_to_batch(batch, name="add", input_=3)
174
- add_to_batch(batch, name="get")
175
- context_builder = EntityContextBuilder(batch=batch)
176
-
177
- # Run the entity, get observed result
178
- result = get_entity_state_result(
179
- context_builder,
180
- counter_entity_function_with_pystein,
181
- uses_pystein=True
182
- )
183
-
184
- # Construct expected result
185
- expected_state = entity_base_expected_state()
186
- apply_operation(expected_state, result="The state is now: 3", state=3)
187
- expected = expected_state.to_json()
188
-
189
- # Ensure expectation matches observed behavior
190
- #assert_valid_schema(result)
191
- assert_entity_state_equals(expected, result)
192
-
193
- def apply_operation(entity_state: EntityState, result: Any, state: Any, is_error: bool = False):
194
- """Apply the effects of an operation to the expected entity state object
195
-
196
- Parameters
197
- ----------
198
- entity_state: EntityState
199
- The expected entity state object
200
- result: Any
201
- The result of the latest operation
202
- state: Any
203
- The state right after the latest operation
204
- is_error: bool
205
- Whether or not the operation resulted in an exception. Defaults to False
206
- """
207
- entity_state.state = state
208
-
209
- # We cannot control duration, so default it to zero and avoid checking for it
210
- # in later asserts
211
- duration = 0
212
- operation_result = OperationResult(
213
- is_error=is_error,
214
- duration=duration,
215
- result=result
216
- )
217
- entity_state._results.append(operation_result)
218
-
219
- def add_to_batch(batch: List[Dict[str, Any]], name: str, input_: Any=None):
220
- """Add new work item to the batch of entity operations.
221
-
222
- Parameters
223
- ----------
224
- batch: List[Dict[str, Any]]
225
- Current list of json-serialized entity work items
226
- name: str
227
- Name of the entity operation to be performed
228
- input_: Optional[Any]:
229
- Input to the operation. Defaults to None.
230
-
231
- Returns
232
- --------
233
- List[Dict[str, str]]:
234
- Batch of json-serialized entity work items
235
- """
236
- # It is key to serialize the input twice, as this is
237
- # the extension behavior
238
- packet = {
239
- "name": name,
240
- "input": json.dumps(json.dumps(input_))
241
- }
242
- batch.append(packet)
243
- return batch
244
-
245
-
246
- def entity_base_expected_state() -> EntityState:
247
- """Get a base entity state.
248
-
249
- Returns
250
- -------
251
- EntityState:
252
- An EntityState with no results, no signals, a None state, and entity_exists set to True.
253
- """
254
- return EntityState(results=[], signals=[], entity_exists=True, state=None)
255
-
256
- def add_call_entity_action_for_entity(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
257
- action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
258
- state.actions.append([action])
259
-
260
-
261
- def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState:
262
- return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema.value)
263
-
264
- def add_call_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
265
- action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
266
- state.actions.append([action])
267
-
268
- def add_signal_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
269
- action = SignalEntityAction(entity_id=id_, operation=op, input_=input_)
270
- state.actions.append([action])
271
-
272
- def add_call_entity_completed_events(
273
- context_builder: ContextBuilder, op: str, instance_id=str, input_=None, event_id=0, is_error=False, literal_input=False):
274
- context_builder.add_event_sent_event(instance_id, event_id)
275
- context_builder.add_orchestrator_completed_event()
276
- context_builder.add_orchestrator_started_event()
277
- context_builder.add_event_raised_event(name="0000", id_=0, input_=input_, is_entity=True, is_error=is_error, literal_input=literal_input)
278
-
279
- def test_call_entity_sent():
280
- context_builder = ContextBuilder('test_simple_function')
281
-
282
- entityId = df.EntityId("Counter", "myCounter")
283
- result = get_orchestration_state_result(
284
- context_builder, generator_function_call_entity)
285
-
286
- expected_state = base_expected_state()
287
- add_call_entity_action(expected_state, entityId, "add", 3)
288
- expected = expected_state.to_json()
289
-
290
- #assert_valid_schema(result)
291
- assert_orchestration_state_equals(expected, result)
292
-
293
- def test_signal_entity_sent():
294
- context_builder = ContextBuilder('test_simple_function')
295
-
296
- entityId = df.EntityId("Counter", "myCounter")
297
- result = get_orchestration_state_result(
298
- context_builder, generator_function_signal_entity)
299
-
300
- expected_state = base_expected_state()
301
- add_signal_entity_action(expected_state, entityId, "add", 3)
302
- add_call_entity_action(expected_state, entityId, "get", None)
303
- expected = expected_state.to_json()
304
-
305
- #assert_valid_schema(result)
306
- assert_orchestration_state_equals(expected, result)
307
-
308
- def test_signal_entity_sent_and_response_received():
309
- entityId = df.EntityId("Counter", "myCounter")
310
- context_builder = ContextBuilder('test_simple_function')
311
- add_call_entity_completed_events(context_builder, "get", df.EntityId.get_scheduler_id(entityId), 3, 1)
312
-
313
-
314
- result = get_orchestration_state_result(
315
- context_builder, generator_function_signal_entity)
316
-
317
- expected_state = base_expected_state([3])
318
- add_signal_entity_action(expected_state, entityId, "add", 3)
319
- add_call_entity_action(expected_state, entityId, "get", None)
320
- expected_state._is_done = True
321
- expected = expected_state.to_json()
322
-
323
- #assert_valid_schema(result)
324
- assert_orchestration_state_equals(expected, result)
325
-
326
-
327
- def test_call_entity_raised():
328
- entityId = df.EntityId("Counter", "myCounter")
329
- context_builder = ContextBuilder('test_simple_function')
330
- add_call_entity_completed_events(context_builder, "add", df.EntityId.get_scheduler_id(entityId), 3, 0)
331
-
332
- result = get_orchestration_state_result(
333
- context_builder, generator_function_call_entity)
334
-
335
- expected_state = base_expected_state(
336
- [3]
337
- )
338
-
339
- add_call_entity_action(expected_state, entityId, "add", 3)
340
- expected_state._is_done = True
341
- expected = expected_state.to_json()
342
-
343
- #assert_valid_schema(result)
344
-
345
- assert_orchestration_state_equals(expected, result)
346
-
347
- def test_call_entity_catch_exception():
348
- entityId = df.EntityId("Counter", "myCounter")
349
- context_builder = ContextBuilder('catch exceptions')
350
- add_call_entity_completed_events(
351
- context_builder,
352
- "add",
353
- df.EntityId.get_scheduler_id(entityId),
354
- input_="I am an error!",
355
- event_id=0,
356
- is_error=True
357
- )
358
-
359
- result = get_orchestration_state_result(
360
- context_builder, generator_function_catch_entity_exception)
361
-
362
- expected_state = base_expected_state(
363
- "Exception thrown"
364
- )
365
-
366
- add_call_entity_action(expected_state, entityId, "add", 3)
367
- expected_state._is_done = True
368
- expected = expected_state.to_json()
369
-
370
- assert_orchestration_state_equals(expected, result)
371
-
372
- def test_timeout_entity_catch_exception():
373
- entityId = df.EntityId("Counter", "myCounter")
374
- context_builder = ContextBuilder('catch timeout exceptions')
375
- add_call_entity_completed_events(
376
- context_builder,
377
- "add",
378
- df.EntityId.get_scheduler_id(entityId),
379
- input_="Timeout value of 00:02:00 was exceeded by function: Functions.SlowEntity.",
380
- event_id=0,
381
- is_error=False,
382
- literal_input=True
383
- )
384
-
385
- result = get_orchestration_state_result(
386
- context_builder, generator_function_catch_entity_exception)
387
-
388
- expected_state = base_expected_state(
389
- "Exception thrown"
390
- )
391
-
392
- add_call_entity_action(expected_state, entityId, "add", 3)
393
- expected_state._is_done = True
394
- expected = expected_state.to_json()
395
-
1
+ from azure.durable_functions.models.ReplaySchema import ReplaySchema
2
+ from .orchestrator_test_utils \
3
+ import assert_orchestration_state_equals, assert_results_are_equal, get_orchestration_state_result, assert_valid_schema, \
4
+ get_entity_state_result, assert_entity_state_equals
5
+ from tests.test_utils.ContextBuilder import ContextBuilder
6
+ from tests.test_utils.EntityContextBuilder import EntityContextBuilder
7
+ from azure.durable_functions.models.OrchestratorState import OrchestratorState
8
+ from azure.durable_functions.models.entities.EntityState import EntityState, OperationResult
9
+ from azure.durable_functions.models.actions.CallEntityAction \
10
+ import CallEntityAction
11
+ from azure.durable_functions.models.actions.SignalEntityAction \
12
+ import SignalEntityAction
13
+ from tests.test_utils.testClasses import SerializableClass
14
+ import azure.durable_functions as df
15
+ from typing import Any, Dict, List
16
+ import json
17
+ import azure.functions as func
18
+
19
+ app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
20
+
21
+
22
+ def generator_function_call_entity(context):
23
+ outputs = []
24
+ entityId = df.EntityId("Counter", "myCounter")
25
+ x = yield context.call_entity(entityId, "add", 3)
26
+
27
+ outputs.append(x)
28
+ return outputs
29
+
30
+ def generator_function_catch_entity_exception(context):
31
+ entityId = df.EntityId("Counter", "myCounter")
32
+ try:
33
+ yield context.call_entity(entityId, "add", 3)
34
+ return "No exception thrown"
35
+ except:
36
+ return "Exception thrown"
37
+
38
+ def generator_function_signal_entity(context):
39
+ outputs = []
40
+ entityId = df.EntityId("Counter", "myCounter")
41
+ context.signal_entity(entityId, "add", 3)
42
+ x = yield context.call_entity(entityId, "get")
43
+
44
+ outputs.append(x)
45
+ return outputs
46
+
47
+ def counter_entity_function(context):
48
+ """A Counter Durable Entity.
49
+
50
+ A simple example of a Durable Entity that implements
51
+ a simple counter.
52
+ """
53
+
54
+ current_value = context.get_state(lambda: 0)
55
+ operation = context.operation_name
56
+ if operation == "add":
57
+ amount = context.get_input()
58
+ current_value += amount
59
+ elif operation == "reset":
60
+ current_value = 0
61
+ elif operation == "get":
62
+ pass
63
+
64
+ result = f"The state is now: {current_value}"
65
+ context.set_state(current_value)
66
+ context.set_result(result)
67
+
68
+ @app.entity_trigger(context_name="context")
69
+ def counter_entity_function_with_pystein(context):
70
+ """A Counter Durable Entity.
71
+
72
+ A simple example of a Durable Entity that implements
73
+ a simple counter.
74
+ """
75
+
76
+ current_value = context.get_state(lambda: 0)
77
+ operation = context.operation_name
78
+ if operation == "add":
79
+ amount = context.get_input()
80
+ current_value += amount
81
+ elif operation == "reset":
82
+ current_value = 0
83
+ elif operation == "get":
84
+ pass
85
+
86
+ result = f"The state is now: {current_value}"
87
+ context.set_state(current_value)
88
+ context.set_result(result)
89
+
90
+ def counter_entity_function_raises_exception(context):
91
+ raise Exception("boom!")
92
+
93
+ @app.entity_trigger(context_name="context")
94
+ def counter_entity_function_raises_exception_with_pystein(context):
95
+ raise Exception("boom!")
96
+
97
+ def test_entity_raises_exception():
98
+ # Create input batch
99
+ batch = []
100
+ add_to_batch(batch, name="get")
101
+ context_builder = EntityContextBuilder(batch=batch)
102
+
103
+ # Run the entity, get observed result
104
+ result = get_entity_state_result(
105
+ context_builder,
106
+ counter_entity_function_raises_exception,
107
+ )
108
+
109
+ # Construct expected result
110
+ expected_state = entity_base_expected_state()
111
+ apply_operation(expected_state, result="boom!", state=None, is_error=True)
112
+ expected = expected_state.to_json()
113
+
114
+ # Ensure expectation matches observed behavior
115
+ #assert_valid_schema(result)
116
+ assert_entity_state_equals(expected, result)
117
+
118
+ def test_entity_raises_exception_with_pystein():
119
+ # Create input batch
120
+ batch = []
121
+ add_to_batch(batch, name="get")
122
+ context_builder = EntityContextBuilder(batch=batch)
123
+
124
+ # Run the entity, get observed result
125
+ result = get_entity_state_result(
126
+ context_builder,
127
+ counter_entity_function_raises_exception_with_pystein,
128
+ uses_pystein=True
129
+ )
130
+
131
+ # Construct expected result
132
+ expected_state = entity_base_expected_state()
133
+ apply_operation(expected_state, result="boom!", state=None, is_error=True)
134
+ expected = expected_state.to_json()
135
+
136
+ # Ensure expectation matches observed behavior
137
+ #assert_valid_schema(result)
138
+ assert_entity_state_equals(expected, result)
139
+
140
+ def test_entity_signal_then_call():
141
+ """Tests that a simple counter entity outputs the correct value
142
+ after a sequence of operations. Mostly just a sanity check.
143
+ """
144
+
145
+ # Create input batch
146
+ batch = []
147
+ add_to_batch(batch, name="add", input_=3)
148
+ add_to_batch(batch, name="get")
149
+ context_builder = EntityContextBuilder(batch=batch)
150
+
151
+ # Run the entity, get observed result
152
+ result = get_entity_state_result(
153
+ context_builder,
154
+ counter_entity_function,
155
+ )
156
+
157
+ # Construct expected result
158
+ expected_state = entity_base_expected_state()
159
+ apply_operation(expected_state, result="The state is now: 3", state=3)
160
+ expected = expected_state.to_json()
161
+
162
+ # Ensure expectation matches observed behavior
163
+ #assert_valid_schema(result)
164
+ assert_entity_state_equals(expected, result)
165
+
166
+ def test_entity_signal_then_call_with_pystein():
167
+ """Tests that a simple counter entity outputs the correct value
168
+ after a sequence of operations. Mostly just a sanity check.
169
+ """
170
+
171
+ # Create input batch
172
+ batch = []
173
+ add_to_batch(batch, name="add", input_=3)
174
+ add_to_batch(batch, name="get")
175
+ context_builder = EntityContextBuilder(batch=batch)
176
+
177
+ # Run the entity, get observed result
178
+ result = get_entity_state_result(
179
+ context_builder,
180
+ counter_entity_function_with_pystein,
181
+ uses_pystein=True
182
+ )
183
+
184
+ # Construct expected result
185
+ expected_state = entity_base_expected_state()
186
+ apply_operation(expected_state, result="The state is now: 3", state=3)
187
+ expected = expected_state.to_json()
188
+
189
+ # Ensure expectation matches observed behavior
190
+ #assert_valid_schema(result)
191
+ assert_entity_state_equals(expected, result)
192
+
193
+ def apply_operation(entity_state: EntityState, result: Any, state: Any, is_error: bool = False):
194
+ """Apply the effects of an operation to the expected entity state object
195
+
196
+ Parameters
197
+ ----------
198
+ entity_state: EntityState
199
+ The expected entity state object
200
+ result: Any
201
+ The result of the latest operation
202
+ state: Any
203
+ The state right after the latest operation
204
+ is_error: bool
205
+ Whether or not the operation resulted in an exception. Defaults to False
206
+ """
207
+ entity_state.state = state
208
+
209
+ # We cannot control duration, so default it to zero and avoid checking for it
210
+ # in later asserts
211
+ duration = 0
212
+ start_time = 0
213
+ operation_result = OperationResult(
214
+ is_error=is_error,
215
+ duration=duration,
216
+ execution_start_time_ms=start_time,
217
+ result=result
218
+ )
219
+ entity_state._results.append(operation_result)
220
+
221
+ def add_to_batch(batch: List[Dict[str, Any]], name: str, input_: Any=None):
222
+ """Add new work item to the batch of entity operations.
223
+
224
+ Parameters
225
+ ----------
226
+ batch: List[Dict[str, Any]]
227
+ Current list of json-serialized entity work items
228
+ name: str
229
+ Name of the entity operation to be performed
230
+ input_: Optional[Any]:
231
+ Input to the operation. Defaults to None.
232
+
233
+ Returns
234
+ --------
235
+ List[Dict[str, str]]:
236
+ Batch of json-serialized entity work items
237
+ """
238
+ # It is key to serialize the input twice, as this is
239
+ # the extension behavior
240
+ packet = {
241
+ "name": name,
242
+ "input": json.dumps(json.dumps(input_))
243
+ }
244
+ batch.append(packet)
245
+ return batch
246
+
247
+
248
+ def entity_base_expected_state() -> EntityState:
249
+ """Get a base entity state.
250
+
251
+ Returns
252
+ -------
253
+ EntityState:
254
+ An EntityState with no results, no signals, a None state, and entity_exists set to True.
255
+ """
256
+ return EntityState(results=[], signals=[], entity_exists=True, state=None)
257
+
258
+ def add_call_entity_action_for_entity(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
259
+ action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
260
+ state.actions.append([action])
261
+
262
+
263
+ def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState:
264
+ return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema.value)
265
+
266
+ def add_call_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
267
+ action = CallEntityAction(entity_id=id_, operation=op, input_=input_)
268
+ state.actions.append([action])
269
+
270
+ def add_signal_entity_action(state: OrchestratorState, id_: df.EntityId, op: str, input_: Any):
271
+ action = SignalEntityAction(entity_id=id_, operation=op, input_=input_)
272
+ state.actions.append([action])
273
+
274
+ def add_call_entity_completed_events(
275
+ context_builder: ContextBuilder, op: str, instance_id=str, input_=None, event_id=0, is_error=False, literal_input=False):
276
+ context_builder.add_event_sent_event(instance_id, event_id)
277
+ context_builder.add_orchestrator_completed_event()
278
+ context_builder.add_orchestrator_started_event()
279
+ context_builder.add_event_raised_event(name="0000", id_=0, input_=input_, is_entity=True, is_error=is_error, literal_input=literal_input)
280
+
281
+ def test_call_entity_sent():
282
+ context_builder = ContextBuilder('test_simple_function')
283
+
284
+ entityId = df.EntityId("Counter", "myCounter")
285
+ result = get_orchestration_state_result(
286
+ context_builder, generator_function_call_entity)
287
+
288
+ expected_state = base_expected_state()
289
+ add_call_entity_action(expected_state, entityId, "add", 3)
290
+ expected = expected_state.to_json()
291
+
292
+ #assert_valid_schema(result)
293
+ assert_orchestration_state_equals(expected, result)
294
+
295
+ def test_signal_entity_sent():
296
+ context_builder = ContextBuilder('test_simple_function')
297
+
298
+ entityId = df.EntityId("Counter", "myCounter")
299
+ result = get_orchestration_state_result(
300
+ context_builder, generator_function_signal_entity)
301
+
302
+ expected_state = base_expected_state()
303
+ add_signal_entity_action(expected_state, entityId, "add", 3)
304
+ add_call_entity_action(expected_state, entityId, "get", None)
305
+ expected = expected_state.to_json()
306
+
307
+ #assert_valid_schema(result)
308
+ assert_orchestration_state_equals(expected, result)
309
+
310
+ def test_signal_entity_sent_and_response_received():
311
+ entityId = df.EntityId("Counter", "myCounter")
312
+ context_builder = ContextBuilder('test_simple_function')
313
+ add_call_entity_completed_events(context_builder, "get", df.EntityId.get_scheduler_id(entityId), 3, 1)
314
+
315
+
316
+ result = get_orchestration_state_result(
317
+ context_builder, generator_function_signal_entity)
318
+
319
+ expected_state = base_expected_state([3])
320
+ add_signal_entity_action(expected_state, entityId, "add", 3)
321
+ add_call_entity_action(expected_state, entityId, "get", None)
322
+ expected_state._is_done = True
323
+ expected = expected_state.to_json()
324
+
325
+ #assert_valid_schema(result)
326
+ assert_orchestration_state_equals(expected, result)
327
+
328
+
329
+ def test_call_entity_raised():
330
+ entityId = df.EntityId("Counter", "myCounter")
331
+ context_builder = ContextBuilder('test_simple_function')
332
+ add_call_entity_completed_events(context_builder, "add", df.EntityId.get_scheduler_id(entityId), 3, 0)
333
+
334
+ result = get_orchestration_state_result(
335
+ context_builder, generator_function_call_entity)
336
+
337
+ expected_state = base_expected_state(
338
+ [3]
339
+ )
340
+
341
+ add_call_entity_action(expected_state, entityId, "add", 3)
342
+ expected_state._is_done = True
343
+ expected = expected_state.to_json()
344
+
345
+ #assert_valid_schema(result)
346
+
347
+ assert_orchestration_state_equals(expected, result)
348
+
349
+ def test_call_entity_catch_exception():
350
+ entityId = df.EntityId("Counter", "myCounter")
351
+ context_builder = ContextBuilder('catch exceptions')
352
+ add_call_entity_completed_events(
353
+ context_builder,
354
+ "add",
355
+ df.EntityId.get_scheduler_id(entityId),
356
+ input_="I am an error!",
357
+ event_id=0,
358
+ is_error=True
359
+ )
360
+
361
+ result = get_orchestration_state_result(
362
+ context_builder, generator_function_catch_entity_exception)
363
+
364
+ expected_state = base_expected_state(
365
+ "Exception thrown"
366
+ )
367
+
368
+ add_call_entity_action(expected_state, entityId, "add", 3)
369
+ expected_state._is_done = True
370
+ expected = expected_state.to_json()
371
+
372
+ assert_orchestration_state_equals(expected, result)
373
+
374
+ def test_timeout_entity_catch_exception():
375
+ entityId = df.EntityId("Counter", "myCounter")
376
+ context_builder = ContextBuilder('catch timeout exceptions')
377
+ add_call_entity_completed_events(
378
+ context_builder,
379
+ "add",
380
+ df.EntityId.get_scheduler_id(entityId),
381
+ input_="Timeout value of 00:02:00 was exceeded by function: Functions.SlowEntity.",
382
+ event_id=0,
383
+ is_error=False,
384
+ literal_input=True
385
+ )
386
+
387
+ result = get_orchestration_state_result(
388
+ context_builder, generator_function_catch_entity_exception)
389
+
390
+ expected_state = base_expected_state(
391
+ "Exception thrown"
392
+ )
393
+
394
+ add_call_entity_action(expected_state, entityId, "add", 3)
395
+ expected_state._is_done = True
396
+ expected = expected_state.to_json()
397
+
396
398
  assert_orchestration_state_equals(expected, result)