Skip to content

Commit a73e10e

Browse files
STOKES-DOTclaude
andcommitted
Add automatic scheduling and notification features
New Features: - setup-schedule.sh: Automatic cron/LaunchAgent setup - notifications.py: System and email notification support - --test-notify: Test notification setup - --setup-schedule: One-command scheduler setup Notification Features: - Desktop/system notifications (macOS/Linux/Windows) - Email notifications via SMTP - Automatic notification on collection success/failure - Test notification command for verification Configuration: - Added notifications section to config.yaml - system_notification: true by default - email_enabled: false (requires SMTP setup) Usage: python main.py --setup-schedule # Set up automatic daily execution python main.py --test-notify # Send test notification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 32c0c0e commit a73e10e

File tree

5 files changed

+426
-0
lines changed

5 files changed

+426
-0
lines changed

config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,18 @@ logging:
6868
max_papers: 50 # Maximum number of papers to include in report
6969
abstract_max_length: 1000 # Maximum abstract length in report
7070

71+
# ============================================
72+
# NOTIFICATION SETTINGS
73+
# ============================================
74+
notifications:
75+
# Desktop/system notification (requires compatible OS)
76+
system_notification: true
77+
78+
# Email notification (optional)
79+
email_enabled: false
80+
smtp_server: "smtp.gmail.com"
81+
smtp_port: 587
82+
email_from: "your-email@gmail.com"
83+
email_to: "your-email@gmail.com"
84+
email_password: "your-app-password" # Use app-specific password, not regular password
85+

main.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
PdfCompiler,
2323
PaperScheduler
2424
)
25+
from modules.notifications import NotificationManager, send_test_notification
2526

2627

2728
def setup_logging(config: dict) -> logging.Logger:
@@ -219,9 +220,28 @@ def run_collector(config: dict, logger: logging.Logger) -> bool:
219220
logger.info("=" * 60)
220221
logger.info("Collection completed successfully!")
221222
logger.info("=" * 60)
223+
224+
# Send notification
225+
try:
226+
notification_config = config.get("notifications", {})
227+
notifier = NotificationManager(notification_config)
228+
total_papers = sum(len(paper_list) for paper_list in grouped_papers.values())
229+
notifier.send_notification(total_papers, pdf_path)
230+
except Exception as e:
231+
logger.warning(f"Failed to send notification: {e}")
232+
222233
return True
223234
else:
224235
logger.error("PDF compilation failed")
236+
237+
# Send failure notification
238+
try:
239+
notification_config = config.get("notifications", {})
240+
notifier = NotificationManager(notification_config)
241+
notifier.send_notification(0, "", error="PDF compilation failed")
242+
except Exception:
243+
pass # Don't fail on notification errors
244+
225245
return False
226246

227247
except Exception as e:
@@ -280,6 +300,18 @@ def main():
280300
help="Open config file in default editor for keyword editing"
281301
)
282302

303+
parser.add_argument(
304+
"--test-notify",
305+
action="store_true",
306+
help="Send a test notification to verify notification setup"
307+
)
308+
309+
parser.add_argument(
310+
"--setup-schedule",
311+
action="store_true",
312+
help="Set up automatic scheduled execution (cron/LaunchAgent)"
313+
)
314+
283315
args = parser.parse_args()
284316

285317
# Load configuration
@@ -293,6 +325,30 @@ def main():
293325
os.system(f"{editor} {args.config}")
294326
return
295327

328+
# Handle --test-notify
329+
if args.test_notify:
330+
print("Sending test notification...")
331+
notification_config = config.get("notifications", {})
332+
success = send_test_notification(notification_config)
333+
if success:
334+
print("✓ Test notification sent!")
335+
print(" Check your desktop/system for the notification.")
336+
else:
337+
print("⚠ Test notification failed.")
338+
print(" For system notifications, ensure your OS supports desktop notifications.")
339+
print(" For email notifications, configure email settings in config.yaml.")
340+
return
341+
342+
# Handle --setup-schedule
343+
if args.setup_schedule:
344+
print("Setting up scheduled execution...")
345+
if os.path.exists("setup-schedule.sh"):
346+
os.execv("/bin/bash", ["bash", "setup-schedule.sh"])
347+
else:
348+
print("Error: setup-schedule.sh not found")
349+
sys.exit(1)
350+
return
351+
296352
# Handle --status
297353
if args.status:
298354
schedule_config = config.get("schedule", {})

modules/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
from .latex_generator import LatexGenerator
1111
from .pdf_compiler import PdfCompiler
1212
from .scheduler import PaperScheduler
13+
from .notifications import NotificationManager, send_test_notification
14+
from .config_loader import ConfigLoader
1315

1416
__all__ = [
1517
"ArxivFetcher",
1618
"PaperFilter",
1719
"LatexGenerator",
1820
"PdfCompiler",
1921
"PaperScheduler",
22+
"NotificationManager",
23+
"send_test_notification",
24+
"ConfigLoader",
2025
]

