DI-friendly wall-clock and monotonic clock providers with typed time units.
Project description
typed-time-provider
DI-friendly wall-clock and monotonic clock providers with typed time units.
typed-time-provider is a small Python package for applications that need explicit, typed, testable time handling.
It separates three different responsibilities:
- capturing wall-clock UNIX timestamps
- measuring monotonic elapsed time and deadlines
- formatting UNIX timestamps into human-readable strings
The package is designed for dependency injection, strict typing, deterministic tests, and domain models that should not pass raw int timestamps everywhere.
Status
Alpha.
The public API is intentionally small, but the package is already covered by strict linting, type checking, and tests.
Features
- Typed time units:
HoursMinutesSecondsMillisecondsMicrosecondsNanoseconds
- Semantic time unit subclasses, for example:
UnixTimestampMicrosecondsDurationMillisecondsMonotonicDeadlineMilliseconds
- Wall-clock provider for UNIX timestamps
- Monotonic clock provider for elapsed time and deadline checks
- Timestamp formatter with explicit timezone and precision handling
- Protocol interfaces for dependency injection
- Runtime contract validator for application composition boundaries
py.typedmarker for typed package consumers- Strict
mypyandpyrightcompatible public API
Installation
python -m pip install typed-time-provider
For local development:
python -m pip install -e ".[dev]"
Requirements
Python >=3.10.
Runtime dependencies:
base-typed-intbase-typed-stringtzdata
Quick start
Capture a UNIX timestamp
from __future__ import annotations
from typed_time_provider import Microseconds, Nanoseconds, Seconds, WallClock
def main() -> None:
wall_clock: WallClock[Microseconds] = WallClock(
preferred_time_unit_type=Microseconds,
)
current_unix_microseconds: Microseconds = wall_clock.now_unix()
current_unix_seconds: Seconds = wall_clock.now_unix(
return_type=Seconds,
)
current_unix_nanoseconds: Nanoseconds = wall_clock.now_unix(
return_type=Nanoseconds,
)
print(current_unix_microseconds)
print(current_unix_seconds)
print(current_unix_nanoseconds)
if __name__ == "__main__":
main()
Wall-clock time
WallClock is for UNIX timestamps only.
Use it when you need to store or send a real-world timestamp, for example:
- database
created_at - audit events
- API response timestamps
- domain event timestamps
from __future__ import annotations
from typed_time_provider import Microseconds, Seconds, WallClock
def fixed_unix_nanosecond_factory() -> int:
return 1_775_000_000_123_456_789
def main() -> None:
wall_clock: WallClock[Microseconds] = WallClock(
preferred_time_unit_type=Microseconds,
unix_nanosecond_factory=fixed_unix_nanosecond_factory,
)
current_unix_microseconds: Microseconds = wall_clock.now_unix()
shifted_unix_seconds: Seconds = wall_clock.now_unix_with_delta(
delta_time=Seconds(30),
return_type=Seconds,
)
print(current_unix_microseconds)
print(shifted_unix_seconds)
if __name__ == "__main__":
main()
Monotonic time
MonotonicClock is for durations, elapsed time, and deadlines.
Monotonic values are not UNIX timestamps. Do not store them as real-world time and do not format them as calendar time.
from __future__ import annotations
from typed_time_provider import Milliseconds, MonotonicClock, Nanoseconds
def main() -> None:
monotonic_clock: MonotonicClock[Milliseconds] = MonotonicClock(
preferred_time_unit_type=Milliseconds,
)
started_at_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic(
return_type=Nanoseconds,
)
elapsed_milliseconds: Milliseconds = monotonic_clock.elapsed_since(
started_at=started_at_nanoseconds,
)
has_elapsed: bool = monotonic_clock.has_elapsed_since(
started_at=started_at_nanoseconds,
elapsed_time=Milliseconds(500),
)
deadline_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic_with_delta(
delta_time=Milliseconds(250),
return_type=Nanoseconds,
)
is_deadline_reached: bool = monotonic_clock.is_deadline_reached(
deadline_time=deadline_nanoseconds,
)
print(elapsed_milliseconds)
print(has_elapsed)
print(deadline_nanoseconds)
print(is_deadline_reached)
if __name__ == "__main__":
main()
Timestamp formatting
TimeFormatter converts already captured UNIX timestamps into human-readable strings.
It does not read system time.
from __future__ import annotations
from typed_time_provider import (
NanosecondPrecisionFormattedTimestamp,
Nanoseconds,
SecondPrecisionFormattedTimestamp,
Seconds,
TimeFormatter,
TimePrecision,
)
def main() -> None:
nanosecond_formatter: TimeFormatter[NanosecondPrecisionFormattedTimestamp]
nanosecond_formatter = TimeFormatter(
default_time_precision=TimePrecision.NANOSECOND,
)
second_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp]
second_formatter = TimeFormatter(
default_time_precision=TimePrecision.SECOND,
)
berlin_timestamp: NanosecondPrecisionFormattedTimestamp
berlin_timestamp = nanosecond_formatter.format_unix_to_human(
unix_timestamp=Nanoseconds(1_775_000_000_123_456_789),
user_timezone_name="Europe/Berlin",
)
utc_timestamp: SecondPrecisionFormattedTimestamp
utc_timestamp = second_formatter.format_unix_to_human(
unix_timestamp=Seconds(1_775_000_000),
user_timezone_name=None,
)
print(berlin_timestamp)
print(utc_timestamp)
if __name__ == "__main__":
main()
user_timezone_name=None means UTC.
Timezone names must be valid IANA timezone names, for example:
"UTC"
"Europe/Berlin"
"Asia/Baku"
"America/New_York"
Formatting precision
Supported formatting precision values:
from typed_time_provider import TimePrecision
TimePrecision.HOUR
TimePrecision.MINUTE
TimePrecision.SECOND
TimePrecision.MILLISECOND
TimePrecision.MICROSECOND
TimePrecision.NANOSECOND
Example output formats:
2026-04-30 14 Europe/Berlin
2026-04-30 14:32 Europe/Berlin
2026-04-30 14:32:45 Europe/Berlin
2026-04-30 14:32:45.123 Europe/Berlin
2026-04-30 14:32:45.123456 Europe/Berlin
2026-04-30 14:32:45.123456789 Europe/Berlin
Dependency injection
Application code should usually depend on protocols, not concrete classes.
from __future__ import annotations
from dataclasses import dataclass
from typed_time_provider import (
Microseconds,
SecondPrecisionFormattedTimestamp,
TimeFormatter,
TimeFormatterInterface,
TimePrecision,
WallClock,
WallClockInterface,
)
@dataclass(frozen=True, slots=True)
class AuditEvent:
event_name: str
created_at_unix_microseconds: Microseconds
created_at_human: SecondPrecisionFormattedTimestamp
class AuditEventFactory:
def __init__(
self,
wall_clock: WallClockInterface[Microseconds],
time_formatter: TimeFormatterInterface[SecondPrecisionFormattedTimestamp],
default_timezone_name: str | None,
) -> None:
self._wall_clock: WallClockInterface[Microseconds] = wall_clock
self._time_formatter: TimeFormatterInterface[
SecondPrecisionFormattedTimestamp
] = time_formatter
self._default_timezone_name: str | None = default_timezone_name
def create_audit_event(
self,
event_name: str,
) -> AuditEvent:
created_at_unix_microseconds: Microseconds = self._wall_clock.now_unix()
created_at_human: SecondPrecisionFormattedTimestamp
created_at_human = self._time_formatter.format_unix_to_human(
unix_timestamp=created_at_unix_microseconds,
user_timezone_name=self._default_timezone_name,
)
audit_event: AuditEvent = AuditEvent(
event_name=event_name,
created_at_unix_microseconds=created_at_unix_microseconds,
created_at_human=created_at_human,
)
return audit_event
def build_audit_event_factory() -> AuditEventFactory:
wall_clock: WallClock[Microseconds] = WallClock(
preferred_time_unit_type=Microseconds,
)
time_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp] = TimeFormatter(
default_time_precision=TimePrecision.SECOND,
)
audit_event_factory: AuditEventFactory = AuditEventFactory(
wall_clock=wall_clock,
time_formatter=time_formatter,
default_timezone_name="UTC",
)
return audit_event_factory
Semantic time units
You can create semantic subclasses for stronger domain meaning.
from __future__ import annotations
from typed_time_provider import Microseconds, Milliseconds, WallClock
class UnixTimestampMicroseconds(Microseconds):
"""UNIX timestamp stored with microsecond precision."""
class DurationMilliseconds(Milliseconds):
"""Duration stored with millisecond precision."""
def main() -> None:
wall_clock: WallClock[UnixTimestampMicroseconds] = WallClock(
preferred_time_unit_type=UnixTimestampMicroseconds,
)
created_at: UnixTimestampMicroseconds = wall_clock.now_unix()
retry_delay: DurationMilliseconds = DurationMilliseconds(500)
print(created_at)
print(retry_delay)
if __name__ == "__main__":
main()
Semantic subclasses are preserved when possible.
Runtime contract validation
Constructors keep injected factories lightweight and side-effect free.
Use TypedTimeProviderRuntimeContractValidator at composition boundaries when invalid wiring should fail fast during startup.
from __future__ import annotations
from typed_time_provider import (
Microseconds,
TimePrecision,
TypedTimeProviderRuntimeContractValidator,
TimeFormatter,
WallClock,
)
def unix_nanosecond_factory() -> int:
return 1_775_000_000_123_456_789
def main() -> None:
wall_clock: WallClock[Microseconds] = WallClock(
preferred_time_unit_type=Microseconds,
unix_nanosecond_factory=unix_nanosecond_factory,
)
time_formatter: TimeFormatter = TimeFormatter(
default_time_precision=TimePrecision.SECOND,
)
runtime_contract_validator: TypedTimeProviderRuntimeContractValidator
runtime_contract_validator = TypedTimeProviderRuntimeContractValidator()
runtime_contract_validator.validate_wall_clock(
wall_clock=wall_clock,
)
runtime_contract_validator.validate_time_formatter(
time_formatter=time_formatter,
)
if __name__ == "__main__":
main()
Public API
Main imports are available from the root package:
from typed_time_provider import (
BaseFormattedTimestamp,
BaseTimeUnit,
HourPrecisionFormattedTimestamp,
Hours,
MicrosecondPrecisionFormattedTimestamp,
Microseconds,
MillisecondPrecisionFormattedTimestamp,
Milliseconds,
MinutePrecisionFormattedTimestamp,
Minutes,
MonotonicClock,
MonotonicClockInterface,
NanosecondPrecisionFormattedTimestamp,
Nanoseconds,
SecondPrecisionFormattedTimestamp,
Seconds,
TimeFormatter,
TimeFormatterInterface,
TimePrecision,
TypedTimeProviderError,
TypedTimeProviderInvalidInputTypeError,
TypedTimeProviderInvalidInputValueError,
TypedTimeProviderInvariantViolationError,
TypedTimeProviderRuntimeContractValidator,
TimeUnitType,
TimeUnitValue,
WallClock,
WallClockInterface,
)
Design rules
Wall-clock and monotonic time are separate
Use WallClock for real-world UNIX timestamps.
Use MonotonicClock for elapsed time, deadlines, and duration checks.
This avoids mixing two different time domains.
Formatting is separate from capturing time
TimeFormatter does not call time.time_ns().
This keeps formatting deterministic and easy to test.
Nanoseconds are the internal canonical unit
Conversions use nanoseconds internally and return the requested typed unit at API boundaries.
Lower precision conversion truncates
When converting to a lower precision unit, fractional lower-level units are truncated by integer division.
Example:
from typed_time_provider import Microseconds, Nanoseconds
from typed_time_provider.conversion import convert_nanoseconds_to_time_unit
converted_microseconds: Microseconds = convert_nanoseconds_to_time_unit(
nanoseconds=Nanoseconds(2_500),
time_unit_type=Microseconds,
)
assert converted_microseconds == Microseconds(2)
Error hierarchy
All package-specific exceptions inherit from TypedTimeProviderError.
TypedTimeProviderError
├── TypedTimeProviderInvalidInputTypeError
├── TypedTimeProviderInvalidInputValueError
└── TypedTimeProviderInvariantViolationError
Development
Install development dependencies:
python -m pip install -e ".[dev]"
Run linting:
ruff check .
Run tests:
pytest
Run mypy:
mypy
Run pyright:
pyright
Build package:
python -m build
Check package metadata:
python -m twine check dist/*
Upload to PyPI:
python -m twine upload dist/*
Examples
Example scripts are available in the examples package.
Run from project root after installing the package in editable mode:
python -m examples.basic_wall_clock_usage
python -m examples.basic_time_formatter_usage
python -m examples.basic_monotonic_clock_usage
python -m examples.dependency_injection_usage
python -m examples.runtime_contract_validation_usage
python -m examples.semantic_time_units_usage
License
MIT License.
See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file typed_time_provider-0.1.0.tar.gz.
File metadata
- Download URL: typed_time_provider-0.1.0.tar.gz
- Upload date:
- Size: 28.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d7eceb81af58c85709ff2f477f363467d8349d0aef2619e53be36c4ca31aaec
|
|
| MD5 |
dca6d2588b693a716e9b13861bf4663b
|
|
| BLAKE2b-256 |
6a8aa0b9c6c477b0b6b0202986d36e7d97c5566fe6eabf666f5864b837094cff
|
File details
Details for the file typed_time_provider-0.1.0-py3-none-any.whl.
File metadata
- Download URL: typed_time_provider-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
740d0e0017fafa7c7d37513f9c37125c7a6f8edfb6665e39b6d11d3c73f4d8b1
|
|
| MD5 |
97c79ae77fe943fb5cee993587a07632
|
|
| BLAKE2b-256 |
d531cffe1c9908d638555b9ee1534fe62e4210889de4b8b3ef0a05d23b48793c
|