104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import urllib.error
|
||
|
|
import urllib.parse
|
||
|
|
import urllib.request
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
|
||
|
|
TIMEOUT_SECONDS = 5
|
||
|
|
LIST_TIMEOUT_SECONDS = 20
|
||
|
|
|
||
|
|
# Observed status vocabulary (2026-07): "requested", "completed". No fixed
|
||
|
|
# enum is published by the API (free-form string field) -- treat anything
|
||
|
|
# not in this terminal set as still-open rather than assuming a closed list.
|
||
|
|
TERMINAL_STATUSES = {"completed", "cancelled", "rejected", "resolved", "closed"}
|
||
|
|
|
||
|
|
|
||
|
|
def _request(
|
||
|
|
method: str,
|
||
|
|
path: str,
|
||
|
|
base_url: str,
|
||
|
|
*,
|
||
|
|
body: dict[str, Any] | None = None,
|
||
|
|
params: dict[str, str] | None = None,
|
||
|
|
timeout: int = TIMEOUT_SECONDS,
|
||
|
|
) -> dict[str, Any] | list[Any]:
|
||
|
|
url = f"{base_url.rstrip('/')}{path}"
|
||
|
|
if params:
|
||
|
|
query = "&".join(
|
||
|
|
f"{k}={urllib.parse.quote(v)}" for k, v in params.items() if v
|
||
|
|
)
|
||
|
|
if query:
|
||
|
|
url = f"{url}?{query}"
|
||
|
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||
|
|
request = urllib.request.Request(
|
||
|
|
url,
|
||
|
|
data=data,
|
||
|
|
method=method,
|
||
|
|
headers={"Content-Type": "application/json", "User-Agent": "reuse-surface/0.1"},
|
||
|
|
)
|
||
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||
|
|
return json.loads(response.read().decode("utf-8"))
|
||
|
|
|
||
|
|
|
||
|
|
def state_hub_reachable(base_url: str = DEFAULT_BASE_URL) -> bool:
|
||
|
|
try:
|
||
|
|
_request("GET", "/state/health", base_url)
|
||
|
|
return True
|
||
|
|
except (urllib.error.URLError, TimeoutError, OSError):
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def file_capability_request(
|
||
|
|
*,
|
||
|
|
title: str,
|
||
|
|
description: str,
|
||
|
|
requesting_domain: str,
|
||
|
|
requesting_agent: str,
|
||
|
|
capability_type: str = "reuse-surface-new-verdict",
|
||
|
|
requesting_workplan_id: str | None = None,
|
||
|
|
base_url: str = DEFAULT_BASE_URL,
|
||
|
|
) -> dict[str, Any] | None:
|
||
|
|
"""Files a State Hub capability request for a plan-check 'new' verdict.
|
||
|
|
|
||
|
|
Returns the created request record, or None if the hub is unreachable
|
||
|
|
(degrades gracefully -- plan-check never blocks on this)."""
|
||
|
|
if not state_hub_reachable(base_url):
|
||
|
|
return None
|
||
|
|
body = {
|
||
|
|
"title": title,
|
||
|
|
"description": description,
|
||
|
|
"capability_type": capability_type,
|
||
|
|
"requesting_domain": requesting_domain,
|
||
|
|
"requesting_agent": requesting_agent,
|
||
|
|
"requesting_workplan_id": requesting_workplan_id,
|
||
|
|
}
|
||
|
|
try:
|
||
|
|
return _request("POST", "/capability-requests/", base_url, body=body)
|
||
|
|
except (urllib.error.URLError, TimeoutError, OSError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def list_open_capability_requests(
|
||
|
|
base_url: str = DEFAULT_BASE_URL,
|
||
|
|
) -> list[dict[str, Any]] | None:
|
||
|
|
"""Returns capability requests whose status is not terminal (see
|
||
|
|
TERMINAL_STATUSES), or None if the hub is unreachable. A request can
|
||
|
|
carry a catalog_entry_id while still 'requested' (routed, not
|
||
|
|
fulfilled), so status -- not catalog_entry_id presence -- is the open
|
||
|
|
signal. The list endpoint can be slow (observed ~7s with a handful of
|
||
|
|
rows); a longer timeout than the health check is used deliberately."""
|
||
|
|
if not state_hub_reachable(base_url):
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
requests_ = _request(
|
||
|
|
"GET", "/capability-requests/", base_url, timeout=LIST_TIMEOUT_SECONDS
|
||
|
|
)
|
||
|
|
except (urllib.error.URLError, TimeoutError, OSError):
|
||
|
|
return None
|
||
|
|
if not isinstance(requests_, list):
|
||
|
|
return []
|
||
|
|
return [r for r in requests_ if r.get("status") not in TERMINAL_STATUSES]
|