clock-pattern 0.10.0__tar.gz → 0.11.0__tar.gz
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.
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/PKG-INFO +26 -10
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/README.md +24 -9
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/__init__.py +1 -1
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/fixed_clock.py +58 -4
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/models/deadline.py +1 -2
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/system_deadline.py +3 -4
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/testing/mock_deadline.py +0 -1
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/models/poller.py +3 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/models/poller_async.py +4 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/system_poller.py +6 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/system_poller_async.py +48 -1
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/models/retrier.py +3 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/models/retrier_async.py +3 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/system_retrier.py +11 -2
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/system_retrier_async.py +11 -2
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/testing/mock_retrier.py +18 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/testing/mock_retrier_async.py +18 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper.py +3 -3
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper_async.py +5 -3
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper.py +4 -4
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper_async.py +14 -7
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/testing/mock_sleeper_async.py +9 -2
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/pyproject.toml +24 -14
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/.gitignore +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/LICENSE.md +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/system_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/mock_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/clocks/utc_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/errors/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/errors/timeout_expired_error.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/deadlines/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/monotonic_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/system_monotonic_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/testing/mock_poller.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/testing/mock_poller_async.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/py.typed +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/testing/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/testing/mock_sleeper.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/stopwatches/__init__.py +0 -0
- {clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/stopwatches/stopwatch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clock-pattern
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: The Clock Pattern is a Python package that turns time into an injectable dependency.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adriamontoto/clock-pattern
|
|
6
6
|
Project-URL: Repository, https://github.com/adriamontoto/clock-pattern
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: typing-extensions>=4.4.0; python_version < '3.12'
|
|
23
24
|
Requires-Dist: value-object-pattern>=1.36.0
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
@@ -43,9 +44,6 @@ Description-Content-Type: text/markdown
|
|
|
43
44
|
<a href="https://pepy.tech/projects/clock-pattern" target="_blank">
|
|
44
45
|
<img src="https://static.pepy.tech/badge/clock-pattern/month" alt="Package Downloads">
|
|
45
46
|
</a>
|
|
46
|
-
<a href="https://deepwiki.com/adriamontoto/clock-pattern" target="_blank">
|
|
47
|
-
<img src="https://img.shields.io/badge/DeepWiki-adriamontoto%2Fclock--pattern-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==" alt="Project Documentation">
|
|
48
|
-
</a>
|
|
49
47
|
</p>
|
|
50
48
|
|
|
51
49
|
The **Clock Pattern** is a Python 🐍 package that turns time into an injectable dependency 🧩. Instead of scattering
|
|
@@ -103,10 +101,9 @@ The root README is the entry point. Deeper guides live in this repository and ar
|
|
|
103
101
|
- [`docs/README.md`](docs/README.md): Documentation hub.
|
|
104
102
|
- [`docs/usage/README.md`](docs/usage/README.md): Core usage patterns and service composition.
|
|
105
103
|
- [`docs/timezones/README.md`](docs/timezones/README.md): Timezone behavior, UTC defaults, and date-boundary guidance.
|
|
104
|
+
- [`docs/examples/README.md`](docs/examples/README.md): Token expiration, cache TTL, polling, and retry recipes.
|
|
106
105
|
- [`docs/testing/README.md`](docs/testing/README.md): `FixedClock`, `MockClock`, and deterministic test patterns.
|
|
107
106
|
|
|
108
|
-
This [project's DeepWiki documentation](https://deepwiki.com/adriamontoto/clock-pattern) is also available for generated
|
|
109
|
-
repository navigation.
|
|
110
107
|
|
|
111
108
|
<p align="right">
|
|
112
109
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -214,7 +211,7 @@ Use the top-level package for contracts and production helpers, and each feature
|
|
|
214
211
|
|
|
215
212
|
| API | Import path | Purpose |
|
|
216
213
|
| --- | --- | --- |
|
|
217
|
-
| [`FixedClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/fixed_clock.py) | `from clock_pattern.clocks.testing import FixedClock` | Test clock that
|
|
214
|
+
| [`FixedClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/fixed_clock.py) | `from clock_pattern.clocks.testing import FixedClock` | Test clock that stays fixed until explicitly set or advanced. |
|
|
218
215
|
| [`MockClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/mock_clock.py) | `from clock_pattern.clocks.testing import MockClock` | Test clock with prepared return values and call assertions. |
|
|
219
216
|
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
220
217
|
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
@@ -223,26 +220,43 @@ Use the top-level package for contracts and production helpers, and each feature
|
|
|
223
220
|
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
224
221
|
|
|
225
222
|
```python
|
|
226
|
-
from clock_pattern import Stopwatch,
|
|
223
|
+
from clock_pattern import Stopwatch, SystemMonotonicClock, SystemSleeper
|
|
227
224
|
|
|
228
225
|
monotonic_clock = SystemMonotonicClock()
|
|
229
226
|
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
230
|
-
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
231
227
|
|
|
232
228
|
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
233
229
|
pass
|
|
234
230
|
|
|
235
|
-
with sleeper.minimum_duration(seconds=
|
|
231
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
236
232
|
pass
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The deadline context requires a Unix main thread:
|
|
237
236
|
|
|
237
|
+
```python unix
|
|
238
|
+
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
239
|
+
|
|
240
|
+
monotonic_clock = SystemMonotonicClock()
|
|
238
241
|
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
239
242
|
pass
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Continue with polling and retries on all platforms:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from clock_pattern import SystemMonotonicClock, SystemPoller, SystemRetrier, SystemSleeper
|
|
249
|
+
|
|
250
|
+
monotonic_clock = SystemMonotonicClock()
|
|
251
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
252
|
+
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
240
253
|
|
|
241
254
|
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
242
255
|
SystemRetrier(sleeper=sleeper).retry(
|
|
243
256
|
operation=lambda: 'done',
|
|
244
257
|
attempts=3,
|
|
245
258
|
delay_seconds=0.2,
|
|
259
|
+
max_delay_seconds=2,
|
|
246
260
|
backoff=2,
|
|
247
261
|
jitter=True,
|
|
248
262
|
)
|
|
@@ -321,6 +335,8 @@ clock.assert_today_method_was_called_once()
|
|
|
321
335
|
clock.assert_now_method_was_not_called()
|
|
322
336
|
```
|
|
323
337
|
|
|
338
|
+
`FixedClock.advance(delta=timedelta(...))` moves time forward explicitly; `set(instant=...)` replaces it, including backward jumps. Both keep `now()` and `today()` consistent.
|
|
339
|
+
|
|
324
340
|
More testing recipes are available in [`docs/testing/README.md`](docs/testing/README.md).
|
|
325
341
|
|
|
326
342
|
<p align="right">
|
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
<a href="https://pepy.tech/projects/clock-pattern" target="_blank">
|
|
19
19
|
<img src="https://static.pepy.tech/badge/clock-pattern/month" alt="Package Downloads">
|
|
20
20
|
</a>
|
|
21
|
-
<a href="https://deepwiki.com/adriamontoto/clock-pattern" target="_blank">
|
|
22
|
-
<img src="https://img.shields.io/badge/DeepWiki-adriamontoto%2Fclock--pattern-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==" alt="Project Documentation">
|
|
23
|
-
</a>
|
|
24
21
|
</p>
|
|
25
22
|
|
|
26
23
|
The **Clock Pattern** is a Python 🐍 package that turns time into an injectable dependency 🧩. Instead of scattering
|
|
@@ -78,10 +75,9 @@ The root README is the entry point. Deeper guides live in this repository and ar
|
|
|
78
75
|
- [`docs/README.md`](docs/README.md): Documentation hub.
|
|
79
76
|
- [`docs/usage/README.md`](docs/usage/README.md): Core usage patterns and service composition.
|
|
80
77
|
- [`docs/timezones/README.md`](docs/timezones/README.md): Timezone behavior, UTC defaults, and date-boundary guidance.
|
|
78
|
+
- [`docs/examples/README.md`](docs/examples/README.md): Token expiration, cache TTL, polling, and retry recipes.
|
|
81
79
|
- [`docs/testing/README.md`](docs/testing/README.md): `FixedClock`, `MockClock`, and deterministic test patterns.
|
|
82
80
|
|
|
83
|
-
This [project's DeepWiki documentation](https://deepwiki.com/adriamontoto/clock-pattern) is also available for generated
|
|
84
|
-
repository navigation.
|
|
85
81
|
|
|
86
82
|
<p align="right">
|
|
87
83
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -189,7 +185,7 @@ Use the top-level package for contracts and production helpers, and each feature
|
|
|
189
185
|
|
|
190
186
|
| API | Import path | Purpose |
|
|
191
187
|
| --- | --- | --- |
|
|
192
|
-
| [`FixedClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/fixed_clock.py) | `from clock_pattern.clocks.testing import FixedClock` | Test clock that
|
|
188
|
+
| [`FixedClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/fixed_clock.py) | `from clock_pattern.clocks.testing import FixedClock` | Test clock that stays fixed until explicitly set or advanced. |
|
|
193
189
|
| [`MockClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/mock_clock.py) | `from clock_pattern.clocks.testing import MockClock` | Test clock with prepared return values and call assertions. |
|
|
194
190
|
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
195
191
|
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
@@ -198,26 +194,43 @@ Use the top-level package for contracts and production helpers, and each feature
|
|
|
198
194
|
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
199
195
|
|
|
200
196
|
```python
|
|
201
|
-
from clock_pattern import Stopwatch,
|
|
197
|
+
from clock_pattern import Stopwatch, SystemMonotonicClock, SystemSleeper
|
|
202
198
|
|
|
203
199
|
monotonic_clock = SystemMonotonicClock()
|
|
204
200
|
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
205
|
-
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
206
201
|
|
|
207
202
|
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
208
203
|
pass
|
|
209
204
|
|
|
210
|
-
with sleeper.minimum_duration(seconds=
|
|
205
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
211
206
|
pass
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The deadline context requires a Unix main thread:
|
|
212
210
|
|
|
211
|
+
```python unix
|
|
212
|
+
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
213
|
+
|
|
214
|
+
monotonic_clock = SystemMonotonicClock()
|
|
213
215
|
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
214
216
|
pass
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Continue with polling and retries on all platforms:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from clock_pattern import SystemMonotonicClock, SystemPoller, SystemRetrier, SystemSleeper
|
|
223
|
+
|
|
224
|
+
monotonic_clock = SystemMonotonicClock()
|
|
225
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
226
|
+
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
215
227
|
|
|
216
228
|
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
217
229
|
SystemRetrier(sleeper=sleeper).retry(
|
|
218
230
|
operation=lambda: 'done',
|
|
219
231
|
attempts=3,
|
|
220
232
|
delay_seconds=0.2,
|
|
233
|
+
max_delay_seconds=2,
|
|
221
234
|
backoff=2,
|
|
222
235
|
jitter=True,
|
|
223
236
|
)
|
|
@@ -296,6 +309,8 @@ clock.assert_today_method_was_called_once()
|
|
|
296
309
|
clock.assert_now_method_was_not_called()
|
|
297
310
|
```
|
|
298
311
|
|
|
312
|
+
`FixedClock.advance(delta=timedelta(...))` moves time forward explicitly; `set(instant=...)` replaces it, including backward jumps. Both keep `now()` and `today()` consistent.
|
|
313
|
+
|
|
299
314
|
More testing recipes are available in [`docs/testing/README.md`](docs/testing/README.md).
|
|
300
315
|
|
|
301
316
|
<p align="right">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Deterministic clock that
|
|
2
|
+
Deterministic clock that stays fixed until explicitly adjusted.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from sys import version_info
|
|
@@ -9,7 +9,7 @@ if version_info >= (3, 12):
|
|
|
9
9
|
else:
|
|
10
10
|
from typing_extensions import override # pragma: no cover
|
|
11
11
|
|
|
12
|
-
from datetime import UTC, date, datetime
|
|
12
|
+
from datetime import UTC, date, datetime, timedelta
|
|
13
13
|
|
|
14
14
|
from value_object_pattern.usables.dates import DatetimeValueObject
|
|
15
15
|
|
|
@@ -21,7 +21,8 @@ class FixedClock(Clock):
|
|
|
21
21
|
Return a fixed datetime and the date derived from it.
|
|
22
22
|
|
|
23
23
|
`FixedClock` is useful when a test only needs stable time. Naive datetimes are normalized to UTC during
|
|
24
|
-
initialization
|
|
24
|
+
initialization and `set()`. Use `advance()` to simulate elapsed time, or `set()` to choose another instant.
|
|
25
|
+
Use `MockClock` when a test also needs to assert whether `now()` or `today()` was called.
|
|
25
26
|
|
|
26
27
|
Example:
|
|
27
28
|
```python
|
|
@@ -45,7 +46,7 @@ class FixedClock(Clock):
|
|
|
45
46
|
If `instant` is naive, UTC is added as its timezone. A timezone-aware `instant` is preserved as provided.
|
|
46
47
|
|
|
47
48
|
Args:
|
|
48
|
-
instant: Datetime returned by `now()` and used by `today()`.
|
|
49
|
+
instant (datetime): Datetime returned by `now()` and used by `today()`.
|
|
49
50
|
|
|
50
51
|
Raises:
|
|
51
52
|
TypeError: If `instant` is not a datetime.
|
|
@@ -62,6 +63,30 @@ class FixedClock(Clock):
|
|
|
62
63
|
# >>> 1999-01-01 00:00:00+00:00
|
|
63
64
|
```
|
|
64
65
|
"""
|
|
66
|
+
self.set(instant=instant)
|
|
67
|
+
|
|
68
|
+
def set(self, *, instant: datetime) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Set the instant returned by both clock methods, allowing forward or backward jumps.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
instant (datetime): Datetime returned by `now()` and used by `today()`.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If `instant` is not a datetime.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
from datetime import UTC, datetime
|
|
81
|
+
|
|
82
|
+
from clock_pattern.clocks.testing import FixedClock
|
|
83
|
+
|
|
84
|
+
clock = FixedClock(instant=datetime(2025, 2, 1, tzinfo=UTC))
|
|
85
|
+
clock.set(instant=datetime(2025, 1, 1, tzinfo=UTC))
|
|
86
|
+
print(clock.now())
|
|
87
|
+
# >>> 2025-01-01 00:00:00+00:00
|
|
88
|
+
```
|
|
89
|
+
"""
|
|
65
90
|
DatetimeValueObject(value=instant, title='FixedClock', parameter='instant')
|
|
66
91
|
|
|
67
92
|
if instant.tzinfo is None:
|
|
@@ -69,6 +94,35 @@ class FixedClock(Clock):
|
|
|
69
94
|
|
|
70
95
|
self._instant = instant
|
|
71
96
|
|
|
97
|
+
def advance(self, *, delta: timedelta) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Advance by a non-negative elapsed duration, preserving the clock's timezone.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
delta (timedelta): Non-negative elapsed duration to add to the instant.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If `delta` is negative.
|
|
106
|
+
OverflowError: If the resulting instant is outside datetime's range.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
```python
|
|
110
|
+
from datetime import UTC, datetime, timedelta
|
|
111
|
+
|
|
112
|
+
from clock_pattern.clocks.testing import FixedClock
|
|
113
|
+
|
|
114
|
+
clock = FixedClock(instant=datetime(2025, 12, 31, 23, 59, tzinfo=UTC))
|
|
115
|
+
clock.advance(delta=timedelta(minutes=2))
|
|
116
|
+
print(clock.now())
|
|
117
|
+
# >>> 2026-01-01 00:01:00+00:00
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
if delta < timedelta(0):
|
|
121
|
+
raise ValueError(f'FixedClock delta <<<{delta}>>> must be greater than or equal to zero.')
|
|
122
|
+
|
|
123
|
+
if delta:
|
|
124
|
+
self._instant = (self._instant.astimezone(UTC) + delta).astimezone(self._instant.tzinfo)
|
|
125
|
+
|
|
72
126
|
@override
|
|
73
127
|
def now(self) -> datetime:
|
|
74
128
|
"""
|
|
@@ -11,7 +11,7 @@ class Deadline(AbstractContextManager['Deadline']):
|
|
|
11
11
|
Define the interface for injectable deadlines.
|
|
12
12
|
|
|
13
13
|
Example:
|
|
14
|
-
```python
|
|
14
|
+
```python unix
|
|
15
15
|
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
16
16
|
|
|
17
17
|
with SystemDeadline(seconds=1, monotonic_clock=SystemMonotonicClock()) as deadline:
|
|
@@ -101,6 +101,5 @@ class Deadline(AbstractContextManager['Deadline']):
|
|
|
101
101
|
monotonic_clock = MockMonotonicClock()
|
|
102
102
|
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
103
103
|
monotonic_clock.advance(seconds=1)
|
|
104
|
-
deadline.raise_if_expired()
|
|
105
104
|
```
|
|
106
105
|
"""
|
|
@@ -30,7 +30,7 @@ class SystemDeadline(Deadline):
|
|
|
30
30
|
Expose monotonic deadline state and enforce Unix main-thread context timeouts.
|
|
31
31
|
|
|
32
32
|
Example:
|
|
33
|
-
```python
|
|
33
|
+
```python unix
|
|
34
34
|
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
35
35
|
|
|
36
36
|
with SystemDeadline(seconds=1, monotonic_clock=SystemMonotonicClock()) as deadline:
|
|
@@ -160,7 +160,6 @@ class SystemDeadline(Deadline):
|
|
|
160
160
|
monotonic_clock = MockMonotonicClock()
|
|
161
161
|
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
162
162
|
monotonic_clock.advance(seconds=1)
|
|
163
|
-
deadline.raise_if_expired()
|
|
164
163
|
```
|
|
165
164
|
"""
|
|
166
165
|
elapsed_seconds = self.elapsed_seconds
|
|
@@ -183,7 +182,7 @@ class SystemDeadline(Deadline):
|
|
|
183
182
|
Self: This deadline instance.
|
|
184
183
|
|
|
185
184
|
Example:
|
|
186
|
-
```python
|
|
185
|
+
```python unix
|
|
187
186
|
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
188
187
|
|
|
189
188
|
with SystemDeadline(seconds=1, monotonic_clock=SystemMonotonicClock()) as deadline:
|
|
@@ -252,7 +251,7 @@ class SystemDeadline(Deadline):
|
|
|
252
251
|
bool | None: `None`, so exceptions from the managed block are not suppressed.
|
|
253
252
|
|
|
254
253
|
Example:
|
|
255
|
-
```python
|
|
254
|
+
```python unix
|
|
256
255
|
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
257
256
|
|
|
258
257
|
with SystemDeadline(seconds=1, monotonic_clock=SystemMonotonicClock()) as deadline:
|
|
@@ -41,6 +41,9 @@ class Poller(ABC):
|
|
|
41
41
|
"""
|
|
42
42
|
Poll a condition until it succeeds or the timeout expires.
|
|
43
43
|
|
|
44
|
+
Timeout checks are cooperative: conditions are not interrupted. Success at the boundary is accepted;
|
|
45
|
+
success after a positive timeout is rejected. A zero timeout evaluates the condition once.
|
|
46
|
+
|
|
44
47
|
Args:
|
|
45
48
|
condition (Callable[[], bool]): Condition checked until it returns `True`.
|
|
46
49
|
timeout_seconds (float): Maximum duration to wait.
|
|
@@ -41,6 +41,10 @@ class PollerAsync(ABC):
|
|
|
41
41
|
"""
|
|
42
42
|
Poll a condition until it succeeds or the timeout expires.
|
|
43
43
|
|
|
44
|
+
The timeout covers condition evaluation and sleeping. Overdue awaits are cancelled; synchronous blocking code
|
|
45
|
+
cannot be interrupted. Conditions must propagate cancellation. Injected-clock checks reject success after a
|
|
46
|
+
positive timeout. A zero timeout permits one evaluation only if it completes without suspending.
|
|
47
|
+
|
|
44
48
|
Args:
|
|
45
49
|
condition (Callable[[], bool | Awaitable[bool]]): Sync or async condition checked until it succeeds.
|
|
46
50
|
timeout_seconds (float): Maximum duration to wait.
|
|
@@ -69,6 +69,9 @@ class SystemPoller(Poller):
|
|
|
69
69
|
"""
|
|
70
70
|
Poll `condition` until it returns `True`.
|
|
71
71
|
|
|
72
|
+
Conditions run cooperatively and are not interrupted. A successful result at the timeout boundary is
|
|
73
|
+
accepted, but a result after a positive timeout raises. A zero timeout evaluates the condition once.
|
|
74
|
+
|
|
72
75
|
Args:
|
|
73
76
|
condition (Callable[[], bool]): Condition checked until it returns `True`.
|
|
74
77
|
timeout_seconds (float): Maximum duration to wait.
|
|
@@ -101,6 +104,9 @@ class SystemPoller(Poller):
|
|
|
101
104
|
deadline = SystemDeadline(seconds=timeout_seconds, monotonic_clock=self._monotonic_clock)
|
|
102
105
|
while True:
|
|
103
106
|
condition_result = BooleanValueObject(value=condition(), title='SystemPoller', parameter='condition').value
|
|
107
|
+
if timeout_seconds > 0 and deadline.elapsed_seconds > timeout_seconds:
|
|
108
|
+
deadline.raise_if_expired()
|
|
109
|
+
|
|
104
110
|
if condition_result:
|
|
105
111
|
return
|
|
106
112
|
|
|
@@ -9,12 +9,13 @@ if version_info >= (3, 12):
|
|
|
9
9
|
else:
|
|
10
10
|
from typing_extensions import override # pragma: no cover
|
|
11
11
|
|
|
12
|
+
from asyncio import timeout
|
|
12
13
|
from collections.abc import Awaitable, Callable
|
|
13
14
|
from inspect import isawaitable
|
|
14
15
|
|
|
15
16
|
from value_object_pattern.usables import BooleanValueObject, PositiveNumberValueObject, PositiveOrZeroNumberValueObject
|
|
16
17
|
|
|
17
|
-
from clock_pattern.deadlines import SystemDeadline
|
|
18
|
+
from clock_pattern.deadlines import SystemDeadline, TimeoutExpiredError
|
|
18
19
|
from clock_pattern.monotonic_clocks.models import MonotonicClock
|
|
19
20
|
from clock_pattern.pollers.models import PollerAsync
|
|
20
21
|
from clock_pattern.sleepers.models import SleeperAsync
|
|
@@ -70,6 +71,11 @@ class SystemPollerAsync(PollerAsync):
|
|
|
70
71
|
"""
|
|
71
72
|
Poll `condition` until it returns or awaits to `True`.
|
|
72
73
|
|
|
74
|
+
The timeout covers condition evaluation and sleeping, cancelling overdue awaits through asyncio. Blocking
|
|
75
|
+
synchronous code and coroutines that suppress cancellation cannot be forcibly interrupted. Injected-clock checks
|
|
76
|
+
also reject results after a positive timeout. A zero timeout allows one immediate evaluation, but cancels it if
|
|
77
|
+
it suspends. External cancellation and condition exceptions propagate.
|
|
78
|
+
|
|
73
79
|
Args:
|
|
74
80
|
condition (Callable[[], bool | Awaitable[bool]]): Sync or async condition checked until it is true.
|
|
75
81
|
timeout_seconds (float): Maximum duration to wait.
|
|
@@ -100,6 +106,44 @@ class SystemPollerAsync(PollerAsync):
|
|
|
100
106
|
PositiveNumberValueObject(value=interval_seconds, title='SystemPollerAsync', parameter='interval_seconds')
|
|
101
107
|
|
|
102
108
|
deadline = SystemDeadline(seconds=timeout_seconds, monotonic_clock=self._monotonic_clock)
|
|
109
|
+
|
|
110
|
+
timeout_context = timeout(timeout_seconds)
|
|
111
|
+
try:
|
|
112
|
+
async with timeout_context:
|
|
113
|
+
await self._poll_until(
|
|
114
|
+
condition=condition,
|
|
115
|
+
deadline=deadline,
|
|
116
|
+
timeout_seconds=timeout_seconds,
|
|
117
|
+
interval_seconds=interval_seconds,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except TimeoutError as error:
|
|
121
|
+
if timeout_context.expired():
|
|
122
|
+
raise TimeoutExpiredError(elapsed_seconds=deadline.elapsed_seconds) from error
|
|
123
|
+
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
async def _poll_until(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
condition: Callable[[], bool | Awaitable[bool]],
|
|
130
|
+
deadline: SystemDeadline,
|
|
131
|
+
timeout_seconds: float,
|
|
132
|
+
interval_seconds: float,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Poll using the injected clock while the enclosing asyncio timeout bounds awaits.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
condition (Callable[[], bool | Awaitable[bool]]): Sync or async condition checked until it is true.
|
|
139
|
+
deadline (SystemDeadline): Deadline used to check for timeout expiration.
|
|
140
|
+
timeout_seconds (float): Maximum duration to wait.
|
|
141
|
+
interval_seconds (float): Duration between condition checks.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
TypeError: If `condition` does not return a boolean.
|
|
145
|
+
TimeoutExpiredError: If the timeout expires before `condition` returns `True`.
|
|
146
|
+
"""
|
|
103
147
|
while True:
|
|
104
148
|
condition_result = condition()
|
|
105
149
|
|
|
@@ -107,6 +151,9 @@ class SystemPollerAsync(PollerAsync):
|
|
|
107
151
|
condition_result = await condition_result
|
|
108
152
|
|
|
109
153
|
condition_result = BooleanValueObject(value=condition_result, title='SystemPollerAsync', parameter='condition').value # noqa: E501 # fmt: skip
|
|
154
|
+
if timeout_seconds > 0 and deadline.elapsed_seconds > timeout_seconds:
|
|
155
|
+
deadline.raise_if_expired()
|
|
156
|
+
|
|
110
157
|
if condition_result:
|
|
111
158
|
return
|
|
112
159
|
|
|
@@ -42,6 +42,7 @@ class Retrier(ABC):
|
|
|
42
42
|
operation: Callable[[], T],
|
|
43
43
|
attempts: int,
|
|
44
44
|
delay_seconds: float = 0.0,
|
|
45
|
+
max_delay_seconds: float | None = None,
|
|
45
46
|
backoff: float = 1.0,
|
|
46
47
|
jitter: bool = False,
|
|
47
48
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -54,6 +55,8 @@ class Retrier(ABC):
|
|
|
54
55
|
attempts (int): Maximum number of attempts, including the first call.
|
|
55
56
|
delay_seconds (float, optional): Finite, non-negative initial delay between failed attempts. Defaults
|
|
56
57
|
to 0.0 seconds.
|
|
58
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
59
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
57
60
|
backoff (float, optional): Finite, positive multiplier applied to the delay after each failed attempt.
|
|
58
61
|
Defaults to 1.0 (no backoff).
|
|
59
62
|
jitter (bool, optional): Whether to randomize each delay. Defaults to `False`.
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/models/retrier_async.py
RENAMED
|
@@ -45,6 +45,7 @@ class RetrierAsync(ABC):
|
|
|
45
45
|
operation: Callable[[], Awaitable[T]],
|
|
46
46
|
attempts: int,
|
|
47
47
|
delay_seconds: float = 0.0,
|
|
48
|
+
max_delay_seconds: float | None = None,
|
|
48
49
|
backoff: float = 1.0,
|
|
49
50
|
jitter: bool = False,
|
|
50
51
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -57,6 +58,8 @@ class RetrierAsync(ABC):
|
|
|
57
58
|
attempts (int): Maximum number of attempts, including the first call.
|
|
58
59
|
delay_seconds (float, optional): Finite, non-negative initial delay between failed attempts. Defaults
|
|
59
60
|
to 0.0 seconds.
|
|
61
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
62
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
60
63
|
backoff (float, optional): Finite, positive multiplier applied to the delay after each failed attempt.
|
|
61
64
|
Defaults to 1.0 (no backoff).
|
|
62
65
|
jitter (bool, optional): Whether to randomize each delay. Defaults to `False`.
|
|
@@ -73,6 +73,7 @@ class SystemRetrier(Retrier):
|
|
|
73
73
|
operation: Callable[[], T],
|
|
74
74
|
attempts: int,
|
|
75
75
|
delay_seconds: float = 0.0,
|
|
76
|
+
max_delay_seconds: float | None = None,
|
|
76
77
|
backoff: float = 1.0,
|
|
77
78
|
jitter: bool = False,
|
|
78
79
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -85,6 +86,8 @@ class SystemRetrier(Retrier):
|
|
|
85
86
|
attempts (int): Maximum number of attempts, including the first call.
|
|
86
87
|
delay_seconds (float, optional): Finite, non-negative initial delay between failed attempts. Defaults
|
|
87
88
|
to 0.0 seconds.
|
|
89
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
90
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
88
91
|
backoff (float, optional): Finite, positive multiplier applied to the delay after each failed attempt.
|
|
89
92
|
Defaults to 1.0 (no backoff).
|
|
90
93
|
jitter (bool, optional): Whether to randomize each delay. Defaults to `False`.
|
|
@@ -96,6 +99,8 @@ class SystemRetrier(Retrier):
|
|
|
96
99
|
ValueError: If the `attempts` is not a positive integer.
|
|
97
100
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
98
101
|
ValueError: If the `delay_seconds` is negative.
|
|
102
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
103
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
99
104
|
TypeError: If the `backoff` is not an integer or float.
|
|
100
105
|
ValueError: If the `backoff` is not positive.
|
|
101
106
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -118,10 +123,14 @@ class SystemRetrier(Retrier):
|
|
|
118
123
|
"""
|
|
119
124
|
PositiveIntegerValueObject(value=attempts, title='SystemRetrier', parameter='attempts')
|
|
120
125
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='SystemRetrier', parameter='delay_seconds')
|
|
126
|
+
if max_delay_seconds is not None:
|
|
127
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='SystemRetrier', parameter='max_delay_seconds') # noqa: E501 # fmt: skip
|
|
128
|
+
|
|
121
129
|
PositiveNumberValueObject(value=backoff, title='SystemRetrier', parameter='backoff')
|
|
122
130
|
BooleanValueObject(value=jitter, title='SystemRetrier', parameter='jitter')
|
|
123
131
|
|
|
124
|
-
|
|
132
|
+
delay_cap = float('inf') if max_delay_seconds is None else max_delay_seconds
|
|
133
|
+
current_delay_seconds = min(delay_seconds, delay_cap)
|
|
125
134
|
for attempt_number in range(1, attempts + 1):
|
|
126
135
|
try:
|
|
127
136
|
return operation()
|
|
@@ -134,6 +143,6 @@ class SystemRetrier(Retrier):
|
|
|
134
143
|
if sleep_seconds > 0:
|
|
135
144
|
self._sleeper.sleep(seconds=sleep_seconds)
|
|
136
145
|
|
|
137
|
-
current_delay_seconds
|
|
146
|
+
current_delay_seconds = min(current_delay_seconds * backoff, delay_cap)
|
|
138
147
|
|
|
139
148
|
raise RuntimeError('SystemRetrier attempts loop ended unexpectedly.') # pragma: no cover
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/system_retrier_async.py
RENAMED
|
@@ -83,6 +83,7 @@ class SystemRetrierAsync(RetrierAsync):
|
|
|
83
83
|
operation: Callable[[], Awaitable[T]],
|
|
84
84
|
attempts: int,
|
|
85
85
|
delay_seconds: float = 0.0,
|
|
86
|
+
max_delay_seconds: float | None = None,
|
|
86
87
|
backoff: float = 1.0,
|
|
87
88
|
jitter: bool = False,
|
|
88
89
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -95,6 +96,8 @@ class SystemRetrierAsync(RetrierAsync):
|
|
|
95
96
|
attempts (int): Maximum number of attempts, including the first call.
|
|
96
97
|
delay_seconds (float, optional): Finite, non-negative initial delay between failed attempts. Defaults
|
|
97
98
|
to 0.0 seconds.
|
|
99
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
100
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
98
101
|
backoff (float, optional): Finite, positive multiplier applied to the delay after each failed attempt.
|
|
99
102
|
Defaults to 1.0 (no backoff).
|
|
100
103
|
jitter (bool, optional): Whether to randomize each delay. Defaults to `False`.
|
|
@@ -106,6 +109,8 @@ class SystemRetrierAsync(RetrierAsync):
|
|
|
106
109
|
ValueError: If the `attempts` is not a positive integer.
|
|
107
110
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
108
111
|
ValueError: If the `delay_seconds` is negative.
|
|
112
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
113
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
109
114
|
TypeError: If the `backoff` is not an integer or float.
|
|
110
115
|
ValueError: If the `backoff` is not positive.
|
|
111
116
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -133,10 +138,14 @@ class SystemRetrierAsync(RetrierAsync):
|
|
|
133
138
|
"""
|
|
134
139
|
PositiveIntegerValueObject(value=attempts, title='AsyncSystemRetrier', parameter='attempts')
|
|
135
140
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='AsyncSystemRetrier', parameter='delay_seconds')
|
|
141
|
+
if max_delay_seconds is not None:
|
|
142
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='AsyncSystemRetrier', parameter='max_delay_seconds') # noqa: E501 # fmt: skip
|
|
143
|
+
|
|
136
144
|
PositiveNumberValueObject(value=backoff, title='AsyncSystemRetrier', parameter='backoff')
|
|
137
145
|
BooleanValueObject(value=jitter, title='AsyncSystemRetrier', parameter='jitter')
|
|
138
146
|
|
|
139
|
-
|
|
147
|
+
delay_cap = float('inf') if max_delay_seconds is None else max_delay_seconds
|
|
148
|
+
current_delay_seconds = min(delay_seconds, delay_cap)
|
|
140
149
|
for attempt_number in range(1, attempts + 1):
|
|
141
150
|
try:
|
|
142
151
|
return await operation()
|
|
@@ -149,6 +158,6 @@ class SystemRetrierAsync(RetrierAsync):
|
|
|
149
158
|
if sleep_seconds > 0:
|
|
150
159
|
await self._sleeper.sleep(seconds=sleep_seconds)
|
|
151
160
|
|
|
152
|
-
current_delay_seconds
|
|
161
|
+
current_delay_seconds = min(current_delay_seconds * backoff, delay_cap)
|
|
153
162
|
|
|
154
163
|
raise RuntimeError('SystemRetrierAsync attempts loop ended unexpectedly.') # pragma: no cover
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/testing/mock_retrier.py
RENAMED
|
@@ -76,6 +76,7 @@ class MockRetrier(Retrier):
|
|
|
76
76
|
operation: Callable[[], T],
|
|
77
77
|
attempts: int,
|
|
78
78
|
delay_seconds: float = 0.0,
|
|
79
|
+
max_delay_seconds: float | None = None,
|
|
79
80
|
backoff: float = 1.0,
|
|
80
81
|
jitter: bool = False,
|
|
81
82
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -88,6 +89,8 @@ class MockRetrier(Retrier):
|
|
|
88
89
|
attempts (int): Maximum number of attempts, including the first call.
|
|
89
90
|
delay_seconds (float, optional): Finite, non-negative initial delay between failed attempts. Defaults
|
|
90
91
|
to 0.0 seconds.
|
|
92
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
93
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
91
94
|
backoff (float, optional): Finite, positive multiplier applied to the delay after each failed attempt.
|
|
92
95
|
Defaults to 1.0 (no backoff).
|
|
93
96
|
jitter (bool, optional): Whether to randomize each delay. Defaults to `False`.
|
|
@@ -99,6 +102,8 @@ class MockRetrier(Retrier):
|
|
|
99
102
|
ValueError: If the `attempts` is not a positive integer.
|
|
100
103
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
101
104
|
ValueError: If the `delay_seconds` is negative.
|
|
105
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
106
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
102
107
|
TypeError: If the `backoff` is not an integer or float.
|
|
103
108
|
ValueError: If the `backoff` is not positive.
|
|
104
109
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -122,6 +127,9 @@ class MockRetrier(Retrier):
|
|
|
122
127
|
"""
|
|
123
128
|
PositiveIntegerValueObject(value=attempts, title='MockRetrier', parameter='attempts')
|
|
124
129
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='MockRetrier', parameter='delay_seconds')
|
|
130
|
+
if max_delay_seconds is not None:
|
|
131
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='MockRetrier', parameter='max_delay_seconds')
|
|
132
|
+
|
|
125
133
|
PositiveNumberValueObject(value=backoff, title='MockRetrier', parameter='backoff')
|
|
126
134
|
BooleanValueObject(value=jitter, title='MockRetrier', parameter='jitter')
|
|
127
135
|
|
|
@@ -129,6 +137,7 @@ class MockRetrier(Retrier):
|
|
|
129
137
|
operation=operation,
|
|
130
138
|
attempts=attempts,
|
|
131
139
|
delay_seconds=delay_seconds,
|
|
140
|
+
max_delay_seconds=max_delay_seconds,
|
|
132
141
|
backoff=backoff,
|
|
133
142
|
jitter=jitter,
|
|
134
143
|
retry_on=retry_on,
|
|
@@ -195,6 +204,7 @@ class MockRetrier(Retrier):
|
|
|
195
204
|
operation: Callable[[], Any],
|
|
196
205
|
attempts: int,
|
|
197
206
|
delay_seconds: float = 0.0,
|
|
207
|
+
max_delay_seconds: float | None = None,
|
|
198
208
|
backoff: float = 1.0,
|
|
199
209
|
jitter: bool = False,
|
|
200
210
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -206,6 +216,8 @@ class MockRetrier(Retrier):
|
|
|
206
216
|
operation (Callable[[], Any]): Expected operation callable.
|
|
207
217
|
attempts (int): Expected maximum number of attempts.
|
|
208
218
|
delay_seconds (float, optional): Expected finite, non-negative initial delay. Defaults to 0.0 seconds.
|
|
219
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
220
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
209
221
|
backoff (float, optional): Expected finite, positive delay multiplier. Defaults to 1.0.
|
|
210
222
|
jitter (bool, optional): Expected jitter flag. Defaults to False.
|
|
211
223
|
retry_on (type[Exception] | tuple[type[Exception], ...], optional): Expected retryable exception types.
|
|
@@ -216,6 +228,8 @@ class MockRetrier(Retrier):
|
|
|
216
228
|
ValueError: If the `attempts` is not a positive integer.
|
|
217
229
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
218
230
|
ValueError: If the `delay_seconds` is negative.
|
|
231
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
232
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
219
233
|
TypeError: If the `backoff` is not an integer or float.
|
|
220
234
|
ValueError: If the `backoff` is not positive.
|
|
221
235
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -233,6 +247,9 @@ class MockRetrier(Retrier):
|
|
|
233
247
|
"""
|
|
234
248
|
PositiveIntegerValueObject(value=attempts, title='MockRetrier', parameter='attempts')
|
|
235
249
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='MockRetrier', parameter='delay_seconds')
|
|
250
|
+
if max_delay_seconds is not None:
|
|
251
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='MockRetrier', parameter='max_delay_seconds')
|
|
252
|
+
|
|
236
253
|
PositiveNumberValueObject(value=backoff, title='MockRetrier', parameter='backoff')
|
|
237
254
|
BooleanValueObject(value=jitter, title='MockRetrier', parameter='jitter')
|
|
238
255
|
|
|
@@ -240,6 +257,7 @@ class MockRetrier(Retrier):
|
|
|
240
257
|
operation=operation,
|
|
241
258
|
attempts=attempts,
|
|
242
259
|
delay_seconds=delay_seconds,
|
|
260
|
+
max_delay_seconds=max_delay_seconds,
|
|
243
261
|
backoff=backoff,
|
|
244
262
|
jitter=jitter,
|
|
245
263
|
retry_on=retry_on,
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/retriers/testing/mock_retrier_async.py
RENAMED
|
@@ -76,6 +76,7 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
76
76
|
operation: Callable[[], Awaitable[T]],
|
|
77
77
|
attempts: int,
|
|
78
78
|
delay_seconds: float = 0.0,
|
|
79
|
+
max_delay_seconds: float | None = None,
|
|
79
80
|
backoff: float = 1.0,
|
|
80
81
|
jitter: bool = False,
|
|
81
82
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -88,6 +89,8 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
88
89
|
attempts (int): Maximum number of attempts requested by the code under test.
|
|
89
90
|
delay_seconds (float, optional): Finite, non-negative initial delay requested by the code under test.
|
|
90
91
|
Defaults to 0.0 seconds.
|
|
92
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
93
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
91
94
|
backoff (float, optional): Finite, positive delay multiplier requested by the code under test.
|
|
92
95
|
Defaults to 1.0.
|
|
93
96
|
jitter (bool, optional): Whether jitter was requested. Defaults to False.
|
|
@@ -99,6 +102,8 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
99
102
|
ValueError: If the `attempts` is not a positive integer.
|
|
100
103
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
101
104
|
ValueError: If the `delay_seconds` is negative.
|
|
105
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
106
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
102
107
|
TypeError: If the `backoff` is not an integer or float.
|
|
103
108
|
ValueError: If the `backoff` is not positive.
|
|
104
109
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -126,6 +131,9 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
126
131
|
"""
|
|
127
132
|
PositiveIntegerValueObject(value=attempts, title='MockRetrierAsync', parameter='attempts')
|
|
128
133
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='MockRetrierAsync', parameter='delay_seconds')
|
|
134
|
+
if max_delay_seconds is not None:
|
|
135
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='MockRetrierAsync', parameter='max_delay_seconds') # noqa: E501 # fmt: skip
|
|
136
|
+
|
|
129
137
|
PositiveNumberValueObject(value=backoff, title='MockRetrierAsync', parameter='backoff')
|
|
130
138
|
BooleanValueObject(value=jitter, title='MockRetrierAsync', parameter='jitter')
|
|
131
139
|
|
|
@@ -133,6 +141,7 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
133
141
|
operation=operation,
|
|
134
142
|
attempts=attempts,
|
|
135
143
|
delay_seconds=delay_seconds,
|
|
144
|
+
max_delay_seconds=max_delay_seconds,
|
|
136
145
|
backoff=backoff,
|
|
137
146
|
jitter=jitter,
|
|
138
147
|
retry_on=retry_on,
|
|
@@ -204,6 +213,7 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
204
213
|
operation: Callable[[], Awaitable[Any]],
|
|
205
214
|
attempts: int,
|
|
206
215
|
delay_seconds: float = 0.0,
|
|
216
|
+
max_delay_seconds: float | None = None,
|
|
207
217
|
backoff: float = 1.0,
|
|
208
218
|
jitter: bool = False,
|
|
209
219
|
retry_on: type[Exception] | tuple[type[Exception], ...] = Exception,
|
|
@@ -215,6 +225,8 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
215
225
|
operation (Callable[[], Awaitable[Any]]): Expected async operation callable.
|
|
216
226
|
attempts (int): Expected maximum number of attempts.
|
|
217
227
|
delay_seconds (float, optional): Expected finite, non-negative initial delay. Defaults to 0.0 seconds.
|
|
228
|
+
max_delay_seconds (float | None, optional): Finite, non-negative delay cap applied before jitter. Defaults
|
|
229
|
+
to `None` (uncapped). Zero disables sleeping. Backoff grows from the capped delay.
|
|
218
230
|
backoff (float, optional): Expected finite, positive delay multiplier. Defaults to 1.0.
|
|
219
231
|
jitter (bool, optional): Expected jitter flag. Defaults to False.
|
|
220
232
|
retry_on (type[Exception] | tuple[type[Exception], ...], optional): Expected retryable exception types.
|
|
@@ -225,6 +237,8 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
225
237
|
ValueError: If the `attempts` is not a positive integer.
|
|
226
238
|
TypeError: If the `delay_seconds` is not an integer or float.
|
|
227
239
|
ValueError: If the `delay_seconds` is negative.
|
|
240
|
+
ValueError: If `max_delay_seconds` is negative or non-finite.
|
|
241
|
+
TypeError: If `max_delay_seconds` is neither a number nor `None`.
|
|
228
242
|
TypeError: If the `backoff` is not an integer or float.
|
|
229
243
|
ValueError: If the `backoff` is not positive.
|
|
230
244
|
TypeError: If the `jitter` is not a boolean.
|
|
@@ -246,6 +260,9 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
246
260
|
"""
|
|
247
261
|
PositiveIntegerValueObject(value=attempts, title='MockRetrierAsync', parameter='attempts')
|
|
248
262
|
PositiveOrZeroNumberValueObject(value=delay_seconds, title='MockRetrierAsync', parameter='delay_seconds')
|
|
263
|
+
if max_delay_seconds is not None:
|
|
264
|
+
PositiveOrZeroNumberValueObject(value=max_delay_seconds, title='MockRetrierAsync', parameter='max_delay_seconds') # noqa: E501 # fmt: skip
|
|
265
|
+
|
|
249
266
|
PositiveNumberValueObject(value=backoff, title='MockRetrierAsync', parameter='backoff')
|
|
250
267
|
BooleanValueObject(value=jitter, title='MockRetrierAsync', parameter='jitter')
|
|
251
268
|
|
|
@@ -253,6 +270,7 @@ class MockRetrierAsync(RetrierAsync):
|
|
|
253
270
|
operation=operation,
|
|
254
271
|
attempts=attempts,
|
|
255
272
|
delay_seconds=delay_seconds,
|
|
273
|
+
max_delay_seconds=max_delay_seconds,
|
|
256
274
|
backoff=backoff,
|
|
257
275
|
jitter=jitter,
|
|
258
276
|
retry_on=retry_on,
|
|
@@ -16,7 +16,7 @@ class Sleeper(ABC):
|
|
|
16
16
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
17
17
|
|
|
18
18
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
19
|
-
sleeper.sleep(seconds=
|
|
19
|
+
sleeper.sleep(seconds=0.001)
|
|
20
20
|
```
|
|
21
21
|
"""
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ class Sleeper(ABC):
|
|
|
34
34
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
35
35
|
|
|
36
36
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
37
|
-
sleeper.sleep(seconds=
|
|
37
|
+
sleeper.sleep(seconds=0.001)
|
|
38
38
|
```
|
|
39
39
|
"""
|
|
40
40
|
|
|
@@ -56,7 +56,7 @@ class Sleeper(ABC):
|
|
|
56
56
|
|
|
57
57
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
58
58
|
|
|
59
|
-
with sleeper.minimum_duration(seconds=
|
|
59
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
60
60
|
pass
|
|
61
61
|
```
|
|
62
62
|
"""
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper_async.py
RENAMED
|
@@ -16,7 +16,7 @@ class SleeperAsync(ABC):
|
|
|
16
16
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
17
17
|
|
|
18
18
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
19
|
-
await sleeper.sleep(seconds=
|
|
19
|
+
await sleeper.sleep(seconds=0.001)
|
|
20
20
|
```
|
|
21
21
|
"""
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ class SleeperAsync(ABC):
|
|
|
34
34
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
35
35
|
|
|
36
36
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
37
|
-
await sleeper.sleep(seconds=0.
|
|
37
|
+
await sleeper.sleep(seconds=0.001)
|
|
38
38
|
```
|
|
39
39
|
"""
|
|
40
40
|
|
|
@@ -43,6 +43,8 @@ class SleeperAsync(ABC):
|
|
|
43
43
|
"""
|
|
44
44
|
Create an async context manager that ensures the enclosed work takes at least `seconds`.
|
|
45
45
|
|
|
46
|
+
Cancellation propagates without padding the duration. Other body errors still pad the duration.
|
|
47
|
+
|
|
46
48
|
Args:
|
|
47
49
|
seconds (float): The minimum duration the enclosed work should take.
|
|
48
50
|
|
|
@@ -56,7 +58,7 @@ class SleeperAsync(ABC):
|
|
|
56
58
|
|
|
57
59
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
58
60
|
|
|
59
|
-
async with sleeper.minimum_duration(seconds=
|
|
61
|
+
async with sleeper.minimum_duration(seconds=0.001):
|
|
60
62
|
pass
|
|
61
63
|
```
|
|
62
64
|
"""
|
|
@@ -29,7 +29,7 @@ class SystemSleeper(Sleeper):
|
|
|
29
29
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
30
30
|
|
|
31
31
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
32
|
-
sleeper.sleep(seconds=
|
|
32
|
+
sleeper.sleep(seconds=0.001)
|
|
33
33
|
```
|
|
34
34
|
"""
|
|
35
35
|
|
|
@@ -48,7 +48,7 @@ class SystemSleeper(Sleeper):
|
|
|
48
48
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
49
49
|
|
|
50
50
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
51
|
-
sleeper.sleep(seconds=
|
|
51
|
+
sleeper.sleep(seconds=0.001)
|
|
52
52
|
```
|
|
53
53
|
"""
|
|
54
54
|
self._monotonic_clock = monotonic_clock
|
|
@@ -71,7 +71,7 @@ class SystemSleeper(Sleeper):
|
|
|
71
71
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
72
72
|
|
|
73
73
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
74
|
-
sleeper.sleep(seconds=
|
|
74
|
+
sleeper.sleep(seconds=0.001)
|
|
75
75
|
```
|
|
76
76
|
"""
|
|
77
77
|
PositiveOrZeroNumberValueObject(value=seconds, title='SystemSleeper', parameter='seconds')
|
|
@@ -102,7 +102,7 @@ class SystemSleeper(Sleeper):
|
|
|
102
102
|
|
|
103
103
|
sleeper = SystemSleeper(monotonic_clock=SystemMonotonicClock())
|
|
104
104
|
|
|
105
|
-
with sleeper.minimum_duration(seconds=
|
|
105
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
106
106
|
pass
|
|
107
107
|
```
|
|
108
108
|
"""
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper_async.py
RENAMED
|
@@ -9,7 +9,7 @@ if version_info >= (3, 12):
|
|
|
9
9
|
else:
|
|
10
10
|
from typing_extensions import override # pragma: no cover
|
|
11
11
|
|
|
12
|
-
from asyncio import sleep
|
|
12
|
+
from asyncio import CancelledError, sleep
|
|
13
13
|
from collections.abc import AsyncIterator
|
|
14
14
|
from contextlib import asynccontextmanager
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ class SystemSleeperAsync(SleeperAsync):
|
|
|
29
29
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
30
30
|
|
|
31
31
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
32
|
-
await sleeper.sleep(seconds=
|
|
32
|
+
await sleeper.sleep(seconds=0.001)
|
|
33
33
|
```
|
|
34
34
|
"""
|
|
35
35
|
|
|
@@ -48,7 +48,7 @@ class SystemSleeperAsync(SleeperAsync):
|
|
|
48
48
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
49
49
|
|
|
50
50
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
51
|
-
await sleeper.sleep(seconds=
|
|
51
|
+
await sleeper.sleep(seconds=0.001)
|
|
52
52
|
```
|
|
53
53
|
"""
|
|
54
54
|
self._monotonic_clock = monotonic_clock
|
|
@@ -71,7 +71,7 @@ class SystemSleeperAsync(SleeperAsync):
|
|
|
71
71
|
from clock_pattern.monotonic_clocks import SystemMonotonicClock
|
|
72
72
|
|
|
73
73
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
74
|
-
await sleeper.sleep(seconds=
|
|
74
|
+
await sleeper.sleep(seconds=0.001)
|
|
75
75
|
```
|
|
76
76
|
"""
|
|
77
77
|
PositiveOrZeroNumberValueObject(value=seconds, title='SystemSleeperAsync', parameter='seconds')
|
|
@@ -84,6 +84,9 @@ class SystemSleeperAsync(SleeperAsync):
|
|
|
84
84
|
"""
|
|
85
85
|
Create an async context manager that ensures the enclosed work takes at least `seconds`.
|
|
86
86
|
|
|
87
|
+
Cancellation propagates immediately without padding the duration. Other body errors still wait out
|
|
88
|
+
the remaining duration before propagating.
|
|
89
|
+
|
|
87
90
|
Args:
|
|
88
91
|
seconds (float): The minimum elapsed duration for the enclosed asynchronous work.
|
|
89
92
|
|
|
@@ -102,19 +105,23 @@ class SystemSleeperAsync(SleeperAsync):
|
|
|
102
105
|
|
|
103
106
|
sleeper = SystemSleeperAsync(monotonic_clock=SystemMonotonicClock())
|
|
104
107
|
|
|
105
|
-
async with sleeper.minimum_duration(seconds=
|
|
108
|
+
async with sleeper.minimum_duration(seconds=0.001):
|
|
106
109
|
pass
|
|
107
110
|
```
|
|
108
111
|
"""
|
|
109
112
|
PositiveOrZeroNumberValueObject(value=seconds, title='SystemSleeperAsync', parameter='seconds')
|
|
110
113
|
|
|
111
114
|
started_time = self._monotonic_clock.current_seconds()
|
|
115
|
+
cancelled = False
|
|
112
116
|
try:
|
|
113
117
|
yield
|
|
114
118
|
|
|
119
|
+
except CancelledError:
|
|
120
|
+
cancelled = True
|
|
121
|
+
raise
|
|
122
|
+
|
|
115
123
|
finally:
|
|
116
|
-
|
|
117
|
-
remaining_seconds = seconds - elapsed_seconds
|
|
124
|
+
remaining_seconds = 0.0 if cancelled else seconds - (self._monotonic_clock.current_seconds() - started_time)
|
|
118
125
|
|
|
119
126
|
if remaining_seconds > 0:
|
|
120
127
|
await self.sleep(seconds=remaining_seconds)
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/testing/mock_sleeper_async.py
RENAMED
|
@@ -9,6 +9,7 @@ if version_info >= (3, 12):
|
|
|
9
9
|
else:
|
|
10
10
|
from typing_extensions import override # pragma: no cover
|
|
11
11
|
|
|
12
|
+
from asyncio import CancelledError
|
|
12
13
|
from collections.abc import AsyncIterator
|
|
13
14
|
from contextlib import asynccontextmanager
|
|
14
15
|
from unittest.mock import AsyncMock
|
|
@@ -138,6 +139,8 @@ class MockSleeperAsync(SleeperAsync):
|
|
|
138
139
|
"""
|
|
139
140
|
Create an async context manager that ensures the enclosed work takes at least `seconds`.
|
|
140
141
|
|
|
142
|
+
Cancellation propagates without recording sleep or advancing time. Other body errors still pad the duration.
|
|
143
|
+
|
|
141
144
|
Args:
|
|
142
145
|
seconds (float): The minimum elapsed duration for the enclosed asynchronous work.
|
|
143
146
|
|
|
@@ -162,12 +165,16 @@ class MockSleeperAsync(SleeperAsync):
|
|
|
162
165
|
PositiveOrZeroNumberValueObject(value=seconds, title='MockSleeperAsync', parameter='seconds')
|
|
163
166
|
|
|
164
167
|
started_time = self._monotonic_clock.current_seconds()
|
|
168
|
+
cancelled = False
|
|
165
169
|
try:
|
|
166
170
|
yield
|
|
167
171
|
|
|
172
|
+
except CancelledError:
|
|
173
|
+
cancelled = True
|
|
174
|
+
raise
|
|
175
|
+
|
|
168
176
|
finally:
|
|
169
|
-
|
|
170
|
-
remaining_seconds = seconds - elapsed_seconds
|
|
177
|
+
remaining_seconds = 0.0 if cancelled else seconds - (self._monotonic_clock.current_seconds() - started_time)
|
|
171
178
|
|
|
172
179
|
if remaining_seconds > 0:
|
|
173
180
|
await self.sleep(seconds=remaining_seconds)
|
|
@@ -38,7 +38,10 @@ keywords = [
|
|
|
38
38
|
'domain-driven-design',
|
|
39
39
|
]
|
|
40
40
|
requires-python = '>=3.11'
|
|
41
|
-
dependencies = [
|
|
41
|
+
dependencies = [
|
|
42
|
+
'value-object-pattern>=1.36.0',
|
|
43
|
+
'typing-extensions>=4.4.0; python_version < "3.12"',
|
|
44
|
+
]
|
|
42
45
|
dynamic = ['version']
|
|
43
46
|
|
|
44
47
|
[project.urls]
|
|
@@ -52,11 +55,13 @@ coverage = ['coverage[toml]>=7.0.0', 'smokeshow>=0.5.0']
|
|
|
52
55
|
develop = ['pre-commit>=3.0.0']
|
|
53
56
|
format = ['ruff>=0.11.10']
|
|
54
57
|
lint = ['ruff>=0.11.10', 'ty>=0.0.77', { include-group = 'test' }]
|
|
55
|
-
|
|
58
|
+
# TODO: Remove the pin when python-semantic-release supports click>=8.3.3 (PYSEC-2026-2132).
|
|
59
|
+
release = ['build>=1.2.2', 'python-semantic-release==10.6.1']
|
|
56
60
|
test = [
|
|
57
61
|
'pytest>=8.0.0',
|
|
58
62
|
'pytest-asyncio>=1.3.0',
|
|
59
63
|
'pytest-randomly>=3.0.0',
|
|
64
|
+
'sybil[pytest]>=10.1.0',
|
|
60
65
|
'object-mother-pattern>=2.1.0',
|
|
61
66
|
{ include-group = 'coverage' },
|
|
62
67
|
]
|
|
@@ -68,18 +73,13 @@ all = [
|
|
|
68
73
|
{ include-group = 'release' },
|
|
69
74
|
{ include-group = 'test' },
|
|
70
75
|
]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[tool.uv]
|
|
74
|
-
constraint-dependencies = ['click>=8.3.3'] # PYSEC-2026-2132
|
|
75
|
-
|
|
76
|
-
|
|
77
76
|
[tool.pytest.ini_options]
|
|
78
77
|
pythonpath = '.'
|
|
79
78
|
addopts = [
|
|
80
|
-
'--strict-config',
|
|
81
|
-
'--strict-markers',
|
|
82
|
-
'--color=yes',
|
|
79
|
+
'--strict-config', # fail if an unregistered option is used
|
|
80
|
+
'--strict-markers', # fail if an unregistered marker is used
|
|
81
|
+
'--color=yes', # color the output
|
|
82
|
+
'-p tests.test_docstrings',
|
|
83
83
|
]
|
|
84
84
|
markers = ['unit_testing: Check a unique functionality']
|
|
85
85
|
xfail_strict = true
|
|
@@ -113,7 +113,7 @@ source = ['clock_pattern']
|
|
|
113
113
|
[tool.ruff]
|
|
114
114
|
indent-width = 4
|
|
115
115
|
line-length = 120
|
|
116
|
-
target-version = '
|
|
116
|
+
target-version = 'py311'
|
|
117
117
|
output-format = 'grouped'
|
|
118
118
|
|
|
119
119
|
[tool.ruff.format]
|
|
@@ -199,8 +199,18 @@ version_variables = ['clock_pattern/__init__.py:__version__']
|
|
|
199
199
|
|
|
200
200
|
[tool.semantic_release.commit_parser_options]
|
|
201
201
|
minor_tags = ['feat']
|
|
202
|
-
patch_tags = ['fix', 'perf'
|
|
203
|
-
allowed_tags = [
|
|
202
|
+
patch_tags = ['fix', 'perf']
|
|
203
|
+
allowed_tags = [
|
|
204
|
+
'feat',
|
|
205
|
+
'fix',
|
|
206
|
+
'perf',
|
|
207
|
+
'build',
|
|
208
|
+
'refactor',
|
|
209
|
+
'test',
|
|
210
|
+
'docs',
|
|
211
|
+
'ci',
|
|
212
|
+
'chore',
|
|
213
|
+
]
|
|
204
214
|
default_bump_level = 0
|
|
205
215
|
parse_squash_commits = false
|
|
206
216
|
ignore_merge_commits = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/testing/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/pollers/testing/mock_poller_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clock_pattern-0.10.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/testing/mock_sleeper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|