Automating Notifications

Automating Notifications

In today's fast-paced world, staying on top of information is crucial. Whether you're monitoring a long-running script, tracking system metrics, or just want to be alerted when specific events occur, automating notifications can save you time and keep you informed. Python offers several excellent libraries to help you send alerts through various channels like email, SMS, or even messaging apps. Let's explore how you can set up automated notifications for your projects.

Why Automate Notifications?

Automating notifications eliminates the need for constant manual checking. Imagine you have a data processing script that takes hours to run. Instead of periodically checking if it's finished, you can set up a notification to alert you when the task is complete. This is especially useful for:

  • Long-running computations or data processing jobs
  • System monitoring and alerting for errors or thresholds
  • Daily/weekly report generation
  • Website status monitoring
  • Backup completion alerts

Automating notifications not only saves time but also ensures you never miss important events. You can respond to issues immediately rather than discovering them hours or days later.

Email Notifications

Email remains one of the most universal notification methods. Python's smtplib module makes it easy to send emails programmatically. Here's a basic example:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email(subject, body, to_email):
    # Your email credentials
    from_email = "your_email@gmail.com"
    password = "your_app_password"  # Use app-specific password for Gmail

    # Create message
    msg = MIMEMultipart()
    msg['From'] = from_email
    msg['To'] = to_email
    msg['Subject'] = subject

    # Attach body
    msg.attach(MIMEText(body, 'plain'))

    # Send email
    try:
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login(from_email, password)
        text = msg.as_string()
        server.sendmail(from_email, to_email, text)
        server.quit()
        print("Email sent successfully!")
    except Exception as e:
        print(f"Failed to send email: {e}")

# Example usage
send_email("Task Completed", "Your data processing is finished!", "recipient@example.com")

For security reasons, it's recommended to use environment variables or configuration files to store your email credentials rather than hardcoding them.

Notification Method Setup Complexity Cost Delivery Speed Reliability
Email Low Free Moderate High
SMS Medium Paid Fast High
Push Notifications Medium Free Instant High
Webhooks High Free Instant Medium

SMS Notifications

For time-sensitive alerts, SMS notifications can be more effective than email. Several services provide APIs for sending SMS messages. Twilio is one of the most popular options:

from twilio.rest import Client

def send_sms(message, to_number):
    # Your Twilio credentials
    account_sid = 'your_account_sid'
    auth_token = 'your_auth_token'
    from_number = 'your_twilio_number'

    client = Client(account_sid, auth_token)

    try:
        message = client.messages.create(
            body=message,
            from_=from_number,
            to=to_number
        )
        print(f"SMS sent! Message SID: {message.sid}")
    except Exception as e:
        print(f"Failed to send SMS: {e}")

# Example usage
send_sms("Server is down! Immediate attention required.", "+1234567890")

Remember that SMS services typically charge per message, so this method is best reserved for critical alerts rather than routine notifications.

When choosing a notification method, consider these factors: - Urgency of the information - Audience preferences and accessibility - Cost constraints - Reliability requirements - Integration complexity with existing systems

Push Notifications

Push notifications are excellent for mobile alerts. Services like Pushbullet or Pushover make it easy to send notifications to your mobile devices:

import requests

def push_notification(title, message):
    # Pushbullet API
    api_token = 'your_access_token'
    url = 'https://api.pushbullet.com/v2/pushes'

    headers = {
        'Access-Token': api_token,
        'Content-Type': 'application/json'
    }

    data = {
        'type': 'note',
        'title': title,
        'body': message
    }

    try:
        response = requests.post(url, json=data, headers=headers)
        if response.status_code == 200:
            print("Push notification sent!")
        else:
            print(f"Failed: {response.text}")
    except Exception as e:
        print(f"Error: {e}")

# Example usage
push_notification("Backup Complete", "The nightly backup finished successfully.")

Push notifications offer near-instant delivery and high visibility, making them ideal for time-sensitive information.

Desktop Notifications

For local machine alerts, you can use desktop notifications that appear as system toast messages:

