Files
aitbc/cli/core/node_client.py
aitbc a7b6e39cdf fix: replace all print() with click.echo() or logger in CLI production code
- 55 CLI files: handlers/, aitbc_cli/commands/, cli/core/, cli/utils/, top-level scripts
- Click-based files: print() -> click.echo()
- Library modules: print() -> logger.info/error/warning
- Fixed pre-existing indentation bugs in monitor.py dashboard function
- Fixed bare print() -> logger.info('') in chain_manager.py
- 0 remaining print() in production CLI code
- All files compile cleanly
2026-05-25 13:53:49 +02:00

391 lines
16 KiB
Python
Executable File

"""
Node client for multi-chain operations
"""
import asyncio
import httpx
import json
import os
import logging
from typing import Dict, List, Optional, Any
from core.config import NodeConfig
from models.chain import ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm
logger = logging.getLogger(__name__)
class NodeClient:
"""Client for communicating with AITBC nodes"""
def __init__(self, node_config: NodeConfig):
self.config = node_config
self._client: Optional[httpx.AsyncClient] = None
self._session_id: Optional[str] = None
self._mock_fallback_count = 0
self._dev_mocks_enabled = os.getenv("DEV_MOCKS_ENABLED", "false").lower() == "true"
async def __aenter__(self):
"""Async context manager entry"""
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(self.config.timeout),
limits=httpx.Limits(max_connections=self.config.max_connections)
)
await self._authenticate()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit"""
if self._client:
await self._client.aclose()
async def _authenticate(self):
"""Authenticate with the node"""
try:
# For now, we'll use a simple authentication
# In production, this would use proper authentication
response = await self._client.post(
f"{self.config.endpoint}/api/auth",
json={"action": "authenticate"}
)
if response.status_code == 200:
data = response.json()
self._session_id = data.get("session_id")
except Exception as e:
# For development, we'll continue without authentication
if self._dev_mocks_enabled:
logger.warning(f"[DEV_MODE] Authentication failed for node {self.config.id}: {e}")
else:
logger.error(f"Authentication failed for node {self.config.id}: {e}")
raise
async def get_node_info(self) -> Dict[str, Any]:
"""Get node information"""
try:
response = await self._client.get(f"{self.config.endpoint}/api/node/info")
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Node info request failed: {response.status_code}")
except Exception as e:
# Return mock data for development
if self._dev_mocks_enabled:
self._mock_fallback_count += 1
logger.warning(f"[DEV_MODE] Using mock node info for {self.config.id} (fallback #{self._mock_fallback_count})")
return self._get_mock_node_info()
else:
logger.error(f"Failed to get node info for {self.config.id}: {e}")
raise
async def get_hosted_chains(self) -> List[ChainInfo]:
"""Get all chains hosted by this node"""
try:
health_url = f"{self.config.endpoint}/health"
if "/rpc" in self.config.endpoint:
health_url = self.config.endpoint.replace("/rpc", "/health")
response = await self._client.get(health_url)
if response.status_code == 200:
health_data = response.json()
chains = health_data.get("supported_chains", ["ait-devnet"])
result = []
for cid in chains:
# Try to fetch real block height
block_height = 0
try:
head_url = f"{self.config.endpoint}/rpc/head?chain_id={cid}"
if "/rpc" in self.config.endpoint:
head_url = f"{self.config.endpoint}/head?chain_id={cid}"
head_resp = await self._client.get(head_url, timeout=2.0)
if head_resp.status_code == 200:
head_data = head_resp.json()
block_height = head_data.get("height", 0)
except Exception:
pass
result.append(self._parse_chain_info({
"id": cid,
"name": f"AITBC {cid.split('-')[-1].capitalize()} Chain",
"type": "topic" if "health" in cid else "main",
"purpose": "specialized" if "health" in cid else "general",
"status": "active",
"size_mb": 50.5,
"nodes": 3,
"smart_contracts": 5,
"active_clients": 25,
"active_miners": 8,
"block_height": block_height,
"privacy": {"visibility": "public"}
}))
return result
else:
return self._get_mock_chains()
except Exception as e:
return self._get_mock_chains()
async def get_chain_info(self, chain_id: str) -> Optional[ChainInfo]:
"""Get specific chain information"""
try:
# Re-use the health endpoint logic
health_url = f"{self.config.endpoint}/health"
if "/rpc" in self.config.endpoint:
health_url = self.config.endpoint.replace("/rpc", "/health")
response = await self._client.get(health_url)
if response.status_code == 200:
health_data = response.json()
chains = health_data.get("supported_chains", ["ait-devnet"])
if chain_id in chains:
block_height = 0
try:
head_url = f"{self.config.endpoint}/rpc/head?chain_id={chain_id}"
if "/rpc" in self.config.endpoint:
head_url = f"{self.config.endpoint}/head?chain_id={chain_id}"
head_resp = await self._client.get(head_url, timeout=2.0)
if head_resp.status_code == 200:
head_data = head_resp.json()
block_height = head_data.get("height", 0)
except Exception:
pass
return self._parse_chain_info({
"id": chain_id,
"name": f"AITBC {chain_id.split('-')[-1].capitalize()} Chain",
"type": "topic" if "health" in chain_id else "main",
"purpose": "specialized" if "health" in chain_id else "general",
"status": "active",
"size_mb": 50.5,
"nodes": 3,
"smart_contracts": 5,
"active_clients": 25,
"active_miners": 8,
"block_height": block_height,
"privacy": {"visibility": "public"}
})
return None
except Exception as e:
# Fallback to pure mock
chains = self._get_mock_chains()
for chain in chains:
if chain.id == chain_id:
return chain
return None
async def create_chain(self, genesis_block: Dict[str, Any]) -> str:
"""Create a new chain on this node"""
try:
response = await self._client.post(
f"{self.config.endpoint}/api/chains",
json=genesis_block
)
if response.status_code == 201:
data = response.json()
return data["chain_id"]
else:
raise Exception(f"Chain creation failed: {response.status_code}")
except Exception as e:
# Mock chain creation for development
chain_id = genesis_block.get("chain_id", f"MOCK-CHAIN-{hash(str(genesis_block)) % 10000}")
logger.info(f"Mock created chain {chain_id} on node {self.config.id}")
return chain_id
async def delete_chain(self, chain_id: str) -> bool:
"""Delete a chain from this node"""
try:
response = await self._client.delete(f"{self.config.endpoint}/api/chains/{chain_id}")
if response.status_code == 200:
return True
else:
raise Exception(f"Chain deletion failed: {response.status_code}")
except Exception as e:
# Mock chain deletion for development
logger.info(f"Mock deleted chain {chain_id} from node {self.config.id}")
return True
async def get_chain_stats(self, chain_id: str) -> Dict[str, Any]:
"""Get chain statistics"""
try:
response = await self._client.get(f"{self.config.endpoint}/api/chains/{chain_id}/stats")
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Chain stats request failed: {response.status_code}")
except Exception as e:
# Return mock stats for development
return self._get_mock_chain_stats(chain_id)
async def backup_chain(self, chain_id: str, backup_path: str) -> Dict[str, Any]:
"""Backup a chain"""
try:
response = await self._client.post(
f"{self.config.endpoint}/api/chains/{chain_id}/backup",
json={"backup_path": backup_path}
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Chain backup failed: {response.status_code}")
except Exception as e:
# Mock backup for development
backup_info = {
"chain_id": chain_id,
"backup_file": f"{backup_path}/{chain_id}_backup.tar.gz",
"original_size_mb": 100.0,
"backup_size_mb": 50.0,
"checksum": "mock_checksum_12345"
}
logger.info(f"Mock backed up chain {chain_id} to {backup_info['backup_file']}")
return backup_info
async def restore_chain(self, backup_file: str, chain_id: Optional[str] = None) -> Dict[str, Any]:
"""Restore a chain from backup"""
try:
response = await self._client.post(
f"{self.config.endpoint}/api/chains/restore",
json={"backup_file": backup_file, "chain_id": chain_id}
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Chain restore failed: {response.status_code}")
except Exception as e:
# Mock restore for development
restore_info = {
"chain_id": chain_id or "RESTORED-MOCK-CHAIN",
"blocks_restored": 1000,
"verification_passed": True
}
logger.info(f"Mock restored chain from {backup_file}")
return restore_info
def _parse_chain_info(self, chain_data: Dict[str, Any]) -> ChainInfo:
"""Parse chain data from node response"""
from datetime import datetime
from models.chain import PrivacyConfig
return ChainInfo(
id=chain_data.get("chain_id", chain_data.get("id", "unknown")),
type=ChainType(chain_data.get("chain_type", "topic")),
purpose=chain_data.get("purpose", "unknown"),
name=chain_data.get("name", "Unnamed Chain"),
description=chain_data.get("description"),
status=ChainStatus(chain_data.get("status", "active")),
created_at=datetime.fromisoformat(chain_data.get("created_at", "2024-01-01T00:00:00")),
block_height=chain_data.get("block_height", 0),
size_mb=chain_data.get("size_mb", 0.0),
node_count=chain_data.get("node_count", 1),
active_nodes=chain_data.get("active_nodes", 1),
contract_count=chain_data.get("contract_count", 0),
client_count=chain_data.get("client_count", 0),
miner_count=chain_data.get("miner_count", 0),
agent_count=chain_data.get("agent_count", 0),
consensus_algorithm=ConsensusAlgorithm(chain_data.get("consensus_algorithm", "pos")),
block_time=chain_data.get("block_time", 5),
tps=chain_data.get("tps", 0.0),
avg_block_time=chain_data.get("avg_block_time", 5.0),
avg_gas_used=chain_data.get("avg_gas_used", 0),
growth_rate_mb_per_day=chain_data.get("growth_rate_mb_per_day", 0.0),
gas_price=chain_data.get("gas_price", 20000000000),
memory_usage_mb=chain_data.get("memory_usage_mb", 0.0),
disk_usage_mb=chain_data.get("disk_usage_mb", 0.0),
privacy=PrivacyConfig(
visibility=chain_data.get("privacy", {}).get("visibility", "public"),
access_control=chain_data.get("privacy", {}).get("access_control", "open")
)
)
def _get_mock_node_info(self) -> Dict[str, Any]:
"""Get mock node information for development"""
return {
"node_id": self.config.id,
"type": "full",
"status": "active",
"version": "1.0.0",
"uptime_days": 30,
"uptime_hours": 720,
"hosted_chains": {},
"cpu_usage": 25.5,
"memory_usage_mb": 1024.0,
"disk_usage_mb": 10240.0,
"network_in_mb": 10.5,
"network_out_mb": 8.2
}
def _get_mock_chains(self) -> List[ChainInfo]:
"""Get mock chains for development"""
from datetime import datetime
from models.chain import PrivacyConfig
return [
ChainInfo(
id="AITBC-TOPIC-HEALTHCARE-001",
type=ChainType.TOPIC,
purpose="healthcare",
name="Healthcare AI Chain",
description="A specialized chain for healthcare AI applications",
status=ChainStatus.ACTIVE,
created_at=datetime.now(),
block_height=1000,
size_mb=50.5,
node_count=3,
active_nodes=3,
contract_count=5,
client_count=25,
miner_count=8,
agent_count=12,
consensus_algorithm=ConsensusAlgorithm.POS,
block_time=3,
tps=15.5,
avg_block_time=3.2,
avg_gas_used=5000000,
growth_rate_mb_per_day=2.1,
gas_price=20000000000,
memory_usage_mb=256.0,
disk_usage_mb=512.0,
privacy=PrivacyConfig(visibility="public", access_control="open")
),
ChainInfo(
id="AITBC-PRIVATE-COLLAB-001",
type=ChainType.PRIVATE,
purpose="collaboration",
name="Private Research Chain",
description="A private chain for trusted agent collaboration",
status=ChainStatus.ACTIVE,
created_at=datetime.now(),
block_height=500,
size_mb=25.2,
node_count=2,
active_nodes=2,
contract_count=3,
client_count=8,
miner_count=4,
agent_count=6,
consensus_algorithm=ConsensusAlgorithm.POA,
block_time=5,
tps=8.0,
avg_block_time=5.1,
avg_gas_used=3000000,
growth_rate_mb_per_day=1.0,
gas_price=15000000000,
memory_usage_mb=128.0,
disk_usage_mb=256.0,
privacy=PrivacyConfig(visibility="private", access_control="invite_only")
)
]
def _get_mock_chain_stats(self, chain_id: str) -> Dict[str, Any]:
"""Get mock chain statistics for development"""
return {
"chain_id": chain_id,
"block_height": 1000,
"tps": 15.5,
"avg_block_time": 3.2,
"gas_price": 20000000000,
"memory_usage_mb": 256.0,
"disk_usage_mb": 512.0,
"active_nodes": 3,
"client_count": 25,
"miner_count": 8,
"agent_count": 12,
"last_block_time": "2024-03-02T10:00:00Z"
}