Metadata-Version: 2.4
Name: typed-time-provider
Version: 0.1.0
Summary: DI-friendly wall-clock and monotonic clock providers with typed time units.
Author-email: Eldeniz Guseinli <eldenizfamilyanskicode@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/eldenizfamilyanskicode/typed-time-provider
Project-URL: Repository, https://github.com/eldenizfamilyanskicode/typed-time-provider
Project-URL: Issues, https://github.com/eldenizfamilyanskicode/typed-time-provider/issues
Keywords: time,timestamp,typed-int,dependency-injection,domain-model,pydantic
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: base-typed-int<1,>=0.1.1
Requires-Dist: base-typed-string<1,>=0.1.2
Requires-Dist: tzdata>=2024.1
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pytest-cov>=5.0; extra == "test"
Requires-Dist: pydantic<3,>=2.6; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff>=0.5; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy>=1.10; extra == "typecheck"
Requires-Dist: pyright>=1.1; extra == "typecheck"
Requires-Dist: pydantic<3,>=2.6; extra == "typecheck"
Provides-Extra: build
Requires-Dist: build>=1.2; extra == "build"
Requires-Dist: twine>=5.1; extra == "build"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.1; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pyright>=1.1; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: pydantic<3,>=2.6; extra == "dev"
Dynamic: license-file

# 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

```bash
python -m pip install typed-time-provider
```

For local development:

```bash
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

```python
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

```python
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.

```python
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.

```python
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:

```python
"UTC"
"Europe/Berlin"
"Asia/Baku"
"America/New_York"
```

## Formatting precision

Supported formatting precision values:

```python
from typed_time_provider import TimePrecision

TimePrecision.HOUR
TimePrecision.MINUTE
TimePrecision.SECOND
TimePrecision.MILLISECOND
TimePrecision.MICROSECOND
TimePrecision.NANOSECOND
```

Example output formats:

```text
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.

```python
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.

```python
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.

```python
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:

```python
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:

```python
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`.

```python
TypedTimeProviderError
├── TypedTimeProviderInvalidInputTypeError
├── TypedTimeProviderInvalidInputValueError
└── TypedTimeProviderInvariantViolationError
```

## Development

Install development dependencies:

```bash
python -m pip install -e ".[dev]"
```

Run linting:

```bash
ruff check .
```

Run tests:

```bash
pytest
```

Run mypy:

```bash
mypy
```

Run pyright:

```bash
pyright
```

Build package:

```bash
python -m build
```

Check package metadata:

```bash
python -m twine check dist/*
```

Upload to PyPI:

```bash
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:

```bash
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.
