evolvingmachines-evolve 0.0.55.dev1355__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
evolve/_http.py ADDED
@@ -0,0 +1,71 @@
1
+ """Shared stdlib HTTP core for every urllib client in the SDK.
2
+
3
+ One opener, one policy, four clients (hosted, browser credentials, browser
4
+ profiles, managed secrets). REDIRECTS ARE DISABLED: urllib's default opener
5
+ replays a redirected request at whatever Location the server names — with the
6
+ original headers, Authorization included, across hosts — so a redirecting (or
7
+ compromised) endpoint could bounce the caller's bearer key to a host of its
8
+ choosing. Here a 3xx is surfaced as the HTTPError it is, never followed.
9
+
10
+ Zero-dependency on purpose: stdlib only, like everything it replaces.
11
+ """
12
+
13
+ import json
14
+ import urllib.error
15
+ import urllib.request
16
+ from typing import Any, Dict, Optional
17
+
18
+
19
+ class _RedirectRefusedHandler(urllib.request.HTTPRedirectHandler):
20
+ """Refuse every redirect: returning None makes the 3xx raise as HTTPError."""
21
+
22
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
23
+ return None
24
+
25
+
26
+ # build_opener drops the default HTTPRedirectHandler because a subclass of it
27
+ # is supplied — every other default handler (HTTPS, proxies) stays.
28
+ _OPENER = urllib.request.build_opener(_RedirectRefusedHandler())
29
+
30
+
31
+ def urlopen(request: urllib.request.Request, timeout: Optional[float] = None):
32
+ """Open ``request`` without ever following a redirect.
33
+
34
+ The single seam every SDK HTTP call goes through — tests patch
35
+ ``evolve._http.urlopen``, and a call that bypasses it bypasses the
36
+ redirect policy too.
37
+ """
38
+ return _OPENER.open(request, timeout=timeout)
39
+
40
+
41
+ def request_json(
42
+ url: str,
43
+ *,
44
+ api_key: str,
45
+ error_prefix: str,
46
+ method: str = 'GET',
47
+ body: Optional[Dict[str, Any]] = None,
48
+ timeout: float = 30,
49
+ ) -> Dict[str, Any]:
50
+ """One authenticated JSON request against the dashboard API (sync).
51
+
52
+ The request/parse/error shape the three standalone dashboard clients used
53
+ to carry as three private copies. An HTTP failure raises RuntimeError as
54
+ ``"{error_prefix} request failed (status): detail"``; an empty body is an
55
+ empty dict.
56
+ """
57
+ data = json.dumps(body).encode('utf-8') if body is not None else None
58
+ headers = {
59
+ 'Authorization': f'Bearer {api_key}',
60
+ 'Accept': 'application/json',
61
+ }
62
+ if data is not None:
63
+ headers['Content-Type'] = 'application/json'
64
+ request = urllib.request.Request(url, data=data, headers=headers, method=method)
65
+ try:
66
+ with urlopen(request, timeout=timeout) as response:
67
+ payload = response.read().decode('utf-8')
68
+ except urllib.error.HTTPError as exc:
69
+ detail = exc.read().decode('utf-8', errors='replace')
70
+ raise RuntimeError(f'{error_prefix} request failed ({exc.code}): {detail}') from exc
71
+ return json.loads(payload) if payload else {}