modules/notifications.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""
2+
Notification Module
3+
Supports email and system notifications for completed collections
4+
"""
5+
6+
import os
7+
import smtplib
8+
import subprocess
9+
import platform
10+
from email.mime.text import MIMEText
11+
from email.mime.multipart import MIMEMultipart
12+
from typing import Optional, List
13+
import logging
14+
15+
16+
class NotificationManager:
17+
"""Manages notifications for paper collection completion"""
18+
19+
def __init__(self, config: dict = None):
20+
"""
21+
Initialize notification manager
22+
23+
Args:
24+
config: Configuration dictionary with notification settings
25+
"""
26+
self.config = config or {}
27+
self.logger = logging.getLogger(__name__)
28+
29+
def send_notification(
30+
self,
31+
num_papers: int,
32+
pdf_path: str,
33+
error: Optional[str] = None
34+
) -> bool:
35+
"""
36+
Send notification based on configured methods
37+
38+
Args:
39+
num_papers: Number of papers collected
40+
pdf_path: Path to generated PDF
41+
error: Error message if collection failed
42+
43+
Returns:
44+
True if notification was sent successfully
45+
"""
46+
success = False
47+
48+
# Determine message content
49+
if error:
50+
title = "ArXiv Paper Collection - Failed"
51+
message = f"Paper collection failed with error:\n\n{error}"
52+
else:
53+
title = "ArXiv Paper Collection - Completed"
54+
message = f"Successfully collected {num_papers} papers\n\nPDF: {pdf_path}"
55+
56+
# Send system notification
57+
if self.config.get("system_notification", True):
58+
success |= self._send_system_notification(title, message)
59+
60+
# Send email notification
61+
if self.config.get("email_enabled", False):
62+
success |= self._send_email(title, message)
63+
64+
return success
65+
66+
def _send_system_notification(self, title: str, message: str) -> bool:
67+
"""
68+
Send desktop system notification
69+
70+
Args:
71+
title: Notification title
72+
message: Notification message
73+
74+
Returns:
75+
True if successful
76+
"""
77+
try:
78+
system = platform.system()
79+
80+
if system == "Darwin": # macOS
81+
# Use osascript for macOS notifications
82+
cmd = [
83+
"osascript", "-e",
84+
f'display notification "{message}" with title "{title}" sound name "default"'
85+
]
86+
subprocess.run(cmd, check=True, capture_output=True)
87+
return True
88+
89+
elif system == "Linux":
90+
# Check for notify-send
91+
if self._command_exists("notify-send"):
92+
cmd = [
93+
"notify-send",
94+
title,
95+
message,
96+
"-i", "dialog-information"
97+
]
98+
subprocess.run(cmd, check=True, capture_output=True)
99+
return True
100+
else:
101+
self.logger.warning("notify-send not found. Install: sudo apt-get install libnotify-bin")
102+
return False
103+
104+
elif system == "Windows":
105+
# Use Windows toast notification
106+
try:
107+
from win10toast import ToastNotifier
108+
toaster = ToastNotifier()
109+
toaster.show_toast(
110+
title,
111+
message,
112+
duration=10,
113+
threaded=True
114+
)
115+
return True
116+
except ImportError:
117+
self.logger.warning("win10toast not installed. Install: pip install win10toast")
118+
return False
119+
120+
except Exception as e:
121+
self.logger.error(f"System notification failed: {e}")
122+
123+
return False
124+
125+
def _send_email(self, subject: str, body: str) -> bool:
126+
"""
127+
Send email notification
128+
129+
Args:
130+
subject: Email subject
131+
body: Email body
132+
133+
Returns:
134+
True if successful
135+
"""
136+
try:
137+
# Get email settings from config
138+
smtp_server = self.config.get("smtp_server", "smtp.gmail.com")
139+
smtp_port = self.config.get("smtp_port", 587)
140+
email_from = self.config.get("email_from")
141+
email_to = self.config.get("email_to")
142+
email_password = self.config.get("email_password")
143+
144+
if not all([email_from, email_to, email_password]):
145+
self.logger.warning("Email configuration incomplete. Skipping email notification.")
146+
return False
147+
148+
# Create message
149+
msg = MIMEMultipart()
150+
msg['From'] = email_from
151+
msg['To'] = email_to
152+
msg['Subject'] = subject
153+
154+
msg.attach(MIMEText(body, 'plain'))
155+
156+
# Send email
157+
with smtplib.SMTP(smtp_server, smtp_port) as server:
158+
server.starttls()
159+
server.login(email_from, email_password)
160+
server.send_message(msg)
161+
162+
self.logger.info(f"Email notification sent to {email_to}")
163+
return True
164+
165+
except Exception as e:
166+
self.logger.error(f"Email notification failed: {e}")
167+
return False
168+
169+
@staticmethod
170+
def _command_exists(command: str) -> bool:
171+
"""Check if a command exists on the system"""
172+
try:
173+
subprocess.run(
174+
["which", command],
175+
check=True,
176+
capture_output=True
177+
)
178+
return True
179+
except subprocess.CalledProcessError:
180+
return False
181+
182+
@staticmethod
183+
def get_email_config_template() -> dict:
184+
"""Get template for email configuration"""
185+
return {
186+
"notifications": {
187+
"system_notification": True,
188+
"email_enabled": False,
189+
"smtp_server": "smtp.gmail.com",
190+
"smtp_port": 587,
191+
"email_from": "your-email@gmail.com",
192+
"email_to": "your-email@gmail.com",
193+
"email_password": "your-app-password" # Use app-specific password
194+
}
195+
}
196+
197+
198+
def send_test_notification(config: dict = None) -> bool:
199+
"""
200+
Send a test notification to verify setup
201+
202+
Args:
203+
config: Notification configuration
204+
205+
Returns:
206+
True if test notification was sent
207+
"""
208+
manager = NotificationManager(config)
209+
return manager.send_notification(
210+
num_papers=0,
211+
pdf_path="test.pdf",
212+
error=None
213+
)

0 commit comments

Comments
 (0)