Skip to main content

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:

  1. capturing wall-clock UNIX timestamps
  2. measuring monotonic elapsed time and deadlines
  3. 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:
    • Hours
    • Minutes
    • Seconds
    • Milliseconds
    • Microseconds
    • Nanoseconds
  • Semantic time unit subclasses, for example:
    • UnixTimestampMicroseconds
    • DurationMilliseconds
    • MonotonicDeadlineMilliseconds
  • 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.typed marker for typed package consumers
  • Strict mypy and pyright compatible 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-int
  • base-typed-string
  • tzdata

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typed_time_provider-0.1.0.tar.gz (28.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typed_time_provider-0.1.0-py3-none-any.whl (29.6 kB view details)

Uploaded Python 3

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

Hashes for typed_time_provider-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1d7eceb81af58c85709ff2f477f363467d8349d0aef2619e53be36c4ca31aaec
MD5 dca6d2588b693a716e9b13861bf4663b
BLAKE2b-256 6a8aa0b9c6c477b0b6b0202986d36e7d97c5566fe6eabf666f5864b837094cff

See more details on using hashes here.

File details

Details for the file typed_time_provider-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for typed_time_provider-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 740d0e0017fafa7c7d37513f9c37125c7a6f8edfb6665e39b6d11d3c73f4d8b1
MD5 97c79ae77fe943fb5cee993587a07632
BLAKE2b-256 d531cffe1c9908d638555b9ee1534fe62e4210889de4b8b3ef0a05d23b48793c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page