clock-pattern 0.7.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/.gitignore +3 -0
- clock_pattern-0.10.0/PKG-INFO +413 -0
- clock_pattern-0.10.0/README.md +388 -0
- clock_pattern-0.10.0/clock_pattern/__init__.py +33 -0
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/__init__.py +2 -0
- clock_pattern-0.10.0/clock_pattern/clocks/models/clock.py +66 -0
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/system_clock.py +29 -19
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/fixed_clock.py +15 -12
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/mock_clock.py +34 -27
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/utc_clock.py +6 -3
- clock_pattern-0.10.0/clock_pattern/deadlines/__init__.py +9 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/errors/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/errors/timeout_expired_error.py +60 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/models/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/models/deadline.py +106 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/system_deadline.py +318 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/testing/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/deadlines/testing/mock_deadline.py +254 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/models/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/models/monotonic_clock.py +48 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/system_monotonic_clock.py +48 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/monotonic_clocks/testing/mock_monotonic_clock.py +173 -0
- clock_pattern-0.10.0/clock_pattern/pollers/__init__.py +10 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/poller.py +63 -0
- clock_pattern-0.10.0/clock_pattern/pollers/models/poller_async.py +63 -0
- clock_pattern-0.10.0/clock_pattern/pollers/system_poller.py +108 -0
- clock_pattern-0.10.0/clock_pattern/pollers/system_poller_async.py +114 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller.py +171 -0
- clock_pattern-0.10.0/clock_pattern/pollers/testing/mock_poller_async.py +171 -0
- clock_pattern-0.10.0/clock_pattern/retriers/__init__.py +10 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/retrier.py +86 -0
- clock_pattern-0.10.0/clock_pattern/retriers/models/retrier_async.py +92 -0
- clock_pattern-0.10.0/clock_pattern/retriers/system_retrier.py +139 -0
- clock_pattern-0.10.0/clock_pattern/retriers/system_retrier_async.py +154 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier.py +260 -0
- clock_pattern-0.10.0/clock_pattern/retriers/testing/mock_retrier_async.py +273 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/__init__.py +10 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/models/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/models/sleeper.py +62 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/models/sleeper_async.py +62 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/system_sleeper.py +120 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/system_sleeper_async.py +120 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/__init__.py +7 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper.py +197 -0
- clock_pattern-0.10.0/clock_pattern/sleepers/testing/mock_sleeper_async.py +195 -0
- clock_pattern-0.10.0/clock_pattern/stopwatches/__init__.py +3 -0
- clock_pattern-0.10.0/clock_pattern/stopwatches/stopwatch.py +225 -0
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/pyproject.toml +16 -12
- clock_pattern-0.7.0/PKG-INFO +0 -208
- clock_pattern-0.7.0/README.md +0 -183
- clock_pattern-0.7.0/clock_pattern/__init__.py +0 -10
- clock_pattern-0.7.0/clock_pattern/models/clock.py +0 -59
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/LICENSE.md +0 -0
- {clock_pattern-0.7.0/clock_pattern → clock_pattern-0.10.0/clock_pattern/clocks}/models/__init__.py +0 -0
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/clocks/testing/__init__.py +0 -0
- {clock_pattern-0.7.0 → clock_pattern-0.10.0}/clock_pattern/py.typed +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: clock-pattern
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: The Clock Pattern is a Python package that turns time into an injectable dependency.
|
|
5
|
+
Project-URL: Homepage, https://github.com/adriamontoto/clock-pattern
|
|
6
|
+
Project-URL: Repository, https://github.com/adriamontoto/clock-pattern
|
|
7
|
+
Project-URL: Issues, https://github.com/adriamontoto/clock-pattern/issues
|
|
8
|
+
Author: Adria Montoto
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Keywords: clock,dependency-injection,development,domain-driven-design,pattern,python,utilities
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: value-object-pattern>=1.36.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<a name="readme-top"></a>
|
|
27
|
+
|
|
28
|
+
# 🕰️ Clock Pattern
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://github.com/adriamontoto/clock-pattern/actions/workflows/ci.yaml?event=push&branch=master" target="_blank">
|
|
32
|
+
<img src="https://github.com/adriamontoto/clock-pattern/actions/workflows/ci.yaml/badge.svg?event=push&branch=master" alt="CI Pipeline">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/adriamontoto/clock-pattern" target="_blank">
|
|
35
|
+
<img src="https://coverage-badge.samuelcolvin.workers.dev/adriamontoto/clock-pattern.svg" alt="Coverage Pipeline">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://pypi.org/project/clock-pattern" target="_blank">
|
|
38
|
+
<img src="https://img.shields.io/pypi/v/clock-pattern?color=%2334D058&label=pypi%20package" alt="Package Version">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pypi.org/project/clock-pattern/" target="_blank">
|
|
41
|
+
<img src="https://img.shields.io/pypi/pyversions/clock-pattern.svg?color=%2334D058" alt="Supported Python Versions">
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://pepy.tech/projects/clock-pattern" target="_blank">
|
|
44
|
+
<img src="https://static.pepy.tech/badge/clock-pattern/month" alt="Package Downloads">
|
|
45
|
+
</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
|
+
</p>
|
|
50
|
+
|
|
51
|
+
The **Clock Pattern** is a Python 🐍 package that turns time into an injectable dependency 🧩. Instead of scattering
|
|
52
|
+
`datetime.now()` or `date.today()` through application code, domain services depend on a small `Clock` interface. That
|
|
53
|
+
keeps time-sensitive logic deterministic in tests, makes timezone choices explicit, and lets production code swap clock
|
|
54
|
+
implementations without touching business rules.
|
|
55
|
+
<br><br>
|
|
56
|
+
|
|
57
|
+
## Table of Contents
|
|
58
|
+
|
|
59
|
+
- [📥 Installation](#installation)
|
|
60
|
+
- [📚 Documentation](#documentation)
|
|
61
|
+
- [⚡ Quick Start](#quick-start)
|
|
62
|
+
- [🧩 Why Inject a Clock?](#why-inject-a-clock)
|
|
63
|
+
- [📚 Public API](#public-api)
|
|
64
|
+
- [🌍 Timezone Behavior](#timezone-behavior)
|
|
65
|
+
- [🧪 Testing Time-Sensitive Code](#testing-time-sensitive-code)
|
|
66
|
+
- [🎄 Real-Life Case: Christmas Detector Service](#real-life-case-christmas-detector-service)
|
|
67
|
+
- [🤝 Contributing](#contributing)
|
|
68
|
+
- [🔑 License](#license)
|
|
69
|
+
|
|
70
|
+
<p align="right">
|
|
71
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
72
|
+
</p><br><br>
|
|
73
|
+
|
|
74
|
+
<a name="installation"></a>
|
|
75
|
+
|
|
76
|
+
## 📥 Installation
|
|
77
|
+
|
|
78
|
+
You can install **Clock Pattern** using `pip`:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install clock-pattern
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You can install the companion AI-agent skill from [skills.sh](https://www.skills.sh/) with Vercel's `skills` CLI:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx skills add adriamontoto/clock-pattern
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Review the skill source in [`skills/clock-pattern`](skills/clock-pattern) before installing it in sensitive
|
|
91
|
+
environments.
|
|
92
|
+
|
|
93
|
+
<p align="right">
|
|
94
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
95
|
+
</p><br><br>
|
|
96
|
+
|
|
97
|
+
<a name="documentation"></a>
|
|
98
|
+
|
|
99
|
+
## 📚 Documentation
|
|
100
|
+
|
|
101
|
+
The root README is the entry point. Deeper guides live in this repository and are linked here:
|
|
102
|
+
|
|
103
|
+
- [`docs/README.md`](docs/README.md): Documentation hub.
|
|
104
|
+
- [`docs/usage/README.md`](docs/usage/README.md): Core usage patterns and service composition.
|
|
105
|
+
- [`docs/timezones/README.md`](docs/timezones/README.md): Timezone behavior, UTC defaults, and date-boundary guidance.
|
|
106
|
+
- [`docs/testing/README.md`](docs/testing/README.md): `FixedClock`, `MockClock`, and deterministic test patterns.
|
|
107
|
+
|
|
108
|
+
This [project's DeepWiki documentation](https://deepwiki.com/adriamontoto/clock-pattern) is also available for generated
|
|
109
|
+
repository navigation.
|
|
110
|
+
|
|
111
|
+
<p align="right">
|
|
112
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
113
|
+
</p><br><br>
|
|
114
|
+
|
|
115
|
+
<a name="quick-start"></a>
|
|
116
|
+
|
|
117
|
+
## ⚡ Quick Start
|
|
118
|
+
|
|
119
|
+
Inject a `Clock` into code that needs the current time. Production code can pass a real clock, while tests can pass a
|
|
120
|
+
fixed or mock clock.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from clock_pattern import Clock, UtcClock
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TimestampService:
|
|
127
|
+
def __init__(self, *, clock: Clock) -> None:
|
|
128
|
+
self._clock = clock
|
|
129
|
+
|
|
130
|
+
def issued_at(self) -> str:
|
|
131
|
+
return self._clock.now().isoformat()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
service = TimestampService(clock=UtcClock())
|
|
135
|
+
print(service.issued_at())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use [`SystemClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/system_clock.py) when
|
|
139
|
+
you need a specific timezone:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from clock_pattern import SystemClock
|
|
143
|
+
|
|
144
|
+
clock = SystemClock(timezone='Europe/Madrid')
|
|
145
|
+
print(clock.now())
|
|
146
|
+
# >>> 2025-06-16 15:57:26.210964+02:00
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
<p align="right">
|
|
150
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
151
|
+
</p><br><br>
|
|
152
|
+
|
|
153
|
+
<a name="why-inject-a-clock"></a>
|
|
154
|
+
|
|
155
|
+
## 🧩 Why Inject a Clock?
|
|
156
|
+
|
|
157
|
+
Time is global state. Reading it directly from the operating system makes behavior depend on the moment a test happens
|
|
158
|
+
to run, the machine timezone, daylight-saving transitions, and the speed of the test suite.
|
|
159
|
+
|
|
160
|
+
Clock Pattern keeps those decisions explicit:
|
|
161
|
+
|
|
162
|
+
- Domain code depends on `Clock`, not on Python's global datetime functions.
|
|
163
|
+
- Tests can choose exact dates and datetimes without monkeypatching built-in modules.
|
|
164
|
+
- Production wiring decides whether the application uses UTC or another timezone.
|
|
165
|
+
- Custom clocks can be introduced for logical time, simulation, replay, or high-precision infrastructure.
|
|
166
|
+
|
|
167
|
+
The package exposes two methods:
|
|
168
|
+
|
|
169
|
+
| Method | Returns | Typical use |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `now()` | `datetime` | Timestamps, expiration windows, and audit fields. |
|
|
172
|
+
| `today()` | `date` | Calendar rules, billing days, holiday checks, date-only decisions. |
|
|
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
|
+
|
|
178
|
+
<p align="right">
|
|
179
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
180
|
+
</p><br><br>
|
|
181
|
+
|
|
182
|
+
<a name="public-api"></a>
|
|
183
|
+
|
|
184
|
+
## 📚 Public API
|
|
185
|
+
|
|
186
|
+
Use the top-level package for contracts and production helpers, and each feature's `testing` package for test doubles.
|
|
187
|
+
|
|
188
|
+
### Wall-Clock API
|
|
189
|
+
|
|
190
|
+
| API | Import path | Purpose |
|
|
191
|
+
| --- | --- | --- |
|
|
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()`. |
|
|
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. |
|
|
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
|
+
| --- | --- | --- |
|
|
200
|
+
| `MonotonicClock` | `from clock_pattern import MonotonicClock` | Abstract contract for elapsed-time sources. |
|
|
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. |
|
|
203
|
+
| `SystemSleeper` / `SystemSleeperAsync` | `from clock_pattern import SystemSleeper, SystemSleeperAsync` | Injectable sync and async sleeping. |
|
|
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
|
+
| --- | --- | --- |
|
|
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. |
|
|
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
|
+
```
|
|
250
|
+
|
|
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.
|
|
256
|
+
|
|
257
|
+
<p align="right">
|
|
258
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
259
|
+
</p><br><br>
|
|
260
|
+
|
|
261
|
+
<a name="timezone-behavior"></a>
|
|
262
|
+
|
|
263
|
+
## 🌍 Timezone Behavior
|
|
264
|
+
|
|
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()`.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from datetime import UTC
|
|
270
|
+
|
|
271
|
+
from clock_pattern import SystemClock
|
|
272
|
+
|
|
273
|
+
utc_clock = SystemClock(timezone=UTC)
|
|
274
|
+
madrid_clock = SystemClock(timezone='Europe/Madrid')
|
|
275
|
+
|
|
276
|
+
print(utc_clock.timezone)
|
|
277
|
+
# >>> UTC
|
|
278
|
+
print(madrid_clock.timezone)
|
|
279
|
+
# >>> Europe/Madrid
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`UtcClock` is a convenience clock for the common production choice of UTC.
|
|
283
|
+
|
|
284
|
+
`today()` is calculated in the clock timezone. Around midnight, `SystemClock(timezone='UTC').today()` and
|
|
285
|
+
`SystemClock(timezone='America/New_York').today()` may return different dates. For more details, see
|
|
286
|
+
[`docs/timezones/README.md`](docs/timezones/README.md).
|
|
287
|
+
|
|
288
|
+
<p align="right">
|
|
289
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
290
|
+
</p><br><br>
|
|
291
|
+
|
|
292
|
+
<a name="testing-time-sensitive-code"></a>
|
|
293
|
+
|
|
294
|
+
## 🧪 Testing Time-Sensitive Code
|
|
295
|
+
|
|
296
|
+
Use `FixedClock` when the test only needs a stable instant:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from datetime import datetime
|
|
300
|
+
|
|
301
|
+
from clock_pattern.clocks.testing import FixedClock
|
|
302
|
+
|
|
303
|
+
clock = FixedClock(instant=datetime(year=2025, month=1, day=1, hour=10, minute=30))
|
|
304
|
+
|
|
305
|
+
assert clock.now().isoformat() == '2025-01-01T10:30:00+00:00'
|
|
306
|
+
assert clock.today().isoformat() == '2025-01-01'
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Use `MockClock` when the test also needs to prove that time was requested:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from datetime import date
|
|
313
|
+
|
|
314
|
+
from clock_pattern.clocks.testing import MockClock
|
|
315
|
+
|
|
316
|
+
clock = MockClock()
|
|
317
|
+
clock.prepare_today_method_return_value(today=date(year=2025, month=1, day=7))
|
|
318
|
+
|
|
319
|
+
assert clock.today() == date(year=2025, month=1, day=7)
|
|
320
|
+
clock.assert_today_method_was_called_once()
|
|
321
|
+
clock.assert_now_method_was_not_called()
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
More testing recipes are available in [`docs/testing/README.md`](docs/testing/README.md).
|
|
325
|
+
|
|
326
|
+
<p align="right">
|
|
327
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
328
|
+
</p><br><br>
|
|
329
|
+
|
|
330
|
+
<a name="real-life-case-christmas-detector-service"></a>
|
|
331
|
+
|
|
332
|
+
## 🎄 Real-Life Case: Christmas Detector Service
|
|
333
|
+
|
|
334
|
+
This service checks whether the current date falls within a Christmas holiday range. The service depends on `Clock`, so
|
|
335
|
+
production code can use [`UtcClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/utc_clock.py)
|
|
336
|
+
and tests can use [`MockClock`](https://github.com/adriamontoto/clock-pattern/blob/master/clock_pattern/clocks/testing/mock_clock.py)
|
|
337
|
+
without changing the service.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from datetime import date
|
|
341
|
+
|
|
342
|
+
from clock_pattern import Clock, UtcClock
|
|
343
|
+
from clock_pattern.clocks.testing import MockClock
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ChristmasDetectorService:
|
|
347
|
+
def __init__(self, *, clock: Clock) -> None:
|
|
348
|
+
self._clock = clock
|
|
349
|
+
self._christmas_start = date(year=2024, month=12, day=24)
|
|
350
|
+
self._christmas_end = date(year=2025, month=1, day=6)
|
|
351
|
+
|
|
352
|
+
def is_christmas(self) -> bool:
|
|
353
|
+
return self._christmas_start <= self._clock.today() <= self._christmas_end
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
clock = UtcClock()
|
|
357
|
+
service = ChristmasDetectorService(clock=clock)
|
|
358
|
+
|
|
359
|
+
print(service.is_christmas())
|
|
360
|
+
# >>> False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_christmas_detector_is_christmas() -> None:
|
|
364
|
+
clock = MockClock()
|
|
365
|
+
service = ChristmasDetectorService(clock=clock)
|
|
366
|
+
|
|
367
|
+
today = date(year=2024, month=12, day=25)
|
|
368
|
+
clock.prepare_today_method_return_value(today=today)
|
|
369
|
+
|
|
370
|
+
assert service.is_christmas() is True
|
|
371
|
+
clock.assert_today_method_was_called_once()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_christmas_detector_is_not_christmas() -> None:
|
|
375
|
+
clock = MockClock()
|
|
376
|
+
service = ChristmasDetectorService(clock=clock)
|
|
377
|
+
|
|
378
|
+
today = date(year=2025, month=1, day=7)
|
|
379
|
+
clock.prepare_today_method_return_value(today=today)
|
|
380
|
+
|
|
381
|
+
assert service.is_christmas() is False
|
|
382
|
+
clock.assert_today_method_was_called_once()
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
<p align="right">
|
|
386
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
387
|
+
</p><br><br>
|
|
388
|
+
|
|
389
|
+
<a name="contributing"></a>
|
|
390
|
+
|
|
391
|
+
## 🤝 Contributing
|
|
392
|
+
|
|
393
|
+
We love community help! Before you open an issue or pull request, please read:
|
|
394
|
+
|
|
395
|
+
- [`🤝 How to Contribute`](https://github.com/adriamontoto/clock-pattern/blob/master/.github/CONTRIBUTING.md)
|
|
396
|
+
- [`🧭 Code of Conduct`](https://github.com/adriamontoto/clock-pattern/blob/master/.github/CODE_OF_CONDUCT.md)
|
|
397
|
+
- [`🔐 Security Policy`](https://github.com/adriamontoto/clock-pattern/blob/master/.github/SECURITY.md)
|
|
398
|
+
|
|
399
|
+
_Thank you for helping make **🕰️ Clock Pattern** package awesome! 🌟_
|
|
400
|
+
|
|
401
|
+
<p align="right">
|
|
402
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
403
|
+
</p><br><br>
|
|
404
|
+
|
|
405
|
+
<a name="license"></a>
|
|
406
|
+
|
|
407
|
+
## 🔑 License
|
|
408
|
+
|
|
409
|
+
This project is licensed under the terms of the [`MIT license`](https://github.com/adriamontoto/clock-pattern/blob/master/LICENSE.md).
|
|
410
|
+
|
|
411
|
+
<p align="right">
|
|
412
|
+
<a href="#readme-top">🔼 Back to top</a>
|
|
413
|
+
</p>
|