Initial commit
This commit is contained in:
97
scripts/scheduler_standalone.py
Normal file
97
scripts/scheduler_standalone.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Standalone scheduler service for Workolik daily email reports.
|
||||
This runs independently of the Streamlit application to avoid multiple instances.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# Add the project root to Python path (scripts/ -> project root)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from app_core.services.daily_report import main as run_daily_report
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def send_daily_report():
|
||||
"""Send the daily report with database-based deduplication using existing email_logs table."""
|
||||
try:
|
||||
|
||||
# Check if we already sent today's report
|
||||
today = datetime.now(ZoneInfo('Asia/Kolkata')).date()
|
||||
today_str = today.strftime('%Y-%m-%d')
|
||||
|
||||
from app_core.db.database import SessionLocal
|
||||
from sqlalchemy import text
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Check if daily report was already sent today using existing email_logs table
|
||||
# Match the exact subject we generate in daily_report.py ("Daily Digest - YYYY-MM-DD")
|
||||
result = db.execute(
|
||||
text("SELECT id FROM email_logs WHERE date_for = :date_for AND subject = :subject LIMIT 1"),
|
||||
{"date_for": today_str, "subject": f"Daily Digest - {today_str}"}
|
||||
).fetchone()
|
||||
|
||||
if result:
|
||||
logger.info(f"Daily report already sent today ({today}), skipping...")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error checking existing reports: {e}")
|
||||
return
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info(f"Starting daily report at {datetime.now(ZoneInfo('Asia/Kolkata'))}")
|
||||
result = run_daily_report()
|
||||
if result == 0:
|
||||
logger.info("Daily report sent successfully")
|
||||
else:
|
||||
logger.warning(f"Daily report failed with exit code: {result}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending daily report: {str(e)}")
|
||||
|
||||
def main():
|
||||
"""Main scheduler function."""
|
||||
logger.info("Starting Workolik Daily Email Scheduler")
|
||||
|
||||
# Create scheduler
|
||||
scheduler = BlockingScheduler(timezone=ZoneInfo('Asia/Kolkata'))
|
||||
|
||||
# Schedule daily email at 8:00 PM IST (20:00)
|
||||
scheduler.add_job(
|
||||
func=send_daily_report,
|
||||
trigger=CronTrigger(hour=20, minute=0, timezone=ZoneInfo('Asia/Kolkata')),
|
||||
id='daily_email_report',
|
||||
name='Daily Email Report',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
logger.info("Daily email scheduler started - will send reports at 8:00 PM IST")
|
||||
|
||||
try:
|
||||
# Keep the scheduler running
|
||||
scheduler.start()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Scheduler stopped by user")
|
||||
scheduler.shutdown()
|
||||
except Exception as e:
|
||||
logger.error(f"Scheduler error: {e}")
|
||||
scheduler.shutdown()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
68
scripts/send_past_reports.py
Normal file
68
scripts/send_past_reports.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# Add the project root to Python path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from app_core.config.settings import AppSettings
|
||||
from app_core.services.mailer_service import MailerService
|
||||
|
||||
def send_report_for_date(service, settings, report_date):
|
||||
print(f"--- Processing date: {report_date} ---")
|
||||
|
||||
# Check if we should skip if already sent (the user said "we need to send", so I'll skip the check unless specified)
|
||||
# if service.has_sent_for_date(str(report_date)):
|
||||
# print(f"Already sent for {report_date}; skipping.")
|
||||
# return
|
||||
|
||||
df = service.fetch_daily_rows(report_date)
|
||||
if df.empty:
|
||||
print(f"No data for {report_date}. Skipping.")
|
||||
return
|
||||
|
||||
row = df.iloc[0].to_dict()
|
||||
html = service.build_email_html(row, df)
|
||||
|
||||
recipients_env = settings.report_recipients or os.getenv("REPORT_RECIPIENTS")
|
||||
if not recipients_env:
|
||||
print("Error: REPORT_RECIPIENTS env var is empty.")
|
||||
return
|
||||
|
||||
recipients = [r.strip() for r in recipients_env.split(',') if r.strip()]
|
||||
subject = f"Daily Digest - {report_date}"
|
||||
|
||||
print(f"Sending email to: {recipients}")
|
||||
ok, msg = service.send_email(recipients, subject=subject, html=html)
|
||||
|
||||
service.log_email(
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
date_for=str(report_date),
|
||||
status="sent" if ok else "failed",
|
||||
error=None if ok else msg
|
||||
)
|
||||
|
||||
if ok:
|
||||
print(f"Successfully sent report for {report_date}")
|
||||
else:
|
||||
print(f"Failed to send report for {report_date}: {msg}")
|
||||
|
||||
def main():
|
||||
settings = AppSettings()
|
||||
service = MailerService(settings)
|
||||
|
||||
dates_to_send = [
|
||||
date(2026, 3, 21),
|
||||
date(2026, 3, 22),
|
||||
]
|
||||
|
||||
for d in dates_to_send:
|
||||
send_report_for_date(service, settings, d)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
91
scripts/send_specific_report.py
Normal file
91
scripts/send_specific_report.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Send a one-off Daily Digest email for a specific date using the app's template.
|
||||
|
||||
Default date: 14.10.2025 (dd.mm.yyyy)
|
||||
Default recipient: suriyakumar.vijayanayagam@gmail.com
|
||||
|
||||
Usage examples:
|
||||
python scripts/send_specific_report.py
|
||||
python scripts/send_specific_report.py --date 14.10.2025 --to you@example.com
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# Ensure project root on PYTHONPATH when running from scripts/
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from app_core.config.settings import AppSettings
|
||||
from app_core.services.mailer_service import MailerService
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Send a daily digest for a specific date")
|
||||
parser.add_argument(
|
||||
"--date",
|
||||
help="Target date in dd.mm.yyyy format (e.g., 14.10.2025)",
|
||||
default="14.10.2025",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--to",
|
||||
help="Recipient email (comma-separated for multiple)",
|
||||
default="suriyakumar.vijayanayagam@gmail.com",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_ddmmyyyy(value: str) -> datetime.date:
|
||||
try:
|
||||
return datetime.strptime(value, "%d.%m.%Y").date()
|
||||
except ValueError as ex:
|
||||
raise SystemExit(f"Invalid date '{value}'. Use dd.mm.yyyy (e.g., 14.10.2025)") from ex
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
target_date = parse_ddmmyyyy(args.date)
|
||||
recipients = [e.strip() for e in args.to.split(",") if e.strip()]
|
||||
|
||||
print("=" * 60)
|
||||
print(f"SENDING Daily Digest for {target_date} to: {', '.join(recipients)}")
|
||||
print("=" * 60)
|
||||
|
||||
settings = AppSettings()
|
||||
service = MailerService(settings)
|
||||
|
||||
# Fetch rows for the date
|
||||
df = service.fetch_daily_rows(target_date)
|
||||
if df.empty:
|
||||
print(f"❌ No rows found for {target_date}. Nothing to send.")
|
||||
return 1
|
||||
|
||||
# Build HTML using first row context + full DataFrame for per-store summary
|
||||
row = df.iloc[0].to_dict()
|
||||
html = service.build_email_html(row, df)
|
||||
|
||||
subject = f"Daily Digest - {target_date}"
|
||||
print(f"Subject: {subject}")
|
||||
|
||||
ok, msg = service.send_email(recipients, subject, html)
|
||||
if ok:
|
||||
print("Email sent successfully")
|
||||
service.log_email(recipients, subject, str(target_date), "sent", None)
|
||||
print("Logged in database")
|
||||
return 0
|
||||
else:
|
||||
print(f"Email failed: {msg}")
|
||||
service.log_email(recipients, subject, str(target_date), "failed", msg)
|
||||
print("Failure logged in database")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
85
scripts/test_mail.py
Normal file
85
scripts/test_mail.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Single test file for mail service - does everything in one place
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
print("=" * 60)
|
||||
print("📧 MAIL SERVICE TEST")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from app_core.services.mailer_service import MailerService
|
||||
from app_core.config.settings import AppSettings
|
||||
|
||||
# Initialize services
|
||||
settings = AppSettings()
|
||||
service = MailerService(settings)
|
||||
|
||||
# Get most recent date with data
|
||||
chosen_date = service.select_report_date()
|
||||
if not chosen_date:
|
||||
print("❌ No data available")
|
||||
exit(1)
|
||||
|
||||
print(f"✅ Using date: {chosen_date}")
|
||||
|
||||
# Fetch data
|
||||
df = service.fetch_daily_rows(chosen_date)
|
||||
print(f"✅ Found {len(df)} records")
|
||||
|
||||
# Build email
|
||||
row = df.iloc[0].to_dict()
|
||||
html = service.build_email_html(row, df)
|
||||
print(f"✅ Email HTML generated ({len(html)} characters)")
|
||||
|
||||
# Show what would be logged
|
||||
ist = ZoneInfo("Asia/Kolkata")
|
||||
now_ist = datetime.now(ist)
|
||||
|
||||
print(f"\n📝 Data that would be inserted in email_logs:")
|
||||
print(f" sent_at: {now_ist}")
|
||||
print(f" recipients: loyalydigital@gmail.com")
|
||||
print(f" subject: Daily Digest - {chosen_date}")
|
||||
print(f" status: sent")
|
||||
print(f" date_for: {chosen_date}")
|
||||
print(f" error: null")
|
||||
|
||||
# Ask user
|
||||
print(f"\n🚀 Send email to loyalydigital@gmail.com? (y/n):")
|
||||
send_confirm = input(" Send? ").strip().lower()
|
||||
|
||||
if send_confirm == 'y':
|
||||
print(f"\n📤 Sending email...")
|
||||
|
||||
recipients = ["loyalydigital@gmail.com"]
|
||||
subject = f"Daily Digest - {chosen_date}"
|
||||
|
||||
ok, msg = service.send_email(recipients, subject, html)
|
||||
|
||||
if ok:
|
||||
print(f"✅ Email sent successfully!")
|
||||
service.log_email(recipients, subject, str(chosen_date), "sent", None)
|
||||
print(f"✅ Logged in database")
|
||||
else:
|
||||
print(f"❌ Email failed: {msg}")
|
||||
service.log_email(recipients, subject, str(chosen_date), "failed", msg)
|
||||
print(f"✅ Failed attempt logged")
|
||||
else:
|
||||
print(f"\n⏭️ Email not sent (test mode)")
|
||||
|
||||
# Show recent logs
|
||||
print(f"\n📋 Recent email logs:")
|
||||
logs = service.recent_logs(limit=5)
|
||||
for log in logs:
|
||||
print(f" {log['status']} - {log['subject']} at {log['sent_at']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"\n" + "=" * 60)
|
||||
print("🏁 Done!")
|
||||
print("=" * 60)
|
||||
104
scripts/validate_setup.py
Normal file
104
scripts/validate_setup.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# Ensure project root is on PYTHONPATH when running from scripts/
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from app_core.config.settings import AppSettings
|
||||
from app_core.db.database import engine
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
def check_env(settings: AppSettings) -> list[str]:
|
||||
missing: list[str] = []
|
||||
required = [
|
||||
("DATABASE_URL", settings.database_url or os.getenv("DATABASE_URL")),
|
||||
("SMTP_HOST", settings.smtp_host),
|
||||
("SMTP_PORT", settings.smtp_port),
|
||||
("SMTP_USER", settings.smtp_user),
|
||||
("SMTP_PASSWORD", settings.smtp_password),
|
||||
("SMTP_FROM_EMAIL", settings.smtp_from_email),
|
||||
("REPORT_RECIPIENTS", settings.report_recipients or os.getenv("REPORT_RECIPIENTS")),
|
||||
]
|
||||
for key, val in required:
|
||||
if not val:
|
||||
missing.append(key)
|
||||
return missing
|
||||
|
||||
|
||||
def check_db_connection() -> tuple[bool, str | None]:
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
# SQLAlchemy 2.0: wrap SQL in text() or use exec_driver_sql
|
||||
conn.execute(text("SELECT 1"))
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def check_smtp_login(s: AppSettings) -> tuple[bool, str | None]:
|
||||
import smtplib
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(s.smtp_host, s.smtp_port, timeout=20)
|
||||
if s.smtp_use_tls:
|
||||
server.starttls()
|
||||
if s.smtp_user and s.smtp_password:
|
||||
server.login(s.smtp_user, s.smtp_password)
|
||||
# Probe NOOP and quit without sending
|
||||
server.noop()
|
||||
server.quit()
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=== Workolik Production Validation ===")
|
||||
settings = AppSettings()
|
||||
|
||||
# 1) Environment variables
|
||||
missing = check_env(settings)
|
||||
if missing:
|
||||
print("❌ Missing required env vars:", ", ".join(sorted(set(missing))))
|
||||
else:
|
||||
print("✅ Required env vars present")
|
||||
|
||||
# Optional BCC
|
||||
bcc = os.getenv("BCC_RECIPIENTS", "").strip()
|
||||
if bcc:
|
||||
print(f"✅ BCC_RECIPIENTS set: {bcc}")
|
||||
else:
|
||||
print("ℹ️ BCC_RECIPIENTS not set (no BCC will be added)")
|
||||
|
||||
# 2) Database connectivity
|
||||
ok_db, err_db = check_db_connection()
|
||||
if ok_db:
|
||||
print("✅ Database connectivity OK")
|
||||
else:
|
||||
print(f"❌ Database connectivity FAILED: {err_db}")
|
||||
|
||||
# 3) SMTP connectivity (no email will be sent)
|
||||
ok_smtp, err_smtp = check_smtp_login(settings)
|
||||
if ok_smtp:
|
||||
print("✅ SMTP login OK (no email sent)")
|
||||
else:
|
||||
print(f"❌ SMTP login FAILED: {err_smtp}")
|
||||
|
||||
# 4) Scheduler subject check (ensure dedupe matches)
|
||||
today_str = datetime.now().date().strftime('%Y-%m-%d')
|
||||
expected_subject = f"Daily Digest - {today_str}"
|
||||
print(f"✅ Scheduler dedupe subject pattern: {expected_subject}")
|
||||
|
||||
failures = (1 if missing else 0) + (0 if ok_db else 1) + (0 if ok_smtp else 1)
|
||||
print("=== Validation Complete ===")
|
||||
return 0 if failures == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
48
scripts/verify_scheduler.py
Normal file
48
scripts/verify_scheduler.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
print("🔍 VERIFYING SCHEDULER SETUP")
|
||||
print("=" * 50)
|
||||
|
||||
# Check current time
|
||||
ist = ZoneInfo('Asia/Kolkata')
|
||||
now = datetime.now(ist)
|
||||
print(f"Current IST time: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||
|
||||
# Check 8 PM today
|
||||
eight_pm = now.replace(hour=20, minute=0, second=0, microsecond=0)
|
||||
print(f"8:00 PM today: {eight_pm.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||
|
||||
# Test scheduler
|
||||
try:
|
||||
from app_core.services.scheduler_service import SchedulerService
|
||||
s = SchedulerService()
|
||||
s.start_scheduler()
|
||||
print(f"✅ Scheduler started: {s.is_running()}")
|
||||
|
||||
next_run = s.get_next_run_time()
|
||||
if next_run:
|
||||
next_run_ist = next_run.astimezone(ist)
|
||||
print(f"✅ Next run: {next_run_ist.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||
else:
|
||||
print("❌ No next run time found")
|
||||
|
||||
s.stop_scheduler()
|
||||
print("✅ Scheduler stopped")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Scheduler error: {e}")
|
||||
|
||||
# Test daily report
|
||||
try:
|
||||
from app_core.services.daily_report import main
|
||||
print("\n🧪 Testing daily report...")
|
||||
result = main()
|
||||
print(f"✅ Daily report result: {result}")
|
||||
except Exception as e:
|
||||
print(f"❌ Daily report error: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ VERIFICATION COMPLETE")
|
||||
print("=" * 50)
|
||||
Reference in New Issue
Block a user