YnM Kamion Stop Sopel py

import json
import os
import time
import requests
import logging
from bs4 import BeautifulSoup
from datetime import datetime, timedelta
from sopel import plugin
from apscheduler.schedulers.background import BackgroundScheduler
from functools import lru_cache
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from threading import Lock

Type definitions and data classes for better structure

@dataclass
class TruckStop:
country: str
interval: str
tonnage: str

@dataclass
class Config:
JSON_FILE: str = os.path.expanduser(“/home/ai/.sopel/plugins/notify/kamionstop.json”)
COUNTRIES: List[str] = None
SOURCE_URL: str = “Teherautóra vonatkozó forgalmi korlátozások Európában
TARGET_CHANNEL: str = “#Magyar
MENTION_USER: str = “@Zsolt

def __post_init__(self):
    self.COUNTRIES = ["Ausztria", "Németország", "Belgium"]

class TruckStopBot:
def init(self):
self.config = Config()
self.logger = self._setup_logger()
self.data_lock = Lock()
self.scheduler = None
self._setup_directories()

def _setup_logger(self) -> logging.Logger:
    """Setup logging with proper configuration."""
    logger = logging.getLogger(__name__)
    if not logger.handlers:
        handler = logging.StreamHandler()
        handler.setFormatter(
            logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
        )
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)
    return logger

def _setup_directories(self) -> None:
    """Ensure necessary directories exist."""
    os.makedirs(os.path.dirname(self.config.JSON_FILE), exist_ok=True)

@lru_cache(maxsize=1, typed=True)
def _get_session(self) -> requests.Session:
    """Get a cached session for requests."""
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    })
    return session

def load_data(self) -> Dict:
    """Thread-safe data loading with error handling."""
    with self.data_lock:
        try:
            if os.path.exists(self.config.JSON_FILE):
                with open(self.config.JSON_FILE, "r", encoding="utf-8") as f:
                    return json.load(f)
        except Exception as e:
            self.logger.error(f"Error loading data: {e}")
        return {}

def save_data(self, data: Dict) -> None:
    """Thread-safe data saving with error handling."""
    with self.data_lock:
        try:
            with open(self.config.JSON_FILE, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=4)
        except Exception as e:
            self.logger.error(f"Error saving data: {e}")

def fetch_truck_stops(self) -> Dict:
    """Fetch truck stop data with improved error handling and parsing."""
    try:
        session = self._get_session()
        response = session.get(self.config.SOURCE_URL, timeout=10)
        response.raise_for_status()
        
        soup = BeautifulSoup(response.text, "html.parser")
        stops = {}
        
        for date_header in soup.find_all("h3"):
            date = self._parse_date(date_header.get_text(strip=True))
            if not date:
                continue
            
            table = date_header.find_next("table", class_="table")
            if not table:
                continue
            
            stops[date] = self._parse_table(table)
        
        return stops

    except Exception as e:
        self.logger.error(f"Error fetching data: {e}")
        return {}

def _parse_date(self, date_text: str) -> Optional[str]:
    """Parse date text with better error handling."""
    date_text = date_text.split("(")[0].strip().replace('.', '')
    date_formats = ["%Y %B %d", "%Y. %B %d"]
    
    for fmt in date_formats:
        try:
            return datetime.strptime(date_text, fmt).strftime("%Y-%m-%d")
        except ValueError:
            continue
    return None

def _parse_table(self, table) -> List[Dict]:
    """Parse table data with validation."""
    stops = []
    for row in table.find_all("tr"):
        cols = row.find_all("td")
        if len(cols) >= 3:
            country = cols[0].get_text(strip=True).split(" ")[-1]
            if country in self.config.COUNTRIES:
                stop = TruckStop(
                    country=country,
                    interval=cols[1].get_text(strip=True),
                    tonnage=cols[2].get_text(strip=True)
                )
                stops.append(asdict(stop))
    return stops

def update_data(self) -> None:
    """Update truck stop data with validation."""
    new_data = self.fetch_truck_stops()
    if new_data:
        self.save_data(new_data)
        self.logger.info("Truck stop data updated successfully")

def get_weekly_stops(self, start_date: datetime) -> Dict:
    """Get weekly stops with date validation."""
    data = self.load_data()
    if not data:
        return {}

    end_date = start_date + timedelta(days=6)
    return {
        k: v for k, v in data.items()
        if start_date.strftime("%Y-%m-%d") <= k <= end_date.strftime("%Y-%m-%d")
    }

