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.
Files changed (59) hide show
  1. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/PKG-INFO +70 -11
  2. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/README.md +68 -9
  3. clock_pattern-0.10.0/clock_pattern/__init__.py +33 -0
  4. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/system_clock.py +3 -1
  5. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/mock_clock.py +2 -2
  6. clock_pattern-0.10.0/clock_pattern/deadlines/__init__.py +9 -0
  7. clock_pattern-0.10.0/clock_pattern/deadlines/errors/__init__.py +3 -0
  8. clock_pattern-0.10.0/clock_pattern/deadlines/errors/timeout_expired_error.py +60 -0
  9. clock_pattern-0.10.0/clock_pattern/deadlines/models/__init__.py +3 -0
  10. clock_pattern-0.10.0/clock_pattern/deadlines/models/deadline.py +106 -0
  11. clock_pattern-0.10.0/clock_pattern/deadlines/system_deadline.py +318 -0
  12. clock_pattern-0.10.0/clock_pattern/deadlines/testing/__init__.py +3 -0
  13. clock_pattern-0.10.0/clock_pattern/deadlines/testing/mock_deadline.py +254 -0
  14. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/models/monotonic_clock.py +1 -1
  15. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/system_monotonic_clock.py +1 -1
  16. clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/__init__.py +3 -0
  17. clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +173 -0
  18. clock_pattern-0.10.0/clock_pattern/pollers/__init__.py +10 -0
  19. clock_pattern-0.10.0/clock_pattern/pollers/models/__init__.py +7 -0
  20. clock_pattern-0.10.0/clock_pattern/pollers/models/poller.py +63 -0
  21. clock_pattern-0.10.0/clock_pattern/pollers/models/poller_async.py +63 -0
  22. clock_pattern-0.10.0/clock_pattern/pollers/system_poller.py +108 -0
  23. clock_pattern-0.10.0/clock_pattern/pollers/system_poller_async.py +114 -0
  24. clock_pattern-0.10.0/clock_pattern/pollers/testing/__init__.py +7 -0
  25. clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller.py +171 -0
  26. clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller_async.py +171 -0
  27. clock_pattern-0.10.0/clock_pattern/retriers/__init__.py +10 -0
  28. clock_pattern-0.10.0/clock_pattern/retriers/models/__init__.py +7 -0
  29. clock_pattern-0.10.0/clock_pattern/retriers/models/retrier.py +86 -0
  30. clock_pattern-0.10.0/clock_pattern/retriers/models/retrier_async.py +92 -0
  31. clock_pattern-0.10.0/clock_pattern/retriers/system_retrier.py +139 -0
  32. clock_pattern-0.10.0/clock_pattern/retriers/system_retrier_async.py +154 -0
  33. clock_pattern-0.10.0/clock_pattern/retriers/testing/__init__.py +7 -0
  34. clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier.py +260 -0
  35. clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier_async.py +273 -0
  36. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/sleeper.py +4 -4
  37. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/sleeper_async.py +4 -4
  38. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/system_sleeper.py +13 -32
  39. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/system_sleeper_async.py +13 -26
  40. clock_pattern-0.10.0/clock_pattern/sleepers/testing/__init__.py +7 -0
  41. clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper.py +197 -0
  42. clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper_async.py +195 -0
  43. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/stopwatches/stopwatch.py +83 -2
  44. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/pyproject.toml +15 -12
  45. clock_pattern-0.9.0/clock_pattern/__init__.py +0 -19
  46. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/.gitignore +0 -0
  47. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/LICENSE.md +0 -0
  48. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/__init__.py +0 -0
  49. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/models/__init__.py +0 -0
  50. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/models/clock.py +0 -0
  51. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/__init__.py +0 -0
  52. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/fixed_clock.py +0 -0
  53. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/clocks/utc_clock.py +0 -0
  54. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/__init__.py +0 -0
  55. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/monotonic_clocks/models/__init__.py +0 -0
  56. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/py.typed +0 -0
  57. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/__init__.py +0 -0
  58. {clock_pattern-0.9.0 → clock_pattern-0.10.0}/clock_pattern/sleepers/models/__init__.py +0 -0
  59. {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.9.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.31.0
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
- - [📚 Available Clocks](#available-clocks)
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, elapsed-time calculations. |
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="available-clocks"></a>
182
+ <a name="public-api"></a>
183
+
184
+ ## 📚 Public API
179
185
 
180
- ## 📚 Available Clocks
186
+ Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
181
187
 
182
- The package offers several clock implementations to suit different needs:
188
+ ### Wall-Clock API
183
189
 
184
- | Clock | Import path | Purpose |
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
- Use the top-level package for production clocks and `clock_pattern.clocks.testing` for test-only clocks.
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 stores the timezone with `ZoneInfo` and
207
- uses it for both `now()` and `today()`.
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
- - [📚 Available Clocks](#available-clocks)
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, elapsed-time calculations. |
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="available-clocks"></a>
157
+ <a name="public-api"></a>
158
+
159
+ ## 📚 Public API
154
160
 
155
- ## 📚 Available Clocks
161
+ Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
156
162
 
157
- The package offers several clock implementations to suit different needs:
163
+ ### Wall-Clock API
158
164
 
159
- | Clock | Import path | Purpose |
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
- Use the top-level package for production clocks and `clock_pattern.clocks.testing` for test-only clocks.
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 stores the timezone with `ZoneInfo` and
182
- uses it for both `now()` and `today()`.
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
- 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
 
@@ -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',)
@@ -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
+ """