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.
@@ -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))