An asynchronous timer with a human-friendly API and rich functionality.
- State management with
start(),stop(), andreset()methods. - On-the-fly adjustment of the duration with
set(),prolong(), andshorten()methods. - Introspection with
elapsed,remaining, andstateproperties. - 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.9onward. - Zero third-party dependencies.
- Usage examples
- Public API
- States and transitions
- Configuring durations
- Event system
- Advanced usage
- Contributing
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())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())More usage examples are available here.
await timer.start()starts the timer that is in theInitialor in theStoppedstate.await timer.stop()stops the timer that is in theRunningstate. 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.
await timer.set(duration)sets the duration of the currently running interval toduration. In case the elapsed time is greater thanduration, the interval would complete immediately.await timer.prolong(delta)prolongs the duration of the currently running interval bydelta.await timer.shorten(delta)shortens the duration of the currently running interval bydelta. In case the elapsed time is greater than the resulting duration after shortening, the interval would complete immediately.
await timer.elapsedreturns the elapsed time for the currently running interval.await timer.remainingreturns the remaining time for the currently running interval.await timer.statereturns the type of the current state of the timer.
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.
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.
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
timerproperty 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.
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: Timerinterval_count: int
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: Timerinterval_number: intinterval_duration: float
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: Timererror: Exception
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 defas well as any other types of compatible callables. It's perfectly fine to usedefin the async mode andasync defin sync mode.
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.
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
Timerwhich 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())# 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=NoAdditionally, convenient Quick QA and Full QA run configuration are provided for PyCharm users.
