clock-pattern 0.9.0__tar.gz → 0.10.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.9.0 → clock_pattern-0.10.0}/PKG-INFO +70 -11
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/README.md +68 -9
- clock_pattern-0.10.0/clock_pattern/__init__.py +33 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/system_clock.py +3 -1
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/mock_clock.py +2 -2
- clock_pattern-0.10.0/clock_pattern/deadlines/__init__.py +9 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/errors/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/errors/timeout_expired_error.py +60 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/models/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/models/deadline.py +106 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/system_deadline.py +318 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/testing/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/testing/mock_deadline.py +254 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/models/monotonic_clock.py +1 -1
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/system_monotonic_clock.py +1 -1
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +173 -0
- clock_pattern-0.10.0/clock_pattern/pollers/__init__.py +10 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/poller.py +63 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/poller_async.py +63 -0
- clock_pattern-0.10.0/clock_pattern/pollers/system_poller.py +108 -0
- clock_pattern-0.10.0/clock_pattern/pollers/system_poller_async.py +114 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller.py +171 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller_async.py +171 -0
- clock_pattern-0.10.0/clock_pattern/retriers/__init__.py +10 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/retrier.py +86 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/retrier_async.py +92 -0
- clock_pattern-0.10.0/clock_pattern/retriers/system_retrier.py +139 -0
- clock_pattern-0.10.0/clock_pattern/retriers/system_retrier_async.py +154 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier.py +260 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier_async.py +273 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/sleeper.py +4 -4
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/sleeper_async.py +4 -4
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/system_sleeper.py +13 -32
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/system_sleeper_async.py +13 -26
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper.py +197 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper_async.py +195 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/stopwatches/stopwatch.py +83 -2
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/pyproject.toml +15 -12
- clock_pattern-0.9.0/clock_pattern/__init__.py +0 -19
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/.gitignore +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/LICENSE.md +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/models/clock.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/fixed_clock.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/utc_clock.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/py.typed +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/stopwatches/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clock-pattern
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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,7 +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: value-object-pattern>=1.
|
|
23
|
+
Requires-Dist: value-object-pattern>=1.36.0
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
<a name="readme-top"></a>
|
|
@@ -60,7 +60,7 @@ implementations without touching business rules.
|
|
|
60
60
|
- [📚 Documentation](#documentation)
|
|
61
61
|
- [⚡ Quick Start](#quick-start)
|
|
62
62
|
- [🧩 Why Inject a Clock?](#why-inject-a-clock)
|
|
63
|
-
- [📚
|
|
63
|
+
- [📚 Public API](#public-api)
|
|
64
64
|
- [🌍 Timezone Behavior](#timezone-behavior)
|
|
65
65
|
- [🧪 Testing Time-Sensitive Code](#testing-time-sensitive-code)
|
|
66
66
|
- [🎄 Real-Life Case: Christmas Detector Service](#real-life-case-christmas-detector-service)
|
|
@@ -168,32 +168,91 @@ The package exposes two methods:
|
|
|
168
168
|
|
|
169
169
|
| Method | Returns | Typical use |
|
|
170
170
|
| --- | --- | --- |
|
|
171
|
-
| `now()` | `datetime` | Timestamps, expiration windows, audit fields
|
|
171
|
+
| `now()` | `datetime` | Timestamps, expiration windows, and audit fields. |
|
|
172
172
|
| `today()` | `date` | Calendar rules, billing days, holiday checks, date-only decisions. |
|
|
173
173
|
|
|
174
|
+
Clock Pattern also includes injectable helpers for elapsed-duration behavior: monotonic clocks, sleepers, stopwatches,
|
|
175
|
+
deadlines, pollers, and retriers. These use monotonic seconds instead of wall-clock datetimes so system clock changes do
|
|
176
|
+
not affect timeout or retry behavior.
|
|
177
|
+
|
|
174
178
|
<p align="right">
|
|
175
179
|
<a href="#readme-top">🔼 Back to top</a>
|
|
176
180
|
</p><br><br>
|
|
177
181
|
|
|
178
|
-
<a name="
|
|
182
|
+
<a name="public-api"></a>
|
|
183
|
+
|
|
184
|
+
## 📚 Public API
|
|
179
185
|
|
|
180
|
-
|
|
186
|
+
Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
|
|
181
187
|
|
|
182
|
-
|
|
188
|
+
### Wall-Clock API
|
|
183
189
|
|
|
184
|
-
|
|
|
190
|
+
| API | Import path | Purpose |
|
|
185
191
|
| --- | --- | --- |
|
|
186
192
|
| [`Clock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/models/clock.py) | `from clock_pattern import Clock` | Abstract contract for code that needs `now()` or `today()`. |
|
|
187
193
|
| [`SystemClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/system_clock.py) | `from clock_pattern import SystemClock` | Production clock backed by system time in a configured timezone. |
|
|
188
194
|
| [`UtcClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/utc_clock.py) | `from clock_pattern import UtcClock` | Production clock fixed to UTC. |
|
|
195
|
+
|
|
196
|
+
### Elapsed-Time API
|
|
197
|
+
|
|
198
|
+
| API | Import path | Purpose |
|
|
199
|
+
| --- | --- | --- |
|
|
189
200
|
| `MonotonicClock` | `from clock_pattern import MonotonicClock` | Abstract contract for elapsed-time sources. |
|
|
190
201
|
| `SystemMonotonicClock` | `from clock_pattern import SystemMonotonicClock` | Production monotonic clock for elapsed-time measurement. |
|
|
202
|
+
| `Sleeper` / `SleeperAsync` | `from clock_pattern import Sleeper, SleeperAsync` | Abstract contracts for injectable sync and async sleeping. |
|
|
191
203
|
| `SystemSleeper` / `SystemSleeperAsync` | `from clock_pattern import SystemSleeper, SystemSleeperAsync` | Injectable sync and async sleeping. |
|
|
192
204
|
| `Stopwatch` | `from clock_pattern import Stopwatch` | Measure elapsed seconds with `.start()`, `.end()`, or a context manager. |
|
|
205
|
+
| `Deadline` | `from clock_pattern import Deadline` | Abstract contract for injectable deadline state. |
|
|
206
|
+
| `SystemDeadline` | `from clock_pattern import SystemDeadline` | Monotonic deadline with an interrupting Unix main-thread context. |
|
|
207
|
+
| `TimeoutExpiredError` | `from clock_pattern import TimeoutExpiredError` | Error raised when a deadline or poll timeout expires. |
|
|
208
|
+
| `Poller` / `PollerAsync` | `from clock_pattern import Poller, PollerAsync` | Abstract contracts for condition polling. |
|
|
209
|
+
| `SystemPoller` / `SystemPollerAsync` | `from clock_pattern import SystemPoller, SystemPollerAsync` | Production polling implementations. |
|
|
210
|
+
| `Retrier` / `RetrierAsync` | `from clock_pattern import Retrier, RetrierAsync` | Abstract contracts for retrying operations. |
|
|
211
|
+
| `SystemRetrier` / `SystemRetrierAsync` | `from clock_pattern import SystemRetrier, SystemRetrierAsync` | Production retry implementations. |
|
|
212
|
+
|
|
213
|
+
### Test Doubles
|
|
214
|
+
|
|
215
|
+
| API | Import path | Purpose |
|
|
216
|
+
| --- | --- | --- |
|
|
193
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 always returns the same datetime and derived date. |
|
|
194
218
|
| [`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
|
+
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
220
|
+
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
221
|
+
| `MockSleeper` / `MockSleeperAsync` | `from clock_pattern.sleepers.testing import MockSleeper, MockSleeperAsync` | Sleeping test doubles that advance a mock monotonic clock. |
|
|
222
|
+
| `MockPoller` / `MockPollerAsync` | `from clock_pattern.pollers.testing import MockPoller, MockPollerAsync` | Polling test doubles with call assertions. |
|
|
223
|
+
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from clock_pattern import Stopwatch, SystemDeadline, SystemMonotonicClock, SystemPoller, SystemRetrier, SystemSleeper
|
|
227
|
+
|
|
228
|
+
monotonic_clock = SystemMonotonicClock()
|
|
229
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
230
|
+
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
231
|
+
|
|
232
|
+
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
with sleeper.minimum_duration(seconds=2):
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
242
|
+
SystemRetrier(sleeper=sleeper).retry(
|
|
243
|
+
operation=lambda: 'done',
|
|
244
|
+
attempts=3,
|
|
245
|
+
delay_seconds=0.2,
|
|
246
|
+
backoff=2,
|
|
247
|
+
jitter=True,
|
|
248
|
+
)
|
|
249
|
+
```
|
|
195
250
|
|
|
196
|
-
|
|
251
|
+
`SystemDeadline` context managers use `SIGALRM` to interrupt Python code and interruptible system calls. Context use is
|
|
252
|
+
limited to Unix main-thread execution, cannot be nested or share an existing alarm, and may be delayed by C code that
|
|
253
|
+
does not return control to the Python interpreter. Deadline properties and `raise_if_expired()` remain cooperative when
|
|
254
|
+
used outside a context manager. `TimeoutExpiredError.elapsed_seconds` exposes the measured elapsed duration reported by
|
|
255
|
+
either timeout path.
|
|
197
256
|
|
|
198
257
|
<p align="right">
|
|
199
258
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -203,8 +262,8 @@ Use the top-level package for production clocks and `clock_pattern.clocks.testin
|
|
|
203
262
|
|
|
204
263
|
## 🌍 Timezone Behavior
|
|
205
264
|
|
|
206
|
-
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It
|
|
207
|
-
uses
|
|
265
|
+
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It converts strings to `ZoneInfo`, preserves
|
|
266
|
+
`tzinfo` instances directly, and uses the resulting timezone for both `now()` and `today()`.
|
|
208
267
|
|
|
209
268
|
```python
|
|
210
269
|
from datetime import UTC
|
|
@@ -35,7 +35,7 @@ implementations without touching business rules.
|
|
|
35
35
|
- [📚 Documentation](#documentation)
|
|
36
36
|
- [⚡ Quick Start](#quick-start)
|
|
37
37
|
- [🧩 Why Inject a Clock?](#why-inject-a-clock)
|
|
38
|
-
- [📚
|
|
38
|
+
- [📚 Public API](#public-api)
|
|
39
39
|
- [🌍 Timezone Behavior](#timezone-behavior)
|
|
40
40
|
- [🧪 Testing Time-Sensitive Code](#testing-time-sensitive-code)
|
|
41
41
|
- [🎄 Real-Life Case: Christmas Detector Service](#real-life-case-christmas-detector-service)
|
|
@@ -143,32 +143,91 @@ The package exposes two methods:
|
|
|
143
143
|
|
|
144
144
|
| Method | Returns | Typical use |
|
|
145
145
|
| --- | --- | --- |
|
|
146
|
-
| `now()` | `datetime` | Timestamps, expiration windows, audit fields
|
|
146
|
+
| `now()` | `datetime` | Timestamps, expiration windows, and audit fields. |
|
|
147
147
|
| `today()` | `date` | Calendar rules, billing days, holiday checks, date-only decisions. |
|
|
148
148
|
|
|
149
|
+
Clock Pattern also includes injectable helpers for elapsed-duration behavior: monotonic clocks, sleepers, stopwatches,
|
|
150
|
+
deadlines, pollers, and retriers. These use monotonic seconds instead of wall-clock datetimes so system clock changes do
|
|
151
|
+
not affect timeout or retry behavior.
|
|
152
|
+
|
|
149
153
|
<p align="right">
|
|
150
154
|
<a href="#readme-top">🔼 Back to top</a>
|
|
151
155
|
</p><br><br>
|
|
152
156
|
|
|
153
|
-
<a name="
|
|
157
|
+
<a name="public-api"></a>
|
|
158
|
+
|
|
159
|
+
## 📚 Public API
|
|
154
160
|
|
|
155
|
-
|
|
161
|
+
Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
|
|
156
162
|
|
|
157
|
-
|
|
163
|
+
### Wall-Clock API
|
|
158
164
|
|
|
159
|
-
|
|
|
165
|
+
| API | Import path | Purpose |
|
|
160
166
|
| --- | --- | --- |
|
|
161
167
|
| [`Clock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/models/clock.py) | `from clock_pattern import Clock` | Abstract contract for code that needs `now()` or `today()`. |
|
|
162
168
|
| [`SystemClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/system_clock.py) | `from clock_pattern import SystemClock` | Production clock backed by system time in a configured timezone. |
|
|
163
169
|
| [`UtcClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/utc_clock.py) | `from clock_pattern import UtcClock` | Production clock fixed to UTC. |
|
|
170
|
+
|
|
171
|
+
### Elapsed-Time API
|
|
172
|
+
|
|
173
|
+
| API | Import path | Purpose |
|
|
174
|
+
| --- | --- | --- |
|
|
164
175
|
| `MonotonicClock` | `from clock_pattern import MonotonicClock` | Abstract contract for elapsed-time sources. |
|
|
165
176
|
| `SystemMonotonicClock` | `from clock_pattern import SystemMonotonicClock` | Production monotonic clock for elapsed-time measurement. |
|
|
177
|
+
| `Sleeper` / `SleeperAsync` | `from clock_pattern import Sleeper, SleeperAsync` | Abstract contracts for injectable sync and async sleeping. |
|
|
166
178
|
| `SystemSleeper` / `SystemSleeperAsync` | `from clock_pattern import SystemSleeper, SystemSleeperAsync` | Injectable sync and async sleeping. |
|
|
167
179
|
| `Stopwatch` | `from clock_pattern import Stopwatch` | Measure elapsed seconds with `.start()`, `.end()`, or a context manager. |
|
|
180
|
+
| `Deadline` | `from clock_pattern import Deadline` | Abstract contract for injectable deadline state. |
|
|
181
|
+
| `SystemDeadline` | `from clock_pattern import SystemDeadline` | Monotonic deadline with an interrupting Unix main-thread context. |
|
|
182
|
+
| `TimeoutExpiredError` | `from clock_pattern import TimeoutExpiredError` | Error raised when a deadline or poll timeout expires. |
|
|
183
|
+
| `Poller` / `PollerAsync` | `from clock_pattern import Poller, PollerAsync` | Abstract contracts for condition polling. |
|
|
184
|
+
| `SystemPoller` / `SystemPollerAsync` | `from clock_pattern import SystemPoller, SystemPollerAsync` | Production polling implementations. |
|
|
185
|
+
| `Retrier` / `RetrierAsync` | `from clock_pattern import Retrier, RetrierAsync` | Abstract contracts for retrying operations. |
|
|
186
|
+
| `SystemRetrier` / `SystemRetrierAsync` | `from clock_pattern import SystemRetrier, SystemRetrierAsync` | Production retry implementations. |
|
|
187
|
+
|
|
188
|
+
### Test Doubles
|
|
189
|
+
|
|
190
|
+
| API | Import path | Purpose |
|
|
191
|
+
| --- | --- | --- |
|
|
168
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 always returns the same datetime and derived date. |
|
|
169
193
|
| [`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
|
+
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
195
|
+
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
196
|
+
| `MockSleeper` / `MockSleeperAsync` | `from clock_pattern.sleepers.testing import MockSleeper, MockSleeperAsync` | Sleeping test doubles that advance a mock monotonic clock. |
|
|
197
|
+
| `MockPoller` / `MockPollerAsync` | `from clock_pattern.pollers.testing import MockPoller, MockPollerAsync` | Polling test doubles with call assertions. |
|
|
198
|
+
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from clock_pattern import Stopwatch, SystemDeadline, SystemMonotonicClock, SystemPoller, SystemRetrier, SystemSleeper
|
|
202
|
+
|
|
203
|
+
monotonic_clock = SystemMonotonicClock()
|
|
204
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
205
|
+
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)
|
|
206
|
+
|
|
207
|
+
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
with sleeper.minimum_duration(seconds=2):
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
217
|
+
SystemRetrier(sleeper=sleeper).retry(
|
|
218
|
+
operation=lambda: 'done',
|
|
219
|
+
attempts=3,
|
|
220
|
+
delay_seconds=0.2,
|
|
221
|
+
backoff=2,
|
|
222
|
+
jitter=True,
|
|
223
|
+
)
|
|
224
|
+
```
|
|
170
225
|
|
|
171
|
-
|
|
226
|
+
`SystemDeadline` context managers use `SIGALRM` to interrupt Python code and interruptible system calls. Context use is
|
|
227
|
+
limited to Unix main-thread execution, cannot be nested or share an existing alarm, and may be delayed by C code that
|
|
228
|
+
does not return control to the Python interpreter. Deadline properties and `raise_if_expired()` remain cooperative when
|
|
229
|
+
used outside a context manager. `TimeoutExpiredError.elapsed_seconds` exposes the measured elapsed duration reported by
|
|
230
|
+
either timeout path.
|
|
172
231
|
|
|
173
232
|
<p align="right">
|
|
174
233
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -178,8 +237,8 @@ Use the top-level package for production clocks and `clock_pattern.clocks.testin
|
|
|
178
237
|
|
|
179
238
|
## 🌍 Timezone Behavior
|
|
180
239
|
|
|
181
|
-
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It
|
|
182
|
-
uses
|
|
240
|
+
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It converts strings to `ZoneInfo`, preserves
|
|
241
|
+
`tzinfo` instances directly, and uses the resulting timezone for both `now()` and `today()`.
|
|
183
242
|
|
|
184
243
|
```python
|
|
185
244
|
from datetime import UTC
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
__version__ = '0.10.0'
|
|
2
|
+
|
|
3
|
+
from .clocks import Clock, SystemClock, UtcClock
|
|
4
|
+
from .deadlines import Deadline, SystemDeadline, TimeoutExpiredError
|
|
5
|
+
from .monotonic_clocks import MonotonicClock, SystemMonotonicClock
|
|
6
|
+
from .pollers import Poller, PollerAsync, SystemPoller, SystemPollerAsync
|
|
7
|
+
from .retriers import Retrier, RetrierAsync, SystemRetrier, SystemRetrierAsync
|
|
8
|
+
from .sleepers import Sleeper, SleeperAsync, SystemSleeper, SystemSleeperAsync
|
|
9
|
+
from .stopwatches import Stopwatch
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'Clock',
|
|
13
|
+
'Deadline',
|
|
14
|
+
'MonotonicClock',
|
|
15
|
+
'Poller',
|
|
16
|
+
'PollerAsync',
|
|
17
|
+
'Retrier',
|
|
18
|
+
'RetrierAsync',
|
|
19
|
+
'Sleeper',
|
|
20
|
+
'SleeperAsync',
|
|
21
|
+
'Stopwatch',
|
|
22
|
+
'SystemClock',
|
|
23
|
+
'SystemDeadline',
|
|
24
|
+
'SystemMonotonicClock',
|
|
25
|
+
'SystemPoller',
|
|
26
|
+
'SystemPollerAsync',
|
|
27
|
+
'SystemRetrier',
|
|
28
|
+
'SystemRetrierAsync',
|
|
29
|
+
'SystemSleeper',
|
|
30
|
+
'SystemSleeperAsync',
|
|
31
|
+
'TimeoutExpiredError',
|
|
32
|
+
'UtcClock',
|
|
33
|
+
)
|
|
@@ -61,7 +61,9 @@ class SystemClock(Clock):
|
|
|
61
61
|
```
|
|
62
62
|
"""
|
|
63
63
|
if isinstance(timezone, tzinfo):
|
|
64
|
-
|
|
64
|
+
TimezoneValueObject(value=timezone, title='SystemClock', parameter='timezone')
|
|
65
|
+
self._timezone = timezone
|
|
66
|
+
return
|
|
65
67
|
|
|
66
68
|
StringTimezoneValueObject(value=timezone, title='SystemClock', parameter='timezone')
|
|
67
69
|
|
|
@@ -106,7 +106,7 @@ class MockClock(Clock):
|
|
|
106
106
|
|
|
107
107
|
self._now_mock()
|
|
108
108
|
|
|
109
|
-
return self._now_datetime # type: ignore[return-
|
|
109
|
+
return self._now_datetime # type: ignore[ty:invalid-return-type]
|
|
110
110
|
|
|
111
111
|
def prepare_now_method_return_value(self, *, now: datetime) -> None:
|
|
112
112
|
"""
|
|
@@ -220,7 +220,7 @@ class MockClock(Clock):
|
|
|
220
220
|
|
|
221
221
|
self._today_mock()
|
|
222
222
|
|
|
223
|
-
return self._today_date # type: ignore[return-
|
|
223
|
+
return self._today_date # type: ignore[ty:invalid-return-type]
|
|
224
224
|
|
|
225
225
|
def prepare_today_method_return_value(self, *, today: date) -> None:
|
|
226
226
|
"""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deadline exceptions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TimeoutExpiredError(TimeoutError):
|
|
7
|
+
"""
|
|
8
|
+
Raised when a deadline timeout expires.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from clock_pattern import TimeoutExpiredError
|
|
13
|
+
|
|
14
|
+
error = TimeoutExpiredError(elapsed_seconds=1.5)
|
|
15
|
+
print(error)
|
|
16
|
+
# >>> Deadline expired after <<<1.5>>> seconds.
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_elapsed_seconds: float
|
|
21
|
+
|
|
22
|
+
def __init__(self, *, elapsed_seconds: float) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Create a `TimeoutExpiredError` with the configured duration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
elapsed_seconds (float): Duration in seconds that expired.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
from clock_pattern import TimeoutExpiredError
|
|
32
|
+
|
|
33
|
+
error = TimeoutExpiredError(elapsed_seconds=1.5)
|
|
34
|
+
print(error)
|
|
35
|
+
# >>> Deadline expired after <<<1.5>>> seconds.
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
self._elapsed_seconds = elapsed_seconds
|
|
39
|
+
|
|
40
|
+
message = f'Deadline expired after <<<{self._elapsed_seconds}>>> seconds.'
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def elapsed_seconds(self) -> float:
|
|
45
|
+
"""
|
|
46
|
+
Retrieve the duration in seconds that expired.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
float: Duration in seconds that expired.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
```python
|
|
53
|
+
from clock_pattern import TimeoutExpiredError
|
|
54
|
+
|
|
55
|
+
error = TimeoutExpiredError(elapsed_seconds=1.5)
|
|
56
|
+
print(error.elapsed_seconds)
|
|
57
|
+
# >>> 1.5
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
return self._elapsed_seconds
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract deadline contract for injectable timeout state.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from contextlib import AbstractContextManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Deadline(AbstractContextManager['Deadline']):
|
|
10
|
+
"""
|
|
11
|
+
Define the interface for injectable deadlines.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
```python
|
|
15
|
+
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
16
|
+
|
|
17
|
+
with SystemDeadline(seconds=1, monotonic_clock=SystemMonotonicClock()) as deadline:
|
|
18
|
+
print(deadline.remaining_seconds)
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def elapsed_seconds(self) -> float:
|
|
25
|
+
"""
|
|
26
|
+
Retrieve elapsed seconds since the deadline started.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
float: Elapsed seconds since the deadline started.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
```python
|
|
33
|
+
from clock_pattern import SystemDeadline
|
|
34
|
+
from clock_pattern.monotonic_clocks.testing import MockMonotonicClock
|
|
35
|
+
|
|
36
|
+
monotonic_clock = MockMonotonicClock()
|
|
37
|
+
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
38
|
+
monotonic_clock.advance(seconds=0.25)
|
|
39
|
+
print(deadline.elapsed_seconds)
|
|
40
|
+
# >>> 0.25
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def remaining_seconds(self) -> float:
|
|
47
|
+
"""
|
|
48
|
+
Retrieve remaining seconds before expiry.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
float: Remaining seconds, never less than zero.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
```python
|
|
55
|
+
from clock_pattern import SystemDeadline
|
|
56
|
+
from clock_pattern.monotonic_clocks.testing import MockMonotonicClock
|
|
57
|
+
|
|
58
|
+
monotonic_clock = MockMonotonicClock()
|
|
59
|
+
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
60
|
+
monotonic_clock.advance(seconds=0.25)
|
|
61
|
+
print(deadline.remaining_seconds)
|
|
62
|
+
# >>> 0.75
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def expired(self) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check whether the deadline has expired.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool: `True` when the configured duration has elapsed.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
```python
|
|
77
|
+
from clock_pattern import SystemDeadline
|
|
78
|
+
from clock_pattern.monotonic_clocks.testing import MockMonotonicClock
|
|
79
|
+
|
|
80
|
+
monotonic_clock = MockMonotonicClock()
|
|
81
|
+
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
82
|
+
monotonic_clock.advance(seconds=1)
|
|
83
|
+
print(deadline.expired)
|
|
84
|
+
# >>> True
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def raise_if_expired(self) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Raise when the configured duration has elapsed.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
TimeoutExpiredError: If the deadline expired.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
```python
|
|
98
|
+
from clock_pattern import SystemDeadline
|
|
99
|
+
from clock_pattern.monotonic_clocks.testing import MockMonotonicClock
|
|
100
|
+
|
|
101
|
+
monotonic_clock = MockMonotonicClock()
|
|
102
|
+
deadline = SystemDeadline(seconds=1, monotonic_clock=monotonic_clock)
|
|
103
|
+
monotonic_clock.advance(seconds=1)
|
|
104
|
+
deadline.raise_if_expired()
|
|
105
|
+
```
|
|
106
|
+
"""
|