from plyer import notification
import time

def desktop_alert(title, message):
    notification.notify(
        title=title,
        message=message,
        app_name="Python Notifier",
        timeout=10  # seconds
    )

# Example: Monitor CPU usage and alert if high
# (pseudo-code for illustration)
while True:
    cpu_usage = get_cpu_usage()  # You'd implement this
    if cpu_usage > 90:
        desktop_alert("High CPU Usage", f"CPU at {cpu_usage}%")
    time.sleep(300)  # Check every 5 minutes

The plyer library provides a cross-platform interface for various system features, including notifications.

Notification Type Best For Limitations Setup Time
Email Detailed reports, non-urgent updates May be overlooked in crowded inboxes 15 minutes
SMS Critical, time-sensitive alerts Cost per message, character limits 30 minutes
Push Mobile alerts, quick updates Requires app installation 45 minutes
Desktop Local machine monitoring Only works on user's active session 10 minutes

Scheduling Regular Notifications

Sometimes you want notifications to be sent at regular intervals, such as daily reports or weekly summaries. The schedule library is perfect for this:

import schedule
import time
from datetime import datetime

def daily_report():
    # Generate and send your daily report
    report_data = generate_daily_stats()  # Your function
    send_email("Daily Report", report_data, "team@example.com")

def weekly_summary():
    # Generate and send weekly summary
    summary = generate_weekly_summary()  # Your function
    send_email("Weekly Summary", summary, "manager@example.com")

# Schedule tasks
schedule.every().day.at("09:00").do(daily_report)
schedule.every().monday.at("10:00").do(weekly_summary)

print("Scheduler started...")
while True:
    schedule.run_pending()
    time.sleep(60)  # Check every minute

This approach lets you automate routine reporting without manual intervention, ensuring consistency and timeliness.

Error Handling and Logging

When building notification systems, robust error handling is crucial. You don't want your notification system to fail silently:

import logging

