python-bestbuy 0.1.0__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.
bestbuy/__init__.py ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ from .catalog import AsyncCatalogClient, CatalogClient
2
+ from .commerce import AsyncCommerceClient, CommerceClient
3
+
4
+ __all__ = [
5
+ "AsyncCatalogClient",
6
+ "AsyncCommerceClient",
7
+ "CatalogClient",
8
+ "CommerceClient",
9
+ ]
@@ -0,0 +1,178 @@
1
+ import abc
2
+ import logging
3
+ from types import TracebackType
4
+ from typing import Any, Dict, List, Type
5
+
6
+ import httpx
7
+ from pydantic import ValidationError
8
+
9
+ from ..configs import BaseConfig
10
+ from ..exceptions import ConfigError
11
+ from ..loggers import make_console_logger
12
+ from ..utils import check_for_json_errors, check_for_xml_errors
13
+
14
+ from ..typing import SyncAsync
15
+
16
+
17
+ class BaseClient:
18
+ config_parser: Type[BaseConfig]
19
+
20
+ def __init__(
21
+ self,
22
+ client: httpx.Client | httpx.AsyncClient,
23
+ options: Dict[str, Any] | BaseConfig | None = None,
24
+ **kwargs: Any,
25
+ ) -> None:
26
+ try:
27
+ if options is None:
28
+ options = self.config_parser(**kwargs)
29
+ elif isinstance(options, dict):
30
+ options = self.config_parser(**options)
31
+ except ValidationError as e:
32
+ errors = []
33
+ for error in e.errors():
34
+ field = ".".join(str(loc) for loc in error["loc"])
35
+ msg = error["msg"]
36
+ errors.append(f"{field}: {msg}")
37
+ raise ConfigError(
38
+ "Configuration validation failed:\n - " + "\n - ".join(errors)
39
+ ) from e
40
+ self.options = options
41
+ self.logger = self.options.logger or make_console_logger()
42
+ self.logger.setLevel(self.options.log_level)
43
+ self._clients: List[httpx.Client | httpx.AsyncClient] = []
44
+ self.client = client
45
+
46
+ @property
47
+ def base_url(self):
48
+ return self.options.base_url
49
+
50
+ @property
51
+ def client(self) -> httpx.Client | httpx.AsyncClient:
52
+ return self._clients[-1]
53
+
54
+ @client.setter
55
+ def client(self, client: httpx.Client | httpx.AsyncClient) -> None:
56
+ client.base_url = httpx.URL(self.base_url)
57
+ client.timeout = httpx.Timeout(timeout=self.options.timeout_ms / 1_000)
58
+ headers: Dict[str, str] = {"X-API-KEY": self.options.api_key}
59
+ if self.options.content_type:
60
+ headers["Content-Type"] = self.options.content_type
61
+ client.headers = httpx.Headers(headers)
62
+ self._clients.append(client)
63
+
64
+ @abc.abstractmethod
65
+ def request(self, request: httpx.Request) -> SyncAsync[httpx.Response]:
66
+ raise NotImplementedError
67
+
68
+
69
+ class Client(BaseClient):
70
+ client: httpx.Client
71
+
72
+ def __init__(
73
+ self,
74
+ client: httpx.Client | None = None,
75
+ options: Dict[str, Any] | BaseConfig | None = None,
76
+ **kwargs: Any,
77
+ ) -> None:
78
+ if client is None:
79
+ client = httpx.Client()
80
+ super().__init__(client, options, **kwargs)
81
+
82
+ def __enter__(self) -> "Client":
83
+ self.client = httpx.Client()
84
+ self.client.__enter__()
85
+ return self
86
+
87
+ def __exit__(
88
+ self,
89
+ exc_type: Type[BaseException],
90
+ exc_value: BaseException,
91
+ traceback: TracebackType,
92
+ ) -> None:
93
+ self.client.__exit__(exc_type, exc_value, traceback)
94
+ del self._clients[-1]
95
+
96
+ def close(self) -> None:
97
+ self.client.close()
98
+
99
+ def request(self, request: httpx.Request) -> httpx.Response:
100
+ if self.logger.isEnabledFor(logging.DEBUG):
101
+ self.logger.debug(
102
+ f"Request: {request.method} {request.url}\n"
103
+ f"Headers: {dict(request.headers)}\n"
104
+ f"Body: {request.content.decode('utf-8') if request.content else None}"
105
+ )
106
+ response = self.client.send(request)
107
+ if self.logger.isEnabledFor(logging.DEBUG):
108
+ self.logger.debug(
109
+ f"Response: {response.status_code} {response.reason_phrase}\n"
110
+ f"Headers: {dict(response.headers)}\n"
111
+ f"Body: {response.text}"
112
+ )
113
+ try:
114
+ response.raise_for_status()
115
+ except httpx.HTTPStatusError as e:
116
+ content_type = e.response.headers.get("content-type", "")
117
+ if "xml" in content_type:
118
+ check_for_xml_errors(e.response.text)
119
+ elif "json" in content_type:
120
+ check_for_json_errors(e.response.text)
121
+ raise
122
+ return response
123
+
124
+
125
+ class AsyncClient(BaseClient):
126
+ client: httpx.AsyncClient
127
+
128
+ def __init__(
129
+ self,
130
+ client: httpx.AsyncClient | None = None,
131
+ options: Dict[str, Any] | BaseConfig | None = None,
132
+ **kwargs: Any,
133
+ ) -> None:
134
+ if client is None:
135
+ client = httpx.AsyncClient()
136
+ super().__init__(client, options, **kwargs)
137
+
138
+ async def __aenter__(self) -> "AsyncClient":
139
+ self.client = httpx.AsyncClient()
140
+ await self.client.__aenter__()
141
+ return self
142
+
143
+ async def __aexit__(
144
+ self,
145
+ exc_type: Type[BaseException],
146
+ exc_value: BaseException,
147
+ traceback: TracebackType,
148
+ ) -> None:
149
+ await self.client.__aexit__(exc_type, exc_value, traceback)
150
+ del self._clients[-1]
151
+
152
+ async def aclose(self) -> None:
153
+ await self.client.aclose()
154
+
155
+ async def request(self, request: httpx.Request) -> httpx.Response:
156
+ if self.logger.isEnabledFor(logging.DEBUG):
157
+ self.logger.debug(
158
+ f"Request: {request.method} {request.url}\n"
159
+ f"Headers: {dict(request.headers)}\n"
160
+ f"Body: {request.content.decode('utf-8') if request.content else None}"
161
+ )
162
+ response = await self.client.send(request)
163
+ if self.logger.isEnabledFor(logging.DEBUG):
164
+ self.logger.debug(
165
+ f"Response: {response.status_code} {response.reason_phrase}\n"
166
+ f"Headers: {dict(response.headers)}\n"
167
+ f"Body: {response.text}"
168
+ )
169
+ try:
170
+ response.raise_for_status()
171
+ except httpx.HTTPStatusError as e:
172
+ content_type = e.response.headers.get("content-type", "")
173
+ if "xml" in content_type:
174
+ check_for_xml_errors(e.response.text)
175
+ elif "json" in content_type:
176
+ check_for_json_errors(e.response.text)
177
+ raise
178
+ return response
@@ -0,0 +1,64 @@
1
+ from typing import Any, Dict
2
+
3
+ import httpx
4
+
5
+ from ..configs import CatalogConfig
6
+ from ..operations.catalog import (
7
+ AsyncCategoryOperations,
8
+ AsyncProductOperations,
9
+ AsyncRecommendationOperations,
10
+ AsyncStoreOperations,
11
+ CategoryOperations,
12
+ ProductOperations,
13
+ RecommendationOperations,
14
+ StoreOperations,
15
+ )
16
+ from .base import AsyncClient, Client
17
+
18
+
19
+ class CatalogClient(Client):
20
+ """Synchronous client for the Best Buy Catalog API."""
21
+
22
+ config_parser = CatalogConfig
23
+
24
+ def __init__(
25
+ self,
26
+ client: httpx.Client | None = None,
27
+ options: Dict[str, Any] | CatalogConfig | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ super().__init__(client, options, **kwargs)
31
+ self.products = ProductOperations(self)
32
+ self.categories = CategoryOperations(self)
33
+ self.recommendations = RecommendationOperations(self)
34
+ self.stores = StoreOperations(self)
35
+
36
+ @property
37
+ def base_url(self):
38
+ if self.options.base_url is not None:
39
+ return self.options.base_url
40
+ return "https://api.bestbuy.com"
41
+
42
+
43
+ class AsyncCatalogClient(AsyncClient):
44
+ """Asynchronous client for the Best Buy Catalog API."""
45
+
46
+ config_parser = CatalogConfig
47
+
48
+ def __init__(
49
+ self,
50
+ client: httpx.AsyncClient | None = None,
51
+ options: Dict[str, Any] | CatalogConfig | None = None,
52
+ **kwargs: Any,
53
+ ) -> None:
54
+ super().__init__(client, options, **kwargs)
55
+ self.products = AsyncProductOperations(self)
56
+ self.categories = AsyncCategoryOperations(self)
57
+ self.recommendations = AsyncRecommendationOperations(self)
58
+ self.stores = AsyncStoreOperations(self)
59
+
60
+ @property
61
+ def base_url(self):
62
+ if self.options.base_url is not None:
63
+ return self.options.base_url
64
+ return "https://api.bestbuy.com"
@@ -0,0 +1,74 @@
1
+ from typing import Any, Dict
2
+
3
+ import httpx
4
+
5
+ from ..configs import CommerceConfig
6
+ from ..operations.commerce import (
7
+ AsyncAuthOperations,
8
+ AsyncEncryptionOperations,
9
+ AsyncFulfillmentOperations,
10
+ AsyncOrderOperations,
11
+ AsyncPricingOperations,
12
+ AuthOperations,
13
+ EncryptionOperations,
14
+ FulfillmentOperations,
15
+ OrderOperations,
16
+ PricingOperations,
17
+ )
18
+ from .base import AsyncClient, Client
19
+
20
+
21
+ class CommerceClient(Client):
22
+ config_parser = CommerceConfig
23
+ options: CommerceConfig
24
+
25
+ def __init__(
26
+ self,
27
+ client: httpx.Client | None = None,
28
+ options: Dict[str, Any] | CommerceConfig | None = None,
29
+ **kwargs: Any,
30
+ ) -> None:
31
+ super().__init__(client, options, **kwargs)
32
+ self.auth = AuthOperations(self)
33
+ self.fulfillment = FulfillmentOperations(self)
34
+ self.pricing = PricingOperations(self)
35
+ self.orders = OrderOperations(self)
36
+ self.encryption = EncryptionOperations(self)
37
+
38
+ @property
39
+ def base_url(self):
40
+ if self.options.base_url is not None:
41
+ return self.options.base_url
42
+ return (
43
+ "https://commerce-ssl.sandbox.bestbuy.com"
44
+ if self.options.sandbox is True
45
+ else "https://commerce-ssl.bestbuy.com"
46
+ )
47
+
48
+
49
+ class AsyncCommerceClient(AsyncClient):
50
+ config_parser = CommerceConfig
51
+ options: CommerceConfig
52
+
53
+ def __init__(
54
+ self,
55
+ client: httpx.AsyncClient | None = None,
56
+ options: Dict[str, Any] | CommerceConfig | None = None,
57
+ **kwargs: Any,
58
+ ) -> None:
59
+ super().__init__(client, options, **kwargs)
60
+ self.auth = AsyncAuthOperations(self)
61
+ self.fulfillment = AsyncFulfillmentOperations(self)
62
+ self.pricing = AsyncPricingOperations(self)
63
+ self.orders = AsyncOrderOperations(self)
64
+ self.encryption = AsyncEncryptionOperations(self)
65
+
66
+ @property
67
+ def base_url(self):
68
+ if self.options.base_url is not None:
69
+ return self.options.base_url
70
+ return (
71
+ "https://commerce-ssl.sandbox.bestbuy.com"
72
+ if self.options.sandbox is True
73
+ else "https://commerce-ssl.bestbuy.com"
74
+ )
@@ -0,0 +1,5 @@
1
+ from .base import BaseConfig
2
+ from .catalog import CatalogConfig
3
+ from .commerce import CommerceConfig
4
+
5
+ __all__ = ["BaseConfig", "CatalogConfig", "CommerceConfig"]
@@ -0,0 +1,14 @@
1
+ import logging
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class BaseConfig(BaseModel):
7
+ model_config = ConfigDict(arbitrary_types_allowed=True)
8
+
9
+ api_key: str
10
+ base_url: str | None = None
11
+ content_type: str | None = None
12
+ log_level: int = logging.WARNING
13
+ logger: logging.Logger | None = None
14
+ timeout_ms: int = 60_000
@@ -0,0 +1,6 @@
1
+ from .base import BaseConfig
2
+
3
+
4
+ class CatalogConfig(BaseConfig):
5
+ base_url: str = "https://api.bestbuy.com"
6
+ content_type: str = "application/json"
@@ -0,0 +1,10 @@
1
+ from .base import BaseConfig
2
+
3
+
4
+ class CommerceConfig(BaseConfig):
5
+ auto_logout: bool = False
6
+ content_type: str = "application/xml"
7
+ partner_id: str | None = None
8
+ password: str | None = None
9
+ sandbox: bool = True
10
+ username: str | None = None
bestbuy/exceptions.py ADDED
@@ -0,0 +1,59 @@
1
+ """Custom exceptions for the Best Buy API client."""
2
+
3
+
4
+ class BestBuyError(Exception):
5
+ """Base exception for all Best Buy API errors."""
6
+
7
+ pass
8
+
9
+
10
+ class ConfigError(BestBuyError):
11
+ """Raised when there's an error with client configuration."""
12
+
13
+ pass
14
+
15
+
16
+ class AuthenticationError(BestBuyError):
17
+ """Raised when authentication fails."""
18
+
19
+ pass
20
+
21
+
22
+ class SessionRequiredError(BestBuyError):
23
+ """Raised when an operation requires an active session but none exists."""
24
+
25
+ pass
26
+
27
+
28
+ class APIError(BestBuyError):
29
+ """Raised when the API returns an error.
30
+
31
+ Used to handle errors from both Commerce and Catalog APIs.
32
+
33
+ Attributes:
34
+ message: Human-readable error message
35
+ code: Error code from the API (if available)
36
+ sku: SKU that caused the error (if available)
37
+ response_text: Raw response text (XML or JSON)
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ message: str,
43
+ code: str | None = None,
44
+ sku: str | None = None,
45
+ response_text: str | None = None,
46
+ ):
47
+ self.message = message
48
+ self.code = code
49
+ self.sku = sku
50
+ self.response_text = response_text
51
+
52
+ # Build error message
53
+ parts = [message]
54
+ if code:
55
+ parts.append(f"Error code: {code}")
56
+ if sku:
57
+ parts.append(f"SKU: {sku}")
58
+
59
+ super().__init__(" | ".join(parts))
bestbuy/loggers.py ADDED
@@ -0,0 +1,11 @@
1
+ import logging
2
+ from logging import Logger
3
+
4
+
5
+ def make_console_logger() -> Logger:
6
+ logger = logging.getLogger(__package__)
7
+ handler = logging.StreamHandler()
8
+ formatter = logging.Formatter(logging.BASIC_FORMAT)
9
+ handler.setFormatter(formatter)
10
+ logger.addHandler(handler)
11
+ return logger
@@ -0,0 +1,205 @@
1
+ from .catalog import (
2
+ AccessorySku,
3
+ BundledProduct,
4
+ CatalogStore,
5
+ CatalogStoresResponse,
6
+ CategoriesResponse,
7
+ Category,
8
+ CategoryPathItem,
9
+ Contract,
10
+ ContractPrice,
11
+ ContractTerm,
12
+ GiftSku,
13
+ IncludedItem,
14
+ LanguageOption,
15
+ MemberSku,
16
+ Offer,
17
+ PlaybackFormat,
18
+ Product,
19
+ ProductDetail,
20
+ ProductFeature,
21
+ ProductImage,
22
+ ProductList,
23
+ ProductsResponse,
24
+ ProductVariant,
25
+ ProductVariation,
26
+ RecommendationsContext,
27
+ RecommendationsMetadata,
28
+ RecommendationsResponse,
29
+ RecommendationsResultSet,
30
+ RecommendationCustomerReviews,
31
+ RecommendationDescriptions,
32
+ RecommendationImages,
33
+ RecommendationLinks,
34
+ RecommendationNames,
35
+ RecommendationPrices,
36
+ RecommendedProduct,
37
+ RequiredPart,
38
+ ShippingInfo,
39
+ ShippingLevelOfService,
40
+ StoreDetailedHours,
41
+ StoreService,
42
+ SubCategory,
43
+ )
44
+ from .commerce import (
45
+ AddressFulfillment,
46
+ ApiError,
47
+ ApiErrorMessage,
48
+ ApiErrors,
49
+ AvailabilityQuery,
50
+ AvailabilityQueryRequest,
51
+ AvailabilityQueryResponse,
52
+ BillingAddress,
53
+ CCTender,
54
+ Cart,
55
+ CartTotal,
56
+ CommerceResponse,
57
+ CreditCard,
58
+ DeliveryDateQuery,
59
+ DeliveryDateQueryItem,
60
+ DeliveryDateResponse,
61
+ DeliveryOption,
62
+ DeliveryOptionsResponse,
63
+ DeliveryService,
64
+ DeliveryServicesResponse,
65
+ EstimateTaxQuery,
66
+ EstimatedTax,
67
+ FriendsFamilyDetails,
68
+ Fulfillment,
69
+ GuestCCTender,
70
+ GuestCreditCard,
71
+ GuestTender,
72
+ HomeDeliveryFulfillment,
73
+ IdMapEntry,
74
+ Link,
75
+ OrderAddress,
76
+ OrderItem,
77
+ OrderList,
78
+ OrderQueryRequest,
79
+ OrderResponse,
80
+ OrderSubmitGuestRequest,
81
+ OrderSubmitRegisteredRequest,
82
+ OrderSubmitResponse,
83
+ OrderStatus,
84
+ PriceQueryRequest,
85
+ PriceResponse,
86
+ ProductServiceRequest,
87
+ ProductServiceResponse,
88
+ PublicKeyEncryptionResponse,
89
+ RegisteredCCTender,
90
+ RegisteredTender,
91
+ ShippingAddress,
92
+ ShippingOption,
93
+ ShippingOptionsResponse,
94
+ ShippingQueryRequest,
95
+ ShippingQueryItem,
96
+ SimpleError,
97
+ Store,
98
+ StoreFulfillment,
99
+ StoreQuery,
100
+ StoresResponse,
101
+ Tender,
102
+ UnitPrice,
103
+ )
104
+
105
+ __all__ = [
106
+ "AccessorySku",
107
+ "BundledProduct",
108
+ "CatalogStore",
109
+ "CatalogStoresResponse",
110
+ "CategoriesResponse",
111
+ "Category",
112
+ "CategoryPathItem",
113
+ "Contract",
114
+ "ContractPrice",
115
+ "ContractTerm",
116
+ "GiftSku",
117
+ "IncludedItem",
118
+ "LanguageOption",
119
+ "MemberSku",
120
+ "Offer",
121
+ "PlaybackFormat",
122
+ "Product",
123
+ "ProductDetail",
124
+ "ProductFeature",
125
+ "ProductImage",
126
+ "ProductList",
127
+ "ProductsResponse",
128
+ "ProductVariant",
129
+ "ProductVariation",
130
+ "RecommendationsContext",
131
+ "RecommendationsMetadata",
132
+ "RecommendationsResponse",
133
+ "RecommendationsResultSet",
134
+ "RecommendationCustomerReviews",
135
+ "RecommendationDescriptions",
136
+ "RecommendationImages",
137
+ "RecommendationLinks",
138
+ "RecommendationNames",
139
+ "RecommendationPrices",
140
+ "RecommendedProduct",
141
+ "RequiredPart",
142
+ "ShippingInfo",
143
+ "ShippingLevelOfService",
144
+ "StoreDetailedHours",
145
+ "StoreService",
146
+ "SubCategory",
147
+ "AddressFulfillment",
148
+ "ApiError",
149
+ "ApiErrorMessage",
150
+ "ApiErrors",
151
+ "AvailabilityQuery",
152
+ "AvailabilityQueryRequest",
153
+ "AvailabilityQueryResponse",
154
+ "BillingAddress",
155
+ "CCTender",
156
+ "Cart",
157
+ "CartTotal",
158
+ "CommerceResponse",
159
+ "CreditCard",
160
+ "DeliveryDateQuery",
161
+ "DeliveryDateQueryItem",
162
+ "DeliveryDateResponse",
163
+ "DeliveryOption",
164
+ "DeliveryOptionsResponse",
165
+ "DeliveryService",
166
+ "DeliveryServicesResponse",
167
+ "EstimateTaxQuery",
168
+ "EstimatedTax",
169
+ "FriendsFamilyDetails",
170
+ "Fulfillment",
171
+ "GuestCCTender",
172
+ "GuestCreditCard",
173
+ "GuestTender",
174
+ "HomeDeliveryFulfillment",
175
+ "IdMapEntry",
176
+ "Link",
177
+ "OrderAddress",
178
+ "OrderItem",
179
+ "OrderList",
180
+ "OrderQueryRequest",
181
+ "OrderResponse",
182
+ "OrderSubmitGuestRequest",
183
+ "OrderSubmitRegisteredRequest",
184
+ "OrderSubmitResponse",
185
+ "OrderStatus",
186
+ "PriceQueryRequest",
187
+ "PriceResponse",
188
+ "ProductServiceRequest",
189
+ "ProductServiceResponse",
190
+ "PublicKeyEncryptionResponse",
191
+ "RegisteredCCTender",
192
+ "RegisteredTender",
193
+ "ShippingAddress",
194
+ "ShippingOption",
195
+ "ShippingOptionsResponse",
196
+ "ShippingQueryRequest",
197
+ "ShippingQueryItem",
198
+ "SimpleError",
199
+ "Store",
200
+ "StoreFulfillment",
201
+ "StoreQuery",
202
+ "StoresResponse",
203
+ "Tender",
204
+ "UnitPrice",
205
+ ]