""" Application Context Module This module provides a centralized context object for the StingrayExplorer dashboard that encapsulates all UI containers and state management. The AppContext class solves the "container passing anti-pattern" by providing a single object that contains references to all UI containers and the state manager, eliminating the need to pass 7-9 container parameters to every function. Benefits: - Reduces function signatures from 9 parameters to 1 - Centralizes UI container management - Makes code more maintainable and testable - Easier to add new containers without updating all function signatures - Provides a clean API for container updates Example: >>> from utils.app_context import AppContext >>> context = AppContext() >>> context.update_container('output_box', create_output("Loading...")) >>> data = context.state.get_event_data() """ import panel as pn from typing import Dict, Any, Optional from utils.state_manager import state_manager from services import ServiceRegistry class AppContext: """ Application context that encapsulates all UI containers, state management, and services. This class provides a centralized way to access and update UI containers, application state, and business logic services throughout the dashboard. Attributes: state (StateManager): The application state manager services (ServiceRegistry): Registry of all business logic services containers (Dict[str, pn.viewable.Viewable]): Dictionary of UI containers Example: >>> context = AppContext() >>> context.update_container('header', create_home_header()) >>> result = context.services.data.load_event_list(...) >>> header = context.get_container('header') """ def __init__(self): """Initialize the AppContext with state manager, services, and empty containers.""" # Reference to the global state manager self.state = state_manager # Initialize service registry with state manager # Services provide all business logic operations self.services = ServiceRegistry(state_manager) # Dictionary of all UI containers # Keys are container names, values are Panel viewable objects self.containers: Dict[str, Any] = {} # Container metadata for debugging and validation self._container_metadata: Dict[str, Dict[str, Any]] = {} def register_container(self, name: str, container: Any, metadata: Optional[Dict[str, Any]] = None) -> None: """ Register a UI container with the context. Args: name (str): Unique identifier for the container container: Panel viewable object (Column, Row, FlexBox, etc.) metadata (Dict, optional): Additional metadata about the container Raises: ValueError: If container name is empty or already exists Example: >>> context.register_container('header', pn.Column()) >>> context.register_container('main_area', pn.Column(), {'purpose': 'main workspace'}) """ if not name or not name.strip(): raise ValueError("Container name cannot be empty") if name in self.containers: raise ValueError(f"Container '{name}' is already registered") self.containers[name] = container # Store metadata if metadata is None: metadata = {} metadata['registered_at'] = pn.state.as_cached('current_time', lambda: str(pn.pane.Markdown(''))) self._container_metadata[name] = metadata def get_container(self, name: str) -> Optional[Any]: """ Get a container by name. Args: name (str): Name of the container to retrieve Returns: Panel viewable object or None if not found Example: >>> header = context.get_container('header') >>> if header is not None: ... header.objects = [new_content] """ return self.containers.get(name) def update_container(self, name: str, content: Any) -> bool: """ Update the content of a container. This is the preferred method for updating containers as it uses Panel's official API (container.objects = [content]) instead of the slice assignment pattern (container[:] = [content]). Args: name (str): Name of the container to update content: New content to set (can be single item or list) Returns: bool: True if updated successfully, False if container not found Example: >>> context.update_container('output_box', create_output("Success!")) >>> context.update_container('plots', [plot1, plot2]) """ container = self.containers.get(name) if container is None: return False # Ensure content is a list if not isinstance(content, list): content = [content] # Use Panel's official API for updating container content container.objects = content return True def append_to_container(self, name: str, content: Any) -> bool: """ Append content to a container without replacing existing content. Args: name (str): Name of the container content: Content to append Returns: bool: True if appended successfully, False if container not found Example: >>> context.append_to_container('plots', new_plot) """ container = self.containers.get(name) if container is None: return False container.append(content) return True def clear_container(self, name: str) -> bool: """ Clear all content from a container. Args: name (str): Name of the container to clear Returns: bool: True if cleared successfully, False if container not found Example: >>> context.clear_container('output_box') """ container = self.containers.get(name) if container is None: return False container.clear() return True def has_container(self, name: str) -> bool: """ Check if a container exists. Args: name (str): Name of the container Returns: bool: True if container exists, False otherwise Example: >>> if context.has_container('header'): ... print("Header container exists") """ return name in self.containers def get_container_names(self) -> list: """ Get list of all registered container names. Returns: List[str]: List of container names Example: >>> names = context.get_container_names() >>> print(names) # ['header', 'main_area', 'output_box', ...] """ return list(self.containers.keys()) def get_container_metadata(self, name: str) -> Optional[Dict[str, Any]]: """ Get metadata for a container. Args: name (str): Name of the container Returns: Dict with metadata or None if not found Example: >>> metadata = context.get_container_metadata('header') """ return self._container_metadata.get(name) def unregister_container(self, name: str) -> bool: """ Unregister a container from the context. Args: name (str): Name of the container to unregister Returns: bool: True if unregistered, False if not found Example: >>> context.unregister_container('old_container') """ if name in self.containers: del self.containers[name] if name in self._container_metadata: del self._container_metadata[name] return True return False def get_info(self) -> Dict[str, Any]: """ Get information about the application context. Returns: Dict with context information including container count, state stats, services, etc. Example: >>> info = context.get_info() >>> print(f"Total containers: {info['container_count']}") """ return { 'container_count': len(self.containers), 'container_names': self.get_container_names(), 'state_stats': self.state.get_stats(), 'services': { 'data': 'DataService', 'lightcurve': 'LightcurveService', 'spectrum': 'SpectrumService', 'timing': 'TimingService', 'export': 'ExportService', } } def __repr__(self) -> str: """String representation of AppContext.""" return ( f"AppContext(containers={len(self.containers)}, " f"state={repr(self.state)})" ) # ============================================================================= # Singleton Instance (Optional) # ============================================================================= # You can create a singleton instance if needed, but for testing purposes # it's often better to create instances explicitly in the main application # app_context = AppContext()