humanleg 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.
- humanleg-0.1.0/.gitignore +12 -0
- humanleg-0.1.0/PKG-INFO +36 -0
- humanleg-0.1.0/README.md +17 -0
- humanleg-0.1.0/humanleg/__init__.py +4 -0
- humanleg-0.1.0/humanleg/client.py +105 -0
- humanleg-0.1.0/pyproject.toml +29 -0
- humanleg-0.1.0/tests/test_client.py +46 -0
humanleg-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: humanleg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the HumanLeg.ai public agent API
|
|
5
|
+
Project-URL: Homepage, https://humanleg.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/humanleg-ai/humanleg
|
|
7
|
+
Project-URL: Issues, https://github.com/humanleg-ai/humanleg/issues
|
|
8
|
+
Author: HumanLeg.ai contributors
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# humanleg
|
|
21
|
+
|
|
22
|
+
Dependency-light Python client for the HumanLeg.ai public agent API.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install humanleg
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from humanleg import HumanLegClient
|
|
30
|
+
|
|
31
|
+
client = HumanLegClient(
|
|
32
|
+
base_url="http://127.0.0.1:8787",
|
|
33
|
+
token="sandbox-agent-token",
|
|
34
|
+
)
|
|
35
|
+
task = client.get_task("task-id")
|
|
36
|
+
```
|
humanleg-0.1.0/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# humanleg
|
|
2
|
+
|
|
3
|
+
Dependency-light Python client for the HumanLeg.ai public agent API.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install humanleg
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from humanleg import HumanLegClient
|
|
11
|
+
|
|
12
|
+
client = HumanLegClient(
|
|
13
|
+
base_url="http://127.0.0.1:8787",
|
|
14
|
+
token="sandbox-agent-token",
|
|
15
|
+
)
|
|
16
|
+
task = client.get_task("task-id")
|
|
17
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
from urllib import error, parse, request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HumanLegApiError(RuntimeError):
|
|
11
|
+
def __init__(self, status: int, problem: dict[str, Any]):
|
|
12
|
+
self.status = status
|
|
13
|
+
self.problem = problem
|
|
14
|
+
super().__init__(
|
|
15
|
+
str(problem.get("detail") or problem.get("title") or f"HumanLeg API failed with {status}")
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class HumanLegClient:
|
|
21
|
+
base_url: str
|
|
22
|
+
token: str
|
|
23
|
+
timeout: float = 15.0
|
|
24
|
+
idempotency_key: Callable[[], str] = lambda: str(uuid.uuid4())
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
if not self.base_url.startswith(("http://", "https://")):
|
|
28
|
+
raise ValueError("base_url must use http:// or https://")
|
|
29
|
+
if not self.token:
|
|
30
|
+
raise ValueError("token is required")
|
|
31
|
+
object.__setattr__(self, "base_url", self.base_url.rstrip("/"))
|
|
32
|
+
|
|
33
|
+
def create_fee_quote(self, *, reward_atomic: int, display_aud_cents: int) -> dict[str, Any]:
|
|
34
|
+
return self._request(
|
|
35
|
+
"/v1/fee-quotes",
|
|
36
|
+
method="POST",
|
|
37
|
+
body={"rewardAtomic": reward_atomic, "displayAudCents": display_aud_cents},
|
|
38
|
+
idempotent=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def create_task(self, **task: Any) -> dict[str, Any]:
|
|
42
|
+
return self._request("/v1/tasks", method="POST", body=task, idempotent=True)
|
|
43
|
+
|
|
44
|
+
def get_task(self, task_id: str) -> dict[str, Any]:
|
|
45
|
+
return self._request(f"/v1/tasks/{parse.quote(task_id, safe='')}")
|
|
46
|
+
|
|
47
|
+
def update_task(self, task_id: str, **task: Any) -> dict[str, Any]:
|
|
48
|
+
return self._request(
|
|
49
|
+
f"/v1/tasks/{parse.quote(task_id, safe='')}",
|
|
50
|
+
method="PATCH",
|
|
51
|
+
body=task,
|
|
52
|
+
idempotent=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def get_compliance_review(self, task_id: str) -> dict[str, Any]:
|
|
56
|
+
return self._request(f"/v1/tasks/{parse.quote(task_id, safe='')}/compliance-review")
|
|
57
|
+
|
|
58
|
+
def create_funding_intent(self, task_id: str) -> dict[str, Any]:
|
|
59
|
+
return self._request(
|
|
60
|
+
f"/v1/tasks/{parse.quote(task_id, safe='')}/funding-intents",
|
|
61
|
+
method="POST",
|
|
62
|
+
idempotent=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def publish_task(self, task_id: str, *, expected_version: int) -> dict[str, Any]:
|
|
66
|
+
return self._request(
|
|
67
|
+
f"/v1/tasks/{parse.quote(task_id, safe='')}/open",
|
|
68
|
+
method="POST",
|
|
69
|
+
body={"expectedVersion": expected_version},
|
|
70
|
+
idempotent=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def list_events(self, *, after: str | None = None) -> dict[str, Any]:
|
|
74
|
+
suffix = "" if after is None else f"?after={parse.quote(after)}"
|
|
75
|
+
return self._request(f"/v1/events{suffix}")
|
|
76
|
+
|
|
77
|
+
def _request(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
*,
|
|
81
|
+
method: str = "GET",
|
|
82
|
+
body: dict[str, Any] | None = None,
|
|
83
|
+
idempotent: bool = False,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
headers = {"Accept": "application/json", "Authorization": f"Bearer {self.token}"}
|
|
86
|
+
data = None
|
|
87
|
+
if body is not None:
|
|
88
|
+
headers["Content-Type"] = "application/json"
|
|
89
|
+
data = json.dumps(body).encode("utf-8")
|
|
90
|
+
if idempotent:
|
|
91
|
+
headers["Idempotency-Key"] = self.idempotency_key()
|
|
92
|
+
api_request = request.Request(
|
|
93
|
+
f"{self.base_url}{path}", data=data, headers=headers, method=method
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
with request.urlopen(api_request, timeout=self.timeout) as response:
|
|
97
|
+
content = response.read()
|
|
98
|
+
return json.loads(content) if content else {}
|
|
99
|
+
except error.HTTPError as exc:
|
|
100
|
+
content = exc.read()
|
|
101
|
+
try:
|
|
102
|
+
problem = json.loads(content) if content else {}
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
problem = {"title": "HTTP error", "detail": content.decode("utf-8", "replace")}
|
|
105
|
+
raise HumanLegApiError(exc.code, problem) from exc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "humanleg"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the HumanLeg.ai public agent API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "HumanLeg.ai contributors" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://humanleg.ai"
|
|
25
|
+
Repository = "https://github.com/humanleg-ai/humanleg"
|
|
26
|
+
Issues = "https://github.com/humanleg-ai/humanleg/issues"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["humanleg"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import pathlib
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(pathlib.Path(__file__).parents[1]))
|
|
9
|
+
|
|
10
|
+
from humanleg import HumanLegClient # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeResponse:
|
|
14
|
+
def __init__(self, body):
|
|
15
|
+
self.body = json.dumps(body).encode()
|
|
16
|
+
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def __exit__(self, *_args):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def read(self):
|
|
24
|
+
return self.body
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HumanLegClientTests(unittest.TestCase):
|
|
28
|
+
def test_fee_quote_headers_and_body(self):
|
|
29
|
+
client = HumanLegClient(
|
|
30
|
+
base_url="https://sandbox.humanleg.ai/",
|
|
31
|
+
token="test-token",
|
|
32
|
+
idempotency_key=lambda: "idem-12345678",
|
|
33
|
+
)
|
|
34
|
+
with patch("urllib.request.urlopen", return_value=FakeResponse({"id": "quote-1"})) as call:
|
|
35
|
+
result = client.create_fee_quote(reward_atomic=20_000_000, display_aud_cents=3_000)
|
|
36
|
+
|
|
37
|
+
self.assertEqual(result["id"], "quote-1")
|
|
38
|
+
api_request = call.call_args.args[0]
|
|
39
|
+
self.assertEqual(api_request.full_url, "https://sandbox.humanleg.ai/v1/fee-quotes")
|
|
40
|
+
self.assertEqual(api_request.headers["Authorization"], "Bearer test-token")
|
|
41
|
+
self.assertEqual(api_request.headers["Idempotency-key"], "idem-12345678")
|
|
42
|
+
self.assertEqual(json.loads(api_request.data), {"rewardAtomic": 20_000_000, "displayAudCents": 3_000})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
unittest.main()
|