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.
@@ -0,0 +1,12 @@
1
+ # Build & packaging artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ *.egg
6
+
7
+ # Caches
8
+ __pycache__/
9
+ *.py[cod]
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
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
@@ -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('/')}"
@@ -0,0 +1,4 @@
1
+ """Single source of truth for the package version."""
2
+ from __future__ import annotations
3
+
4
+ __version__ = "0.1.0"