def format_stop_message(self, date: str, stop: Dict, prefix: str = "") -> str:
    """Format stop message consistently."""
    return (f"{prefix}*{date}*: - *{stop['country']}*: "
            f"Intervallum: {stop['interval']} - Raksúly: {stop['tonnage']}")

Plugin commands and setup

bot_instance = TruckStopBot()

@plugin.rule(r’^[(\S+)]!stop’)
def check_weekly_stops(bot, trigger):
“”“Check weekly stops with rate limiting.”“”
now = datetime.now()
weekly_stops = bot_instance.get_weekly_stops(now)

if not weekly_stops:
    bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Nincs elérhető kamionstop adat!**",
            bot_instance.config.TARGET_CHANNEL)
    return

bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Ez a hét kamionstopjai:**",
        bot_instance.config.TARGET_CHANNEL)

for date, stops in sorted(weekly_stops.items()):
    for stop in stops:
        time.sleep(0.5)  # Rate limiting
        bot.say(bot_instance.format_stop_message(date, stop),
               bot_instance.config.TARGET_CHANNEL)

@plugin.rule(r’^[(\S+)]!upstop’)
def update_truck_stop_data(bot, trigger):
“”“Update truck stop data with feedback.”“”
bot.say(f"{bot_instance.config.MENTION_USER} :articulated_lorry: Kamionstop adat frissítése…“,
bot_instance.config.TARGET_CHANNEL)
bot_instance.update_data()
bot.say(f”{bot_instance.config.MENTION_USER} :articulated_lorry: Kamionstop adatok frissítve!",
bot_instance.config.TARGET_CHANNEL)

def setup(bot):
“”“Setup scheduler with error handling.”“”
try:
bot_instance.scheduler = BackgroundScheduler()
bot_instance.scheduler.add_job(
bot_instance.update_data,
‘interval’,
days=1,
next_run_time=datetime.now()
)
bot_instance.scheduler.start()
bot_instance.logger.info(“Scheduler started successfully”)
except Exception as e:
bot_instance.logger.error(f"Error starting scheduler: {e}")

def shutdown(bot):
“”“Graceful shutdown.”“”
if bot_instance.scheduler:
bot_instance.scheduler.shutdown()
bot_instance.logger.info(“Scheduler shut down successfully”)

V 1.2

import json
import os
import time
import requests
import logging
from bs4 import BeautifulSoup
from datetime import datetime, timedelta
from sopel import plugin
from apscheduler.schedulers.background import BackgroundScheduler
from functools import lru_cache
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from threading import Lock

@dataclass
class TruckStop:
    country: str
    interval: str
    tonnage: str

@dataclass
class Config:
    JSON_FILE: str = os.path.expanduser("/home/ai/.sopel/plugins/json/kamionstop.json")
    COUNTRIES: List[str] = None
    SOURCE_URL: str = "https://www.cargopedia.hu/teheraut%C3%B3ra-vonatkoz%C3%B3-forgalmi-korl%C3%A1toz%C3%A1sok-eur%C3%B3p%C3%A1ban"
    TARGET_CHANNEL: str = "#Magyar"
    MENTION_USER: str = "@Zsolt"
    
    def __post_init__(self):
        self.COUNTRIES = ["Ausztria", "Franciaország",  "Németország", "Belgium"]

