Skip to main content

Endless tick-tock generators

Project description

logo

Downloads Downloads codecov Lines of code Hits-of-Code Test-Package Python versions PyPI version Checked with mypy Ruff

This library offers the easiest way to run regular tasks. Just give it a function and tell it how many seconds you need to run it, then step back and don't interfere. For all its simplicity, there are:

  • 📜 Logging the start and end of each operation.
  • 🛡️ Error escaping. But not when you don't even know about the errors, but again - with detailed logging.
  • ⇆ Thread safety.
  • ❌ Support for cancellation tokens.

Table of contents

Quick start

Install it:

pip install metronomes

And use:

from time import sleep
from metronomes import Metronome

metronome = Metronome(0.2, lambda: print('go!'))

metronome.start()
sleep(1)
metronome.stop()
#> go!
#> go!
#> go!
#> go!
#> go!

You can also quickly try out this and other packages without having to install using instld.

Why?

A metronome is a special device that musicians often use. It looks something like this (if you are interested in how it works, I recommend watching the video on YouTube):

metronome

Its task is to produce sounds regularly and monotonously, which is very convenient if you want to develop a sense of rhythm. Unlike a person for whom strict rhythmicity is unusual, the metronome counts down time very accurately and therefore is used as a "guide" to which other rhythmic actions that we want to do are attached, whether it's tapping on a drum or pressing keys.

When we write programs, we also sometimes want some action to be performed regularly. And sometimes it happens that it may take a different amount of time, but we need the next action to start on time. This is exactly the task this library solves. When you call the start() method on a metronome object, it starts calling the function you passed once in a certain period of time. This happens in a separate specially created thread, so you can use these function calls to orchestrate some other actions in the main thread or even in several different threads.

At the same time, it may be important to you that even if in some cases the function does not work well and raises exceptions, in general the metronome continues to work, and after a certain time it will try to call this function again, and will not break at the first exception. After all, it would be strange if your real metronome went silent when you made a mistake in the rhythm that you are tapping on the drum, right? You may also want the errors that have occurred not to be lost, but to be recorded in your log. This library also provides all these amenities.

Basic usage

The metronome object has 2 main methods: start() and stop(). Calling the start() method starts an additional thread, inside which the passed function starts running regularly in the loop. At the same time, the main thread can continue and, when it's necessary, stop the metronome calling the stop() method. Often, a metronome can be used for background notifications that are called during the execution of other code:

from metronomes import Metronome

metronome = Metronome(0.001, lambda: print('An another function in progress...'))

metronome.start()
another_function()
metronome.stop()
#> An another function in progress...
#> An another function in progress...
#> An another function in progress...

Instead of manually starting and stopping the metronome, you can also use the context manager mode.

The metronome object from this library cannot be used twice. If you started it once and then stopped it, you can't do it again:

metronome = Metronome(0.2, lambda: print('go!'))

metronome.start()
sleep(0.2)
metronome.stop()
#> go!
metronome.start()
#> ...
#> metronomes.errors.RunStoppedMetronomeError: Metronomes are disposable, you cannot restart a stopped metronome.

Use it as a context manager

We recommend using a metronome exclusively in the context manager mode, without "manual" start and stop:

from time import sleep
from metronomes import Metronome

with Metronome(0.2, lambda: print('go!')):
    sleep(1)
#> go!
#> go!
#> go!
#> go!
#> go!

In this case, it is guaranteed that the metronome will be completed correctly after the end of the code block and will not remain "hanging" in a separate thread. Even if an error occurs in the block.

Logging

In order for events inside the metronome to start logging, you need to pass the logger object there:

import logging
from time import sleep
from metronomes import Metronome

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.StreamHandler(),
    ]
)

logger = logging.getLogger('logger_name')
metronome = Metronome(0.2, lambda: print('go!'), logger=logger)

metronome.start()
sleep(1)
metronome.stop()
#> 2024-04-18 19:38:42,910 [INFO] The metronome starts...
#> 2024-04-18 19:38:42,910 [DEBUG] The beginning of the execution of callback "<lambda>".
#> go!
#> 2024-04-18 19:38:42,910 [DEBUG] Callback "<lambda>" has been successfully completed.
#> 2024-04-18 19:38:43,115 [DEBUG] The beginning of the execution of callback "<lambda>".
#> go!
#> 2024-04-18 19:38:43,116 [DEBUG] Callback "<lambda>" has been successfully completed.
#> 2024-04-18 19:38:43,321 [DEBUG] The beginning of the execution of callback "<lambda>".
#> go!
#> 2024-04-18 19:38:43,322 [DEBUG] Callback "<lambda>" has been successfully completed.
#> 2024-04-18 19:38:43,523 [DEBUG] The beginning of the execution of callback "<lambda>".
#> go!
#> 2024-04-18 19:38:43,524 [DEBUG] Callback "<lambda>" has been successfully completed.
#> 2024-04-18 19:38:43,727 [DEBUG] The beginning of the execution of callback "<lambda>".
#> go!
#> 2024-04-18 19:38:43,729 [DEBUG] Callback "<lambda>" has been successfully completed.
#> 2024-04-18 19:38:43,933 [INFO] The metronome has stopped.

