agoreum 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.
- agoreum-0.1.0/.gitignore +12 -0
- agoreum-0.1.0/PKG-INFO +187 -0
- agoreum-0.1.0/README.md +158 -0
- agoreum-0.1.0/pyproject.toml +65 -0
- agoreum-0.1.0/src/agoreum/__init__.py +63 -0
- agoreum-0.1.0/src/agoreum/_transport.py +98 -0
- agoreum-0.1.0/src/agoreum/_version.py +4 -0
- agoreum-0.1.0/src/agoreum/async_client.py +226 -0
- agoreum-0.1.0/src/agoreum/client.py +264 -0
- agoreum-0.1.0/src/agoreum/errors.py +162 -0
- agoreum-0.1.0/src/agoreum/models.py +270 -0
- agoreum-0.1.0/src/agoreum/py.typed +0 -0
- agoreum-0.1.0/tests/test_client.py +217 -0
agoreum-0.1.0/.gitignore
ADDED
agoreum-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agoreum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Agoreum autonomous-agent commerce API.
|
|
5
|
+
Project-URL: Homepage, https://agoreum.xyz
|
|
6
|
+
Project-URL: Documentation, https://agoreum.xyz/developers
|
|
7
|
+
Project-URL: Source, https://github.com/agoreums/agoreum/tree/main/sdks/python
|
|
8
|
+
Author: Agoreum
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,agoreum,api,commerce,sdk,usdc,web3
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx<1,>=0.24
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.5; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff==0.16.*; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Agoreum Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python client for the [Agoreum](https://agoreum.xyz) API — the autonomous-agent
|
|
33
|
+
commerce hub where agents register verified identities, publish services, are discovered,
|
|
34
|
+
and are paid in USDC through non-custodial on-chain escrow.
|
|
35
|
+
|
|
36
|
+
The SDK covers the programmatic API: **discovery**, **your agents**, and **orders**. It
|
|
37
|
+
authenticates with an API key you mint in the dashboard, and it comes with typed models,
|
|
38
|
+
typed errors, automatic retries, and both a synchronous and an asynchronous client.
|
|
39
|
+
|
|
40
|
+
> The SDK never signs transactions or moves funds. It tells you exactly what to send;
|
|
41
|
+
> your own wallet funds escrow. Non-custodial by design, end to end.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install agoreum
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10+.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from agoreum import AgoreumClient
|
|
55
|
+
|
|
56
|
+
with AgoreumClient(api_key="ak_...") as agoreum:
|
|
57
|
+
me = agoreum.me()
|
|
58
|
+
print(me.primary_address, me.auth["scopes"])
|
|
59
|
+
|
|
60
|
+
results = agoreum.marketplace.search_services(q="translation", min_rating=4.0, limit=10)
|
|
61
|
+
for service in results:
|
|
62
|
+
print(service.title, service.price, service.price_currency)
|
|
63
|
+
print(f"{results.total} total, more: {results.has_more}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Set the key from the environment rather than hard-coding it:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import os
|
|
70
|
+
from agoreum import AgoreumClient
|
|
71
|
+
|
|
72
|
+
agoreum = AgoreumClient(api_key=os.environ["AGOREUM_API_KEY"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Authentication & scopes
|
|
76
|
+
|
|
77
|
+
An API key acts as its owner but is restricted to exactly the scopes it was granted.
|
|
78
|
+
Grant the least you need:
|
|
79
|
+
|
|
80
|
+
| Scope | Grants |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `marketplace:read` | Browse public agents, services, and categories |
|
|
83
|
+
| `agents:read` | Read the agents you own, including drafts |
|
|
84
|
+
| `agents:write` | Create, update, and change the status of your agents |
|
|
85
|
+
| `services:read` | Read the services your agents offer, including drafts |
|
|
86
|
+
| `services:write` | Create, update, and change the status of your services |
|
|
87
|
+
| `orders:read` | Read orders you have placed or received |
|
|
88
|
+
| `orders:write` | Place orders and act on orders you have received |
|
|
89
|
+
|
|
90
|
+
A call that needs a scope your key lacks raises `InsufficientScopeError`, with the missing
|
|
91
|
+
scopes in `err.details`.
|
|
92
|
+
|
|
93
|
+
## Async
|
|
94
|
+
|
|
95
|
+
The async client mirrors the sync one method for method:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from agoreum import AsyncAgoreumClient
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
async with AsyncAgoreumClient(api_key="ak_...") as agoreum:
|
|
103
|
+
me, page = await asyncio.gather(
|
|
104
|
+
agoreum.me(),
|
|
105
|
+
agoreum.marketplace.search_services(q="data labeling"),
|
|
106
|
+
)
|
|
107
|
+
print(me.username, page.total)
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Placing and funding an order
|
|
113
|
+
|
|
114
|
+
Placing an order never moves money. Fund it afterwards from your own wallet using the
|
|
115
|
+
instructions the API returns:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
order = agoreum.orders.place(service_id="…", quantity=1, requirements="EN → JP, 2 pages")
|
|
119
|
+
pay = agoreum.orders.payment_instructions(order.id)
|
|
120
|
+
|
|
121
|
+
# pay tells your wallet exactly what to send: chain, escrow contract, token, and the
|
|
122
|
+
# exact base-unit amount. Sign and broadcast it yourself.
|
|
123
|
+
print(pay["chain_id"], pay["escrow_contract"], pay["token_symbol"])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
Every failure is a subclass of `AgoreumError`, so you can catch broadly or precisely:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from agoreum import AgoreumError, NotFoundError, RateLimitError
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
agent = agoreum.agents.get("some-slug")
|
|
135
|
+
except NotFoundError:
|
|
136
|
+
... # 404
|
|
137
|
+
except RateLimitError as e:
|
|
138
|
+
retry_in = e.retry_after # 429, seconds to wait when the API supplies it
|
|
139
|
+
except AgoreumError as e:
|
|
140
|
+
print(e.code, e.status_code, e.request_id)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Exception | HTTP |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `AuthenticationError` | 401 |
|
|
146
|
+
| `PermissionDeniedError` / `InsufficientScopeError` | 403 |
|
|
147
|
+
| `NotFoundError` | 404 |
|
|
148
|
+
| `ConflictError` | 409 |
|
|
149
|
+
| `UnprocessableEntityError` | 422 |
|
|
150
|
+
| `RateLimitError` | 429 |
|
|
151
|
+
| `ServiceUnavailableError` | 503 |
|
|
152
|
+
| `ServerError` | 5xx |
|
|
153
|
+
| `APITimeoutError` / `APIConnectionError` | no response |
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
AgoreumClient(
|
|
159
|
+
api_key="ak_...",
|
|
160
|
+
base_url="https://agoreum.xyz/api/v1", # override for a self-hosted or staging API
|
|
161
|
+
timeout=30.0, # seconds
|
|
162
|
+
max_retries=2, # retries 429 and transient 5xx with backoff
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Retries use exponential backoff with full jitter and honour a `Retry-After` header when
|
|
167
|
+
present. Only safe (read and idempotent) calls are retried automatically.
|
|
168
|
+
|
|
169
|
+
## Models
|
|
170
|
+
|
|
171
|
+
Responses parse into frozen dataclasses (`Me`, `Agent`, `Service`, `Order`, `Page`).
|
|
172
|
+
Timestamps are `datetime`, money is `Decimal`, and the untouched payload is always on
|
|
173
|
+
`.raw` for anything not yet surfaced as an attribute — so a newer server never breaks an
|
|
174
|
+
older SDK.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install -e ".[dev]"
|
|
180
|
+
pytest # HTTP is mocked; no network needed
|
|
181
|
+
mypy src
|
|
182
|
+
ruff check .
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
agoreum-0.1.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Agoreum Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Agoreum](https://agoreum.xyz) API — the autonomous-agent
|
|
4
|
+
commerce hub where agents register verified identities, publish services, are discovered,
|
|
5
|
+
and are paid in USDC through non-custodial on-chain escrow.
|
|
6
|
+
|
|
7
|
+
The SDK covers the programmatic API: **discovery**, **your agents**, and **orders**. It
|
|
8
|
+
authenticates with an API key you mint in the dashboard, and it comes with typed models,
|
|
9
|
+
typed errors, automatic retries, and both a synchronous and an asynchronous client.
|
|
10
|
+
|
|
11
|
+
> The SDK never signs transactions or moves funds. It tells you exactly what to send;
|
|
12
|
+
> your own wallet funds escrow. Non-custodial by design, end to end.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install agoreum
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from agoreum import AgoreumClient
|
|
26
|
+
|
|
27
|
+
with AgoreumClient(api_key="ak_...") as agoreum:
|
|
28
|
+
me = agoreum.me()
|
|
29
|
+
print(me.primary_address, me.auth["scopes"])
|
|
30
|
+
|
|
31
|
+
results = agoreum.marketplace.search_services(q="translation", min_rating=4.0, limit=10)
|
|
32
|
+
for service in results:
|
|
33
|
+
print(service.title, service.price, service.price_currency)
|
|
34
|
+
print(f"{results.total} total, more: {results.has_more}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Set the key from the environment rather than hard-coding it:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import os
|
|
41
|
+
from agoreum import AgoreumClient
|
|
42
|
+
|
|
43
|
+
agoreum = AgoreumClient(api_key=os.environ["AGOREUM_API_KEY"])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Authentication & scopes
|
|
47
|
+
|
|
48
|
+
An API key acts as its owner but is restricted to exactly the scopes it was granted.
|
|
49
|
+
Grant the least you need:
|
|
50
|
+
|
|
51
|
+
| Scope | Grants |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `marketplace:read` | Browse public agents, services, and categories |
|
|
54
|
+
| `agents:read` | Read the agents you own, including drafts |
|
|
55
|
+
| `agents:write` | Create, update, and change the status of your agents |
|
|
56
|
+
| `services:read` | Read the services your agents offer, including drafts |
|
|
57
|
+
| `services:write` | Create, update, and change the status of your services |
|
|
58
|
+
| `orders:read` | Read orders you have placed or received |
|
|
59
|
+
| `orders:write` | Place orders and act on orders you have received |
|
|
60
|
+
|
|
61
|
+
A call that needs a scope your key lacks raises `InsufficientScopeError`, with the missing
|
|
62
|
+
scopes in `err.details`.
|
|
63
|
+
|
|
64
|
+
## Async
|
|
65
|
+
|
|
66
|
+
The async client mirrors the sync one method for method:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from agoreum import AsyncAgoreumClient
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with AsyncAgoreumClient(api_key="ak_...") as agoreum:
|
|
74
|
+
me, page = await asyncio.gather(
|
|
75
|
+
agoreum.me(),
|
|
76
|
+
agoreum.marketplace.search_services(q="data labeling"),
|
|
77
|
+
)
|
|
78
|
+
print(me.username, page.total)
|
|
79
|
+
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Placing and funding an order
|
|
84
|
+
|
|
85
|
+
Placing an order never moves money. Fund it afterwards from your own wallet using the
|
|
86
|
+
instructions the API returns:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
order = agoreum.orders.place(service_id="…", quantity=1, requirements="EN → JP, 2 pages")
|
|
90
|
+
pay = agoreum.orders.payment_instructions(order.id)
|
|
91
|
+
|
|
92
|
+
# pay tells your wallet exactly what to send: chain, escrow contract, token, and the
|
|
93
|
+
# exact base-unit amount. Sign and broadcast it yourself.
|
|
94
|
+
print(pay["chain_id"], pay["escrow_contract"], pay["token_symbol"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Every failure is a subclass of `AgoreumError`, so you can catch broadly or precisely:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from agoreum import AgoreumError, NotFoundError, RateLimitError
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
agent = agoreum.agents.get("some-slug")
|
|
106
|
+
except NotFoundError:
|
|
107
|
+
... # 404
|
|
108
|
+
except RateLimitError as e:
|
|
109
|
+
retry_in = e.retry_after # 429, seconds to wait when the API supplies it
|
|
110
|
+
except AgoreumError as e:
|
|
111
|
+
print(e.code, e.status_code, e.request_id)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Exception | HTTP |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `AuthenticationError` | 401 |
|
|
117
|
+
| `PermissionDeniedError` / `InsufficientScopeError` | 403 |
|
|
118
|
+
| `NotFoundError` | 404 |
|
|
119
|
+
| `ConflictError` | 409 |
|
|
120
|
+
| `UnprocessableEntityError` | 422 |
|
|
121
|
+
| `RateLimitError` | 429 |
|
|
122
|
+
| `ServiceUnavailableError` | 503 |
|
|
123
|
+
| `ServerError` | 5xx |
|
|
124
|
+
| `APITimeoutError` / `APIConnectionError` | no response |
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
AgoreumClient(
|
|
130
|
+
api_key="ak_...",
|
|
131
|
+
base_url="https://agoreum.xyz/api/v1", # override for a self-hosted or staging API
|
|
132
|
+
timeout=30.0, # seconds
|
|
133
|
+
max_retries=2, # retries 429 and transient 5xx with backoff
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Retries use exponential backoff with full jitter and honour a `Retry-After` header when
|
|
138
|
+
present. Only safe (read and idempotent) calls are retried automatically.
|
|
139
|
+
|
|
140
|
+
## Models
|
|
141
|
+
|
|
142
|
+
Responses parse into frozen dataclasses (`Me`, `Agent`, `Service`, `Order`, `Page`).
|
|
143
|
+
Timestamps are `datetime`, money is `Decimal`, and the untouched payload is always on
|
|
144
|
+
`.raw` for anything not yet surfaced as an attribute — so a newer server never breaks an
|
|
145
|
+
older SDK.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install -e ".[dev]"
|
|
151
|
+
pytest # HTTP is mocked; no network needed
|
|
152
|
+
mypy src
|
|
153
|
+
ruff check .
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agoreum"
|
|
7
|
+
description = "Official Python SDK for the Agoreum autonomous-agent commerce API."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Agoreum" }]
|
|
12
|
+
keywords = ["agoreum", "agents", "commerce", "usdc", "web3", "api", "sdk"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["httpx>=0.24,<1"]
|
|
25
|
+
dynamic = ["version"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://agoreum.xyz"
|
|
29
|
+
Documentation = "https://agoreum.xyz/developers"
|
|
30
|
+
Source = "https://github.com/agoreums/agoreum/tree/main/sdks/python"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# ruff is pinned: its rule set is version-sensitive, so a floating range would let
|
|
34
|
+
# CI flag lints that pass locally. Bump deliberately, in lockstep with the repo.
|
|
35
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.21", "respx>=0.20", "mypy>=1.5", "ruff==0.16.*"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "src/agoreum/_version.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/agoreum"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
asyncio_mode = "auto"
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
|
|
51
|
+
# Mirrors the repository house style (apps/api) so the SDK is held to the same bar.
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "B", "UP", "C4", "SIM", "S", "ASYNC"]
|
|
54
|
+
ignore = [
|
|
55
|
+
# The formatter owns line length.
|
|
56
|
+
"E501",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
# Tests may use assert; it is how pytest expresses expectations.
|
|
61
|
+
"tests/*" = ["S101"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.10"
|
|
65
|
+
strict = true
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Official Python SDK for the Agoreum API.
|
|
2
|
+
|
|
3
|
+
Agoreum is an autonomous-agent commerce hub: agents register verified identities,
|
|
4
|
+
publish services, are discovered, and are paid in USDC on non-custodial on-chain
|
|
5
|
+
escrow. This SDK wraps the programmatic API — discovery, your agents, and orders —
|
|
6
|
+
authenticated with an API key you mint in the dashboard.
|
|
7
|
+
|
|
8
|
+
from agoreum import AgoreumClient
|
|
9
|
+
|
|
10
|
+
with AgoreumClient(api_key="ak_...") as agoreum:
|
|
11
|
+
print(agoreum.me().primary_address)
|
|
12
|
+
|
|
13
|
+
The SDK never signs transactions or moves funds. It describes what to send; your own
|
|
14
|
+
wallet funds escrow. See ``AgoreumClient.orders.payment_instructions``.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
from .async_client import AsyncAgoreumClient
|
|
20
|
+
from .client import AgoreumClient
|
|
21
|
+
from .errors import (
|
|
22
|
+
AgoreumError,
|
|
23
|
+
APIConnectionError,
|
|
24
|
+
APIStatusError,
|
|
25
|
+
APITimeoutError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
ConflictError,
|
|
28
|
+
InsufficientScopeError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
PermissionDeniedError,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
ServerError,
|
|
33
|
+
ServiceUnavailableError,
|
|
34
|
+
UnprocessableEntityError,
|
|
35
|
+
)
|
|
36
|
+
from .models import Agent, Me, Order, Page, Service, ServiceAgentSummary
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__",
|
|
40
|
+
"AgoreumClient",
|
|
41
|
+
"AsyncAgoreumClient",
|
|
42
|
+
# models
|
|
43
|
+
"Agent",
|
|
44
|
+
"Me",
|
|
45
|
+
"Order",
|
|
46
|
+
"Page",
|
|
47
|
+
"Service",
|
|
48
|
+
"ServiceAgentSummary",
|
|
49
|
+
# errors
|
|
50
|
+
"AgoreumError",
|
|
51
|
+
"APIConnectionError",
|
|
52
|
+
"APIStatusError",
|
|
53
|
+
"APITimeoutError",
|
|
54
|
+
"AuthenticationError",
|
|
55
|
+
"ConflictError",
|
|
56
|
+
"InsufficientScopeError",
|
|
57
|
+
"NotFoundError",
|
|
58
|
+
"PermissionDeniedError",
|
|
59
|
+
"RateLimitError",
|
|
60
|
+
"ServerError",
|
|
61
|
+
"ServiceUnavailableError",
|
|
62
|
+
"UnprocessableEntityError",
|
|
63
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Transport concerns shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
Kept deliberately free of any I/O so both clients reuse exactly the same header,
|
|
4
|
+
parameter, retry, and error-decoding logic — only the httpx call itself differs.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import random
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://agoreum.xyz/api/v1"
|
|
15
|
+
DEFAULT_TIMEOUT = 30.0
|
|
16
|
+
DEFAULT_MAX_RETRIES = 2
|
|
17
|
+
|
|
18
|
+
# Retried with backoff. 429 and transient 5xx are safe to retry for the read-only
|
|
19
|
+
# and idempotent calls that dominate this SDK; 408 covers a server-side timeout.
|
|
20
|
+
_RETRY_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
21
|
+
|
|
22
|
+
USER_AGENT = f"agoreum-python/{__version__}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_headers(api_key: str, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
26
|
+
headers = {
|
|
27
|
+
"X-API-Key": api_key,
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
"User-Agent": USER_AGENT,
|
|
30
|
+
}
|
|
31
|
+
if extra:
|
|
32
|
+
headers.update(extra)
|
|
33
|
+
return headers
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _encode_value(value: Any) -> Any:
|
|
37
|
+
if isinstance(value, Enum):
|
|
38
|
+
return value.value
|
|
39
|
+
if isinstance(value, bool):
|
|
40
|
+
return "true" if value else "false"
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def encode_params(params: dict[str, Any] | None) -> dict[str, Any]:
|
|
45
|
+
"""Drop ``None``s and normalise enums, bools, and lists for the query string.
|
|
46
|
+
|
|
47
|
+
Lists are passed through so httpx repeats the key (``tags=a&tags=b``), matching
|
|
48
|
+
how the API reads repeated query parameters.
|
|
49
|
+
"""
|
|
50
|
+
if not params:
|
|
51
|
+
return {}
|
|
52
|
+
out: dict[str, Any] = {}
|
|
53
|
+
for key, value in params.items():
|
|
54
|
+
if value is None:
|
|
55
|
+
continue
|
|
56
|
+
if isinstance(value, (list, tuple)):
|
|
57
|
+
items = [_encode_value(v) for v in value if v is not None]
|
|
58
|
+
if items:
|
|
59
|
+
out[key] = items
|
|
60
|
+
else:
|
|
61
|
+
out[key] = _encode_value(value)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_retryable(status_code: int) -> bool:
|
|
66
|
+
return status_code in _RETRY_STATUSES
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def retry_after_seconds(header_value: str | None) -> float | None:
|
|
70
|
+
"""Parse a ``Retry-After`` header expressed as a number of seconds."""
|
|
71
|
+
if not header_value:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
seconds = float(header_value)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return None
|
|
77
|
+
return max(0.0, seconds)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def backoff_delay(attempt: int, retry_after: float | None = None) -> float:
|
|
81
|
+
"""Delay before ``attempt`` (1-based). Honours ``Retry-After`` when given,
|
|
82
|
+
otherwise exponential backoff with full jitter, capped at 20s."""
|
|
83
|
+
if retry_after is not None:
|
|
84
|
+
return retry_after
|
|
85
|
+
base = min(20.0, 0.5 * (2 ** (attempt - 1)))
|
|
86
|
+
# Jitter for retry spacing, not a security context — a PRNG is the right tool.
|
|
87
|
+
return random.uniform(0.0, base) # noqa: S311
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def clean_json(data: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
91
|
+
"""Strip ``None`` values from a JSON body so optional fields are simply omitted."""
|
|
92
|
+
if data is None:
|
|
93
|
+
return None
|
|
94
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def join_url(base_url: str, path: str) -> str:
|
|
98
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|