class TruckStopBot:
    def __init__(self):
        self.config = Config()
        self.logger = self._setup_logger()
        self.data_lock = Lock()
        self.scheduler = None
        self._setup_directories()
        self.bot_instance = None
        self.connected = False

    def _setup_logger(self) -> logging.Logger:
        logger = logging.getLogger(__name__)
        if not logger.handlers:
            handler = logging.StreamHandler()
            handler.setFormatter(
                logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
            )
            logger.addHandler(handler)
            logger.setLevel(logging.INFO)
        return logger

    def _setup_directories(self) -> None:
        try:
            os.makedirs(os.path.dirname(self.config.JSON_FILE), exist_ok=True)
        except Exception as e:
            self.logger.error(f"Error creating directories: {e}")

    @lru_cache(maxsize=1, typed=True)
    def _get_session(self) -> requests.Session:
        session = requests.Session()
        session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
        return session

    def load_data(self) -> Dict:
        with self.data_lock:
            try:
                if os.path.exists(self.config.JSON_FILE):
                    with open(self.config.JSON_FILE, "r", encoding="utf-8") as f:
                        return json.load(f)
            except Exception as e:
                self.logger.error(f"Error loading data: {e}")
            return {}

    def save_data(self, data: Dict) -> None:
        with self.data_lock:
            try:
                temp_file = f"{self.config.JSON_FILE}.tmp"
                with open(temp_file, "w", encoding="utf-8") as f:
                    json.dump(data, f, ensure_ascii=False, indent=4)
                os.replace(temp_file, self.config.JSON_FILE)
            except Exception as e:
                self.logger.error(f"Error saving data: {e}")
                if os.path.exists(temp_file):
                    try:
                        os.remove(temp_file)
                    except:
                        pass

    def fetch_truck_stops(self) -> Dict:
        try:
            session = self._get_session()
            response = session.get(self.config.SOURCE_URL, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.text, "html.parser")
            stops = {}
            
            for date_header in soup.find_all("h3"):
                date = self._parse_date(date_header.get_text(strip=True))
                if not date:
                    continue
                
                table = date_header.find_next("table", class_="table")
                if not table:
                    continue
                
                stops[date] = self._parse_table(table)
            
            return stops

        except requests.RequestException as e:
            self.logger.error(f"Network error while fetching data: {e}")
            return {}
        except Exception as e:
            self.logger.error(f"Error fetching data: {e}")
            return {}

    def _parse_date(self, date_text: str) -> Optional[str]:
        try:
            # Eltávolítja a zárójelben lévő részt és a pontokat
            date_text = date_text.split("(")[0].strip().replace('.', '')
            hu_to_en = {
                "január": "January", "február": "February", "március": "March",
                "április": "April", "május": "May", "június": "June",
                "július": "July", "augusztus": "August", "szeptember": "September",
                "október": "October", "november": "November", "december": "December"
            }
            for hu, en in hu_to_en.items():
                if hu in date_text.lower():
                    date_text = date_text.lower().replace(hu, en)
                    break
            return datetime.strptime(date_text, "%Y %B %d").strftime("%Y-%m-%d")
        except Exception as e:
            self.logger.error(f"Error parsing date {date_text}: {e}")
            return None

    def _parse_table(self, table) -> List[Dict]:
        stops = []
        try:
            for row in table.find_all("tr"):
                country_cell = row.find("td", class_="tara")
                interval_cell = row.find("td", class_="interval")
                tonnage_cell = row.find("td", class_="tonaj")
                if country_cell and interval_cell and tonnage_cell:
                    # Eltávolítjuk az esetleges kép elemeket, és csak a szöveget vesszük
                    country = country_cell.get_text(strip=True)
                    if country in self.config.COUNTRIES:
                        stop = TruckStop(
                            country=country,
                            interval=interval_cell.get_text(strip=True),
                            tonnage=tonnage_cell.get_text(strip=True)
                        )
                        stops.append(asdict(stop))
        except Exception as e:
            self.logger.error(f"Error parsing table row: {e}")
        return stops


    def update_data(self) -> None:
        try:
            new_data = self.fetch_truck_stops()
            if new_data:
                self.save_data(new_data)
                self.logger.info("Truck stop data updated successfully")
                # Check for today's stops after updating
                self.check_today_stops()
            else:
                self.logger.warning("No new data fetched, keeping existing data")
        except Exception as e:
            self.logger.error(f"Error in update_data: {e}")

    def check_today_stops(self) -> None:
        """Check and notify about today's truck stops."""
        try:
            if not self.bot_instance:
                self.logger.error("Bot instance not set")
                return

            if not getattr(self.bot_instance.backend, 'connected', False):
                self.logger.error("Bot is not connected to IRC")
                return

            today = datetime.now().strftime("%Y-%m-%d")
            data = self.load_data()
            
            if not data:
                self.logger.error("No data available")
                return

            try:
                if today in data and data[today]:
                    self.bot_instance.say(
                        f"{self.config.MENTION_USER} 🚛 **Mai kamionstopok:**",
                        self.config.TARGET_CHANNEL
                    )
                    
                    for stop in data[today]:
                        time.sleep(0.5)  # Rate limiting
                        message = self.format_stop_message(today, stop)
                        self.bot_instance.say(message, self.config.TARGET_CHANNEL)
                else:
                    self.bot_instance.say(
                        f"{self.config.MENTION_USER} 🚛 **Ma nincs kamionstop.**",
                        self.config.TARGET_CHANNEL
                    )
            except Exception as e:
                self.logger.error(f"Error sending messages: {e}")
        except Exception as e:
            self.logger.error(f"Error in check_today_stops: {e}")

    def get_weekly_stops(self, start_date: datetime) -> Dict:
        try:
            data = self.load_data()
            if not data:
                return {}

            end_date = start_date + timedelta(days=6)
            return {
                k: v for k, v in data.items()
                if start_date.strftime("%Y-%m-%d") <= k <= end_date.strftime("%Y-%m-%d")
            }
        except Exception as e:
            self.logger.error(f"Error getting weekly stops: {e}")
            return {}

    def format_stop_message(self, date: str, stop: Dict, prefix: str = "") -> str:
        try:
            return (f"{prefix}*{date}*: - *{stop['country']}*: "
                    f"Intervallum: {stop['interval']} - Raksúly: {stop['tonnage']}")
        except Exception as e:
            self.logger.error(f"Error formatting message: {e}")
            return "Error formatting message"

    def set_bot_instance(self, bot):
        """Set the bot instance for notifications."""
        try:
            self.bot_instance = bot
            self.connected = True
            self.logger.info("Bot instance set successfully")
        except Exception as e:
            self.logger.error(f"Error setting bot instance: {e}")

