feat: migrate coordinator-api routers and exchange_island CLI to use centralized aitbc package HTTP client
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
CLI Tests / test-cli (push) Failing after 3s
Integration Tests / test-service-integration (push) Successful in 42s
Python Tests / test-python (push) Failing after 39s
Security Scanning / security-scan (push) Successful in 2m36s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 3s
Blockchain Synchronization Verification / sync-verification (push) Failing after 2s

- Replace httpx.Client with aitbc.AITBCHTTPClient in client.py get_blocks endpoint
- Migrate monitoring_dashboard.py from httpx.AsyncClient to AITBCHTTPClient
- Replace httpx with AITBCHTTPClient in blockchain.py get_balance function
- Add NetworkError exception handling across all migrated endpoints
- Remove async context managers in favor of direct AITBCHTTPClient usage
- Remove httpx imports
This commit is contained in:
aitbc
2026-04-25 06:34:59 +02:00
parent e60aa70da9
commit 08d6921444
5 changed files with 135 additions and 113 deletions

View File

@@ -226,22 +226,27 @@ async def get_blocks(
) -> dict: # type: ignore[arg-type] ) -> dict: # type: ignore[arg-type]
"""Get recent blockchain blocks""" """Get recent blockchain blocks"""
try: try:
import httpx
# Query the local blockchain node for blocks # Query the local blockchain node for blocks
with httpx.Client() as client: client = AITBCHTTPClient(timeout=5.0)
response = client.get( try:
"http://10.1.223.93:8082/rpc/blocks-range", params={"start": offset, "end": offset + limit}, timeout=5 blocks_data = client.get(
"http://10.1.223.93:8082/rpc/blocks-range", params={"start": offset, "end": offset + limit}
) )
if response.status_code == 200:
blocks_data = response.json()
return { return {
"blocks": blocks_data.get("blocks", []), "blocks": blocks_data.get("blocks", []),
"total": blocks_data.get("total", 0), "total": blocks_data.get("total", 0),
"limit": limit, "limit": limit,
"offset": offset, "offset": offset,
} }
except NetworkError as e:
logger.error(f"Failed to fetch blocks: {e}")
return {
"blocks": [],
"total": 0,
"limit": limit,
"offset": offset,
"error": "Failed to fetch blocks",
}
else: else:
# Fallback to empty response if blockchain node is unavailable # Fallback to empty response if blockchain node is unavailable
return { return {

View File

@@ -191,7 +191,7 @@ async def collect_all_health_data() -> dict[str, Any]:
"""Collect health data from all enhanced services""" """Collect health data from all enhanced services"""
health_data = {} health_data = {}
async with httpx.AsyncClient(timeout=5.0) as client: client = AITBCHTTPClient(timeout=5.0)
tasks = [] tasks = []
for service_id, service_info in SERVICES.items(): for service_id, service_info in SERVICES.items():
@@ -209,6 +209,7 @@ async def collect_all_health_data() -> dict[str, Any]:
"timestamp": datetime.utcnow().isoformat(), "timestamp": datetime.utcnow().isoformat(),
} }
else: else:
health_data[service_id] = result
return health_data return health_data

View File

@@ -53,17 +53,16 @@ def get_balance(address: str) -> float | None:
return None return None
try: try:
with httpx.Client() as client: client = AITBCHTTPClient(timeout=10.0)
try:
response = client.get( response = client.get(
f"{BLOCKCHAIN_RPC}/getBalance/{address}", f"{BLOCKCHAIN_RPC}/getBalance/{address}",
headers={"X-Api-Key": settings.admin_api_keys[0] if settings.admin_api_keys else ""}, headers={"X-Api-Key": settings.admin_api_keys[0] if settings.admin_api_keys else ""},
) )
return float(response.get("balance", 0))
if response.status_code == 200: except NetworkError as e:
data = response.json() logger.error("Error getting balance: %s", e)
return float(data.get("balance", 0)) return None
except Exception as e: except Exception as e:
logger.error("Error getting balance: %s", e) logger.error("Error getting balance: %s", e)
return None return None

View File

