import os import requests import logging from typing import Optional from pathlib import Path class PushoverError(Exception): """Custom exception for Pushover-related errors""" pass def notify( message: str, title: Optional[str] = None, attachment: Optional[Path] = None, priority: int = 0 ) -> bool: """ Send a push notification via Pushover API. Args: message: The message to send title: Optional title (defaults to hostname) attachment: Optional file path to attach (supports images, PDFs etc) priority: Message priority (-2 to 2, default 0) -2: Lowest, -1: Low, 0: Normal, 1: High, 2: Emergency Returns: bool: True if successful, False if failed Raises: PushoverError: If environment variables are missing or API call fails """ # Check for required environment variables app_token = os.environ.get('PUSHOVER_APP_TOKEN') user_token = os.environ.get('PUSHOVER_USER_TOKEN') if not app_token or not user_token: raise PushoverError("Missing required environment variables: PUSHOVER_APP_TOKEN, PUSHOVER_USER_TOKEN") # Default title to hostname if not specified if title is None: title = os.uname().nodename # Prepare the POST data data = { 'token': app_token, 'user': user_token, 'message': message, 'title': title, 'priority': priority } files = {} # Handle attachment if provided if attachment: if not attachment.exists(): raise PushoverError(f"Attachment file does not exist: {attachment}") # Check file size (Pushover limit is 5MB) if attachment.stat().st_size > 5 * 1024 * 1024: raise PushoverError("Attachment exceeds Pushover's 5MB limit") files = {'attachment': open(attachment, 'rb')} try: response = requests.post( 'https://api.pushover.net/1/messages.json', data=data, files=files, timeout=10 ) response.raise_for_status() return True except requests.exceptions.RequestException as e: logging.error(f"Failed to send Pushover notification: {str(e)}") return False finally: # Make sure we close the file if it was opened if files and 'attachment' in files: files['attachment'].close() # Optional: Add rate limiting functionality class RateLimiter: """Simple rate limiter for notifications""" def __init__(self, cooldown_seconds: int = 300): self.last_notification = {} self.cooldown = cooldown_seconds def can_notify(self, category: str) -> bool: """Check if enough time has passed since last notification in this category""" import time now = time.time() if category not in self.last_notification: self.last_notification[category] = now return True if now - self.last_notification[category] >= self.cooldown: self.last_notification[category] = now return True return False