synchronity-sdk 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.
- synchronity_sdk-0.1.0/PKG-INFO +181 -0
- synchronity_sdk-0.1.0/README.md +167 -0
- synchronity_sdk-0.1.0/agentmesh/__init__.py +62 -0
- synchronity_sdk-0.1.0/agentmesh/client.py +44 -0
- synchronity_sdk-0.1.0/agentmesh/config.py +18 -0
- synchronity_sdk-0.1.0/agentmesh/errors.py +97 -0
- synchronity_sdk-0.1.0/agentmesh/http.py +139 -0
- synchronity_sdk-0.1.0/agentmesh/modules/__init__.py +14 -0
- synchronity_sdk-0.1.0/agentmesh/modules/cart.py +51 -0
- synchronity_sdk-0.1.0/agentmesh/modules/checkout.py +49 -0
- synchronity_sdk-0.1.0/agentmesh/modules/discovery.py +42 -0
- synchronity_sdk-0.1.0/agentmesh/modules/orders.py +36 -0
- synchronity_sdk-0.1.0/agentmesh/modules/products.py +73 -0
- synchronity_sdk-0.1.0/agentmesh/tools/__init__.py +6 -0
- synchronity_sdk-0.1.0/agentmesh/tools/_defs.py +195 -0
- synchronity_sdk-0.1.0/agentmesh/tools/anthropic.py +13 -0
- synchronity_sdk-0.1.0/agentmesh/tools/executor.py +107 -0
- synchronity_sdk-0.1.0/agentmesh/tools/langchain.py +13 -0
- synchronity_sdk-0.1.0/agentmesh/tools/openai.py +16 -0
- synchronity_sdk-0.1.0/agentmesh/types/__init__.py +57 -0
- synchronity_sdk-0.1.0/agentmesh/types/amps.py +234 -0
- synchronity_sdk-0.1.0/pyproject.toml +30 -0
- synchronity_sdk-0.1.0/setup.cfg +4 -0
- synchronity_sdk-0.1.0/synchronity_sdk.egg-info/PKG-INFO +181 -0
- synchronity_sdk-0.1.0/synchronity_sdk.egg-info/SOURCES.txt +31 -0
- synchronity_sdk-0.1.0/synchronity_sdk.egg-info/dependency_links.txt +1 -0
- synchronity_sdk-0.1.0/synchronity_sdk.egg-info/requires.txt +8 -0
- synchronity_sdk-0.1.0/synchronity_sdk.egg-info/top_level.txt +1 -0
- synchronity_sdk-0.1.0/tests/test_cart.py +128 -0
- synchronity_sdk-0.1.0/tests/test_checkout.py +102 -0
- synchronity_sdk-0.1.0/tests/test_client.py +64 -0
- synchronity_sdk-0.1.0/tests/test_errors.py +240 -0
- synchronity_sdk-0.1.0/tests/test_products.py +125 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: synchronity-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the AgentMesh agentic commerce infrastructure
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
13
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# Synchronity Python SDK
|
|
16
|
+
|
|
17
|
+
Python SDK for the [Synchronity](https://agentmesh.ai) agentic commerce infrastructure — makes any e-commerce site transactable by autonomous AI agents via authenticated REST APIs.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install agentmesh-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with dev dependencies for testing:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install "agentmesh-sdk[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio
|
|
35
|
+
from agentmesh import SynchronityClient, SynchronityConfig
|
|
36
|
+
|
|
37
|
+
async def main():
|
|
38
|
+
config = SynchronityConfig(agent_token="your-agent-token")
|
|
39
|
+
|
|
40
|
+
async with SynchronityClient(config) as client:
|
|
41
|
+
# Search products on a site
|
|
42
|
+
results = await client.products.search("site_abc123", q="laptop", in_stock=True)
|
|
43
|
+
print(results["products"])
|
|
44
|
+
|
|
45
|
+
# Create a cart and add an item
|
|
46
|
+
cart = await client.cart.create("site_abc123", currency="USD")
|
|
47
|
+
cart = await client.cart.add_item("site_abc123", cart.cart_id, "prod_42", quantity=1)
|
|
48
|
+
|
|
49
|
+
# Apply a coupon
|
|
50
|
+
cart = await client.cart.apply_coupon("site_abc123", cart.cart_id, "SAVE10")
|
|
51
|
+
|
|
52
|
+
# Execute checkout
|
|
53
|
+
from agentmesh import ShippingAddressParams
|
|
54
|
+
order = await client.checkout.execute(
|
|
55
|
+
site_id="site_abc123",
|
|
56
|
+
cart_id=cart.cart_id,
|
|
57
|
+
buyer_delegation_token="buyer-token-from-auth-flow",
|
|
58
|
+
shipping_address=ShippingAddressParams(
|
|
59
|
+
name="Jane Smith",
|
|
60
|
+
line1="123 Main St",
|
|
61
|
+
city="San Francisco",
|
|
62
|
+
state="CA",
|
|
63
|
+
postal_code="94102",
|
|
64
|
+
country="US",
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
print(order.order_id, order.status)
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Tools for LLM Frameworks
|
|
73
|
+
|
|
74
|
+
The SDK ships ready-to-use tool definitions for Anthropic, OpenAI, and LangChain.
|
|
75
|
+
|
|
76
|
+
### Anthropic
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agentmesh.tools import anthropic_tools, ToolExecutor
|
|
80
|
+
from agentmesh import SynchronityClient, SynchronityConfig
|
|
81
|
+
import anthropic
|
|
82
|
+
|
|
83
|
+
client = SynchronityClient(SynchronityConfig(agent_token="your-token"))
|
|
84
|
+
executor = ToolExecutor(client)
|
|
85
|
+
|
|
86
|
+
anthropic_client = anthropic.Anthropic()
|
|
87
|
+
response = anthropic_client.messages.create(
|
|
88
|
+
model="claude-opus-4-5",
|
|
89
|
+
max_tokens=1024,
|
|
90
|
+
tools=anthropic_tools,
|
|
91
|
+
messages=[{"role": "user", "content": "Find me a red laptop under $1000 on site site_abc123"}],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Execute tool calls
|
|
95
|
+
import asyncio
|
|
96
|
+
for block in response.content:
|
|
97
|
+
if block.type == "tool_use":
|
|
98
|
+
result = asyncio.run(executor.execute(block.name, block.input))
|
|
99
|
+
print(result)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### OpenAI
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from agentmesh.tools import openai_tools, ToolExecutor
|
|
106
|
+
from openai import OpenAI
|
|
107
|
+
|
|
108
|
+
openai_client = OpenAI()
|
|
109
|
+
response = openai_client.chat.completions.create(
|
|
110
|
+
model="gpt-4o",
|
|
111
|
+
tools=openai_tools,
|
|
112
|
+
messages=[{"role": "user", "content": "Search for laptops on site_abc123"}],
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### LangChain
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from agentmesh.tools import langchain_tools
|
|
120
|
+
# langchain_tools is a list of dicts with name, description, and schema
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Available Tools (11)
|
|
124
|
+
|
|
125
|
+
| Tool | Description |
|
|
126
|
+
|------|-------------|
|
|
127
|
+
| `search_products` | Search products on a site with filters |
|
|
128
|
+
| `get_product` | Get full product details by ID |
|
|
129
|
+
| `compare_products` | Compare products across multiple sites |
|
|
130
|
+
| `create_cart` | Create a new shopping cart |
|
|
131
|
+
| `add_to_cart` | Add a product/variant to a cart |
|
|
132
|
+
| `remove_from_cart` | Remove a line item from a cart |
|
|
133
|
+
| `apply_coupon` | Apply a discount code to a cart |
|
|
134
|
+
| `get_cart` | Get current cart contents |
|
|
135
|
+
| `execute_checkout` | Execute checkout and return a confirmed order |
|
|
136
|
+
| `get_order` | Get order details by ID |
|
|
137
|
+
| `list_orders` | List orders with optional status filter |
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from agentmesh import SynchronityConfig
|
|
143
|
+
|
|
144
|
+
config = SynchronityConfig(
|
|
145
|
+
agent_token="your-agent-token", # required
|
|
146
|
+
base_url="https://gateway.agentmesh.ai", # default
|
|
147
|
+
timeout=30.0, # seconds
|
|
148
|
+
retries=3, # auto-retry on 429 and 5xx
|
|
149
|
+
retry_delay=1.0, # base delay for exponential backoff
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Error Handling
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from agentmesh.errors import (
|
|
157
|
+
AuthenticationError, # 401
|
|
158
|
+
AuthorizationError, # 403
|
|
159
|
+
NotFoundError, # 404 / 410
|
|
160
|
+
ValidationError, # 400
|
|
161
|
+
RateLimitError, # 429 — has .retry_after attribute
|
|
162
|
+
ConnectorError, # 422
|
|
163
|
+
NetworkError, # network failures
|
|
164
|
+
TimeoutError, # request timeout
|
|
165
|
+
SynchronityError, # base class
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
product = await client.products.get_by_id("site_abc", "prod_999")
|
|
170
|
+
except NotFoundError as e:
|
|
171
|
+
print(f"Product not found: {e.code}")
|
|
172
|
+
except RateLimitError as e:
|
|
173
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Running Tests
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install -e ".[dev]"
|
|
180
|
+
pytest tests/ -v
|
|
181
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Synchronity Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Synchronity](https://agentmesh.ai) agentic commerce infrastructure — makes any e-commerce site transactable by autonomous AI agents via authenticated REST APIs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentmesh-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with dev dependencies for testing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "agentmesh-sdk[dev]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from agentmesh import SynchronityClient, SynchronityConfig
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
config = SynchronityConfig(agent_token="your-agent-token")
|
|
25
|
+
|
|
26
|
+
async with SynchronityClient(config) as client:
|
|
27
|
+
# Search products on a site
|
|
28
|
+
results = await client.products.search("site_abc123", q="laptop", in_stock=True)
|
|
29
|
+
print(results["products"])
|
|
30
|
+
|
|
31
|
+
# Create a cart and add an item
|
|
32
|
+
cart = await client.cart.create("site_abc123", currency="USD")
|
|
33
|
+
cart = await client.cart.add_item("site_abc123", cart.cart_id, "prod_42", quantity=1)
|
|
34
|
+
|
|
35
|
+
# Apply a coupon
|
|
36
|
+
cart = await client.cart.apply_coupon("site_abc123", cart.cart_id, "SAVE10")
|
|
37
|
+
|
|
38
|
+
# Execute checkout
|
|
39
|
+
from agentmesh import ShippingAddressParams
|
|
40
|
+
order = await client.checkout.execute(
|
|
41
|
+
site_id="site_abc123",
|
|
42
|
+
cart_id=cart.cart_id,
|
|
43
|
+
buyer_delegation_token="buyer-token-from-auth-flow",
|
|
44
|
+
shipping_address=ShippingAddressParams(
|
|
45
|
+
name="Jane Smith",
|
|
46
|
+
line1="123 Main St",
|
|
47
|
+
city="San Francisco",
|
|
48
|
+
state="CA",
|
|
49
|
+
postal_code="94102",
|
|
50
|
+
country="US",
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
print(order.order_id, order.status)
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tools for LLM Frameworks
|
|
59
|
+
|
|
60
|
+
The SDK ships ready-to-use tool definitions for Anthropic, OpenAI, and LangChain.
|
|
61
|
+
|
|
62
|
+
### Anthropic
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from agentmesh.tools import anthropic_tools, ToolExecutor
|
|
66
|
+
from agentmesh import SynchronityClient, SynchronityConfig
|
|
67
|
+
import anthropic
|
|
68
|
+
|
|
69
|
+
client = SynchronityClient(SynchronityConfig(agent_token="your-token"))
|
|
70
|
+
executor = ToolExecutor(client)
|
|
71
|
+
|
|
72
|
+
anthropic_client = anthropic.Anthropic()
|
|
73
|
+
response = anthropic_client.messages.create(
|
|
74
|
+
model="claude-opus-4-5",
|
|
75
|
+
max_tokens=1024,
|
|
76
|
+
tools=anthropic_tools,
|
|
77
|
+
messages=[{"role": "user", "content": "Find me a red laptop under $1000 on site site_abc123"}],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Execute tool calls
|
|
81
|
+
import asyncio
|
|
82
|
+
for block in response.content:
|
|
83
|
+
if block.type == "tool_use":
|
|
84
|
+
result = asyncio.run(executor.execute(block.name, block.input))
|
|
85
|
+
print(result)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### OpenAI
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from agentmesh.tools import openai_tools, ToolExecutor
|
|
92
|
+
from openai import OpenAI
|
|
93
|
+
|
|
94
|
+
openai_client = OpenAI()
|
|
95
|
+
response = openai_client.chat.completions.create(
|
|
96
|
+
model="gpt-4o",
|
|
97
|
+
tools=openai_tools,
|
|
98
|
+
messages=[{"role": "user", "content": "Search for laptops on site_abc123"}],
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### LangChain
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from agentmesh.tools import langchain_tools
|
|
106
|
+
# langchain_tools is a list of dicts with name, description, and schema
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Available Tools (11)
|
|
110
|
+
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `search_products` | Search products on a site with filters |
|
|
114
|
+
| `get_product` | Get full product details by ID |
|
|
115
|
+
| `compare_products` | Compare products across multiple sites |
|
|
116
|
+
| `create_cart` | Create a new shopping cart |
|
|
117
|
+
| `add_to_cart` | Add a product/variant to a cart |
|
|
118
|
+
| `remove_from_cart` | Remove a line item from a cart |
|
|
119
|
+
| `apply_coupon` | Apply a discount code to a cart |
|
|
120
|
+
| `get_cart` | Get current cart contents |
|
|
121
|
+
| `execute_checkout` | Execute checkout and return a confirmed order |
|
|
122
|
+
| `get_order` | Get order details by ID |
|
|
123
|
+
| `list_orders` | List orders with optional status filter |
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from agentmesh import SynchronityConfig
|
|
129
|
+
|
|
130
|
+
config = SynchronityConfig(
|
|
131
|
+
agent_token="your-agent-token", # required
|
|
132
|
+
base_url="https://gateway.agentmesh.ai", # default
|
|
133
|
+
timeout=30.0, # seconds
|
|
134
|
+
retries=3, # auto-retry on 429 and 5xx
|
|
135
|
+
retry_delay=1.0, # base delay for exponential backoff
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from agentmesh.errors import (
|
|
143
|
+
AuthenticationError, # 401
|
|
144
|
+
AuthorizationError, # 403
|
|
145
|
+
NotFoundError, # 404 / 410
|
|
146
|
+
ValidationError, # 400
|
|
147
|
+
RateLimitError, # 429 — has .retry_after attribute
|
|
148
|
+
ConnectorError, # 422
|
|
149
|
+
NetworkError, # network failures
|
|
150
|
+
TimeoutError, # request timeout
|
|
151
|
+
SynchronityError, # base class
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
product = await client.products.get_by_id("site_abc", "prod_999")
|
|
156
|
+
except NotFoundError as e:
|
|
157
|
+
print(f"Product not found: {e.code}")
|
|
158
|
+
except RateLimitError as e:
|
|
159
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Running Tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pip install -e ".[dev]"
|
|
166
|
+
pytest tests/ -v
|
|
167
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .client import AgentMeshClient
|
|
2
|
+
from .config import AgentMeshConfig, SDK_VERSION
|
|
3
|
+
from .errors import (
|
|
4
|
+
AgentMeshError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
AuthorizationError,
|
|
7
|
+
ConnectorError,
|
|
8
|
+
NetworkError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .http import HttpClient
|
|
15
|
+
from .modules import (
|
|
16
|
+
CartModule,
|
|
17
|
+
CheckoutModule,
|
|
18
|
+
DiscoveryModule,
|
|
19
|
+
OrdersModule,
|
|
20
|
+
ProductsModule,
|
|
21
|
+
ShippingAddressParams,
|
|
22
|
+
)
|
|
23
|
+
from .types import (
|
|
24
|
+
AMPSCapabilityManifest,
|
|
25
|
+
AMPSCart,
|
|
26
|
+
AMPSOrder,
|
|
27
|
+
AMPSOrderStatus,
|
|
28
|
+
AMPSProduct,
|
|
29
|
+
AvailabilityStatus,
|
|
30
|
+
MonetaryAmount,
|
|
31
|
+
SiteListItem,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AgentMeshClient",
|
|
36
|
+
"AgentMeshConfig",
|
|
37
|
+
"SDK_VERSION",
|
|
38
|
+
"AgentMeshError",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"AuthorizationError",
|
|
41
|
+
"ConnectorError",
|
|
42
|
+
"NetworkError",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"RateLimitError",
|
|
45
|
+
"TimeoutError",
|
|
46
|
+
"ValidationError",
|
|
47
|
+
"HttpClient",
|
|
48
|
+
"CartModule",
|
|
49
|
+
"CheckoutModule",
|
|
50
|
+
"DiscoveryModule",
|
|
51
|
+
"OrdersModule",
|
|
52
|
+
"ProductsModule",
|
|
53
|
+
"ShippingAddressParams",
|
|
54
|
+
"AMPSCapabilityManifest",
|
|
55
|
+
"AMPSCart",
|
|
56
|
+
"AMPSOrder",
|
|
57
|
+
"AMPSOrderStatus",
|
|
58
|
+
"AMPSProduct",
|
|
59
|
+
"AvailabilityStatus",
|
|
60
|
+
"MonetaryAmount",
|
|
61
|
+
"SiteListItem",
|
|
62
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .config import AgentMeshConfig
|
|
6
|
+
from .http import HttpClient
|
|
7
|
+
from .modules.cart import CartModule
|
|
8
|
+
from .modules.checkout import CheckoutModule
|
|
9
|
+
from .modules.discovery import DiscoveryModule
|
|
10
|
+
from .modules.orders import OrdersModule
|
|
11
|
+
from .modules.products import ProductsModule
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentMeshClient:
|
|
15
|
+
def __init__(self, config: AgentMeshConfig) -> None:
|
|
16
|
+
self._http = HttpClient(config)
|
|
17
|
+
self.discovery = DiscoveryModule(self._http)
|
|
18
|
+
self.products = ProductsModule(self._http)
|
|
19
|
+
self.cart = CartModule(self._http)
|
|
20
|
+
self.checkout = CheckoutModule(self._http)
|
|
21
|
+
self.orders = OrdersModule(self._http)
|
|
22
|
+
|
|
23
|
+
async def __aenter__(self) -> "AgentMeshClient":
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
27
|
+
await self._http.close()
|
|
28
|
+
|
|
29
|
+
async def close(self) -> None:
|
|
30
|
+
await self._http.close()
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def with_http_client(
|
|
34
|
+
cls, config: AgentMeshConfig, http_client: HttpClient
|
|
35
|
+
) -> "AgentMeshClient":
|
|
36
|
+
"""Inject a pre-built HttpClient — useful for testing."""
|
|
37
|
+
instance = cls.__new__(cls)
|
|
38
|
+
instance._http = http_client
|
|
39
|
+
instance.discovery = DiscoveryModule(http_client)
|
|
40
|
+
instance.products = ProductsModule(http_client)
|
|
41
|
+
instance.cart = CartModule(http_client)
|
|
42
|
+
instance.checkout = CheckoutModule(http_client)
|
|
43
|
+
instance.orders = OrdersModule(http_client)
|
|
44
|
+
return instance
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
SDK_VERSION = "0.1.0"
|
|
6
|
+
DEFAULT_BASE_URL = "https://gateway.agentmesh.ai"
|
|
7
|
+
DEFAULT_TIMEOUT = 30.0
|
|
8
|
+
DEFAULT_RETRIES = 3
|
|
9
|
+
DEFAULT_RETRY_DELAY = 1.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AgentMeshConfig:
|
|
14
|
+
agent_token: str
|
|
15
|
+
base_url: str = DEFAULT_BASE_URL
|
|
16
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
17
|
+
retries: int = DEFAULT_RETRIES
|
|
18
|
+
retry_delay: float = DEFAULT_RETRY_DELAY
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgentMeshError(Exception):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
message: str,
|
|
9
|
+
code: str,
|
|
10
|
+
status_code: int,
|
|
11
|
+
request_id: Optional[str] = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.code = code
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.request_id = request_id
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthenticationError(AgentMeshError):
|
|
20
|
+
"""Raised on HTTP 401 responses."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthorizationError(AgentMeshError):
|
|
24
|
+
"""Raised on HTTP 403 responses."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotFoundError(AgentMeshError):
|
|
28
|
+
"""Raised on HTTP 404 / 410 responses."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValidationError(AgentMeshError):
|
|
32
|
+
"""Raised on HTTP 400 responses."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RateLimitError(AgentMeshError):
|
|
36
|
+
"""Raised on HTTP 429 responses."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
message: str,
|
|
41
|
+
code: str,
|
|
42
|
+
status_code: int,
|
|
43
|
+
request_id: Optional[str] = None,
|
|
44
|
+
retry_after: int = 60,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(message, code, status_code, request_id)
|
|
47
|
+
self.retry_after = retry_after
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConnectorError(AgentMeshError):
|
|
51
|
+
"""Raised on HTTP 422 responses (upstream connector failure)."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NetworkError(AgentMeshError):
|
|
55
|
+
"""Raised on network-level failures (connection errors, etc.)."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
58
|
+
super().__init__(message, "NETWORK_ERROR", 0, request_id)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TimeoutError(AgentMeshError):
|
|
62
|
+
"""Raised when a request times out."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
65
|
+
super().__init__(message, "TIMEOUT", 0, request_id)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_error_response(
|
|
69
|
+
status: int,
|
|
70
|
+
body: dict,
|
|
71
|
+
request_id: Optional[str] = None,
|
|
72
|
+
retry_after: Optional[int] = None,
|
|
73
|
+
) -> AgentMeshError:
|
|
74
|
+
error = body.get("error", {})
|
|
75
|
+
code = error.get("code", "UNKNOWN_ERROR")
|
|
76
|
+
message = error.get("message", f"HTTP {status}")
|
|
77
|
+
|
|
78
|
+
if status == 400:
|
|
79
|
+
return ValidationError(message, code, status, request_id)
|
|
80
|
+
elif status == 401:
|
|
81
|
+
return AuthenticationError(message, code, status, request_id)
|
|
82
|
+
elif status == 403:
|
|
83
|
+
return AuthorizationError(message, code, status, request_id)
|
|
84
|
+
elif status in (404, 410):
|
|
85
|
+
return NotFoundError(message, code, status, request_id)
|
|
86
|
+
elif status == 422:
|
|
87
|
+
return ConnectorError(message, code, status, request_id)
|
|
88
|
+
elif status == 429:
|
|
89
|
+
return RateLimitError(
|
|
90
|
+
message,
|
|
91
|
+
code,
|
|
92
|
+
status,
|
|
93
|
+
request_id,
|
|
94
|
+
retry_after=retry_after if retry_after is not None else 60,
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
return AgentMeshError(message, code, status, request_id)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .config import AgentMeshConfig, SDK_VERSION
|
|
9
|
+
from .errors import (
|
|
10
|
+
AgentMeshError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
parse_error_response,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpClient:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
config: AgentMeshConfig,
|
|
21
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._config = config
|
|
24
|
+
self._base_url = config.base_url.rstrip("/")
|
|
25
|
+
self._headers = {
|
|
26
|
+
"Authorization": f"Bearer {config.agent_token}",
|
|
27
|
+
"User-Agent": f"agentmesh-python/{SDK_VERSION}",
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
}
|
|
30
|
+
self._owned = http_client is None
|
|
31
|
+
self._client = http_client or httpx.AsyncClient(
|
|
32
|
+
timeout=config.timeout,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def __aenter__(self) -> "HttpClient":
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
39
|
+
await self.close()
|
|
40
|
+
|
|
41
|
+
async def close(self) -> None:
|
|
42
|
+
if self._owned:
|
|
43
|
+
await self._client.aclose()
|
|
44
|
+
|
|
45
|
+
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> dict:
|
|
46
|
+
return await self._request("GET", path, params=params)
|
|
47
|
+
|
|
48
|
+
async def post(self, path: str, body: Optional[dict] = None) -> dict:
|
|
49
|
+
return await self._request("POST", path, body=body)
|
|
50
|
+
|
|
51
|
+
async def delete(self, path: str) -> dict:
|
|
52
|
+
return await self._request("DELETE", path)
|
|
53
|
+
|
|
54
|
+
def _build_url(self, path: str) -> str:
|
|
55
|
+
return f"{self._base_url}{path}"
|
|
56
|
+
|
|
57
|
+
def _clean_params(
|
|
58
|
+
self, params: Optional[Dict[str, Any]]
|
|
59
|
+
) -> Optional[Dict[str, str]]:
|
|
60
|
+
if not params:
|
|
61
|
+
return None
|
|
62
|
+
result: Dict[str, str] = {}
|
|
63
|
+
for k, v in params.items():
|
|
64
|
+
if v is None:
|
|
65
|
+
continue
|
|
66
|
+
if isinstance(v, list):
|
|
67
|
+
result[k] = ",".join(str(i) for i in v)
|
|
68
|
+
else:
|
|
69
|
+
result[k] = str(v)
|
|
70
|
+
return result or None
|
|
71
|
+
|
|
72
|
+
async def _request(
|
|
73
|
+
self,
|
|
74
|
+
method: str,
|
|
75
|
+
path: str,
|
|
76
|
+
params: Optional[Dict[str, Any]] = None,
|
|
77
|
+
body: Optional[dict] = None,
|
|
78
|
+
attempt: int = 0,
|
|
79
|
+
) -> dict:
|
|
80
|
+
url = self._build_url(path)
|
|
81
|
+
cleaned_params = self._clean_params(params)
|
|
82
|
+
|
|
83
|
+
headers = dict(self._headers)
|
|
84
|
+
if body is not None:
|
|
85
|
+
headers["Content-Type"] = "application/json"
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
response = await self._client.request(
|
|
89
|
+
method,
|
|
90
|
+
url,
|
|
91
|
+
params=cleaned_params,
|
|
92
|
+
json=body,
|
|
93
|
+
headers=headers,
|
|
94
|
+
)
|
|
95
|
+
except httpx.TimeoutException as exc:
|
|
96
|
+
raise TimeoutError(f"Request timed out: {method} {path}") from exc
|
|
97
|
+
except httpx.RequestError as exc:
|
|
98
|
+
raise NetworkError(f"Network error: {method} {path}: {exc}") from exc
|
|
99
|
+
|
|
100
|
+
request_id = response.headers.get("X-AgentMesh-Request-Id")
|
|
101
|
+
|
|
102
|
+
if response.is_success:
|
|
103
|
+
if response.status_code == 204:
|
|
104
|
+
return {}
|
|
105
|
+
return response.json()
|
|
106
|
+
|
|
107
|
+
# Parse retry-after header for 429
|
|
108
|
+
retry_after: Optional[int] = None
|
|
109
|
+
raw_retry = response.headers.get("Retry-After")
|
|
110
|
+
if raw_retry is not None:
|
|
111
|
+
try:
|
|
112
|
+
retry_after = int(raw_retry)
|
|
113
|
+
except ValueError:
|
|
114
|
+
retry_after = 60
|
|
115
|
+
|
|
116
|
+
# Retry on 429 and 5xx
|
|
117
|
+
should_retry = attempt < self._config.retries and (
|
|
118
|
+
response.status_code == 429 or response.status_code >= 500
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if should_retry:
|
|
122
|
+
if response.status_code == 429 and retry_after is not None:
|
|
123
|
+
delay = float(retry_after)
|
|
124
|
+
else:
|
|
125
|
+
delay = self._config.retry_delay * (2**attempt)
|
|
126
|
+
await asyncio.sleep(delay)
|
|
127
|
+
return await self._request(method, path, params=params, body=body, attempt=attempt + 1)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
error_body = response.json()
|
|
131
|
+
except Exception:
|
|
132
|
+
error_body = {
|
|
133
|
+
"error": {
|
|
134
|
+
"code": "UNKNOWN_ERROR",
|
|
135
|
+
"message": f"HTTP {response.status_code}: {response.text}",
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
raise parse_error_response(response.status_code, error_body, request_id, retry_after)
|