Skip to main content
Glama
client.py8.85 kB
""" Reddit API client manager using PRAW. This module provides a singleton RedditClientManager for managing Reddit API authentication and client lifecycle, following the system architecture specification in Section 2.2.A. """ import os import logging from typing import Optional import praw from praw.exceptions import ( PRAWException, InvalidToken, ResponseException, RequestException, ) from .exceptions import ( AuthenticationError, RedditAPIError, ServerError, ) logger = logging.getLogger(__name__) class RedditClientManager: """ Singleton manager for Reddit API client (PRAW). This class ensures only one Reddit client instance exists throughout the application lifecycle. It handles OAuth2 authentication, connection pooling, and credential validation. Attributes: _instance: Singleton instance _client: PRAW Reddit client _initialized: Flag indicating successful initialization Example: >>> manager = RedditClientManager() >>> reddit = manager.get_client() >>> posts = reddit.subreddit("python").hot(limit=10) """ _instance: Optional["RedditClientManager"] = None _client: Optional[praw.Reddit] = None _initialized: bool = False def __new__(cls) -> "RedditClientManager": """ Create or return singleton instance. Returns: RedditClientManager singleton instance """ if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self) -> None: """ Initialize RedditClientManager. Only initializes once (singleton pattern). Subsequent calls are no-ops. """ # Only initialize once if not self._initialized: self._initialize_client() self._initialized = True def _initialize_client(self) -> None: """ Initialize PRAW Reddit client with OAuth2 credentials. Loads credentials from environment variables and validates authentication by making a test request. Raises: AuthenticationError: If credentials are missing or invalid RedditAPIError: If Reddit API is unreachable """ # Load credentials from environment client_id = os.getenv("REDDIT_CLIENT_ID") client_secret = os.getenv("REDDIT_CLIENT_SECRET") user_agent = os.getenv( "REDDIT_USER_AGENT", "Reddit-MCP-Server/1.0 (by /u/apify-mcp)" ) # Validate required credentials if not client_id: logger.error("REDDIT_CLIENT_ID environment variable not set") raise AuthenticationError("REDDIT_CLIENT_ID is required") if not client_secret: logger.error("REDDIT_CLIENT_SECRET environment variable not set") raise AuthenticationError("REDDIT_CLIENT_SECRET is required") logger.info( "Initializing Reddit client", extra={ "user_agent": user_agent, "client_id": f"{client_id[:8]}...", # Partial log for security } ) try: # Initialize PRAW with OAuth2 self._client = praw.Reddit( client_id=client_id, client_secret=client_secret, user_agent=user_agent, # Read-only mode (no user authentication needed for MVP) username=None, password=None, ) # Configure PRAW settings self._client.read_only = True # Explicitly set read-only self._client.config.timeout = 30 # 30 second timeout # Validate credentials by making a test request self._validate_credentials() logger.info( "Reddit client initialized successfully", extra={"read_only": self._client.read_only} ) except InvalidToken as e: logger.error(f"Invalid Reddit OAuth token: {e}") raise AuthenticationError("Invalid Reddit credentials") from e except RequestException as e: logger.error(f"Reddit API request failed during init: {e}") raise RedditAPIError("Failed to connect to Reddit API") from e except PRAWException as e: logger.error(f"PRAW error during initialization: {e}") raise RedditAPIError(f"Reddit client initialization failed: {e}") from e def _validate_credentials(self) -> None: """ Validate Reddit API credentials by making a test request. Tests authentication by fetching the authenticated user (which should be None in read-only mode) or fetching r/all metadata. Raises: AuthenticationError: If credentials are invalid ServerError: If Reddit API returns server error RedditAPIError: If validation fails for other reasons """ try: # In read-only mode, we can't use user.me() # Instead, fetch r/all metadata as a validation test_subreddit = self._client.subreddit("all") # Access a property to force API call _ = test_subreddit.display_name logger.debug("Credential validation successful") except InvalidToken as e: raise AuthenticationError("Invalid Reddit OAuth token") from e except ResponseException as e: if e.response.status_code >= 500: raise ServerError( "Reddit API unavailable during validation", status_code=e.response.status_code ) from e raise AuthenticationError( f"Authentication failed: {e.response.status_code}" ) from e except PRAWException as e: raise RedditAPIError(f"Credential validation failed: {e}") from e def get_client(self) -> praw.Reddit: """ Get the Reddit API client instance. Returns a singleton PRAW Reddit client. If not initialized, initializes it first. Returns: praw.Reddit: Authenticated Reddit API client Raises: AuthenticationError: If client initialization fails RedditAPIError: If client is unavailable Example: >>> manager = RedditClientManager() >>> reddit = manager.get_client() >>> posts = list(reddit.subreddit("python").hot(limit=5)) """ if self._client is None: logger.warning("Client not initialized, initializing now") self._initialize_client() if self._client is None: raise RedditAPIError("Reddit client initialization failed") return self._client def reset_client(self) -> None: """ Reset the Reddit client instance. Forces re-initialization on next get_client() call. Useful for: - Credential rotation - Recovering from auth errors - Testing Example: >>> manager = RedditClientManager() >>> manager.reset_client() >>> reddit = manager.get_client() # Re-initializes """ logger.info("Resetting Reddit client") self._client = None self._initialized = False def is_initialized(self) -> bool: """ Check if client is initialized. Returns: bool: True if client is initialized and ready Example: >>> manager = RedditClientManager() >>> if manager.is_initialized(): ... reddit = manager.get_client() """ return self._initialized and self._client is not None @property def client(self) -> praw.Reddit: """ Property accessor for Reddit client. Provides convenient property-style access to the client. Returns: praw.Reddit: Authenticated Reddit API client Example: >>> manager = RedditClientManager() >>> reddit = manager.client >>> print(reddit.read_only) True """ return self.get_client() # Singleton instance for convenient import reddit_client_manager = RedditClientManager() def get_reddit_client() -> praw.Reddit: """ Convenience function to get Reddit client. This is a module-level function that returns the singleton client, providing a simpler import pattern. Returns: praw.Reddit: Authenticated Reddit API client Example: >>> from src.reddit.client import get_reddit_client >>> reddit = get_reddit_client() >>> posts = reddit.subreddit("python").hot(limit=10) """ return reddit_client_manager.get_client()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/padak/apify-actor-reddit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server