muad-skill-http 0.1.0__tar.gz
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.
- muad_skill_http-0.1.0/PKG-INFO +88 -0
- muad_skill_http-0.1.0/README.md +66 -0
- muad_skill_http-0.1.0/muad_skill_http/__init__.py +47 -0
- muad_skill_http-0.1.0/muad_skill_http/_http.py +109 -0
- muad_skill_http-0.1.0/muad_skill_http/_normalize.py +49 -0
- muad_skill_http-0.1.0/muad_skill_http/_platform.py +404 -0
- muad_skill_http-0.1.0/muad_skill_http/_trace.py +216 -0
- muad_skill_http-0.1.0/muad_skill_http.egg-info/PKG-INFO +88 -0
- muad_skill_http-0.1.0/muad_skill_http.egg-info/SOURCES.txt +13 -0
- muad_skill_http-0.1.0/muad_skill_http.egg-info/dependency_links.txt +1 -0
- muad_skill_http-0.1.0/muad_skill_http.egg-info/requires.txt +1 -0
- muad_skill_http-0.1.0/muad_skill_http.egg-info/top_level.txt +1 -0
- muad_skill_http-0.1.0/pyproject.toml +35 -0
- muad_skill_http-0.1.0/setup.cfg +4 -0
- muad_skill_http-0.1.0/test/test_http.py +679 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: muad-skill-http
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trace-aware HTTP helpers and platform request client for muad skills
|
|
5
|
+
Author: muad-openclaw maintainers
|
|
6
|
+
Project-URL: Homepage, https://github.com/Michaelxwb/muad-openclaw
|
|
7
|
+
Project-URL: Source, https://github.com/Michaelxwb/muad-openclaw/tree/main/tools/muad-skill-sdk
|
|
8
|
+
Keywords: http,requests,tracing,skills
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests<3,>=2.31
|
|
22
|
+
|
|
23
|
+
# muad-skill-http
|
|
24
|
+
|
|
25
|
+
Trace-aware HTTP helpers and a platform request client for muad skills.
|
|
26
|
+
|
|
27
|
+
`platform_request()` sends business-platform requests through a platform session
|
|
28
|
+
managed by `session-manager`. It assembles the browser defaults, platform host
|
|
29
|
+
header, platform static headers, cookies, CSRF header and a fresh `Traceid`
|
|
30
|
+
UUID v4 for every request, and re-asserts the mandatory headers last so caller
|
|
31
|
+
headers cannot override them. Cookie and CSRF values never reach the trace
|
|
32
|
+
spool.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install muad-skill-http
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from muad_skill_http import get, platform_request
|
|
44
|
+
|
|
45
|
+
# Plain HTTP with trace accounting.
|
|
46
|
+
response = get("https://example.com/api/v1/items")
|
|
47
|
+
|
|
48
|
+
# Business-platform request: headers and Traceid are built by the SDK.
|
|
49
|
+
response = platform_request("demo", "GET", "/api/v1/targets")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`get` / `post` / `put` / `patch` / `delete` / `head` / `options` / `request`
|
|
53
|
+
wrap `requests` and record request counts for the current skill execution.
|
|
54
|
+
|
|
55
|
+
### Runtime requirements for `platform_request`
|
|
56
|
+
|
|
57
|
+
`platform_request` resolves the platform session from the skill runtime, not
|
|
58
|
+
from the local machine. It needs:
|
|
59
|
+
|
|
60
|
+
- `MUAD_SKILL_NAME` set to the running skill name;
|
|
61
|
+
- a `session-manager` executable on `PATH` that answers
|
|
62
|
+
`session-manager get-state --skill-name <name>` with
|
|
63
|
+
`{"sessionStateFile": "..."}`;
|
|
64
|
+
- a session state file containing the requested platform's credentials.
|
|
65
|
+
|
|
66
|
+
Outside that environment it raises `muad_skill_http.PlatformSessionError`.
|
|
67
|
+
Plain `get` / `post` calls work anywhere.
|
|
68
|
+
|
|
69
|
+
## Environment variables
|
|
70
|
+
|
|
71
|
+
| Variable | Purpose |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `MUAD_SKILL_NAME` | Skill name used to resolve the platform session. |
|
|
74
|
+
| `MUAD_TRACE_SPOOL` | Optional JSONL trace spool path; paired with `MUAD_EXECUTION_ID`. |
|
|
75
|
+
| `MUAD_EXECUTION_ID` | Execution id written to the trace spool. |
|
|
76
|
+
| `MUAD_SKILL_REQUEST_LIMIT` | Per-execution interface request limit; unset or `0` means unlimited. Exceeding the limit exits with code `86`. |
|
|
77
|
+
|
|
78
|
+
I/O failures while writing the trace spool are swallowed: auditing never breaks
|
|
79
|
+
a skill request.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -m pytest test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Releases are published by pushing an `sdk-v*` tag; the tag must match
|
|
88
|
+
`project.version` in `pyproject.toml`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# muad-skill-http
|
|
2
|
+
|
|
3
|
+
Trace-aware HTTP helpers and a platform request client for muad skills.
|
|
4
|
+
|
|
5
|
+
`platform_request()` sends business-platform requests through a platform session
|
|
6
|
+
managed by `session-manager`. It assembles the browser defaults, platform host
|
|
7
|
+
header, platform static headers, cookies, CSRF header and a fresh `Traceid`
|
|
8
|
+
UUID v4 for every request, and re-asserts the mandatory headers last so caller
|
|
9
|
+
headers cannot override them. Cookie and CSRF values never reach the trace
|
|
10
|
+
spool.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install muad-skill-http
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from muad_skill_http import get, platform_request
|
|
22
|
+
|
|
23
|
+
# Plain HTTP with trace accounting.
|
|
24
|
+
response = get("https://example.com/api/v1/items")
|
|
25
|
+
|
|
26
|
+
# Business-platform request: headers and Traceid are built by the SDK.
|
|
27
|
+
response = platform_request("demo", "GET", "/api/v1/targets")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`get` / `post` / `put` / `patch` / `delete` / `head` / `options` / `request`
|
|
31
|
+
wrap `requests` and record request counts for the current skill execution.
|
|
32
|
+
|
|
33
|
+
### Runtime requirements for `platform_request`
|
|
34
|
+
|
|
35
|
+
`platform_request` resolves the platform session from the skill runtime, not
|
|
36
|
+
from the local machine. It needs:
|
|
37
|
+
|
|
38
|
+
- `MUAD_SKILL_NAME` set to the running skill name;
|
|
39
|
+
- a `session-manager` executable on `PATH` that answers
|
|
40
|
+
`session-manager get-state --skill-name <name>` with
|
|
41
|
+
`{"sessionStateFile": "..."}`;
|
|
42
|
+
- a session state file containing the requested platform's credentials.
|
|
43
|
+
|
|
44
|
+
Outside that environment it raises `muad_skill_http.PlatformSessionError`.
|
|
45
|
+
Plain `get` / `post` calls work anywhere.
|
|
46
|
+
|
|
47
|
+
## Environment variables
|
|
48
|
+
|
|
49
|
+
| Variable | Purpose |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `MUAD_SKILL_NAME` | Skill name used to resolve the platform session. |
|
|
52
|
+
| `MUAD_TRACE_SPOOL` | Optional JSONL trace spool path; paired with `MUAD_EXECUTION_ID`. |
|
|
53
|
+
| `MUAD_EXECUTION_ID` | Execution id written to the trace spool. |
|
|
54
|
+
| `MUAD_SKILL_REQUEST_LIMIT` | Per-execution interface request limit; unset or `0` means unlimited. Exceeding the limit exits with code `86`. |
|
|
55
|
+
|
|
56
|
+
I/O failures while writing the trace spool are swallowed: auditing never breaks
|
|
57
|
+
a skill request.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m pytest test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Releases are published by pushing an `sdk-v*` tag; the tag must match
|
|
66
|
+
`project.version` in `pyproject.toml`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""muad_skill_http: trace-aware requests helper and platform request client.
|
|
2
|
+
|
|
3
|
+
Usage in skills::
|
|
4
|
+
|
|
5
|
+
import muad_skill_http
|
|
6
|
+
|
|
7
|
+
response = muad_skill_http.get("http://service/api")
|
|
8
|
+
response = muad_skill_http.platform_request("mssw", "GET", "/api/v1/targets")
|
|
9
|
+
|
|
10
|
+
Without importing this module, existing skills keep their original behavior:
|
|
11
|
+
``requests`` is never patched and no trace spool is written.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from . import _platform, _trace
|
|
17
|
+
from ._http import delete, get, head, options, patch, post, put, request, session
|
|
18
|
+
from ._platform import PlatformClient, PlatformSessionError, platform, platform_request
|
|
19
|
+
from ._trace import EXIT_REQUEST_LIMIT
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"EXIT_REQUEST_LIMIT",
|
|
23
|
+
"PlatformClient",
|
|
24
|
+
"PlatformSessionError",
|
|
25
|
+
"delete",
|
|
26
|
+
"get",
|
|
27
|
+
"head",
|
|
28
|
+
"options",
|
|
29
|
+
"patch",
|
|
30
|
+
"platform",
|
|
31
|
+
"platform_request",
|
|
32
|
+
"post",
|
|
33
|
+
"put",
|
|
34
|
+
"request",
|
|
35
|
+
"session",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _reset_for_tests():
|
|
40
|
+
"""Drop the process-wide recorder and session cache (tests only)."""
|
|
41
|
+
_trace.reset()
|
|
42
|
+
_platform.reset_cache()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _flush_summary():
|
|
46
|
+
"""Write the trace summary early (tests and one-shot scripts)."""
|
|
47
|
+
_trace.flush_summary()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Thin requests pass-through that records every helper request."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from . import _trace
|
|
10
|
+
from ._normalize import split_target
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _duration_ms(started):
|
|
14
|
+
return int((time.monotonic() - started) * 1000)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _send(call, method, url, **kwargs):
|
|
18
|
+
method = str(method).upper()
|
|
19
|
+
extra = kwargs.pop("_trace_extra", None)
|
|
20
|
+
recorder = _trace.get_recorder()
|
|
21
|
+
host, path = split_target(url)
|
|
22
|
+
recorder.admit(host, method, path)
|
|
23
|
+
started = time.monotonic()
|
|
24
|
+
try:
|
|
25
|
+
response = call(method, url, **kwargs)
|
|
26
|
+
except requests.RequestException as exc:
|
|
27
|
+
recorder.complete(host, method, path, None, _duration_ms(started), type(exc).__name__, extra)
|
|
28
|
+
raise
|
|
29
|
+
status = response.status_code
|
|
30
|
+
error_class = "http_%d" % status if status >= 400 else ""
|
|
31
|
+
recorder.complete(host, method, path, status, _duration_ms(started), error_class, extra)
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def request(method, url, **kwargs):
|
|
36
|
+
"""requests.request with trace + per-interface limit accounting."""
|
|
37
|
+
return _send(requests.request, method, url, **kwargs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get(url, **kwargs):
|
|
41
|
+
return request("GET", url, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def head(url, **kwargs):
|
|
45
|
+
return request("HEAD", url, **kwargs)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def options(url, **kwargs):
|
|
49
|
+
return request("OPTIONS", url, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def post(url, **kwargs):
|
|
53
|
+
return request("POST", url, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def put(url, **kwargs):
|
|
57
|
+
return request("PUT", url, **kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def patch(url, **kwargs):
|
|
61
|
+
return request("PATCH", url, **kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def delete(url, **kwargs):
|
|
65
|
+
return request("DELETE", url, **kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _TrackedSession:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self._session = requests.Session()
|
|
71
|
+
|
|
72
|
+
def request(self, method, url, **kwargs):
|
|
73
|
+
return _send(self._session.request, method, url, **kwargs)
|
|
74
|
+
|
|
75
|
+
def get(self, url, **kwargs):
|
|
76
|
+
return self.request("GET", url, **kwargs)
|
|
77
|
+
|
|
78
|
+
def head(self, url, **kwargs):
|
|
79
|
+
return self.request("HEAD", url, **kwargs)
|
|
80
|
+
|
|
81
|
+
def options(self, url, **kwargs):
|
|
82
|
+
return self.request("OPTIONS", url, **kwargs)
|
|
83
|
+
|
|
84
|
+
def post(self, url, **kwargs):
|
|
85
|
+
return self.request("POST", url, **kwargs)
|
|
86
|
+
|
|
87
|
+
def put(self, url, **kwargs):
|
|
88
|
+
return self.request("PUT", url, **kwargs)
|
|
89
|
+
|
|
90
|
+
def patch(self, url, **kwargs):
|
|
91
|
+
return self.request("PATCH", url, **kwargs)
|
|
92
|
+
|
|
93
|
+
def delete(self, url, **kwargs):
|
|
94
|
+
return self.request("DELETE", url, **kwargs)
|
|
95
|
+
|
|
96
|
+
def close(self):
|
|
97
|
+
self._session.close()
|
|
98
|
+
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def __exit__(self, exc_type, exc, tb):
|
|
103
|
+
self.close()
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def session():
|
|
108
|
+
"""A requests.Session wrapper (supports ``with``) that traces each call."""
|
|
109
|
+
return _TrackedSession()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Path and target normalization for interface counting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
_UUID_RE = re.compile(
|
|
9
|
+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
10
|
+
)
|
|
11
|
+
_HEX_RE = re.compile(r"^[0-9a-fA-F]{8,}$")
|
|
12
|
+
_DIGITS_RE = re.compile(r"^\d+$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_path(path):
|
|
16
|
+
"""Collapse volatile path segments and drop query/fragment.
|
|
17
|
+
|
|
18
|
+
Numeric, UUID and long hex segments become ``{id}`` so one business
|
|
19
|
+
interface is counted once regardless of object ids.
|
|
20
|
+
"""
|
|
21
|
+
if not isinstance(path, str):
|
|
22
|
+
path = str(path or "")
|
|
23
|
+
if path.startswith(("http://", "https://")):
|
|
24
|
+
raw = urlsplit(path).path or "/"
|
|
25
|
+
else:
|
|
26
|
+
raw = path.split("?", 1)[0].split("#", 1)[0]
|
|
27
|
+
if not raw:
|
|
28
|
+
return "/"
|
|
29
|
+
segments = ["{id}" if _is_volatile(segment) else segment for segment in raw.split("/")]
|
|
30
|
+
return "/".join(segments) or "/"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def split_target(url):
|
|
34
|
+
"""Return (host:port, normalized path) for a request URL."""
|
|
35
|
+
parts = urlsplit(str(url))
|
|
36
|
+
host = (parts.hostname or "").lower()
|
|
37
|
+
try:
|
|
38
|
+
port = parts.port
|
|
39
|
+
except ValueError:
|
|
40
|
+
port = None
|
|
41
|
+
if port:
|
|
42
|
+
host = "%s:%d" % (host, port)
|
|
43
|
+
return host, normalize_path(parts.path or "/")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_volatile(segment):
|
|
47
|
+
if not segment:
|
|
48
|
+
return False
|
|
49
|
+
return bool(_DIGITS_RE.match(segment) or _UUID_RE.match(segment) or _HEX_RE.match(segment))
|