ordercloud-python 2026.4.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.
Files changed (114) hide show
  1. ordercloud/__init__.py +37 -0
  2. ordercloud/auth.py +136 -0
  3. ordercloud/client.py +211 -0
  4. ordercloud/config.py +42 -0
  5. ordercloud/errors.py +47 -0
  6. ordercloud/http.py +218 -0
  7. ordercloud/middleware.py +66 -0
  8. ordercloud/models/__init__.py +271 -0
  9. ordercloud/models/address.py +47 -0
  10. ordercloud/models/api_client.py +116 -0
  11. ordercloud/models/approval.py +73 -0
  12. ordercloud/models/assignments.py +402 -0
  13. ordercloud/models/auth_models.py +114 -0
  14. ordercloud/models/bundle.py +31 -0
  15. ordercloud/models/buyer.py +271 -0
  16. ordercloud/models/catalog.py +33 -0
  17. ordercloud/models/category.py +35 -0
  18. ordercloud/models/cost_center.py +27 -0
  19. ordercloud/models/credit_card.py +35 -0
  20. ordercloud/models/delivery.py +277 -0
  21. ordercloud/models/discount.py +63 -0
  22. ordercloud/models/integration.py +76 -0
  23. ordercloud/models/inventory_record.py +53 -0
  24. ordercloud/models/line_item.py +95 -0
  25. ordercloud/models/line_item_types.py +89 -0
  26. ordercloud/models/message_sender.py +80 -0
  27. ordercloud/models/misc.py +280 -0
  28. ordercloud/models/open_id_connect.py +47 -0
  29. ordercloud/models/order.py +477 -0
  30. ordercloud/models/order_return.py +92 -0
  31. ordercloud/models/payment.py +77 -0
  32. ordercloud/models/price_schedule.py +76 -0
  33. ordercloud/models/product.py +227 -0
  34. ordercloud/models/product_collection.py +186 -0
  35. ordercloud/models/promotion.py +297 -0
  36. ordercloud/models/security.py +89 -0
  37. ordercloud/models/shared.py +131 -0
  38. ordercloud/models/shipment.py +150 -0
  39. ordercloud/models/spec.py +67 -0
  40. ordercloud/models/spending_account.py +33 -0
  41. ordercloud/models/subscription.py +125 -0
  42. ordercloud/models/supplier.py +43 -0
  43. ordercloud/models/sync.py +172 -0
  44. ordercloud/models/user.py +207 -0
  45. ordercloud/models/user_group.py +27 -0
  46. ordercloud/models/webhook.py +58 -0
  47. ordercloud/py.typed +0 -0
  48. ordercloud/resources/__init__.py +65 -0
  49. ordercloud/resources/addresses.py +228 -0
  50. ordercloud/resources/admin_addresses.py +128 -0
  51. ordercloud/resources/admin_user_groups.py +185 -0
  52. ordercloud/resources/admin_users.py +150 -0
  53. ordercloud/resources/api_clients.py +308 -0
  54. ordercloud/resources/approval_rules.py +144 -0
  55. ordercloud/resources/base.py +145 -0
  56. ordercloud/resources/bundle_line_items.py +59 -0
  57. ordercloud/resources/bundle_subscription_items.py +54 -0
  58. ordercloud/resources/bundles.py +278 -0
  59. ordercloud/resources/buyer_groups.py +128 -0
  60. ordercloud/resources/buyers.py +164 -0
  61. ordercloud/resources/cart.py +613 -0
  62. ordercloud/resources/catalogs.py +311 -0
  63. ordercloud/resources/categories.py +392 -0
  64. ordercloud/resources/cost_centers.py +222 -0
  65. ordercloud/resources/credit_cards.py +227 -0
  66. ordercloud/resources/delivery_configurations.py +132 -0
  67. ordercloud/resources/discounts.py +201 -0
  68. ordercloud/resources/entity_syncs.py +534 -0
  69. ordercloud/resources/error_configs.py +71 -0
  70. ordercloud/resources/forgotten_credentials.py +74 -0
  71. ordercloud/resources/group_orders.py +28 -0
  72. ordercloud/resources/impersonation_configs.py +132 -0
  73. ordercloud/resources/incrementors.py +128 -0
  74. ordercloud/resources/integration_events.py +203 -0
  75. ordercloud/resources/inventory_integrations.py +65 -0
  76. ordercloud/resources/inventory_records.py +484 -0
  77. ordercloud/resources/line_items.py +262 -0
  78. ordercloud/resources/locales.py +203 -0
  79. ordercloud/resources/me.py +1882 -0
  80. ordercloud/resources/message_senders.py +261 -0
  81. ordercloud/resources/open_id_connects.py +128 -0
  82. ordercloud/resources/order_returns.py +306 -0
  83. ordercloud/resources/order_syncs.py +65 -0
  84. ordercloud/resources/orders.py +689 -0
  85. ordercloud/resources/payments.py +176 -0
  86. ordercloud/resources/price_schedules.py +164 -0
  87. ordercloud/resources/product_collections.py +116 -0
  88. ordercloud/resources/product_facets.py +128 -0
  89. ordercloud/resources/product_syncs.py +76 -0
  90. ordercloud/resources/products.py +454 -0
  91. ordercloud/resources/promotion_integrations.py +65 -0
  92. ordercloud/resources/promotions.py +203 -0
  93. ordercloud/resources/security_profiles.py +222 -0
  94. ordercloud/resources/seller_approval_rules.py +128 -0
  95. ordercloud/resources/shipments.py +256 -0
  96. ordercloud/resources/specs.py +313 -0
  97. ordercloud/resources/spending_accounts.py +227 -0
  98. ordercloud/resources/subscription_integrations.py +65 -0
  99. ordercloud/resources/subscription_items.py +146 -0
  100. ordercloud/resources/subscriptions.py +128 -0
  101. ordercloud/resources/supplier_addresses.py +144 -0
  102. ordercloud/resources/supplier_user_groups.py +210 -0
  103. ordercloud/resources/supplier_users.py +170 -0
  104. ordercloud/resources/suppliers.py +190 -0
  105. ordercloud/resources/tracking_events.py +130 -0
  106. ordercloud/resources/user_groups.py +210 -0
  107. ordercloud/resources/users.py +254 -0
  108. ordercloud/resources/webhooks.py +128 -0
  109. ordercloud/resources/xp_indices.py +77 -0
  110. ordercloud/sync_client.py +170 -0
  111. ordercloud_python-2026.4.1.dist-info/METADATA +552 -0
  112. ordercloud_python-2026.4.1.dist-info/RECORD +114 -0
  113. ordercloud_python-2026.4.1.dist-info/WHEEL +4 -0
  114. ordercloud_python-2026.4.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,254 @@
