This commit is contained in:
2026-04-07 12:44:06 +05:30
parent 7f81fc64c1
commit 5dd4196014
49 changed files with 2795 additions and 0 deletions

View 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()

View 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()

View 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
View 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
View 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())

View 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)