from typing import Optional, Callable, Type, Any, Dict, TypeVar, Generic
from pawnlib.config import pawn, NestedNamespace
from pawnlib.utils.http import NetworkInfo
from dataclasses import dataclass, field
from pawnlib.output import print_var
import os
from dotenv import load_dotenv, find_dotenv
[docs]def str2bool(value: str) -> bool:
return value.lower() in ("yes", "true", "t", "1")
[docs]class SettingDefinition:
def __init__(
self,
env_var: str,
default: Optional[Any] = None,
value_type: Type = str,
is_list: bool = False,
custom_converter: Optional[Callable[[str], Any]] = None
):
self.env_var = env_var
self.default = default
self.value_type = value_type
self.is_list = is_list
self.custom_converter = custom_converter
[docs] def get_value(self, args, attr_name: str) -> Any:
"""설정 값을 가져오는 메서드"""
# 명령줄 인수 우선 확인
value = getattr(args, attr_name, None)
if args.priority == 'args' and value is not None and value != 0:
pawn.console.debug(f"Using command-line argument for '{attr_name}': {value}")
return value
# 환경 변수 확인
env_value = os.environ.get(self.env_var, None)
if env_value is not None:
try:
if self.is_list:
pawn.console.debug(f"<is_list> Using environment variable for '{attr_name}': {env_value}")
return [item.strip() for item in env_value.split(",") if item.strip()]
elif self.custom_converter:
pawn.console.debug(f"Using custom converter for '{attr_name}': {env_value}")
return self.custom_converter(env_value)
elif self.value_type == bool:
pawn.console.debug(f"Using environment variable for '{attr_name}': {env_value}")
return str2bool(env_value)
elif self.value_type == int:
env_value_int = int(env_value)
pawn.console.debug(f"Using environment variable for '{attr_name}': {env_value_int}")
return env_value_int
else:
pawn.console.debug(f"Using environment variable for '{attr_name}': {env_value}")
return env_value
except (ValueError, TypeError) as e:
pawn.console.debug(f"[WARN] Invalid value for '{self.env_var}' (type: {self.value_type.__name__}), using default: {self.default}. Error: {e}")
return self.default
# 기본값 반환
pawn.console.debug(f"Using default value for '{attr_name}': {self.default}")
return self.default
[docs]@dataclass
class BaseSettingsConfig:
[docs] def get_definitions(self) -> Dict[str, SettingDefinition]:
"""모든 설정 정의를 딕셔너리로 반환"""
return {key: value for key, value in self.__dict__.items() if isinstance(value, SettingDefinition)}
[docs] def add_definition(self, name: str, definition: SettingDefinition):
"""새로운 설정 정의 추가"""
setattr(self, name, definition)
T = TypeVar('T', bound=BaseSettingsConfig)
[docs]def load_environment_settings(args, settings_config: Type[T] = BaseSettingsConfig) -> Dict[str, Any]:
"""
Load environment settings from command-line arguments or environment variables,
prioritizing args if provided, otherwise using environment variables.
Args:
args: Command-line arguments object
settings_config: BaseSettingsConfig instance (or its subclass) containing setting definitions
"""
load_dotenv()
settings: Dict[str, Any] = {}
# 정의된 설정 로드
for attr_name, definition in settings_config().get_definitions().items():
settings[attr_name] = definition.get_value(args, attr_name)
# 동적으로 추가된 args 속성 반영
for attr_name in dir(args):
if (attr_name not in settings_config().get_definitions() and
not attr_name.startswith('_') and
attr_name != 'priority'):
settings[attr_name] = getattr(args, attr_name)
return settings
[docs]class AppConfig(NestedNamespace):
"""
A configuration class accessible in a namespace style.
The settings are provided as a dictionary and converted to NestedNamespace for use.
Attributes:
network: Network information (based on NetworkInfo, converted to NestedNamespace)
extras: Additional settings or metadata (converted to NestedNamespace)
"""
def __init__(
self,
network_info: NetworkInfo = None,
extras: Optional[Dict[str, Any]] = None,
**settings
):
if not isinstance(settings, dict):
raise ValueError(f"settings must be a dict. Received type: {type(settings)}")
# Check if settings is a dictionary and convert it to NestedNamespace
# if settings and not isinstance(settings, dict):
# raise ValueError(f"settings must be a dict. Received type: {type(settings)}")
# Initialize with NestedNamespace for settings, network, and extras
super().__init__(
network_info=network_info,
extras=NestedNamespace(**(extras or {})),
**settings
)
def __repr__(self):
return f"AppConfig({self.__dict__})"