BearWatch Python SDK - Job monitoring with heartbeat-based detection
Project description
bearwatch
Official BearWatch SDK for Python - Job monitoring and alerting for indie developers.
Installation
pip install bearwatch
Requirements
- Python 3.9 or higher
- httpx >= 0.25.0 (installed automatically)
Quick Start
1. Get API Key
Go to BearWatch Dashboard → Project Settings → Create API Key (e.g., bw_kI6t8QA21on0DKeRDlen8r2hzucVNL3WdAfaZgQdetY).
2. Create a Job
Create a job in the dashboard. You'll get a job ID (24-character hex string, e.g., 507f1f77bcf86cd799439011).
3. Install and Use
Let's assume you have a daily backup job that runs at 2:00 AM:
from apscheduler.schedulers.blocking import BlockingScheduler
from bearwatch import BearWatch
bw = BearWatch(api_key="your-api-key")
def backup_job():
bw.wrap("507f1f77bcf86cd799439011", lambda: backup())
scheduler = BlockingScheduler()
scheduler.add_job(backup_job, "cron", hour=2)
scheduler.start()
Usage
ping - Manual Status Reporting
Use ping when you need fine-grained control over status reporting:
def backup_job():
try:
backup()
bw.ping("507f1f77bcf86cd799439011", status="SUCCESS")
except Exception as e:
bw.ping("507f1f77bcf86cd799439011", status="FAILED", error=str(e))
Include output and metadata:
def backup_job():
bytes_written = backup()
bw.ping(
"507f1f77bcf86cd799439011",
status="SUCCESS",
output=f"Backup completed: {bytes_written} bytes",
metadata={
"server": "backup-01",
"region": "ap-northeast-2",
"version": "1.2.0",
},
)
PingOptions
| Option | Type | Default | Description |
|---|---|---|---|
status |
RequestStatus |
"SUCCESS" |
"RUNNING", "SUCCESS", or "FAILED" |
output |
str |
- | Output message (max 10KB) |
error |
str |
- | Error message for FAILED status (max 10KB) |
started_at |
datetime | str |
current time | Job start time |
completed_at |
datetime | str |
current time | Job completion time |
metadata |
dict[str, Any] |
- | Additional key-value pairs (max 10KB) |
retry |
bool |
True |
Enable/disable retry |
Note:
TIMEOUTandMISSEDare server-detected states and cannot be set in requests.
wrap - Automatic Status Reporting
Wraps a function and automatically:
- Measures
started_atandcompleted_at - Reports
SUCCESSorFAILEDbased on whether the function completes or throws
def backup_job():
bw.wrap("507f1f77bcf86cd799439011", lambda: backup())
Error handling behavior:
- On success: reports
SUCCESSwith execution duration - On error: reports
FAILEDwith error message, then re-raises the original exception
def backup_job():
try:
bw.wrap("507f1f77bcf86cd799439011", lambda: backup())
except Exception as e:
# BearWatch already reported FAILED status
# You can add additional error handling here
logger.error(e)
Tip: Use
wrapfor most cases. Usepingwhen you need more control (e.g., reporting RUNNING status for long jobs).
Async Support
The SDK provides async versions of all methods:
# Async ping
await bw.ping_async("507f1f77bcf86cd799439011")
# Async ping with options
await bw.ping_async("507f1f77bcf86cd799439011", status="FAILED", error="Timeout")
# Async wrap
result = await bw.wrap_async("507f1f77bcf86cd799439011", async_backup)
Configuration
bw = BearWatch(
api_key="your-api-key",
# Optional (defaults shown)
timeout=30.0, # 30 seconds
max_retries=3,
retry_delay=0.5, # 500ms base delay
)
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
api_key |
str |
Yes | - | API key for authentication |
timeout |
float |
No | 30.0 |
Request timeout (seconds) |
max_retries |
int |
No | 3 |
Max retry attempts |
retry_delay |
float |
No | 0.5 |
Initial retry delay (seconds) |
Context Manager
Use context managers for automatic resource cleanup:
# Sync
with BearWatch(api_key="your-api-key") as bw:
bw.ping("507f1f77bcf86cd799439011")
# Async
async with BearWatch(api_key="your-api-key") as bw:
await bw.ping_async("507f1f77bcf86cd799439011")
Retry Policy
| Method | Default Retry | Reason |
|---|---|---|
ping() |
Enabled | Idempotent operation |
ping_async() |
Enabled | Idempotent operation |
wrap() |
Enabled | Uses ping() internally |
wrap_async() |
Enabled | Uses ping_async() internally |
Retry Behavior
- Exponential backoff: 500ms → 1000ms → 2000ms
- 429 Rate Limit: Respects
Retry-Afterheader (rate limit: 100 requests/minute per API key) - 5xx Server Errors: Retries with backoff
- 401/404: No retry (client errors)
Disable Retry
# Disable retry for a specific call
bw.ping("507f1f77bcf86cd799439011", retry=False)
Error Handling
When the SDK fails to communicate with BearWatch (network failure, server down, invalid API key, etc.), it raises a BearWatchError:
from bearwatch import BearWatch, BearWatchError
try:
bw.ping("507f1f77bcf86cd799439011")
except BearWatchError as e:
# SDK failed to report to BearWatch
print(f"Code: {e.code}")
print(f"Status: {e.status_code}")
print(f"Context: {e.context}")
Error Codes
| Code | Description | Retry |
|---|---|---|
INVALID_API_KEY |
401 - Invalid API key | No |
JOB_NOT_FOUND |
404 - Job not found | No |
RATE_LIMITED |
429 - Rate limit reached | Yes |
SERVER_ERROR |
5xx - Server error | Yes |
INVALID_RESPONSE |
Unexpected response format | No |
NETWORK_ERROR |
Network failure | Yes |
TIMEOUT |
Request timed out | Yes |
Type Hints
The SDK includes full type hints for IDE support:
from bearwatch import (
BearWatch,
BearWatchConfig,
BearWatchError,
ErrorCode,
ErrorContext,
HeartbeatResponse,
PingOptions,
WrapOptions,
RequestStatus, # For requests: "RUNNING" | "SUCCESS" | "FAILED"
ResponseStatus, # For responses: includes "TIMEOUT" | "MISSED"
Status, # Alias for ResponseStatus
)
Method Signatures
class BearWatch:
def __init__(
self,
api_key: str,
*,
timeout: float = 30.0,
max_retries: int = 3,
retry_delay: float = 0.5,
) -> None: ...
@classmethod
def create(cls, config: BearWatchConfig) -> BearWatch: ...
def ping(
self,
job_id: str,
*,
status: RequestStatus = "SUCCESS",
output: str | None = None,
error: str | None = None,
started_at: datetime | str | None = None,
completed_at: datetime | str | None = None,
metadata: dict[str, Any] | None = None,
retry: bool = True,
) -> HeartbeatResponse: ...
def wrap(
self,
job_id: str,
fn: Callable[[], T],
*,
output: str | None = None,
metadata: dict[str, Any] | None = None,
retry: bool = True,
) -> T: ...
async def ping_async(
self,
job_id: str,
*,
status: RequestStatus = "SUCCESS",
output: str | None = None,
error: str | None = None,
started_at: datetime | str | None = None,
completed_at: datetime | str | None = None,
metadata: dict[str, Any] | None = None,
retry: bool = True,
) -> HeartbeatResponse: ...
async def wrap_async(
self,
job_id: str,
fn: Callable[[], Awaitable[T]],
*,
output: str | None = None,
metadata: dict[str, Any] | None = None,
retry: bool = True,
) -> T: ...
Common Patterns
APScheduler
from apscheduler.schedulers.blocking import BlockingScheduler
from bearwatch import BearWatch
bw = BearWatch(api_key="your-api-key")
def backup_job():
bw.wrap("6848c9e5f8a2b3d4e5f60001", lambda: backup())
scheduler = BlockingScheduler()
scheduler.add_job(backup_job, "cron", hour=3)
scheduler.start()
Celery Beat
from celery import Celery
from bearwatch import BearWatch
app = Celery("tasks")
bw = BearWatch(api_key="your-api-key")
@app.task
def backup_task():
bw.wrap("6848c9e5f8a2b3d4e5f60002", lambda: backup())
AWS Lambda (EventBridge Scheduler)
import os
from bearwatch import BearWatch
bw = BearWatch(api_key=os.environ["BEARWATCH_API_KEY"])
def handler(event, context):
bw.wrap("6848c9e5f8a2b3d4e5f60003", lambda: backup())
Long-Running Jobs
from datetime import datetime, timezone
def run_backup():
job_id = "6848c9e5f8a2b3d4e5f60004"
started_at = datetime.now(timezone.utc)
bw.ping(job_id, status="RUNNING")
try:
backup()
bw.ping(
job_id,
status="SUCCESS",
started_at=started_at,
completed_at=datetime.now(timezone.utc),
)
except Exception as e:
bw.ping(
job_id,
status="FAILED",
started_at=started_at,
completed_at=datetime.now(timezone.utc),
error=str(e),
)
raise
FAQ
Q: Do I need to create jobs in the dashboard first? A: Yes, create a job in the BearWatch Dashboard first to get a job ID.
Q: What's the difference between wrap and ping?
A: wrap automatically measures execution time and reports SUCCESS/FAILED based on whether the function completes or raises an exception. ping gives you manual control over when and what to report.
Q: What happens if the SDK fails to report (network error)?
A: By default, the SDK attempts up to 4 times total (1 initial + 3 retries) with exponential backoff. If all attempts fail, ping raises a BearWatchError. For wrap, the original function's exception takes priority and is always re-raised.
Q: Can I use this with async frameworks like FastAPI?
A: Yes, use ping_async and wrap_async for async contexts.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bearwatch-0.1.1.tar.gz.
File metadata
- Download URL: bearwatch-0.1.1.tar.gz
- Upload date:
- Size: 15.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b777498bff743d840601726c8a3d668d92e0916ac9ba1a8553b2fbc1ae36f2f
|
|
| MD5 |
6a5e9d59f0685e43b881b2a5159d27f8
|
|
| BLAKE2b-256 |
76f80aeef4b6c23405e42991c31aeeb7b9fc1cd8ae7e9ed67b9251ac78d78dd4
|
File details
Details for the file bearwatch-0.1.1-py3-none-any.whl.
File metadata
- Download URL: bearwatch-0.1.1-py3-none-any.whl
- Upload date:
- Size: 14.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2ba9490ab685557cf91db26eb36a2b7332a775f82044ce7b47b8cab381ac6e9
|
|
| MD5 |
27be608ab7dd1f518cc40c7438b289f0
|
|
| BLAKE2b-256 |
3792a74e54d6c8e01ed53d1aedced3d10a97af67caf57279d175f06ed136e7da
|