2525import uuid
2626from collections .abc import Sequence
2727from dataclasses import dataclass
28- from datetime import datetime
29- from typing import Optional , Protocol , TypeVar , runtime_checkable
28+ from datetime import datetime , timedelta
29+ from typing import Optional , TypeVar
3030
3131
3232@dataclass
@@ -99,17 +99,6 @@ def deterministic_uuid_v5(instance_id: str, current_datetime: datetime, counter:
9999 return uuid .uuid5 (namespace , name )
100100
101101
102- @runtime_checkable
103- class DeterministicContextProtocol (Protocol ):
104- """Protocol for contexts that provide deterministic operations."""
105-
106- @property
107- def instance_id (self ) -> str : ...
108-
109- @property
110- def current_utc_datetime (self ) -> datetime : ...
111-
112-
113102class DeterministicContextMixin :
114103 """
115104 Mixin providing deterministic helpers for workflow contexts.
@@ -121,25 +110,25 @@ class DeterministicContextMixin:
121110 """
122111
123112 def __init__ (self , * args , ** kwargs ):
124- """Initialize the mixin with a UUID counter ."""
113+ """Initialize the mixin with UUID and timestamp counters ."""
125114 super ().__init__ (* args , ** kwargs )
126115 # Counter for deterministic UUID generation (matches .NET newGuidCounter)
127116 # This counter resets to 0 on each replay, ensuring determinism
128117 self ._uuid_counter : int = 0
118+ # Counter for deterministic timestamp sequencing (resets on replay)
119+ self ._timestamp_counter : int = 0
129120
130121 def now (self ) -> datetime :
131- """Return orchestration time (deterministic UTC)."""
132- value = self .current_utc_datetime # type: ignore[attr-defined]
133- assert isinstance (value , datetime )
134- return value
122+ """Alias for deterministic current_utc_datetime."""
123+ return self .current_utc_datetime # type: ignore[attr-defined]
135124
136125 def random (self ) -> random .Random :
137126 """Return a PRNG seeded deterministically from instance id and orchestration time."""
138127 rnd = deterministic_random (
139128 self .instance_id , # type: ignore[attr-defined]
140129 self .current_utc_datetime , # type: ignore[attr-defined]
141130 )
142- # Mark as deterministic for sandbox detector whitelisting of bound methods
131+ # Mark as deterministic for asyncio sandbox detector whitelisting of bound methods (randint, random)
143132 try :
144133 rnd ._dt_deterministic = True
145134 except Exception :
@@ -201,3 +190,35 @@ def random_choice(self, sequence: Sequence[T]) -> T:
201190 raise IndexError ("Cannot choose from empty sequence" )
202191 rnd = self .random ()
203192 return rnd .choice (sequence )
193+
194+ def now_with_sequence (self ) -> datetime :
195+ """
196+ Return deterministic timestamp with microsecond increment per call.
197+
198+ Each call returns: current_utc_datetime + (counter * 1 microsecond)
199+
200+ This provides ordered, unique timestamps for tracing/telemetry while maintaining
201+ determinism across replays. The counter resets to 0 on each replay (similar to
202+ _uuid_counter pattern).
203+
204+ Perfect for preserving event ordering within a workflow without requiring activities.
205+
206+ Returns:
207+ datetime: Deterministic timestamp that increments on each call
208+
209+ Example:
210+ ```python
211+ def workflow(ctx):
212+ t1 = ctx.now_with_sequence() # 2024-01-01 12:00:00.000000
213+ result = yield ctx.call_activity(some_activity, input="data")
214+ t2 = ctx.now_with_sequence() # 2024-01-01 12:00:00.000001
215+ # t1 < t2, preserving order for telemetry
216+ ```
217+ """
218+ offset = timedelta (microseconds = self ._timestamp_counter )
219+ self ._timestamp_counter += 1
220+ return self .current_utc_datetime + offset # type: ignore[attr-defined]
221+
222+ def current_utc_datetime_with_sequence (self ):
223+ """Alias for now_with_sequence for API parity with other SDKs."""
224+ return self .now_with_sequence ()
0 commit comments