Added pushover support
This commit is contained in:
@@ -84,3 +84,34 @@ email:
|
||||
log_levels:
|
||||
- "ERROR"
|
||||
- "CRITICAL"
|
||||
|
||||
# Pushover configuration for push notifications (alternative to email)
|
||||
pushover:
|
||||
# Pushover API credentials (get from https://pushover.net)
|
||||
user_key: !secret PUSHOVER_USER_KEY # Your user/group key
|
||||
api_token: !secret PUSHOVER_API_TOKEN # Your application API token
|
||||
|
||||
# Monitoring and alerting (same structure as email)
|
||||
monitoring:
|
||||
# Daily report configuration
|
||||
daily_report:
|
||||
enabled: true # Set to true to enable daily reports
|
||||
send_time: "08:00" # Time to send daily report (24h format, local time)
|
||||
include_stats: true # Include reservation/customer stats
|
||||
include_errors: true # Include error summary
|
||||
priority: 0 # Pushover priority: -2=lowest, -1=low, 0=normal, 1=high, 2=emergency
|
||||
|
||||
# Error alert configuration (hybrid approach)
|
||||
error_alerts:
|
||||
enabled: true # Set to true to enable error alerts
|
||||
# Alert is sent immediately if threshold is reached
|
||||
error_threshold: 5 # Send immediate alert after N errors
|
||||
# Otherwise, alert is sent after buffer time expires
|
||||
buffer_minutes: 15 # Wait N minutes before sending buffered errors
|
||||
# Cooldown period to prevent alert spam
|
||||
cooldown_minutes: 15 # Wait N min before sending another alert
|
||||
# Error severity levels to monitor
|
||||
log_levels:
|
||||
- "ERROR"
|
||||
- "CRITICAL"
|
||||
priority: 1 # Pushover priority: -2=lowest, -1=low, 0=normal, 1=high, 2=emergency
|
||||
|
||||
@@ -17,6 +17,7 @@ dependencies = [
|
||||
"generateds>=2.44.3",
|
||||
"httpx>=0.28.1",
|
||||
"lxml>=6.0.1",
|
||||
"pushover-complete>=2.0.0",
|
||||
"pydantic[email]>=2.11.9",
|
||||
"pytest>=8.4.2",
|
||||
"pytest-asyncio>=1.2.0",
|
||||
|
||||
@@ -43,6 +43,9 @@ from .db import Reservation as DBReservation
|
||||
from .email_monitoring import ReservationStatsCollector
|
||||
from .email_service import create_email_service
|
||||
from .logging_config import get_logger, setup_logging
|
||||
from .notification_adapters import EmailNotificationAdapter, PushoverNotificationAdapter
|
||||
from .notification_service import NotificationService
|
||||
from .pushover_service import create_pushover_service
|
||||
from .rate_limit import (
|
||||
BURST_RATE_LIMIT,
|
||||
DEFAULT_RATE_LIMIT,
|
||||
@@ -229,6 +232,9 @@ async def lifespan(app: FastAPI):
|
||||
# Initialize email service (before logging setup so it can be used by handlers)
|
||||
email_service = create_email_service(config)
|
||||
|
||||
# Initialize pushover service
|
||||
pushover_service = create_pushover_service(config)
|
||||
|
||||
# Setup logging from config with email monitoring
|
||||
# Only primary worker should have the report scheduler running
|
||||
email_handler, report_scheduler = setup_logging(
|
||||
@@ -246,6 +252,7 @@ async def lifespan(app: FastAPI):
|
||||
app.state.alpine_bits_server = AlpineBitsServer(config)
|
||||
app.state.event_dispatcher = event_dispatcher
|
||||
app.state.email_service = email_service
|
||||
app.state.pushover_service = pushover_service
|
||||
app.state.email_handler = email_handler
|
||||
app.state.report_scheduler = report_scheduler
|
||||
|
||||
@@ -304,15 +311,36 @@ async def lifespan(app: FastAPI):
|
||||
try:
|
||||
# Use lookback_hours=24 to get stats from last 24 hours
|
||||
stats = await stats_collector.collect_stats(lookback_hours=24)
|
||||
|
||||
# Send via email (if configured)
|
||||
if email_service:
|
||||
success = await email_service.send_daily_report(
|
||||
recipients=report_scheduler.recipients,
|
||||
stats=stats,
|
||||
errors=None,
|
||||
)
|
||||
if success:
|
||||
_LOGGER.info("Test daily report sent successfully on startup")
|
||||
_LOGGER.info("Test daily report sent via email successfully on startup")
|
||||
else:
|
||||
_LOGGER.error("Failed to send test daily report on startup")
|
||||
_LOGGER.error("Failed to send test daily report via email on startup")
|
||||
|
||||
# Send via Pushover (if configured)
|
||||
if pushover_service:
|
||||
pushover_config = config.get("pushover", {})
|
||||
pushover_monitoring = pushover_config.get("monitoring", {})
|
||||
pushover_daily_report = pushover_monitoring.get("daily_report", {})
|
||||
priority = pushover_daily_report.get("priority", 0)
|
||||
|
||||
success = await pushover_service.send_daily_report(
|
||||
stats=stats,
|
||||
errors=None,
|
||||
priority=priority,
|
||||
)
|
||||
if success:
|
||||
_LOGGER.info("Test daily report sent via Pushover successfully on startup")
|
||||
else:
|
||||
_LOGGER.error("Failed to send test daily report via Pushover on startup")
|
||||
|
||||
except Exception:
|
||||
_LOGGER.exception("Error sending test daily report on startup")
|
||||
|
||||
|
||||
@@ -146,6 +146,52 @@ email_schema = Schema(
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
# Pushover daily report configuration schema
|
||||
pushover_daily_report_schema = Schema(
|
||||
{
|
||||
Required("enabled", default=False): Boolean(),
|
||||
Required("send_time", default="08:00"): str,
|
||||
Required("include_stats", default=True): Boolean(),
|
||||
Required("include_errors", default=True): Boolean(),
|
||||
Required("priority", default=0): Range(min=-2, max=2), # Pushover priority levels
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
# Pushover error alerts configuration schema
|
||||
pushover_error_alerts_schema = Schema(
|
||||
{
|
||||
Required("enabled", default=False): Boolean(),
|
||||
Required("error_threshold", default=5): Range(min=1),
|
||||
Required("buffer_minutes", default=15): Range(min=1),
|
||||
Required("cooldown_minutes", default=15): Range(min=0),
|
||||
Required("log_levels", default=["ERROR", "CRITICAL"]): [
|
||||
In(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
|
||||
],
|
||||
Required("priority", default=1): Range(min=-2, max=2), # Pushover priority levels
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
# Pushover monitoring configuration schema
|
||||
pushover_monitoring_schema = Schema(
|
||||
{
|
||||
Optional("daily_report", default={}): pushover_daily_report_schema,
|
||||
Optional("error_alerts", default={}): pushover_error_alerts_schema,
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
# Complete pushover configuration schema
|
||||
pushover_schema = Schema(
|
||||
{
|
||||
Optional("user_key"): str, # Optional but required for pushover to work
|
||||
Optional("api_token"): str, # Optional but required for pushover to work
|
||||
Optional("monitoring", default={}): pushover_monitoring_schema,
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
config_schema = Schema(
|
||||
{
|
||||
Required(CONF_DATABASE): database_schema,
|
||||
@@ -153,6 +199,7 @@ config_schema = Schema(
|
||||
Required(CONF_SERVER): server_info,
|
||||
Required(CONF_LOGGING): logger_schema,
|
||||
Optional("email"): email_schema, # Email is optional
|
||||
Optional("pushover"): pushover_schema, # Pushover is optional
|
||||
Optional("api_tokens", default=[]): [str], # API tokens for bearer auth
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
|
||||
127
src/alpine_bits_python/notification_adapters.py
Normal file
127
src/alpine_bits_python/notification_adapters.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Adapters for notification backends.
|
||||
|
||||
This module provides adapters that wrap email and Pushover services
|
||||
to work with the unified notification service interface.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .email_service import EmailService
|
||||
from .logging_config import get_logger
|
||||
from .pushover_service import PushoverService
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class EmailNotificationAdapter:
|
||||
"""Adapter for EmailService to work with NotificationService."""
|
||||
|
||||
def __init__(self, email_service: EmailService, recipients: list[str]):
|
||||
"""Initialize the email notification adapter.
|
||||
|
||||
Args:
|
||||
email_service: EmailService instance
|
||||
recipients: List of recipient email addresses
|
||||
|
||||
"""
|
||||
self.email_service = email_service
|
||||
self.recipients = recipients
|
||||
|
||||
async def send_alert(self, title: str, message: str, **kwargs) -> bool:
|
||||
"""Send an alert via email.
|
||||
|
||||
Args:
|
||||
title: Email subject
|
||||
message: Email body
|
||||
**kwargs: Ignored for email
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
|
||||
"""
|
||||
return await self.email_service.send_alert(
|
||||
recipients=self.recipients,
|
||||
subject=title,
|
||||
body=message,
|
||||
)
|
||||
|
||||
async def send_daily_report(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Send a daily report via email.
|
||||
|
||||
Args:
|
||||
stats: Statistics dictionary
|
||||
errors: Optional list of errors
|
||||
**kwargs: Ignored for email
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
|
||||
"""
|
||||
return await self.email_service.send_daily_report(
|
||||
recipients=self.recipients,
|
||||
stats=stats,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class PushoverNotificationAdapter:
|
||||
"""Adapter for PushoverService to work with NotificationService."""
|
||||
|
||||
def __init__(self, pushover_service: PushoverService, priority: int = 0):
|
||||
"""Initialize the Pushover notification adapter.
|
||||
|
||||
Args:
|
||||
pushover_service: PushoverService instance
|
||||
priority: Default priority level for notifications
|
||||
|
||||
"""
|
||||
self.pushover_service = pushover_service
|
||||
self.priority = priority
|
||||
|
||||
async def send_alert(self, title: str, message: str, **kwargs) -> bool:
|
||||
"""Send an alert via Pushover.
|
||||
|
||||
Args:
|
||||
title: Notification title
|
||||
message: Notification message
|
||||
**kwargs: Can include 'priority' to override default
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
|
||||
"""
|
||||
priority = kwargs.get("priority", self.priority)
|
||||
return await self.pushover_service.send_alert(
|
||||
title=title,
|
||||
message=message,
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
async def send_daily_report(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Send a daily report via Pushover.
|
||||
|
||||
Args:
|
||||
stats: Statistics dictionary
|
||||
errors: Optional list of errors
|
||||
**kwargs: Can include 'priority' to override default
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
|
||||
"""
|
||||
priority = kwargs.get("priority", self.priority)
|
||||
return await self.pushover_service.send_daily_report(
|
||||
stats=stats,
|
||||
errors=errors,
|
||||
priority=priority,
|
||||
)
|
||||
177
src/alpine_bits_python/notification_service.py
Normal file
177
src/alpine_bits_python/notification_service.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Unified notification service supporting multiple backends.
|
||||
|
||||
This module provides a unified interface for sending notifications through
|
||||
different channels (email, Pushover, etc.) for alerts and daily reports.
|
||||
"""
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
from .logging_config import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class NotificationBackend(Protocol):
|
||||
"""Protocol for notification backends."""
|
||||
|
||||
async def send_alert(self, title: str, message: str, **kwargs) -> bool:
|
||||
"""Send an alert notification.
|
||||
|
||||
Args:
|
||||
title: Alert title/subject
|
||||
message: Alert message/body
|
||||
**kwargs: Backend-specific parameters
|
||||
|
||||
Returns:
|
||||
True if sent successfully, False otherwise
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def send_daily_report(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Send a daily report notification.
|
||||
|
||||
Args:
|
||||
stats: Statistics dictionary
|
||||
errors: Optional list of errors
|
||||
**kwargs: Backend-specific parameters
|
||||
|
||||
Returns:
|
||||
True if sent successfully, False otherwise
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""Unified notification service that supports multiple backends.
|
||||
|
||||
This service can send notifications through multiple channels simultaneously
|
||||
(email, Pushover, etc.) based on configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the notification service."""
|
||||
self.backends: dict[str, NotificationBackend] = {}
|
||||
|
||||
def register_backend(self, name: str, backend: NotificationBackend) -> None:
|
||||
"""Register a notification backend.
|
||||
|
||||
Args:
|
||||
name: Backend name (e.g., "email", "pushover")
|
||||
backend: Backend instance implementing NotificationBackend protocol
|
||||
|
||||
"""
|
||||
self.backends[name] = backend
|
||||
_LOGGER.info("Registered notification backend: %s", name)
|
||||
|
||||
async def send_alert(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
backends: list[str] | None = None,
|
||||
**kwargs,
|
||||
) -> dict[str, bool]:
|
||||
"""Send an alert through specified backends.
|
||||
|
||||
Args:
|
||||
title: Alert title/subject
|
||||
message: Alert message/body
|
||||
backends: List of backend names to use (None = all registered)
|
||||
**kwargs: Backend-specific parameters
|
||||
|
||||
Returns:
|
||||
Dictionary mapping backend names to success status
|
||||
|
||||
"""
|
||||
if backends is None:
|
||||
backends = list(self.backends.keys())
|
||||
|
||||
results = {}
|
||||
for backend_name in backends:
|
||||
backend = self.backends.get(backend_name)
|
||||
if backend is None:
|
||||
_LOGGER.warning("Backend not found: %s", backend_name)
|
||||
results[backend_name] = False
|
||||
continue
|
||||
|
||||
try:
|
||||
success = await backend.send_alert(title, message, **kwargs)
|
||||
results[backend_name] = success
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error sending alert through backend %s", backend_name
|
||||
)
|
||||
results[backend_name] = False
|
||||
|
||||
return results
|
||||
|
||||
async def send_daily_report(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
backends: list[str] | None = None,
|
||||
**kwargs,
|
||||
) -> dict[str, bool]:
|
||||
"""Send a daily report through specified backends.
|
||||
|
||||
Args:
|
||||
stats: Statistics dictionary
|
||||
errors: Optional list of errors
|
||||
backends: List of backend names to use (None = all registered)
|
||||
**kwargs: Backend-specific parameters
|
||||
|
||||
Returns:
|
||||
Dictionary mapping backend names to success status
|
||||
|
||||
"""
|
||||
if backends is None:
|
||||
backends = list(self.backends.keys())
|
||||
|
||||
results = {}
|
||||
for backend_name in backends:
|
||||
backend = self.backends.get(backend_name)
|
||||
if backend is None:
|
||||
_LOGGER.warning("Backend not found: %s", backend_name)
|
||||
results[backend_name] = False
|
||||
continue
|
||||
|
||||
try:
|
||||
success = await backend.send_daily_report(stats, errors, **kwargs)
|
||||
results[backend_name] = success
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error sending daily report through backend %s", backend_name
|
||||
)
|
||||
results[backend_name] = False
|
||||
|
||||
return results
|
||||
|
||||
def get_backend(self, name: str) -> NotificationBackend | None:
|
||||
"""Get a specific notification backend.
|
||||
|
||||
Args:
|
||||
name: Backend name
|
||||
|
||||
Returns:
|
||||
Backend instance or None if not found
|
||||
|
||||
"""
|
||||
return self.backends.get(name)
|
||||
|
||||
def has_backend(self, name: str) -> bool:
|
||||
"""Check if a backend is registered.
|
||||
|
||||
Args:
|
||||
name: Backend name
|
||||
|
||||
Returns:
|
||||
True if backend is registered
|
||||
|
||||
"""
|
||||
return name in self.backends
|
||||
268
src/alpine_bits_python/pushover_service.py
Normal file
268
src/alpine_bits_python/pushover_service.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""Pushover service for sending push notifications.
|
||||
|
||||
This module provides push notification functionality for the AlpineBits application,
|
||||
including error alerts and daily reports via Pushover.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pushover_complete import PushoverAPI
|
||||
|
||||
from .logging_config import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class PushoverConfig:
|
||||
"""Configuration for Pushover service.
|
||||
|
||||
Attributes:
|
||||
user_key: Pushover user/group key
|
||||
api_token: Pushover application API token
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
"""Initialize Pushover configuration from config dict.
|
||||
|
||||
Args:
|
||||
config: Pushover configuration dictionary
|
||||
|
||||
"""
|
||||
self.user_key: str | None = config.get("user_key")
|
||||
self.api_token: str | None = config.get("api_token")
|
||||
|
||||
# Validate configuration
|
||||
if not self.user_key or not self.api_token:
|
||||
msg = "Both user_key and api_token are required for Pushover"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
class PushoverService:
|
||||
"""Service for sending push notifications via Pushover.
|
||||
|
||||
This service handles sending notifications through the Pushover API,
|
||||
including alerts and daily reports.
|
||||
"""
|
||||
|
||||
def __init__(self, config: PushoverConfig):
|
||||
"""Initialize Pushover service.
|
||||
|
||||
Args:
|
||||
config: Pushover configuration
|
||||
|
||||
"""
|
||||
self.config = config
|
||||
self.api = PushoverAPI(config.api_token)
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
priority: int = 0,
|
||||
url: str | None = None,
|
||||
url_title: str | None = None,
|
||||
) -> bool:
|
||||
"""Send a push notification via Pushover.
|
||||
|
||||
Args:
|
||||
title: Notification title
|
||||
message: Notification message
|
||||
priority: Priority level (-2 to 2, default 0)
|
||||
url: Optional supplementary URL
|
||||
url_title: Optional title for the URL
|
||||
|
||||
Returns:
|
||||
True if notification was sent successfully, False otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
# Send notification in thread pool (API is blocking)
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
self._send_pushover,
|
||||
title,
|
||||
message,
|
||||
priority,
|
||||
url,
|
||||
url_title,
|
||||
)
|
||||
|
||||
_LOGGER.info("Pushover notification sent successfully: %s", title)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
_LOGGER.exception("Failed to send Pushover notification: %s", title)
|
||||
return False
|
||||
|
||||
def _send_pushover(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
priority: int,
|
||||
url: str | None,
|
||||
url_title: str | None,
|
||||
) -> None:
|
||||
"""Send notification via Pushover (blocking operation).
|
||||
|
||||
Args:
|
||||
title: Notification title
|
||||
message: Notification message
|
||||
priority: Priority level
|
||||
url: Optional URL
|
||||
url_title: Optional URL title
|
||||
|
||||
Raises:
|
||||
Exception: If notification sending fails
|
||||
|
||||
"""
|
||||
kwargs = {
|
||||
"user": self.config.user_key,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"priority": priority,
|
||||
}
|
||||
|
||||
if url:
|
||||
kwargs["url"] = url
|
||||
if url_title:
|
||||
kwargs["url_title"] = url_title
|
||||
|
||||
self.api.send_message(**kwargs)
|
||||
|
||||
async def send_alert(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
priority: int = 1,
|
||||
) -> bool:
|
||||
"""Send an alert notification (convenience method).
|
||||
|
||||
Args:
|
||||
title: Alert title
|
||||
message: Alert message
|
||||
priority: Priority level (default 1 for high priority)
|
||||
|
||||
Returns:
|
||||
True if notification was sent successfully, False otherwise
|
||||
|
||||
"""
|
||||
return await self.send_notification(title, message, priority=priority)
|
||||
|
||||
async def send_daily_report(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
priority: int = 0,
|
||||
) -> bool:
|
||||
"""Send a daily report notification.
|
||||
|
||||
Args:
|
||||
stats: Dictionary containing statistics to include in report
|
||||
errors: Optional list of errors to include
|
||||
priority: Priority level (default 0 for normal)
|
||||
|
||||
Returns:
|
||||
True if notification was sent successfully, False otherwise
|
||||
|
||||
"""
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
title = f"AlpineBits Daily Report - {date_str}"
|
||||
|
||||
# Build message body (Pushover has a 1024 character limit)
|
||||
message = self._build_daily_report_message(date_str, stats, errors)
|
||||
|
||||
return await self.send_notification(title, message, priority=priority)
|
||||
|
||||
def _build_daily_report_message(
|
||||
self,
|
||||
date_str: str,
|
||||
stats: dict[str, Any],
|
||||
errors: list[dict[str, Any]] | None,
|
||||
) -> str:
|
||||
"""Build daily report message for Pushover.
|
||||
|
||||
Args:
|
||||
date_str: Date string for the report
|
||||
stats: Statistics dictionary
|
||||
errors: Optional list of errors
|
||||
|
||||
Returns:
|
||||
Formatted message string (max 1024 chars for Pushover)
|
||||
|
||||
"""
|
||||
lines = [f"Report for {date_str}", ""]
|
||||
|
||||
# Add statistics (simplified for push notification)
|
||||
if stats:
|
||||
# Handle reporting period
|
||||
period = stats.get("reporting_period", {})
|
||||
if period:
|
||||
start = period.get("start", "")
|
||||
end = period.get("end", "")
|
||||
if start and end:
|
||||
# Extract just the time portion
|
||||
start_time = start.split(" ")[1] if " " in start else start
|
||||
end_time = end.split(" ")[1] if " " in end else end
|
||||
lines.append(f"Period: {start_time} - {end_time}")
|
||||
|
||||
# Total reservations
|
||||
total = stats.get("total_reservations", 0)
|
||||
lines.append(f"Total Reservations: {total}")
|
||||
|
||||
# Per-hotel breakdown (top 5 only to save space)
|
||||
hotels = stats.get("hotels", [])
|
||||
if hotels:
|
||||
lines.append("")
|
||||
lines.append("By Hotel:")
|
||||
for hotel in hotels[:5]: # Top 5 hotels
|
||||
hotel_name = hotel.get("hotel_name", "Unknown")
|
||||
count = hotel.get("reservations", 0)
|
||||
# Truncate long hotel names
|
||||
if len(hotel_name) > 20:
|
||||
hotel_name = hotel_name[:17] + "..."
|
||||
lines.append(f" • {hotel_name}: {count}")
|
||||
|
||||
if len(hotels) > 5:
|
||||
lines.append(f" • ... and {len(hotels) - 5} more")
|
||||
|
||||
# Add error summary if present
|
||||
if errors:
|
||||
lines.append("")
|
||||
lines.append(f"Errors: {len(errors)} (see logs)")
|
||||
|
||||
message = "\n".join(lines)
|
||||
|
||||
# Truncate if too long (Pushover limit is 1024 chars)
|
||||
if len(message) > 1020:
|
||||
message = message[:1017] + "..."
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def create_pushover_service(config: dict[str, Any]) -> PushoverService | None:
|
||||
"""Create a Pushover service from configuration.
|
||||
|
||||
Args:
|
||||
config: Full application configuration dictionary
|
||||
|
||||
Returns:
|
||||
PushoverService instance if Pushover is configured, None otherwise
|
||||
|
||||
"""
|
||||
pushover_config = config.get("pushover")
|
||||
if not pushover_config:
|
||||
_LOGGER.info("Pushover not configured, push notification service disabled")
|
||||
return None
|
||||
|
||||
try:
|
||||
pushover_cfg = PushoverConfig(pushover_config)
|
||||
service = PushoverService(pushover_cfg)
|
||||
_LOGGER.info("Pushover service initialized successfully")
|
||||
return service
|
||||
except Exception:
|
||||
_LOGGER.exception("Failed to initialize Pushover service")
|
||||
return None
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -27,6 +27,7 @@ dependencies = [
|
||||
{ name = "generateds" },
|
||||
{ name = "httpx" },
|
||||
{ name = "lxml" },
|
||||
{ name = "pushover-complete" },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -55,6 +56,7 @@ requires-dist = [
|
||||
{ name = "generateds", specifier = ">=2.44.3" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "lxml", specifier = ">=6.0.1" },
|
||||
{ name = "pushover-complete", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.9" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||
@@ -646,6 +648,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pushover-complete"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/ae/2ed5c277e22316d8a31e2f67c6c9fd5021189ed3754e144aad53d874d687/pushover_complete-2.0.0.tar.gz", hash = "sha256:24fc7d84d73426840e7678fee80d36f40df0114cb30352ba4f99ab3842ed21a7", size = 19035, upload-time = "2025-05-20T12:47:59.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c2/7debacdeb30d5956e5c5573f129ea2a422eeaaba8993ddfc61c9c0e54c95/pushover_complete-2.0.0-py3-none-any.whl", hash = "sha256:9dbb540daf86b26375e0aaa4b798ad5936b27047ee82cf3213bafeee96929527", size = 9952, upload-time = "2025-05-20T12:47:58.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.9"
|
||||
|
||||
Reference in New Issue
Block a user