Skip to content

Kolyunya/aiotimer

Repository files navigation

Asyncio Timer

QA Bugs Code Smells codecov

An asynchronous timer with a human-friendly API and rich functionality.

  • State management with start(), stop(), and reset() methods.
  • On-the-fly adjustment of the duration with set(), prolong(), and shorten() methods.
  • Introspection with elapsed, remaining, and state properties.
  • Multi-interval configuration when a timer runs multiple times with a predefined configuration of durations.
  • Looping capabilities for continuously running timers.
  • Rich callback system enabling hooking into the timer lifecycle events.
  • Synchronous and asynchronous callback modes.
  • Concurrency-safe architecture designed to prevent race conditions and deadlocks.
  • Support for a wide range of Python versions from 3.9 onward.
  • Zero third-party dependencies.

Table of contents

Usage examples

One-off timer

A timer may have just one time interval.

from asyncio import run, sleep

from aiotimer import Timer
from aiotimer.duration import once


async def main() -> None:
  """
  Will run the timer for 3 seconds.
  Then will print a message.
  """

  timer = Timer(once(3), lambda: print('3 seconds passed'))
  await timer.start()

  # Wait for the timer to complete.
  await sleep(3 + 1)


if __name__ == '__main__':
  run(main())

Multi-interval timer

A timer may have multiple time intervals of arbitrary durations.

from asyncio import run, sleep

from aiotimer import Timer
from aiotimer.duration import thrice


async def main() -> None:
  """
  Will run the timer three times for 1 second each.
  And will print intermediate messages every second.
  Then will print the final message after a total of 3 seconds.
  """

  timer = Timer(
    thrice(1),
    on_timer_complete=lambda: print('3 seconds passed'),
    on_interval_complete=lambda: print('1 more second passed'),
  )
  await timer.start()

  # Wait for the timer to complete.
  await sleep(3 + 1)


if __name__ == '__main__':
  run(main())

Other usage examples

More usage examples are available here.

Public API

Controlling the state

  • await timer.start() starts the timer that is in the Initial or in the Stopped state.
  • await timer.stop() stops the timer that is in the Running state. The elapsed and remaining times for the current time interval as well as the current interval itself are preserved.
  • await timer.reset() resets the timer. The elapsed and remaining times for the current time interval as well as the current interval itself are discarded. The timer is reset to the initial state it had after instantiation.

Duration modification

  • await timer.set(duration) sets the duration of the currently running interval to duration. In case the elapsed time is greater than duration, the interval would complete immediately.
  • await timer.prolong(delta) prolongs the duration of the currently running interval by delta.
  • await timer.shorten(delta) shortens the duration of the currently running interval by delta. In case the elapsed time is greater than the resulting duration after shortening, the interval would complete immediately.

Introspection

  • await timer.elapsed returns the elapsed time for the currently running interval.
  • await timer.remaining returns the remaining time for the currently running interval.
  • await timer.state returns the type of the current state of the timer.

States and transitions

The timer class implements the State Pattern. Methods that modify the timer state may only be called when the timer is in a supported state.

Any transition not listed in the diagram will raise an InvalidStateError. For example, you cannot reset() a timer while it is in the InitialState, and you cannot run() a timer that is in the CompleteState.

This design is used as a defensive programming technique that helps catch any logic errors in the code early and simplifies the debugging process.

Configuring durations

The first parameter of the timer constructor is a Duration Factory. It is responsible for generating durations for the timers. A timer may have one or more time intervals of arbitrary durations.

All interval durations must be non-negative. Duration factory, producing a negative duration yields an undefined behavior.

There are many built-in duration factories that should cover the majority of common use cases.

from aiotimer.duration import *

# Generates 1 interval of 5 seconds.
once(5)


# Generates 2 intervals of 5 seconds each.
twice(5)


# Generates 3 intervals of 5 seconds each.
thrice(5)


# Generates 3 intervals of 1, 2, and 3 seconds.
sequentially(1, 2, 3)


# Generates 5 intervals of: 1, 2, 4, 8, and 16 seconds (powers of 2).
exponentially(2, interval_count=5)


# Generates 5 intervals of: 1, 2, 4, 8, and 16 seconds (powers of 2).
exponentially(2, maximum_duration=16)


# Generates 1 interval between 5 and 10 seconds.
randomly(5, 10)


# Generates 30 intervals of 1, 2, 3, 1, 2, 3, ... seconds.
# Any other factory may be passed as the first argument.
repeatedly(sequentially(1, 2, 3), 10)