@@ -8,6 +8,8 @@ import os
import pickle import pickle
from typing import Any from typing import Any
from aitbc import REPO_DIR
# Safe classes whitelist: builtins and common types # Safe classes whitelist: builtins and common types
SAFE_MODULES = { SAFE_MODULES = {
"builtins": { "builtins": {
@@ -40,8 +42,8 @@ def _initialize_allowed_origins():
# 1. All site-packages directories that are under the application venv # 1. All site-packages directories that are under the application venv
for entry in os.sys.path: for entry in os.sys.path:
if "site-packages" in entry and os.path.isdir(entry): if "site-packages" in entry and os.path.isdir(entry):
# Only include if it's inside /opt/aitbc/apps/coordinator-api/.venv or similar # Only include if it's inside the AITBC repository
if "/opt/aitbc" in entry: # restrict to our app directory if str(REPO_DIR) in entry: # restrict to our app directory
_ALLOWED_ORIGINS.add(os.path.realpath(entry)) _ALLOWED_ORIGINS.add(os.path.realpath(entry))
# 2. Standard library paths (typically without site-packages) # 2. Standard library paths (typically without site-packages)
# We'll allow any origin that resolves to a .py file outside site-packages and not in user dirs # We'll allow any origin that resolves to a .py file outside site-packages and not in user dirs
@@ -95,14 +97,14 @@ def _lock_sys_path():
if isinstance(sys.path, list): if isinstance(sys.path, list):
trusted = [] trusted = []
for p in sys.path: for p in sys.path:
# Keep site-packages under /opt/aitbc (our venv) # Keep site-packages under REPO_DIR (our venv)
if "site-packages" in p and "/opt/aitbc" in p: if "site-packages" in p and str(REPO_DIR) in p:
trusted.append(p) trusted.append(p)
# Keep stdlib paths (no site-packages, under /usr/lib/python) # Keep stdlib paths (no site-packages, under /usr/lib/python)
elif "site-packages" not in p and ("/usr/lib/python" in p or "/usr/local/lib/python" in p): elif "site-packages" not in p and ("/usr/lib/python" in p or "/usr/local/lib/python" in p):
trusted.append(p) trusted.append(p)
# Keep our application directory # Keep our application directory
elif p.startswith("/opt/aitbc/apps/coordinator-api"): elif p.startswith(str(REPO_DIR / "apps" / "coordinator-api")):
trusted.append(p) trusted.append(p)
sys.path = trusted sys.path = trusted

View File

@@ -198,16 +198,8 @@ def sell(ctx, ait_amount: float, quote_currency: str, min_price: Optional[float]
# Submit transaction to blockchain # Submit transaction to blockchain
try: try:
import httpx http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
with httpx.Client() as client: result = http_client.post("/transaction", json=sell_order_data)
response = client.post(
f"{rpc_endpoint}/transaction",
json=sell_order_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Sell order created successfully!") success(f"Sell order created successfully!")
success(f"Order ID: {order_id}") success(f"Order ID: {order_id}")
success(f"Selling {ait_amount} AIT for {quote_currency}") success(f"Selling {ait_amount} AIT for {quote_currency}")
@@ -225,17 +217,10 @@ def sell(ctx, ait_amount: float, quote_currency: str, min_price: Optional[float]
"User": user_id[:16] + "...", "User": user_id[:16] + "...",
"Island": island_id[:16] + "..." "Island": island_id[:16] + "..."
} }
output(order_info, ctx.obj.get('output_format', 'table')) output(order_info, ctx.obj.get('output_format', 'table'))
else: except NetworkError as e:
error(f"Failed to submit transaction: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception as e:
error(f"Network error submitting transaction: {e}") error(f"Network error submitting transaction: {e}")
raise click.Abort() raise click.Abort()
except Exception as e: except Exception as e:
error(f"Error creating sell order: {str(e)}") error(f"Error creating sell order: {str(e)}")
raise click.Abort() raise click.Abort()
@@ -255,7 +240,6 @@ def orderbook(ctx, pair: str, limit: int):
# Query blockchain for exchange orders # Query blockchain for exchange orders
try: try:
import httpx
params = { params = {
'transaction_type': 'exchange', 'transaction_type': 'exchange',
'island_id': island_id, 'island_id': island_id,
@@ -264,21 +248,14 @@ def orderbook(ctx, pair: str, limit: int):
'limit': limit * 2 # Get both buys and sells 'limit': limit * 2 # Get both buys and sells
} }
with httpx.Client() as client: http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
response = client.get( transactions = http_client.get("/transactions", params=params)
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
orders = response.json()
# Separate buy and sell orders # Separate buy and sell orders
buy_orders = [] buy_orders = []
sell_orders = [] sell_orders = []
for order in orders: for order in transactions:
if order.get('side') == 'buy': if order.get('side') == 'buy':
buy_orders.append(order) buy_orders.append(order)
elif order.get('side') == 'sell': elif order.get('side') == 'sell':
@@ -331,6 +308,44 @@ def orderbook(ctx, pair: str, limit: int):
info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)") info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)")
info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT") info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT")
info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT") info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT")
except NetworkError as e:
error(f"Network error fetching order book: {e}")
raise click.Abort()
except Exception as e:
error(f"Error fetching order book: {str(e)}")
raise click.Abort()
"Amount": f"{order.get('amount', 0):.4f} AIT",
"Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
"User": order.get('user_id', '')[:16] + "...",
"Order": order.get('order_id', '')[:16] + "..."
})
output(asks_data, ctx.obj.get('output_format', 'table'), title=f"Sell Orders (Asks) - {pair}")
# Display buy orders (bids)
if buy_orders:
bids_data = []
for order in buy_orders[:limit]:
bids_data.append({
"Price": f"{order.get('max_price', 0):.8f}",
"Amount": f"{order.get('amount', 0):.4f} AIT",
"Total": f"{order.get('max_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
"User": order.get('user_id', '')[:16] + "...",
"Order": order.get('order_id', '')[:16] + "..."
})
output(bids_data, ctx.obj.get('output_format', 'table'), title=f"Buy Orders (Bids) - {pair}")
# Calculate spread if both exist
if sell_orders and buy_orders:
best_ask = sell_orders[0].get('min_price', 0)
best_bid = buy_orders[0].get('max_price', 0)
spread = best_ask - best_bid
if best_bid > 0:
spread_pct = (spread / best_bid) * 100
info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)")
info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT")
info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT")
else: else:
error(f"Failed to query blockchain: {response.status_code}") error(f"Failed to query blockchain: {response.status_code}")