agent-cv/agent_cv/util.py

55 lines
1.6 KiB
Python

"""Tiny, dependency-free helpers (stdlib only)."""
from __future__ import annotations
import json
import urllib.request
import urllib.error
from datetime import datetime, timezone
class HttpError(Exception):
def __init__(self, status: int, url: str, body: str = ""):
super().__init__(f"HTTP {status} for {url}")
self.status, self.url, self.body = status, url, body
def default_get(url: str, headers: dict | None = None, timeout: int = 15):
"""Return (status, text). The single network primitive — injected in tests."""
req = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return getattr(r, "status", r.getcode()), r.read().decode("utf-8")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
def get_json(url: str, headers: dict | None = None, getter=default_get):
status, body = getter(url, headers)
if status != 200:
raise HttpError(status, url, body)
return json.loads(body)
def dig(data, path: str):
"""Read a dotted path out of nested dicts: dig(d, 'a.b') -> d['a']['b'] or None."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def utcnow() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def fmt(v) -> str:
if v is None:
return "" # em dash
try:
f = float(v)
return str(int(f)) if f.is_integer() else str(round(f, 2))
except (TypeError, ValueError):
return str(v)