# Generates an infinite number of intervals of 1, 2, 3, 1, 2, 3, ... seconds.
# Any other factory may be passed as the first argument.
forever(sequentially(1, 2, 3))


# Generates 3 intervals of 5±0.5 seconds (10% relative jitter).
# Any other factory may be passed as the first argument.
jittery(thrice(5), relative=0.1)


# Generates 3 intervals of 5±0.5 seconds (0.5 second absolute jitter).
# Any other factory may be passed as the first argument.
jittery(thrice(5), absolute=0.5)


# Generates 4 intervals of 0, 5, 5, and 5 seconds.
# Any other factory may be passed as the first argument.
immediately_then(thrice(5))


# Generates a zero-second interval followed by 5 exponentially growing retries.
# The default exponent base is 2, resulting in durations of 0, 1, 2, 4, 8, 16 seconds.
backoff(retries=5)


# The exponent base parameter is optional and may be reconfigured.
backoff(retries=5, base=3)


# Generates no intervals.
# Used in the test suite for testing edge cases. 
never()

If you believe some type of duration factory is missing, feel free to submit an issue or a pull request.

Event system

There are several event handlers that may be configured for a timer through the constructor arguments.

All event handlers must comply with the following API contract. Non-compliant event handlers result in undefined behavior.

  • Event handler must have either:
    • Zero parameters.
    • Exactly one positional parameter accepting the corresponding event object type.
  • An event handler's signature must not be modified at runtime after registration with the timer object.
  • Event handler should not return any values because they will be ignored and discarded by the timer.
  • Event handler may be either:
    • Synchronous callable.
    • Asynchronous callable.

All event objects have a timer property that references the timer object that fired the event.

Any public method of a timer object may be safely called from any event handler. The internal timer architecture prevents any race conditions and deadlocks from occurring.

Timer complete event

This event is fired each time the last interval of a timer is complete. An on_timer_complete handler may optionally accept a TimerCompleteEvent object. Events of this type have the following properties:

  • timer: Timer
  • interval_count: int

Interval complete event

This event is fired each time any interval of a timer is complete. An on_interval_complete handler may optionally accept an IntervalCompleteEvent object. Events of this type have the following properties:

  • timer: Timer
  • interval_number: int
  • interval_duration: float

Error event

This event is fired each time any exception is propagated from any of the event handlers described above. Additionally, it is fired when an exception occurs inside a system coroutine of a timer. An on_error handler may optionally accept an ErrorEvent object. Events of this type have the following properties:

  • timer: Timer
  • error: Exception

Advanced usage

Sync and Async callbacks

Use the await_callbacks parameter of the Timer constructor to control the way the callbacks are handled.

In the sync mode (await_callbacks == True) the next interval would not start until the on_interval_complete callback finishes execution.

In the async mode (await_callbacks == False) the next interval would start immediately after the previous one completes.

Both modes support def, async def as well as any other types of compatible callables. It's perfectly fine to use def in the async mode and async def in sync mode.

Configuring precision

The timer class has a configurable precision: float parameter. It represents the amount of seconds a timer would idle between its system ticks.

For adequate accuracy, it is recommended to have the precision value configured significantly (at least several times) smaller than the shortest interval the timer would have.

At the same time, having the precision configured to an extremely low value (e.g. 0.001) may yield a high CPU load.

Custom duration factories

The first argument to the timer constructor is a Duration Factory. It is a callable that returns an Iterable of durations in seconds.

This design is required to support the following features.

  • Perpetually running Timer which requires infinitely-iterable objects.
  • The reset() functionality which requires a fresh instance of an iterable.
from asyncio import run, sleep

from aiotimer import Timer


async def main() -> None:
  # The simplest form of a custom Duration Factory.
  duration_factory = lambda: [1, 2, 3]

  timer = Timer(duration_factory, lambda: print('6 seconds passed'))
  await timer.start()

  # Wait for the timer to complete.
  await sleep(6 + 1)


if __name__ == '__main__':
  run(main())

Contributing

Configuring the development environment

# Create and activate a virtual environment.
python -m venv .
source bin/activate

# Install the library and its dependencies.
pip install --upgrade pip
pip install --editable ".[development]"

# Run the test suite.
BEARTYPE=Yes python -m test --skip-slow=No

Additionally, convenient Quick QA and Full QA run configuration are provided for PyCharm users.

About

An asynchronous timer with a human-friendly API and rich functionality.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages