durabletask 0.3.0__py3-none-any.whl → 0.4.1__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.

Potentially problematic release.


This version of durabletask might be problematic. Click here for more details.

durabletask/task.py CHANGED
@@ -35,6 +35,21 @@ class OrchestrationContext(ABC):
35
35
  """
36
36
  pass
37
37
 
38
+ @property
39
+ @abstractmethod
40
+ def version(self) -> Optional[str]:
41
+ """Get the version of the orchestration instance.
42
+
43
+ This version is set when the orchestration is scheduled and can be used
44
+ to determine which version of the orchestrator function is being executed.
45
+
46
+ Returns
47
+ -------
48
+ Optional[str]
49
+ The version of the orchestration instance, or None if not set.
50
+ """
51
+ pass
52
+
38
53
  @property
39
54
  @abstractmethod
40
55
  def current_utc_datetime(self) -> datetime:
@@ -100,7 +115,8 @@ class OrchestrationContext(ABC):
100
115
  @abstractmethod
101
116
  def call_activity(self, activity: Union[Activity[TInput, TOutput], str], *,
102
117
  input: Optional[TInput] = None,
103
- retry_policy: Optional[RetryPolicy] = None) -> Task[TOutput]:
118
+ retry_policy: Optional[RetryPolicy] = None,
119
+ tags: Optional[dict[str, str]] = None) -> Task[TOutput]:
104
120
  """Schedule an activity for execution.
105
121
 
106
122
  Parameters
@@ -111,6 +127,8 @@ class OrchestrationContext(ABC):
111
127
  The JSON-serializable input (or None) to pass to the activity.
112
128
  retry_policy: Optional[RetryPolicy]
113
129
  The retry policy to use for this activity call.
130
+ tags: Optional[dict[str, str]]
131
+ Optional tags to associate with the activity invocation.
114
132
 
115
133
  Returns
116
134
  -------
