clock-pattern 0.9.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.9.0 → clock_pattern-0.11.0}/PKG-INFO +92 -17
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/README.md +89 -15
- clock_pattern-0.11.0/clock_pattern/__init__.py +33 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/system_clock.py +3 -1
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/fixed_clock.py +58 -4
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/mock_clock.py +2 -2
- clock_pattern-0.11.0/clock_pattern/deadlines/__init__.py +9 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/errors/__init__.py +3 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/errors/timeout_expired_error.py +60 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/models/__init__.py +3 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/models/deadline.py +105 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/system_deadline.py +317 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/testing/__init__.py +3 -0
- clock_pattern-0.11.0/clock_pattern/deadlines/testing/mock_deadline.py +253 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/monotonic_clock.py +1 -1
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/system_monotonic_clock.py +1 -1
- clock_pattern-0.11.0/clock_pattern/monotonic_clocks/testing/__init__.py +3 -0
- clock_pattern-0.11.0/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +173 -0
- clock_pattern-0.11.0/clock_pattern/pollers/__init__.py +10 -0
- clock_pattern-0.11.0/clock_pattern/pollers/models/__init__.py +7 -0
- clock_pattern-0.11.0/clock_pattern/pollers/models/poller.py +66 -0
- clock_pattern-0.11.0/clock_pattern/pollers/models/poller_async.py +67 -0
- clock_pattern-0.11.0/clock_pattern/pollers/system_poller.py +114 -0
- clock_pattern-0.11.0/clock_pattern/pollers/system_poller_async.py +161 -0
- clock_pattern-0.11.0/clock_pattern/pollers/testing/__init__.py +7 -0
- clock_pattern-0.11.0/clock_pattern/pollers/testing/mock_poller.py +171 -0
- clock_pattern-0.11.0/clock_pattern/pollers/testing/mock_poller_async.py +171 -0
- clock_pattern-0.11.0/clock_pattern/retriers/__init__.py +10 -0
- clock_pattern-0.11.0/clock_pattern/retriers/models/__init__.py +7 -0
- clock_pattern-0.11.0/clock_pattern/retriers/models/retrier.py +89 -0
- clock_pattern-0.11.0/clock_pattern/retriers/models/retrier_async.py +95 -0
- clock_pattern-0.11.0/clock_pattern/retriers/system_retrier.py +148 -0
- clock_pattern-0.11.0/clock_pattern/retriers/system_retrier_async.py +163 -0
- clock_pattern-0.11.0/clock_pattern/retriers/testing/__init__.py +7 -0
- clock_pattern-0.11.0/clock_pattern/retriers/testing/mock_retrier.py +278 -0
- clock_pattern-0.11.0/clock_pattern/retriers/testing/mock_retrier_async.py +291 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper.py +7 -7
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper_async.py +9 -7
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper.py +17 -36
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper_async.py +27 -33
- clock_pattern-0.11.0/clock_pattern/sleepers/testing/__init__.py +7 -0
- clock_pattern-0.11.0/clock_pattern/sleepers/testing/mock_sleeper.py +197 -0
- clock_pattern-0.11.0/clock_pattern/sleepers/testing/mock_sleeper_async.py +202 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/stopwatches/stopwatch.py +83 -2
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/pyproject.toml +33 -20
- clock_pattern-0.9.0/clock_pattern/__init__.py +0 -19
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/.gitignore +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/LICENSE.md +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/clock.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/utc_clock.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/py.typed +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/__init__.py +0 -0
- {clock_pattern-0.9.0 → clock_pattern-0.11.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.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,7 +20,8 @@ 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:
|
|
23
|
+
Requires-Dist: typing-extensions>=4.4.0; python_version < '3.12'
|
|
24
|
+
Requires-Dist: value-object-pattern>=1.36.0
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
26
27
|
<a name="readme-top"></a>
|
|
@@ -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
|
|
@@ -60,7 +58,7 @@ implementations without touching business rules.
|
|
|
60
58
|
- [📚 Documentation](#documentation)
|
|
61
59
|
- [⚡ Quick Start](#quick-start)
|
|
62
60
|
- [🧩 Why Inject a Clock?](#why-inject-a-clock)
|
|
63
|
-
- [📚
|
|
61
|
+
- [📚 Public API](#public-api)
|
|
64
62
|
- [🌍 Timezone Behavior](#timezone-behavior)
|
|
65
63
|
- [🧪 Testing Time-Sensitive Code](#testing-time-sensitive-code)
|
|
66
64
|
- [🎄 Real-Life Case: Christmas Detector Service](#real-life-case-christmas-detector-service)
|
|
@@ -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>
|
|
@@ -168,32 +165,108 @@ The package exposes two methods:
|
|
|
168
165
|
|
|
169
166
|
| Method | Returns | Typical use |
|
|
170
167
|
| --- | --- | --- |
|
|
171
|
-
| `now()` | `datetime` | Timestamps, expiration windows, audit fields
|
|
168
|
+
| `now()` | `datetime` | Timestamps, expiration windows, and audit fields. |
|
|
172
169
|
| `today()` | `date` | Calendar rules, billing days, holiday checks, date-only decisions. |
|
|
173
170
|
|
|
171
|
+
Clock Pattern also includes injectable helpers for elapsed-duration behavior: monotonic clocks, sleepers, stopwatches,
|
|
172
|
+
deadlines, pollers, and retriers. These use monotonic seconds instead of wall-clock datetimes so system clock changes do
|
|
173
|
+
not affect timeout or retry behavior.
|
|
174
|
+
|
|
174
175
|
<p align="right">
|
|
175
176
|
<a href="#readme-top">🔼 Back to top</a>
|
|
176
177
|
</p><br><br>
|
|
177
178
|
|
|
178
|
-
<a name="
|
|
179
|
+
<a name="public-api"></a>
|
|
180
|
+
|
|
181
|
+
## 📚 Public API
|
|
179
182
|
|
|
180
|
-
|
|
183
|
+
Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
### Wall-Clock API
|
|
183
186
|
|
|
184
|
-
|
|
|
187
|
+
| API | Import path | Purpose |
|
|
185
188
|
| --- | --- | --- |
|
|
186
189
|
| [`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
190
|
| [`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
191
|
| [`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. |
|
|
192
|
+
|
|
193
|
+
### Elapsed-Time API
|
|
194
|
+
|
|
195
|
+
| API | Import path | Purpose |
|
|
196
|
+
| --- | --- | --- |
|
|
189
197
|
| `MonotonicClock` | `from clock_pattern import MonotonicClock` | Abstract contract for elapsed-time sources. |
|
|
190
198
|
| `SystemMonotonicClock` | `from clock_pattern import SystemMonotonicClock` | Production monotonic clock for elapsed-time measurement. |
|
|
199
|
+
| `Sleeper` / `SleeperAsync` | `from clock_pattern import Sleeper, SleeperAsync` | Abstract contracts for injectable sync and async sleeping. |
|
|
191
200
|
| `SystemSleeper` / `SystemSleeperAsync` | `from clock_pattern import SystemSleeper, SystemSleeperAsync` | Injectable sync and async sleeping. |
|
|
192
201
|
| `Stopwatch` | `from clock_pattern import Stopwatch` | Measure elapsed seconds with `.start()`, `.end()`, or a context manager. |
|
|
193
|
-
|
|
|
202
|
+
| `Deadline` | `from clock_pattern import Deadline` | Abstract contract for injectable deadline state. |
|
|
203
|
+
| `SystemDeadline` | `from clock_pattern import SystemDeadline` | Monotonic deadline with an interrupting Unix main-thread context. |
|
|
204
|
+
| `TimeoutExpiredError` | `from clock_pattern import TimeoutExpiredError` | Error raised when a deadline or poll timeout expires. |
|
|
205
|
+
| `Poller` / `PollerAsync` | `from clock_pattern import Poller, PollerAsync` | Abstract contracts for condition polling. |
|
|
206
|
+
| `SystemPoller` / `SystemPollerAsync` | `from clock_pattern import SystemPoller, SystemPollerAsync` | Production polling implementations. |
|
|
207
|
+
| `Retrier` / `RetrierAsync` | `from clock_pattern import Retrier, RetrierAsync` | Abstract contracts for retrying operations. |
|
|
208
|
+
| `SystemRetrier` / `SystemRetrierAsync` | `from clock_pattern import SystemRetrier, SystemRetrierAsync` | Production retry implementations. |
|
|
209
|
+
|
|
210
|
+
### Test Doubles
|
|
211
|
+
|
|
212
|
+
| API | Import path | Purpose |
|
|
213
|
+
| --- | --- | --- |
|
|
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. |
|
|
194
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. |
|
|
216
|
+
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
217
|
+
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
218
|
+
| `MockSleeper` / `MockSleeperAsync` | `from clock_pattern.sleepers.testing import MockSleeper, MockSleeperAsync` | Sleeping test doubles that advance a mock monotonic clock. |
|
|
219
|
+
| `MockPoller` / `MockPollerAsync` | `from clock_pattern.pollers.testing import MockPoller, MockPollerAsync` | Polling test doubles with call assertions. |
|
|
220
|
+
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from clock_pattern import Stopwatch, SystemMonotonicClock, SystemSleeper
|
|
224
|
+
|
|
225
|
+
monotonic_clock = SystemMonotonicClock()
|
|
226
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
195
227
|
|
|
196
|
-
|
|
228
|
+
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
232
|
+
pass
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The deadline context requires a Unix main thread:
|
|
236
|
+
|
|
237
|
+
```python unix
|
|
238
|
+
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
239
|
+
|
|
240
|
+
monotonic_clock = SystemMonotonicClock()
|
|
241
|
+
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
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)
|
|
253
|
+
|
|
254
|
+
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
255
|
+
SystemRetrier(sleeper=sleeper).retry(
|
|
256
|
+
operation=lambda: 'done',
|
|
257
|
+
attempts=3,
|
|
258
|
+
delay_seconds=0.2,
|
|
259
|
+
max_delay_seconds=2,
|
|
260
|
+
backoff=2,
|
|
261
|
+
jitter=True,
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`SystemDeadline` context managers use `SIGALRM` to interrupt Python code and interruptible system calls. Context use is
|
|
266
|
+
limited to Unix main-thread execution, cannot be nested or share an existing alarm, and may be delayed by C code that
|
|
267
|
+
does not return control to the Python interpreter. Deadline properties and `raise_if_expired()` remain cooperative when
|
|
268
|
+
used outside a context manager. `TimeoutExpiredError.elapsed_seconds` exposes the measured elapsed duration reported by
|
|
269
|
+
either timeout path.
|
|
197
270
|
|
|
198
271
|
<p align="right">
|
|
199
272
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -203,8 +276,8 @@ Use the top-level package for production clocks and `clock_pattern.clocks.testin
|
|
|
203
276
|
|
|
204
277
|
## 🌍 Timezone Behavior
|
|
205
278
|
|
|
206
|
-
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It
|
|
207
|
-
uses
|
|
279
|
+
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It converts strings to `ZoneInfo`, preserves
|
|
280
|
+
`tzinfo` instances directly, and uses the resulting timezone for both `now()` and `today()`.
|
|
208
281
|
|
|
209
282
|
```python
|
|
210
283
|
from datetime import UTC
|
|
@@ -262,6 +335,8 @@ clock.assert_today_method_was_called_once()
|
|
|
262
335
|
clock.assert_now_method_was_not_called()
|
|
263
336
|
```
|
|
264
337
|
|
|
338
|
+
`FixedClock.advance(delta=timedelta(...))` moves time forward explicitly; `set(instant=...)` replaces it, including backward jumps. Both keep `now()` and `today()` consistent.
|
|
339
|
+
|
|
265
340
|
More testing recipes are available in [`docs/testing/README.md`](docs/testing/README.md).
|
|
266
341
|
|
|
267
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
|
|
@@ -35,7 +32,7 @@ implementations without touching business rules.
|
|
|
35
32
|
- [📚 Documentation](#documentation)
|
|
36
33
|
- [⚡ Quick Start](#quick-start)
|
|
37
34
|
- [🧩 Why Inject a Clock?](#why-inject-a-clock)
|
|
38
|
-
- [📚
|
|
35
|
+
- [📚 Public API](#public-api)
|
|
39
36
|
- [🌍 Timezone Behavior](#timezone-behavior)
|
|
40
37
|
- [🧪 Testing Time-Sensitive Code](#testing-time-sensitive-code)
|
|
41
38
|
- [🎄 Real-Life Case: Christmas Detector Service](#real-life-case-christmas-detector-service)
|
|
@@ -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>
|
|
@@ -143,32 +139,108 @@ The package exposes two methods:
|
|
|
143
139
|
|
|
144
140
|
| Method | Returns | Typical use |
|
|
145
141
|
| --- | --- | --- |
|
|
146
|
-
| `now()` | `datetime` | Timestamps, expiration windows, audit fields
|
|
142
|
+
| `now()` | `datetime` | Timestamps, expiration windows, and audit fields. |
|
|
147
143
|
| `today()` | `date` | Calendar rules, billing days, holiday checks, date-only decisions. |
|
|
148
144
|
|
|
145
|
+
Clock Pattern also includes injectable helpers for elapsed-duration behavior: monotonic clocks, sleepers, stopwatches,
|
|
146
|
+
deadlines, pollers, and retriers. These use monotonic seconds instead of wall-clock datetimes so system clock changes do
|
|
147
|
+
not affect timeout or retry behavior.
|
|
148
|
+
|
|
149
149
|
<p align="right">
|
|
150
150
|
<a href="#readme-top">🔼 Back to top</a>
|
|
151
151
|
</p><br><br>
|
|
152
152
|
|
|
153
|
-
<a name="
|
|
153
|
+
<a name="public-api"></a>
|
|
154
|
+
|
|
155
|
+
## 📚 Public API
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
|
|
156
158
|
|
|
157
|
-
|
|
159
|
+
### Wall-Clock API
|
|
158
160
|
|
|
159
|
-
|
|
|
161
|
+
| API | Import path | Purpose |
|
|
160
162
|
| --- | --- | --- |
|
|
161
163
|
| [`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
164
|
| [`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
165
|
| [`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. |
|
|
166
|
+
|
|
167
|
+
### Elapsed-Time API
|
|
168
|
+
|
|
169
|
+
| API | Import path | Purpose |
|
|
170
|
+
| --- | --- | --- |
|
|
164
171
|
| `MonotonicClock` | `from clock_pattern import MonotonicClock` | Abstract contract for elapsed-time sources. |
|
|
165
172
|
| `SystemMonotonicClock` | `from clock_pattern import SystemMonotonicClock` | Production monotonic clock for elapsed-time measurement. |
|
|
173
|
+
| `Sleeper` / `SleeperAsync` | `from clock_pattern import Sleeper, SleeperAsync` | Abstract contracts for injectable sync and async sleeping. |
|
|
166
174
|
| `SystemSleeper` / `SystemSleeperAsync` | `from clock_pattern import SystemSleeper, SystemSleeperAsync` | Injectable sync and async sleeping. |
|
|
167
175
|
| `Stopwatch` | `from clock_pattern import Stopwatch` | Measure elapsed seconds with `.start()`, `.end()`, or a context manager. |
|
|
168
|
-
|
|
|
176
|
+
| `Deadline` | `from clock_pattern import Deadline` | Abstract contract for injectable deadline state. |
|
|
177
|
+
| `SystemDeadline` | `from clock_pattern import SystemDeadline` | Monotonic deadline with an interrupting Unix main-thread context. |
|
|
178
|
+
| `TimeoutExpiredError` | `from clock_pattern import TimeoutExpiredError` | Error raised when a deadline or poll timeout expires. |
|
|
179
|
+
| `Poller` / `PollerAsync` | `from clock_pattern import Poller, PollerAsync` | Abstract contracts for condition polling. |
|
|
180
|
+
| `SystemPoller` / `SystemPollerAsync` | `from clock_pattern import SystemPoller, SystemPollerAsync` | Production polling implementations. |
|
|
181
|
+
| `Retrier` / `RetrierAsync` | `from clock_pattern import Retrier, RetrierAsync` | Abstract contracts for retrying operations. |
|
|
182
|
+
| `SystemRetrier` / `SystemRetrierAsync` | `from clock_pattern import SystemRetrier, SystemRetrierAsync` | Production retry implementations. |
|
|
183
|
+
|
|
184
|
+
### Test Doubles
|
|
185
|
+
|
|
186
|
+
| API | Import path | Purpose |
|
|
187
|
+
| --- | --- | --- |
|
|
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. |
|
|
169
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. |
|
|
190
|
+
| `MockMonotonicClock` | `from clock_pattern.monotonic_clocks.testing import MockMonotonicClock` | Controllable elapsed-time source with call assertions. |
|
|
191
|
+
| `MockDeadline` | `from clock_pattern.deadlines.testing import MockDeadline` | Controllable deadline with expiry call assertions. |
|
|
192
|
+
| `MockSleeper` / `MockSleeperAsync` | `from clock_pattern.sleepers.testing import MockSleeper, MockSleeperAsync` | Sleeping test doubles that advance a mock monotonic clock. |
|
|
193
|
+
| `MockPoller` / `MockPollerAsync` | `from clock_pattern.pollers.testing import MockPoller, MockPollerAsync` | Polling test doubles with call assertions. |
|
|
194
|
+
| `MockRetrier` / `MockRetrierAsync` | `from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync` | Retry test doubles with prepared results. |
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from clock_pattern import Stopwatch, SystemMonotonicClock, SystemSleeper
|
|
198
|
+
|
|
199
|
+
monotonic_clock = SystemMonotonicClock()
|
|
200
|
+
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
|
|
170
201
|
|
|
171
|
-
|
|
202
|
+
with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
with sleeper.minimum_duration(seconds=0.001):
|
|
206
|
+
pass
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The deadline context requires a Unix main thread:
|
|
210
|
+
|
|
211
|
+
```python unix
|
|
212
|
+
from clock_pattern import SystemDeadline, SystemMonotonicClock
|
|
213
|
+
|
|
214
|
+
monotonic_clock = SystemMonotonicClock()
|
|
215
|
+
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
|
|
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)
|
|
227
|
+
|
|
228
|
+
poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
|
|
229
|
+
SystemRetrier(sleeper=sleeper).retry(
|
|
230
|
+
operation=lambda: 'done',
|
|
231
|
+
attempts=3,
|
|
232
|
+
delay_seconds=0.2,
|
|
233
|
+
max_delay_seconds=2,
|
|
234
|
+
backoff=2,
|
|
235
|
+
jitter=True,
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`SystemDeadline` context managers use `SIGALRM` to interrupt Python code and interruptible system calls. Context use is
|
|
240
|
+
limited to Unix main-thread execution, cannot be nested or share an existing alarm, and may be delayed by C code that
|
|
241
|
+
does not return control to the Python interpreter. Deadline properties and `raise_if_expired()` remain cooperative when
|
|
242
|
+
used outside a context manager. `TimeoutExpiredError.elapsed_seconds` exposes the measured elapsed duration reported by
|
|
243
|
+
either timeout path.
|
|
172
244
|
|
|
173
245
|
<p align="right">
|
|
174
246
|
<a href="#readme-top">🔼 Back to top</a>
|
|
@@ -178,8 +250,8 @@ Use the top-level package for production clocks and `clock_pattern.clocks.testin
|
|
|
178
250
|
|
|
179
251
|
## 🌍 Timezone Behavior
|
|
180
252
|
|
|
181
|
-
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It
|
|
182
|
-
uses
|
|
253
|
+
`SystemClock` accepts either an IANA timezone string or a `tzinfo` instance. It converts strings to `ZoneInfo`, preserves
|
|
254
|
+
`tzinfo` instances directly, and uses the resulting timezone for both `now()` and `today()`.
|
|
183
255
|
|
|
184
256
|
```python
|
|
185
257
|
from datetime import UTC
|
|
@@ -237,6 +309,8 @@ clock.assert_today_method_was_called_once()
|
|
|
237
309
|
clock.assert_now_method_was_not_called()
|
|
238
310
|
```
|
|
239
311
|
|
|
312
|
+
`FixedClock.advance(delta=timedelta(...))` moves time forward explicitly; `set(instant=...)` replaces it, including backward jumps. Both keep `now()` and `today()` consistent.
|
|
313
|
+
|
|
240
314
|
More testing recipes are available in [`docs/testing/README.md`](docs/testing/README.md).
|
|
241
315
|
|
|
242
316
|
<p align="right">
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
__version__ = '0.11.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
|
|
|
@@ -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
|
"""
|
|
@@ -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
|