typed-time-provider 0.1.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.
- typed_time_provider-0.1.0/LICENSE +9 -0
- typed_time_provider-0.1.0/PKG-INFO +645 -0
- typed_time_provider-0.1.0/README.md +595 -0
- typed_time_provider-0.1.0/pyproject.toml +144 -0
- typed_time_provider-0.1.0/setup.cfg +4 -0
- typed_time_provider-0.1.0/src/typed_time_provider/__init__.py +68 -0
- typed_time_provider-0.1.0/src/typed_time_provider/conversion/__init__.py +25 -0
- typed_time_provider-0.1.0/src/typed_time_provider/conversion/_datetime_conversion.py +40 -0
- typed_time_provider-0.1.0/src/typed_time_provider/conversion/_time_unit_conversion.py +129 -0
- typed_time_provider-0.1.0/src/typed_time_provider/conversion/_time_unit_resolution.py +129 -0
- typed_time_provider-0.1.0/src/typed_time_provider/conversion/_timezone_conversion.py +45 -0
- typed_time_provider-0.1.0/src/typed_time_provider/monotonic_clock/__init__.py +9 -0
- typed_time_provider-0.1.0/src/typed_time_provider/monotonic_clock/_monotonic_clock.py +545 -0
- typed_time_provider-0.1.0/src/typed_time_provider/monotonic_clock/_monotonic_clock_interface.py +223 -0
- typed_time_provider-0.1.0/src/typed_time_provider/py.typed +0 -0
- typed_time_provider-0.1.0/src/typed_time_provider/runtime_contract_validation/__init__.py +11 -0
- typed_time_provider-0.1.0/src/typed_time_provider/runtime_contract_validation/_runtime_contract_validator.py +99 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/__init__.py +45 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/_constants.py +18 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/_exceptions.py +22 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/_helpers.py +100 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/_validation.py +69 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/types/__init__.py +61 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/types/_formatted_timestamps.py +29 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/types/_generic_time_units.py +25 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/types/_time_precision.py +49 -0
- typed_time_provider-0.1.0/src/typed_time_provider/shared/types/_time_units.py +65 -0
- typed_time_provider-0.1.0/src/typed_time_provider/time_formatter/__init__.py +9 -0
- typed_time_provider-0.1.0/src/typed_time_provider/time_formatter/_time_formatter.py +241 -0
- typed_time_provider-0.1.0/src/typed_time_provider/time_formatter/_time_formatter_interface.py +111 -0
- typed_time_provider-0.1.0/src/typed_time_provider/wall_clock/__init__.py +7 -0
- typed_time_provider-0.1.0/src/typed_time_provider/wall_clock/_wall_clock.py +266 -0
- typed_time_provider-0.1.0/src/typed_time_provider/wall_clock/_wall_clock_interface.py +124 -0
- typed_time_provider-0.1.0/src/typed_time_provider.egg-info/PKG-INFO +645 -0
- typed_time_provider-0.1.0/src/typed_time_provider.egg-info/SOURCES.txt +43 -0
- typed_time_provider-0.1.0/src/typed_time_provider.egg-info/dependency_links.txt +1 -0
- typed_time_provider-0.1.0/src/typed_time_provider.egg-info/requires.txt +30 -0
- typed_time_provider-0.1.0/src/typed_time_provider.egg-info/top_level.txt +1 -0
- typed_time_provider-0.1.0/tests/test_exception_hierarchy_and_types.py +100 -0
- typed_time_provider-0.1.0/tests/test_monotonic_clock.py +248 -0
- typed_time_provider-0.1.0/tests/test_public_contracts.py +155 -0
- typed_time_provider-0.1.0/tests/test_runtime_contract_validator.py +93 -0
- typed_time_provider-0.1.0/tests/test_time_formatter_and_datetime.py +261 -0
- typed_time_provider-0.1.0/tests/test_time_unit_conversion.py +230 -0
- typed_time_provider-0.1.0/tests/test_wall_clock.py +127 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eldeniz Guseinli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typed-time-provider
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DI-friendly wall-clock and monotonic clock providers with typed time units.
|
|
5
|
+
Author-email: Eldeniz Guseinli <eldenizfamilyanskicode@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/eldenizfamilyanskicode/typed-time-provider
|
|
8
|
+
Project-URL: Repository, https://github.com/eldenizfamilyanskicode/typed-time-provider
|
|
9
|
+
Project-URL: Issues, https://github.com/eldenizfamilyanskicode/typed-time-provider/issues
|
|
10
|
+
Keywords: time,timestamp,typed-int,dependency-injection,domain-model,pydantic
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: base-typed-int<1,>=0.1.1
|
|
25
|
+
Requires-Dist: base-typed-string<1,>=0.1.2
|
|
26
|
+
Requires-Dist: tzdata>=2024.1
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
30
|
+
Requires-Dist: pydantic<3,>=2.6; extra == "test"
|
|
31
|
+
Provides-Extra: lint
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == "lint"
|
|
33
|
+
Provides-Extra: typecheck
|
|
34
|
+
Requires-Dist: mypy>=1.10; extra == "typecheck"
|
|
35
|
+
Requires-Dist: pyright>=1.1; extra == "typecheck"
|
|
36
|
+
Requires-Dist: pydantic<3,>=2.6; extra == "typecheck"
|
|
37
|
+
Provides-Extra: build
|
|
38
|
+
Requires-Dist: build>=1.2; extra == "build"
|
|
39
|
+
Requires-Dist: twine>=5.1; extra == "build"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
42
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
43
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
44
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
47
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
48
|
+
Requires-Dist: pydantic<3,>=2.6; extra == "dev"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# typed-time-provider
|
|
52
|
+
|
|
53
|
+
DI-friendly wall-clock and monotonic clock providers with typed time units.
|
|
54
|
+
|
|
55
|
+
`typed-time-provider` is a small Python package for applications that need explicit, typed, testable time handling.
|
|
56
|
+
|
|
57
|
+
It separates three different responsibilities:
|
|
58
|
+
|
|
59
|
+
1. capturing wall-clock UNIX timestamps
|
|
60
|
+
2. measuring monotonic elapsed time and deadlines
|
|
61
|
+
3. formatting UNIX timestamps into human-readable strings
|
|
62
|
+
|
|
63
|
+
The package is designed for dependency injection, strict typing, deterministic tests, and domain models that should not pass raw `int` timestamps everywhere.
|
|
64
|
+
|
|
65
|
+
## Status
|
|
66
|
+
|
|
67
|
+
Alpha.
|
|
68
|
+
|
|
69
|
+
The public API is intentionally small, but the package is already covered by strict linting, type checking, and tests.
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
- Typed time units:
|
|
74
|
+
- `Hours`
|
|
75
|
+
- `Minutes`
|
|
76
|
+
- `Seconds`
|
|
77
|
+
- `Milliseconds`
|
|
78
|
+
- `Microseconds`
|
|
79
|
+
- `Nanoseconds`
|
|
80
|
+
- Semantic time unit subclasses, for example:
|
|
81
|
+
- `UnixTimestampMicroseconds`
|
|
82
|
+
- `DurationMilliseconds`
|
|
83
|
+
- `MonotonicDeadlineMilliseconds`
|
|
84
|
+
- Wall-clock provider for UNIX timestamps
|
|
85
|
+
- Monotonic clock provider for elapsed time and deadline checks
|
|
86
|
+
- Timestamp formatter with explicit timezone and precision handling
|
|
87
|
+
- Protocol interfaces for dependency injection
|
|
88
|
+
- Runtime contract validator for application composition boundaries
|
|
89
|
+
- `py.typed` marker for typed package consumers
|
|
90
|
+
- Strict `mypy` and `pyright` compatible public API
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m pip install typed-time-provider
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For local development:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python -m pip install -e ".[dev]"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
Python `>=3.10`.
|
|
107
|
+
|
|
108
|
+
Runtime dependencies:
|
|
109
|
+
|
|
110
|
+
- `base-typed-int`
|
|
111
|
+
- `base-typed-string`
|
|
112
|
+
- `tzdata`
|
|
113
|
+
|
|
114
|
+
## Quick start
|
|
115
|
+
|
|
116
|
+
### Capture a UNIX timestamp
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from __future__ import annotations
|
|
120
|
+
|
|
121
|
+
from typed_time_provider import Microseconds, Nanoseconds, Seconds, WallClock
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> None:
|
|
125
|
+
wall_clock: WallClock[Microseconds] = WallClock(
|
|
126
|
+
preferred_time_unit_type=Microseconds,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
current_unix_microseconds: Microseconds = wall_clock.now_unix()
|
|
130
|
+
|
|
131
|
+
current_unix_seconds: Seconds = wall_clock.now_unix(
|
|
132
|
+
return_type=Seconds,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
current_unix_nanoseconds: Nanoseconds = wall_clock.now_unix(
|
|
136
|
+
return_type=Nanoseconds,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
print(current_unix_microseconds)
|
|
140
|
+
print(current_unix_seconds)
|
|
141
|
+
print(current_unix_nanoseconds)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
main()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Wall-clock time
|
|
149
|
+
|
|
150
|
+
`WallClock` is for UNIX timestamps only.
|
|
151
|
+
|
|
152
|
+
Use it when you need to store or send a real-world timestamp, for example:
|
|
153
|
+
|
|
154
|
+
- database `created_at`
|
|
155
|
+
- audit events
|
|
156
|
+
- API response timestamps
|
|
157
|
+
- domain event timestamps
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from __future__ import annotations
|
|
161
|
+
|
|
162
|
+
from typed_time_provider import Microseconds, Seconds, WallClock
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def fixed_unix_nanosecond_factory() -> int:
|
|
166
|
+
return 1_775_000_000_123_456_789
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main() -> None:
|
|
170
|
+
wall_clock: WallClock[Microseconds] = WallClock(
|
|
171
|
+
preferred_time_unit_type=Microseconds,
|
|
172
|
+
unix_nanosecond_factory=fixed_unix_nanosecond_factory,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
current_unix_microseconds: Microseconds = wall_clock.now_unix()
|
|
176
|
+
|
|
177
|
+
shifted_unix_seconds: Seconds = wall_clock.now_unix_with_delta(
|
|
178
|
+
delta_time=Seconds(30),
|
|
179
|
+
return_type=Seconds,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
print(current_unix_microseconds)
|
|
183
|
+
print(shifted_unix_seconds)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Monotonic time
|
|
191
|
+
|
|
192
|
+
`MonotonicClock` is for durations, elapsed time, and deadlines.
|
|
193
|
+
|
|
194
|
+
Monotonic values are not UNIX timestamps. Do not store them as real-world time and do not format them as calendar time.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from __future__ import annotations
|
|
198
|
+
|
|
199
|
+
from typed_time_provider import Milliseconds, MonotonicClock, Nanoseconds
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main() -> None:
|
|
203
|
+
monotonic_clock: MonotonicClock[Milliseconds] = MonotonicClock(
|
|
204
|
+
preferred_time_unit_type=Milliseconds,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
started_at_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic(
|
|
208
|
+
return_type=Nanoseconds,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
elapsed_milliseconds: Milliseconds = monotonic_clock.elapsed_since(
|
|
212
|
+
started_at=started_at_nanoseconds,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
has_elapsed: bool = monotonic_clock.has_elapsed_since(
|
|
216
|
+
started_at=started_at_nanoseconds,
|
|
217
|
+
elapsed_time=Milliseconds(500),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
deadline_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic_with_delta(
|
|
221
|
+
delta_time=Milliseconds(250),
|
|
222
|
+
return_type=Nanoseconds,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
is_deadline_reached: bool = monotonic_clock.is_deadline_reached(
|
|
226
|
+
deadline_time=deadline_nanoseconds,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
print(elapsed_milliseconds)
|
|
230
|
+
print(has_elapsed)
|
|
231
|
+
print(deadline_nanoseconds)
|
|
232
|
+
print(is_deadline_reached)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Timestamp formatting
|
|
240
|
+
|
|
241
|
+
`TimeFormatter` converts already captured UNIX timestamps into human-readable strings.
|
|
242
|
+
|
|
243
|
+
It does not read system time.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from __future__ import annotations
|
|
247
|
+
|
|
248
|
+
from typed_time_provider import (
|
|
249
|
+
NanosecondPrecisionFormattedTimestamp,
|
|
250
|
+
Nanoseconds,
|
|
251
|
+
SecondPrecisionFormattedTimestamp,
|
|
252
|
+
Seconds,
|
|
253
|
+
TimeFormatter,
|
|
254
|
+
TimePrecision,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main() -> None:
|
|
259
|
+
nanosecond_formatter: TimeFormatter[NanosecondPrecisionFormattedTimestamp]
|
|
260
|
+
nanosecond_formatter = TimeFormatter(
|
|
261
|
+
default_time_precision=TimePrecision.NANOSECOND,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
second_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp]
|
|
265
|
+
second_formatter = TimeFormatter(
|
|
266
|
+
default_time_precision=TimePrecision.SECOND,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
berlin_timestamp: NanosecondPrecisionFormattedTimestamp
|
|
270
|
+
berlin_timestamp = nanosecond_formatter.format_unix_to_human(
|
|
271
|
+
unix_timestamp=Nanoseconds(1_775_000_000_123_456_789),
|
|
272
|
+
user_timezone_name="Europe/Berlin",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
utc_timestamp: SecondPrecisionFormattedTimestamp
|
|
276
|
+
utc_timestamp = second_formatter.format_unix_to_human(
|
|
277
|
+
unix_timestamp=Seconds(1_775_000_000),
|
|
278
|
+
user_timezone_name=None,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
print(berlin_timestamp)
|
|
282
|
+
print(utc_timestamp)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`user_timezone_name=None` means UTC.
|
|
290
|
+
|
|
291
|
+
Timezone names must be valid IANA timezone names, for example:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
"UTC"
|
|
295
|
+
"Europe/Berlin"
|
|
296
|
+
"Asia/Baku"
|
|
297
|
+
"America/New_York"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Formatting precision
|
|
301
|
+
|
|
302
|
+
Supported formatting precision values:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from typed_time_provider import TimePrecision
|
|
306
|
+
|
|
307
|
+
TimePrecision.HOUR
|
|
308
|
+
TimePrecision.MINUTE
|
|
309
|
+
TimePrecision.SECOND
|
|
310
|
+
TimePrecision.MILLISECOND
|
|
311
|
+
TimePrecision.MICROSECOND
|
|
312
|
+
TimePrecision.NANOSECOND
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Example output formats:
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
2026-04-30 14 Europe/Berlin
|
|
319
|
+
2026-04-30 14:32 Europe/Berlin
|
|
320
|
+
2026-04-30 14:32:45 Europe/Berlin
|
|
321
|
+
2026-04-30 14:32:45.123 Europe/Berlin
|
|
322
|
+
2026-04-30 14:32:45.123456 Europe/Berlin
|
|
323
|
+
2026-04-30 14:32:45.123456789 Europe/Berlin
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Dependency injection
|
|
327
|
+
|
|
328
|
+
Application code should usually depend on protocols, not concrete classes.
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from __future__ import annotations
|
|
332
|
+
|
|
333
|
+
from dataclasses import dataclass
|
|
334
|
+
|
|
335
|
+
from typed_time_provider import (
|
|
336
|
+
Microseconds,
|
|
337
|
+
SecondPrecisionFormattedTimestamp,
|
|
338
|
+
TimeFormatter,
|
|
339
|
+
TimeFormatterInterface,
|
|
340
|
+
TimePrecision,
|
|
341
|
+
WallClock,
|
|
342
|
+
WallClockInterface,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dataclass(frozen=True, slots=True)
|
|
347
|
+
class AuditEvent:
|
|
348
|
+
event_name: str
|
|
349
|
+
created_at_unix_microseconds: Microseconds
|
|
350
|
+
created_at_human: SecondPrecisionFormattedTimestamp
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class AuditEventFactory:
|
|
354
|
+
def __init__(
|
|
355
|
+
self,
|
|
356
|
+
wall_clock: WallClockInterface[Microseconds],
|
|
357
|
+
time_formatter: TimeFormatterInterface[SecondPrecisionFormattedTimestamp],
|
|
358
|
+
default_timezone_name: str | None,
|
|
359
|
+
) -> None:
|
|
360
|
+
self._wall_clock: WallClockInterface[Microseconds] = wall_clock
|
|
361
|
+
self._time_formatter: TimeFormatterInterface[
|
|
362
|
+
SecondPrecisionFormattedTimestamp
|
|
363
|
+
] = time_formatter
|
|
364
|
+
self._default_timezone_name: str | None = default_timezone_name
|
|
365
|
+
|
|
366
|
+
def create_audit_event(
|
|
367
|
+
self,
|
|
368
|
+
event_name: str,
|
|
369
|
+
) -> AuditEvent:
|
|
370
|
+
created_at_unix_microseconds: Microseconds = self._wall_clock.now_unix()
|
|
371
|
+
|
|
372
|
+
created_at_human: SecondPrecisionFormattedTimestamp
|
|
373
|
+
created_at_human = self._time_formatter.format_unix_to_human(
|
|
374
|
+
unix_timestamp=created_at_unix_microseconds,
|
|
375
|
+
user_timezone_name=self._default_timezone_name,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
audit_event: AuditEvent = AuditEvent(
|
|
379
|
+
event_name=event_name,
|
|
380
|
+
created_at_unix_microseconds=created_at_unix_microseconds,
|
|
381
|
+
created_at_human=created_at_human,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return audit_event
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def build_audit_event_factory() -> AuditEventFactory:
|
|
388
|
+
wall_clock: WallClock[Microseconds] = WallClock(
|
|
389
|
+
preferred_time_unit_type=Microseconds,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
time_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp] = TimeFormatter(
|
|
393
|
+
default_time_precision=TimePrecision.SECOND,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
audit_event_factory: AuditEventFactory = AuditEventFactory(
|
|
397
|
+
wall_clock=wall_clock,
|
|
398
|
+
time_formatter=time_formatter,
|
|
399
|
+
default_timezone_name="UTC",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return audit_event_factory
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Semantic time units
|
|
406
|
+
|
|
407
|
+
You can create semantic subclasses for stronger domain meaning.
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from __future__ import annotations
|
|
411
|
+
|
|
412
|
+
from typed_time_provider import Microseconds, Milliseconds, WallClock
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class UnixTimestampMicroseconds(Microseconds):
|
|
416
|
+
"""UNIX timestamp stored with microsecond precision."""
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class DurationMilliseconds(Milliseconds):
|
|
420
|
+
"""Duration stored with millisecond precision."""
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def main() -> None:
|
|
424
|
+
wall_clock: WallClock[UnixTimestampMicroseconds] = WallClock(
|
|
425
|
+
preferred_time_unit_type=UnixTimestampMicroseconds,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
created_at: UnixTimestampMicroseconds = wall_clock.now_unix()
|
|
429
|
+
|
|
430
|
+
retry_delay: DurationMilliseconds = DurationMilliseconds(500)
|
|
431
|
+
|
|
432
|
+
print(created_at)
|
|
433
|
+
print(retry_delay)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
main()
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Semantic subclasses are preserved when possible.
|
|
441
|
+
|
|
442
|
+
## Runtime contract validation
|
|
443
|
+
|
|
444
|
+
Constructors keep injected factories lightweight and side-effect free.
|
|
445
|
+
|
|
446
|
+
Use `TypedTimeProviderRuntimeContractValidator` at composition boundaries when invalid wiring should fail fast during startup.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
from __future__ import annotations
|
|
450
|
+
|
|
451
|
+
from typed_time_provider import (
|
|
452
|
+
Microseconds,
|
|
453
|
+
TimePrecision,
|
|
454
|
+
TypedTimeProviderRuntimeContractValidator,
|
|
455
|
+
TimeFormatter,
|
|
456
|
+
WallClock,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def unix_nanosecond_factory() -> int:
|
|
461
|
+
return 1_775_000_000_123_456_789
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def main() -> None:
|
|
465
|
+
wall_clock: WallClock[Microseconds] = WallClock(
|
|
466
|
+
preferred_time_unit_type=Microseconds,
|
|
467
|
+
unix_nanosecond_factory=unix_nanosecond_factory,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
time_formatter: TimeFormatter = TimeFormatter(
|
|
471
|
+
default_time_precision=TimePrecision.SECOND,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
runtime_contract_validator: TypedTimeProviderRuntimeContractValidator
|
|
475
|
+
runtime_contract_validator = TypedTimeProviderRuntimeContractValidator()
|
|
476
|
+
|
|
477
|
+
runtime_contract_validator.validate_wall_clock(
|
|
478
|
+
wall_clock=wall_clock,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
runtime_contract_validator.validate_time_formatter(
|
|
482
|
+
time_formatter=time_formatter,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
main()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Public API
|
|
491
|
+
|
|
492
|
+
Main imports are available from the root package:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from typed_time_provider import (
|
|
496
|
+
BaseFormattedTimestamp,
|
|
497
|
+
BaseTimeUnit,
|
|
498
|
+
HourPrecisionFormattedTimestamp,
|
|
499
|
+
Hours,
|
|
500
|
+
MicrosecondPrecisionFormattedTimestamp,
|
|
501
|
+
Microseconds,
|
|
502
|
+
MillisecondPrecisionFormattedTimestamp,
|
|
503
|
+
Milliseconds,
|
|
504
|
+
MinutePrecisionFormattedTimestamp,
|
|
505
|
+
Minutes,
|
|
506
|
+
MonotonicClock,
|
|
507
|
+
MonotonicClockInterface,
|
|
508
|
+
NanosecondPrecisionFormattedTimestamp,
|
|
509
|
+
Nanoseconds,
|
|
510
|
+
SecondPrecisionFormattedTimestamp,
|
|
511
|
+
Seconds,
|
|
512
|
+
TimeFormatter,
|
|
513
|
+
TimeFormatterInterface,
|
|
514
|
+
TimePrecision,
|
|
515
|
+
TypedTimeProviderError,
|
|
516
|
+
TypedTimeProviderInvalidInputTypeError,
|
|
517
|
+
TypedTimeProviderInvalidInputValueError,
|
|
518
|
+
TypedTimeProviderInvariantViolationError,
|
|
519
|
+
TypedTimeProviderRuntimeContractValidator,
|
|
520
|
+
TimeUnitType,
|
|
521
|
+
TimeUnitValue,
|
|
522
|
+
WallClock,
|
|
523
|
+
WallClockInterface,
|
|
524
|
+
)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Design rules
|
|
528
|
+
|
|
529
|
+
### Wall-clock and monotonic time are separate
|
|
530
|
+
|
|
531
|
+
Use `WallClock` for real-world UNIX timestamps.
|
|
532
|
+
|
|
533
|
+
Use `MonotonicClock` for elapsed time, deadlines, and duration checks.
|
|
534
|
+
|
|
535
|
+
This avoids mixing two different time domains.
|
|
536
|
+
|
|
537
|
+
### Formatting is separate from capturing time
|
|
538
|
+
|
|
539
|
+
`TimeFormatter` does not call `time.time_ns()`.
|
|
540
|
+
|
|
541
|
+
This keeps formatting deterministic and easy to test.
|
|
542
|
+
|
|
543
|
+
### Nanoseconds are the internal canonical unit
|
|
544
|
+
|
|
545
|
+
Conversions use nanoseconds internally and return the requested typed unit at API boundaries.
|
|
546
|
+
|
|
547
|
+
### Lower precision conversion truncates
|
|
548
|
+
|
|
549
|
+
When converting to a lower precision unit, fractional lower-level units are truncated by integer division.
|
|
550
|
+
|
|
551
|
+
Example:
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
from typed_time_provider import Microseconds, Nanoseconds
|
|
555
|
+
from typed_time_provider.conversion import convert_nanoseconds_to_time_unit
|
|
556
|
+
|
|
557
|
+
converted_microseconds: Microseconds = convert_nanoseconds_to_time_unit(
|
|
558
|
+
nanoseconds=Nanoseconds(2_500),
|
|
559
|
+
time_unit_type=Microseconds,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
assert converted_microseconds == Microseconds(2)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Error hierarchy
|
|
566
|
+
|
|
567
|
+
All package-specific exceptions inherit from `TypedTimeProviderError`.
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
TypedTimeProviderError
|
|
571
|
+
├── TypedTimeProviderInvalidInputTypeError
|
|
572
|
+
├── TypedTimeProviderInvalidInputValueError
|
|
573
|
+
└── TypedTimeProviderInvariantViolationError
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Development
|
|
577
|
+
|
|
578
|
+
Install development dependencies:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
python -m pip install -e ".[dev]"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Run linting:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
ruff check .
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Run tests:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
pytest
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Run mypy:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
mypy
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Run pyright:
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
pyright
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Build package:
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
python -m build
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Check package metadata:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
python -m twine check dist/*
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Upload to PyPI:
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
python -m twine upload dist/*
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
## Examples
|
|
627
|
+
|
|
628
|
+
Example scripts are available in the `examples` package.
|
|
629
|
+
|
|
630
|
+
Run from project root after installing the package in editable mode:
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
python -m examples.basic_wall_clock_usage
|
|
634
|
+
python -m examples.basic_time_formatter_usage
|
|
635
|
+
python -m examples.basic_monotonic_clock_usage
|
|
636
|
+
python -m examples.dependency_injection_usage
|
|
637
|
+
python -m examples.runtime_contract_validation_usage
|
|
638
|
+
python -m examples.semantic_time_units_usage
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## License
|
|
642
|
+
|
|
643
|
+
MIT License.
|
|
644
|
+
|
|
645
|
+
See `LICENSE` for details.
|