mirror of
https://github.com/smittix/intercept.git
synced 2026-06-17 09:59:47 -07:00
d4652017f5
- meshcore pin >=2.3.0 (EventType.STATS_CORE floor); setup.sh derives optional packages from requirements.txt; Python 3.10 warning - agent-mode wifi clients proxy route + bare-array response handling - remove dead AIS/ACARS/VDL2 SPA wiring and orphaned partials/CSS - agent TLE download to data/tle/ (was littering repo root as gp.php) - gate deferred background init off under pytest (mock-pollution race) - complete Popen mocks (context manager protocol, communicate tuples) - real pipe fds in weather-sat decoder tests (fd 10/11 collision caused 10s SQLite stalls); satellite tests no longer rewrite data/satellites.py - register 'live' pytest marker, excluded by default - update stale test assertions to current APIs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
286 lines
9.2 KiB
Python
286 lines
9.2 KiB
Python
"""
|
|
HTTP client for communicating with remote Intercept agents.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger("intercept.agent_client")
|
|
|
|
|
|
class AgentHTTPError(RuntimeError):
|
|
"""Exception raised when agent HTTP request fails."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None):
|
|
super().__init__(message)
|
|
self.status_code = status_code
|
|
|
|
|
|
class AgentConnectionError(AgentHTTPError):
|
|
"""Exception raised when agent is unreachable."""
|
|
|
|
pass
|
|
|
|
|
|
class AgentClient:
|
|
"""HTTP client for communicating with a remote Intercept agent."""
|
|
|
|
def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 60.0):
|
|
"""
|
|
Initialize agent client.
|
|
|
|
Args:
|
|
base_url: Base URL of the agent (e.g., http://192.168.1.50:8020)
|
|
api_key: Optional API key for authentication
|
|
timeout: Request timeout in seconds
|
|
"""
|
|
self.base_url = base_url.rstrip("/")
|
|
self.api_key = api_key
|
|
self.timeout = timeout
|
|
|
|
def _headers(self) -> dict:
|
|
"""Get request headers."""
|
|
headers = {"Content-Type": "application/json"}
|
|
if self.api_key:
|
|
headers["X-API-Key"] = self.api_key
|
|
return headers
|
|
|
|
def _get(self, path: str, params: dict | None = None) -> dict:
|
|
"""
|
|
Perform GET request to agent.
|
|
|
|
Args:
|
|
path: URL path (e.g., /capabilities)
|
|
params: Optional query parameters
|
|
|
|
Returns:
|
|
Parsed JSON response
|
|
|
|
Raises:
|
|
AgentHTTPError: On HTTP errors
|
|
AgentConnectionError: If agent is unreachable
|
|
"""
|
|
url = f"{self.base_url}{path}"
|
|
try:
|
|
response = requests.get(url, headers=self._headers(), params=params, timeout=self.timeout)
|
|
response.raise_for_status()
|
|
return response.json() if response.content else {}
|
|
except requests.ConnectionError as e:
|
|
raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}")
|
|
except requests.Timeout:
|
|
raise AgentConnectionError(f"Request to agent timed out after {self.timeout}s")
|
|
except requests.HTTPError as e:
|
|
# Try to extract error message from response body
|
|
error_msg = f"Agent returned error: {e.response.status_code}"
|
|
try:
|
|
error_data = e.response.json()
|
|
if "message" in error_data:
|
|
error_msg = error_data["message"]
|
|
elif "error" in error_data:
|
|
error_msg = error_data["error"]
|
|
except Exception:
|
|
pass
|
|
raise AgentHTTPError(error_msg, status_code=e.response.status_code)
|
|
except requests.RequestException as e:
|
|
raise AgentHTTPError(f"Request failed: {e}")
|
|
|
|
def _post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
|
|
"""
|
|
Perform POST request to agent.
|
|
|
|
Args:
|
|
path: URL path (e.g., /sensor/start)
|
|
data: Optional JSON body
|
|
|
|
Returns:
|
|
Parsed JSON response
|
|
|
|
Raises:
|
|
AgentHTTPError: On HTTP errors
|
|
AgentConnectionError: If agent is unreachable
|
|
"""
|
|
url = f"{self.base_url}{path}"
|
|
request_timeout = self.timeout if timeout is None else timeout
|
|
try:
|
|
response = requests.post(url, json=data or {}, headers=self._headers(), timeout=request_timeout)
|
|
response.raise_for_status()
|
|
return response.json() if response.content else {}
|
|
except requests.ConnectionError as e:
|
|
raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}")
|
|
except requests.Timeout:
|
|
raise AgentConnectionError(f"Request to agent timed out after {request_timeout}s")
|
|
except requests.HTTPError as e:
|
|
# Try to extract error message from response body
|
|
error_msg = f"Agent returned error: {e.response.status_code}"
|
|
try:
|
|
error_data = e.response.json()
|
|
if "message" in error_data:
|
|
error_msg = error_data["message"]
|
|
elif "error" in error_data:
|
|
error_msg = error_data["error"]
|
|
except Exception:
|
|
pass
|
|
raise AgentHTTPError(error_msg, status_code=e.response.status_code)
|
|
except requests.RequestException as e:
|
|
raise AgentHTTPError(f"Request failed: {e}")
|
|
|
|
def get(self, path: str, params: dict | None = None) -> dict:
|
|
"""Public GET method for arbitrary endpoints."""
|
|
return self._get(path, params)
|
|
|
|
def post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
|
|
"""Public POST method for arbitrary endpoints."""
|
|
return self._post(path, data, timeout=timeout)
|
|
|
|
# =========================================================================
|
|
# Capability & Status
|
|
# =========================================================================
|
|
|
|
def get_capabilities(self) -> dict:
|
|
"""
|
|
Get agent capabilities (available modes, devices).
|
|
|
|
Returns:
|
|
Dict with 'modes' (mode -> bool), 'devices' (list), 'agent_version'
|
|
"""
|
|
return self._get("/capabilities")
|
|
|
|
def get_status(self) -> dict:
|
|
"""
|
|
Get agent status.
|
|
|
|
Returns:
|
|
Dict with 'running_modes', 'uptime', 'push_enabled', etc.
|
|
"""
|
|
return self._get("/status")
|
|
|
|
def health_check(self) -> bool:
|
|
"""
|
|
Check if agent is healthy.
|
|
|
|
Returns:
|
|
True if agent is reachable and healthy
|
|
"""
|
|
try:
|
|
result = self._get("/health")
|
|
return result.get("status") == "healthy"
|
|
except (AgentHTTPError, AgentConnectionError):
|
|
return False
|
|
|
|
def get_config(self) -> dict:
|
|
"""Get agent configuration (non-sensitive fields)."""
|
|
return self._get("/config")
|
|
|
|
def update_config(self, **kwargs) -> dict:
|
|
"""
|
|
Update agent configuration.
|
|
|
|
Args:
|
|
push_enabled: Enable/disable push mode
|
|
push_interval: Push interval in seconds
|
|
|
|
Returns:
|
|
Updated config
|
|
"""
|
|
return self._post("/config", kwargs)
|
|
|
|
# =========================================================================
|
|
# Mode Operations
|
|
# =========================================================================
|
|
|
|
def start_mode(self, mode: str, params: dict | None = None) -> dict:
|
|
"""
|
|
Start a mode on the agent.
|
|
|
|
Args:
|
|
mode: Mode name (e.g., 'sensor', 'adsb', 'wifi')
|
|
params: Mode-specific parameters
|
|
|
|
Returns:
|
|
Start result with 'status' field
|
|
"""
|
|
return self._post(f"/{mode}/start", params or {})
|
|
|
|
def stop_mode(self, mode: str, timeout: float = 8.0) -> dict:
|
|
"""
|
|
Stop a running mode on the agent.
|
|
|
|
Args:
|
|
mode: Mode name
|
|
|
|
Returns:
|
|
Stop result with 'status' field
|
|
"""
|
|
return self._post(f"/{mode}/stop", timeout=timeout)
|
|
|
|
def get_mode_status(self, mode: str) -> dict:
|
|
"""
|
|
Get status of a specific mode.
|
|
|
|
Args:
|
|
mode: Mode name
|
|
|
|
Returns:
|
|
Mode status with 'running' field
|
|
"""
|
|
return self._get(f"/{mode}/status")
|
|
|
|
def get_mode_data(self, mode: str) -> dict:
|
|
"""
|
|
Get current data snapshot for a mode.
|
|
|
|
Args:
|
|
mode: Mode name
|
|
|
|
Returns:
|
|
Data snapshot with 'data' field
|
|
"""
|
|
return self._get(f"/{mode}/data")
|
|
|
|
# =========================================================================
|
|
# Convenience Methods
|
|
# =========================================================================
|
|
|
|
def refresh_metadata(self) -> dict:
|
|
"""
|
|
Fetch comprehensive metadata from agent.
|
|
|
|
Returns:
|
|
Dict with capabilities, status, and config
|
|
"""
|
|
metadata = {
|
|
"capabilities": None,
|
|
"status": None,
|
|
"config": None,
|
|
"healthy": False,
|
|
}
|
|
|
|
try:
|
|
metadata["capabilities"] = self.get_capabilities()
|
|
metadata["status"] = self.get_status()
|
|
metadata["config"] = self.get_config()
|
|
metadata["healthy"] = True
|
|
except (AgentHTTPError, AgentConnectionError) as e:
|
|
logger.warning(f"Failed to refresh agent metadata: {e}")
|
|
|
|
return metadata
|
|
|
|
def __repr__(self) -> str:
|
|
return f"AgentClient({self.base_url})"
|
|
|
|
|
|
def create_client_from_agent(agent: dict) -> AgentClient:
|
|
"""
|
|
Create an AgentClient from an agent database record.
|
|
|
|
Args:
|
|
agent: Agent dict from database
|
|
|
|
Returns:
|
|
Configured AgentClient
|
|
"""
|
|
return AgentClient(base_url=agent["base_url"], api_key=agent.get("api_key"), timeout=60.0)
|