# Import required MongoDB libraries and utilities from pymongo import MongoClient from pymongo.database import Database from pymongo.collection import Collection from pymongo.errors import ConnectionFailure, ConfigurationError, OperationFailure import logging from typing import Optional from src.config.settings import ( MONGO_URI, MONGO_DB_NAME, MONGO_NEWS_COLLECTION_NAME, MONGO_SESSIONS_COLLECTION_NAME, MONGO_TRACKING_COLLECTION_NAME ) # Set up logger for this module logger = logging.getLogger(__name__) class MongoDB: """ MongoDB connection handler implementing the Singleton pattern. This class manages the connection to MongoDB Atlas and provides access to collections. """ # Class variables to store singleton instance and connection details _instance: Optional['MongoDB'] = None _client: Optional[MongoClient] = None _db: Optional[Database] = None def __new__(cls): """ Implements the Singleton pattern by ensuring only one instance of MongoDB class exists. Returns the existing instance if it exists, otherwise creates a new one. """ if cls._instance is None: cls._instance = super(MongoDB, cls).__new__(cls) return cls._instance def __init__(self): """ Initialize the MongoDB connection if it hasn't been established yet. Only creates a new connection if _client is None. """ if self._client is None: self._connect() def _connect(self): """ Establishes connection to MongoDB Atlas. Handles connection errors and sets up the database instance. Does not raise exceptions if connection fails - allows graceful degradation. """ try: logger.info(f"Connecting to MongoDB Atlas: DB='{MONGO_DB_NAME}'") # Create MongoDB client with 5 second timeout for server selection self._client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) # Verify connection by running a simple command self._client.admin.command('ismaster') # Set up database instance self._db = self._client[MONGO_DB_NAME] logger.info("Successfully connected to MongoDB.") except (ConnectionFailure, ConfigurationError) as e: logger.warning(f"MongoDB connection failed: {e}") self._client = None self._db = None # Don't raise the exception - allow the application to continue without MongoDB except Exception as e: logger.warning(f"An unexpected error occurred during MongoDB connection: {e}") self._client = None self._db = None # Don't raise the exception - allow the application to continue without MongoDB @property def db(self) -> Optional[Database]: """ Property to get the database instance. Returns None if the connection is not available. Returns: Database: MongoDB database instance or None if not connected """ if self._db is None: # Try to connect once, but don't keep retrying try: self._connect() except Exception as e: logger.warning(f"Failed to establish MongoDB connection: {e}") return None return self._db @property def news_collection(self) -> Collection: """ Property to get the news collection. Returns: Collection: MongoDB collection for news data """ return self.db[MONGO_NEWS_COLLECTION_NAME] @property def sessions_collection(self) -> Collection: """ Property to get the sessions collection. Returns: Collection: MongoDB collection for session data """ return self.db[MONGO_SESSIONS_COLLECTION_NAME] @property def tracking_collection(self) -> Collection: """ Property to get the tracking collection. Returns: Collection: MongoDB collection for user feedback/tracking data """ return self.db[MONGO_TRACKING_COLLECTION_NAME] def close(self): """ Closes the MongoDB connection and cleans up resources. Handles any errors during connection closure. """ if self._client: try: self._client.close() logger.info("MongoDB connection closed.") except Exception as e: logger.error(f"Error closing MongoDB connection: {e}", exc_info=True) finally: self._client = None self._db = None # Create a singleton instance of MongoDB for use throughout the application mongodb = MongoDB()