Skip to content

Commit 0048f37

Browse files
authored
bump: 1.2.0 (#7)
2 parents 5522524 + 4a5c2f0 commit 0048f37

File tree

13 files changed

+356
-899
lines changed

13 files changed

+356
-899
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ build/
1111
*.egg-info
1212
instructions.txt
1313

14-
asyncTests.py
15-
tests.py
14+
*tests.py

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ data = walmart_io.catalog_product(category='3944', maxId='8342714')
7272

7373
A catalog response contains category, format, nextPage, totalPages, and a list of items
7474

75+
The returned API data is paginated. To get the next page, pass in `data.nextPage` as a kwarg.
76+
All other kwargs will be stripped out. They will be embed in the nextPage response.
77+
78+
```py
79+
data = walmart_io.catalog_product(category="3944", maxId="8342714")
80+
next_data = walmart_io.catalog_product(nextPage=data.nextPage)
81+
# or
82+
next_data = walmart_io.catalog_product(category="3944", maxId="8342714", nextPage=data.nextPage)
83+
```
84+
7585
### [Post Browsed Products](https://walmart.io/docs/affiliate/post-browsed-products)
7686

7787
The post browsed products API allows you to recommend products to someone based on their product viewing history.

pyproject.toml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "WIOpy"
3-
version = "1.1.1"
3+
version = "1.2.0"
44
description = "A Python wrapper for the Walmart IO API"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
license = { file = "LICENSE" }
77
authors = [{ name = "CoderJoshDK" }]
8-
requires-python = ">= 3.8"
8+
requires-python = ">=3.9"
99
dependencies = [
1010
"requests",
1111
"pycryptodome",
@@ -26,19 +26,23 @@ classifiers = [
2626
"Intended Audience :: Developers",
2727
"Natural Language :: English",
2828
"Operating System :: OS Independent",
29-
"Programming Language :: Python :: 3.8",
3029
"Programming Language :: Python :: 3.9",
3130
"Programming Language :: Python :: 3.10",
3231
"Programming Language :: Python :: 3.11",
32+
"Programming Language :: Python :: 3.12",
33+
"Programming Language :: Python :: 3.13",
3334
"Topic :: Internet",
3435
"Topic :: Software Development :: Libraries",
3536
"Topic :: Software Development :: Libraries :: Python Modules",
3637
"Topic :: Utilities",
3738
"Typing :: Typed",
3839
]
3940

40-
[project.optional-dependencies]
41-
dev = ["ruff", "pre-commit"]
41+
[dependency-groups]
42+
dev = [
43+
"basedpyright>=1.24.0",
44+
"ruff>=0.9.3",
45+
]
4246

4347
[project.urls]
4448
"Homepage" = "https://github.com/CoderJoshDK/WIOpy"
@@ -48,7 +52,7 @@ dev = ["ruff", "pre-commit"]
4852
[tool.ruff]
4953
line-length = 100
5054
fix = true
51-
target-version = "py38"
55+
target-version = "py39"
5256

5357
[tool.ruff.lint]
5458
select = ["E", "F", "B", "W", "Q", "A", "I", "D",
@@ -62,3 +66,17 @@ package = true
6266
[build-system]
6367
requires = ["hatchling"]
6468
build-backend = "hatchling.build"
69+
70+
[tool.basedpyright]
71+
include = ["wiopy"]
72+
exclude = [
73+
"**/__pycache__",
74+
"wiopy/typestubs"
75+
]
76+
defineConstant = { DEBUG = true }
77+
78+
reportMissingImports = "error"
79+
reportMissingTypeStubs = false
80+
reportAny = false
81+
reportExplicitAny = false
82+
reportImplicitStringConcatenation = false

uv.lock

Lines changed: 68 additions & 656 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wiopy/AsyncWIO.py

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
import datetime
1111
import logging
1212
import time
13-
from typing import Any, Generator
13+
from collections.abc import AsyncGenerator, Generator
14+
from typing import Any, Final
1415

1516
import aiohttp
1617
from Crypto.Hash import SHA256
1718
from Crypto.PublicKey import RSA
1819
from Crypto.Signature import PKCS1_v1_5
1920

20-
from .utils import InvalidRequestException, _get_items_ids, _ttl_cache
21+
from .utils import InvalidRequestException, get_items_ids, ttl_cache
2122
from .WalmartResponse import (
2223
WalmartCatalog,
2324
WalmartProduct,
@@ -54,7 +55,7 @@ class AsyncWalmartIO:
5455
5556
"""
5657

57-
__slots__ = (
58+
__slots__: tuple[str, ...] = (
5859
"_consumer_id",
5960
"_private_key",
6061
"_private_key_version",
@@ -65,7 +66,7 @@ class AsyncWalmartIO:
6566
"publisherId",
6667
)
6768

68-
ENDPOINT = "https://developer.api.walmart.com/api-proxy/service"
69+
ENDPOINT: Final[str] = "https://developer.api.walmart.com/api-proxy/service"
6970

7071
def __init__(
7172
self,
@@ -102,27 +103,29 @@ def __init__(
102103
The filename will look something like `./WM_IO_private_key.pem`
103104
104105
"""
105-
self._private_key_version = private_key_version
106+
self._private_key_version: Final[str] = private_key_version
106107

107108
# IOError is triggered if the file cannot be opened
108109
with open(private_key_filename) as f:
109-
self._private_key = RSA.importKey(f.read())
110+
self._private_key: Final[RSA.RsaKey] = RSA.importKey(f.read())
110111

111-
self._consumer_id = consumer_id
112+
self._consumer_id: Final[str] = consumer_id
112113

113-
self.headers = {}
114+
self.headers: dict[str, str] = {}
114115
self.headers["WM_CONSUMER.ID"] = consumer_id
115116
self.headers["WM_SEC.KEY_VERSION"] = private_key_version
116117

117-
self._update_daily_calls_time = datetime.datetime.now() + datetime.timedelta(days=1)
118-
self.daily_calls = daily_calls
119-
self.daily_calls_remaining = daily_calls
118+
self._update_daily_calls_time: datetime.datetime = (
119+
datetime.datetime.now() + datetime.timedelta(days=1)
120+
)
121+
self.daily_calls: int = daily_calls
122+
self.daily_calls_remaining: int = daily_calls
120123

121-
self.publisherId = publisherId or None
124+
self.publisherId: Final[str | None] = publisherId or None
122125

123126
log.info(f"Walmart IO connection with consumer id ending in {consumer_id[-6:]}")
124127

125-
async def catalog_product(self, **kwargs) -> WalmartCatalog:
128+
async def catalog_product(self, **kwargs: str | int | bool) -> WalmartCatalog:
126129
"""
127130
Catalog Product Endpoint.
128131
@@ -165,13 +168,15 @@ async def catalog_product(self, **kwargs) -> WalmartCatalog:
165168
https://www.walmart.io/docs/affiliate/catalog-product
166169
167170
"""
168-
if "nextPage" in kwargs:
169-
url = "https://developer.api.walmart.com" + kwargs.pop("nextPage")
170-
else:
171-
url = self.ENDPOINT + "/affil/product/v2/paginated/items"
171+
if next_page := kwargs.pop("nextPage", ""):
172+
assert isinstance(next_page, str), ValueError(
173+
"Expected type string for kwarg 'nextPage'"
174+
)
175+
url = "https://developer.api.walmart.com" + next_page
176+
return WalmartCatalog(await self._send_request(url))
172177

173-
response = await self._send_request(url, **kwargs)
174-
return WalmartCatalog(response)
178+
url = self.ENDPOINT + "/affil/product/v2/paginated/items"
179+
return WalmartCatalog(await self._send_request(url, **kwargs))
175180

176181
async def post_browsed_products(self, itemId: str) -> list[WalmartProduct]:
177182
"""
@@ -208,10 +213,10 @@ async def post_browsed_products(self, itemId: str) -> list[WalmartProduct]:
208213
209214
"""
210215
url = f"{self.ENDPOINT}/affil/product/v2/postbrowse?itemId={itemId}"
211-
response = await self._send_request(url)
216+
response: list[dict[str, Any]] = await self._send_request(url)
212217
return [WalmartProduct(item) for item in response]
213218

214-
async def product_lookup(self, ids: str | list[str], **kwargs) -> list[WalmartProduct]:
219+
async def product_lookup(self, ids: str | list[str], **kwargs: str) -> list[WalmartProduct]:
215220
"""
216221
Walmart product lookup.
217222
@@ -244,13 +249,13 @@ async def product_lookup(self, ids: str | list[str], **kwargs) -> list[WalmartPr
244249
url = self.ENDPOINT + "/affil/product/v2/items"
245250

246251
params = kwargs
247-
ids = _get_items_ids(ids)
252+
ids = get_items_ids(ids)
248253
if len(ids) > 200:
249254
log.debug(
250255
"For large id lists, try using bulk_product_lookup. "
251-
"It will continue to run even if one chunk of ids raise an error"
256+
"It will continue to run even if one chunk of ids raise an error",
252257
)
253-
products = []
258+
products: list[WalmartProduct] = []
254259

255260
for idGroup in self._get_product_id_chunk(list(set(ids)), 20):
256261
params["ids"] = idGroup
@@ -261,8 +266,12 @@ async def product_lookup(self, ids: str | list[str], **kwargs) -> list[WalmartPr
261266
return products
262267

263268
async def bulk_product_lookup(
264-
self, ids: str | list[str], amount: int = 20, retries: int = 1, **kwargs
265-
):
269+
self,
270+
ids: str | list[str],
271+
amount: int = 20,
272+
retries: int = 1,
273+
**kwargs: str,
274+
) -> AsyncGenerator[list[WalmartProduct]]:
266275
"""
267276
Walmart product lookup for a bulk of products.
268277
@@ -286,8 +295,6 @@ async def bulk_product_lookup(
286295
Your Impact Radius Advertisement Id
287296
campaignId:
288297
Your Impact Radius Campaign Id
289-
format:
290-
Type of response required, allowed values [json, xml(deprecated)]. Default is json.
291298
upc:
292299
upc of the item
293300
@@ -308,7 +315,7 @@ async def bulk_product_lookup(
308315
url = self.ENDPOINT + "/affil/product/v2/items"
309316

310317
params = kwargs
311-
ids = _get_items_ids(ids)
318+
ids = get_items_ids(ids)
312319

313320
# Clamp amount [1, 20]
314321
amount = min(max(1, amount), 20)
@@ -367,7 +374,7 @@ async def product_recommendation(self, itemId: str) -> list[WalmartProduct]:
367374
response = await self._send_request(url)
368375
return [WalmartProduct(item) for item in response]
369376

370-
async def reviews(self, itemId: str, **kwargs) -> WalmartReviewResponse:
377+
async def reviews(self, itemId: str, **kwargs: str) -> WalmartReviewResponse:
371378
"""
372379
Reviews Endpoint.
373380
@@ -405,7 +412,7 @@ async def reviews(self, itemId: str, **kwargs) -> WalmartReviewResponse:
405412
response = await self._send_request(url, **kwargs)
406413
return WalmartReviewResponse(response)
407414

408-
async def search(self, query: str, **kwargs) -> WalmartSearch:
415+
async def search(self, query: str, **kwargs: str | int) -> WalmartSearch:
409416
"""
410417
Search Endpoint.
411418
@@ -482,7 +489,7 @@ async def search(self, query: str, **kwargs) -> WalmartSearch:
482489
response = await self._send_request(url, **kwargs)
483490
return WalmartSearch(response)
484491

485-
async def stores(self, **kwargs) -> list[WalmartStore]:
492+
async def stores(self, **kwargs: float) -> list[WalmartStore]:
486493
"""
487494
Store Locator Endpoint.
488495
@@ -508,7 +515,7 @@ async def stores(self, **kwargs) -> list[WalmartStore]:
508515
response = await self._send_request(url, **kwargs)
509516
return [WalmartStore(store) for store in response]
510517

511-
async def taxonomy(self, **kwargs) -> WalmartTaxonomy:
518+
async def taxonomy(self) -> WalmartTaxonomy:
512519
"""
513520
Taxonomy Endpoint.
514521
@@ -524,16 +531,11 @@ async def taxonomy(self, **kwargs) -> WalmartTaxonomy:
524531
For example, Search API can be restricted to search within a category by supplying id as
525532
per the taxonomy.
526533
527-
Parameters
528-
----------
529-
**kwargs
530-
unknown; WalmartIO documentation does not expose what the acceptable
531-
532534
"""
533535
url = self.ENDPOINT + "/affil/product/v2/taxonomy"
534-
return WalmartTaxonomy(await self._send_request(url, **kwargs))
536+
return WalmartTaxonomy(await self._send_request(url))
535537

536-
async def trending(self, publisherId=None) -> list[WalmartProduct]:
538+
async def trending(self, publisherId: str | None = None) -> list[WalmartProduct]:
537539
"""
538540
Trending Items Endpoint.
539541
@@ -557,13 +559,13 @@ async def trending(self, publisherId=None) -> list[WalmartProduct]:
557559
url = self.ENDPOINT + "/affil/product/v2/trends"
558560

559561
if publisherId:
560-
response = await self._send_request(url, publisherId=publisherId)
562+
response: dict[str, Any] = await self._send_request(url, publisherId=publisherId)
561563
else:
562564
response = await self._send_request(url)
563565
return [WalmartProduct(item) for item in response["items"]]
564566

565-
@_ttl_cache(maxsize=2, ttl=170)
566-
def _get_headers(self) -> dict:
567+
@ttl_cache(maxsize=2, ttl=170)
568+
def _get_headers(self) -> dict[str, str]:
567569
"""
568570
Get the headers required for making an API call.
569571
@@ -609,7 +611,7 @@ def _get_headers(self) -> dict:
609611

610612
return self.headers
611613

612-
async def _send_request(self, url, **kwargs) -> Any:
614+
async def _send_request(self, url: str, **kwargs: str | int | float | bool) -> Any:
613615
"""
614616
Send a request to the Walmart API and return the HTTP response.
615617
@@ -637,13 +639,13 @@ async def _send_request(self, url, **kwargs) -> Any:
637639
log.debug(f"Making connection to {url}")
638640

639641
# Avoid format to be changed, always go for json
640-
kwargs.pop("format", None)
642+
_ = kwargs.pop("format", None)
641643
request_params = kwargs
642644

643645
# Convert from native boolean python type to string 'true' or 'false'. This allows to set
644646
# richAttributes with python boolean types
645647
if "richAttributes" in request_params and isinstance(
646-
request_params["richAttributes"], bool
648+
request_params.get("richAttributes"), bool
647649
):
648650
if request_params["richAttributes"]:
649651
request_params["richAttributes"] = "true"
@@ -661,16 +663,17 @@ async def _send_request(self, url, **kwargs) -> Any:
661663
"Too many calls in one day. If this is incorrect, try increasing `daily_calls`"
662664
)
663665

664-
async with aiohttp.ClientSession() as session, session.get(
665-
url, headers=self._get_headers(), params=request_params
666-
) as response:
666+
async with (
667+
aiohttp.ClientSession() as session,
668+
session.get(url, headers=self._get_headers(), params=request_params) as response,
669+
):
667670
status_code = response.status
668671
if status_code in (200, 201):
669672
return await response.json()
670673

671674
if status_code == 400:
672675
# Send exception detail when it is a 400 bad error
673-
jsonData = await response.json()
676+
jsonData: dict[str, Any] = await response.json()
674677
raise InvalidRequestException(status_code, detail=jsonData["errors"][0]["message"])
675678
raise InvalidRequestException(status_code)
676679

0 commit comments

Comments
 (0)