oneclaw 0.1.0__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.
- oneclaw/__init__.py +33 -0
- oneclaw/client.py +148 -0
- oneclaw/errors.py +124 -0
- oneclaw/http_client.py +242 -0
- oneclaw/py.typed +0 -0
- oneclaw/resources/__init__.py +41 -0
- oneclaw/resources/agents.py +236 -0
- oneclaw/resources/api_keys.py +39 -0
- oneclaw/resources/approvals.py +55 -0
- oneclaw/resources/audit.py +37 -0
- oneclaw/resources/auth.py +199 -0
- oneclaw/resources/billing.py +66 -0
- oneclaw/resources/chains.py +24 -0
- oneclaw/resources/org.py +63 -0
- oneclaw/resources/platform.py +170 -0
- oneclaw/resources/policies.py +68 -0
- oneclaw/resources/risk.py +61 -0
- oneclaw/resources/secrets.py +96 -0
- oneclaw/resources/sharing.py +77 -0
- oneclaw/resources/signing_keys.py +46 -0
- oneclaw/resources/treasury.py +143 -0
- oneclaw/resources/treasury_wallets.py +99 -0
- oneclaw/resources/vaults.py +85 -0
- oneclaw/resources/webhooks.py +47 -0
- oneclaw/types.py +759 -0
- oneclaw-0.1.0.dist-info/METADATA +289 -0
- oneclaw-0.1.0.dist-info/RECORD +29 -0
- oneclaw-0.1.0.dist-info/WHEEL +4 -0
- oneclaw-0.1.0.dist-info/licenses/LICENSE +21 -0
oneclaw/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Official Python SDK for the 1Claw secrets management platform."""
|
|
2
|
+
|
|
3
|
+
from oneclaw.client import OneclawClient, create_client
|
|
4
|
+
from oneclaw.errors import (
|
|
5
|
+
ApprovalRequiredError,
|
|
6
|
+
AuthError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
OneclawError,
|
|
9
|
+
PaymentRequiredError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ResourceLimitExceededError,
|
|
12
|
+
ServerError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from oneclaw.types import OneclawClientConfig, OneclawResponse
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"OneclawClient",
|
|
21
|
+
"create_client",
|
|
22
|
+
"OneclawClientConfig",
|
|
23
|
+
"OneclawResponse",
|
|
24
|
+
"OneclawError",
|
|
25
|
+
"AuthError",
|
|
26
|
+
"NotFoundError",
|
|
27
|
+
"PaymentRequiredError",
|
|
28
|
+
"RateLimitError",
|
|
29
|
+
"ResourceLimitExceededError",
|
|
30
|
+
"ValidationError",
|
|
31
|
+
"ServerError",
|
|
32
|
+
"ApprovalRequiredError",
|
|
33
|
+
]
|
oneclaw/client.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Main client for the 1Claw Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from oneclaw.http_client import HttpClient
|
|
6
|
+
from oneclaw.resources.agents import AgentsResource
|
|
7
|
+
from oneclaw.resources.api_keys import ApiKeysResource
|
|
8
|
+
from oneclaw.resources.approvals import ApprovalsResource
|
|
9
|
+
from oneclaw.resources.audit import AuditResource
|
|
10
|
+
from oneclaw.resources.auth import AuthResource
|
|
11
|
+
from oneclaw.resources.billing import BillingResource
|
|
12
|
+
from oneclaw.resources.chains import ChainsResource
|
|
13
|
+
from oneclaw.resources.org import OrgResource
|
|
14
|
+
from oneclaw.resources.platform import PlatformResource
|
|
15
|
+
from oneclaw.resources.policies import PoliciesResource
|
|
16
|
+
from oneclaw.resources.risk import RiskResource
|
|
17
|
+
from oneclaw.resources.secrets import SecretsResource
|
|
18
|
+
from oneclaw.resources.sharing import SharingResource
|
|
19
|
+
from oneclaw.resources.signing_keys import SigningKeysResource
|
|
20
|
+
from oneclaw.resources.treasury import TreasuryResource
|
|
21
|
+
from oneclaw.resources.treasury_wallets import TreasuryWalletsResource
|
|
22
|
+
from oneclaw.resources.vaults import VaultResource
|
|
23
|
+
from oneclaw.resources.webhooks import WebhooksResource
|
|
24
|
+
from oneclaw.types import OneclawClientConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OneclawClient:
|
|
28
|
+
"""The main 1Claw SDK client.
|
|
29
|
+
|
|
30
|
+
All API resources are exposed as namespaced attributes for
|
|
31
|
+
discoverability.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> from oneclaw import create_client
|
|
36
|
+
>>>
|
|
37
|
+
>>> # Agent with API key (auto-exchanges for JWT)
|
|
38
|
+
>>> client = create_client(api_key="ocv_...")
|
|
39
|
+
>>>
|
|
40
|
+
>>> # Pre-authenticated with JWT
|
|
41
|
+
>>> client = create_client(token="eyJ...")
|
|
42
|
+
>>>
|
|
43
|
+
>>> # Use resources
|
|
44
|
+
>>> vaults = client.vaults.list()
|
|
45
|
+
>>> secret = client.secrets.get(vault_id, "my-key")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config: OneclawClientConfig) -> None:
|
|
49
|
+
self._http = HttpClient(config)
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
config.api_key
|
|
53
|
+
and not config.token
|
|
54
|
+
and not config.agent_id
|
|
55
|
+
and not config.api_key.startswith("ocv_")
|
|
56
|
+
):
|
|
57
|
+
self._auto_authenticate_user_key(config.api_key)
|
|
58
|
+
|
|
59
|
+
self.auth = AuthResource(self._http)
|
|
60
|
+
self.vaults = VaultResource(self._http)
|
|
61
|
+
self.secrets = SecretsResource(self._http)
|
|
62
|
+
self.agents = AgentsResource(self._http)
|
|
63
|
+
self.policies = PoliciesResource(self._http)
|
|
64
|
+
self.chains = ChainsResource(self._http)
|
|
65
|
+
self.sharing = SharingResource(self._http)
|
|
66
|
+
self.billing = BillingResource(self._http)
|
|
67
|
+
self.audit = AuditResource(self._http)
|
|
68
|
+
self.org = OrgResource(self._http)
|
|
69
|
+
self.api_keys = ApiKeysResource(self._http)
|
|
70
|
+
self.signing_keys = SigningKeysResource(self._http)
|
|
71
|
+
self.treasury = TreasuryResource(self._http)
|
|
72
|
+
self.treasury_wallets = TreasuryWalletsResource(self._http)
|
|
73
|
+
self.platform = PlatformResource(self._http)
|
|
74
|
+
self.approvals = ApprovalsResource(self._http)
|
|
75
|
+
self.webhooks = WebhooksResource(self._http)
|
|
76
|
+
self.risk = RiskResource(self._http)
|
|
77
|
+
|
|
78
|
+
def _auto_authenticate_user_key(self, api_key: str) -> None:
|
|
79
|
+
"""Exchange a ``1ck_`` user API key for a JWT (fire-and-forget)."""
|
|
80
|
+
try:
|
|
81
|
+
resp = self._http.request(
|
|
82
|
+
"POST", "/v1/auth/api-key-token",
|
|
83
|
+
body={"api_key": api_key},
|
|
84
|
+
skip_auth=True,
|
|
85
|
+
)
|
|
86
|
+
if resp.data and resp.data.get("access_token"):
|
|
87
|
+
self._http.set_token(resp.data["access_token"])
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def resolved_agent_id(self) -> str | None:
|
|
93
|
+
"""Agent ID resolved from the token exchange (for key-only auth)."""
|
|
94
|
+
return self._http.resolved_agent_id
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
"""Close the underlying HTTP connection pool."""
|
|
98
|
+
self._http.close()
|
|
99
|
+
|
|
100
|
+
def __enter__(self) -> OneclawClient:
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, *args: object) -> None:
|
|
104
|
+
self.close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def create_client(
|
|
108
|
+
*,
|
|
109
|
+
base_url: str = "https://api.1claw.xyz",
|
|
110
|
+
token: str | None = None,
|
|
111
|
+
api_key: str | None = None,
|
|
112
|
+
agent_id: str | None = None,
|
|
113
|
+
timeout: float = 30.0,
|
|
114
|
+
) -> OneclawClient:
|
|
115
|
+
"""Create a new 1Claw SDK client.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
base_url : str
|
|
120
|
+
The 1Claw API base URL (default: ``https://api.1claw.xyz``).
|
|
121
|
+
token : str, optional
|
|
122
|
+
A pre-existing JWT to use for authentication.
|
|
123
|
+
api_key : str, optional
|
|
124
|
+
An API key (``ocv_`` for agents, ``1ck_`` for users).
|
|
125
|
+
Agent keys auto-exchange for JWTs and refresh before expiry.
|
|
126
|
+
agent_id : str, optional
|
|
127
|
+
The agent UUID (optional with ``ocv_`` keys — auto-discovered).
|
|
128
|
+
timeout : float
|
|
129
|
+
HTTP request timeout in seconds (default: 30).
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
OneclawClient
|
|
134
|
+
A fully configured client instance.
|
|
135
|
+
|
|
136
|
+
Examples
|
|
137
|
+
--------
|
|
138
|
+
>>> client = create_client(api_key="ocv_abc123")
|
|
139
|
+
>>> vaults = client.vaults.list()
|
|
140
|
+
"""
|
|
141
|
+
config = OneclawClientConfig(
|
|
142
|
+
base_url=base_url,
|
|
143
|
+
token=token,
|
|
144
|
+
api_key=api_key,
|
|
145
|
+
agent_id=agent_id,
|
|
146
|
+
timeout=timeout,
|
|
147
|
+
)
|
|
148
|
+
return OneclawClient(config)
|
oneclaw/errors.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Error types for the 1Claw Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OneclawError(Exception):
|
|
10
|
+
"""Base error for all 1Claw SDK errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
status: int,
|
|
16
|
+
error_type: str = "unknown",
|
|
17
|
+
detail: str | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.status = status
|
|
21
|
+
self.error_type = error_type
|
|
22
|
+
self.detail = detail
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthError(OneclawError):
|
|
26
|
+
"""Raised on 401 Unauthorized or 403 Forbidden responses."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, status: int = 401) -> None:
|
|
29
|
+
super().__init__(message, status, "auth_error")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ResourceLimitExceededError(OneclawError):
|
|
33
|
+
"""Raised when a resource limit is exceeded (403 with type resource_limit_exceeded)."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str) -> None:
|
|
36
|
+
super().__init__(message, 403, "resource_limit_exceeded")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PaymentRequiredError(OneclawError):
|
|
40
|
+
"""Raised on 402 Payment Required responses."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, requirement: dict[str, Any] | None = None) -> None:
|
|
43
|
+
super().__init__(message, 402, "payment_required")
|
|
44
|
+
self.payment_requirement = requirement
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ApprovalRequiredError(OneclawError):
|
|
48
|
+
"""Raised when human approval is needed to access a resource."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, approval_request_id: str, message: str | None = None) -> None:
|
|
51
|
+
super().__init__(
|
|
52
|
+
message or "Human approval is required to access this secret",
|
|
53
|
+
403,
|
|
54
|
+
"approval_required",
|
|
55
|
+
)
|
|
56
|
+
self.approval_request_id = approval_request_id
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NotFoundError(OneclawError):
|
|
60
|
+
"""Raised on 404 Not Found responses."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message: str = "Resource not found") -> None:
|
|
63
|
+
super().__init__(message, 404, "not_found")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RateLimitError(OneclawError):
|
|
67
|
+
"""Raised on 429 Too Many Requests. Includes retry timing when available."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
message: str = "Rate limit exceeded",
|
|
72
|
+
retry_after_ms: int | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(message, 429, "rate_limit")
|
|
75
|
+
self.retry_after_ms = retry_after_ms
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ValidationError(OneclawError):
|
|
79
|
+
"""Raised on 400 Bad Request responses for validation failures."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, message: str, fields: dict[str, str] | None = None) -> None:
|
|
82
|
+
super().__init__(message, 400, "validation_error")
|
|
83
|
+
self.fields = fields
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ServerError(OneclawError):
|
|
87
|
+
"""Raised on 500+ server-side errors."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, message: str = "Internal server error", status: int = 500) -> None:
|
|
90
|
+
super().__init__(message, status, "server_error")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def error_from_response(
|
|
94
|
+
status: int,
|
|
95
|
+
body: dict[str, Any],
|
|
96
|
+
headers: dict[str, str] | None = None,
|
|
97
|
+
) -> OneclawError:
|
|
98
|
+
"""Parse an HTTP response into the appropriate typed error."""
|
|
99
|
+
message = body.get("detail") or body.get("message") or body.get("error") or f"HTTP {status}"
|
|
100
|
+
|
|
101
|
+
if status == 400:
|
|
102
|
+
return ValidationError(message, body.get("fields"))
|
|
103
|
+
if status == 401:
|
|
104
|
+
return AuthError(message, 401)
|
|
105
|
+
if status == 403:
|
|
106
|
+
error_type = body.get("type", "")
|
|
107
|
+
if error_type == "resource_limit_exceeded":
|
|
108
|
+
return ResourceLimitExceededError(message)
|
|
109
|
+
if error_type == "approval_required":
|
|
110
|
+
return ApprovalRequiredError(body.get("approval_request_id", ""), message)
|
|
111
|
+
return AuthError(message, 403)
|
|
112
|
+
if status == 402:
|
|
113
|
+
return PaymentRequiredError(message, body)
|
|
114
|
+
if status == 404:
|
|
115
|
+
return NotFoundError(message)
|
|
116
|
+
if status == 429:
|
|
117
|
+
retry_after_ms = None
|
|
118
|
+
if headers and "retry-after" in headers:
|
|
119
|
+
with contextlib.suppress(ValueError):
|
|
120
|
+
retry_after_ms = int(headers["retry-after"]) * 1000
|
|
121
|
+
return RateLimitError(message, retry_after_ms)
|
|
122
|
+
if status >= 500:
|
|
123
|
+
return ServerError(message, status)
|
|
124
|
+
return OneclawError(message, status, "unknown")
|
oneclaw/http_client.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Internal HTTP transport for the 1Claw Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from oneclaw.errors import OneclawError, error_from_response
|
|
14
|
+
from oneclaw.types import ErrorInfo, OneclawClientConfig, OneclawResponse, ResponseMeta
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
_REFRESH_BUFFER_SECS = 60
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpClient:
|
|
22
|
+
"""Low-level HTTP client that handles authentication and error mapping.
|
|
23
|
+
|
|
24
|
+
All resource modules delegate requests through this class.
|
|
25
|
+
Agent ``ocv_`` API keys are automatically exchanged for JWTs and
|
|
26
|
+
refreshed 60 seconds before expiry.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: OneclawClientConfig) -> None:
|
|
30
|
+
self._base_url = config.base_url.rstrip("/")
|
|
31
|
+
self._token = config.token
|
|
32
|
+
self._token_expires_at: float = 0
|
|
33
|
+
self._timeout = config.timeout
|
|
34
|
+
|
|
35
|
+
self._agent_credentials: dict[str, str | None] | None = None
|
|
36
|
+
self._resolved_agent_id: str | None = None
|
|
37
|
+
self._refresh_lock = threading.Lock()
|
|
38
|
+
|
|
39
|
+
is_agent_key = (config.api_key or "").startswith("ocv_") or config.agent_id is not None
|
|
40
|
+
if config.api_key and is_agent_key:
|
|
41
|
+
self._agent_credentials = {
|
|
42
|
+
"agent_id": config.agent_id,
|
|
43
|
+
"api_key": config.api_key,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
self._client = httpx.Client(
|
|
47
|
+
base_url=self._base_url,
|
|
48
|
+
timeout=self._timeout,
|
|
49
|
+
headers={"Content-Type": "application/json"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# -- public helpers --------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def resolved_agent_id(self) -> str | None:
|
|
56
|
+
if self._resolved_agent_id:
|
|
57
|
+
return self._resolved_agent_id
|
|
58
|
+
if self._agent_credentials:
|
|
59
|
+
return self._agent_credentials.get("agent_id")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def set_token(self, token: str) -> None:
|
|
63
|
+
self._token = token
|
|
64
|
+
self._token_expires_at = self._decode_expiry(token)
|
|
65
|
+
|
|
66
|
+
def get_token(self) -> str | None:
|
|
67
|
+
return self._token
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def base_url(self) -> str:
|
|
71
|
+
return self._base_url
|
|
72
|
+
|
|
73
|
+
# -- request methods -------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def request(
|
|
76
|
+
self,
|
|
77
|
+
method: str,
|
|
78
|
+
path: str,
|
|
79
|
+
*,
|
|
80
|
+
body: Any | None = None,
|
|
81
|
+
query: dict[str, Any] | None = None,
|
|
82
|
+
headers: dict[str, str] | None = None,
|
|
83
|
+
skip_auth: bool = False,
|
|
84
|
+
) -> OneclawResponse[Any]:
|
|
85
|
+
"""Perform a typed request and return an :class:`OneclawResponse` envelope."""
|
|
86
|
+
if not skip_auth:
|
|
87
|
+
self._ensure_token()
|
|
88
|
+
|
|
89
|
+
req_headers: dict[str, str] = {}
|
|
90
|
+
if headers:
|
|
91
|
+
req_headers.update(headers)
|
|
92
|
+
if not skip_auth and self._token:
|
|
93
|
+
req_headers["Authorization"] = f"Bearer {self._token}"
|
|
94
|
+
|
|
95
|
+
cleaned_query = None
|
|
96
|
+
if query:
|
|
97
|
+
cleaned_query = {k: str(v) for k, v in query.items() if v is not None}
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
response = self._client.request(
|
|
101
|
+
method,
|
|
102
|
+
path,
|
|
103
|
+
json=body if body is not None else None,
|
|
104
|
+
params=cleaned_query,
|
|
105
|
+
headers=req_headers,
|
|
106
|
+
)
|
|
107
|
+
except httpx.HTTPError as exc:
|
|
108
|
+
return OneclawResponse(
|
|
109
|
+
data=None,
|
|
110
|
+
error=ErrorInfo(type="network_error", message=str(exc)),
|
|
111
|
+
meta=ResponseMeta(status=0),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not response.is_success:
|
|
115
|
+
try:
|
|
116
|
+
err_body = response.json()
|
|
117
|
+
except Exception:
|
|
118
|
+
err_body = {}
|
|
119
|
+
|
|
120
|
+
err = error_from_response(
|
|
121
|
+
response.status_code,
|
|
122
|
+
err_body if isinstance(err_body, dict) else {},
|
|
123
|
+
dict(response.headers),
|
|
124
|
+
)
|
|
125
|
+
return OneclawResponse(
|
|
126
|
+
data=None,
|
|
127
|
+
error=ErrorInfo(type=err.error_type, message=str(err), detail=err.detail),
|
|
128
|
+
meta=ResponseMeta(status=response.status_code),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if response.status_code == 204:
|
|
132
|
+
return OneclawResponse(data=None, error=None, meta=ResponseMeta(status=204))
|
|
133
|
+
|
|
134
|
+
return OneclawResponse(
|
|
135
|
+
data=response.json(),
|
|
136
|
+
error=None,
|
|
137
|
+
meta=ResponseMeta(status=response.status_code),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def request_or_throw(
|
|
141
|
+
self,
|
|
142
|
+
method: str,
|
|
143
|
+
path: str,
|
|
144
|
+
*,
|
|
145
|
+
body: Any | None = None,
|
|
146
|
+
query: dict[str, Any] | None = None,
|
|
147
|
+
headers: dict[str, str] | None = None,
|
|
148
|
+
) -> Any:
|
|
149
|
+
"""Same as :meth:`request` but raises :class:`OneclawError` on failure."""
|
|
150
|
+
self._ensure_token()
|
|
151
|
+
|
|
152
|
+
req_headers: dict[str, str] = {}
|
|
153
|
+
if headers:
|
|
154
|
+
req_headers.update(headers)
|
|
155
|
+
if self._token:
|
|
156
|
+
req_headers["Authorization"] = f"Bearer {self._token}"
|
|
157
|
+
|
|
158
|
+
cleaned_query = None
|
|
159
|
+
if query:
|
|
160
|
+
cleaned_query = {k: str(v) for k, v in query.items() if v is not None}
|
|
161
|
+
|
|
162
|
+
response = self._client.request(
|
|
163
|
+
method,
|
|
164
|
+
path,
|
|
165
|
+
json=body if body is not None else None,
|
|
166
|
+
params=cleaned_query,
|
|
167
|
+
headers=req_headers,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if not response.is_success:
|
|
171
|
+
try:
|
|
172
|
+
err_body = response.json()
|
|
173
|
+
except Exception:
|
|
174
|
+
err_body = {}
|
|
175
|
+
raise error_from_response(
|
|
176
|
+
response.status_code,
|
|
177
|
+
err_body if isinstance(err_body, dict) else {},
|
|
178
|
+
dict(response.headers),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if response.status_code == 204:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return response.json()
|
|
185
|
+
|
|
186
|
+
def close(self) -> None:
|
|
187
|
+
"""Close the underlying HTTP connection pool."""
|
|
188
|
+
self._client.close()
|
|
189
|
+
|
|
190
|
+
# -- private helpers -------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _ensure_token(self) -> None:
|
|
193
|
+
if not self._agent_credentials:
|
|
194
|
+
return
|
|
195
|
+
if self._token and time.time() < self._token_expires_at - _REFRESH_BUFFER_SECS:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
with self._refresh_lock:
|
|
199
|
+
if self._token and time.time() < self._token_expires_at - _REFRESH_BUFFER_SECS:
|
|
200
|
+
return
|
|
201
|
+
self._refresh_agent_token()
|
|
202
|
+
|
|
203
|
+
def _refresh_agent_token(self) -> None:
|
|
204
|
+
assert self._agent_credentials is not None
|
|
205
|
+
payload: dict[str, Any] = {"api_key": self._agent_credentials["api_key"]}
|
|
206
|
+
if self._agent_credentials.get("agent_id"):
|
|
207
|
+
payload["agent_id"] = self._agent_credentials["agent_id"]
|
|
208
|
+
|
|
209
|
+
response = self._client.post(
|
|
210
|
+
"/v1/auth/agent-token",
|
|
211
|
+
json=payload,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if not response.is_success:
|
|
215
|
+
raise OneclawError(
|
|
216
|
+
f"Agent token refresh failed: HTTP {response.status_code}",
|
|
217
|
+
response.status_code,
|
|
218
|
+
"auth_error",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
data = response.json()
|
|
222
|
+
self._token = data["access_token"]
|
|
223
|
+
self._token_expires_at = time.time() + data.get("expires_in", 3600)
|
|
224
|
+
|
|
225
|
+
if data.get("agent_id"):
|
|
226
|
+
self._resolved_agent_id = data["agent_id"]
|
|
227
|
+
if self._agent_credentials and not self._agent_credentials.get("agent_id"):
|
|
228
|
+
self._agent_credentials["agent_id"] = data["agent_id"]
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _decode_expiry(jwt: str) -> float:
|
|
232
|
+
try:
|
|
233
|
+
parts = jwt.split(".")
|
|
234
|
+
if len(parts) != 3:
|
|
235
|
+
return 0
|
|
236
|
+
padding = 4 - len(parts[1]) % 4
|
|
237
|
+
padded = parts[1] + "=" * padding
|
|
238
|
+
payload = json.loads(base64.urlsafe_b64decode(padded))
|
|
239
|
+
exp = payload.get("exp")
|
|
240
|
+
return float(exp) if isinstance(exp, (int, float)) else 0
|
|
241
|
+
except Exception:
|
|
242
|
+
return 0
|
oneclaw/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Resource modules for the 1Claw Python SDK."""
|
|
2
|
+
|
|
3
|
+
from oneclaw.resources.agents import AgentsResource
|
|
4
|
+
from oneclaw.resources.api_keys import ApiKeysResource
|
|
5
|
+
from oneclaw.resources.approvals import ApprovalsResource
|
|
6
|
+
from oneclaw.resources.audit import AuditResource
|
|
7
|
+
from oneclaw.resources.auth import AuthResource
|
|
8
|
+
from oneclaw.resources.billing import BillingResource
|
|
9
|
+
from oneclaw.resources.chains import ChainsResource
|
|
10
|
+
from oneclaw.resources.org import OrgResource
|
|
11
|
+
from oneclaw.resources.platform import PlatformResource
|
|
12
|
+
from oneclaw.resources.policies import PoliciesResource
|
|
13
|
+
from oneclaw.resources.risk import RiskResource
|
|
14
|
+
from oneclaw.resources.secrets import SecretsResource
|
|
15
|
+
from oneclaw.resources.sharing import SharingResource
|
|
16
|
+
from oneclaw.resources.signing_keys import SigningKeysResource
|
|
17
|
+
from oneclaw.resources.treasury import TreasuryResource
|
|
18
|
+
from oneclaw.resources.treasury_wallets import TreasuryWalletsResource
|
|
19
|
+
from oneclaw.resources.vaults import VaultResource
|
|
20
|
+
from oneclaw.resources.webhooks import WebhooksResource
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AuthResource",
|
|
24
|
+
"VaultResource",
|
|
25
|
+
"SecretsResource",
|
|
26
|
+
"AgentsResource",
|
|
27
|
+
"PoliciesResource",
|
|
28
|
+
"ChainsResource",
|
|
29
|
+
"SharingResource",
|
|
30
|
+
"BillingResource",
|
|
31
|
+
"AuditResource",
|
|
32
|
+
"OrgResource",
|
|
33
|
+
"ApiKeysResource",
|
|
34
|
+
"SigningKeysResource",
|
|
35
|
+
"TreasuryResource",
|
|
36
|
+
"TreasuryWalletsResource",
|
|
37
|
+
"PlatformResource",
|
|
38
|
+
"ApprovalsResource",
|
|
39
|
+
"WebhooksResource",
|
|
40
|
+
"RiskResource",
|
|
41
|
+
]
|