# Create global bot instance
bot_instance = TruckStopBot()

@plugin.rule(r'^\[(\S+)\]!stop')
def check_weekly_stops(bot, trigger):
    """Check weekly stops with rate limiting."""
    try:
        now = datetime.now()
        weekly_stops = bot_instance.get_weekly_stops(now)
        
        if not weekly_stops:
            bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Nincs elérhető kamionstop adat!**",
                    bot_instance.config.TARGET_CHANNEL)
            return

        bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Ez a hét kamionstopjai:**",
                bot_instance.config.TARGET_CHANNEL)
        
        for date, stops in sorted(weekly_stops.items()):
            for stop in stops:
                time.sleep(0.5)  # Rate limiting
                bot.say(bot_instance.format_stop_message(date, stop),
                       bot_instance.config.TARGET_CHANNEL)
    except Exception as e:
        bot_instance.logger.error(f"Error in check_weekly_stops: {e}")

@plugin.rule(r'^\[(\S+)\]!upstop')
def update_truck_stop_data(bot, trigger):
    """Update truck stop data with feedback."""
    try:
        bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Kamionstop adat frissítése...**",
                bot_instance.config.TARGET_CHANNEL)
        bot_instance.update_data()
        bot.say(f"{bot_instance.config.MENTION_USER} 🚛 **Kamionstop adatok frissítve!**",
                bot_instance.config.TARGET_CHANNEL)
    except Exception as e:
        bot_instance.logger.error(f"Error in update_truck_stop_data: {e}")

def setup(bot):
    """Setup scheduler with error handling and daily notifications."""
    try:
        # Set bot instance for notifications first
        bot_instance.set_bot_instance(bot)
        
        # Wait a brief moment to ensure connection is established
        time.sleep(2)
        
        # Initialize scheduler if not already running
        if not bot_instance.scheduler or not bot_instance.scheduler.running:
            bot_instance.scheduler = BackgroundScheduler(
                timezone='Europe/Bucharest'  # Explicitly set timezone
            )
            
            # Add job for data updates with a 5-minute delay for initial run
            bot_instance.scheduler.add_job(
                bot_instance.update_data,
                'interval',
                days=1,
                next_run_time=datetime.now(bot_instance.scheduler.timezone) + timedelta(minutes=5)
            )
            
            # Add job for daily notifications at 00:01
            bot_instance.scheduler.add_job(
                bot_instance.check_today_stops,
                'cron',
                hour=0,
                minute=1,
                timezone='Europe/Bucharest'  # Explicitly set timezone for cron job
            )
            
            bot_instance.scheduler.start()
            bot_instance.logger.info("Scheduler started successfully with timezone: Europe/Bucharest")
    except Exception as e:
        bot_instance.logger.error(f"Error starting scheduler: {e}")

def shutdown(bot):
    """Graceful shutdown."""
    try:
        if bot_instance.scheduler and bot_instance.scheduler.running:
            bot_instance.scheduler.shutdown()
            bot_instance.logger.info("Scheduler shut down successfully")
    except Exception as e:
        bot_instance.logger.error(f"Error during shutdown: {e}")