Metadata-Version: 2.4
Name: emping
Version: 0.1.0
Summary: Multi-host ping monitor with a rich static HTML report.
Author-email: Rasmus Karlsson <rasmus@routeralle.se>
License-Expression: MIT
Project-URL: Documentation, https://github.com/routeralle-se/emping#readme
Project-URL: Homepage, https://github.com/routeralle-se/emping
Project-URL: Issues, https://github.com/routeralle-se/emping/issues
Project-URL: Repository, https://github.com/routeralle-se/emping
Keywords: ping,icmp,monitoring,network,latency,report
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Networking :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jinja2>=3.1
Requires-Dist: markupsafe>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# emping

Ping many hosts at once and produce a **rich, self-contained HTML report**, all built
to deliver a nice report and a clean, reusable Python API.

`emping` is both:

- a **library** you can drop into other projects (`from emping import Monitor`), and
- a **CLI** app (`emping 8.8.8.8 1.1.1.1 -c 10`).

## Highlights

- **Auto-selecting ping backend.** Uses unprivileged ICMP *datagram* sockets
  when the OS allows (macOS, modern Linux) for precise RTT/TTL, and falls back to
  the system `ping` command otherwise (Windows, locked-down Linux). No root
  required in the common case. IPv4 and IPv6 are supported.
- **Continuous or fixed-count** monitoring, on a steady cadence. Built for network
  migrations where minutes or hours of downtime needs to be monitored and recorded.
- **A report that's clear:** summary cards (up/degraded/down, overall loss, average
  & slowest host), click-to-sort columns (numeric-aware), a live text filter,
  color-coded health, per-host **latency sparklines** with packet-loss ticks, and
  light/dark themes. All grafted into a single `.html` file that works offline and
  is easy to distribute.

## Install

```bash
pip install emping
```

Or from a checkout of this repository:

```bash
pip install -e .          # editable install, from this directory
```

## CLI usage

```bash
# Fixed count: 5 pings per host, then write the report and exit
emping 8.8.8.8 1.1.1.1 9.9.9.9 -c 5 -o report.html

# Continuous monitoring until Ctrl-C, updating the report as it goes
emping -f hosts.json --interval 2 -o report.html --open

# Read targets (with descriptions) from a file, auto-refresh the report in-browser
emping -f hosts.json --refresh 5 -o report.html
```

Without installing, use `python -m emping …`.

### Key options

| Option | Meaning | Default |
|---|---|---|
| `hosts …` | targets to ping (positional) | — |
| `-f, --file` | read targets from a JSON file | — |
| `-i, --interval` | seconds between rounds | `1.0` |
| `-c, --count` | pings per host; **`0` = continuous** until Ctrl-C | `0` |
| `-t, --timeout` | per-ping timeout (seconds) | `1.0` |
| `-o, --output` | HTML report path | `ping_report.html` |
| `--backend` | `auto` \| `socket` \| `subprocess` | `auto` |
| `--refresh` | embed an auto-refresh (seconds) in the report | `0` (off) |
| `--report-every` | min seconds between report writes while running | `2.0` |
| `--workers` | max concurrent pings | `min(hosts, 64)` |
| `--open` | open the report in a browser after first write | off |
| `-q, --quiet` | suppress per-round console output | off |

### Targets file format

`-f/--file` reads targets from a JSON file:

```json
{
  "hosts": [
    { "host": "8.8.8.8", "description": "Google DNS" },
    { "host": "192.0.2.1", "description": "Lab gateway", "site": "sto1" },
    "one.one.one.one"
  ]
}
```

Schema:

| | |
|---|---|
| top level | an object with a `hosts` array, **or** the array on its own |
| entry | a string (the host), **or** an object |
| `host` | **required**, string — address or DNS name |
| `description` | optional, string — shown in the report (default `""`) |
| other keys | **ignored**, so the file can double as an inventory carrying `site`, `owner`, ticket refs, etc. |

Anything malformed — bad JSON, a missing `host`, a non-string value — exits `2`
with a message naming the offending entry, e.g. `hosts[3]: object entry is
missing the required 'host' key`. A full example is in `hosts.example.json`.

Run mode is chosen with `-c`: give a count for a batch run, or omit it (or pass
`0`) for continuous monitoring. Continuous runs update the report periodically
and always write a final report on Ctrl-C.

## Library usage

```python
from emping import Monitor, Host, write_report, ReportMeta

monitor = Monitor(
    [Host("8.8.8.8", "Google DNS"), Host("1.1.1.1", "Cloudflare")],
    interval=1.0,
    count=10,          # 0 for continuous; call monitor.stop() from another thread
    timeout=1.0,
)
snapshots = monitor.run()                       # blocks; returns list[HostSnapshot]
write_report("report.html", snapshots, ReportMeta(title="My Report"))
```

For live/streaming use, pass callbacks:

```python
def on_round(round_num, snapshots):
    write_report("report.html", snapshots)      # atomic write, safe to re-read live

Monitor(hosts, count=0, on_round=on_round,
        on_result=lambda i, r: print(r.host, r.rtt_ms)).run()
```

Targets files are loadable directly, without going through the CLI:

```python
from emping import load_hosts, HostFileError

try:
    hosts = load_hosts("hosts.json")        # -> list[Host]; fmt="json"|"text" to force
except HostFileError as exc:
    ...                                     # malformed file; OSError if unreadable
```

Lower-level pieces are exported too: `get_pinger()`, `PingResult`, `HostStats`,
`render_report()`, `build_sparkline()`, `parse_hosts_json()`.

## How pinging works (and privileges)

`get_pinger(backend="auto")` probes for an unprivileged ICMP datagram socket:

- **`socket` backend** — `SOCK_DGRAM` + `IPPROTO_ICMP`/`IPPROTO_ICMPV6`. Precise
  RTT and TTL. Works without root on macOS/BSD and on Linux where
  `net.ipv4.ping_group_range` permits it. Reply parsing is platform-aware (macOS
  includes the IP header, Linux strips it; TTL comes from the header or
  `recvmsg` ancillary data).
- **`subprocess` backend** — shells out to the system `ping`. The portable
  fallback (Windows, locked-down Linux). TTL/reply-IP are parsed from the output.

Force one with `--backend socket` / `--backend subprocess`.

### Known limitation

On **macOS**, the kernel does not deliver **loopback** (`127.0.0.1` / `::1`)
ICMP replies to unprivileged datagram sockets — even the system `ping` shows no
reply line. Loopback isn't a realistic monitoring target, but if you must ping it
on macOS, use `--backend subprocess` (which still returns exit status) or expect
it to read as "down". Remote hosts are unaffected.

## Testing

The package uses a `src/` layout, so install it before running the tests —
`pytest` from the repository root will not otherwise find it.

```bash
pip install -e ".[dev]"
pytest -q          # run unit tests (stats, report, parsing, checksum)
```

## Building a release

```bash
pip install build twine
python -m build            # writes dist/*.whl and dist/*.tar.gz
twine check dist/*
```

## Project layout

```
pyproject.toml
LICENSE
README.md
src/emping/
  pinger.py    # ICMP datagram + subprocess backends, auto-selection
  stats.py     # HostStats accumulator + immutable HostSnapshot
  hosts.py     # JSON targets file loading + validation
  monitor.py   # fixed-cadence, concurrent multi-host ping loop
  report.py    # self-contained HTML rendering (Jinja2), atomic writes
  cli.py       # argparse CLI
  templates/report.html.j2
tests/test_emping.py
```

## License

MIT.