1
+ # GENERATED by tools/codegen — DO NOT EDIT
2
+ # Source: ordercloud-openapi-v3.json
3
+ """OrderCloud Users API resource."""
4
+
5
+ from __future__ import annotations
6
+ from typing import Any, Optional, Union
7
+
8
+ from ..models.auth_models import AccessToken, ImpersonateTokenRequest
9
+ from ..models.user import User, UserOrderMoveOption
10
+ from ..models.shared import ListPage
11
+ from .base import BaseResource
12
+
13
+ __all__ = ["UsersResource"]
14
+
15
+
16
+ class UsersResource(BaseResource):
17
+ """Operations on OrderCloud Users."""
18
+
19
+ async def list(
20
+ self,
21
+ buyer_id: str,
22
+ *,
23
+ user_group_id: Optional[str] = None,
24
+ search: Optional[str] = None,
25
+ search_on: Optional[str] = None,
26
+ sort_by: Optional[str] = None,
27
+ page: Optional[int] = None,
28
+ page_size: Optional[int] = None,
29
+ filters: Optional[dict[str, Any]] = None,
30
+ ) -> ListPage[User]:
31
+ """List users
32
+
33
+ Args:
34
+ buyer_id: ID of the buyer.
35
+ user_group_id: ID of the user group.
36
+ search: Word or phrase to search for.
37
+ search_on: Comma-delimited list of fields to search on.
38
+ sort_by: Comma-delimited list of fields to sort by.
39
+ page: Page of results to return. When paginating through many items (> page 30), we recommend the "Last ID" method, as outlined in the Advanced Querying documentation.
40
+ page_size: Number of results to return per page.
41
+ filters: An object or dictionary representing key/value pairs to apply as filters. Valid keys are top-level properties of the returned model or 'xp.???'
42
+
43
+ Returns:
44
+ A paginated list of User objects.
45
+ """
46
+ params = self._build_list_params(
47
+ search=search,
48
+ search_on=search_on,
49
+ sort_by=sort_by,
50
+ page=page,
51
+ page_size=page_size,
52
+ filters=filters,
53
+ )
54
+ if user_group_id is not None:
55
+ params["userGroupID"] = user_group_id
56
+ resp = await self._http.get(f"/buyers/{buyer_id}/users", **params)
57
+ return self._parse_list(resp.json(), User)
58
+
59
+ async def create(
60
+ self,
61
+ buyer_id: str,
62
+ user: Union[User, dict[str, Any]],
63
+ ) -> User:
64
+ """Create a user
65
+
66
+ Args:
67
+ buyer_id: ID of the buyer.
68
+ user: A ``User`` model or dict. Required fields: Username, FirstName, LastName, Email, Active.
69
+
70
+ Returns:
71
+ The User object.
72
+ """
73
+ resp = await self._http.post(f"/buyers/{buyer_id}/users", json=self._serialize(user))
74
+ return User(**resp.json())
75
+
76
+ async def get(
77
+ self,
78
+ buyer_id: str,
79
+ user_id: str,
80
+ ) -> User:
81
+ """Retrieve a user
82
+
83
+ Args:
84
+ buyer_id: ID of the buyer.
85
+ user_id: ID of the user.
86
+
87
+ Returns:
88
+ The User object.
89
+ """
90
+ resp = await self._http.get(f"/buyers/{buyer_id}/users/{user_id}")
91
+ return User(**resp.json())
92
+
93
+ async def save(
94
+ self,
95
+ buyer_id: str,
96
+ user_id: str,
97
+ user: Union[User, dict[str, Any]],
98
+ ) -> User:
99
+ """Create or update a user
100
+
101
+ Args:
102
+ buyer_id: ID of the buyer.
103
+ user_id: ID of the user.
104
+ user: A ``User`` model or dict. Required fields: Username, FirstName, LastName, Email, Active.
105
+
106
+ Returns:
107
+ The User object.
108
+ """
109
+ resp = await self._http.put(
110
+ f"/buyers/{buyer_id}/users/{user_id}",
111
+ json=self._serialize(user),
112
+ )
113
+ return User(**resp.json())
114
+
115
+ async def delete(
116
+ self,
117
+ buyer_id: str,
118
+ user_id: str,
119
+ ) -> None:
120
+ """Delete a user
121
+
122
+ Args:
123
+ buyer_id: ID of the buyer.
124
+ user_id: ID of the user.
125
+ """
126
+ await self._http.delete(f"/buyers/{buyer_id}/users/{user_id}")
127
+
128
+ async def patch(
129
+ self,
130
+ buyer_id: str,
131
+ user_id: str,
132
+ partial: dict[str, Any],
133
+ ) -> User:
134
+ """Partially update a user
135
+
136
+ Args:
137
+ buyer_id: ID of the buyer.
138
+ user_id: ID of the user.
139
+ partial: A dict of fields to update.
140
+
141
+ Returns:
142
+ The User object.
143
+ """
144
+ resp = await self._http.patch(f"/buyers/{buyer_id}/users/{user_id}", json=partial)
145
+ return User(**resp.json())
146
+
147
+ async def get_access_token(
148
+ self,
149
+ buyer_id: str,
150
+ user_id: str,
151
+ impersonate_token_request: Union[ImpersonateTokenRequest, dict[str, Any]],
152
+ ) -> AccessToken:
153
+ """Retrieve a user access token
154
+
155
+ Args:
156
+ buyer_id: ID of the buyer.
157
+ user_id: ID of the user.
158
+ impersonate_token_request: A ``ImpersonateTokenRequest`` model or dict. Required fields: ClientID, Roles.
159
+
160
+ Returns:
161
+ The AccessToken object.
162
+ """
163
+ resp = await self._http.post(
164
+ f"/buyers/{buyer_id}/users/{user_id}/accesstoken",
165
+ json=self._serialize(impersonate_token_request),
166
+ )
167
+ return AccessToken(**resp.json())
168
+
169
+ async def move(
170
+ self,
171
+ buyer_id: str,
172
+ user_id: str,
173
+ new_buyer_id: str,
174
+ *,
175
+ orders: UserOrderMoveOption,
176
+ ) -> User:
177
+ """Move a user to a different buyer
178
+
179
+ Args:
180
+ buyer_id: ID of the buyer.
181
+ user_id: ID of the user.
182
+ new_buyer_id: ID of the new buyer.
183
+ orders: Orders of the user. Possible values: None, Unsubmitted, All.
184
+
185
+ Returns:
186
+ The User object.
187
+ """
188
+ _params: dict[str, Any] = {}
189
+ if orders is not None:
190
+ _params["orders"] = orders
191
+ resp = await self._http.post(
192
+ f"/buyers/{buyer_id}/users/{user_id}/moveto/{new_buyer_id}", params=_params or None
193
+ )
194
+ return User(**resp.json())
195
+
196
+ async def revoke_user_tokens(
197
+ self,
198
+ buyer_id: str,
199
+ user_id: str,
200
+ ) -> None:
201
+ """Revoke a user tokens
202
+
203
+ Args:
204
+ buyer_id: ID of the buyer.
205
+ user_id: ID of the user.
206
+ """
207
+ await self._http.delete(f"/buyers/{buyer_id}/users/{user_id}/tokens")
208
+
209
+ async def unlock_user_account(
210
+ self,
211
+ buyer_id: str,
212
+ user_id: str,
213
+ ) -> None:
214
+ """Unlock a user account
215
+
216
+ Args:
217
+ buyer_id: ID of the buyer.
218
+ user_id: ID of the user.
219
+ """
220
+ await self._http.post(f"/buyers/{buyer_id}/users/{user_id}/unlock")
221
+
222
+ async def list_across_buyers(
223
+ self,
224
+ *,
225
+ search: Optional[str] = None,
226
+ search_on: Optional[str] = None,
227
+ sort_by: Optional[str] = None,
228
+ page: Optional[int] = None,
229
+ page_size: Optional[int] = None,
230
+ filters: Optional[dict[str, Any]] = None,
231
+ ) -> ListPage[User]:
232
+ """List user across buyers
233
+
234
+ Args:
235
+ search: Word or phrase to search for.
236
+ search_on: Comma-delimited list of fields to search on.
237
+ sort_by: Comma-delimited list of fields to sort by.
238
+ page: Page of results to return. When paginating through many items (> page 30), we recommend the "Last ID" method, as outlined in the Advanced Querying documentation.
239
+ page_size: Number of results to return per page.
240
+ filters: An object or dictionary representing key/value pairs to apply as filters. Valid keys are top-level properties of the returned model or 'xp.???'
241
+
242
+ Returns:
243
+ A paginated list of User objects.
244
+ """
245
+ params = self._build_list_params(
246
+ search=search,
247
+ search_on=search_on,
248
+ sort_by=sort_by,
249
+ page=page,
250
+ page_size=page_size,
251
+ filters=filters,
252
+ )
253
+ resp = await self._http.get("/buyerusers", **params)
254
+ return self._parse_list(resp.json(), User)
@@ -0,0 +1,128 @@
1
+ # GENERATED by tools/codegen — DO NOT EDIT
2
+ # Source: ordercloud-openapi-v3.json
3
+ """OrderCloud Webhooks API resource."""
4
+
5
+ from __future__ import annotations
6
+ from typing import Any, Optional, Union
7
+
8
+ from ..models.webhook import Webhook
9
+ from ..models.shared import ListPage
10
+ from .base import BaseResource
11
+
12
+ __all__ = ["WebhooksResource"]
13
+
14
+
15
+ class WebhooksResource(BaseResource):
16
+ """Operations on OrderCloud Webhooks."""
17
+
18
+ async def list(
19
+ self,
20
+ *,
21
+ search: Optional[str] = None,
22
+ search_on: Optional[str] = None,
23
+ sort_by: Optional[str] = None,
24
+ page: Optional[int] = None,
25
+ page_size: Optional[int] = None,
26
+ filters: Optional[dict[str, Any]] = None,
27
+ ) -> ListPage[Webhook]:
28
+ """List webhooks
29
+
30
+ Args:
31
+ search: Word or phrase to search for.
32
+ search_on: Comma-delimited list of fields to search on.
33
+ sort_by: Comma-delimited list of fields to sort by.
34
+ page: Page of results to return. When paginating through many items (> page 30), we recommend the "Last ID" method, as outlined in the Advanced Querying documentation.
35
+ page_size: Number of results to return per page.
36
+ filters: An object or dictionary representing key/value pairs to apply as filters. Valid keys are top-level properties of the returned model or 'xp.???'
37
+
38
+ Returns:
39
+ A paginated list of Webhook objects.
40
+ """
41
+ params = self._build_list_params(
42
+ search=search,
43
+ search_on=search_on,
44
+ sort_by=sort_by,
45
+ page=page,
46
+ page_size=page_size,
47
+ filters=filters,
48
+ )
49
+ resp = await self._http.get("/webhooks", **params)
50
+ return self._parse_list(resp.json(), Webhook)
51
+
52
+ async def create(
53
+ self,
54
+ webhook: Union[Webhook, dict[str, Any]],
55
+ ) -> Webhook:
56
+ """Create a webhook
57
+
58
+ Args:
59
+ webhook: A ``Webhook`` model or dict. Required fields: Name.
60
+
61
+ Returns:
62
+ The Webhook object.
63
+ """
64
+ resp = await self._http.post("/webhooks", json=self._serialize(webhook))
65
+ return Webhook(**resp.json())
66
+
67
+ async def get(
68
+ self,
69
+ webhook_id: str,
70
+ ) -> Webhook:
71
+ """Retrieve a webhook
72
+
73
+ Args:
74
+ webhook_id: ID of the webhook.
75
+
76
+ Returns:
77
+ The Webhook object.
78
+ """
79
+ resp = await self._http.get(f"/webhooks/{webhook_id}")
80
+ return Webhook(**resp.json())
81
+
82
+ async def save(
83
+ self,
84
+ webhook_id: str,
85
+ webhook: Union[Webhook, dict[str, Any]],
86
+ ) -> Webhook:
87
+ """Create or update a webhook
88
+
89
+ Args:
90
+ webhook_id: ID of the webhook.
91
+ webhook: A ``Webhook`` model or dict. Required fields: Name.
92
+
93
+ Returns:
94
+ The Webhook object.
95
+ """
96
+ resp = await self._http.put(
97
+ f"/webhooks/{webhook_id}",
98
+ json=self._serialize(webhook),
99
+ )
100
+ return Webhook(**resp.json())
101
+
102
+ async def delete(
103
+ self,
104
+ webhook_id: str,
105
+ ) -> None:
106
+ """Delete a webhook
107
+
108
+ Args:
109
+ webhook_id: ID of the webhook.
110
+ """
111
+ await self._http.delete(f"/webhooks/{webhook_id}")
112
+
113
+ async def patch(
114
+ self,
115
+ webhook_id: str,
116
+ partial: dict[str, Any],
117
+ ) -> Webhook:
118
+ """Partially update a webhook
119
+
120
+ Args:
121
+ webhook_id: ID of the webhook.
122
+ partial: A dict of fields to update.
123
+
124
+ Returns:
125
+ The Webhook object.
126
+ """
127
+ resp = await self._http.patch(f"/webhooks/{webhook_id}", json=partial)
128
+ return Webhook(**resp.json())
@@ -0,0 +1,77 @@
1
+ # GENERATED by tools/codegen — DO NOT EDIT
2
+ # Source: ordercloud-openapi-v3.json
3
+ """OrderCloud XpIndices API resource."""
4
+
5
+ from __future__ import annotations
6
+ from typing import Any, Optional, Union
7
+
8
+ from ..models.misc import XpIndex, XpThingType
9
+ from ..models.shared import ListPage
10
+ from .base import BaseResource
11
+
12
+ __all__ = ["XpIndicesResource"]
13
+
14
+
15
+ class XpIndicesResource(BaseResource):
16
+ """Operations on OrderCloud XpIndices."""
17
+
18
+ async def list(
19
+ self,
20
+ *,
21
+ search: Optional[str] = None,
22
+ search_on: Optional[str] = None,
23
+ sort_by: Optional[str] = None,
24
+ page: Optional[int] = None,
25
+ page_size: Optional[int] = None,
26
+ filters: Optional[dict[str, Any]] = None,
27
+ ) -> ListPage[XpIndex]:
28
+ """List XP indices
29
+
30
+ Args:
31
+ search: Word or phrase to search for.
32
+ search_on: Comma-delimited list of fields to search on.
33
+ sort_by: Comma-delimited list of fields to sort by.
34
+ page: Page of results to return. When paginating through many items (> page 30), we recommend the "Last ID" method, as outlined in the Advanced Querying documentation.
35
+ page_size: Number of results to return per page.
36
+ filters: An object or dictionary representing key/value pairs to apply as filters. Valid keys are top-level properties of the returned model or 'xp.???'
37
+
38
+ Returns:
39
+ A paginated list of XpIndex objects.
40
+ """
41
+ params = self._build_list_params(
42
+ search=search,
43
+ search_on=search_on,
44
+ sort_by=sort_by,
45
+ page=page,
46
+ page_size=page_size,
47
+ filters=filters,
48
+ )
49
+ resp = await self._http.get("/xpindices", **params)
50
+ return self._parse_list(resp.json(), XpIndex)
51
+
52
+ async def put(
53
+ self,
54
+ xp_index: Union[XpIndex, dict[str, Any]],
55
+ ) -> None:
56
+ """Create or update an xp index
57
+
58
+ Args:
59
+ xp_index: A ``XpIndex`` model or dict.
60
+ """
61
+ await self._http.put(
62
+ "/xpindices",
63
+ json=self._serialize(xp_index),
64
+ )
65
+
66
+ async def delete(
67
+ self,
68
+ thing_type: XpThingType,
69
+ key: str,
70
+ ) -> None:
71
+ """Delete a XP index
72
+
73
+ Args:
74
+ thing_type: Thing type of the xp index. Possible values: Address, Variant, Order, OrderReturn, LineItem, CostCenter, CreditCard, Payment, Spec, SpecOption, UserGroup, Company, Category, PriceSchedule, Shipment, SpendingAccount, User, Promotion, ApprovalRule, SellerApprovalRule, Catalog, ProductFacet, MessageSender, InventoryRecord, ProductCollection, Subscription, GroupOrderInvitation, BuyerGroup, Discount.
75
+ key: Key of the xp index.
76
+ """
77
+ await self._http.delete(f"/xpindices/{thing_type}/{key}")
@@ -0,0 +1,170 @@
1
+ """Synchronous wrapper around the async OrderCloud client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ from collections.abc import Iterator
8
+ from typing import Any, TypeVar
9
+
10
+ from .config import OrderCloudConfig
11
+ from .middleware import AfterResponse, BeforeRequest
12
+ from .resources.base import BaseResource
13
+
14
+ T = TypeVar("T")
15
+
16
+ __all__ = ["SyncOrderCloudClient", "paginate_sync"]
17
+
18
+
19
+ class _SyncProxy:
20
+ """Wraps an async resource, making all async methods synchronous.
21
+
22
+ When an attribute is accessed that is a coroutine function, it is
23
+ wrapped so that calling it runs the coroutine to completion on the
24
+ client's event loop and returns the result directly.
25
+ """
26
+
27
+ def __init__(self, async_resource: BaseResource, runner: Any) -> None:
28
+ self._async = async_resource
29
+ self._run = runner
30
+
31
+ def __getattr__(self, name: str) -> Any:
32
+ attr = getattr(self._async, name)
33
+ if asyncio.iscoroutinefunction(attr):
34
+
35
+ @functools.wraps(attr)
36
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
37
+ return self._run(attr(*args, **kwargs))
38
+
39
+ return sync_wrapper
40
+ return attr
41
+
42
+
43
+ class SyncOrderCloudClient:
44
+ """Synchronous client for the OrderCloud API.
45
+
46
+ A thin wrapper around ``OrderCloudClient`` that manages its own
47
+ event loop internally. All resource methods are available as
48
+ regular (non-async) calls.
49
+
50
+ Usage::
51
+
52
+ with SyncOrderCloudClient.create(
53
+ client_id="YOUR_CLIENT_ID",
54
+ client_secret="YOUR_CLIENT_SECRET",
55
+ ) as client:
56
+ products = client.products.list()
57
+ """
58
+
59
+ def __init__(self, config: OrderCloudConfig) -> None:
60
+ # Import here to avoid circular import at module level.
61
+ from .client import OrderCloudClient
62
+
63
+ self._loop = asyncio.new_event_loop()
64
+ self._async_client = OrderCloudClient(config)
65
+
66
+ # Auto-wrap all resource attributes with sync proxies.
67
+ for name, value in vars(self._async_client).items():
68
+ if isinstance(value, BaseResource):
69
+ object.__setattr__(self, name, _SyncProxy(value, self._run))
70
+
71
+ @classmethod
72
+ def create(
73
+ cls,
74
+ *,
75
+ client_id: str,
76
+ client_secret: str,
77
+ base_url: str = "https://api.ordercloud.io/v1",
78
+ auth_url: str = "https://auth.ordercloud.io/oauth/token",
79
+ scopes: list[str] | None = None,
80
+ timeout: float = 30.0,
81
+ max_retries: int = 0,
82
+ retry_backoff: float = 0.5,
83
+ ) -> SyncOrderCloudClient:
84
+ """Create a client from individual parameters.
85
+
86
+ Args:
87
+ client_id: OAuth2 client ID.
88
+ client_secret: OAuth2 client secret.
89
+ base_url: API base URL (default: US production).
90
+ auth_url: OAuth2 token endpoint.
91
+ scopes: Requested scopes (default: ``["FullAccess"]``).
92
+ timeout: HTTP request timeout in seconds.
93
+ max_retries: Maximum retries on 429/5xx (0 = disabled).
94
+ retry_backoff: Base delay for exponential backoff.
95
+
96
+ Returns:
97
+ A configured ``SyncOrderCloudClient`` instance.
98
+ """
99
+ config = OrderCloudConfig(
100
+ client_id=client_id,
101
+ client_secret=client_secret,
102
+ base_url=base_url,
103
+ auth_url=auth_url,
104
+ scopes=tuple(scopes) if scopes else ("FullAccess",),
105
+ timeout=timeout,
106
+ max_retries=max_retries,
107
+ retry_backoff=retry_backoff,
108
+ )
109
+ return cls(config)
110
+
111
+ def _run(self, coro: Any) -> Any:
112
+ """Run a coroutine to completion on the internal event loop."""
113
+ return self._loop.run_until_complete(coro)
114
+
115
+ def add_before_request(self, hook: BeforeRequest) -> None:
116
+ """Register a hook called before each HTTP request."""
117
+ self._async_client.add_before_request(hook)
118
+
119
+ def add_after_response(self, hook: AfterResponse) -> None:
120
+ """Register a hook called after each HTTP response."""
121
+ self._async_client.add_after_response(hook)
122
+
123
+ def close(self) -> None:
124
+ """Close the underlying HTTP client and event loop."""
125
+ self._run(self._async_client.close())
126
+ self._loop.close()
127
+
128
+ def __enter__(self) -> SyncOrderCloudClient:
129
+ return self
130
+
131
+ def __exit__(self, *args: object) -> None:
132
+ self.close()
133
+
134
+
135
+ def paginate_sync(
136
+ list_method: Any,
137
+ *args: Any,
138
+ page_size: int = 100,
139
+ **kwargs: Any,
140
+ ) -> Iterator[Any]:
141
+ """Auto-paginate any sync list method, yielding individual items.
142
+
143
+ Works with the sync proxy methods on ``SyncOrderCloudClient``.
144
+
145
+ Args:
146
+ list_method: A bound sync list method (e.g. ``client.products.list``).
147
+ *args: Positional arguments forwarded to the list method.
148
+ page_size: Items per page (default 100, the API maximum).
149
+ **kwargs: Keyword arguments forwarded to the list method.
150
+
151
+ Yields:
152
+ Individual items across all pages.
153
+
154
+ Example::
155
+
156
+ from ordercloud.sync_client import SyncOrderCloudClient, paginate_sync
157
+
158
+ with SyncOrderCloudClient.create(...) as client:
159
+ for product in paginate_sync(client.products.list, search="widget"):
160
+ print(product.name)
161
+ """
162
+ page = 1
163
+ kwargs["page_size"] = page_size
164
+ while True:
165
+ kwargs["page"] = page
166
+ result = list_method(*args, **kwargs)
167
+ yield from result.items
168
+ if page >= result.meta.total_pages:
169
+ break
170
+ page += 1