agentref 1.0.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.
agentref/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ from .client import AgentRef, AsyncAgentRef
2
+ from .errors import (
3
+ AgentRefError,
4
+ AuthError,
5
+ ConflictError,
6
+ ForbiddenError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ ValidationError,
11
+ )
12
+
13
+ __all__ = [
14
+ "AgentRef",
15
+ "AsyncAgentRef",
16
+ "AgentRefError",
17
+ "AuthError",
18
+ "ForbiddenError",
19
+ "ValidationError",
20
+ "NotFoundError",
21
+ "ConflictError",
22
+ "RateLimitError",
23
+ "ServerError",
24
+ ]
agentref/_http.py ADDED
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import importlib.metadata
5
+ import os
6
+ import time
7
+ from email.utils import parsedate_to_datetime
8
+ from typing import Any, Dict, Mapping, Optional, Set, cast
9
+
10
+ import httpx
11
+
12
+ from .errors import (
13
+ AgentRefError,
14
+ AuthError,
15
+ ConflictError,
16
+ ForbiddenError,
17
+ NotFoundError,
18
+ RateLimitError,
19
+ ServerError,
20
+ ValidationError,
21
+ )
22
+
23
+ _SAFE_METHODS: Set[str] = {"GET", "HEAD"}
24
+ _DEFAULT_BASE_URL = "https://www.agentref.dev/api/v1"
25
+ _DEFAULT_TIMEOUT = 30.0
26
+ _DEFAULT_MAX_RETRIES = 2
27
+
28
+
29
+ def _sdk_version() -> str:
30
+ try:
31
+ return importlib.metadata.version("agentref")
32
+ except importlib.metadata.PackageNotFoundError:
33
+ return "unknown"
34
+
35
+
36
+ def _parse_retry_after_seconds(value: Optional[str]) -> int:
37
+ if not value:
38
+ return 60
39
+
40
+ try:
41
+ parsed = float(value)
42
+ if parsed >= 0:
43
+ return int(parsed) if parsed.is_integer() else int(parsed) + 1
44
+ except ValueError:
45
+ pass
46
+
47
+ try:
48
+ parsed_date = parsedate_to_datetime(value)
49
+ delta = parsed_date.timestamp() - time.time()
50
+ if delta <= 0:
51
+ return 0
52
+ return int(delta) if float(delta).is_integer() else int(delta) + 1
53
+ except (TypeError, ValueError, OverflowError):
54
+ return 60
55
+
56
+
57
+ def _can_retry(method: str, idempotency_key: Optional[str]) -> bool:
58
+ upper = method.upper()
59
+ return upper in _SAFE_METHODS or (upper == "POST" and idempotency_key is not None)
60
+
61
+
62
+ def _json_object(response: httpx.Response) -> Dict[str, Any]:
63
+ try:
64
+ payload = response.json()
65
+ except ValueError:
66
+ return {}
67
+
68
+ if isinstance(payload, dict):
69
+ return cast(Dict[str, Any], payload)
70
+
71
+ return {}
72
+
73
+
74
+ class _BaseHttpClient:
75
+ def __init__(
76
+ self,
77
+ *,
78
+ api_key: Optional[str] = None,
79
+ base_url: str = _DEFAULT_BASE_URL,
80
+ timeout: float = _DEFAULT_TIMEOUT,
81
+ max_retries: int = _DEFAULT_MAX_RETRIES,
82
+ ) -> None:
83
+ resolved_key = api_key or os.environ.get("AGENTREF_API_KEY")
84
+ if not resolved_key:
85
+ raise ValueError(
86
+ "[AgentRef] API key is required. Pass it as api_key or set AGENTREF_API_KEY environment variable."
87
+ )
88
+
89
+ self._api_key = resolved_key
90
+ self._base_url = base_url.rstrip("/")
91
+ self._timeout = timeout
92
+ self._max_retries = max_retries
93
+ self._user_agent = f"agentref-python/{_sdk_version()}"
94
+
95
+ def _headers(self, method: str, idempotency_key: Optional[str]) -> Dict[str, str]:
96
+ headers: Dict[str, str] = {
97
+ "Authorization": f"Bearer {self._api_key}",
98
+ "Content-Type": "application/json",
99
+ "User-Agent": self._user_agent,
100
+ }
101
+
102
+ if method.upper() == "POST" and idempotency_key is not None:
103
+ headers["Idempotency-Key"] = idempotency_key
104
+
105
+ return headers
106
+
107
+ def _parse_error(self, response: httpx.Response) -> AgentRefError:
108
+ payload = _json_object(response)
109
+ raw_error = payload.get("error")
110
+ raw_meta = payload.get("meta")
111
+
112
+ error = raw_error if isinstance(raw_error, dict) else {}
113
+ meta = raw_meta if isinstance(raw_meta, dict) else {}
114
+
115
+ code = cast(str, error.get("code", "UNKNOWN_ERROR"))
116
+ message = cast(str, error.get("message", response.reason_phrase))
117
+ request_id = cast(str, meta.get("requestId", ""))
118
+ details = error.get("details")
119
+
120
+ if response.status_code == 400:
121
+ return ValidationError(message, code, request_id, details)
122
+ if response.status_code == 401:
123
+ return AuthError(message, code, request_id)
124
+ if response.status_code == 403:
125
+ return ForbiddenError(message, code, request_id)
126
+ if response.status_code == 404:
127
+ return NotFoundError(message, code, request_id)
128
+ if response.status_code == 409:
129
+ return ConflictError(message, code, request_id)
130
+ if response.status_code == 429:
131
+ retry_after = _parse_retry_after_seconds(response.headers.get("Retry-After"))
132
+ return RateLimitError(message, code, request_id, retry_after)
133
+
134
+ return ServerError(message, code, response.status_code, request_id)
135
+
136
+ @staticmethod
137
+ def _is_retryable(status: int) -> bool:
138
+ return status == 429 or status >= 500
139
+
140
+ @staticmethod
141
+ def _backoff_seconds(attempt: int) -> float:
142
+ return float(0.5 * (2 ** attempt))
143
+
144
+
145
+ class SyncHttpClient(_BaseHttpClient):
146
+ def __init__(self, **kwargs: Any) -> None:
147
+ super().__init__(**kwargs)
148
+ self._client = httpx.Client(base_url=self._base_url, timeout=self._timeout)
149
+
150
+ def close(self) -> None:
151
+ self._client.close()
152
+
153
+ def request(
154
+ self,
155
+ method: str,
156
+ path: str,
157
+ *,
158
+ params: Optional[Mapping[str, Any]] = None,
159
+ json: Optional[Dict[str, Any]] = None,
160
+ idempotency_key: Optional[str] = None,
161
+ ) -> Dict[str, Any]:
162
+ can_retry = _can_retry(method, idempotency_key)
163
+ attempts = self._max_retries + 1 if can_retry else 1
164
+
165
+ for attempt in range(attempts):
166
+ try:
167
+ response = self._client.request(
168
+ method,
169
+ path,
170
+ params={k: v for k, v in (params or {}).items() if v is not None},
171
+ json=json,
172
+ headers=self._headers(method, idempotency_key),
173
+ )
174
+ except httpx.HTTPError:
175
+ if can_retry and attempt < attempts - 1:
176
+ time.sleep(self._backoff_seconds(attempt))
177
+ continue
178
+ raise
179
+
180
+ if response.is_error:
181
+ parsed = self._parse_error(response)
182
+ if can_retry and self._is_retryable(response.status_code) and attempt < attempts - 1:
183
+ delay = (
184
+ float(_parse_retry_after_seconds(response.headers.get("Retry-After")))
185
+ if response.status_code == 429
186
+ else self._backoff_seconds(attempt)
187
+ )
188
+ time.sleep(delay)
189
+ continue
190
+ raise parsed
191
+
192
+ return _json_object(response)
193
+
194
+ raise ServerError("Request failed after retries", "REQUEST_RETRY_EXHAUSTED", 500, "")
195
+
196
+
197
+ class AsyncHttpClient(_BaseHttpClient):
198
+ def __init__(self, **kwargs: Any) -> None:
199
+ super().__init__(**kwargs)
200
+ self._client = httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout)
201
+
202
+ async def __aenter__(self) -> "AsyncHttpClient":
203
+ return self
204
+
205
+ async def __aexit__(self, *args: Any) -> None:
206
+ await self._client.aclose()
207
+
208
+ async def aclose(self) -> None:
209
+ await self._client.aclose()
210
+
211
+ async def request(
212
+ self,
213
+ method: str,
214
+ path: str,
215
+ *,
216
+ params: Optional[Mapping[str, Any]] = None,
217
+ json: Optional[Dict[str, Any]] = None,
218
+ idempotency_key: Optional[str] = None,
219
+ ) -> Dict[str, Any]:
220
+ can_retry = _can_retry(method, idempotency_key)
221
+ attempts = self._max_retries + 1 if can_retry else 1
222
+
223
+ for attempt in range(attempts):
224
+ try:
225
+ response = await self._client.request(
226
+ method,
227
+ path,
228
+ params={k: v for k, v in (params or {}).items() if v is not None},
229
+ json=json,
230
+ headers=self._headers(method, idempotency_key),
231
+ )
232
+ except httpx.HTTPError:
233
+ if can_retry and attempt < attempts - 1:
234
+ await asyncio.sleep(self._backoff_seconds(attempt))
235
+ continue
236
+ raise
237
+
238
+ if response.is_error:
239
+ parsed = self._parse_error(response)
240
+ if can_retry and self._is_retryable(response.status_code) and attempt < attempts - 1:
241
+ delay = (
242
+ float(_parse_retry_after_seconds(response.headers.get("Retry-After")))
243
+ if response.status_code == 429
244
+ else self._backoff_seconds(attempt)
245
+ )
246
+ await asyncio.sleep(delay)
247
+ continue
248
+ raise parsed
249
+
250
+ return _json_object(response)
251
+
252
+ raise ServerError("Request failed after retries", "REQUEST_RETRY_EXHAUSTED", 500, "")
agentref/client.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from ._http import AsyncHttpClient, SyncHttpClient
6
+ from .resources import (
7
+ AffiliatesResource,
8
+ AsyncAffiliatesResource,
9
+ AsyncBillingResource,
10
+ AsyncConversionsResource,
11
+ AsyncFlagsResource,
12
+ AsyncMerchantResource,
13
+ AsyncPayoutsResource,
14
+ AsyncProgramsResource,
15
+ BillingResource,
16
+ ConversionsResource,
17
+ FlagsResource,
18
+ MerchantResource,
19
+ PayoutsResource,
20
+ ProgramsResource,
21
+ )
22
+
23
+
24
+ class AgentRef:
25
+ def __init__(
26
+ self,
27
+ api_key: Optional[str] = None,
28
+ *,
29
+ base_url: str = "https://www.agentref.dev/api/v1",
30
+ timeout: float = 30.0,
31
+ max_retries: int = 2,
32
+ ) -> None:
33
+ self._http = SyncHttpClient(
34
+ api_key=api_key,
35
+ base_url=base_url,
36
+ timeout=timeout,
37
+ max_retries=max_retries,
38
+ )
39
+
40
+ self.programs = ProgramsResource(self._http)
41
+ self.affiliates = AffiliatesResource(self._http)
42
+ self.conversions = ConversionsResource(self._http)
43
+ self.payouts = PayoutsResource(self._http)
44
+ self.flags = FlagsResource(self._http)
45
+ self.billing = BillingResource(self._http)
46
+ self.merchant = MerchantResource(self._http)
47
+
48
+ def close(self) -> None:
49
+ self._http.close()
50
+
51
+
52
+ class AsyncAgentRef:
53
+ """Async variant — use as async context manager for connection pooling."""
54
+
55
+ def __init__(
56
+ self,
57
+ api_key: Optional[str] = None,
58
+ *,
59
+ base_url: str = "https://www.agentref.dev/api/v1",
60
+ timeout: float = 30.0,
61
+ max_retries: int = 2,
62
+ ) -> None:
63
+ self._http = AsyncHttpClient(
64
+ api_key=api_key,
65
+ base_url=base_url,
66
+ timeout=timeout,
67
+ max_retries=max_retries,
68
+ )
69
+
70
+ self.programs = AsyncProgramsResource(self._http)
71
+ self.affiliates = AsyncAffiliatesResource(self._http)
72
+ self.conversions = AsyncConversionsResource(self._http)
73
+ self.payouts = AsyncPayoutsResource(self._http)
74
+ self.flags = AsyncFlagsResource(self._http)
75
+ self.billing = AsyncBillingResource(self._http)
76
+ self.merchant = AsyncMerchantResource(self._http)
77
+
78
+ async def __aenter__(self) -> "AsyncAgentRef":
79
+ await self._http.__aenter__()
80
+ return self
81
+
82
+ async def __aexit__(self, *args: Any) -> None:
83
+ await self._http.__aexit__(*args)
agentref/errors.py ADDED
@@ -0,0 +1,62 @@
1
+ from typing import Optional
2
+
3
+
4
+ class AgentRefError(Exception):
5
+ code: str
6
+ status: int
7
+ request_id: str
8
+
9
+ def __init__(self, message: str, code: str, status: int, request_id: str) -> None:
10
+ super().__init__(message)
11
+ self.code = code
12
+ self.status = status
13
+ self.request_id = request_id
14
+
15
+
16
+ class AuthError(AgentRefError):
17
+ def __init__(self, message: str, code: str, request_id: str) -> None:
18
+ super().__init__(message, code, 401, request_id)
19
+
20
+
21
+ class ForbiddenError(AgentRefError):
22
+ """403 — authenticated but not authorized: wrong scope, ownerType, or trust level."""
23
+
24
+ def __init__(self, message: str, code: str, request_id: str) -> None:
25
+ super().__init__(message, code, 403, request_id)
26
+
27
+
28
+ class ValidationError(AgentRefError):
29
+ details: Optional[object]
30
+
31
+ def __init__(
32
+ self,
33
+ message: str,
34
+ code: str,
35
+ request_id: str,
36
+ details: Optional[object] = None,
37
+ ) -> None:
38
+ super().__init__(message, code, 400, request_id)
39
+ self.details = details
40
+
41
+
42
+ class NotFoundError(AgentRefError):
43
+ def __init__(self, message: str, code: str, request_id: str) -> None:
44
+ super().__init__(message, code, 404, request_id)
45
+
46
+
47
+ class ConflictError(AgentRefError):
48
+ def __init__(self, message: str, code: str, request_id: str) -> None:
49
+ super().__init__(message, code, 409, request_id)
50
+
51
+
52
+ class RateLimitError(AgentRefError):
53
+ retry_after: int
54
+
55
+ def __init__(self, message: str, code: str, request_id: str, retry_after: int) -> None:
56
+ super().__init__(message, code, 429, request_id)
57
+ self.retry_after = retry_after
58
+
59
+
60
+ class ServerError(AgentRefError):
61
+ def __init__(self, message: str, code: str, status: int, request_id: str) -> None:
62
+ super().__init__(message, code, status, request_id)
@@ -0,0 +1,24 @@
1
+ from .affiliates import AffiliatesResource, AsyncAffiliatesResource
2
+ from .billing import AsyncBillingResource, BillingResource
3
+ from .conversions import AsyncConversionsResource, ConversionsResource
4
+ from .flags import AsyncFlagsResource, FlagsResource
5
+ from .merchant import AsyncMerchantResource, MerchantResource
6
+ from .payouts import AsyncPayoutsResource, PayoutsResource
7
+ from .programs import AsyncProgramsResource, ProgramsResource
8
+
9
+ __all__ = [
10
+ "ProgramsResource",
11
+ "AffiliatesResource",
12
+ "ConversionsResource",
13
+ "PayoutsResource",
14
+ "FlagsResource",
15
+ "BillingResource",
16
+ "MerchantResource",
17
+ "AsyncProgramsResource",
18
+ "AsyncAffiliatesResource",
19
+ "AsyncConversionsResource",
20
+ "AsyncPayoutsResource",
21
+ "AsyncFlagsResource",
22
+ "AsyncBillingResource",
23
+ "AsyncMerchantResource",
24
+ ]
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from .._http import AsyncHttpClient, SyncHttpClient
6
+ from ..types.models import Affiliate, PaginatedResponse
7
+
8
+
9
+ class AffiliatesResource:
10
+ def __init__(self, http: SyncHttpClient) -> None:
11
+ self._http = http
12
+
13
+ def list(
14
+ self,
15
+ *,
16
+ program_id: Optional[str] = None,
17
+ include_blocked: Optional[bool] = None,
18
+ cursor: Optional[str] = None,
19
+ limit: Optional[int] = None,
20
+ page: Optional[int] = None,
21
+ page_size: Optional[int] = None,
22
+ offset: Optional[int] = None,
23
+ ) -> PaginatedResponse[Affiliate]:
24
+ envelope = self._http.request(
25
+ "GET",
26
+ "/affiliates",
27
+ params={
28
+ "programId": program_id,
29
+ "includeBlocked": include_blocked,
30
+ "cursor": cursor,
31
+ "limit": limit,
32
+ "page": page,
33
+ "pageSize": page_size,
34
+ "offset": offset,
35
+ },
36
+ )
37
+ return PaginatedResponse[Affiliate].model_validate(envelope)
38
+
39
+ def get(self, id: str) -> Affiliate:
40
+ envelope = self._http.request("GET", f"/affiliates/{id}")
41
+ return Affiliate.model_validate(envelope["data"])
42
+
43
+ def approve(self, id: str, *, idempotency_key: Optional[str] = None) -> Affiliate:
44
+ envelope = self._http.request("POST", f"/affiliates/{id}/approve", idempotency_key=idempotency_key)
45
+ return Affiliate.model_validate(envelope["data"])
46
+
47
+ def block(
48
+ self,
49
+ id: str,
50
+ *,
51
+ reason: Optional[str] = None,
52
+ idempotency_key: Optional[str] = None,
53
+ ) -> Affiliate:
54
+ envelope = self._http.request(
55
+ "POST",
56
+ f"/affiliates/{id}/block",
57
+ json={"reason": reason} if reason is not None else None,
58
+ idempotency_key=idempotency_key,
59
+ )
60
+ return Affiliate.model_validate(envelope["data"])
61
+
62
+ def unblock(self, id: str, *, idempotency_key: Optional[str] = None) -> Affiliate:
63
+ envelope = self._http.request("POST", f"/affiliates/{id}/unblock", idempotency_key=idempotency_key)
64
+ return Affiliate.model_validate(envelope["data"])
65
+
66
+
67
+ class AsyncAffiliatesResource:
68
+ def __init__(self, http: AsyncHttpClient) -> None:
69
+ self._http = http
70
+
71
+ async def list(
72
+ self,
73
+ *,
74
+ program_id: Optional[str] = None,
75
+ include_blocked: Optional[bool] = None,
76
+ cursor: Optional[str] = None,
77
+ limit: Optional[int] = None,
78
+ page: Optional[int] = None,
79
+ page_size: Optional[int] = None,
80
+ offset: Optional[int] = None,
81
+ ) -> PaginatedResponse[Affiliate]:
82
+ envelope = await self._http.request(
83
+ "GET",
84
+ "/affiliates",
85
+ params={
86
+ "programId": program_id,
87
+ "includeBlocked": include_blocked,
88
+ "cursor": cursor,
89
+ "limit": limit,
90
+ "page": page,
91
+ "pageSize": page_size,
92
+ "offset": offset,
93
+ },
94
+ )
95
+ return PaginatedResponse[Affiliate].model_validate(envelope)
96
+
97
+ async def get(self, id: str) -> Affiliate:
98
+ envelope = await self._http.request("GET", f"/affiliates/{id}")
99
+ return Affiliate.model_validate(envelope["data"])
100
+
101
+ async def approve(self, id: str, *, idempotency_key: Optional[str] = None) -> Affiliate:
102
+ envelope = await self._http.request("POST", f"/affiliates/{id}/approve", idempotency_key=idempotency_key)
103
+ return Affiliate.model_validate(envelope["data"])
104
+
105
+ async def block(
106
+ self,
107
+ id: str,
108
+ *,
109
+ reason: Optional[str] = None,
110
+ idempotency_key: Optional[str] = None,
111
+ ) -> Affiliate:
112
+ envelope = await self._http.request(
113
+ "POST",
114
+ f"/affiliates/{id}/block",
115
+ json={"reason": reason} if reason is not None else None,
116
+ idempotency_key=idempotency_key,
117
+ )
118
+ return Affiliate.model_validate(envelope["data"])
119
+
120
+ async def unblock(self, id: str, *, idempotency_key: Optional[str] = None) -> Affiliate:
121
+ envelope = await self._http.request("POST", f"/affiliates/{id}/unblock", idempotency_key=idempotency_key)
122
+ return Affiliate.model_validate(envelope["data"])
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Literal, Optional
4
+
5
+ from .._http import AsyncHttpClient, SyncHttpClient
6
+ from ..types.models import BillingStatus, BillingTier
7
+
8
+
9
+ class BillingResource:
10
+ def __init__(self, http: SyncHttpClient) -> None:
11
+ self._http = http
12
+
13
+ def current(self) -> BillingStatus:
14
+ envelope = self._http.request("GET", "/billing")
15
+ return BillingStatus.model_validate(envelope["data"])
16
+
17
+ def tiers(self) -> List[BillingTier]:
18
+ envelope = self._http.request("GET", "/billing/tiers")
19
+ raw = envelope.get("data", [])
20
+ if not isinstance(raw, list):
21
+ return []
22
+ return [BillingTier.model_validate(item) for item in raw]
23
+
24
+ def subscribe(
25
+ self,
26
+ *,
27
+ tier: Literal["starter", "growth", "pro", "scale"],
28
+ idempotency_key: Optional[str] = None,
29
+ ) -> BillingStatus:
30
+ envelope = self._http.request(
31
+ "POST",
32
+ "/billing/subscribe",
33
+ json={"tier": tier},
34
+ idempotency_key=idempotency_key,
35
+ )
36
+ return BillingStatus.model_validate(envelope["data"])
37
+
38
+
39
+ class AsyncBillingResource:
40
+ def __init__(self, http: AsyncHttpClient) -> None:
41
+ self._http = http
42
+
43
+ async def current(self) -> BillingStatus:
44
+ envelope = await self._http.request("GET", "/billing")
45
+ return BillingStatus.model_validate(envelope["data"])
46
+
47
+ async def tiers(self) -> List[BillingTier]:
48
+ envelope = await self._http.request("GET", "/billing/tiers")
49
+ raw = envelope.get("data", [])
50
+ if not isinstance(raw, list):
51
+ return []
52
+ return [BillingTier.model_validate(item) for item in raw]
53
+
54
+ async def subscribe(
55
+ self,
56
+ *,
57
+ tier: Literal["starter", "growth", "pro", "scale"],
58
+ idempotency_key: Optional[str] = None,
59
+ ) -> BillingStatus:
60
+ envelope = await self._http.request(
61
+ "POST",
62
+ "/billing/subscribe",
63
+ json={"tier": tier},
64
+ idempotency_key=idempotency_key,
65
+ )
66
+ return BillingStatus.model_validate(envelope["data"])