Metadata-Version: 2.5
Name: clankers
Version: 3.0.1
Summary: Get notified when long-running commands and Python tasks finish
Project-URL: Repository, https://github.com/radajakub/clankers
Project-URL: Issues, https://github.com/radajakub/clankers/issues
Project-URL: Changelog, https://github.com/radajakub/clankers/blob/master/CHANGELOG.md
Author-email: Jakub Rada <dev.jakubrada@icloud.com>
License-Expression: MIT
License-File: LICENSE
Keywords: alerts,monitoring,notifications,ntfy,push-notifications
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: python-dotenv>=1.2.1
Requires-Dist: requests>=2.32.5
Description-Content-Type: text/markdown

# clankers

Get an ntfy notification when a long-running command or Python task finishes.

## Configuration

Set the ntfy server and topic with environment variables:

```bash
export NTFY_URL="https://ntfy.example.com"
export NTFY_TOPIC="default"
export NTFY_TOKEN="tk_your_private_access_token"
```

The same variables can live in a `.env` file in your project directory:

```dotenv
NTFY_URL=https://ntfy.example.com
NTFY_TOPIC=default
NTFY_TOKEN=tk_your_private_access_token
```

Both `NTFY_URL` and `NTFY_TOPIC` are required; clankers does not provide deployment defaults in
code. Server URLs must be absolute HTTP(S) URLs without credentials, query strings,
fragments, whitespace, or backslashes. Topic names may contain ASCII letters, numbers, underscores,
and hyphens. `NTFY_TIMEOUT` is a finite positive number of seconds, up to 2,147,483.647 seconds.
Redirects are rejected; configure the final publish URL directly. You can instead create
`~/.config/clankers/config.toml` (or `$XDG_CONFIG_HOME/clankers/config.toml`):

```toml
[ntfy]
url = "https://ntfy.example.com"
topic = "default"
token = "tk_your_private_access_token"
```

The loader returns normalized key/value pairs such as `NTFY_URL`; each backend selects and validates
its own settings. Sources are merged in this order of priority:

1. `.env`
2. process environment variables (those prefixed `NTFY_` or `CLANKERS_`)
3. `~/.config/clankers/config.toml`

Settings themselves never come from the command line — only the files they live in do. Clankers
searches for `.env` from the working directory upward; use `--dotenv` to select one directly, and
`--config` to select a different TOML file.

## CLI

Prefix an arbitrary command with `clankers engage`:

```bash
clankers engage uv run pytest
clankers engage --config ./config.toml uv run train.py
clankers engage -m "nightly training" -- uv run train.py --epochs 100
```

Clankers' own flags must come before the command; everything after the first non-flag argument (or
after `--`) belongs to the wrapped command. The wrapped command keeps its standard input and output.
Clankers reports its duration and result, then returns the command's exit code. Without `--message`
the report is the command line itself. Delivery failures produce warnings without changing
the wrapped command's exit code. Invalid configuration exits with status 2 before starting
the command; an executable that cannot start returns 127. On Unix, a command terminated by a
signal returns `128 + signal number`. Commands execute directly; invoke a shell explicitly
when you need pipes, redirects, or other shell syntax.

Send a notification on its own — at the end of a shell script, or from a Makefile. There are three
levels: `rogerroger` reports success, `blastthem` reports neutral progress, `uhoh` reports failure.

```bash
clankers blastthem -m "deploy started"
clankers rogerroger -m "deploy finished"
clankers uhoh -m "deploy failed"
```

| Argument                  | Behavior                                                                           |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `-m`, `--message message` | Required for manual notifications; overrides the command line reported by `engage` |
| `--config path`           | Select a TOML configuration file                                                   |
| `--dotenv path`           | Select a `.env` file instead of searching parent directories                       |
| `-v`, `--verbose`         | Log configuration discovery and delivery diagnostics to stderr                     |
| `-h`, `--help`            | Show help without configuring ntfy                                                 |
| `--version`               | Show the installed version without configuring ntfy                                |
| `--`                      | End wrapper options and start the command and its arguments                        |

Relative configuration paths and `.env` discovery use the directory where you run the CLI.

`clankers --version` prints the installed version. Delivery problems are reported on stderr —
a rejected or unreachable server never changes the exit code — and `-v` logs every file clankers
reads and every request it makes:

```console
$ clankers rogerroger -m "deploy finished" -v
clankers: reading .env file /home/you/project/.env
clankers: configuration provides NTFY_TOKEN, NTFY_TOPIC, NTFY_URL
clankers: publishing to https://ntfy.example.com/deploys with a 10.0s timeout
clankers: ntfy rejected the notification: 403
```

## Python

