watchman-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- watchman_sdk-0.1.0/PKG-INFO +5 -0
- watchman_sdk-0.1.0/pyproject.toml +9 -0
- watchman_sdk-0.1.0/setup.cfg +4 -0
- watchman_sdk-0.1.0/tests/test_client.py +28 -0
- watchman_sdk-0.1.0/watchman_sdk/__init__.py +39 -0
- watchman_sdk-0.1.0/watchman_sdk/client.py +47 -0
- watchman_sdk-0.1.0/watchman_sdk/error_capture.py +17 -0
- watchman_sdk-0.1.0/watchman_sdk/heartbeat.py +16 -0
- watchman_sdk-0.1.0/watchman_sdk.egg-info/PKG-INFO +5 -0
- watchman_sdk-0.1.0/watchman_sdk.egg-info/SOURCES.txt +11 -0
- watchman_sdk-0.1.0/watchman_sdk.egg-info/dependency_links.txt +1 -0
- watchman_sdk-0.1.0/watchman_sdk.egg-info/requires.txt +1 -0
- watchman_sdk-0.1.0/watchman_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sys, os
|
|
2
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from watchman_sdk.client import WatchmanClient
|
|
5
|
+
|
|
6
|
+
def test_send_heartbeat_success():
|
|
7
|
+
client = WatchmanClient("test-svc", "token123", "http://localhost:8000")
|
|
8
|
+
mock_resp = MagicMock()
|
|
9
|
+
mock_resp.status_code = 200
|
|
10
|
+
with patch("httpx.post", return_value=mock_resp):
|
|
11
|
+
result = client.send_heartbeat()
|
|
12
|
+
assert result is True
|
|
13
|
+
|
|
14
|
+
def test_send_heartbeat_failure():
|
|
15
|
+
client = WatchmanClient("test-svc", "token123", "http://localhost:8000")
|
|
16
|
+
with patch("httpx.post", side_effect=Exception("Connection refused")):
|
|
17
|
+
result = client.send_heartbeat()
|
|
18
|
+
assert result is False
|
|
19
|
+
|
|
20
|
+
def test_send_event():
|
|
21
|
+
client = WatchmanClient("test-svc", "token123", "http://localhost:8000")
|
|
22
|
+
mock_resp = MagicMock()
|
|
23
|
+
mock_resp.status_code = 200
|
|
24
|
+
with patch("httpx.post", return_value=mock_resp) as mock_post:
|
|
25
|
+
result = client.send_event("error", "critical", "Something failed")
|
|
26
|
+
assert result is True
|
|
27
|
+
call_args = mock_post.call_args
|
|
28
|
+
assert call_args[1]["json"]["type"] == "error"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from watchman_sdk.client import WatchmanClient
|
|
3
|
+
from watchman_sdk.heartbeat import HeartbeatThread
|
|
4
|
+
from watchman_sdk.error_capture import setup_error_capture
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
_client: Optional[WatchmanClient] = None
|
|
8
|
+
_heartbeat: Optional[HeartbeatThread] = None
|
|
9
|
+
|
|
10
|
+
def init(token: str, core_url: str, service: str = None, heartbeat_interval: int = 60) -> None:
|
|
11
|
+
global _client, _heartbeat
|
|
12
|
+
_client = WatchmanClient(service=service, token=token, core=core_url)
|
|
13
|
+
setup_error_capture(_client)
|
|
14
|
+
_heartbeat = HeartbeatThread(_client, interval_seconds=heartbeat_interval)
|
|
15
|
+
_heartbeat.start()
|
|
16
|
+
|
|
17
|
+
def send_event(type: str, severity: str, message: str, meta: dict = None) -> bool:
|
|
18
|
+
if _client is None:
|
|
19
|
+
raise RuntimeError("watchman.init() must be called first")
|
|
20
|
+
return _client.send_event(type=type, severity=severity, message=message, meta=meta)
|
|
21
|
+
|
|
22
|
+
def check(name: str):
|
|
23
|
+
"""Decorator for check functions."""
|
|
24
|
+
def decorator(func):
|
|
25
|
+
@functools.wraps(func)
|
|
26
|
+
def wrapper(*args, **kwargs):
|
|
27
|
+
if _client is None:
|
|
28
|
+
raise RuntimeError("watchman.init() must be called first")
|
|
29
|
+
result = func(*args, **kwargs)
|
|
30
|
+
ok = result.get("ok", False)
|
|
31
|
+
_client.send_event(
|
|
32
|
+
type="check",
|
|
33
|
+
severity="critical" if not ok else "info",
|
|
34
|
+
message=f"Check {name}: {'ok' if ok else 'failed'}",
|
|
35
|
+
meta={**result, "check_name": name},
|
|
36
|
+
)
|
|
37
|
+
return result
|
|
38
|
+
return wrapper
|
|
39
|
+
return decorator
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
|
|
4
|
+
class WatchmanClient:
|
|
5
|
+
def __init__(self, service: str, token: str, core: str):
|
|
6
|
+
self.service = service
|
|
7
|
+
self.token = token
|
|
8
|
+
self.core = core.rstrip("/")
|
|
9
|
+
|
|
10
|
+
def _headers(self) -> dict:
|
|
11
|
+
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
|
12
|
+
|
|
13
|
+
def send_heartbeat(self, version: Optional[str] = None) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
resp = httpx.post(
|
|
16
|
+
f"{self.core}/api/heartbeat",
|
|
17
|
+
json={"service": self.service, "status": "alive", "version": version},
|
|
18
|
+
headers=self._headers(),
|
|
19
|
+
timeout=5.0,
|
|
20
|
+
)
|
|
21
|
+
return resp.status_code == 200
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def send_event(
|
|
26
|
+
self,
|
|
27
|
+
type: str,
|
|
28
|
+
severity: str,
|
|
29
|
+
message: str,
|
|
30
|
+
meta: Optional[dict[str, Any]] = None,
|
|
31
|
+
) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
resp = httpx.post(
|
|
34
|
+
f"{self.core}/api/events",
|
|
35
|
+
json={
|
|
36
|
+
"service": self.service,
|
|
37
|
+
"type": type,
|
|
38
|
+
"severity": severity,
|
|
39
|
+
"message": message,
|
|
40
|
+
"meta": meta or {},
|
|
41
|
+
},
|
|
42
|
+
headers=self._headers(),
|
|
43
|
+
timeout=5.0,
|
|
44
|
+
)
|
|
45
|
+
return resp.status_code == 200
|
|
46
|
+
except Exception:
|
|
47
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
def setup_error_capture(client) -> None:
|
|
5
|
+
original_hook = sys.excepthook
|
|
6
|
+
|
|
7
|
+
def custom_excepthook(exc_type, exc_value, exc_tb):
|
|
8
|
+
tb_str = "".join(traceback.format_tb(exc_tb))
|
|
9
|
+
client.send_event(
|
|
10
|
+
type="error",
|
|
11
|
+
severity="critical",
|
|
12
|
+
message=f"{exc_type.__name__}: {exc_value}",
|
|
13
|
+
meta={"traceback": tb_str},
|
|
14
|
+
)
|
|
15
|
+
original_hook(exc_type, exc_value, exc_tb)
|
|
16
|
+
|
|
17
|
+
sys.excepthook = custom_excepthook
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
class HeartbeatThread(threading.Thread):
|
|
4
|
+
def __init__(self, client, interval_seconds: int = 60):
|
|
5
|
+
super().__init__(daemon=True)
|
|
6
|
+
self.client = client
|
|
7
|
+
self.interval = interval_seconds
|
|
8
|
+
self._stop_event = threading.Event()
|
|
9
|
+
|
|
10
|
+
def run(self) -> None:
|
|
11
|
+
while not self._stop_event.is_set():
|
|
12
|
+
self.client.send_heartbeat()
|
|
13
|
+
self._stop_event.wait(self.interval)
|
|
14
|
+
|
|
15
|
+
def stop(self) -> None:
|
|
16
|
+
self._stop_event.set()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
tests/test_client.py
|
|
3
|
+
watchman_sdk/__init__.py
|
|
4
|
+
watchman_sdk/client.py
|
|
5
|
+
watchman_sdk/error_capture.py
|
|
6
|
+
watchman_sdk/heartbeat.py
|
|
7
|
+
watchman_sdk.egg-info/PKG-INFO
|
|
8
|
+
watchman_sdk.egg-info/SOURCES.txt
|
|
9
|
+
watchman_sdk.egg-info/dependency_links.txt
|
|
10
|
+
watchman_sdk.egg-info/requires.txt
|
|
11
|
+
watchman_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
watchman_sdk
|