synchronity-sdk 0.1.1__py3-none-any.whl

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.
agentmesh/__init__.py ADDED
@@ -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
+ ]
agentmesh/client.py ADDED
@@ -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
agentmesh/config.py ADDED
@@ -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
agentmesh/errors.py ADDED
@@ -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)
agentmesh/http.py ADDED
@@ -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)
@@ -0,0 +1,14 @@
1
+ from .cart import CartModule
2
+ from .checkout import CheckoutModule, ShippingAddressParams
3
+ from .discovery import DiscoveryModule
4
+ from .orders import OrdersModule
5
+ from .products import ProductsModule
6
+
7
+ __all__ = [
8
+ "CartModule",
9
+ "CheckoutModule",
10
+ "ShippingAddressParams",
11
+ "DiscoveryModule",
12
+ "OrdersModule",
13
+ "ProductsModule",
14
+ ]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Union
3
+
4
+ from ..http import HttpClient
5
+ from ..types.amps import AMPSCart
6
+
7
+
8
+ class CartModule:
9
+ def __init__(self, http: HttpClient) -> None:
10
+ self._http = http
11
+
12
+ async def create(self, site_id: str, currency: Optional[str] = None) -> AMPSCart:
13
+ body: dict = {}
14
+ if currency is not None:
15
+ body["currency"] = currency
16
+ raw = await self._http.post(f"/v1/sites/{site_id}/cart", body)
17
+ return AMPSCart.model_validate(raw)
18
+
19
+ async def get(self, site_id: str, cart_id: str) -> AMPSCart:
20
+ raw = await self._http.get(f"/v1/sites/{site_id}/cart/{cart_id}")
21
+ return AMPSCart.model_validate(raw)
22
+
23
+ async def add_item(
24
+ self,
25
+ site_id: str,
26
+ cart_id: str,
27
+ product_id: str,
28
+ quantity: int,
29
+ variant_id: Optional[str] = None,
30
+ ) -> AMPSCart:
31
+ body: dict = {"product_id": product_id, "quantity": quantity}
32
+ if variant_id is not None:
33
+ body["variant_id"] = variant_id
34
+ raw = await self._http.post(f"/v1/sites/{site_id}/cart/{cart_id}/items", body)
35
+ return AMPSCart.model_validate(raw)
36
+
37
+ async def remove_item(
38
+ self, site_id: str, cart_id: str, item_id: str
39
+ ) -> AMPSCart:
40
+ raw = await self._http.delete(
41
+ f"/v1/sites/{site_id}/cart/{cart_id}/items/{item_id}"
42
+ )
43
+ return AMPSCart.model_validate(raw)
44
+
45
+ async def apply_coupon(
46
+ self, site_id: str, cart_id: str, code: str
47
+ ) -> AMPSCart:
48
+ raw = await self._http.post(
49
+ f"/v1/sites/{site_id}/cart/{cart_id}/coupon", {"code": code}
50
+ )
51
+ return AMPSCart.model_validate(raw)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Union
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from ..http import HttpClient
7
+ from ..types.amps import AMPSOrder
8
+
9
+
10
+ @dataclass
11
+ class ShippingAddressParams:
12
+ name: str
13
+ line1: str
14
+ city: str
15
+ state: str
16
+ postal_code: str
17
+ country: str
18
+ line2: Optional[str] = None
19
+
20
+
21
+ class CheckoutModule:
22
+ def __init__(self, http: HttpClient) -> None:
23
+ self._http = http
24
+
25
+ async def execute(
26
+ self,
27
+ site_id: str,
28
+ cart_id: str,
29
+ buyer_delegation_token: str,
30
+ shipping_address: ShippingAddressParams,
31
+ ) -> AMPSOrder:
32
+ address: dict = {
33
+ "name": shipping_address.name,
34
+ "line1": shipping_address.line1,
35
+ "city": shipping_address.city,
36
+ "state": shipping_address.state,
37
+ "postal_code": shipping_address.postal_code,
38
+ "country_code": shipping_address.country,
39
+ }
40
+ if shipping_address.line2 is not None:
41
+ address["line2"] = shipping_address.line2
42
+
43
+ body = {
44
+ "cart_id": cart_id,
45
+ "buyer_delegation_token": buyer_delegation_token,
46
+ "shipping_address": address,
47
+ }
48
+ raw = await self._http.post(f"/v1/sites/{site_id}/checkout", body)
49
+ return AMPSOrder.model_validate(raw)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Union, List
3
+
4
+ from ..http import HttpClient
5
+ from ..types.amps import AMPSCapabilityManifest, PaginatedSiteResponse
6
+
7
+
8
+ class DiscoveryModule:
9
+ def __init__(self, http: HttpClient) -> None:
10
+ self._http = http
11
+
12
+ async def list_sites(
13
+ self,
14
+ category: Optional[str] = None,
15
+ ships_to: Optional[str] = None,
16
+ capabilities: Optional[List[str]] = None,
17
+ page: Optional[int] = None,
18
+ limit: Optional[int] = None,
19
+ ) -> dict:
20
+ params: dict = {}
21
+ if category is not None:
22
+ params["category"] = category
23
+ if ships_to is not None:
24
+ params["ships_to"] = ships_to
25
+ if capabilities is not None:
26
+ params["capabilities"] = capabilities
27
+ if page is not None:
28
+ params["page"] = page
29
+ if limit is not None:
30
+ params["limit"] = limit
31
+
32
+ raw = await self._http.get("/v1/sites", params or None)
33
+ response = PaginatedSiteResponse.model_validate(raw)
34
+ return {
35
+ "sites": [s.model_dump() for s in response.data],
36
+ "total": response.pagination.total,
37
+ "page": response.pagination.page,
38
+ }
39
+
40
+ async def get_manifest(self, site_id: str) -> AMPSCapabilityManifest:
41
+ raw = await self._http.get(f"/v1/sites/{site_id}/manifest")
42
+ return AMPSCapabilityManifest.model_validate(raw)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Union
3
+
4
+ from ..http import HttpClient
5
+ from ..types.amps import AMPSOrder, AMPSOrderStatus, PaginatedOrderResponse
6
+
7
+
8
+ class OrdersModule:
9
+ def __init__(self, http: HttpClient) -> None:
10
+ self._http = http
11
+
12
+ async def get(self, site_id: str, order_id: str) -> AMPSOrder:
13
+ raw = await self._http.get(f"/v1/sites/{site_id}/orders/{order_id}")
14
+ return AMPSOrder.model_validate(raw)
15
+
16
+ async def list(
17
+ self,
18
+ site_id: str,
19
+ page: Optional[int] = None,
20
+ limit: Optional[int] = None,
21
+ status: AMPSOrderStatus | Optional[str] = None,
22
+ ) -> dict:
23
+ params: dict = {}
24
+ if page is not None:
25
+ params["page"] = page
26
+ if limit is not None:
27
+ params["limit"] = limit
28
+ if status is not None:
29
+ params["status"] = str(status.value if isinstance(status, AMPSOrderStatus) else status)
30
+
31
+ raw = await self._http.get(f"/v1/sites/{site_id}/orders", params or None)
32
+ response = PaginatedOrderResponse.model_validate(raw)
33
+ return {
34
+ "orders": [o.model_dump() for o in response.data],
35
+ "total": response.pagination.total,
36
+ }
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Union, List
3
+
4
+ from ..http import HttpClient
5
+ from ..types.amps import AMPSProduct, PaginatedProductResponse
6
+
7
+
8
+ class ProductsModule:
9
+ def __init__(self, http: HttpClient) -> None:
10
+ self._http = http
11
+
12
+ async def search(
13
+ self,
14
+ site_id: str,
15
+ q: Optional[str] = None,
16
+ category: Optional[str] = None,
17
+ min_price: Optional[float] = None,
18
+ max_price: Optional[float] = None,
19
+ in_stock: Optional[bool] = None,
20
+ page: Optional[int] = None,
21
+ limit: Optional[int] = None,
22
+ ) -> dict:
23
+ params: dict = {}
24
+ if q is not None:
25
+ params["q"] = q
26
+ if category is not None:
27
+ params["category"] = category
28
+ if min_price is not None:
29
+ params["min_price"] = min_price
30
+ if max_price is not None:
31
+ params["max_price"] = max_price
32
+ if in_stock is not None:
33
+ params["in_stock"] = in_stock
34
+ if page is not None:
35
+ params["page"] = page
36
+ if limit is not None:
37
+ params["limit"] = limit
38
+
39
+ raw = await self._http.get(f"/v1/sites/{site_id}/products", params or None)
40
+ response = PaginatedProductResponse.model_validate(raw)
41
+ pg = response.pagination
42
+ total_pages = (pg.total + pg.limit - 1) // pg.limit if pg.limit > 0 else 1
43
+ return {
44
+ "products": [p.model_dump() for p in response.data],
45
+ "total": pg.total,
46
+ "page": pg.page,
47
+ "total_pages": total_pages,
48
+ }
49
+
50
+ async def get_by_id(self, site_id: str, product_id: str) -> AMPSProduct:
51
+ raw = await self._http.get(f"/v1/sites/{site_id}/products/{product_id}")
52
+ return AMPSProduct.model_validate(raw)
53
+
54
+ async def compare(
55
+ self,
56
+ site_ids: List[str],
57
+ query: str,
58
+ min_price: Optional[float] = None,
59
+ max_price: Optional[float] = None,
60
+ in_stock: Optional[bool] = None,
61
+ ) -> dict:
62
+ body: dict = {"query": query, "site_ids": site_ids}
63
+ filters: dict = {}
64
+ if min_price is not None:
65
+ filters["min_price"] = min_price
66
+ if max_price is not None:
67
+ filters["max_price"] = max_price
68
+ if in_stock is not None:
69
+ filters["in_stock"] = in_stock
70
+ if filters:
71
+ body["filters"] = filters
72
+
73
+ return await self._http.post("/v1/multi/products/compare", body)
@@ -0,0 +1,6 @@
1
+ from .anthropic import anthropic_tools
2
+ from .executor import ToolExecutor
3
+ from .langchain import langchain_tools
4
+ from .openai import openai_tools
5
+
6
+ __all__ = ["anthropic_tools", "openai_tools", "langchain_tools", "ToolExecutor"]