Metadata-Version: 2.4
Name: pingcast
Version: 0.1.0a0
Summary: Pluggable Python notification utility with Discord webhook support
Author: Project Notification
License: MIT
Project-URL: Homepage, https://example.com/pingcast
Project-URL: Repository, https://example.com/pingcast/repo
Keywords: notifications,discord,webhook,logging,async
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0
Provides-Extra: async
Requires-Dist: aiohttp>=3.9.0; extra == "async"
Requires-Dist: httpx>=0.27.0; extra == "async"

# 🚀 PingCast

[![CI](https://github.com/Tarikul-Islam-Anik/scipreprocess/actions/workflows/ci.yml/badge.svg)](https://github.com/Tarikul-Islam-Anik/scipreprocess/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](docs/index.md)

`PingCast` is a lightweight Python library that sends status updates from your scripts (e.g., ML training, data pipelines, ETL jobs) to external channels. It starts with **Discord webhooks** and is built to be **plugin‑friendly** for future channels.

* **🔧 Simple API:** `from pingcast import Discord, Notifier` → `Notifier(Discord(WEBHOOK_URL)).everything()`
* **✅ Covers the basics:** success, failure (uncaught exceptions), periodic heartbeats, and optional log forwarding
* **🔁 Sync & async:** `Notifier` (sync) and `AsyncNotifier` (async)
* **🔌 Extensible:** clean plugin interface; future discovery via entry points

---

## 🧭 Table of contents

* [Why](#why)
* [Install](#install)
* [Quickstart](#quickstart)
* [Usage](#usage)
  * [Manual sends](#manual-sends)
  * [Track “everything”](#track-everything)
  * [Filtering & heartbeat interval](#filtering--heartbeat-interval)
  * [Async usage](#async-usage)
* [Documentation](#documentation)
* [How it works (under the hood)](#how-it-works-under-the-hood)
* [Plugins & extensibility](#plugins--extensibility)
* [Configuration](#configuration)
* [Supported Python versions](#supported-python-versions)
* [Troubleshooting](#troubleshooting)
* [Roadmap](#roadmap)
* [Contributing](#contributing)
* [Security](#security)
* [Code of Conduct](#code-of-conduct)
* [License](#license)

---

## ❓ Why

Training LLMs and running heavy experiments takes hours. People don’t babysit terminals the whole time, and then return to discover the job failed hours ago. `PingCast` keeps you in the loop:

* ✅ **Completion notices** when the script exits normally
* ❌ **Crash alerts** when an uncaught exception terminates your program (via `sys.excepthook`) ([Python documentation][2])
* ⏱️ **Periodic heartbeats** (default every 15 minutes) so you know it’s still running
* 🧾 **Optional log forwarding** using the standard `logging` handlers (no invasive framework) ([Python documentation][3])

---

## 📦 Install

```bash
pip install pingcast
```

---

## 🚀 Quickstart

1. Create a **Discord webhook** in your channel (Server Settings → Integrations → Webhooks), then copy the URL.

2. Use it in code:

```python
from pingcast import Discord, Notifier

discord = Discord("https://discord.com/api/webhooks/XXXXXXXX/XXXXXXXX")
notify = Notifier(discord)

def main():
    # one line to wire everything:
    notify.everything(filter={"level": "INFO"}, interval=900)  # 15 min heartbeats

    # your long job
    import logging, time
    logging.info("training started")
    for epoch in range(5):
        time.sleep(5)
        logging.info("epoch %d done", epoch + 1)

    # if an uncaught exception happens, you'll get a ❌ alert

if __name__ == "__main__":
    main()
```

This sends:

* periodic “still running” messages,
* your `logging` messages (respecting the filter),
* a final ✅ on normal exit, or ❌ on crash.

---

## 🧰 Usage

### ✉️ Manual sends

```python
notify.send("Checkpoint saved", {"epoch": 12, "val_loss": 0.217})
```

### 🔭 Track “everything”

```python
notify.everything()  # start log capture, heartbeats, success/exception hooks
```

This wires:

* a `logging.Handler` to forward records,
* a heartbeat thread for periodic pings,
* `atexit` hook for success on normal interpreter termination,
* `sys.excepthook` to report uncaught exceptions before exit.
  (Functions registered with `atexit` run automatically on **normal interpreter termination**.) ([Python documentation][5])

### ⏱️ Filtering & heartbeat interval

```python
# only ERROR+ logs, heartbeat every 10 minutes
notify.everything(filter={"level": "ERROR"}, interval=600)
```

Supported filter keys (initially):

* `level`: `"DEBUG"|"INFO"|"WARNING"|"ERROR"|"CRITICAL"`

### ⚙️ Async usage

```python
import asyncio
from pingcast import AsyncNotifier, Discord

async def run():
    discord = Discord("https://discord.com/api/webhooks/…")
    notify = AsyncNotifier(discord)
    await notify.send("Job kicked off")
    await notify.everything(filter={"level": "WARNING"}, interval=900)
    # … your async workload …

asyncio.run(run())
```

---

## 📚 Documentation

See the docs site for a brief overview and getting started guide:

[docs/index.md](docs/index.md)

## 🛠️ How it works (under the hood)

* **Logging**: attaches a `logging.Handler` to forward formatted records; you keep using Python’s standard logging API. ([Python documentation][3])
* **Success on exit**: registers an `atexit` handler that fires on normal termination. (Not triggered on hard kills, fatal internal errors, or `os._exit()`.) ([Python documentation][5])
* **Crash reporting**: installs a custom `sys.excepthook` so uncaught exceptions send a ❌ message before the interpreter exits. You can always restore the original hook. ([Python documentation][2])

---

## 🔌 Plugins & extensibility

`PingCast` ships with a **Discord** plugin first. The architecture is intentionally simple:

* A tiny `BasePlugin` interface with `.send()` / `.send_async()`
* `Notifier` holds a list of plugins and broadcasts to each
* New channels (Slack, Email, Telegram, etc.) can be added with small plugin classes

For **automatic plugin discovery** later, we’ll adopt **entry points** so third-party packages can register plugins that `PingCast` discovers at runtime—this is the recommended approach in the Packaging User Guide. ([Python Packaging][9])

**Example entry point (future):**

```toml
# pyproject.toml of a third-party plugin package
[project.entry-points."pingcast.plugins"]
"slack" = "pingcast_slack:SlackPlugin"
```

Consumers can then do:

```python
notify = Notifier(slack="xoxb-…", discord="https://discord.com/api/webhooks/…")
```

---

## ⚙️ Configuration

Minimal constructor:

```python
from pingcast import Discord, Notifier

notify = Notifier(Discord("https://discord.com/api/webhooks/…"))
```

Common options:

* `filter={"level": "INFO"}` — limit forwarded logs by level
* `interval=900` — heartbeat interval in seconds (default 15 min)
* `notify.send(message, details: dict | None)` — manual push
* Legacy keyword-style initialization (`Notifier(discord="…")`) still works, but new plugin-style construction is preferred so multiple channels can be combined.

> Treat webhook URLs as **secrets**. Don’t commit them; prefer env vars or secret managers.

---

## 🐍 Supported Python versions

We target **Python 3.8+**. (Python 3.7 reached **end-of-life on 2023-06-27**; upgrading is strongly recommended.) ([Python Developer's Guide][10])

---

## 🛟 Troubleshooting

* **No messages arrive**
  • Double-check the webhook URL and channel permissions.
  • Look for HTTP 429s (rate limit) and back off before retrying. ([Discord][7])

* **Too many log messages**
  • Raise your filter level: `filter={"level": "ERROR"}`.
  • Consider sending only summary metrics with `notify.send(...)`.

* **Script was killed (SIGKILL / machine reboot)**
  • `atexit` only runs on **normal termination**; if the process is killed, the success hook won’t fire. ([Python documentation][5])

* **I want fewer heartbeats**
  • Increase `interval` (e.g., `interval=3600` for hourly).

---

## 🗺️ Roadmap

* Additional plugins (Slack/Telegram/Email)
* Optional batching & backoff for rate limits (429) ([Discord][7])
* Automatic plugin discovery via entry points ([Python Packaging][11])
* Structured embeds for richer Discord messages ([Discord][8])

---

## 🤝 Contributing

Issues and PRs welcome! Please:

* Open an issue to discuss larger changes before submitting a PR.
* Follow modern packaging practices (PEP 621 / `pyproject.toml`) and ensure tests pass.
* Keep changes focused and add/adjust tests where appropriate.

If you’re adding a new plugin, start with a tiny class implementing `.send()` and consider registering via entry points. Useful references: **Packaging Projects** tutorial and **Writing your `pyproject.toml`**. ([Python Packaging][12])

See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

---

## 🔐 Security

If you believe you’ve found a security issue, please follow the process in [SECURITY.md](SECURITY.md) rather than opening a public issue.

---

## 📜 Code of Conduct

This project adheres to a Contributor Code of Conduct. By participating, you agree to uphold its terms. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).

---

## 📝 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---

## Citation

If you use this library in your research, please cite:

```bibtex
@article{pingcast,
  title={PingCast: A lightweight Python library for sending status updates to Discord},
  author={Islam, MD. Tarikul}
  year={2025}
  url={https://github.com/Tarikul-Islam-Anik/PingCast}
}
```
