# retoor <retoor@molodetz.nl>
from __future__ import annotations
import logging
import xmlrpc.client
from typing import Any
from devplacepy import stealth
from devplacepy.config import INTERNAL_BASE_URL
from .registry import Method
logger = logging.getLogger("xmlrpc.bridge")
FAULT_INVALID_PARAMS = -32602
FAULT_MISSING_PARAM = -32001
FAULT_TRANSPORT = -32300
WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
TIMEOUT_SECONDS = 60.0
def _coerce_value(value: Any) -> str:
if isinstance(value, bool):
return "1" if value else "0"
return str(value)
def _build_path(method: Method, params: dict[str, Any]) -> str:
path = method.path
for param in method.path_params:
if param.name not in params:
raise xmlrpc.client.Fault(
FAULT_MISSING_PARAM,
f"Missing required path parameter '{param.name}' for {method.name}.",
)
path = path.replace("{" + param.name + "}", _coerce_value(params[param.name]))
return path
def _split_params(
method: Method, params: dict[str, Any]
) -> tuple[dict[str, str], dict[str, str]]:
declared = {p.name: p for p in method.params}
path_names = {p.name for p in method.path_params}
query: dict[str, str] = {}
form: dict[str, str] = {}
for key, value in params.items():
if key in path_names:
continue
spec = declared.get(key)
location = spec.location if spec else None
if location == "query":
query[key] = _coerce_value(value)
elif location == "form":
form[key] = _coerce_value(value)
elif method.http_method in WRITE_METHODS:
form[key] = _coerce_value(value)
else:
query[key] = _coerce_value(value)
return query, form
def _check_required(method: Method, params: dict[str, Any]) -> None:
for spec in method.params:
if spec.required and spec.name not in params:
raise xmlrpc.client.Fault(
FAULT_MISSING_PARAM,
f"Missing required parameter '{spec.name}' for {method.name}.",
)
def _decode(response: Any) -> Any:
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
try:
return response.json()
except ValueError:
return response.text
return response.text
def call(method: Method, params: dict[str, Any], auth: dict[str, str]) -> Any:
if not isinstance(params, dict):
raise xmlrpc.client.Fault(
FAULT_INVALID_PARAMS,
f"{method.name} expects a single struct of named parameters.",
)
params = dict(params)
api_key = str(params.pop("api_key", "") or auth.get("api_key", ""))
_check_required(method, params)
path = _build_path(method, params)
query, form = _split_params(method, params)
headers = {
"Accept": "application/json",
"X-Requested-With": "fetch",
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
headers["X-API-KEY"] = api_key
elif auth.get("authorization"):
headers["Authorization"] = auth["authorization"]
if auth.get("real_ip"):
headers["X-Real-IP"] = auth["real_ip"]
if auth.get("forwarded_for"):
headers["X-Forwarded-For"] = auth["forwarded_for"]
logger.debug("Bridge %s -> %s %s", method.name, method.http_method, path)
try:
with stealth.stealth_sync_client(
base_url=INTERNAL_BASE_URL,
timeout=TIMEOUT_SECONDS,
follow_redirects=True,
headers=headers,
) as client:
response = client.request(
method.http_method,
path,
params=query or None,
data=form or None,
)
except Exception as exc:
logger.warning("Bridge transport error for %s: %s", method.name, exc)
raise xmlrpc.client.Fault(
FAULT_TRANSPORT, f"Transport error calling {method.name}: {exc}"
)
body = _decode(response)
if response.status_code >= 400:
message = body
if isinstance(body, dict):
error = body.get("error")
if isinstance(error, dict):
message = error.get("message", body)
elif error:
message = error
else:
message = body.get("detail", body)
raise xmlrpc.client.Fault(response.status_code, _stringify(message))
return body
def _stringify(message: Any) -> str:
if isinstance(message, str):
return message
return str(message)