From 16d6fd92f6d8750632fc0e97cd1950100956dfa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 14:31:45 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20reestruturar=20como=20m=C3=B3dulo=20Pyt?= =?UTF-8?q?hon=20import=C3=A1vel=20(health=5Fcheck/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cria health_check/ com __init__.py, _ping.py e _config.py - healthcheck.py vira CLI fino que usa o pacote - pyproject.toml completo com build system (hatchling) e metadados - docs/ping-payload.md atualizado com instruções de uso como biblioteca --- docs/ping-payload.md | 69 +++++++++++++++++++++++++++++++++----- health_check/__init__.py | 7 ++++ health_check/_config.py | 17 ++++++++++ health_check/_ping.py | 50 ++++++++++++++++++++++++++++ healthcheck.py | 71 +++++++--------------------------------- pyproject.toml | 20 +++++++++-- 6 files changed, 164 insertions(+), 70 deletions(-) create mode 100644 health_check/__init__.py create mode 100644 health_check/_config.py create mode 100644 health_check/_ping.py diff --git a/docs/ping-payload.md b/docs/ping-payload.md index e202536..d3df316 100644 --- a/docs/ping-payload.md +++ b/docs/ping-payload.md @@ -2,6 +2,64 @@ This document defines the standard payload for all health check pings sent to Healthchecks.io across all agents (scripts, bots, services). +## Why not use an existing library? + +The [`healthchecks-io`](https://pypi.org/project/healthchecks-io/) package on PyPI focuses on the management API and does not send a custom body payload. This library fills that gap with an opinionated, consistent payload that identifies the agent, device, IPs, and uptime on every ping. + +--- + +## Using as a Python module + +Install directly from the repository: + +```bash +pip install git+https://github.com/SantosFC/health-check.git +``` + +### Basic usage + +```python +from health_check import ping, ping_fail + +url = "https://hc-ping.com/" + +try: + status = ping(url, agent="my-telegram-bot", device="my-server") + if 200 <= status < 300: + print("Ping OK") + else: + ping_fail(url, agent="my-telegram-bot", device="my-server") +except Exception as exc: + ping_fail(url, agent="my-telegram-bot", device="my-server") + raise +``` + +### Loading config from file + +```python +from health_check import load_config, ping + +config = load_config() # reads ~/.config/health-check +ping(config["HEALTHCHECK_URL"], agent="my-bot", device=config["DEVICE_NAME"]) +``` + +### Custom config path + +```python +from pathlib import Path +from health_check import load_config + +config = load_config(config_file=Path("/etc/my-bot/config")) +``` + +### Timeout + +```python +ping(url, agent="my-bot", device="server", timeout=5) +``` + +--- + ## HTTP Request - **Method:** `POST` @@ -11,16 +69,9 @@ This document defines the standard payload for all health check pings sent to He | Header | Value | |---|---| -| `User-Agent` | `/` | +| `User-Agent` | `health-check/` | | `Content-Type` | `application/json` | -The `User-Agent` must identify the agent and its version. Examples: - -``` -health-check/1.0.2 -telegram-bot-alerts/2.1.0 -``` - ## Body ```json @@ -52,4 +103,4 @@ telegram-bot-alerts/2.1.0 ## Failure Ping -On error, agents must send a POST to `/fail` with the same body. +On error, agents must send a POST to `/fail` with the same body. The `ping_fail` function handles this automatically. diff --git a/health_check/__init__.py b/health_check/__init__.py new file mode 100644 index 0000000..1598366 --- /dev/null +++ b/health_check/__init__.py @@ -0,0 +1,7 @@ +"""health-check: lightweight Healthchecks.io ping client.""" + +from health_check._config import load_config +from health_check._ping import ping, ping_fail + +__version__ = "1.0.2" +__all__ = ["ping", "ping_fail", "load_config"] diff --git a/health_check/_config.py b/health_check/_config.py new file mode 100644 index 0000000..c554fb8 --- /dev/null +++ b/health_check/_config.py @@ -0,0 +1,17 @@ +import os +from pathlib import Path + + +def load_config(config_file: Path | None = None) -> dict: + """Load configuration from file, falling back to environment variables.""" + path = config_file or Path.home() / ".config" / "health-check" + config = {} + if path.exists(): + for line in path.read_text().splitlines(): + if "=" in line: + key, value = line.split("=", 1) + config[key.strip()] = value.strip() + + config.setdefault("HEALTHCHECK_URL", os.environ.get("HEALTHCHECK_URL", "")) + config.setdefault("DEVICE_NAME", os.environ.get("DEVICE_NAME", "unknown")) + return config diff --git a/health_check/_ping.py b/health_check/_ping.py new file mode 100644 index 0000000..b0971e4 --- /dev/null +++ b/health_check/_ping.py @@ -0,0 +1,50 @@ +import json +import subprocess +import urllib.error +import urllib.request +from pathlib import Path + +_USER_AGENT = "health-check/1.0.2" + + +def _build_body(agent: str, device: str) -> bytes: + try: + ips = subprocess.check_output(["hostname", "-I"], timeout=5).decode().split() + except Exception: + ips = [] + + try: + seconds = int(float(Path("/proc/uptime").read_text().split()[0])) + d, rem = divmod(seconds, 86400) + h, rem = divmod(rem, 3600) + m = rem // 60 + uptime = f"{d}d {h}h {m}m" + except Exception: + uptime = "unknown" + + return json.dumps({"user": agent, "device": device, "ips": ips, "uptime": uptime}).encode() + + +def ping(url: str, agent: str, device: str, timeout: int = 10) -> int: + """Send a success ping to Healthchecks.io. Returns the HTTP status code.""" + body = _build_body(agent, device) + req = urllib.request.Request( + url, data=body, method="POST", + headers={"User-Agent": _USER_AGENT, "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=timeout) as response: + return response.getcode() + + +def ping_fail(url: str, agent: str, device: str, timeout: int = 10) -> None: + """Send a failure ping to Healthchecks.io (/fail).""" + body = _build_body(agent, device) + fail_url = url.rstrip("/") + "/fail" + try: + req = urllib.request.Request( + fail_url, data=body, method="POST", + headers={"User-Agent": _USER_AGENT, "Content-Type": "application/json"}, + ) + urllib.request.urlopen(req, timeout=timeout) + except Exception: + pass diff --git a/healthcheck.py b/healthcheck.py index 5bfbfda..a09b138 100644 --- a/healthcheck.py +++ b/healthcheck.py @@ -1,49 +1,17 @@ #!/usr/bin/env python3 -import json +"""CLI entry point for health-check.""" + import os -import subprocess import sys -import urllib.request -import urllib.error from datetime import datetime, timezone -from pathlib import Path -USER_NAME = "Ronaldo Freitas Dias" +import urllib.error +from health_check import ping, ping_fail, load_config + +AGENT = "Ronaldo Freitas Dias" -def load_config(): - config_file = Path.home() / ".config" / "health-check" - config = {} - if config_file.exists(): - for line in config_file.read_text().splitlines(): - if "=" in line: - key, value = line.split("=", 1) - config[key.strip()] = value.strip() - return config - - -def ping(url: str, body: bytes, timeout: int) -> int: - req = urllib.request.Request( - url, data=body, method="POST", - headers={"User-Agent": "health-check/1.0.2", "Content-Type": "application/json"} - ) - with urllib.request.urlopen(req, timeout=timeout) as response: - return response.getcode() - - -def ping_fail(url: str, body: bytes, timeout: int) -> None: - fail_url = url.rstrip("/") + "/fail" - try: - req = urllib.request.Request( - fail_url, data=body, method="POST", - headers={"User-Agent": "health-check/1.0.2", "Content-Type": "application/json"} - ) - urllib.request.urlopen(req, timeout=timeout) - except Exception: - pass - - -def main(): +def main() -> int: config = load_config() url = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("HEALTHCHECK_URL") or config.get("HEALTHCHECK_URL") if not url: @@ -52,43 +20,28 @@ def main(): device = os.environ.get("DEVICE_NAME") or config.get("DEVICE_NAME", "unknown") timeout = int(os.environ.get("HEALTHCHECK_TIMEOUT", 10)) - try: - ips = subprocess.check_output(["hostname", "-I"], timeout=5).decode().split() - except Exception: - ips = [] try: - seconds = int(float(Path("/proc/uptime").read_text().split()[0])) - d, rem = divmod(seconds, 86400) - h, rem = divmod(rem, 3600) - m = rem // 60 - uptime = f"{d}d {h}h {m}m" - except Exception: - uptime = "unknown" - - body = json.dumps({"user": USER_NAME, "device": device, "ips": ips, "uptime": uptime}).encode() - - try: - status = ping(url, body, timeout) + status = ping(url, agent=AGENT, device=device, timeout=timeout) if 200 <= status < 300: print(f"{datetime.now(timezone.utc).isoformat()} status={status} device={device} url={url}") return 0 print(f"ERROR: HTTP {status} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 2 except urllib.error.HTTPError as exc: print(f"HTTPError {exc.code} {exc.reason} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 3 except urllib.error.URLError as exc: print(f"URLError {exc.reason} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 4 except Exception as exc: print(f"ERROR {exc} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 5 diff --git a/pyproject.toml b/pyproject.toml index bb1773c..b088fab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,19 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "health-check" +version = "1.0.2" +description = "Lightweight Healthchecks.io ping client with opinionated payload (agent, device, ips, uptime)" +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } +dependencies = [] + +[project.scripts] +health-check = "healthcheck:main" + [tool.ruff] target-version = "py39" line-length = 110 @@ -13,8 +29,8 @@ select = [ "SIM", # flake8-simplify ] ignore = [ - "B904", # raise from exc — não obrigatório em scripts simples + "B904", ] [tool.ruff.lint.isort] -known-first-party = ["healthcheck"] +known-first-party = ["health_check"]