@@ -123,7 +141,8 @@ class OrchestrationContext(ABC):
123
141
  def call_sub_orchestrator(self, orchestrator: Orchestrator[TInput, TOutput], *,
124
142
  input: Optional[TInput] = None,
125
143
  instance_id: Optional[str] = None,
126
- retry_policy: Optional[RetryPolicy] = None) -> Task[TOutput]:
144
+ retry_policy: Optional[RetryPolicy] = None,
145
+ version: Optional[str] = None) -> Task[TOutput]:
127
146
  """Schedule sub-orchestrator function for execution.
128
147
 
129
148
  Parameters
durabletask/worker.py CHANGED
@@ -10,12 +10,15 @@ from concurrent.futures import ThreadPoolExecutor
10
10
  from datetime import datetime, timedelta
11
11
  from threading import Event, Thread
12
12
  from types import GeneratorType
13
+ from enum import Enum
13
14
  from typing import Any, Generator, Optional, Sequence, TypeVar, Union
15
+ from packaging.version import InvalidVersion, parse
14
16
 
15
17
  import grpc
16
18
  from google.protobuf import empty_pb2
17
19
 
18
20
  import durabletask.internal.helpers as ph
21
+ import durabletask.internal.exceptions as pe
19
22
  import durabletask.internal.orchestrator_service_pb2 as pb
20
23
  import durabletask.internal.orchestrator_service_pb2_grpc as stubs
21
24
  import durabletask.internal.shared as shared
@@ -72,9 +75,56 @@ class ConcurrencyOptions:
72
75
  )
73
76
 
74
77
 
78
+ class VersionMatchStrategy(Enum):
79
+ """Enumeration for version matching strategies."""
80
+
81
+ NONE = 1
82
+ STRICT = 2
83
+ CURRENT_OR_OLDER = 3
84
+
85
+
86
+ class VersionFailureStrategy(Enum):
87
+ """Enumeration for version failure strategies."""
88
+
89
+ REJECT = 1
90
+ FAIL = 2
91
+
92
+
93
+ class VersioningOptions:
94
+ """Configuration options for orchestrator and activity versioning.
95
+
96
+ This class provides options to control how versioning is handled for orchestrators
97
+ and activities, including whether to use the default version and how to compare versions.
98
+ """
99
+
100
+ version: Optional[str] = None
101
+ default_version: Optional[str] = None
102
+ match_strategy: Optional[VersionMatchStrategy] = None
103
+ failure_strategy: Optional[VersionFailureStrategy] = None
104
+
105
+ def __init__(self, version: Optional[str] = None,
106
+ default_version: Optional[str] = None,
107
+ match_strategy: Optional[VersionMatchStrategy] = None,
108
+ failure_strategy: Optional[VersionFailureStrategy] = None
109
+ ):
110
+ """Initialize versioning options.
111
+
112
+ Args:
113
+ version: The version of orchestrations that the worker can work on.
114
+ default_version: The default version that will be used for starting new sub-orchestrations.
115
+ match_strategy: The versioning strategy for the Durable Task worker.
116
+ failure_strategy: The versioning failure strategy for the Durable Task worker.
117
+ """
118
+ self.version = version
119
+ self.default_version = default_version
120
+ self.match_strategy = match_strategy
121
+ self.failure_strategy = failure_strategy
122
+
123
+
75
124
  class _Registry:
76
125
  orchestrators: dict[str, task.Orchestrator]
77
126
  activities: dict[str, task.Activity]
127
+ versioning: Optional[VersioningOptions] = None
78
128
 
79
129
  def __init__(self):
80
130
  self.orchestrators = {}
@@ -279,6 +329,12 @@ class TaskHubGrpcWorker:
279
329
  )
280
330
  return self._registry.add_activity(fn)
281
331
 
332
+ def use_versioning(self, version: VersioningOptions) -> None:
333
+ """Initializes versioning options for sub-orchestrators and activities."""
334
+ if self._is_running:
335
+ raise RuntimeError("Cannot set default version while the worker is running.")
336
+ self._registry.versioning = version
337
+
282
338
  def start(self):
283
339
  """Starts the worker on a background thread and begins listening for work items."""
284
340
  if self._is_running:
@@ -513,6 +569,16 @@ class TaskHubGrpcWorker:
513
569
  customStatus=ph.get_string_value(result.encoded_custom_status),
514
570
  completionToken=completionToken,
515
571
  )
572
+ except pe.AbandonOrchestrationError:
573
+ self._logger.info(
574
+ f"Abandoning orchestration. InstanceId = '{req.instanceId}'. Completion token = '{completionToken}'"
575
+ )
576
+ stub.AbandonTaskOrchestratorWorkItem(
577
+ pb.AbandonOrchestrationTaskRequest(
578
+ completionToken=completionToken
579
+ )
580
+ )
581
+ return
516
582
  except Exception as ex:
517
583
  self._logger.exception(
518
584
  f"An error occurred while trying to execute instance '{req.instanceId}': {ex}"
@@ -574,7 +640,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
574
640
  _generator: Optional[Generator[task.Task, Any, Any]]
575
641
  _previous_task: Optional[task.Task]
576
642
 
577
- def __init__(self, instance_id: str):
643
+ def __init__(self, instance_id: str, registry: _Registry):
578
644
  self._generator = None
579
645
  self._is_replaying = True
580
646
  self._is_complete = False
@@ -584,6 +650,8 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
584
650
  self._sequence_number = 0
585
651
  self._current_utc_datetime = datetime(1000, 1, 1)
586
652
  self._instance_id = instance_id
653
+ self._registry = registry
654
+ self._version: Optional[str] = None
587
655
  self._completion_status: Optional[pb.OrchestrationStatus] = None
588
656
  self._received_events: dict[str, list[Any]] = {}
589
657
  self._pending_events: dict[str, list[task.CompletableTask]] = {}
@@ -646,7 +714,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
646
714
  )
647
715
  self._pending_actions[action.id] = action
648
716
 
649
- def set_failed(self, ex: Exception):
717
+ def set_failed(self, ex: Union[Exception, pb.TaskFailureDetails]):
650
718
  if self._is_complete:
651
719
  return
652
720
 
@@ -658,7 +726,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
658
726
  self.next_sequence_number(),
659
727
  pb.ORCHESTRATION_STATUS_FAILED,
660
728
  None,
661
- ph.new_failure_details(ex),
729
+ ph.new_failure_details(ex) if isinstance(ex, Exception) else ex,
662
730
  )
663
731
  self._pending_actions[action.id] = action
664
732
 
@@ -709,6 +777,10 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
709
777
  def instance_id(self) -> str:
710
778
  return self._instance_id
711
779
 
780
+ @property
781
+ def version(self) -> Optional[str]:
782
+ return self._version
783
+
712
784
  @property
713
785
  def current_utc_datetime(self) -> datetime:
714
786
  return self._current_utc_datetime
@@ -752,11 +824,12 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
752
824
  *,
753
825
  input: Optional[TInput] = None,
754
826
  retry_policy: Optional[task.RetryPolicy] = None,
827
+ tags: Optional[dict[str, str]] = None,
755
828
  ) -> task.Task[TOutput]:
756
829
  id = self.next_sequence_number()
757
830
 
758
831
  self.call_activity_function_helper(
759
- id, activity, input=input, retry_policy=retry_policy, is_sub_orch=False
832
+ id, activity, input=input, retry_policy=retry_policy, is_sub_orch=False, tags=tags
760
833
  )
761
834
  return self._pending_tasks.get(id, task.CompletableTask())
762
835
 
@@ -767,9 +840,12 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
767
840
  input: Optional[TInput] = None,
768
841
  instance_id: Optional[str] = None,
769
842
  retry_policy: Optional[task.RetryPolicy] = None,
843
+ version: Optional[str] = None,
770
844
  ) -> task.Task[TOutput]:
771
845
  id = self.next_sequence_number()
772
846
  orchestrator_name = task.get_name(orchestrator)
847
+ default_version = self._registry.versioning.default_version if self._registry.versioning else None
848
+ orchestrator_version = version if version else default_version
773
849
  self.call_activity_function_helper(
774
850
  id,
775
851
  orchestrator_name,
@@ -777,6 +853,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
777
853
  retry_policy=retry_policy,
778
854
  is_sub_orch=True,
779
855
  instance_id=instance_id,
856
+ version=orchestrator_version
780
857
  )
781
858
  return self._pending_tasks.get(id, task.CompletableTask())
782
859
 
@@ -787,9 +864,11 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
787
864
  *,
788
865
  input: Optional[TInput] = None,
789
866
  retry_policy: Optional[task.RetryPolicy] = None,
867
+ tags: Optional[dict[str, str]] = None,
790
868
  is_sub_orch: bool = False,
791
869
  instance_id: Optional[str] = None,
792
870
  fn_task: Optional[task.CompletableTask[TOutput]] = None,
871
+ version: Optional[str] = None,
793
872
  ):
794
873
  if id is None:
795
874
  id = self.next_sequence_number()
@@ -806,7 +885,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
806
885
  if isinstance(activity_function, str)
807
886
  else task.get_name(activity_function)
808
887
  )
809
- action = ph.new_schedule_task_action(id, name, encoded_input)
888
+ action = ph.new_schedule_task_action(id, name, encoded_input, tags)
810
889
  else:
811
890
  if instance_id is None:
812
891
  # Create a deteministic instance ID based on the parent instance ID
@@ -814,7 +893,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
814
893
  if not isinstance(activity_function, str):
815
894
  raise ValueError("Orchestrator function name must be a string")
816
895
  action = ph.new_create_sub_orchestration_action(
817
- id, activity_function, instance_id, encoded_input
896
+ id, activity_function, instance_id, encoded_input, version
818
897
  )
819
898
  self._pending_actions[id] = action
820
899
 
@@ -890,7 +969,7 @@ class _OrchestrationExecutor:
890
969
  "The new history event list must have at least one event in it."
891
970
  )
892
971
 
893
- ctx = _RuntimeOrchestrationContext(instance_id)
972
+ ctx = _RuntimeOrchestrationContext(instance_id, self._registry)
894
973
  try:
895
974
  # Rebuild local state by replaying old history into the orchestrator function
896
975
  self._logger.debug(
@@ -910,6 +989,15 @@ class _OrchestrationExecutor:
910
989
  for new_event in new_events:
911
990
  self.process_event(ctx, new_event)
912
991
 
992
+ except pe.VersionFailureException as ex:
993
+ if self._registry.versioning and self._registry.versioning.failure_strategy == VersionFailureStrategy.FAIL:
994
+ if ex.error_details:
995
+ ctx.set_failed(ex.error_details)
996
+ else:
997
+ ctx.set_failed(ex)
998
+ elif self._registry.versioning and self._registry.versioning.failure_strategy == VersionFailureStrategy.REJECT:
999
+ raise pe.AbandonOrchestrationError
1000
+
913
1001
  except Exception as ex:
914
1002
  # Unhandled exceptions fail the orchestration
915
1003
  ctx.set_failed(ex)
@@ -959,6 +1047,22 @@ class _OrchestrationExecutor:
959
1047
  f"A '{event.executionStarted.name}' orchestrator was not registered."
960
1048
  )
961
1049
 
1050
+ if event.executionStarted.version:
1051
+ ctx._version = event.executionStarted.version.value
1052
+
1053
+ if self._registry.versioning:
1054
+ version_failure = self.evaluate_orchestration_versioning(
1055
+ self._registry.versioning,
1056
+ ctx.version
1057
+ )
1058
+ if version_failure:
1059
+ self._logger.warning(
1060
+ f"Orchestration version did not meet worker versioning requirements. "
1061
+ f"Error action = '{self._registry.versioning.failure_strategy}'. "
1062
+ f"Version error = '{version_failure}'"
1063
+ )
1064
+ raise pe.VersionFailureException(version_failure)
1065
+
962
1066
  # deserialize the input, if any
963
1067
  input = None
964
1068
  if (
@@ -1221,6 +1325,48 @@ class _OrchestrationExecutor:
1221
1325
  # The orchestrator generator function completed
1222
1326
  ctx.set_complete(generatorStopped.value, pb.ORCHESTRATION_STATUS_COMPLETED)
1223
1327
 
1328
+ def evaluate_orchestration_versioning(self, versioning: Optional[VersioningOptions], orchestration_version: Optional[str]) -> Optional[pb.TaskFailureDetails]:
1329
+ if versioning is None:
1330
+ return None
1331
+ version_comparison = self.compare_versions(orchestration_version, versioning.version)
1332
+ if versioning.match_strategy == VersionMatchStrategy.NONE:
1333
+ return None
1334
+ elif versioning.match_strategy == VersionMatchStrategy.STRICT:
1335
+ if version_comparison != 0:
1336
+ return pb.TaskFailureDetails(
1337
+ errorType="VersionMismatch",
1338
+ errorMessage=f"The orchestration version '{orchestration_version}' does not match the worker version '{versioning.version}'.",
1339
+ isNonRetriable=True,
1340
+ )
1341
+ elif versioning.match_strategy == VersionMatchStrategy.CURRENT_OR_OLDER:
1342
+ if version_comparison > 0:
1343
+ return pb.TaskFailureDetails(
1344
+ errorType="VersionMismatch",
1345
+ errorMessage=f"The orchestration version '{orchestration_version}' is greater than the worker version '{versioning.version}'.",
1346
+ isNonRetriable=True,
1347
+ )
1348
+ else:
1349
+ # If there is a type of versioning we don't understand, it is better to treat it as a versioning failure.
1350
+ return pb.TaskFailureDetails(
1351
+ errorType="VersionMismatch",
1352
+ errorMessage=f"The version match strategy '{versioning.match_strategy}' is unknown.",
1353
+ isNonRetriable=True,
1354
+ )
1355
+
1356
+ def compare_versions(self, source_version: Optional[str], default_version: Optional[str]) -> int:
1357
+ if not source_version and not default_version:
1358
+ return 0
1359
+ if not source_version:
1360
+ return -1
1361
+ if not default_version:
1362
+ return 1
1363
+ try:
1364
+ source_version_parsed = parse(source_version)
1365
+ default_version_parsed = parse(default_version)
1366
+ return (source_version_parsed > default_version_parsed) - (source_version_parsed < default_version_parsed)
1367
+ except InvalidVersion:
1368
+ return (source_version > default_version) - (source_version < default_version)
1369
+
1224
1370
 
1225
1371
  class _ActivityExecutor:
1226
1372
  def __init__(self, registry: _Registry, logger: logging.Logger):
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: durabletask
3
+ Version: 0.4.1
4
+ Summary: A Durable Task Client SDK for Python
5
+ License: MIT License
6
+
7
+ Copyright (c) Microsoft Corporation.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE
26
+
27
+ Project-URL: repository, https://github.com/microsoft/durabletask-python
28
+ Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
29
+ Keywords: durable,task,workflow
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: grpcio
37
+ Requires-Dist: protobuf
38
+ Requires-Dist: asyncio
39
+ Requires-Dist: packaging
40
+ Dynamic: license-file
41
+
42
+ # Durable Task SDK for Python
43
+
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
45
+ [![Build Validation](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/microsoft/durabletask-python/actions/workflows/pr-validation.yml)
46
+ [![PyPI version](https://badge.fury.io/py/durabletask.svg)](https://badge.fury.io/py/durabletask)
47
+
48
+ This repo contains a Python SDK for use with the [Azure Durable Task Scheduler](https://github.com/Azure/Durable-Task-Scheduler). With this SDK, you can define, schedule, and manage durable orchestrations using ordinary Python code.
49
+
50
+ > Note that this SDK is **not** currently compatible with [Azure Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview). If you are looking for a Python SDK for Azure Durable Functions, please see [this repo](https://github.com/Azure/azure-functions-durable-python).
51
+
52
+ # References
53
+ - [Supported Patterns](./docs/supported-patterns.md)
54
+ - [Available Features](./docs/features.md)
55
+ - [Getting Started](./docs/getting-started.md)
56
+ - [Development Guide](./docs/development.md)
57
+ - [Contributing Guide](./CONTRIBUTING.md)
58
+
59
+ ## Trademarks
60
+ This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
61
+ trademarks or logos is subject to and must follow
62
+ [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
63
+ Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
64
+ Any use of third-party trademarks or logos are subject to those third-party's policies.
@@ -0,0 +1,16 @@
1
+ durabletask/__init__.py,sha256=1PTnFPvigLCs2apa7ITASeqqFOS09Zn-rrllyWeoDJE,263
2
+ durabletask/client.py,sha256=VrHXntWfSk4xYAv7JkOOXgNoFzefYWcD7UPGgn2zuWM,10312
3
+ durabletask/task.py,sha256=LCtKrDh_Yb94Zjxx8fUtXt3z5GwSaV-EieIFXKeZNOI,18404
4
+ durabletask/worker.py,sha256=B0cUfzbgQW2WlYzixvQqRZkWRtvsETdD3jDLz64RwjU,73279
5
+ durabletask/internal/exceptions.py,sha256=G1qnH5KReJk04_eewMIr2TTXvdeWDriq-SiAwSahMMs,298
6
+ durabletask/internal/grpc_interceptor.py,sha256=KGl8GGIbNdiEnWVLwQwkOemWvIlcEO0dh-_Tg20h5XA,2834
7
+ durabletask/internal/helpers.py,sha256=7A1Bb-KNuAovQTOh9mxeTFahCPtl45C5j5tUJ43BXuo,7538
8
+ durabletask/internal/orchestrator_service_pb2.py,sha256=q4elBQnofrZ4eYVCVnmed4vsa-FLNFLeBRtFOZYhTv8,44631
9
+ durabletask/internal/orchestrator_service_pb2.pyi,sha256=FFxZhxdV7SX0pKxwKPHTGaAdXKowXnib4WvjhGFw2eo,66480
10
+ durabletask/internal/orchestrator_service_pb2_grpc.py,sha256=5xhDLJ73Ipsp1tjwWhUqVqEm30MjWHlj71zfPNBehWc,54366
11
+ durabletask/internal/shared.py,sha256=dKRGU8z1EQM4_YA6zkKeKfiaWbiZ6-B8lP-wHy7Q_jI,4379
12
+ durabletask-0.4.1.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
13
+ durabletask-0.4.1.dist-info/METADATA,sha256=NMPUDyoxbMC33_X068dk80Ml3p3LKmXuTDV4VQlusw4,3724
14
+ durabletask-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ durabletask-0.4.1.dist-info/top_level.txt,sha256=EBVyuKWnjOwq8bJI1Uvb9U3c4fzQxACWj9p83he6fik,12
16
+ durabletask-0.4.1.dist-info/RECORD,,