agentref 1.0.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.
- agentref-1.0.0/.github/workflows/ci.yml +24 -0
- agentref-1.0.0/.github/workflows/publish.yml +24 -0
- agentref-1.0.0/.gitignore +8 -0
- agentref-1.0.0/CHANGELOG.md +8 -0
- agentref-1.0.0/PKG-INFO +101 -0
- agentref-1.0.0/README.md +84 -0
- agentref-1.0.0/agentref/__init__.py +24 -0
- agentref-1.0.0/agentref/_http.py +252 -0
- agentref-1.0.0/agentref/client.py +83 -0
- agentref-1.0.0/agentref/errors.py +62 -0
- agentref-1.0.0/agentref/resources/__init__.py +24 -0
- agentref-1.0.0/agentref/resources/affiliates.py +122 -0
- agentref-1.0.0/agentref/resources/billing.py +66 -0
- agentref-1.0.0/agentref/resources/conversions.py +124 -0
- agentref-1.0.0/agentref/resources/flags.py +132 -0
- agentref-1.0.0/agentref/resources/merchant.py +34 -0
- agentref-1.0.0/agentref/resources/payouts.py +150 -0
- agentref-1.0.0/agentref/resources/programs.py +320 -0
- agentref-1.0.0/agentref/types/__init__.py +39 -0
- agentref-1.0.0/agentref/types/models.py +210 -0
- agentref-1.0.0/pyproject.toml +35 -0
- agentref-1.0.0/tests/test_async.py +93 -0
- agentref-1.0.0/tests/test_errors.py +78 -0
- agentref-1.0.0/tests/test_http.py +163 -0
- agentref-1.0.0/tests/test_programs.py +98 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ['3.9', '3.11', '3.12']
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: python -m pip install --upgrade pip
|
|
21
|
+
- run: pip install -e .[dev]
|
|
22
|
+
- run: mypy agentref
|
|
23
|
+
- run: pytest -v
|
|
24
|
+
- run: python -m build
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.12'
|
|
19
|
+
- run: python -m pip install --upgrade pip
|
|
20
|
+
- run: pip install -e .[dev]
|
|
21
|
+
- run: mypy agentref
|
|
22
|
+
- run: pytest -v
|
|
23
|
+
- run: python -m build
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
agentref-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentref
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the AgentRef Affiliate API
|
|
5
|
+
Author-email: AgentRef <hi@agentref.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: mypy>=1.9.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# AgentRef Python SDK
|
|
19
|
+
|
|
20
|
+
Official Python SDK for the AgentRef REST API v1.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install agentref
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from agentref import AgentRef
|
|
32
|
+
|
|
33
|
+
client = AgentRef(api_key="ak_live_...")
|
|
34
|
+
programs = client.programs.list()
|
|
35
|
+
print(programs.meta.request_id)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Async quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from agentref import AsyncAgentRef
|
|
42
|
+
|
|
43
|
+
async with AsyncAgentRef(api_key="ak_live_...") as client:
|
|
44
|
+
programs = await client.programs.list()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Authentication
|
|
48
|
+
|
|
49
|
+
- Uses `Authorization: Bearer <key>`.
|
|
50
|
+
- Supports `ak_live_*`, `ak_aff_*`, `ak_onb_*`.
|
|
51
|
+
- Provide `api_key` directly or set `AGENTREF_API_KEY`.
|
|
52
|
+
|
|
53
|
+
## Resources
|
|
54
|
+
|
|
55
|
+
- `client.programs`: `list`, `list_all`, `get`, `create`, `update`, `delete`, `stats`, `list_affiliates`, `list_coupons`, `create_coupon`, `create_invite`
|
|
56
|
+
- `client.affiliates`: `list`, `get`, `approve`, `block`, `unblock`
|
|
57
|
+
- `client.conversions`: `list`, `stats`, `recent`
|
|
58
|
+
- `client.payouts`: `list`, `list_pending`, `stats`
|
|
59
|
+
- `client.flags`: `list`, `stats`, `resolve`
|
|
60
|
+
- `client.billing`: `current`, `tiers`, `subscribe`
|
|
61
|
+
- `client.merchant`: `get`, `domain_status`
|
|
62
|
+
|
|
63
|
+
## Pagination
|
|
64
|
+
|
|
65
|
+
List endpoints return `PaginatedResponse[T]` with:
|
|
66
|
+
|
|
67
|
+
- `meta.total`
|
|
68
|
+
- `meta.page`
|
|
69
|
+
- `meta.page_size`
|
|
70
|
+
- `meta.has_more`
|
|
71
|
+
- `meta.next_cursor`
|
|
72
|
+
- `meta.request_id`
|
|
73
|
+
|
|
74
|
+
Auto-pagination (`list_all`) stops on `has_more is False`.
|
|
75
|
+
|
|
76
|
+
## Idempotency and retry behavior
|
|
77
|
+
|
|
78
|
+
- GET/HEAD: auto-retry on 429/5xx.
|
|
79
|
+
- POST: auto-retry only when `idempotency_key` is provided.
|
|
80
|
+
- PATCH/DELETE: never auto-retry.
|
|
81
|
+
- `Idempotency-Key` header is sent only for POST requests.
|
|
82
|
+
|
|
83
|
+
## Error handling
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from agentref import AgentRef
|
|
87
|
+
from agentref.errors import ForbiddenError, NotFoundError, RateLimitError, AgentRefError
|
|
88
|
+
|
|
89
|
+
client = AgentRef(api_key="ak_live_...")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
client.programs.get("missing-id")
|
|
93
|
+
except ForbiddenError as e:
|
|
94
|
+
print(e.code, e.request_id)
|
|
95
|
+
except NotFoundError as e:
|
|
96
|
+
print(e.request_id)
|
|
97
|
+
except RateLimitError as e:
|
|
98
|
+
print(e.retry_after)
|
|
99
|
+
except AgentRefError as e:
|
|
100
|
+
print(e.status)
|
|
101
|
+
```
|
agentref-1.0.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# AgentRef Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the AgentRef REST API v1.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentref
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentref import AgentRef
|
|
15
|
+
|
|
16
|
+
client = AgentRef(api_key="ak_live_...")
|
|
17
|
+
programs = client.programs.list()
|
|
18
|
+
print(programs.meta.request_id)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Async quickstart
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from agentref import AsyncAgentRef
|
|
25
|
+
|
|
26
|
+
async with AsyncAgentRef(api_key="ak_live_...") as client:
|
|
27
|
+
programs = await client.programs.list()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
- Uses `Authorization: Bearer <key>`.
|
|
33
|
+
- Supports `ak_live_*`, `ak_aff_*`, `ak_onb_*`.
|
|
34
|
+
- Provide `api_key` directly or set `AGENTREF_API_KEY`.
|
|
35
|
+
|
|
36
|
+
## Resources
|
|
37
|
+
|
|
38
|
+
- `client.programs`: `list`, `list_all`, `get`, `create`, `update`, `delete`, `stats`, `list_affiliates`, `list_coupons`, `create_coupon`, `create_invite`
|
|
39
|
+
- `client.affiliates`: `list`, `get`, `approve`, `block`, `unblock`
|
|
40
|
+
- `client.conversions`: `list`, `stats`, `recent`
|
|
41
|
+
- `client.payouts`: `list`, `list_pending`, `stats`
|
|
42
|
+
- `client.flags`: `list`, `stats`, `resolve`
|
|
43
|
+
- `client.billing`: `current`, `tiers`, `subscribe`
|
|
44
|
+
- `client.merchant`: `get`, `domain_status`
|
|
45
|
+
|
|
46
|
+
## Pagination
|
|
47
|
+
|
|
48
|
+
List endpoints return `PaginatedResponse[T]` with:
|
|
49
|
+
|
|
50
|
+
- `meta.total`
|
|
51
|
+
- `meta.page`
|
|
52
|
+
- `meta.page_size`
|
|
53
|
+
- `meta.has_more`
|
|
54
|
+
- `meta.next_cursor`
|
|
55
|
+
- `meta.request_id`
|
|
56
|
+
|
|
57
|
+
Auto-pagination (`list_all`) stops on `has_more is False`.
|
|
58
|
+
|
|
59
|
+
## Idempotency and retry behavior
|
|
60
|
+
|
|
61
|
+
- GET/HEAD: auto-retry on 429/5xx.
|
|
62
|
+
- POST: auto-retry only when `idempotency_key` is provided.
|
|
63
|
+
- PATCH/DELETE: never auto-retry.
|
|
64
|
+
- `Idempotency-Key` header is sent only for POST requests.
|
|
65
|
+
|
|
66
|
+
## Error handling
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from agentref import AgentRef
|
|
70
|
+
from agentref.errors import ForbiddenError, NotFoundError, RateLimitError, AgentRefError
|
|
71
|
+
|
|
72
|
+
client = AgentRef(api_key="ak_live_...")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client.programs.get("missing-id")
|
|
76
|
+
except ForbiddenError as e:
|
|
77
|
+
print(e.code, e.request_id)
|
|
78
|
+
except NotFoundError as e:
|
|
79
|
+
print(e.request_id)
|
|
80
|
+
except RateLimitError as e:
|
|
81
|
+
print(e.retry_after)
|
|
82
|
+
except AgentRefError as e:
|
|
83
|
+
print(e.status)
|
|
84
|
+
```
|
|
@@ -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
|
+
]
|
|
@@ -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, "")
|
|
@@ -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)
|
|
@@ -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
|
+
]
|