# ============================================================================
# science_live/core/config.py
# ============================================================================
"""
Configuration Management
=======================
Configuration system for Science Live applications with support for
YAML, JSON, and environment variables.
"""
import os
import yaml
import json
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Union
from pathlib import Path
[docs]
@dataclass
class EndpointConfig:
"""Configuration for nanopub endpoints"""
name: str
type: str # 'standard', 'test', 'custom'
url: str
is_default: bool = False
timeout: int = 30
retry_attempts: int = 3
headers: Dict[str, str] = field(default_factory=dict)
[docs]
@dataclass
class TemplateConfig:
"""Configuration for template system"""
repository_type: str = 'network' # 'network', 'local', 'hybrid'
cache_enabled: bool = True
cache_dir: str = "./template_cache"
cache_ttl_hours: int = 24
preload_templates: List[str] = field(default_factory=list)
custom_templates_dir: Optional[str] = None
[docs]
@dataclass
class ProcessorConfig:
"""Configuration for query processors"""
enabled_processors: List[str] = field(default_factory=lambda: ['template_based', 'text_search'])
text_search_limit: int = 20
template_match_threshold: float = 0.3
sparql_timeout: int = 30
enable_caching: bool = True
[docs]
@dataclass
class UIConfig:
"""Configuration for user interfaces"""
interface_type: str = 'web' # 'web', 'api', 'cli', 'jupyter'
theme: str = 'default'
enable_suggestions: bool = True
max_results_per_page: int = 20
enable_export: bool = True
export_formats: List[str] = field(default_factory=lambda: ['json', 'csv', 'rdf'])
[docs]
@dataclass
class ScienceLiveConfig:
"""Main configuration for Science Live applications"""
app_name: str
app_type: str = 'general' # 'general', 'citation_explorer', 'concept_explorer', etc.
version: str = "1.0.0"
endpoints: List[EndpointConfig] = field(default_factory=list)
templates: TemplateConfig = field(default_factory=TemplateConfig)
processors: ProcessorConfig = field(default_factory=ProcessorConfig)
ui: UIConfig = field(default_factory=UIConfig)
# Extensions and plugins
plugins: List[str] = field(default_factory=list)
custom_modules: Dict[str, str] = field(default_factory=dict)
# Logging and monitoring
log_level: str = "INFO"
enable_metrics: bool = True
metrics_endpoint: Optional[str] = None
[docs]
class ConfigLoader:
"""Load configuration from various sources"""
[docs]
@staticmethod
def from_yaml(config_path: Union[str, Path]) -> ScienceLiveConfig:
"""Load configuration from YAML file"""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, 'r') as f:
config_data = yaml.safe_load(f)
return ConfigLoader._dict_to_config(config_data)
[docs]
@staticmethod
def from_json(config_path: Union[str, Path]) -> ScienceLiveConfig:
"""Load configuration from JSON file"""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, 'r') as f:
config_data = json.load(f)
return ConfigLoader._dict_to_config(config_data)
[docs]
@staticmethod
def from_dict(config_dict: Dict[str, Any]) -> ScienceLiveConfig:
"""Load configuration from dictionary"""
return ConfigLoader._dict_to_config(config_dict)
[docs]
@staticmethod
def from_env(prefix: str = "SCIENCE_LIVE_") -> ScienceLiveConfig:
"""Load configuration from environment variables"""
config_data = {}
# Extract environment variables with the given prefix
for key, value in os.environ.items():
if key.startswith(prefix):
config_key = key[len(prefix):].lower()
config_data[config_key] = value
# Set defaults for required fields
config_data.setdefault('app_name', 'Science Live')
config_data.setdefault('app_type', 'general')
return ConfigLoader._dict_to_config(config_data)
[docs]
@staticmethod
def _dict_to_config(data: Dict[str, Any]) -> ScienceLiveConfig:
"""Convert dictionary to configuration object"""
# Parse endpoints
endpoints = []
for ep_data in data.get('endpoints', []):
endpoints.append(EndpointConfig(**ep_data))
# Parse template config
template_data = data.get('templates', {})
templates = TemplateConfig(**template_data)
# Parse processor config
processor_data = data.get('processors', {})
processors = ProcessorConfig(**processor_data)
# Parse UI config
ui_data = data.get('ui', {})
ui = UIConfig(**ui_data)
return ScienceLiveConfig(
app_name=data['app_name'],
app_type=data.get('app_type', 'general'),
version=data.get('version', '1.0.0'),
endpoints=endpoints,
templates=templates,
processors=processors,
ui=ui,
plugins=data.get('plugins', []),
custom_modules=data.get('custom_modules', {}),
log_level=data.get('log_level', 'INFO'),
enable_metrics=data.get('enable_metrics', True),
metrics_endpoint=data.get('metrics_endpoint')
)
[docs]
@staticmethod
def create_default_config() -> ScienceLiveConfig:
"""Create a default configuration"""
return ScienceLiveConfig(
app_name="Science Live Default",
app_type="general",
endpoints=[
EndpointConfig(
name="test",
type="test",
url="https://test.nanopub.org",
is_default=True
)
]
)
[docs]
def save_config(config: ScienceLiveConfig, path: Union[str, Path], format: str = 'yaml'):
"""Save configuration to file"""
path = Path(path)
# Convert to dict (simplified)
config_dict = {
'app_name': config.app_name,
'app_type': config.app_type,
'version': config.version,
'endpoints': [
{
'name': ep.name,
'type': ep.type,
'url': ep.url,
'is_default': ep.is_default,
'timeout': ep.timeout
}
for ep in config.endpoints
],
'log_level': config.log_level,
'enable_metrics': config.enable_metrics
}
if format.lower() == 'yaml':
with open(path, 'w') as f:
yaml.dump(config_dict, f, default_flow_style=False, indent=2)
elif format.lower() == 'json':
with open(path, 'w') as f:
json.dump(config_dict, f, indent=2)
else:
raise ValueError(f"Unsupported format: {format}")