The events of the start and stop of the metronome will be logged with the INFO level, start and stop of the passed function - DEBUG. If the operation time of the passed function was longer than the allotted time for one iteration, you will see a WARNING message.

And finally, if an exception is raised inside the function, it will be suppressed, and an ERROR level message will be recorded along with it (with the traceback saved, that is, the exception() method will be called for this from the logger):

# Here should be some imports, logging settings, and the creation of a logger object

def function():
    return 1/0

metronome = Metronome(0.2, function, logger=logger)

metronome.start()
sleep(0.4)
metronome.stop()
#> 2024-04-18 19:58:10,847 [INFO] The metronome starts...
#> 2024-04-18 19:58:10,847 [DEBUG] The beginning of the execution of callback "function".
#> 2024-04-18 19:58:10,847 [ERROR] The "ZeroDivisionError" ("division by zero") exception was suppressed inside the context.
#> Traceback (most recent call last):
#>   File "/project_path/metronomes/metronomes/metronome.py", line 68, in run_loop
#>     self.callback()
#>   File "test.py", line ?, in function
#>     return 1/0
#> ZeroDivisionError: division by zero
#> 2024-04-18 19:58:11,053 [DEBUG] The beginning of the execution of callback "function".
#> 2024-04-18 19:58:11,054 [ERROR] The "ZeroDivisionError" ("division by zero") exception was suppressed inside the context.
#> Traceback (most recent call last):
#>   File "/project_path/metronomes/metronomes/metronome.py", line 68, in run_loop
#>     self.callback()
#>   File "test.py", line ?, in function
#>     return 1/0
#> ZeroDivisionError: division by zero
#> 2024-04-18 19:58:11,258 [INFO] The metronome has stopped.

Error escaping

Exceptions inside the function that you pass to the metronome will be:

  • Suppressed.
  • Logged (if you pass the logger object).

This applies to all the usual exceptions that are expected in normal code. For more information about the types of exceptions that are suppressed by default, read the documentation for the escaping library that is used for this.

Working with Cancellation Tokens

This library is fully compatible with the cancellation token pattern and supports any token objects from the cantok library.

The essence of the pattern is that you can pass an object to metronome object, from which it can find out whether the work of the metronome still needs to be continued or not. If not, the metronome stops.

The token object is passed to the metronome as the token argument:

from time import sleep
from metronomes import Metronome
from cantok import TimeoutToken

metronome = Metronome(0.2, lambda: None, token=TimeoutToken(1))

metronome.start()
print(metronome.stopped)
#> False
sleep(1.5)  # Here I specify a little more time than in the constructor of the token itself, since a small margin is needed for operations related to the creation of the metronome object itself.
print(metronome.stopped)
#> True

You can also pass a token when calling the start() method:

metronome.start(token=TimeoutToken(1))

If you pass cancellation tokens both during metronome initialization and in the start() method, their limitations will be summed up.

How to set a time limit

You can specify a time limit (in seconds) for which the metronome will tick. When the specified time is over, it will simply stop. There are 2 ways to do this: when creating a metronome object or when calling the start() method.

In the first way, the time will start counting from the moment the metronome object is created:

metronome = Metronome(0.2, lambda: print('go!'), duration=0.6)  

metronome.start()
sleep(1)
#> go!
#> go!
#> go!

In the second way, the countdown will start by calling the start() method:

metronome = Metronome(0.2, lambda: print('go!'))  

metronome.start(duration=0.6)
sleep(1)
#> go!
#> go!
#> go!

Choose the right way to limit time. When using the metronome in the context manager mode, only the first way is available to you, in almost all other situations - the second one is preferable.

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

metronomes-0.0.10.tar.gz (8.5 kB view details)

Uploaded Source

Built Distribution

metronomes-0.0.10-py3-none-any.whl (8.6 kB view details)

Uploaded Python 3

File details

Details for the file metronomes-0.0.10.tar.gz.

File metadata

  • Download URL: metronomes-0.0.10.tar.gz
  • Upload date:
  • Size: 8.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.4

File hashes

Hashes for metronomes-0.0.10.tar.gz
Algorithm Hash digest
SHA256 131864ab1f8a45e108af25504b5ed9a7100679afe36d62ae1bf62a72d855bf8c
MD5 bd55d52439946afd585f02665ff09624
BLAKE2b-256 45af91e92fd2b2fcafd112760e904946f7660cc142f515e3235fed4303edfe22

See more details on using hashes here.

File details

Details for the file metronomes-0.0.10-py3-none-any.whl.

File metadata

  • Download URL: metronomes-0.0.10-py3-none-any.whl
  • Upload date:
  • Size: 8.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.4

File hashes

Hashes for metronomes-0.0.10-py3-none-any.whl
Algorithm Hash digest
SHA256 691ebbcf67ec7cd0ecf7a2e160a8393094666922eb9d2ae775d16ced7acc5396
MD5 4e28f19940a589ab08be4c0e2167c549
BLAKE2b-256 0d26a00ff4796257619415ee6dfa04d06953a557c99742ab29520c74b7e50114

See more details on using hashes here.

Supported by

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