From 26f7d72b8e32260a6bd0b31cf5ce9cbdc0dc3fe6 Mon Sep 17 00:00:00 2001 From: aitbc Date: Tue, 19 May 2026 19:53:34 +0200 Subject: [PATCH] fix: resolve CLI config package shadowing config.py module Renamed cli/config/ directory to cli/config_data/ to prevent package shadowing of cli/aitbc_cli/config.py module. Removed problematic sys.modules manipulation in aitbc_cli/__init__.py that was causing circular import issues. Now 'from aitbc_cli.config import CLIConfig' works correctly. --- cli/aitbc_cli/__init__.py | 3 +- cli/config_data/__init__.py | 114 ++++++++++++++++++++++++++++++++ cli/config_data/chains.py | 126 ++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 2 deletions(-) create mode 100755 cli/config_data/__init__.py create mode 100644 cli/config_data/chains.py diff --git a/cli/aitbc_cli/__init__.py b/cli/aitbc_cli/__init__.py index ba5c84ff..13f41ac1 100644 --- a/cli/aitbc_cli/__init__.py +++ b/cli/aitbc_cli/__init__.py @@ -14,8 +14,7 @@ if "aitbc_cli.core" not in sys.modules: sys.modules["aitbc_cli.core"] = import_module("core") if "aitbc_cli.models" not in sys.modules: sys.modules["aitbc_cli.models"] = import_module("models") -if "aitbc_cli.config" not in sys.modules: - sys.modules["aitbc_cli.config"] = import_module("config") +# Note: aitbc_cli.config is imported normally to avoid circular import issues __all__ = ["cli", "main"] diff --git a/cli/config_data/__init__.py b/cli/config_data/__init__.py new file mode 100755 index 00000000..fbe93de5 --- /dev/null +++ b/cli/config_data/__init__.py @@ -0,0 +1,114 @@ +"""Configuration management for AITBC CLI""" + +import os +import yaml +from pathlib import Path +from typing import Optional +from dataclasses import dataclass, field +from dotenv import load_dotenv + + +@dataclass +class Config: + """Configuration object for AITBC CLI""" + coordinator_url: str = "http://127.0.0.1:8011" + api_key: Optional[str] = None + role: Optional[str] = None # admin, client, miner, etc. + config_dir: Path = field(default_factory=lambda: Path.home() / ".aitbc") + config_file: Optional[str] = None + blockchain_rpc_url: str = "http://127.0.0.1:8006" + wallet_url: str = "http://127.0.0.1:8002" + + def _validate_localhost_urls(self): + """Validate that all service URLs point to localhost""" + localhost_prefixes = ["http://localhost:", "http://127.0.0.1:", "https://localhost:", "https://127.0.0.1:"] + + urls_to_check = [ + ("coordinator_url", self.coordinator_url), + ("blockchain_rpc_url", self.blockchain_rpc_url), + ("wallet_url", self.wallet_url) + ] + + for url_name, url in urls_to_check: + if not any(url.startswith(prefix) for prefix in localhost_prefixes): + # Force to localhost if not already + if url_name == "coordinator_url": + self.coordinator_url = "http://localhost:8011" + elif url_name == "blockchain_rpc_url": + self.blockchain_rpc_url = "http://localhost:8006" + elif url_name == "wallet_url": + self.wallet_url = "http://localhost:8002" + + def __post_init__(self): + """Initialize configuration""" + # Load environment variables + load_dotenv() + + # Set default config file based on role if not specified + if not self.config_file: + if self.role: + self.config_file = str(self.config_dir / f"{self.role}-config.yaml") + else: + self.config_file = str(self.config_dir / "config.yaml") + + # Load config from file if it exists + self.load_from_file() + + # Override with environment variables + if os.getenv("AITBC_URL"): + self.coordinator_url = os.getenv("AITBC_URL") + if os.getenv("AITBC_API_KEY"): + self.api_key = os.getenv("AITBC_API_KEY") + if os.getenv("AITBC_ROLE"): + self.role = os.getenv("AITBC_ROLE") + if os.getenv("AITBC_BLOCKCHAIN_RPC_URL"): + self.blockchain_rpc_url = os.getenv("AITBC_BLOCKCHAIN_RPC_URL") + if os.getenv("AITBC_WALLET_URL"): + self.wallet_url = os.getenv("AITBC_WALLET_URL") + + # Validate and enforce localhost URLs + self._validate_localhost_urls() + + def load_from_file(self): + """Load configuration from YAML file""" + if self.config_file and Path(self.config_file).exists(): + try: + with open(self.config_file, 'r') as f: + data = yaml.safe_load(f) or {} + + self.coordinator_url = data.get('coordinator_url', self.coordinator_url) + self.api_key = data.get('api_key', self.api_key) + self.role = data.get('role', self.role) + self.blockchain_rpc_url = data.get('blockchain_rpc_url', self.blockchain_rpc_url) + self.wallet_url = data.get('wallet_url', self.wallet_url) + except Exception as e: + print(f"Warning: Could not load config file: {e}") + + # Validate and enforce localhost URLs after file loading + self._validate_localhost_urls() + + def save_to_file(self): + """Save configuration to YAML file""" + if not self.config_file: + return + + # Ensure config directory exists + Path(self.config_file).parent.mkdir(parents=True, exist_ok=True) + + data = { + 'coordinator_url': self.coordinator_url, + 'api_key': self.api_key, + 'blockchain_rpc_url': self.blockchain_rpc_url, + 'wallet_url': self.wallet_url + } + + if self.role: + data['role'] = self.role + + with open(self.config_file, 'w') as f: + yaml.dump(data, f, default_flow_style=False) + + +def get_config(config_file: Optional[str] = None, role: Optional[str] = None) -> Config: + """Get configuration instance with optional role""" + return Config(config_file=config_file, role=role) diff --git a/cli/config_data/chains.py b/cli/config_data/chains.py new file mode 100644 index 00000000..5a725218 --- /dev/null +++ b/cli/config_data/chains.py @@ -0,0 +1,126 @@ +""" +Chain registry configuration for AITBC CLI +Manages available blockchain networks and their configurations +""" + +from typing import Dict, Optional +from dataclasses import dataclass +import os + + +@dataclass +class ChainConfig: + """Configuration for a blockchain network""" + chain_id: str + name: str + rpc_url: str + explorer_url: Optional[str] = None + is_testnet: bool = False + native_currency: str = "AITBC" + + +class ChainRegistry: + """Registry for managing blockchain network configurations""" + + def __init__(self): + """Initialize chain registry with default chains""" + self.chains: Dict[str, ChainConfig] = {} + self._load_default_chains() + + def _load_default_chains(self) -> None: + """Load default chain configurations""" + # AITBC Devnet + self.chains["ait-devnet"] = ChainConfig( + chain_id="ait-devnet", + name="AITBC Development Network", + rpc_url="http://localhost:8025", + explorer_url="http://localhost:8026", + is_testnet=True, + native_currency="AITBC" + ) + + # AITBC Testnet + self.chains["ait-testnet"] = ChainConfig( + chain_id="ait-testnet", + name="AITBC Test Network", + rpc_url="http://localhost:8027", + explorer_url="http://localhost:8028", + is_testnet=True, + native_currency="AITBC" + ) + + def get_chain(self, chain_id: str) -> Optional[ChainConfig]: + """Get chain configuration by ID""" + return self.chains.get(chain_id) + + def get_all_chains(self) -> Dict[str, ChainConfig]: + """Get all registered chains""" + return self.chains.copy() + + def get_chain_ids(self) -> list[str]: + """Get list of all chain IDs""" + return list(self.chains.keys()) + + def get_testnet_chains(self) -> Dict[str, ChainConfig]: + """Get all testnet chains""" + return { + chain_id: config + for chain_id, config in self.chains.items() + if config.is_testnet + } + + def get_mainnet_chains(self) -> Dict[str, ChainConfig]: + """Get all mainnet chains""" + return { + chain_id: config + for chain_id, config in self.chains.items() + if not config.is_testnet + } + + def register_chain(self, chain_id: str, config: ChainConfig) -> None: + """Register a new chain configuration""" + self.chains[chain_id] = config + + def unregister_chain(self, chain_id: str) -> bool: + """Unregister a chain configuration""" + if chain_id in self.chains: + del self.chains[chain_id] + return True + return False + + def load_from_env(self) -> None: + """Load additional chains from environment variables""" + # Format: AITBC_CHAIN__NAME, AITBC_CHAIN__RPC_URL, etc. + for key, value in os.environ.items(): + if key.startswith("AITBC_CHAIN_") and key.endswith("_RPC_URL"): + chain_id = key.replace("AITBC_CHAIN_", "").replace("_RPC_URL", "").lower() + + name = os.environ.get(f"AITBC_CHAIN_{chain_id.upper()}_NAME", chain_id) + explorer_url = os.environ.get(f"AITBC_CHAIN_{chain_id.upper()}_EXPLORER_URL") + is_testnet = os.environ.get(f"AITBC_CHAIN_{chain_id.upper()}_IS_TESTNET", "false").lower() == "true" + native_currency = os.environ.get(f"AITBC_CHAIN_{chain_id.upper()}_NATIVE_CURRENCY", "AITBC") + + self.register_chain( + chain_id, + ChainConfig( + chain_id=chain_id, + name=name, + rpc_url=value, + explorer_url=explorer_url, + is_testnet=is_testnet, + native_currency=native_currency + ) + ) + + +# Global chain registry instance +_chain_registry: Optional[ChainRegistry] = None + + +def get_chain_registry() -> ChainRegistry: + """Get or create global chain registry instance""" + global _chain_registry + if _chain_registry is None: + _chain_registry = ChainRegistry() + _chain_registry.load_from_env() + return _chain_registry