Skip to content

About

The Clock Pattern is a Python 🐍 package that turns time into an injectable dependency 🧩. By replacing ad-hoc datetime.now() calls with a swappable Clock interface πŸ•°οΈ you unlock deterministic tests πŸ§ͺ and decouple business logic from the OS clock.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

πŸ•°οΈ Clock Pattern

CI Pipeline Coverage Pipeline Package Version Supported Python Versions Package Downloads

The Clock Pattern is a Python 🐍 package that turns time into an injectable dependency 🧩. Instead of scattering datetime.now() or date.today() through application code, domain services depend on a small Clock interface. That keeps time-sensitive logic deterministic in tests, makes timezone choices explicit, and lets production code swap clock implementations without touching business rules.

Table of Contents

πŸ”Ό Back to top



πŸ“₯ Installation

You can install Clock Pattern using pip:

pip install clock-pattern

You can install the companion AI-agent skill from skills.sh with Vercel's skills CLI:

npx skills add adriamontoto/clock-pattern

Review the skill source in skills/clock-pattern before installing it in sensitive environments.

πŸ”Ό Back to top



πŸ“š Documentation

The root README is the entry point. Deeper guides live in this repository and are linked here:

πŸ”Ό Back to top



⚑ Quick Start

Inject a Clock into code that needs the current time. Production code can pass a real clock, while tests can pass a fixed or mock clock.

from clock_pattern import Clock, UtcClock


class TimestampService:
    def __init__(self, *, clock: Clock) -> None:
        self._clock = clock

    def issued_at(self) -> str:
        return self._clock.now().isoformat()


service = TimestampService(clock=UtcClock())
print(service.issued_at())

Use SystemClock when you need a specific timezone:

from clock_pattern import SystemClock

clock = SystemClock(timezone='Europe/Madrid')
print(clock.now())
# >>> 2025-06-16 15:57:26.210964+02:00

πŸ”Ό Back to top



🧩 Why Inject a Clock?

Time is global state. Reading it directly from the operating system makes behavior depend on the moment a test happens to run, the machine timezone, daylight-saving transitions, and the speed of the test suite.

Clock Pattern keeps those decisions explicit:

  • Domain code depends on Clock, not on Python's global datetime functions.
  • Tests can choose exact dates and datetimes without monkeypatching built-in modules.
  • Production wiring decides whether the application uses UTC or another timezone.
  • Custom clocks can be introduced for logical time, simulation, replay, or high-precision infrastructure.

The package exposes two methods:

Method Returns Typical use
now() datetime Timestamps, expiration windows, and audit fields.
today() date Calendar rules, billing days, holiday checks, date-only decisions.

Clock Pattern also includes injectable helpers for elapsed-duration behavior: monotonic clocks, sleepers, stopwatches, deadlines, pollers, and retriers. These use monotonic seconds instead of wall-clock datetimes so system clock changes do not affect timeout or retry behavior.

πŸ”Ό Back to top



πŸ“š Public API

Use the top-level package for contracts and production helpers, and each feature's testing package for test doubles.

Wall-Clock API

API Import path Purpose
Clock from clock_pattern import Clock Abstract contract for code that needs now() or today().
SystemClock from clock_pattern import SystemClock Production clock backed by system time in a configured timezone.
UtcClock from clock_pattern import UtcClock Production clock fixed to UTC.

Elapsed-Time API

API Import path Purpose
MonotonicClock from clock_pattern import MonotonicClock Abstract contract for elapsed-time sources.
SystemMonotonicClock from clock_pattern import SystemMonotonicClock Production monotonic clock for elapsed-time measurement.
Sleeper / SleeperAsync from clock_pattern import Sleeper, SleeperAsync Abstract contracts for injectable sync and async sleeping.
SystemSleeper / SystemSleeperAsync from clock_pattern import SystemSleeper, SystemSleeperAsync Injectable sync and async sleeping.
Stopwatch from clock_pattern import Stopwatch Measure elapsed seconds with .start(), .end(), or a context manager.
Deadline from clock_pattern import Deadline Abstract contract for injectable deadline state.
SystemDeadline from clock_pattern import SystemDeadline Monotonic deadline with an interrupting Unix main-thread context.
TimeoutExpiredError from clock_pattern import TimeoutExpiredError Error raised when a deadline or poll timeout expires.
Poller / PollerAsync from clock_pattern import Poller, PollerAsync Abstract contracts for condition polling.
SystemPoller / SystemPollerAsync from clock_pattern import SystemPoller, SystemPollerAsync Production polling implementations.
Retrier / RetrierAsync from clock_pattern import Retrier, RetrierAsync Abstract contracts for retrying operations.
SystemRetrier / SystemRetrierAsync from clock_pattern import SystemRetrier, SystemRetrierAsync Production retry implementations.

Test Doubles

API Import path Purpose
FixedClock from clock_pattern.clocks.testing import FixedClock Test clock that stays fixed until explicitly set or advanced.
MockClock from clock_pattern.clocks.testing import MockClock Test clock with prepared return values and call assertions.
MockMonotonicClock from clock_pattern.monotonic_clocks.testing import MockMonotonicClock Controllable elapsed-time source with call assertions.
MockDeadline from clock_pattern.deadlines.testing import MockDeadline Controllable deadline with expiry call assertions.
MockSleeper / MockSleeperAsync from clock_pattern.sleepers.testing import MockSleeper, MockSleeperAsync Sleeping test doubles that advance a mock monotonic clock.
MockPoller / MockPollerAsync from clock_pattern.pollers.testing import MockPoller, MockPollerAsync Polling test doubles with call assertions.
MockRetrier / MockRetrierAsync from clock_pattern.retriers.testing import MockRetrier, MockRetrierAsync Retry test doubles with prepared results.
from clock_pattern import Stopwatch, SystemMonotonicClock, SystemSleeper