`clankers.Engage` wraps a block and reports around it: a `blastthem` notification when the block
starts, then `rogerroger` or `uhoh` when it ends, with the duration.

```python
import clankers

with clankers.Engage("Training"):
    train()

with clankers.Engage("Training", announce=False):  # report only the outcome
    train()
```

Each phase can build its message when it is sent, instead of naming it up front. Pass a callable
that takes no arguments and returns the message; it reads whatever the surrounding scope holds at
that moment. The failure builder receives the exception. A phase without a builder reports the
message the context was created with.

```python
state = {"epoch": 0}

with clankers.Engage(
    "Training",
    start=lambda: f"Training from epoch {state['epoch']}",
    success=lambda: f"Training reached epoch {state['epoch']}",
    failure=lambda exc: f"Training died at epoch {state['epoch']}: {exc}",
):
    for state["epoch"] in range(100):
        train_one_epoch()
```

Builders run when the notification is sent, so `success` and `failure` report the final state of
whatever they close over — that is the point of them, and it is up to the caller to keep that state
readable. A builder that fails or returns nothing is logged and the plain message is sent instead;
it never breaks the block it reports on.

The block can also send notifications of its own while it runs, at any of the three levels. These
are extra: the block still reports its own outcome when it exits.

```python
with clankers.Engage("Benchmark") as engage:
    for matchup in matchups:
        engage.blastthem(f"{matchup.name} starting")
        try:
            run(matchup)
            engage.rogerroger(f"{matchup.name} complete")
        except MatchupError as error:
            engage.uhoh(f"{matchup.name} failed: {error}")
```

`clankers.engage` is the decorator form, for a whole function. It reports the start and the outcome
of every call and takes nothing else; use the context manager when you want the rest.

```python
@clankers.engage("Training")
def train(): ...


@clankers.engage()  # the message defaults to the function name
async def evaluate(): ...
```

Normal completion sends success. An exception sends failure and is re-raised unchanged. Async
functions are supported as decorators as well. Configuration is read the first time a notification is
sent, so decorating a function never fails at import time; a missing configuration or an unreachable
ntfy server is logged and never affects the wrapped work.

Clankers logs through the standard `logging` module under the `clankers` logger: failed deliveries at
`WARNING`, every file read and request made at `DEBUG`.

```python
logging.getLogger("clankers").setLevel(logging.DEBUG)
```

Every entry point shares one lazily built backend, so the configuration files are read once per
process. Point it somewhere else — or hand it a ready backend — with `configure()`:

```python
clankers.configure(config_path="./clankers.toml")
```

For a process that reports to more than one topic, build clankers of your own; a `Clanker` owns a
backend and sends notifications, and everything that wraps work takes one with `clanker=`:

```python
training = clankers.Clanker(dotenv_path="./training.env")

with clankers.Engage("Epoch 1", clanker=training):
    ...


@clankers.engage("Evaluation", clanker=training)
def evaluate(): ...


training.rogerroger("Checkpoint uploaded")
```

`clankers.Engage`, `engage`, `rogerroger`, `blastthem` and `uhoh` report through the shared clanker
unless a clanker is named; every configuration option lives on `Clanker` and `configure()`.

For work clankers does not wrap itself, send a notification by hand:

```python
clankers.blastthem("Epoch 40 of 100")
clankers.rogerroger("Checkpoint uploaded")
clankers.uhoh("Validation loss diverged", duration=4200)
```

All three take the same optional `duration=` and, like everything else, only warn when the
notification cannot be delivered.

## Theme

Notifications are labelled `Done`, `Info` and `Failed` by default. For the droid experience, set the
theme to `starwars` and they read `Roger, roger`, `Blast them!` and `Uh-oh` instead:

```toml
[clankers]
theme = "starwars"
```

`CLANKERS_THEME=starwars` works too, in the environment or a `.env` file, and follows the same
priority as the ntfy settings. In Python, `Clanker(theme=...)` and `configure(theme=...)` override
the configuration. An unknown theme makes the CLI exit with status 2 before it runs the command.

## Development

Run development and release commands from the repository root.

```bash
uv sync --project packages/python --group dev
npm ci --prefix packages/nodejs
make check   # Python lint, TypeScript checks, formatting, and shared versions
make fix     # autofix and reformat
make test    # Both packages and release-tool tests
make build   # Python distributions + npm archive, validated for both packages
```

Use `make check-python`, `make test-python`, and `make build-python` to target Python only.

## Releasing

Both packages use one shared version, the root `CHANGELOG.md`, and a paired release process.
Run release commands from the repository root. See
[release setup and recovery](https://github.com/radajakub/clankers/blob/master/docs/releasing.md).