# Set up logging
logging.basicConfig(
    filename='notifications.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def send_notification_with_retry(notification_func, *args, max_retries=3):
    for attempt in range(max_retries):
        try:
            notification_func(*args)
            logging.info("Notification sent successfully")
            return True
        except Exception as e:
            logging.error(f"Attempt {attempt + 1} failed: {str(e)}")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
            else:
                logging.critical("All retry attempts failed")
                return False

# Example usage
send_notification_with_retry(send_email, "Test Subject", "Test Body", "test@example.com")

This retry mechanism with exponential backoff helps handle temporary network issues or service interruptions.

Common notification pitfalls to avoid: - Over-notifying - sending too many alerts leads to notification fatigue - Inadequate error handling - failures going unnoticed - Hardcoded credentials - security risks - No logging - difficult troubleshooting - Single point of failure - relying on one notification method

Advanced: Conditional Notifications

Sometimes you only want to send notifications when certain conditions are met. This prevents unnecessary alerts:

def monitor_disk_space():
    import shutil

    total, used, free = shutil.disk_usage("/")
    free_percent = (free / total) * 100

    # Only alert if disk space is critically low
    if free_percent < 10:
        message = f"Warning: Disk space critically low. Only {free_percent:.1f}% free."
        send_email("Disk Space Alert", message, "admin@example.com")
        push_notification("Disk Alert", message)

    return free_percent

# Run check every hour
schedule.every().hour.do(monitor_disk_space)

Conditional notifications ensure you only receive alerts when action is actually required, reducing noise and increasing the signal-to-noise ratio.

Condition Type Example Scenario Notification Action
Threshold CPU > 90% Send immediate alert
State Change Service down → up Send recovery notice
Time-based Daily at 9 AM Send routine report
Event-based New user signup Send welcome message

Integrating Multiple Notification Channels

For important alerts, you might want to use multiple channels to ensure the message is received:

def multi_channel_alert(title, message, urgency="medium"):
    # Always send email for record-keeping
    send_email(title, message, "alerts@example.com")

    if urgency == "high":
        send_sms(message, "+1234567890")
        push_notification(title, message)
    elif urgency == "critical":
        # All channels for critical alerts
        send_sms(message, "+1234567890")
        push_notification(title, message)
        desktop_alert(title, message)
        # Potentially add phone call here

# Example usage
multi_channel_alert("Database Error", "Primary database connection failed", "critical")

This escalation strategy ensures that critical alerts receive maximum attention while routine notifications use less intrusive methods.

Testing Your Notification System

Before relying on your automated notifications, thorough testing is essential:

def test_notification_system():
    """Test all notification channels"""
    test_cases = [
        (send_email, "Test Email", "This is a test email", "test@example.com"),
        (push_notification, "Test Push", "This is a test push"),
        # Add other notification functions
    ]

    for test_func, *args in test_cases:
        try:
            success = test_func(*args)
            if success:
                print(f"✓ {test_func.__name__} test passed")
            else:
                print(f"✗ {test_func.__name__} test failed")
        except Exception as e:
            print(f"✗ {test_func.__name__} test error: {e}")

# Run tests during development
test_notification_system()

Regular testing ensures your notification system remains reliable and functional as your code evolves.

Best practices for notification systems: - Test thoroughly before deployment - Implement retry logic for failed deliveries - Use appropriate channels for different urgency levels - Monitor your monitoring - ensure the system itself is working - Review and adjust frequency based on feedback - Secure credentials properly - Document procedures for different alert types

Real-world Example: Website Monitoring

Let's create a complete website monitoring script with notifications:

import requests
import time
import logging
from requests.exceptions import RequestException

def check_website(url, expected_status=200):
    try:
        response = requests.get(url, timeout=10)
        if response.status_code == expected_status:
            return True, f"Website {url} is up"
        else:
            return False, f"Website {url} returned status {response.status_code}"
    except RequestException as e:
        return False, f"Website {url} is down: {str(e)}"

def monitor_websites(websites, check_interval=300):
    """Monitor multiple websites with notifications"""
    down_status = {}  # Track which sites are down

    while True:
        for url, expected_status in websites.items():
            is_up, message = check_website(url, expected_status)

            if not is_up and url not in down_status:
                # Site just went down
                multi_channel_alert("Website Down", message, "high")
                down_status[url] = True
                logging.error(message)
            elif is_up and url in down_status:
                # Site came back up
                multi_channel_alert("Website Restored", f"{url} is back online", "medium")
                del down_status[url]
                logging.info(f"{url} restored")

        time.sleep(check_interval)

# Websites to monitor with expected status codes
websites_to_monitor = {
    "https://example.com": 200,
    "https://api.example.com": 200,
    "https://status.example.com": 200
}

# Start monitoring
monitor_websites(websites_to_monitor)

This comprehensive example shows how you can build a robust monitoring system that only notifies you when status changes occur, avoiding repetitive alerts for ongoing issues.

Handling Rate Limits and quotas

Many notification services have rate limits or usage quotas. It's important to handle these gracefully:

from functools import wraps
import time

def rate_limited(max_per_minute):
    """Decorator to limit function calls"""
    min_interval = 60.0 / max_per_minute
    def decorator(func):
        last_called = [0.0]
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            left_to_wait = min_interval - elapsed
            if left_to_wait > 0:
                time.sleep(left_to_wait)
            ret = func(*args, **kwargs)
            last_called[0] = time.time()
            return ret
        return wrapper
    return decorator

# Apply rate limiting to your notification functions
@rate_limited(60)  # Max 60 calls per minute
def send_sms_limited(message, to_number):
    return send_sms(message, to_number)

This rate limiting decorator helps you stay within service limits and avoid being blocked for excessive API calls.

Remember that effective notification systems strike a balance between being informative and being intrusive. The goal is to keep users informed without causing alert fatigue. Start with the most critical notifications and expand gradually as you learn what information is truly valuable to receive automatically.

As you implement your notification system, keep refining it based on actual usage patterns and feedback. The best systems evolve over time to become more precise and less intrusive while maintaining their reliability and usefulness.