monotonic_clock = SystemMonotonicClock()
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)

with Stopwatch(monotonic_clock=monotonic_clock) as stopwatch:
    pass

with sleeper.minimum_duration(seconds=0.001):
    pass

The deadline context requires a Unix main thread:

from clock_pattern import SystemDeadline, SystemMonotonicClock

monotonic_clock = SystemMonotonicClock()
with SystemDeadline(seconds=5, monotonic_clock=monotonic_clock):
    pass

Continue with polling and retries on all platforms:

from clock_pattern import SystemMonotonicClock, SystemPoller, SystemRetrier, SystemSleeper

monotonic_clock = SystemMonotonicClock()
sleeper = SystemSleeper(monotonic_clock=monotonic_clock)
poller = SystemPoller(sleeper=sleeper, monotonic_clock=monotonic_clock)

poller.poll_until(condition=lambda: True, timeout_seconds=5, interval_seconds=0.1)
SystemRetrier(sleeper=sleeper).retry(
    operation=lambda: 'done',
    attempts=3,
    delay_seconds=0.2,
    max_delay_seconds=2,
    backoff=2,
    jitter=True,
)

SystemDeadline context managers use SIGALRM to interrupt Python code and interruptible system calls. Context use is limited to Unix main-thread execution, cannot be nested or share an existing alarm, and may be delayed by C code that does not return control to the Python interpreter. Deadline properties and raise_if_expired() remain cooperative when used outside a context manager. TimeoutExpiredError.elapsed_seconds exposes the measured elapsed duration reported by either timeout path.

πŸ”Ό Back to top



🌍 Timezone Behavior

SystemClock accepts either an IANA timezone string or a tzinfo instance. It converts strings to ZoneInfo, preserves tzinfo instances directly, and uses the resulting timezone for both now() and today().

from datetime import UTC

from clock_pattern import SystemClock

utc_clock = SystemClock(timezone=UTC)
madrid_clock = SystemClock(timezone='Europe/Madrid')

print(utc_clock.timezone)
# >>> UTC
print(madrid_clock.timezone)
# >>> Europe/Madrid

UtcClock is a convenience clock for the common production choice of UTC.

today() is calculated in the clock timezone. Around midnight, SystemClock(timezone='UTC').today() and SystemClock(timezone='America/New_York').today() may return different dates. For more details, see docs/timezones/README.md.

πŸ”Ό Back to top



πŸ§ͺ Testing Time-Sensitive Code

Use FixedClock when the test only needs a stable instant:

from datetime import datetime

from clock_pattern.clocks.testing import FixedClock

clock = FixedClock(instant=datetime(year=2025, month=1, day=1, hour=10, minute=30))

assert clock.now().isoformat() == '2025-01-01T10:30:00+00:00'
assert clock.today().isoformat() == '2025-01-01'

Use MockClock when the test also needs to prove that time was requested:

from datetime import date

from clock_pattern.clocks.testing import MockClock

clock = MockClock()
clock.prepare_today_method_return_value(today=date(year=2025, month=1, day=7))

assert clock.today() == date(year=2025, month=1, day=7)
clock.assert_today_method_was_called_once()
clock.assert_now_method_was_not_called()

FixedClock.advance(delta=timedelta(...)) moves time forward explicitly; set(instant=...) replaces it, including backward jumps. Both keep now() and today() consistent.

More testing recipes are available in docs/testing/README.md.

πŸ”Ό Back to top



πŸŽ„ Real-Life Case: Christmas Detector Service

This service checks whether the current date falls within a Christmas holiday range. The service depends on Clock, so production code can use UtcClock and tests can use MockClock without changing the service.

from datetime import date

from clock_pattern import Clock, UtcClock
from clock_pattern.clocks.testing import MockClock


class ChristmasDetectorService:
    def __init__(self, *, clock: Clock) -> None:
        self._clock = clock
        self._christmas_start = date(year=2024, month=12, day=24)
        self._christmas_end = date(year=2025, month=1, day=6)

    def is_christmas(self) -> bool:
        return self._christmas_start <= self._clock.today() <= self._christmas_end


clock = UtcClock()
service = ChristmasDetectorService(clock=clock)

print(service.is_christmas())
# >>> False


def test_christmas_detector_is_christmas() -> None:
    clock = MockClock()
    service = ChristmasDetectorService(clock=clock)

    today = date(year=2024, month=12, day=25)
    clock.prepare_today_method_return_value(today=today)

    assert service.is_christmas() is True
    clock.assert_today_method_was_called_once()


def test_christmas_detector_is_not_christmas() -> None:
    clock = MockClock()
    service = ChristmasDetectorService(clock=clock)

    today = date(year=2025, month=1, day=7)
    clock.prepare_today_method_return_value(today=today)

    assert service.is_christmas() is False
    clock.assert_today_method_was_called_once()

πŸ”Ό Back to top



🀝 Contributing

We love community help! Before you open an issue or pull request, please read:

Thank you for helping make πŸ•°οΈ Clock Pattern package awesome! 🌟

πŸ”Ό Back to top



πŸ”‘ License

This project is licensed under the terms of the MIT license.

πŸ”Ό Back to top

About

The Clock Pattern is a Python 🐍 package that turns time into an injectable dependency 🧩. By replacing ad-hoc datetime.now() calls with a swappable Clock interface πŸ•°οΈ you unlock deterministic tests πŸ§ͺ and decouple business logic from the OS clock.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages