Skip to content

Instantly share code, notes, and snippets.

@philipjewell
Last active December 14, 2021 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save philipjewell/1090bf397a3965cc7a60497b486b369a to your computer and use it in GitHub Desktop.
Save philipjewell/1090bf397a3965cc7a60497b486b369a to your computer and use it in GitHub Desktop.
Finding which Walmart locations nearby have inventory of a product
#!/usr/bin/python3
from datetime import datetime
import click
import random
import requests
BASE_URL = "https://www.walmart.com/grocery"
# Current Chrome user agent on Mac
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
item_dict = {
'ps5': [987823383, 165545420], # one is the digital, the other is the disc
'bblr': [605417669, 355449356, 261945169],
}
class WalmartGroceryInventory():
def __init__(self, zipcode, number_of_stores=10, user_agent=USER_AGENT, verbose=False):
self.zipcode = zipcode
self.user_agent = user_agent
self.stores = self.get_stores(number_of_stores)
self.store_ids = list(self.stores.keys())
self.verbose = verbose
def walmart_request(self, endpoint, base_url=BASE_URL):
"""Request wrapper for Walmart api requests.
:param endpoint: str, the URI for the request
:return response: dict
"""
# TODO: Add retry wrapper that changes the user agent?
headers = {'content-type': 'application/json', "User-Agent": self.user_agent}
try:
response = requests.get(f"{base_url}/{endpoint}", headers=headers).json()
except:
print("Request did not work, sorry.")
return False
return response
def get_stores(self, n):
"""Build a dictionary with store data based on the zipcode.
:param n: number of entries we are looking at
:return store: dict, {store_id: store_address
}"""
endpoint = f'v4/api/serviceAvailability?postalCode={self.zipcode}'
# TODO: Write results of zipcode to a cache file so subsequent lookups dont require another api request
resp = self.walmart_request(endpoint)
print(
f'Filtering results to the top {n} locations near the zipcode: {self.zipcode} - {resp["geo"]["city"]}, {resp["geo"]["state"]}...')
stores = {}
for entry in resp['accessPointList'][:n]:
stores[entry["dispenseStoreId"]] = entry['address']['line1']
return stores
def build_inventory(self, item_id):
"""Build inventory based on list of stores."""
inventory = {}
no_inventory = {}
for store_id in self.store_ids:
if self.has_inventory(store_id=store_id, item_id=item_id):
inventory[store_id] = item_id
else:
no_inventory[store_id] = item_id
return inventory, no_inventory
def has_inventory(self, store_id, item_id):
"""Get inventory for an item from individual stores."""
endpoint = f'/v3/api/products/{item_id}?itemFields=all&storeId={store_id}'
store_inventory = self.walmart_request(endpoint)
result = None
try:
result = not store_inventory.get('basic').get('isOutOfStock')
except AttributeError:
# I believe this happens after a few requests from the same IP address
if self.verbose:
print("Got hit by the captcha, cannot programatically pull inventory.")
print(f"Navigate to and check the 'isOutOfStock' key: {BASE_URL}/{endpoint}")
return result
def rand(n):
"""Random number to the hundreds"""
return random.randint(0, int("9"*n))
def generate_user_agent():
return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{rand(2)}_{rand(1)}) AppleWebKit/{rand(3)}.{rand(2)} (KHTML, like Gecko) Chrome/95.0.{rand(4)}.{rand(2)} Safari/{rand(3)}.{rand(2)}"
@click.command()
@click.option('-i', '--item', default=None, type=click.Choice(item_dict.keys()),
help='Desired item you are searching for')
@click.option('-z', '--zipcode', default=78109, type=int, help='Desired zipcode you are searching in')
@click.option('-v', '--verbose', default=False, is_flag=True, help='Print more verbose details')
@click.option('-r', '--random-agent', default=False, is_flag=True, help='Randomizes your User Agent for the requests')
def main(item, zipcode, verbose, random_agent):
user_agent = generate_user_agent() if random_agent else USER_AGENT
walmart_grocery_inventory = WalmartGroceryInventory(zipcode=zipcode, user_agent=user_agent, verbose=verbose)
print(datetime.now().strftime("%Y-%m-%d %H:%M"))
for item_id in item_dict[item]:
# simplify this, remove the 'without' - just have it be the negative of the 'with'?
with_inventory, without_inventory = walmart_grocery_inventory.build_inventory(item_id=item_id)
inventory_addresses = []
without_inventory_addresses = []
for store_id in with_inventory.keys():
inventory_addresses.append(walmart_grocery_inventory.stores.get(store_id))
for store_id in without_inventory.keys():
without_inventory_addresses.append(walmart_grocery_inventory.stores.get(store_id))
print(f"Inventory for {item_id}: {inventory_addresses}")
if verbose:
print(f"No inventory: {without_inventory_addresses}")
if __name__ == "__main__":
main()
@philipjewell
Copy link
Author

Recommend only using this a couple times a day. There is a threshold that their site has: once that threshold is exceeded, will use captcha codes when checking for the inventory - thus removing some of the larger benefits of this script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment