Metadata-Version: 2.4
Name: multidog
Version: 0.1.0a1
Summary: A SIGALRM-based watchdog that can handle multiple timeouts.
Project-URL: Source, https://codeberg.org/scy/multidog
Project-URL: Documentation, https://codeberg.org/scy/multidog
Project-URL: Issues, https://codeberg.org/scy/multidog/issues
Author: scy
Maintainer: scy
License-Expression: MIT
License-File: LICENSES/MIT.txt
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: <4,>=3.12
Description-Content-Type: text/markdown

<!--
SPDX-FileCopyrightText: © 2026 scy

SPDX-License-Identifier: MIT
-->

# Multidog

_A SIGALRM-based watchdog that can handle multiple timeouts._

➡️ **Quick Links:** [Repository](https://codeberg.org/scy/multidog) · [Issues](https://codeberg.org/scy/multidog/issues)

Multidog works by keeping track of one or more timeouts and setting up an [`alarm`](https://docs.python.org/3/library/signal.html#signal.alarm) signal that will cause the OS to terminate your process when the timeout occurs.


## 🌱 Status

Multidog has recently been extracted from another project of mine.
It kind of works, but I still have to fix some issues.

Things that are not yet implemented:

- [ ] Timeouts are only checked with the granularity of the shortest timeout. See the example below for what this means.
- [ ] You may only use one running instance of Multidog per process, because a process can only have one handler per signal. There is no safeguard against this at the moment.
- [ ] Adding more timeouts after creating the `Multidog` instance is not possible yet.


## 📚 Usage

Example usage:

```python
from time import sleep
from multidog import Multidog

dog = Multidog({"a": 5, "b": 10})

dog.start()
# This will call `sys.exit()` in 5 seconds unless you reset the timeouts regularly.

sleep(4)
dog.reset("a")
# `sys.exit()` will be deferred for another 5 seconds.

sleep(4)
dog.reset("a")
# `sys.exit()` will be deferred for another 5 seconds.
# This is a bug actually: The "b" timeout has never been reset and only has two
# seconds left. Multidog should defer for only 2 seconds, but right now it doesn't.

sleep(4)
dog.reset("a")
# Multidog finally notices that "b" is overdue and will refuse to reset the alarm,
# causing your application to exit in one second.

dog.stop()
# Stops the watchdog before it kills your app.
```

### Anonymous timeout

If you only have a single timeout to track, you can simplify your usage:

```python
dog = Multidog(5)  # equivalent to Multidog({"": 5})
dog.start()
dog.reset()  # equivalent to dog.reset("")
```

### Exit timeout

Your Python application might not shut down fast enough (or at all) on [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) (because yeah, that's something that can be suppressed).
Therefore, Multidog will set up a second `SIGALRM` after calling `sys.exit()`, with the signal handler reset to the default, which _should_ kill your process when the timeout expires.

The default timeout for this is 5 seconds, you can choose a different one like so:

```python
dog = Multidog({"a": 5, "b": 10}, exit_timeout=120)
```


## 🗃️ Installation

Simply install the `multidog` package from PyPI via your preferred package manager.

For example, to add it as a dependency to your [uv](https://docs.astral.sh/uv/)-managed project, use this:

```sh
uv add multidog
```


## 🧑‍💻 Development

### Preparation

We're using [uv](https://docs.astral.sh/uv/) to manage this project and its dependencies.
After cloning the repository using Git, a simple `uv sync` should get everything you need.

```sh
git clone https://codeberg.org/scy/multidog.git multidog
cd multidog
uv sync
```

### direnv

We recommend using [direnv](https://direnv.net/) to add installed dependencies to your `$PATH`, so that you don't need to prepend `uv run` to every command.
For example, after installing direnv, you can use `direnv edit` in this repository's directory and add the following line:

```sh
PATH_add .venv/bin
```

**The rest of this readme assumes that you have `.venv/bin` in your `$PATH`.**

### EditorConfig

Make sure your editor or IDE supports the [EditorConfig](https://editorconfig.org/) standard, so that your code adheres to the project's [preferred](.editorconfig) indentation, line lengths, etc.

### Makefile

There is a [`Makefile`](Makefile) that contains frequently used commands to speed up development, but it's optional to use.
The following targets exist:

- `fmt`: Use [Ruff](https://docs.astral.sh/ruff/) to format and lint the code.
- `qa`: Use [mypy](https://mypy.readthedocs.io/) for static type analysis.
- `reuse`: Use [`reuse lint`](https://codeberg.org/fsfe/reuse-tool) to make sure every file contains licensing information.
- `test`: Use [pytest](https://pytest.org/) for automated testing and code coverage.
- `noqa`: Add `noqa` statements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.

`make all`, or simply `make`, will run `fmt`, `qa`, `reuse`, and `test`.
You should do this before every commit and fix all issues that are reported.


## 📃 License

This project is licensed under the terms of the [MIT License](https://spdx.org/licenses/MIT.html).

The project also conforms to the [REUSE Specification, version 3.3](https://reuse.software/spec-3.3/).
You can use [the `reuse` tool](https://codeberg.org/fsfe/reuse-tool) to interpret the machine-readable licensing information.
