Spaces:
Running
Running
chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)
Browse filesAutomated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): a11oy_live_feeds.py, serve.py
Deleted (gone from the repo + Dockerfile COPY set): (none)
Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.
- a11oy_live_feeds.py +128 -40
- serve.py +46 -22
a11oy_live_feeds.py
CHANGED
|
@@ -7,12 +7,13 @@ same-origin proxy, keeping the Space sovereign — 0 runtime CDN from the client
|
|
| 7 |
|
| 8 |
Every response carries an HONEST label:
|
| 9 |
{"source": <human source>, "source_url": <upstream URL>,
|
| 10 |
-
"mode": "live" | "cached" | "self", # never fabricated
|
| 11 |
"fetched_at": <iso8601>, "ttl_s": <int>, ...payload}
|
| 12 |
|
| 13 |
- "live" = freshly fetched from upstream this request (or within TTL).
|
| 14 |
- "cached" = upstream was unreachable; serving the last good in-memory value
|
| 15 |
or the bundled on-disk snapshot (stage resilience).
|
|
|
|
| 16 |
- "self" = our own internal real data (not third-party) — used by callers
|
| 17 |
that pass through this layer's helpers; the feed endpoints here
|
| 18 |
are all third-party live/cached.
|
|
@@ -27,22 +28,22 @@ Feeds + TTLs:
|
|
| 27 |
fhir (hapi.fhir.org/baseR4 Observation/Immunization) TTL 10m
|
| 28 |
|
| 29 |
No auth required for any of these feeds. NEVER fabricates: a down feed returns
|
| 30 |
-
|
| 31 |
"""
|
| 32 |
import json
|
| 33 |
import os
|
| 34 |
import time
|
| 35 |
import threading
|
| 36 |
-
import urllib.request
|
| 37 |
-
import urllib.error
|
| 38 |
from datetime import datetime, timezone
|
| 39 |
from pathlib import Path
|
| 40 |
|
|
|
|
| 41 |
from starlette.routing import Route
|
| 42 |
from starlette.responses import JSONResponse
|
| 43 |
|
| 44 |
_SNAP_DIR = Path(os.environ.get("A11OY_LIVE_SNAPSHOTS", "/app/live_snapshots"))
|
| 45 |
_UA = "a11oy-live-proxy/1.0 (+https://szlholdings-a11oy.hf.space)"
|
|
|
|
| 46 |
|
| 47 |
# in-memory cache: feed -> {"data":..., "ts":..., "mode":...}
|
| 48 |
_CACHE = {}
|
|
@@ -75,13 +76,59 @@ def _now_iso():
|
|
| 75 |
return datetime.now(timezone.utc).isoformat()
|
| 76 |
|
| 77 |
|
| 78 |
-
def _http_get(url, timeout=20, headers=None, data=None, method=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
h = {"User-Agent": _UA, "Accept": "application/json"}
|
| 80 |
if headers:
|
| 81 |
h.update(headers)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
|
| 87 |
def _load_snapshot(feed):
|
|
@@ -92,7 +139,43 @@ def _load_snapshot(feed):
|
|
| 92 |
return None
|
| 93 |
|
| 94 |
|
| 95 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
"""Return raw upstream JSON for a feed (raises on failure)."""
|
| 97 |
if feed == "prometheus":
|
| 98 |
import urllib.parse
|
|
@@ -102,17 +185,25 @@ def _fetch(feed):
|
|
| 102 |
("cpu", 'rate(node_cpu_seconds_total{mode="user"}[5m])'),
|
| 103 |
("mem", "node_memory_MemAvailable_bytes"),
|
| 104 |
("http_req", "rate(prometheus_http_requests_total[5m])")):
|
| 105 |
-
out[k] = _http_get(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
return out
|
| 107 |
if feed == "kev":
|
| 108 |
-
return _http_get(
|
|
|
|
|
|
|
| 109 |
if feed == "osv":
|
| 110 |
out = {}
|
| 111 |
for pkg, eco in (("tensorflow", "PyPI"), ("torch", "PyPI"),
|
| 112 |
("transformers", "PyPI"), ("numpy", "PyPI"), ("requests", "PyPI")):
|
| 113 |
body = json.dumps({"package": {"name": pkg, "ecosystem": eco}}).encode()
|
| 114 |
-
r = _http_get("https://api.osv.dev/v1/query",
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
vulns = r.get("vulns", [])
|
| 117 |
out[pkg] = {"ecosystem": eco, "count": len(vulns),
|
| 118 |
"vulns": [{"id": v.get("id"), "summary": v.get("summary"),
|
|
@@ -120,16 +211,24 @@ def _fetch(feed):
|
|
| 120 |
"aliases": (v.get("aliases") or [])[:4]} for v in vulns[:25]]}
|
| 121 |
return out
|
| 122 |
if feed == "rekor":
|
| 123 |
-
return {"log": _http_get(
|
|
|
|
|
|
|
| 124 |
if feed == "celestrak":
|
| 125 |
-
return _http_get(
|
|
|
|
|
|
|
| 126 |
if feed == "iss":
|
| 127 |
-
return _http_get(
|
|
|
|
|
|
|
| 128 |
if feed == "fhir":
|
| 129 |
out = {}
|
| 130 |
for rt in ("Immunization", "Observation"):
|
| 131 |
-
b = _http_get("https://hapi.fhir.org/baseR4/%s?_count=10" % rt,
|
| 132 |
-
|
|
|
|
|
|
|
| 133 |
entries = b.get("entry", [])
|
| 134 |
out[rt] = {"total": b.get("total"), "count": len(entries),
|
| 135 |
"entries": [e.get("resource", {}) for e in entries[:10]]}
|
|
@@ -137,8 +236,8 @@ def _fetch(feed):
|
|
| 137 |
raise ValueError("unknown feed: %s" % feed)
|
| 138 |
|
| 139 |
|
| 140 |
-
def get_feed(feed):
|
| 141 |
-
"""Cached,
|
| 142 |
ttl = _TTL.get(feed, 60)
|
| 143 |
src, url = _SOURCE.get(feed, ("unknown", ""))
|
| 144 |
with _LOCK:
|
|
@@ -149,30 +248,18 @@ def get_feed(feed):
|
|
| 149 |
"fetched_at": ent["iso"], "ttl_s": ttl, "data": ent["data"]}
|
| 150 |
# need refresh
|
| 151 |
try:
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
iso = _now_iso()
|
| 154 |
with _LOCK:
|
| 155 |
_CACHE[feed] = {"data": data, "ts": now, "mode": "live", "iso": iso}
|
| 156 |
return {"source": src, "source_url": url, "mode": "live",
|
| 157 |
"fetched_at": iso, "ttl_s": ttl, "data": data}
|
| 158 |
except Exception as e:
|
| 159 |
-
|
| 160 |
-
if ent:
|
| 161 |
-
return {"source": src, "source_url": url, "mode": "cached",
|
| 162 |
-
"fetched_at": ent["iso"], "ttl_s": ttl,
|
| 163 |
-
"cache_note": "upstream unreachable (%s) — serving last good value" % type(e).__name__,
|
| 164 |
-
"data": ent["data"]}
|
| 165 |
-
# else bundled on-disk snapshot
|
| 166 |
-
snap = _load_snapshot(feed)
|
| 167 |
-
if snap is not None:
|
| 168 |
-
return {"source": src, "source_url": url, "mode": "cached",
|
| 169 |
-
"fetched_at": "bundled-snapshot",
|
| 170 |
-
"ttl_s": ttl,
|
| 171 |
-
"cache_note": "upstream unreachable (%s) — serving bundled in-image snapshot" % type(e).__name__,
|
| 172 |
-
"data": snap}
|
| 173 |
-
return {"source": src, "source_url": url, "mode": "cached",
|
| 174 |
-
"fetched_at": None, "ttl_s": ttl,
|
| 175 |
-
"error": "upstream unreachable and no snapshot: %s" % e, "data": None}
|
| 176 |
|
| 177 |
|
| 178 |
def register(app, ns="a11oy"):
|
|
@@ -201,8 +288,9 @@ def register(app, ns="a11oy"):
|
|
| 201 |
return JSONResponse({
|
| 202 |
"layer": "a11oy live-data proxy",
|
| 203 |
"honest": "Every feed is server-side fetched + cached, CORS-safe via OUR same-origin "
|
| 204 |
-
"proxy (0 client CDN). Mode is honestly labelled live/cached;
|
| 205 |
-
"serves
|
|
|
|
| 206 |
"count": len(feeds), "feeds": feeds,
|
| 207 |
})
|
| 208 |
|
|
|
|
| 7 |
|
| 8 |
Every response carries an HONEST label:
|
| 9 |
{"source": <human source>, "source_url": <upstream URL>,
|
| 10 |
+
"mode": "live" | "cached" | "unavailable" | "self", # never fabricated
|
| 11 |
"fetched_at": <iso8601>, "ttl_s": <int>, ...payload}
|
| 12 |
|
| 13 |
- "live" = freshly fetched from upstream this request (or within TTL).
|
| 14 |
- "cached" = upstream was unreachable; serving the last good in-memory value
|
| 15 |
or the bundled on-disk snapshot (stage resilience).
|
| 16 |
+
- "unavailable" = upstream was unreachable and no cached or bundled data exists.
|
| 17 |
- "self" = our own internal real data (not third-party) — used by callers
|
| 18 |
that pass through this layer's helpers; the feed endpoints here
|
| 19 |
are all third-party live/cached.
|
|
|
|
| 28 |
fhir (hapi.fhir.org/baseR4 Observation/Immunization) TTL 10m
|
| 29 |
|
| 30 |
No auth required for any of these feeds. NEVER fabricates: a down feed returns
|
| 31 |
+
real cached data labelled "cached", or "unavailable" when no cached data exists.
|
| 32 |
"""
|
| 33 |
import json
|
| 34 |
import os
|
| 35 |
import time
|
| 36 |
import threading
|
|
|
|
|
|
|
| 37 |
from datetime import datetime, timezone
|
| 38 |
from pathlib import Path
|
| 39 |
|
| 40 |
+
import httpx
|
| 41 |
from starlette.routing import Route
|
| 42 |
from starlette.responses import JSONResponse
|
| 43 |
|
| 44 |
_SNAP_DIR = Path(os.environ.get("A11OY_LIVE_SNAPSHOTS", "/app/live_snapshots"))
|
| 45 |
_UA = "a11oy-live-proxy/1.0 (+https://szlholdings-a11oy.hf.space)"
|
| 46 |
+
_MAX_RESPONSE_BYTES = 32 * 1024 * 1024
|
| 47 |
|
| 48 |
# in-memory cache: feed -> {"data":..., "ts":..., "mode":...}
|
| 49 |
_CACHE = {}
|
|
|
|
| 76 |
return datetime.now(timezone.utc).isoformat()
|
| 77 |
|
| 78 |
|
| 79 |
+
def _http_get(url, timeout=20, headers=None, data=None, method=None, deadline=None):
|
| 80 |
+
"""Fetch one bounded JSON response.
|
| 81 |
+
|
| 82 |
+
Socket timeouts alone are insufficient because a peer can trickle bytes and
|
| 83 |
+
reset the per-read timer forever. ``iter_bytes`` exposes each received
|
| 84 |
+
network chunk so the absolute monotonic deadline is checked throughout the
|
| 85 |
+
body read. The response context is closed before a deadline error escapes,
|
| 86 |
+
which lets the calling worker finish instead of being abandoned.
|
| 87 |
+
"""
|
| 88 |
h = {"User-Agent": _UA, "Accept": "application/json"}
|
| 89 |
if headers:
|
| 90 |
h.update(headers)
|
| 91 |
+
request_timeout = _remaining_timeout(deadline, timeout)
|
| 92 |
+
request_deadline = (
|
| 93 |
+
deadline
|
| 94 |
+
if deadline is not None
|
| 95 |
+
else time.monotonic() + request_timeout
|
| 96 |
+
)
|
| 97 |
+
request_method = method or ("POST" if data is not None else "GET")
|
| 98 |
+
chunks = []
|
| 99 |
+
received = 0
|
| 100 |
+
try:
|
| 101 |
+
with httpx.stream(
|
| 102 |
+
request_method,
|
| 103 |
+
url,
|
| 104 |
+
content=data,
|
| 105 |
+
headers=h,
|
| 106 |
+
timeout=httpx.Timeout(request_timeout),
|
| 107 |
+
follow_redirects=True,
|
| 108 |
+
) as response:
|
| 109 |
+
response.raise_for_status()
|
| 110 |
+
content_length = response.headers.get("content-length")
|
| 111 |
+
if content_length is not None:
|
| 112 |
+
try:
|
| 113 |
+
declared_bytes = int(content_length)
|
| 114 |
+
except (TypeError, ValueError):
|
| 115 |
+
declared_bytes = None
|
| 116 |
+
if declared_bytes is not None and declared_bytes > _MAX_RESPONSE_BYTES:
|
| 117 |
+
raise ValueError("live-feed response exceeds size limit")
|
| 118 |
+
for chunk in response.iter_bytes():
|
| 119 |
+
if time.monotonic() >= request_deadline:
|
| 120 |
+
raise TimeoutError("live-feed response deadline exhausted")
|
| 121 |
+
if not chunk:
|
| 122 |
+
continue
|
| 123 |
+
received += len(chunk)
|
| 124 |
+
if received > _MAX_RESPONSE_BYTES:
|
| 125 |
+
raise ValueError("live-feed response exceeds size limit")
|
| 126 |
+
chunks.append(chunk)
|
| 127 |
+
if time.monotonic() >= request_deadline:
|
| 128 |
+
raise TimeoutError("live-feed response deadline exhausted")
|
| 129 |
+
except httpx.TimeoutException as exc:
|
| 130 |
+
raise TimeoutError("live-feed response deadline exhausted") from exc
|
| 131 |
+
return json.loads(b"".join(chunks))
|
| 132 |
|
| 133 |
|
| 134 |
def _load_snapshot(feed):
|
|
|
|
| 139 |
return None
|
| 140 |
|
| 141 |
|
| 142 |
+
def get_cached_feed(feed, error):
|
| 143 |
+
"""Return only real cached evidence, without attempting an upstream read."""
|
| 144 |
+
ttl = _TTL.get(feed, 60)
|
| 145 |
+
src, url = _SOURCE.get(feed, ("unknown", ""))
|
| 146 |
+
with _LOCK:
|
| 147 |
+
ent = _CACHE.get(feed)
|
| 148 |
+
if ent:
|
| 149 |
+
return {"source": src, "source_url": url, "mode": "cached",
|
| 150 |
+
"fetched_at": ent["iso"], "ttl_s": ttl,
|
| 151 |
+
"cache_note": "upstream unreachable (%s) — serving last good value"
|
| 152 |
+
% type(error).__name__,
|
| 153 |
+
"data": ent["data"]}
|
| 154 |
+
snap = _load_snapshot(feed)
|
| 155 |
+
if snap is not None:
|
| 156 |
+
return {"source": src, "source_url": url, "mode": "cached",
|
| 157 |
+
"fetched_at": "bundled-snapshot", "ttl_s": ttl,
|
| 158 |
+
"cache_note": "upstream unreachable (%s) — serving bundled in-image snapshot"
|
| 159 |
+
% type(error).__name__,
|
| 160 |
+
"data": snap}
|
| 161 |
+
return {"source": src, "source_url": url, "mode": "unavailable",
|
| 162 |
+
"fetched_at": None, "ttl_s": ttl,
|
| 163 |
+
"error": "upstream unreachable and no snapshot (%s): %s"
|
| 164 |
+
% (type(error).__name__, error),
|
| 165 |
+
"data": None}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _remaining_timeout(deadline, default):
|
| 169 |
+
"""Return a cooperative per-request socket timeout within ``deadline``."""
|
| 170 |
+
if deadline is None:
|
| 171 |
+
return float(default)
|
| 172 |
+
remaining = deadline - time.monotonic()
|
| 173 |
+
if remaining <= 0:
|
| 174 |
+
raise TimeoutError("live-feed network budget exhausted")
|
| 175 |
+
return min(float(default), remaining)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _fetch(feed, deadline=None):
|
| 179 |
"""Return raw upstream JSON for a feed (raises on failure)."""
|
| 180 |
if feed == "prometheus":
|
| 181 |
import urllib.parse
|
|
|
|
| 185 |
("cpu", 'rate(node_cpu_seconds_total{mode="user"}[5m])'),
|
| 186 |
("mem", "node_memory_MemAvailable_bytes"),
|
| 187 |
("http_req", "rate(prometheus_http_requests_total[5m])")):
|
| 188 |
+
out[k] = _http_get(
|
| 189 |
+
base + urllib.parse.quote(q),
|
| 190 |
+
timeout=_remaining_timeout(deadline, 12),
|
| 191 |
+
deadline=deadline,
|
| 192 |
+
)
|
| 193 |
return out
|
| 194 |
if feed == "kev":
|
| 195 |
+
return _http_get(
|
| 196 |
+
_SOURCE["kev"][1], timeout=_remaining_timeout(deadline, 40),
|
| 197 |
+
deadline=deadline)
|
| 198 |
if feed == "osv":
|
| 199 |
out = {}
|
| 200 |
for pkg, eco in (("tensorflow", "PyPI"), ("torch", "PyPI"),
|
| 201 |
("transformers", "PyPI"), ("numpy", "PyPI"), ("requests", "PyPI")):
|
| 202 |
body = json.dumps({"package": {"name": pkg, "ecosystem": eco}}).encode()
|
| 203 |
+
r = _http_get("https://api.osv.dev/v1/query",
|
| 204 |
+
timeout=_remaining_timeout(deadline, 20), data=body,
|
| 205 |
+
headers={"Content-Type": "application/json"}, method="POST",
|
| 206 |
+
deadline=deadline)
|
| 207 |
vulns = r.get("vulns", [])
|
| 208 |
out[pkg] = {"ecosystem": eco, "count": len(vulns),
|
| 209 |
"vulns": [{"id": v.get("id"), "summary": v.get("summary"),
|
|
|
|
| 211 |
"aliases": (v.get("aliases") or [])[:4]} for v in vulns[:25]]}
|
| 212 |
return out
|
| 213 |
if feed == "rekor":
|
| 214 |
+
return {"log": _http_get(
|
| 215 |
+
_SOURCE["rekor"][1], timeout=_remaining_timeout(deadline, 15),
|
| 216 |
+
deadline=deadline)}
|
| 217 |
if feed == "celestrak":
|
| 218 |
+
return _http_get(
|
| 219 |
+
_SOURCE["celestrak"][1], timeout=_remaining_timeout(deadline, 20),
|
| 220 |
+
deadline=deadline)
|
| 221 |
if feed == "iss":
|
| 222 |
+
return _http_get(
|
| 223 |
+
_SOURCE["iss"][1], timeout=_remaining_timeout(deadline, 12),
|
| 224 |
+
deadline=deadline)
|
| 225 |
if feed == "fhir":
|
| 226 |
out = {}
|
| 227 |
for rt in ("Immunization", "Observation"):
|
| 228 |
+
b = _http_get("https://hapi.fhir.org/baseR4/%s?_count=10" % rt,
|
| 229 |
+
timeout=_remaining_timeout(deadline, 25),
|
| 230 |
+
headers={"Accept": "application/fhir+json"},
|
| 231 |
+
deadline=deadline)
|
| 232 |
entries = b.get("entry", [])
|
| 233 |
out[rt] = {"total": b.get("total"), "count": len(entries),
|
| 234 |
"entries": [e.get("resource", {}) for e in entries[:10]]}
|
|
|
|
| 236 |
raise ValueError("unknown feed: %s" % feed)
|
| 237 |
|
| 238 |
|
| 239 |
+
def get_feed(feed, timeout_s=None):
|
| 240 |
+
"""Cached, honestly-labelled accessor with an optional cooperative network budget."""
|
| 241 |
ttl = _TTL.get(feed, 60)
|
| 242 |
src, url = _SOURCE.get(feed, ("unknown", ""))
|
| 243 |
with _LOCK:
|
|
|
|
| 248 |
"fetched_at": ent["iso"], "ttl_s": ttl, "data": ent["data"]}
|
| 249 |
# need refresh
|
| 250 |
try:
|
| 251 |
+
deadline = (
|
| 252 |
+
time.monotonic() + max(0.001, float(timeout_s))
|
| 253 |
+
if timeout_s is not None else None
|
| 254 |
+
)
|
| 255 |
+
data = _fetch(feed, deadline=deadline)
|
| 256 |
iso = _now_iso()
|
| 257 |
with _LOCK:
|
| 258 |
_CACHE[feed] = {"data": data, "ts": now, "mode": "live", "iso": iso}
|
| 259 |
return {"source": src, "source_url": url, "mode": "live",
|
| 260 |
"fetched_at": iso, "ttl_s": ttl, "data": data}
|
| 261 |
except Exception as e:
|
| 262 |
+
return get_cached_feed(feed, e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
|
| 265 |
def register(app, ns="a11oy"):
|
|
|
|
| 288 |
return JSONResponse({
|
| 289 |
"layer": "a11oy live-data proxy",
|
| 290 |
"honest": "Every feed is server-side fetched + cached, CORS-safe via OUR same-origin "
|
| 291 |
+
"proxy (0 client CDN). Mode is honestly labelled live/cached/unavailable; "
|
| 292 |
+
"a down feed serves real cached data when present and reports unavailable "
|
| 293 |
+
"when absent, never fabricated.",
|
| 294 |
"count": len(feeds), "feeds": feeds,
|
| 295 |
})
|
| 296 |
|
serve.py
CHANGED
|
@@ -10708,6 +10708,13 @@ try:
|
|
| 10708 |
# real mode (live/cached), age, and round-trip latency. This is the data-
|
| 10709 |
# provenance heartbeat for the governed-AI mission: a governed system must
|
| 10710 |
# know whether its evidence feeds are actually live. 0 fabricated status.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10711 |
@app.get("/api/a11oy/v1/feeds/pulse")
|
| 10712 |
async def _feeds_pulse():
|
| 10713 |
import anyio, time as _t
|
|
@@ -10715,27 +10722,42 @@ try:
|
|
| 10715 |
async def _one(f):
|
| 10716 |
t0 = _t.time()
|
| 10717 |
try:
|
| 10718 |
-
|
| 10719 |
-
|
| 10720 |
-
|
| 10721 |
-
|
| 10722 |
-
|
| 10723 |
-
|
| 10724 |
-
|
| 10725 |
-
|
| 10726 |
-
|
| 10727 |
-
|
| 10728 |
-
|
| 10729 |
-
|
| 10730 |
-
"payload_bytes": size,
|
| 10731 |
-
"error": p.get("error")}
|
| 10732 |
except Exception as e:
|
| 10733 |
-
|
| 10734 |
-
|
| 10735 |
-
|
| 10736 |
-
|
| 10737 |
-
|
| 10738 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10739 |
live = sum(1 for i in items if i.get("mode")=="live")
|
| 10740 |
return JSONResponse({
|
| 10741 |
"probed_at": _kl_live._now_iso(),
|
|
@@ -10744,8 +10766,10 @@ try:
|
|
| 10744 |
"cached_count": sum(1 for i in items if i.get("mode")=="cached"),
|
| 10745 |
"down_count": sum(1 for i in items if i.get("mode")=="unavailable"),
|
| 10746 |
"note": ("Real-time provenance heartbeat: each row is a live server-side "
|
| 10747 |
-
"probe of an upstream evidence feed. mode/latency are measured, "
|
| 10748 |
-
"
|
|
|
|
|
|
|
| 10749 |
"items": items,
|
| 10750 |
})
|
| 10751 |
|
|
|
|
| 10708 |
# real mode (live/cached), age, and round-trip latency. This is the data-
|
| 10709 |
# provenance heartbeat for the governed-AI mission: a governed system must
|
| 10710 |
# know whether its evidence feeds are actually live. 0 fabricated status.
|
| 10711 |
+
try:
|
| 10712 |
+
_KL_FEED_PULSE_TIMEOUT_S = max(
|
| 10713 |
+
0.25, min(10.0, float(os.environ.get("A11OY_FEED_PULSE_TIMEOUT_SEC", "4.0")))
|
| 10714 |
+
)
|
| 10715 |
+
except (TypeError, ValueError):
|
| 10716 |
+
_KL_FEED_PULSE_TIMEOUT_S = 4.0
|
| 10717 |
+
|
| 10718 |
@app.get("/api/a11oy/v1/feeds/pulse")
|
| 10719 |
async def _feeds_pulse():
|
| 10720 |
import anyio, time as _t
|
|
|
|
| 10722 |
async def _one(f):
|
| 10723 |
t0 = _t.time()
|
| 10724 |
try:
|
| 10725 |
+
# get_feed enforces the absolute deadline inside every streamed
|
| 10726 |
+
# network read. Keep the worker joined: it must exit and return
|
| 10727 |
+
# its honest cache fallback instead of being orphaned.
|
| 10728 |
+
p = await anyio.to_thread.run_sync(
|
| 10729 |
+
_kl_live.get_feed,
|
| 10730 |
+
f,
|
| 10731 |
+
_KL_FEED_PULSE_TIMEOUT_S,
|
| 10732 |
+
)
|
| 10733 |
+
except TimeoutError as e:
|
| 10734 |
+
p = _kl_live.get_cached_feed(f, e)
|
| 10735 |
+
if p.get("mode") == "unavailable":
|
| 10736 |
+
p["error"] = "probe timeout after %.2fs" % _KL_FEED_PULSE_TIMEOUT_S
|
|
|
|
|
|
|
| 10737 |
except Exception as e:
|
| 10738 |
+
p = _kl_live.get_cached_feed(f, e)
|
| 10739 |
+
if p.get("mode") == "unavailable":
|
| 10740 |
+
p["error"] = "%s: %s" % (type(e).__name__, e)
|
| 10741 |
+
dt = round((_t.time()-t0)*1000)
|
| 10742 |
+
d = p.get("data")
|
| 10743 |
+
# honest payload-size signal
|
| 10744 |
+
try:
|
| 10745 |
+
import json as _j; size = len(_j.dumps(d)) if d is not None else 0
|
| 10746 |
+
except Exception:
|
| 10747 |
+
size = 0
|
| 10748 |
+
return {"feed": f, "source": p.get("source"),
|
| 10749 |
+
"source_url": p.get("source_url"),
|
| 10750 |
+
"mode": p.get("mode"), "fetched_at": p.get("fetched_at"),
|
| 10751 |
+
"ttl_s": p.get("ttl_s"), "latency_ms": dt,
|
| 10752 |
+
"payload_bytes": size,
|
| 10753 |
+
"error": p.get("error"),
|
| 10754 |
+
"cache_note": p.get("cache_note")}
|
| 10755 |
+
items = [None] * len(feeds)
|
| 10756 |
+
async def _collect(index, feed):
|
| 10757 |
+
items[index] = await _one(feed)
|
| 10758 |
+
async with anyio.create_task_group() as task_group:
|
| 10759 |
+
for index, feed in enumerate(feeds):
|
| 10760 |
+
task_group.start_soon(_collect, index, feed)
|
| 10761 |
live = sum(1 for i in items if i.get("mode")=="live")
|
| 10762 |
return JSONResponse({
|
| 10763 |
"probed_at": _kl_live._now_iso(),
|
|
|
|
| 10766 |
"cached_count": sum(1 for i in items if i.get("mode")=="cached"),
|
| 10767 |
"down_count": sum(1 for i in items if i.get("mode")=="unavailable"),
|
| 10768 |
"note": ("Real-time provenance heartbeat: each row is a live server-side "
|
| 10769 |
+
"bounded probe of an upstream evidence feed. mode/latency are measured, "
|
| 10770 |
+
"internally handled timeouts remain visible through cache_note when real "
|
| 10771 |
+
"cached data exists and otherwise count as unavailable. A governed-AI "
|
| 10772 |
+
"system must know its feeds are live."),
|
| 10773 |
"items": items,
|
| 10774 |
})
|
| 10775 |
|