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.
Files changed (59) hide show
  1. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/PKG-INFO +92 -17
  2. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/README.md +89 -15
  3. clock_pattern-0.11.0/clock_pattern/__init__.py +33 -0
  4. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/system_clock.py +3 -1
  5. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/fixed_clock.py +58 -4
  6. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/mock_clock.py +2 -2
  7. clock_pattern-0.11.0/clock_pattern/deadlines/__init__.py +9 -0
  8. clock_pattern-0.11.0/clock_pattern/deadlines/errors/__init__.py +3 -0
  9. clock_pattern-0.11.0/clock_pattern/deadlines/errors/timeout_expired_error.py +60 -0
  10. clock_pattern-0.11.0/clock_pattern/deadlines/models/__init__.py +3 -0
  11. clock_pattern-0.11.0/clock_pattern/deadlines/models/deadline.py +105 -0
  12. clock_pattern-0.11.0/clock_pattern/deadlines/system_deadline.py +317 -0
  13. clock_pattern-0.11.0/clock_pattern/deadlines/testing/__init__.py +3 -0
  14. clock_pattern-0.11.0/clock_pattern/deadlines/testing/mock_deadline.py +253 -0
  15. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/monotonic_clock.py +1 -1
  16. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/system_monotonic_clock.py +1 -1
  17. clock_pattern-0.11.0/clock_pattern/monotonic_clocks/testing/__init__.py +3 -0
  18. clock_pattern-0.11.0/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +173 -0
  19. clock_pattern-0.11.0/clock_pattern/pollers/__init__.py +10 -0
  20. clock_pattern-0.11.0/clock_pattern/pollers/models/__init__.py +7 -0
  21. clock_pattern-0.11.0/clock_pattern/pollers/models/poller.py +66 -0
  22. clock_pattern-0.11.0/clock_pattern/pollers/models/poller_async.py +67 -0
  23. clock_pattern-0.11.0/clock_pattern/pollers/system_poller.py +114 -0
  24. clock_pattern-0.11.0/clock_pattern/pollers/system_poller_async.py +161 -0
  25. clock_pattern-0.11.0/clock_pattern/pollers/testing/__init__.py +7 -0
  26. clock_pattern-0.11.0/clock_pattern/pollers/testing/mock_poller.py +171 -0
  27. clock_pattern-0.11.0/clock_pattern/pollers/testing/mock_poller_async.py +171 -0
  28. clock_pattern-0.11.0/clock_pattern/retriers/__init__.py +10 -0
  29. clock_pattern-0.11.0/clock_pattern/retriers/models/__init__.py +7 -0
  30. clock_pattern-0.11.0/clock_pattern/retriers/models/retrier.py +89 -0
  31. clock_pattern-0.11.0/clock_pattern/retriers/models/retrier_async.py +95 -0
  32. clock_pattern-0.11.0/clock_pattern/retriers/system_retrier.py +148 -0
  33. clock_pattern-0.11.0/clock_pattern/retriers/system_retrier_async.py +163 -0
  34. clock_pattern-0.11.0/clock_pattern/retriers/testing/__init__.py +7 -0
  35. clock_pattern-0.11.0/clock_pattern/retriers/testing/mock_retrier.py +278 -0
  36. clock_pattern-0.11.0/clock_pattern/retriers/testing/mock_retrier_async.py +291 -0
  37. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper.py +7 -7
  38. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/sleeper_async.py +9 -7
  39. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper.py +17 -36
  40. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/system_sleeper_async.py +27 -33
  41. clock_pattern-0.11.0/clock_pattern/sleepers/testing/__init__.py +7 -0
  42. clock_pattern-0.11.0/clock_pattern/sleepers/testing/mock_sleeper.py +197 -0
  43. clock_pattern-0.11.0/clock_pattern/sleepers/testing/mock_sleeper_async.py +202 -0
  44. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/stopwatches/stopwatch.py +83 -2
  45. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/pyproject.toml +33 -20
  46. clock_pattern-0.9.0/clock_pattern/__init__.py +0 -19
  47. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/.gitignore +0 -0
  48. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/LICENSE.md +0 -0
  49. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/__init__.py +0 -0
  50. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/__init__.py +0 -0
  51. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/models/clock.py +0 -0
  52. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/testing/__init__.py +0 -0
  53. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/clocks/utc_clock.py +0 -0
  54. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/__init__.py +0 -0
  55. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/monotonic_clocks/models/__init__.py +0 -0
  56. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/py.typed +0 -0
  57. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/__init__.py +0 -0
  58. {clock_pattern-0.9.0 → clock_pattern-0.11.0}/clock_pattern/sleepers/models/__init__.py +0 -0
  59. {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.9.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: value-object-pattern>=1.31.0
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
- - [📚 Available Clocks](#available-clocks)
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, elapsed-time calculations. |
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="available-clocks"></a>
179
+ <a name="public-api"></a>
180
+
181
+ ## 📚 Public API
179
182
 
180
- ## 📚 Available Clocks
183
+ Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
181
184
 
182
- The package offers several clock implementations to suit different needs:
185
+ ### Wall-Clock API
183
186
 
184
- | Clock | Import path | Purpose |
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
- | [`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. |
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
- Use the top-level package for production clocks and `clock_pattern.clocks.testing` for test-only clocks.
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 stores the timezone with `ZoneInfo` and
207
- uses it for both `now()` and `today()`.
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
- - [📚 Available Clocks](#available-clocks)
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, elapsed-time calculations. |
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="available-clocks"></a>
153
+ <a name="public-api"></a>
154
+
155
+ ## 📚 Public API
154
156
 
155
- ## 📚 Available Clocks
157
+ Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
156
158
 
157
- The package offers several clock implementations to suit different needs:
159
+ ### Wall-Clock API
158
160
 
159
- | Clock | Import path | Purpose |
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
- | [`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. |
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
- Use the top-level package for production clocks and `clock_pattern.clocks.testing` for test-only clocks.
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 stores the timezone with `ZoneInfo` and
182
- uses it for both `now()` and `today()`.
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
- timezone = str(TimezoneValueObject(value=timezone, title='SystemClock', parameter='timezone'))
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 always returns the same instant.
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. Use `MockClock` when a test also needs to assert whether `now()` or `today()` was called.
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-value]
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-value]
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,9 @@
1
+ from .errors import TimeoutExpiredError
2
+ from .models import Deadline
3
+ from .system_deadline import SystemDeadline
4
+
5
+ __all__ = (
6
+ 'Deadline',
7
+ 'SystemDeadline',
8
+ 'TimeoutExpiredError',
9
+ )
@@ -0,0 +1,3 @@
1
+ from .timeout_expired_error import TimeoutExpiredError
2
+
3
+ __all__ = ('TimeoutExpiredError',)
@@ -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,3 @@
1
+ from .deadline import Deadline
2
+
3
+ __all__ = ('Deadline',)