synchronity-sdk 0.1.1__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.
Files changed (33) hide show
  1. synchronity_sdk-0.1.1/PKG-INFO +181 -0
  2. synchronity_sdk-0.1.1/README.md +167 -0
  3. synchronity_sdk-0.1.1/agentmesh/__init__.py +62 -0
  4. synchronity_sdk-0.1.1/agentmesh/client.py +44 -0
  5. synchronity_sdk-0.1.1/agentmesh/config.py +18 -0
  6. synchronity_sdk-0.1.1/agentmesh/errors.py +97 -0
  7. synchronity_sdk-0.1.1/agentmesh/http.py +139 -0
  8. synchronity_sdk-0.1.1/agentmesh/modules/__init__.py +14 -0
  9. synchronity_sdk-0.1.1/agentmesh/modules/cart.py +51 -0
  10. synchronity_sdk-0.1.1/agentmesh/modules/checkout.py +49 -0
  11. synchronity_sdk-0.1.1/agentmesh/modules/discovery.py +42 -0
  12. synchronity_sdk-0.1.1/agentmesh/modules/orders.py +36 -0
  13. synchronity_sdk-0.1.1/agentmesh/modules/products.py +73 -0
  14. synchronity_sdk-0.1.1/agentmesh/tools/__init__.py +6 -0
  15. synchronity_sdk-0.1.1/agentmesh/tools/_defs.py +195 -0
  16. synchronity_sdk-0.1.1/agentmesh/tools/anthropic.py +13 -0
  17. synchronity_sdk-0.1.1/agentmesh/tools/executor.py +107 -0
  18. synchronity_sdk-0.1.1/agentmesh/tools/langchain.py +13 -0
  19. synchronity_sdk-0.1.1/agentmesh/tools/openai.py +16 -0
  20. synchronity_sdk-0.1.1/agentmesh/types/__init__.py +57 -0
  21. synchronity_sdk-0.1.1/agentmesh/types/amps.py +234 -0
  22. synchronity_sdk-0.1.1/pyproject.toml +30 -0
  23. synchronity_sdk-0.1.1/setup.cfg +4 -0
  24. synchronity_sdk-0.1.1/synchronity_sdk.egg-info/PKG-INFO +181 -0
  25. synchronity_sdk-0.1.1/synchronity_sdk.egg-info/SOURCES.txt +31 -0
  26. synchronity_sdk-0.1.1/synchronity_sdk.egg-info/dependency_links.txt +1 -0
  27. synchronity_sdk-0.1.1/synchronity_sdk.egg-info/requires.txt +8 -0
  28. synchronity_sdk-0.1.1/synchronity_sdk.egg-info/top_level.txt +1 -0
  29. synchronity_sdk-0.1.1/tests/test_cart.py +128 -0
  30. synchronity_sdk-0.1.1/tests/test_checkout.py +102 -0
  31. synchronity_sdk-0.1.1/tests/test_client.py +64 -0
  32. synchronity_sdk-0.1.1/tests/test_errors.py +240 -0
  33. synchronity_sdk-0.1.1/tests/test_products.py +125 -0
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: synchronity-sdk
3
+ Version: 0.1.1
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
+ # AgentMesh Python SDK
16
+
17
+ Python SDK for the [AgentMesh](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 AgentMeshClient, AgentMeshConfig
36
+
37
+ async def main():
38
+ config = AgentMeshConfig(agent_token="your-agent-token")
39
+
40
+ async with AgentMeshClient(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 AgentMeshClient, AgentMeshConfig
81
+ import anthropic
82
+
83
+ client = AgentMeshClient(AgentMeshConfig(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 AgentMeshConfig
143
+
144
+ config = AgentMeshConfig(
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
+ AgentMeshError, # 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
+ # AgentMesh Python SDK
2
+
3
+ Python SDK for the [AgentMesh](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 AgentMeshClient, AgentMeshConfig
22
+
23
+ async def main():
24
+ config = AgentMeshConfig(agent_token="your-agent-token")
25
+
26
+ async with AgentMeshClient(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 AgentMeshClient, AgentMeshConfig
67
+ import anthropic
68
+
69
+ client = AgentMeshClient(AgentMeshConfig(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 AgentMeshConfig
129
+
130
+ config = AgentMeshConfig(
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
+ AgentMeshError, # 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)