From 75bb1ccc55e3d3560b0b7ffb43ce31d91cf91d9b Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Fri, 20 Mar 2026 14:17:33 -0700 Subject: [PATCH 1/2] Deleted Unused Walmart and Kroger Pipelines --- KrogerPipeline/kroger_catalogue.js | 1232 ---------------- KrogerPipeline/kroger_stores.js | 1065 -------------- PlayWright/scraper.py | 487 +++++++ .../Categories/Food_Grocery_subtree.csv | 22 - WalmartPipeline/Categories/Food_subtree.csv | 1246 ----------------- WalmartPipeline/TaxonomyBrowsing/Note.txt | 2 - .../TaxonomyBrowsing/browseTaxonomyExport.js | 251 ---- .../TaxonomyBrowsing/generateTaxonomy.js | 119 -- WalmartPipeline/classify_ingredients.py | 861 ------------ WalmartPipeline/deleteEmptyCSVs.js | 64 - WalmartPipeline/fetchProducts.js | 548 -------- WalmartPipeline/runningClassifyIngredients.md | 102 -- runner.js | 318 ----- schema.sql | 110 -- supabase_import.js | 408 ------ 15 files changed, 487 insertions(+), 6348 deletions(-) delete mode 100644 KrogerPipeline/kroger_catalogue.js delete mode 100644 KrogerPipeline/kroger_stores.js create mode 100644 PlayWright/scraper.py delete mode 100644 WalmartPipeline/Categories/Food_Grocery_subtree.csv delete mode 100644 WalmartPipeline/Categories/Food_subtree.csv delete mode 100644 WalmartPipeline/TaxonomyBrowsing/Note.txt delete mode 100644 WalmartPipeline/TaxonomyBrowsing/browseTaxonomyExport.js delete mode 100644 WalmartPipeline/TaxonomyBrowsing/generateTaxonomy.js delete mode 100644 WalmartPipeline/classify_ingredients.py delete mode 100644 WalmartPipeline/deleteEmptyCSVs.js delete mode 100644 WalmartPipeline/fetchProducts.js delete mode 100644 WalmartPipeline/runningClassifyIngredients.md delete mode 100644 runner.js delete mode 100644 schema.sql delete mode 100644 supabase_import.js diff --git a/KrogerPipeline/kroger_catalogue.js b/KrogerPipeline/kroger_catalogue.js deleted file mode 100644 index a4d97af..0000000 --- a/KrogerPipeline/kroger_catalogue.js +++ /dev/null @@ -1,1232 +0,0 @@ -/** - * kroger_catalogue.js - * - * Finds Kroger stores near a zip code, searches all food categories at - * each store, and writes the results to food_catalogue.csv. - * - * Each product gets a price column (semicolon-separated) that matches the - * store_ids column order - so price[0] is the price at store_ids[0], etc. - * - * Usage: - * node kroger_catalogue.js --zipcode=90210 - * node kroger_catalogue.js --zipcode=90210 --stores=5 - * node kroger_catalogue.js --zipcode=90210 --dry-run - * - * Flags: - * --zipcode=XXXXX (required) zip code to find stores near - * --stores=10 max stores to query (default: 10, or "all") - * --radius=25 search radius in miles (default: 25) - * --dry-run only do 3 terms per category, good for testing - * --status print catalogue stats and exit - */ - -import dotenv from 'dotenv'; -dotenv.config(); -import fetch from 'node-fetch'; -import fs from 'fs'; -import path from 'path'; -import readline from 'readline'; - -// --- Configuration --- -const BASE_URL = 'https://api.kroger.com/v1'; -const TOKEN_URL = `${BASE_URL}/connect/oauth2/token`; -const PRODUCTS_URL = `${BASE_URL}/products`; -const LOCATIONS_URL = `${BASE_URL}/locations`; - -const PAGE_LIMIT = 50; -const MAX_PAGES = 20; -const REQUEST_DELAY_MS = 350; -const MAX_RETRIES = 3; -const RETRY_DELAY_MS = 2000; - -// --- CLI Args --- -const args = process.argv.slice(2); -function getArg(prefix) { - return ( - (args.find((a) => a.startsWith(prefix)) ?? '').replace(prefix, '') || '' - ); -} - -const outRoot = getArg('--out=') || './kroger_output'; -const zipcodeArg = getArg('--zipcode='); -const storesArg = getArg('--stores='); // number or "all" -const radiusArg = parseInt(getArg('--radius=') || '25', 10); -const categoryFilter = getArg('--categories='); -const isDryRun = args.includes('--dry-run'); -const statusOnly = args.includes('--status'); - -const catalogueDir = path.join(outRoot, 'catalogue'); -const catalogueFile = path.join(catalogueDir, 'food_catalogue.csv'); -const dryRunFile = path.join(catalogueDir, 'food_catalogue_dryrun.csv'); - -const maxStores = - storesArg === 'all' ? Infinity : parseInt(storesArg || '10', 10); - -// --- Food Categories --- -// --- Search Terms - aligned with Walmart pipeline classifier tags --- -// Keys ARE the classifier tags written to the CSV `classifier` column. -// Terms are drawn directly from the INGREDIENT_RULES keyword sets in the -// Walmart classifier, pruned to terms broad enough to return Kroger results. - -const FOOD_CATEGORIES = { - PRODUCE: [ - 'fresh vegetable', - 'fresh fruit', - 'organic vegetable', - 'organic fruit', - 'broccoli', - 'cauliflower', - 'spinach', - 'kale', - 'romaine', - 'mixed greens', - 'brussels sprout', - 'cabbage', - 'carrot', - 'celery', - 'cucumber', - 'zucchini', - 'bell pepper', - 'jalapeño', - 'cherry tomato', - 'roma tomato', - 'red onion', - 'yellow onion', - 'shallot', - 'scallion', - 'leek', - 'garlic bulb', - 'portobello', - 'shiitake', - 'cremini mushroom', - 'asparagus', - 'artichoke', - 'beet', - 'turnip', - 'parsnip', - 'sweet potato', - 'russet potato', - 'red potato', - 'yukon gold', - 'corn on cob', - 'green bean', - 'snap pea', - 'snow pea', - 'eggplant', - 'fennel', - 'radish', - 'apple', - 'pear', - 'orange', - 'lemon', - 'lime', - 'banana', - 'mango', - 'pineapple', - 'papaya', - 'kiwi', - 'strawberry', - 'blueberry', - 'raspberry', - 'blackberry', - 'watermelon', - 'cantaloupe', - 'peach', - 'nectarine', - 'plum', - 'cherry', - 'avocado', - 'pomegranate', - ], - - FRESH_HERB: [ - 'fresh basil', - 'fresh parsley', - 'fresh cilantro', - 'fresh thyme', - 'fresh rosemary', - 'fresh mint', - 'fresh dill', - 'fresh chives', - 'fresh tarragon', - 'fresh oregano', - 'fresh sage', - 'fresh lemongrass', - 'fresh ginger root', - ], - - PROTEIN: [ - 'chicken breast', - 'chicken thigh', - 'chicken wing', - 'chicken drumstick', - 'ground chicken', - 'whole chicken', - 'ground turkey', - 'turkey breast', - 'ground beef', - 'beef chuck', - 'beef brisket', - 'ribeye', - 'sirloin', - 'flank steak', - 'skirt steak', - 'beef roast', - 'beef short rib', - 'pork chop', - 'pork loin', - 'pork belly', - 'pork shoulder', - 'pork tenderloin', - 'baby back rib', - 'spiral ham', - 'ham steak', - 'lamb chop', - 'lamb leg', - 'ground lamb', - 'salmon fillet', - 'tuna fillet', - 'tilapia', - 'cod fillet', - 'halibut', - 'mahi mahi', - 'sea bass', - 'trout', - 'catfish', - 'shrimp', - 'scallop', - 'lobster tail', - 'crab leg', - 'crab meat', - 'clam', - 'mussel', - 'oyster', - 'bacon', - 'pancetta', - 'prosciutto', - 'salami', - 'pepperoni', - 'chorizo', - 'andouille', - 'bratwurst', - 'italian sausage', - 'breakfast sausage', - 'deli turkey', - 'deli ham', - 'deli roast beef', - 'deli chicken', - 'lunch meat', - 'extra firm tofu', - 'silken tofu', - 'tempeh', - 'seitan', - 'edamame', - 'black bean', - 'pinto bean', - 'kidney bean', - 'chickpea', - 'lentil', - 'split pea', - 'navy bean', - 'cannellini bean', - 'large eggs', - 'cage free egg', - 'organic egg', - 'egg whites', - ], - - DAIRY: [ - 'whole milk', - 'skim milk', - '2% milk', - 'lactose free milk', - 'organic milk', - 'buttermilk', - 'evaporated milk', - 'condensed milk', - 'powdered milk', - 'heavy cream', - 'heavy whipping cream', - 'half and half', - 'light cream', - 'sour cream', - 'creme fraiche', - 'cream cheese', - 'mascarpone', - 'ricotta', - 'cottage cheese', - 'fresh mozzarella', - 'burrata', - 'cheddar cheese', - 'parmesan', - 'romano cheese', - 'asiago', - 'gruyere', - 'swiss cheese', - 'gouda', - 'havarti', - 'fontina', - 'provolone', - 'brie', - 'camembert', - 'gorgonzola', - 'blue cheese', - 'feta cheese', - 'queso fresco', - 'monterey jack', - 'pepper jack', - 'unsalted butter', - 'salted butter', - 'european butter', - 'ghee', - 'greek yogurt', - 'plain yogurt', - 'whole milk yogurt', - 'skyr', - 'kefir', - ], - - GRAIN: [ - 'all purpose flour', - 'bread flour', - 'whole wheat flour', - 'cake flour', - 'almond flour', - 'coconut flour', - 'oat flour', - 'rye flour', - 'chickpea flour', - 'rice flour', - 'cassava flour', - 'white rice', - 'brown rice', - 'jasmine rice', - 'basmati rice', - 'arborio rice', - 'wild rice', - 'spaghetti', - 'penne', - 'rigatoni', - 'fusilli', - 'farfalle', - 'linguine', - 'fettuccine', - 'angel hair', - 'orzo', - 'macaroni', - 'lasagna noodle', - 'egg noodle', - 'ramen noodle', - 'soba noodle', - 'udon noodle', - 'rice noodle', - 'rolled oats', - 'quick oats', - 'steel cut oats', - 'cornmeal', - 'polenta', - 'grits', - 'semolina', - 'panko', - 'plain breadcrumb', - 'sandwich bread', - 'whole wheat bread', - 'sourdough bread', - 'french bread', - 'pita bread', - 'naan', - 'flatbread', - 'flour tortilla', - 'corn tortilla', - 'quinoa', - 'farro', - 'bulgur', - 'couscous', - 'barley', - 'millet', - ], - - BAKING: [ - 'baking soda', - 'baking powder', - 'cream of tartar', - 'active dry yeast', - 'instant yeast', - 'vanilla extract', - 'almond extract', - 'peppermint extract', - 'cocoa powder', - 'dutch process cocoa', - 'chocolate chips', - 'white chocolate chips', - 'baking chocolate', - 'powdered sugar', - 'granulated sugar', - 'cane sugar', - 'brown sugar', - 'turbinado sugar', - 'demerara sugar', - 'corn syrup', - 'molasses', - 'cake mix', - 'brownie mix', - 'pancake mix', - 'waffle mix', - 'muffin mix', - ], - - SPICE: [ - 'black pepper', - 'white pepper', - 'peppercorn', - 'sea salt', - 'kosher salt', - 'himalayan salt', - 'garlic salt', - 'garlic powder', - 'onion powder', - 'cumin', - 'paprika', - 'smoked paprika', - 'chili powder', - 'cayenne', - 'red pepper flake', - 'cinnamon', - 'nutmeg', - 'oregano', - 'thyme', - 'rosemary', - 'basil dried', - 'bay leaf', - 'turmeric', - 'coriander', - 'fennel seed', - 'cardamom', - 'clove', - 'allspice', - 'ground ginger', - 'mustard seed', - 'ground mustard', - 'fenugreek', - 'sumac', - "za'atar", - 'herbs de provence', - 'italian seasoning', - 'cajun seasoning', - 'taco seasoning', - 'curry powder', - 'garam masala', - 'ras el hanout', - 'five spice', - 'lemon pepper', - 'steak seasoning', - 'bbq rub', - 'vanilla bean', - 'saffron', - 'dill weed', - 'marjoram', - ], - - OIL_FAT: [ - 'olive oil', - 'extra virgin olive oil', - 'vegetable oil', - 'canola oil', - 'sunflower oil', - 'safflower oil', - 'corn oil', - 'soybean oil', - 'peanut oil', - 'grapeseed oil', - 'avocado oil', - 'coconut oil', - 'sesame oil', - 'toasted sesame oil', - 'walnut oil', - 'flaxseed oil', - 'truffle oil', - 'cooking spray', - 'nonstick spray', - 'shortening', - 'lard', - 'duck fat', - 'beef tallow', - 'vegan butter', - ], - - CONDIMENT: [ - 'soy sauce', - 'tamari', - 'liquid aminos', - 'coconut aminos', - 'fish sauce', - 'oyster sauce', - 'hoisin sauce', - 'worcestershire sauce', - 'hot sauce', - 'sriracha', - 'tabasco', - 'cholula', - 'sambal oelek', - 'gochujang', - 'apple cider vinegar', - 'white vinegar', - 'red wine vinegar', - 'white wine vinegar', - 'balsamic vinegar', - 'rice vinegar', - 'malt vinegar', - 'dijon mustard', - 'whole grain mustard', - 'yellow mustard', - 'ketchup', - 'mayonnaise', - 'relish', - 'bbq sauce', - 'barbecue sauce', - 'steak sauce', - 'buffalo sauce', - 'teriyaki sauce', - 'ponzu sauce', - 'sweet chili sauce', - 'stir fry sauce', - 'tahini', - 'miso paste', - 'tomato paste', - 'marinara sauce', - 'pasta sauce', - 'alfredo sauce', - 'pesto', - 'enchilada sauce', - 'salsa verde', - 'salsa jar', - 'pickle', - 'dill pickle', - 'pickled jalapeno', - 'giardiniera', - 'capers', - 'sun dried tomato', - 'roasted red pepper', - 'horseradish', - 'wasabi paste', - ], - - CANNED_GOOD: [ - 'canned tomato', - 'diced tomato', - 'crushed tomato', - 'whole peeled tomato', - 'san marzano', - 'fire roasted tomato', - 'canned black bean', - 'canned chickpea', - 'canned kidney bean', - 'canned pinto bean', - 'canned navy bean', - 'canned cannellini', - 'canned corn', - 'canned pumpkin', - 'canned artichoke', - 'canned mushroom', - 'canned water chestnut', - 'canned green bean', - 'coconut milk can', - 'coconut cream', - 'chicken broth', - 'beef broth', - 'vegetable broth', - 'chicken stock', - 'beef stock', - 'bone broth', - 'canned tuna', - 'canned salmon', - 'canned sardine', - 'canned anchovy', - 'canned crab', - 'canned clam', - 'chipotle in adobo', - 'green chili can', - ], - - SWEETENER: [ - 'honey', - 'raw honey', - 'manuka honey', - 'maple syrup', - 'pure maple syrup', - 'agave nectar', - 'date syrup', - 'stevia', - 'monk fruit sweetener', - 'erythritol', - ], - - NUT_SEED: [ - 'raw almonds', - 'sliced almonds', - 'slivered almonds', - 'walnut halves', - 'pecans', - 'cashews', - 'pistachios', - 'pine nuts', - 'hazelnuts', - 'macadamia nut', - 'brazil nut', - 'peanut butter', - 'almond butter', - 'cashew butter', - 'sunflower seed', - 'pumpkin seed', - 'pepita', - 'sesame seed', - 'chia seed', - 'flaxseed', - 'hemp seed', - 'poppy seed', - ], - - THICKENER: [ - 'cornstarch', - 'arrowroot powder', - 'tapioca starch', - 'unflavored gelatin', - 'agar agar', - 'xanthan gum', - 'guar gum', - 'pectin', - ], - - ALCOHOL: [ - 'cooking wine', - 'dry sherry', - 'mirin', - 'sake', - 'rice wine', - 'shaoxing wine', - ], - - OTHER_INGR: [ - 'nutritional yeast', - 'dried mushroom', - 'nori sheet', - 'kombu', - 'wakame', - 'dashi', - 'bonito flake', - 'matcha powder', - 'rose water', - 'liquid smoke', - 'raisins', - 'dried cranberry', - 'dried apricot', - 'dried fig', - 'dried mango', - 'dried date', - 'canned peach', - 'canned pear', - 'canned pineapple', - 'lemon juice', - 'lime juice', - 'jam', - 'jelly', - 'fruit preserves', - 'marmalade', - 'chutney', - 'caramel sauce', - 'sweetened condensed milk', - 'cream of mushroom soup', - 'cream of chicken soup', - 'harissa', - 'red curry paste', - 'green curry paste', - 'yellow curry paste', - 'coconut butter', - 'cacao nibs', - 'vital wheat gluten', - 'citric acid', - ], -}; - -// --- CSV Schema --- -const CSV_HEADERS = [ - // ── Product-level fields ────────────────────────────────────────────────── - 'productId', - 'upc', - 'brand', - 'description', - 'categories', - 'countryOrigin', - 'aisleLocations', - 'itemsFacets', - 'image_url', - // ── Item-level fields (items[0]) ────────────────────────────────────────── - 'itemId', - 'size', - 'soldBy', - 'soldInStore', - 'favorite', - 'temperature', // indicator string (e.g. "Refrigerated") - 'temperature_heatSensitive', - 'price_regular', // base price from the API - 'price_promo', // promotional price if active - 'fulfillment_inStore', - 'fulfillment_curbside', - 'fulfillment_delivery', - 'fulfillment_shipGrocery', - 'discount_hasDiscount', - 'discount_digital', - 'discount_inStore', - // ── Pipeline metadata ───────────────────────────────────────────────────── - 'classifier', // ← Walmart classifier tag (PRODUCE, PROTEIN, DAIRY, etc.) - 'search_keyword', // ← the specific term that first found this product - 'store_ids', // ← semicolon-separated locationIds (in order found) - 'price', // ← semicolon-separated per-store prices matching store_ids order -]; - -// --- Token Manager --- -class TokenManager { - constructor(clientId, clientSecret) { - if (!clientId || !clientSecret) - throw new Error( - 'Missing KROGER_CLIENT_ID or KROGER_CLIENT_SECRET in .env', - ); - this.credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( - 'base64', - ); - this.accessToken = null; - this.expiresAt = 0; - } - - async getToken() { - if (this.accessToken && Date.now() < this.expiresAt - 60_000) - return this.accessToken; - process.stdout.write(' Refreshing token... '); - const res = await fetch(TOKEN_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${this.credentials}`, - }, - body: 'grant_type=client_credentials&scope=product.compact', - }); - if (!res.ok) - throw new Error(`Token failed (${res.status}): ${await res.text()}`); - const data = await res.json(); - this.accessToken = data.access_token; - this.expiresAt = Date.now() + data.expires_in * 1000; - console.log(`OK (${Math.round(data.expires_in / 60)} min)`); - return this.accessToken; - } -} - -// --- Helpers --- -function sleep(ms) { - return new Promise((r) => setTimeout(r, ms)); -} - -function esc(v) { - if (v === null || v === undefined) return ''; - const s = String(v); - return s.includes(',') || s.includes('"') || s.includes('\n') - ? `"${s.replace(/"/g, '""')}"` - : s; -} - -function parseCsvLine(line) { - const cols = []; - let cur = '', - inQ = false; - for (let i = 0; i < line.length; i++) { - const ch = line[i]; - if (ch === '"') { - if (inQ && line[i + 1] === '"') { - cur += '"'; - i++; - } else inQ = !inQ; - } else if (ch === ',' && !inQ) { - cols.push(cur); - cur = ''; - } else cur += ch; - } - cols.push(cur); - return cols; -} - -async function* streamCsvRows(filePath) { - const rl = readline.createInterface({ - input: fs.createReadStream(filePath), - crlfDelay: Infinity, - }); - let headers = null; - for await (const line of rl) { - if (!line.trim()) continue; - const cols = parseCsvLine(line); - if (!headers) { - headers = cols; - continue; - } - const row = {}; - headers.forEach((h, i) => { - row[h] = cols[i] ?? ''; - }); - yield row; - } -} - -function productToFields(p, classifier = '', searchKeyword = '') { - const items = p.items?.[0] ?? {}; - const frontImg = (p.images ?? []).find( - (img) => img.perspective === 'front' && img.featured, - ); - const imgUrl = frontImg - ? ((frontImg.sizes ?? []).find((s) => s.id === 'large')?.url ?? - frontImg.sizes?.[0]?.url ?? - '') - : ''; - - const fulfillment = items.fulfillment ?? {}; - const discount = items.discount ?? {}; - - return { - // Product-level - productId: p.productId ?? '', - upc: p.upc ?? '', - brand: p.brand ?? '', - description: p.description ?? '', - categories: (p.categories ?? []).join('; '), - countryOrigin: p.countryOrigin ?? '', - aisleLocations: (p.aisleLocations ?? []) - .map((a) => a.description) - .join('; '), - itemsFacets: (p.itemsFacets ?? []).join('; '), - image_url: imgUrl, - // Item-level (items[0]) - itemId: items.itemId ?? '', - size: items.size ?? '', - soldBy: items.soldBy ?? '', - soldInStore: items.soldInStore ?? '', - favorite: items.favorite ?? '', - temperature: items.temperature?.indicator ?? '', - temperature_heatSensitive: items.temperature?.heatSensitive ?? '', - price_regular: items.price?.regular ?? '', - price_promo: items.price?.promo ?? '', - fulfillment_inStore: fulfillment.inStore ?? '', - fulfillment_curbside: fulfillment.curbside ?? '', - fulfillment_delivery: fulfillment.delivery ?? '', - fulfillment_shipGrocery: fulfillment.shipGrocery ?? '', - discount_hasDiscount: discount.hasDiscount ?? '', - discount_digital: discount.digital ?? '', - discount_inStore: discount.inStore ?? '', - // Pipeline metadata - classifier: classifier, - search_keyword: searchKeyword, - store_ids: '', // filled in during store enrichment (phase 2) - price: '', // per-store prices, semicolon-delimited, matches store_ids order - }; -} - -function rowToCsv(row) { - return CSV_HEADERS.map((h) => esc(row[h])).join(','); -} - -// --- API: Fetch products for a single search term --- -async function fetchProductsForTerm(term, locationId, tokenMgr, dryRun) { - const products = []; - const maxPages = dryRun ? 1 : MAX_PAGES; - - for (let page = 0; page < maxPages; page++) { - const params = new URLSearchParams({ - 'filter.term': term, - 'filter.limit': PAGE_LIMIT, - }); - // Only send filter.start for pages after the first - some Kroger - // endpoints reject start=1 even though it should be a valid value - if (page > 0) params.set('filter.start', page * PAGE_LIMIT + 1); - if (locationId) params.set('filter.locationId', locationId); - - let token = await tokenMgr.getToken(); - let data; - let pageOk = false; - - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - let res; - try { - res = await fetch(`${PRODUCTS_URL}?${params}`, { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/json', - }, - }); - } catch (networkErr) { - if (attempt < MAX_RETRIES) { - await sleep(RETRY_DELAY_MS * attempt); - continue; - } - throw networkErr; - } - - if (res.status === 400) { - // Bad request - Kroger rejects certain terms or parameter combos. - // Not retryable; skip this term entirely. - const body = await res.text().catch(() => ''); - throw Object.assign( - new Error( - `Skipped (400 Bad Request)${body ? ': ' + body.slice(0, 120) : ''}`, - ), - { code: 'BAD_REQUEST' }, - ); - } - - if (res.status === 401) { - tokenMgr.expiresAt = 0; - token = await tokenMgr.getToken(); - continue; - } - - if (res.status === 429) { - const wait = RETRY_DELAY_MS * attempt; - process.stdout.write(`[rate-limited, waiting ${wait / 1000}s] `); - await sleep(wait); - continue; - } - - if (res.status === 500 || res.status === 503) { - if (attempt < MAX_RETRIES) { - await sleep(RETRY_DELAY_MS * attempt); - continue; - } - throw new Error( - `Server error ${res.status} after ${MAX_RETRIES} retries`, - ); - } - - if (!res.ok) { - const body = await res.text().catch(() => ''); - throw new Error(`HTTP ${res.status}: ${body.slice(0, 120)}`); - } - - data = await res.json(); - pageOk = true; - break; - } - - if (!pageOk) break; - - const items = data?.data ?? []; - products.push(...items); - if (items.length < PAGE_LIMIT) break; - await sleep(REQUEST_DELAY_MS); - } - - return products; -} - -// --- API: Look up stores near a zip code --- -async function fetchStoresNearZip(zip, tokenMgr) { - // Validate zip - if (!/^\d{5}$/.test(zip)) { - throw new Error(`"${zip}" is not a valid 5-digit zip code`); - } - - const params = new URLSearchParams({ - 'filter.zipCode.near': zip, - 'filter.radiusInMiles': Math.min(radiusArg, 100), - 'filter.limit': 200, - }); - - const token = await tokenMgr.getToken(); - const res = await fetch(`${LOCATIONS_URL}?${params}`, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, - }); - if (!res.ok) - throw new Error( - `Location lookup failed (${res.status}): ${await res.text()}`, - ); - - const data = await res.json(); - const stores = (data?.data ?? []).slice( - 0, - maxStores === Infinity ? undefined : maxStores, - ); - - if (stores.length === 0) { - throw new Error( - `No stores found within ${radiusArg} miles of ${zip}. Try --radius=50`, - ); - } - - return stores.map((s) => ({ - locationId: s.locationId, - name: s.name ?? '', - chain: s.chain ?? '', - address: - `${s.address?.addressLine1 ?? ''}, ${s.address?.city ?? ''}, ${s.address?.state ?? ''} ${s.address?.zipCode ?? ''}`.trim(), - })); -} - -// --- Catalogue I/O --- -/** Load existing catalogue CSV into a Map: productId -> row object */ -async function loadCatalogue() { - const catalogue = new Map(); - if (!fs.existsSync(catalogueFile)) return catalogue; - - for await (const row of streamCsvRows(catalogueFile)) { - if (row.productId) catalogue.set(row.productId, row); - } - return catalogue; -} - -// --- Core: Run search terms and collect results --- -async function runSearchTerms(locationId, tokenMgr) { - const categoryKeys = categoryFilter - ? categoryFilter - .split(',') - .map((s) => s.trim()) - .filter((k) => FOOD_CATEGORIES[k]) - : Object.keys(FOOD_CATEGORIES); - - // productId -> { product, classifier, search_keyword } - const results = new Map(); - - for (const classifier of categoryKeys) { - let terms = FOOD_CATEGORIES[classifier]; - if (isDryRun) terms = terms.slice(0, 3); - - process.stdout.write(`\n [${classifier}]\n`); - - for (const term of terms) { - process.stdout.write(` "${term}" ... `); - try { - const products = await fetchProductsForTerm( - term, - locationId, - tokenMgr, - isDryRun, - ); - let newCount = 0; - for (const p of products) { - if (p.productId && !results.has(p.productId)) { - // Tag with the classifier and exact term that first found this product - results.set(p.productId, { - product: p, - classifier, - search_keyword: term, - }); - newCount++; - } - } - process.stdout.write(`${products.length} fetched, ${newCount} new\n`); - await sleep(REQUEST_DELAY_MS); - } catch (err) { - if (err.code === 'BAD_REQUEST') { - process.stdout.write(`skipped (400)\n`); - } else { - process.stdout.write(`ERROR: ${err.message}\n`); - } - await sleep(REQUEST_DELAY_MS * 2); - } - } - } - - return results; -} - -// --- Status --- -async function printStatus() { - if (!fs.existsSync(catalogueFile)) { - console.log( - '\n No catalogue found yet. Run with --zipcode to build one.\n', - ); - return; - } - - const catalogue = await loadCatalogue(); - const storeIdSet = new Set(); - let withStores = 0; - - for (const row of catalogue.values()) { - if (row.store_ids) { - withStores++; - row.store_ids - .split(';') - .map((s) => s.trim()) - .filter(Boolean) - .forEach((id) => storeIdSet.add(id)); - } - } - - console.log(`\nCatalogue Status`); - console.log(` File : ${catalogueFile}`); - console.log(` Total products : ${catalogue.size.toLocaleString()}`); - console.log(` With store data : ${withStores.toLocaleString()} products`); - console.log( - ` Stores indexed : ${storeIdSet.size.toLocaleString()} unique store IDs`, - ); - console.log( - ` Coverage : ${catalogue.size ? Math.round((withStores / catalogue.size) * 100) : 0}% of products have at least 1 store\n`, - ); -} - -// Two-phase build: -// Phase 1 — search all terms at the FIRST (nearest) store to build a full -// product catalogue with real prices. The Kroger API returns far -// fewer results without a locationId, so anchoring to a real store -// is the only way to get the full ~19k product universe. -// Phase 2 — re-search the same terms at each remaining store to collect -// their prices and append them to store_ids / price columns. -async function buildCatalogue(tokenMgr) { - const zip = zipcodeArg; - const outFile = isDryRun ? dryRunFile : catalogueFile; - - console.log(`\nBuilding food catalogue for stores near ${zip}`); - console.log(` Output: ${outFile}`); - if (isDryRun) - console.log( - ` Dry run: 3 terms per category, ${storesArg || maxStores} stores max\n`, - ); - - // Find all stores upfront — we need the first one for phase 1 - console.log(`\n Finding stores within ${radiusArg} miles of ${zip}...`); - const stores = await fetchStoresNearZip(zip, tokenMgr); - - const storeLimit = - maxStores === Infinity ? stores.length : Math.min(maxStores, stores.length); - - console.log(`\n Found ${stores.length} store(s) - using ${storeLimit}:\n`); - stores.slice(0, storeLimit).forEach((s, i) => { - console.log( - ` ${String(i + 1).padStart(2)}. [${s.locationId}] ${s.name} (${s.chain})`, - ); - console.log(` ${s.address}`); - }); - - // ── Phase 1: Full catalogue read anchored to the nearest store ───────────── - // Searching with a real locationId is required to get the full product set. - // Without it the API only returns a fraction of available products (~2k vs ~19k). - const primaryStore = stores[0]; - console.log( - `\n Phase 1: Building catalogue from primary store [${primaryStore.locationId}] ${primaryStore.name}...`, - ); - const primaryResults = await runSearchTerms( - primaryStore.locationId, - tokenMgr, - ); - - const catalogue = new Map(); - for (const [ - productId, - { product: p, classifier, search_keyword }, - ] of primaryResults) { - if (p.items?.[0]?.fulfillment?.inStore === false) continue; - const fields = productToFields(p, classifier, search_keyword); - fields.store_ids = primaryStore.locationId; - fields.price = String(p.items?.[0]?.price?.regular ?? ''); - catalogue.set(productId, fields); - } - console.log( - `\n Phase 1 complete: ${catalogue.size.toLocaleString()} products from primary store`, - ); - - // Save after phase 1 so we have something if phase 2 crashes - fs.writeFileSync( - outFile, - [ - CSV_HEADERS.join(','), - ...Array.from(catalogue.values()).map(rowToCsv), - ].join('\n'), - 'utf8', - ); - - // ── Phase 2: Enrich remaining stores ────────────────────────────────────── - // Re-search each additional store to add its prices and store_id. - // Starts at index 1 since the primary store is already done in phase 1. - let totalUpdated = 0; - - for (let i = 1; i < storeLimit; i++) { - const store = stores[i]; - console.log( - `\n [${i + 1}/${storeLimit}] Enriching from store [${store.locationId}] ${store.name}...`, - ); - - const storeProducts = await runSearchTerms(store.locationId, tokenMgr); - let updatedThisStore = 0; - - for (const [ - productId, - { product: p, classifier, search_keyword }, - ] of storeProducts) { - const price = String(p.items?.[0]?.price?.regular ?? ''); - const row = catalogue.get(productId); - - if (row) { - // product was in global catalogue — append this store's id and price - const existingStores = row.store_ids - ? row.store_ids.split(';').filter(Boolean) - : []; - if (!existingStores.includes(store.locationId)) { - row.store_ids = [...existingStores, store.locationId].join(';'); - row.price = row.price ? `${row.price};${price}` : price; - updatedThisStore++; - } - } else if (p.items?.[0]?.fulfillment?.inStore !== false) { - // product wasn't in the global search — add it if it's sold in store - const fields = productToFields(p, classifier, search_keyword); - fields.store_ids = store.locationId; - fields.price = price; - catalogue.set(productId, fields); - updatedThisStore++; - } - } - - totalUpdated += updatedThisStore; - console.log( - `\n Store [${store.locationId}] done: ${updatedThisStore.toLocaleString()} products enriched`, - ); - - // write after every store so a crash doesn't lose progress - const header = CSV_HEADERS.join(','); - const rows = Array.from(catalogue.values()).map(rowToCsv); - fs.writeFileSync(outFile, [header, ...rows].join('\n'), 'utf8'); - console.log( - ` Catalogue saved (${catalogue.size.toLocaleString()} total products)`, - ); - } - - console.log( - `\n-- Done -------------------------------------------------------------------`, - ); - console.log(` Global products : ${catalogue.size.toLocaleString()}`); - console.log(` Stores enriched : ${storeLimit}`); - console.log(` Products with price: ${totalUpdated.toLocaleString()}`); - console.log(` Saved to: ${outFile}\n`); -} - -// --- Main --- -async function main() { - if (!fs.existsSync(catalogueDir)) - fs.mkdirSync(catalogueDir, { recursive: true }); - - if (statusOnly) { - await printStatus(); - return; - } - - if (!zipcodeArg) { - console.error('\n Error: --zipcode=XXXXX is required.\n'); - console.error(' Example: node kroger_catalogue.js --zipcode=90210\n'); - process.exit(1); - } - - const tokenMgr = new TokenManager( - process.env.KROGER_CLIENT_ID, - process.env.KROGER_CLIENT_SECRET, - ); - - await buildCatalogue(tokenMgr); -} - -main().catch((err) => { - console.error(`\nFatal error: ${err.message}`); - process.exit(1); -}); diff --git a/KrogerPipeline/kroger_stores.js b/KrogerPipeline/kroger_stores.js deleted file mode 100644 index 983708d..0000000 --- a/KrogerPipeline/kroger_stores.js +++ /dev/null @@ -1,1065 +0,0 @@ -/** - * kroger_stores.js - * - * Fetches ALL Kroger-family stores in the US by sweeping a geographic grid - * of zip codes at maximum radius (100 miles), then deduplicating by locationId. - * - * WHY THE GRID APPROACH: - * The Kroger API hard-caps results at 200 per request with no working global - * offset pagination. The only way to get all ~2,800 stores is to tile the - * entire US with overlapping radius queries and deduplicate the results. - * - * SETUP: - * Uses the same .env / credentials as kroger_food_scraper.js - * npm install node-fetch@2 dotenv (if not already installed) - * - * USAGE: - * node kroger_stores.js [flags] - * - * FLAGS: - * --out=./my-dir Output directory (default: ./kroger_output) - * --radius=100 Search radius per zip code (default: 100, max: 100) - * --chain=KROGER Filter to one chain only - * Values: KROGER, RALPHS, FRED MEYER, KING SOOPERS, - * SMITHS, FRYSFOOD, HARRIS TEETER, CITY MARKET, - * DILLONS, BAKERS, GERBES, QFC, MARIANO - * --dry-run Only query first 10 zip codes (for credential testing) - * --concurrency=5 Parallel requests at once (default: 5, max: 10) - * - * EXAMPLES: - * node kroger_stores.js - * node kroger_stores.js --chain=KROGER --out=./kroger-only - * node kroger_stores.js --dry-run - */ - -import 'dotenv/config'; -import fetch from 'node-fetch'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// ─── Config ──────────────────────────────────────────────────────────────────── - -const BASE_URL = 'https://api.kroger.com/v1'; -const TOKEN_URL = `${BASE_URL}/connect/oauth2/token`; -const LOCATIONS_URL = `${BASE_URL}/locations`; - -const MAX_LIMIT = 200; // Kroger API hard cap per request -const RETRY_DELAY_MS = 2000; -const MAX_RETRIES = 4; - -// ─── CLI Args ────────────────────────────────────────────────────────────────── - -const args = process.argv.slice(2); -function getArg(prefix) { - return ( - (args.find((a) => a.startsWith(prefix)) ?? '').replace(prefix, '') || '' - ); -} - -const isDryRun = args.includes('--dry-run'); -const outDir = getArg('--out=') || './kroger_output'; -const storesDir = path.join(outDir, 'stores'); -const chainArg = getArg('--chain=').toUpperCase(); -const radius = Math.min(parseInt(getArg('--radius=') || '100', 10), 100); -const concurrency = Math.min(parseInt(getArg('--concurrency=') || '5', 10), 10); - -// ─── US ZIP CODE GRID ────────────────────────────────────────────────────────── -// ~280 zip codes chosen to give contiguous coverage of the entire continental -// US, Alaska, and Hawaii at a 100-mile radius. Each point is a real zip code -// of a city/town in that geographic cell. Overlapping circles ensure no gap -// is larger than ~140 miles (2× the radius), so every Kroger store is reachable -// from at least one query point. - -const GRID_ZIPS = [ - // ── Pacific Northwest ────────────────────────────────────────────────────── - '98101', - '98201', - '98801', - '99201', - '99301', - '99401', - '97201', - '97401', - '97501', - '97701', - '97801', - '97901', - // ── California ──────────────────────────────────────────────────────────── - '96001', - '95901', - '95501', - '94102', - '94601', - '95008', - '93401', - '93101', - '92101', - '91101', - '90001', - '90401', - '92501', - '92701', - '93301', - '93601', - '94201', - '94901', - '95201', - '95301', - '95701', - '96101', - // ── Nevada / Arizona ────────────────────────────────────────────────────── - '89101', - '89301', - '89501', - '89701', - '89801', - '85001', - '85201', - '85301', - '85501', - '85701', - '86001', - '86301', - '86401', - '86501', - // ── Alaska ──────────────────────────────────────────────────────────────── - '99501', - '99701', - '99901', - // ── Hawaii ──────────────────────────────────────────────────────────────── - '96801', - '96720', - '96740', - '96761', - // ── Mountain West ───────────────────────────────────────────────────────── - '83201', - '83401', - '83701', - '84101', - '84301', - '84501', - '84601', - '84701', - '84901', - '85901', - '86001', - // ── Colorado / Wyoming / Montana / Idaho ────────────────────────────────── - '80202', - '80401', - '80501', - '80631', - '80901', - '81001', - '81201', - '81301', - '81401', - '81501', - '82001', - '82301', - '82601', - '82901', - '83001', - '83201', - '59001', - '59401', - '59601', - '59801', - '59901', - '82801', - '82901', - '83001', - // ── New Mexico ──────────────────────────────────────────────────────────── - '87101', - '87401', - '87501', - '87701', - '87901', - '88001', - '88201', - '88401', - '88601', - // ── North / South Dakota / Nebraska / Kansas ────────────────────────────── - '57001', - '57201', - '57401', - '57601', - '57701', - '57901', - '58001', - '58201', - '58401', - '58601', - '58801', - '68001', - '68101', - '68401', - '68501', - '68801', - '69001', - '66101', - '66201', - '66401', - '66601', - '66801', - '67001', - '67201', - '67401', - '67601', - '67801', - '67901', - // ── Oklahoma / Texas ────────────────────────────────────────────────────── - '73101', - '73401', - '73601', - '73801', - '74101', - '74401', - '74601', - '74801', - '75001', - '75201', - '75401', - '75601', - '75801', - '76001', - '76201', - '76401', - '76601', - '76801', - '77001', - '77201', - '77401', - '77601', - '77801', - '78101', - '78201', - '78401', - '78501', - '78701', - '78801', - '79101', - '79201', - '79401', - '79601', - '79701', - '79901', - // ── Minnesota / Wisconsin / Iowa ────────────────────────────────────────── - '55101', - '55301', - '55401', - '55601', - '55701', - '55801', - '56001', - '56201', - '56301', - '56401', - '56501', - '56601', - '56701', - '56801', - '57001', - '54101', - '54201', - '54401', - '54501', - '54601', - '54701', - '54901', - '52001', - '52101', - '52201', - '52301', - '52401', - '52501', - '52601', - '52701', - '52801', - // ── Illinois / Missouri ─────────────────────────────────────────────────── - '60601', - '60901', - '61101', - '61201', - '61401', - '61601', - '61701', - '61801', - '61901', - '62001', - '62201', - '62401', - '62601', - '62701', - '62801', - '62901', - '63101', - '63301', - '63401', - '63501', - '63601', - '63701', - '63801', - '63901', - '64101', - '64501', - '64701', - '64801', - '65101', - '65201', - '65401', - '65601', - '65701', - '65801', - '65901', - // ── Michigan ────────────────────────────────────────────────────────────── - '48101', - '48201', - '48401', - '48601', - '48701', - '48801', - '48901', - '49001', - '49101', - '49201', - '49301', - '49401', - '49501', - '49601', - '49701', - '49801', - '49901', - // ── Indiana / Ohio ──────────────────────────────────────────────────────── - '46201', - '46401', - '46501', - '46601', - '46701', - '46801', - '47201', - '47401', - '47601', - '47701', - '47901', - '43101', - '43201', - '43401', - '43501', - '43601', - '43701', - '43801', - '43901', - '44101', - '44201', - '44301', - '44401', - '44501', - '44601', - '44701', - '44801', - '44901', - '45101', - '45201', - '45301', - '45401', - '45501', - '45601', - '45701', - '45801', - '45901', - // ── Kentucky / Tennessee ────────────────────────────────────────────────── - '40201', - '40601', - '41001', - '41101', - '41201', - '41301', - '41401', - '41501', - '41601', - '41701', - '42001', - '42101', - '42201', - '42301', - '42401', - '42501', - '42601', - '42701', - '37011', - '37101', - '37201', - '37301', - '37401', - '37501', - '37601', - '37701', - '37801', - '37901', - '38001', - '38101', - '38201', - '38301', - '38401', - '38501', - '38601', - '38701', - '38801', - '38901', - // ── Virginia / West Virginia / North Carolina ───────────────────────────── - '22201', - '22301', - '22601', - '22901', - '23101', - '23201', - '23401', - '23601', - '23801', - '24101', - '24201', - '24301', - '24401', - '24501', - '24601', - '24701', - '24801', - '24901', - '25101', - '25301', - '25501', - '25701', - '25801', - '25901', - '26101', - '26201', - '26301', - '26401', - '26501', - '26601', - '27101', - '27201', - '27301', - '27401', - '27501', - '27601', - '27701', - '27801', - '27901', - '28001', - '28101', - '28201', - '28301', - '28401', - '28501', - '28601', - '28701', - '28801', - '28901', - // ── South Carolina / Georgia ────────────────────────────────────────────── - '29101', - '29201', - '29301', - '29401', - '29501', - '29601', - '29701', - '29801', - '29901', - '30001', - '30101', - '30201', - '30301', - '30401', - '30501', - '30601', - '30701', - '30801', - '30901', - '31001', - '31101', - '31201', - '31301', - '31401', - '31501', - '31601', - '31701', - '31801', - '31901', - // ── Florida ─────────────────────────────────────────────────────────────── - '32004', - '32101', - '32201', - '32301', - '32401', - '32501', - '32601', - '32701', - '32801', - '32901', - '33101', - '33301', - '33401', - '33501', - '33601', - '33701', - '33801', - '33901', - '34101', - '34201', - '34401', - '34601', - '34701', - '34801', - '34901', - // ── Alabama / Mississippi ───────────────────────────────────────────────── - '35004', - '35101', - '35201', - '35401', - '35501', - '35601', - '35701', - '35801', - '35901', - '36001', - '36101', - '36201', - '36301', - '36401', - '36501', - '36601', - '36701', - '36801', - '36901', - '39001', - '39101', - '39201', - '39301', - '39401', - '39501', - // ── Louisiana / Arkansas ────────────────────────────────────────────────── - '70001', - '70101', - '70301', - '70401', - '70501', - '70601', - '70701', - '70801', - '70901', - '71001', - '71101', - '71201', - '71301', - '71601', - '71701', - '71801', - '71901', - '72001', - '72101', - '72201', - '72301', - '72401', - '72601', - '72701', - '72801', - '72901', - // ── Great Lakes / Northeast ─────────────────────────────────────────────── - '53001', - '53201', - '53401', - '53501', - '53601', - '53701', - '53901', - '14001', - '14101', - '14201', - '14301', - '14601', - '14701', - '14801', - '14901', - '13201', - '13301', - '13401', - '12001', - '12101', - '12201', - '12301', - '12401', - '12501', - '12601', - '10001', - '10301', - '11001', - '07001', - '07101', - '07201', - '07301', - '07401', - '07501', - '07601', - '07701', - '07801', - '07901', - '08001', - '08101', - '08201', - '08301', - '08401', - '08501', - '08601', - '08701', - '08801', - '08901', - // ── Pennsylvania ────────────────────────────────────────────────────────── - '15001', - '15101', - '15201', - '15301', - '15401', - '15501', - '15601', - '15701', - '15801', - '15901', - '16001', - '16101', - '16201', - '16301', - '16401', - '16501', - '16601', - '16701', - '16801', - '16901', - '17001', - '17101', - '17201', - '17301', - '17401', - '17501', - '17601', - '17701', - '17801', - '17901', - '18001', - '18101', - '18201', - '18301', - '18401', - '18501', - '18601', - '18701', - '18801', - '18901', - '19001', - '19101', - '19201', - '19301', - '19401', - // ── Maryland / Delaware / DC ────────────────────────────────────────────── - '20001', - '20601', - '20701', - '20801', - '20901', - '21001', - '21101', - '21201', - '21301', - '21401', - '21501', - '21601', - '21701', - '21801', - '21901', - '19701', - '19801', - '19901', - // ── New England ─────────────────────────────────────────────────────────── - '06001', - '06101', - '06201', - '06301', - '06401', - '06501', - '06601', - '06701', - '06801', - '06901', - '01001', - '01101', - '01201', - '01301', - '01401', - '01501', - '01601', - '01701', - '01801', - '01901', - '02101', - '02201', - '02301', - '02401', - '02501', - '02601', - '02701', - '02801', - '02901', - '03001', - '03101', - '03201', - '03301', - '03401', - '03501', - '03601', - '03701', - '03801', - '03901', - '04001', - '04101', - '04201', - '04401', - '04601', - '04901', - '05001', - '05101', - '05201', - '05301', - '05401', - '05501', - '05601', - '05701', - '05801', - '05901', -]; - -// ─── Token Manager ───────────────────────────────────────────────────────────── - -class TokenManager { - constructor(clientId, clientSecret) { - if (!clientId || !clientSecret) { - throw new Error( - 'Missing KROGER_CLIENT_ID or KROGER_CLIENT_SECRET.\n' + - 'Set them in a .env file or as environment variables.', - ); - } - this.credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( - 'base64', - ); - this.accessToken = null; - this.expiresAt = 0; - } - - async getToken() { - if (this.accessToken && Date.now() < this.expiresAt - 60_000) - return this.accessToken; - process.stdout.write(' Refreshing OAuth2 token... '); - const res = await fetch(TOKEN_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${this.credentials}`, - }, - body: 'grant_type=client_credentials&scope=product.compact', - }); - if (!res.ok) - throw new Error( - `Token request failed (${res.status}): ${await res.text()}`, - ); - const data = await res.json(); - this.accessToken = data.access_token; - this.expiresAt = Date.now() + data.expires_in * 1000; - console.log(`OK (valid ${Math.round(data.expires_in / 60)} min)`); - return this.accessToken; - } -} - -// ─── Helpers ─────────────────────────────────────────────────────────────────── - -function sleep(ms) { - return new Promise((r) => setTimeout(r, ms)); -} - -async function fetchLocationsForZip(zip, tokenMgr) { - const params = new URLSearchParams({ - 'filter.zipCode.near': zip, - 'filter.radiusInMiles': radius, - 'filter.limit': MAX_LIMIT, - }); - if (chainArg) params.set('filter.chain', chainArg); - - const url = `${LOCATIONS_URL}?${params}`; - - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - const token = await tokenMgr.getToken(); - const res = await fetch(url, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, - }); - - if (res.status === 429) { - const wait = RETRY_DELAY_MS * attempt; - process.stdout.write(`[429 wait ${wait / 1000}s] `); - await sleep(wait); - continue; - } - if (res.status === 401) { - tokenMgr.expiresAt = 0; - continue; - } - if (!res.ok) { - if (attempt < MAX_RETRIES) { - await sleep(RETRY_DELAY_MS); - continue; - } - throw new Error(`HTTP ${res.status} for zip ${zip}`); - } - - const data = await res.json(); - return data?.data ?? []; - } - return []; -} - -// Run up to `concurrency` zip-code fetches in parallel -async function processInBatches(zips, tokenMgr, allStores) { - let done = 0; - for (let i = 0; i < zips.length; i += concurrency) { - const batch = zips.slice(i, i + concurrency); - const results = await Promise.all( - batch.map(async (zip) => { - try { - return await fetchLocationsForZip(zip, tokenMgr); - } catch (err) { - console.error(`\n Error for zip ${zip}: ${err.message}`); - return []; - } - }), - ); - - let newInBatch = 0; - for (const stores of results) { - for (const s of stores) { - if (!allStores.has(s.locationId)) { - allStores.set(s.locationId, s); - newInBatch++; - } - } - } - done += batch.length; - - // Progress bar - const pct = Math.round((done / zips.length) * 100); - const bar = - '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5)); - process.stdout.write( - `\r [${bar}] ${pct}% | ${done}/${zips.length} zips | ${allStores.size} unique stores found`, - ); - } - process.stdout.write('\n'); -} - -// ─── CSV ─────────────────────────────────────────────────────────────────────── - -function esc(v) { - if (v === null || v === undefined) return ''; - const s = String(v); - return s.includes(',') || s.includes('"') || s.includes('\n') - ? `"${s.replace(/"/g, '""')}"` - : s; -} - -const HEADERS = [ - 'locationId', - 'name', - 'chain', - 'phone', - 'address_line1', - 'address_line2', - 'address_city', - 'address_state', - 'address_zipCode', - 'address_county', - 'geo_latitude', - 'geo_longitude', - 'hours_timezone', - 'hours_gmtOffset', - 'hours_open24', - 'hours_monday_open', - 'hours_monday_close', - 'hours_monday_open24', - 'hours_tuesday_open', - 'hours_tuesday_close', - 'hours_tuesday_open24', - 'hours_wednesday_open', - 'hours_wednesday_close', - 'hours_wednesday_open24', - 'hours_thursday_open', - 'hours_thursday_close', - 'hours_thursday_open24', - 'hours_friday_open', - 'hours_friday_close', - 'hours_friday_open24', - 'hours_saturday_open', - 'hours_saturday_close', - 'hours_saturday_open24', - 'hours_sunday_open', - 'hours_sunday_close', - 'hours_sunday_open24', - 'departments_count', - 'departments_names', - 'departments_ids', - 'departments_json', -]; - -const DAYS = [ - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday', -]; - -function storeToRow(s) { - const addr = s.address ?? {}; - const geo = s.geolocation ?? {}; - const hrs = s.hours ?? {}; - const deps = s.departments ?? []; - - const dayFields = DAYS.flatMap((day) => { - const d = hrs[day] ?? {}; - return [d.open ?? '', d.close ?? '', d.open24 ?? '']; - }); - - return [ - s.locationId, - s.name, - s.chain, - s.phone, - addr.addressLine1, - addr.addressLine2, - addr.city, - addr.state, - addr.zipCode, - addr.county, - geo.latitude, - geo.longitude, - hrs.timezone, - hrs.gmtOffset, - hrs.open24, - ...dayFields, - deps.length, - deps.map((d) => d.name).join('; '), - deps.map((d) => d.departmentId).join('; '), - JSON.stringify(deps), - ] - .map(esc) - .join(','); -} - -// ─── Main ────────────────────────────────────────────────────────────────────── - -async function main() { - const tokenMgr = new TokenManager( - process.env.KROGER_CLIENT_ID, - process.env.KROGER_CLIENT_SECRET, - ); - - if (!fs.existsSync(storesDir)) fs.mkdirSync(storesDir, { recursive: true }); - - // De-duplicate zip codes - const zips = [...new Set(GRID_ZIPS)]; - const queryZips = isDryRun ? zips.slice(0, 10) : zips; - - console.log('\nKroger Store Scraper'); - console.log( - ` Strategy : zip-code grid sweep (${queryZips.length} zip codes × ${radius}-mile radius)`, - ); - console.log(` Concurrency: ${concurrency} parallel requests`); - console.log(` Chain filter: ${chainArg || '(all chains)'}`); - console.log(` Output dir : ${path.resolve(storesDir)}`); - console.log( - ` Dry run : ${isDryRun}${isDryRun ? ' (first 10 zips only)' : ''}\n`, - ); - - const allStores = new Map(); // locationId → store object - - await processInBatches(queryZips, tokenMgr, allStores); - - if (allStores.size === 0) { - console.log( - ' No stores found. Check credentials or try --chain with a different value.', - ); - process.exit(0); - } - - const storeList = Array.from(allStores.values()).sort( - (a, b) => - (a.chain ?? '').localeCompare(b.chain ?? '') || - (a.locationId ?? '').localeCompare(b.locationId ?? ''), - ); - - // ── Write main CSV ───────────────────────────────────────────────────────── - const csvPath = path.join(storesDir, 'kroger_stores.csv'); - fs.writeFileSync( - csvPath, - [HEADERS.join(','), ...storeList.map(storeToRow)].join('\n'), - 'utf8', - ); - console.log( - `\n Saved ${storeList.length.toLocaleString()} stores → ${csvPath}`, - ); - - // ── Per-chain CSVs ───────────────────────────────────────────────────────── - const byChain = new Map(); - for (const s of storeList) { - const key = (s.chain ?? 'UNKNOWN').replace(/[^a-zA-Z0-9_]/g, '_'); - if (!byChain.has(key)) byChain.set(key, []); - byChain.get(key).push(s); - } - - const chainDir = path.join(storesDir, 'by_chain'); - if (!fs.existsSync(chainDir)) fs.mkdirSync(chainDir); - for (const [chain, chainStores] of byChain) { - fs.writeFileSync( - path.join(chainDir, `${chain}.csv`), - [HEADERS.join(','), ...chainStores.map(storeToRow)].join('\n'), - 'utf8', - ); - } - console.log(` Per-chain CSVs → ${chainDir}/`); - - // ── Summary ──────────────────────────────────────────────────────────────── - const sorted = [...byChain].sort((a, b) => b[1].length - a[1].length); - const summaryLines = [ - 'chain,store_count', - ...sorted.map(([c, arr]) => `${esc(c)},${arr.length}`), - `TOTAL,${storeList.length}`, - ]; - fs.writeFileSync( - path.join(storesDir, 'stores_summary.csv'), - summaryLines.join('\n'), - 'utf8', - ); - - console.log(`\n Chain breakdown:`); - for (const [chain, arr] of sorted) { - console.log(` ${arr.length.toString().padStart(5)} ${chain}`); - } - console.log(` ${'─'.repeat(20)}`); - console.log(` ${storeList.length.toString().padStart(5)} TOTAL\n`); -} - -main().catch((err) => { - console.error(`\nFatal error: ${err.message}`); - process.exit(1); -}); diff --git a/PlayWright/scraper.py b/PlayWright/scraper.py new file mode 100644 index 0000000..5a9949f --- /dev/null +++ b/PlayWright/scraper.py @@ -0,0 +1,487 @@ +""" +scraper.py — Instacart pipeline scraper +========================================== + +Architecture +------------ + 1. Load credentials from CS178-Shopwise/.env + SUPABASE_URL, SUPABASE_SERVICE_KEY + + 2. Fetch all rows from the Supabase `taxonomy` table (column: `ingredient`) + Ingredient format — two cases: + "eggs" -> single word: search "eggs", keep ALL results + "cabbage, red" -> word + descriptor: search "cabbage, red" (full phrase), + keep only products whose name contains "red" + + 3. For each ingredient: + a. Parse into (search_term, filter_word | None) + - search_term = full ingredient string (e.g. "cabbage, red") + - filter_word = text after the comma (e.g. "red"), or None + b. Open Instacart cross-retailer search: instacart.com/store/s?k= + c. Expand every store's product carousel (Phase 1: Discovery) + d. Click each card, open the detail dialog; extract all fields in one JS call + (Phase 2: Scraping). Fields per product: + store, name, price, price_unit, quantity, image_url, description, out_of_stock + e. If filter_word is set, drop products whose name does not contain it + (case-insensitive substring match) + + 4. Upsert all collected products to the Supabase `ingredients` table. + Columns: id, taxonomy, store, name, price, price_unit, quantity, + image_url, description, out_of_stock + + Single Playwright browser session is reused across all ingredients so that + Instacart's session/location cookies persist between queries. + +Usage +----- + python scraper.py + +Output +------ + Supabase table: ingredients +""" + +import argparse +import asyncio +import os +import re +import uuid +from pathlib import Path +from urllib.parse import quote_plus + +from dotenv import load_dotenv +from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError +from supabase import create_client + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +_ENV_PATH = Path(__file__).parent.parent / ".env" +load_dotenv(_ENV_PATH) + +SUPABASE_URL = os.getenv("SUPABASE_URL") +SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_KEY") + +BASE_URL = "https://www.instacart.com/store/s?k={query}" + +# How long to wait for the product grid to appear (ms). +GRID_WAIT_MS = 5_000 + + +# --------------------------------------------------------------------------- +# Confirmed Instacart DOM selectors (verified against live DOM) +# --------------------------------------------------------------------------- + +STORE_ROW_SELECTOR = '[data-testid="CrossRetailerResultRowWrapper"]' +ITEM_CARD_SELECTOR = '[data-testid^="item_list_item_"]' +NEXT_PAGE_SELECTOR = '[aria-label="Next page"]' +DETAIL_DIALOG_SELECTOR = '[role="dialog"][aria-label="item details"]' +DETAIL_NAME_SELECTOR = '[aria-label="item details"] h2' +LOGIN_MODAL_SELECTOR = ( + '[role="dialog"] input[type="email"], [role="dialog"] input[type="password"], ' + '[aria-modal="true"] input[type="email"], [aria-modal="true"] input[type="password"]' +) + +# Maximum carousel pages to advance per store row. +MAX_CAROUSEL_PAGES = 10 + +# --------------------------------------------------------------------------- +# Ingredient parsing +# --------------------------------------------------------------------------- + +def parse_ingredient(ingredient: str) -> tuple[str, str | None]: + """ + Parse a taxonomy ingredient string into (search_term, filter_word). + + The full ingredient string is always used as the search term so that + Instacart receives the most specific query possible. + + Examples: + "eggs" → ("eggs", None) + "cabbage, red" → ("cabbage, red", "red") + """ + parts = [p.strip() for p in ingredient.split(",", 1)] + search_term = ingredient # search the full phrase + filter_word = parts[1] if len(parts) > 1 else None + return search_term, filter_word + + +def apply_name_filter(products: list[dict], filter_word: str | None) -> list[dict]: + """Keep only products whose name contains filter_word (case-insensitive).""" + if not filter_word: + return products + fw = filter_word.lower() + return [p for p in products if p["name"] and fw in p["name"].lower()] + +# --------------------------------------------------------------------------- +# Core scraper +# --------------------------------------------------------------------------- + +async def scrape_query(page, query: str) -> list[dict]: + """ + Scrape all product cards for *query* on the Instacart cross-retailer page. + Returns a list of product dicts (fields match CSV_FIELDS minus 'taxonomy'). + Returns an empty list if navigation fails or the grid is not found. + """ + url = BASE_URL.format(query=quote_plus(query)) + + # -- Navigate ---------------------------------------------------------- + try: + response = await page.goto(url, wait_until="domcontentloaded", timeout=30_000) + if response and response.status >= 400: + print(f" [ERROR] HTTP {response.status} for query '{query}'") + return [] + except PlaywrightTimeoutError: + print(f" [ERROR] Navigation timeout for query '{query}'") + return [] + except Exception as exc: + print(f" [ERROR] Navigation error for query '{query}': {exc}") + return [] + + # -- Block detection --------------------------------------------------- + try: + body_text = (await page.inner_text("body")).lower() + except Exception: + body_text = "" + + blocked_patterns = [ + "access denied", "403", "robot", "captcha", + "verify you are human", "unusual traffic", "too many requests", "rate limit", + ] + if any(p in body_text for p in blocked_patterns): + print(f" [WARN] Bot-detection page detected for query '{query}' — skipping") + return [] + + if await page.query_selector(LOGIN_MODAL_SELECTOR): + print(f" [WARN] Login modal detected for query '{query}' — skipping") + return [] + + # -- Wait for store rows ----------------------------------------------- + try: + await page.wait_for_selector(STORE_ROW_SELECTOR, timeout=GRID_WAIT_MS) + except PlaywrightTimeoutError: + print(f" [WARN] No store grid found for query '{query}' after {GRID_WAIT_MS}ms") + return [] + + # -- Discovery + Scraping (merged) — process each carousel page inline -- + # Cards are clicked immediately while their carousel page is active, + # so ElementHandles never become stale from carousel advancement. + store_rows = await page.query_selector_all(STORE_ROW_SELECTOR) + + if not store_rows: + print(f" [WARN] No product cards found for query '{query}'") + return [] + + products = [] + total_card_index = 0 + + for row in store_rows: + try: + store_name = await page.evaluate(""" + (row) => { + const lines = (row.innerText || '').split('\\n') + .map(s => s.trim()).filter(Boolean); + return lines[0] || 'Unknown Store'; + } + """, row) + except Exception: + store_name = "Unknown Store" + + seen_testids: set[str] = set() + + for _ in range(MAX_CAROUSEL_PAGES + 1): + cards_now = await row.query_selector_all(ITEM_CARD_SELECTOR) + + # Scrape each new card on this carousel page immediately (before advancing) + for card in cards_now: + try: + testid = await card.get_attribute("data-testid") or "" + except Exception: + testid = "" + if testid in seen_testids: + continue + seen_testids.add(testid) + + i = total_card_index + total_card_index += 1 + + name_text = None + price_text = None + price_unit = None + quantity = None + image_url = None + description = None + out_of_stock = False + item_error = None + + try: + await card.scroll_into_view_if_needed() + await card.click() + await page.wait_for_selector(DETAIL_DIALOG_SELECTOR, timeout=6_000) + except PlaywrightTimeoutError: + item_error = f"Card {i}: timed out waiting for detail dialog" + except Exception as exc: + item_error = f"Card {i}: click error: {exc}" + + if item_error: + print(f" [WARN] {item_error}") + products.append(_empty_product(store_name)) + continue + + # Extract all fields from the open dialog in a single JS round-trip. + try: + _data = await page.evaluate(""" + () => { + const dialog = document.querySelector( + '[role="dialog"][aria-label="item details"]' + ); + if (!dialog) return null; + + // Name — first h2 in dialog + const nameEl = dialog.querySelector('h2'); + const name = nameEl ? nameEl.textContent.trim() : null; + + // Price header span + sibling unit span + // DOM patterns: + // $1.99 /lb per lb → rate → price_unit + // $3.99 18 ct → pkg → quantity + // $9.07 each → fixed → both null + let priceText = '', unitText = '', isRate = false; + const itemDetails = dialog.querySelector('#item_details'); + for (const span of dialog.querySelectorAll('span')) { + if (itemDetails && itemDetails.contains(span)) continue; + if (span.childElementCount !== 0) continue; + const t = span.textContent.trim(); + if (!/^\\$[\\d.,]+/.test(t)) continue; + const parent = span.parentElement; + if (!parent) continue; + const siblings = Array.from(parent.children) + .filter(el => el.tagName === 'SPAN'); + const idx = siblings.indexOf(span); + unitText = (idx !== -1 && siblings[idx + 1]) + ? siblings[idx + 1].textContent.trim() : ''; + isRate = /\\//.test(t) || /^per\\s/i.test(unitText); + priceText = t; + break; + } + + // Price — screen-reader span or first $X.XX span + let price = null; + const spans = dialog.querySelectorAll('span'); + for (const s of spans) { + if (s.childElementCount !== 0) continue; + const t = s.textContent.trim(); + if (t.startsWith('Current price:')) { + const full = t.replace('Current price:', '').trim(); + const m = full.match(/^(\\$[\\d.,]+)/); + price = m ? m[1] : full; + break; + } + } + if (!price) { + for (const s of spans) { + if (s.childElementCount !== 0) continue; + const t = s.textContent.trim(); + const m = t.match(/^(\\$[\\d.,]+)/); + if (m) { price = m[1]; break; } + } + } + + // Image — .ic-image-zoomer img, fallback img[alt]; null if placeholder + let imageUrl = null; + const zoomer = dialog.querySelector('.ic-image-zoomer img'); + if (zoomer && zoomer.src) imageUrl = zoomer.src; + else { + const fb = dialog.querySelector('img[alt]'); + if (fb) imageUrl = fb.src; + } + if (imageUrl && imageUrl.includes('missing-item')) imageUrl = null; + + // Description —

inside container whose sibling h2 is "Details" + let description = null; + for (const h of dialog.querySelectorAll('h2')) { + if (h.textContent.trim() === 'Details') { + const c = h.closest('[tabindex="-1"]') || h.parentElement; + const p = c ? c.querySelector('p') : null; + if (p) description = p.textContent.trim(); + break; + } + } + + // Out of stock — h2 text "out of stock" + let outOfStock = false; + for (const h of dialog.querySelectorAll('h2')) { + if (h.textContent.trim().toLowerCase() === 'out of stock') { + outOfStock = true; break; + } + } + + return { name, priceText, unitText, isRate, price, + imageUrl, description, outOfStock }; + } + """) + except Exception: + _data = None + + if _data: + name_text = _data.get("name") or None + price_text = _data.get("price") or None + image_url = _data.get("imageUrl") or None + description = _data.get("description") or None + out_of_stock = bool(_data.get("outOfStock")) + + _price_raw = _data.get("priceText", "") + _unit_text = _data.get("unitText", "") + if _data.get("isRate"): + _slash = _price_raw.find("/") + if _slash != -1: + _after = _price_raw[_slash + 1:].strip() + if re.match(r'^\d', _after): + quantity = _after + elif _after and re.match( + r'^(?:lb|lbs|oz|fl\.?\s*oz|g|kg|ml|L|gal)\b', + _after, re.IGNORECASE + ): + price_unit = _after + elif _unit_text: + _norm = re.sub( + r'^(?:per\s+|/)', '', _unit_text, flags=re.IGNORECASE + ).strip() + if _norm and not re.match(r'^\d', _norm): + price_unit = _norm + elif _unit_text and not re.match( + r'^\d*\s*each$', _unit_text, re.IGNORECASE + ): + quantity = _unit_text + + # Regex fallback for quantity: two-pass — prefer count patterns + # (e.g. "12-count", "6 pk") over weight/volume so multi-pack items + # resolve to the pack count rather than the per-unit weight. + if quantity is None and price_unit is None and name_text: + m = re.search( + r'\d+(?:[./]\d+)?[\s-]*(?:ct|count|pk|pack)\b', + name_text, re.IGNORECASE + ) + if not m: + m = re.search( + r'\d+(?:[./]\d+)?[\s-]*' + r'(?:oz|fl\.?\s*oz|lb|lbs|g|kg|ml|L|gal)\b', + name_text, re.IGNORECASE + ) + if m: + quantity = m.group(0).strip() + + # Close dialog before moving to next card + try: + await page.keyboard.press("Escape") + await page.wait_for_selector( + DETAIL_DIALOG_SELECTOR, state="hidden", timeout=1_500 + ) + except Exception: + pass # dialog may have already closed; continue regardless + + product_id = testid.removeprefix("item_list_item_") or str(uuid.uuid4()) + products.append({ + "id": product_id, + "store": store_name, + "name": name_text, + "price": price_text, + "price_unit": price_unit, + "quantity": quantity, + "image_url": image_url, + "description": description, + "out_of_stock": out_of_stock, + }) + + # Advance carousel after all cards on this page are processed + try: + next_btn = await row.query_selector(NEXT_PAGE_SELECTOR) + if not next_btn or not await next_btn.is_visible(): + break + await next_btn.click() + await page.wait_for_timeout(150) + except Exception: + break + + return products + + +def _empty_product(store_name: str) -> dict: + return { + "id": str(uuid.uuid4()), "store": store_name, "name": None, "price": None, + "price_unit": None, "quantity": None, "image_url": None, + "description": None, "out_of_stock": False, + } + +# --------------------------------------------------------------------------- +# Pipeline entry point +# --------------------------------------------------------------------------- + +async def run_pipeline(limit: int | None = None) -> None: + """ + Full pipeline: fetch taxonomy → scrape Instacart → upsert to Supabase. + limit: if set, only process the first N ingredients (for test runs). + """ + if not SUPABASE_URL or not SUPABASE_KEY: + raise RuntimeError( + "SUPABASE_URL and SUPABASE_SERVICE_KEY must be set in .env" + ) + + # -- Fetch taxonomy ---------------------------------------------------- + print("Fetching taxonomy from Supabase...") + supabase = create_client(SUPABASE_URL, SUPABASE_KEY) + response = supabase.table("taxonomy").select("ingredient").execute() + ingredients = [row["ingredient"] for row in response.data] + if limit is not None: + ingredients = ingredients[:limit] + print(f" {len(ingredients)} ingredients loaded (limited to first {limit}).") + else: + print(f" {len(ingredients)} ingredients loaded.") + + total_upserted = 0 + + # -- Scrape ------------------------------------------------------------ + async with async_playwright() as p: + browser = await p.chromium.launch( + headless=False, + args=["--disable-blink-features=AutomationControlled"], + ) + context = await browser.new_context( + user_agent=( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), + viewport={"width": 1280, "height": 900}, + ) + page = await context.new_page() + + for ingredient in ingredients: + search_term, filter_word = parse_ingredient(ingredient) + print(f"\n[{ingredient}] search='{search_term}' filter={filter_word!r}") + + products = await scrape_query(page, search_term) + products = apply_name_filter(products, filter_word) + + rows = [{"taxonomy": ingredient, **p} for p in products] + + if rows: + supabase.table("ingredients").upsert(rows, on_conflict="id").execute() + total_upserted += len(rows) + + print(f" → {len(rows)} products upserted") + + await browser.close() + + print(f"\nDone. {total_upserted} total rows upserted to Supabase.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Instacart pipeline scraper") + parser.add_argument( + "--limit", type=int, default=None, metavar="N", + help="Only process the first N taxonomy ingredients (e.g. --limit 3 for a test run)", + ) + args = parser.parse_args() + asyncio.run(run_pipeline(limit=args.limit)) diff --git a/WalmartPipeline/Categories/Food_Grocery_subtree.csv b/WalmartPipeline/Categories/Food_Grocery_subtree.csv deleted file mode 100644 index 59dfa8f..0000000 --- a/WalmartPipeline/Categories/Food_Grocery_subtree.csv +++ /dev/null @@ -1,22 +0,0 @@ -id,name,path -"3734780_7455738","Food & Grocery","Shop by Brand/Food & Grocery" -"3734780_7455738_4395713","Augason Farms","Shop by Brand/Food & Grocery/Augason Farms" -"3734780_7455738_7679722","Bai","Shop by Brand/Food & Grocery/Bai" -"3734780_7455738_7092438","Bob's Red Mill","Shop by Brand/Food & Grocery/Bob's Red Mill" -"3734780_7455738_6336012","Butterball","Shop by Brand/Food & Grocery/Butterball" -"3734780_7455738_1413005","Campbell's","Shop by Brand/Food & Grocery/Campbell's" -"3734780_7455738_6065794","Essentia","Shop by Brand/Food & Grocery/Essentia" -"3734780_7455738_7166534","Frito-Lay","Shop by Brand/Food & Grocery/Frito-Lay" -"3734780_7455738_5413535","Great Value","Shop by Brand/Food & Grocery/Great Value" -"3734780_7455738_2948423","Knorr","Shop by Brand/Food & Grocery/Knorr" -"3734780_7455738_1397051","Kraft Dinners","Shop by Brand/Food & Grocery/Kraft Dinners" -"3734780_7455738_3611340","Lipton","Shop by Brand/Food & Grocery/Lipton" -"3734780_7455738_2728198","Marketside","Shop by Brand/Food & Grocery/Marketside" -"3734780_7455738_9309662","McCormick","Shop by Brand/Food & Grocery/McCormick" -"3734780_7455738_5050871","Nature's Path","Shop by Brand/Food & Grocery/Nature's Path" -"3734780_7455738_3862805","Nestle","Shop by Brand/Food & Grocery/Nestle" -"3734780_7455738_3783723","Oreo","Shop by Brand/Food & Grocery/Oreo" -"3734780_7455738_5312905","Pepperidge Farm","Shop by Brand/Food & Grocery/Pepperidge Farm" -"3734780_7455738_9166487","Planters","Shop by Brand/Food & Grocery/Planters" -"3734780_7455738_2512582","Starbucks","Shop by Brand/Food & Grocery/Starbucks" -"3734780_7455738_9030640","Thai Kitchen","Shop by Brand/Food & Grocery/Thai Kitchen" diff --git a/WalmartPipeline/Categories/Food_subtree.csv b/WalmartPipeline/Categories/Food_subtree.csv deleted file mode 100644 index d5e0036..0000000 --- a/WalmartPipeline/Categories/Food_subtree.csv +++ /dev/null @@ -1,1246 +0,0 @@ -id,name,path -"976759","Food","Food" -"976759_2606229","@ Manual Shelves - Food","Food/@ Manual Shelves - Food" -"976759_2606229_8375512","$1 Food staples","Food/@ Manual Shelves - Food/$1 Food staples" -"976759_2606229_4353203","Almond Flour","Food/@ Manual Shelves - Food/Almond Flour" -"976759_2606229_2387675","Food Total Deals","Food/@ Manual Shelves - Food/Food Total Deals" -"976759_2606229_2958467","General Mills Box Tops Cereal","Food/@ Manual Shelves - Food/General Mills Box Tops Cereal" -"976759_2606229_3643082","General Mills Box Tops Dairy","Food/@ Manual Shelves - Food/General Mills Box Tops Dairy" -"976759_2606229_6093697","General Mills Box Tops Frozen","Food/@ Manual Shelves - Food/General Mills Box Tops Frozen" -"976759_2606229_8503205","General Mills Box Tops Meals","Food/@ Manual Shelves - Food/General Mills Box Tops Meals" -"976759_2606229_2412525","General Mills Box Tops Natural & Organic","Food/@ Manual Shelves - Food/General Mills Box Tops Natural & Organic" -"976759_2606229_3345098","General Mills Box Tops School Supplies","Food/@ Manual Shelves - Food/General Mills Box Tops School Supplies" -"976759_2606229_3638396","General Mills Box Tops Snacks","Food/@ Manual Shelves - Food/General Mills Box Tops Snacks" -"976759_2606229_7979437","General Mills Box Tops Water","Food/@ Manual Shelves - Food/General Mills Box Tops Water" -"976759_2606229_2161613","General Mills Fight Hunger","Food/@ Manual Shelves - Food/General Mills Fight Hunger" -"976759_2606229_2043501","General Mills Snacks","Food/@ Manual Shelves - Food/General Mills Snacks" -"976759_2606229_6420550","General Mills Treat Bars","Food/@ Manual Shelves - Food/General Mills Treat Bars" -"976759_2606229_5358210","GM090419","Food/@ Manual Shelves - Food/GM090419" -"976759_2606229_9723046","Hershey Baking","Food/@ Manual Shelves - Food/Hershey Baking" -"976759_2606229_1992604","Hershey Holiday","Food/@ Manual Shelves - Food/Hershey Holiday" -"976759_2606229_7458550","KC Masterpiece","Food/@ Manual Shelves - Food/KC Masterpiece" -"976759_2606229_4002011","Kellogg's Cereal","Food/@ Manual Shelves - Food/Kellogg's Cereal" -"976759_2606229_6912549","Kellogg's Nutri-Grain","Food/@ Manual Shelves - Food/Kellogg's Nutri-Grain" -"976759_2606229_6496189","Kellogg's PWS Brand Amplifier","Food/@ Manual Shelves - Food/Kellogg's PWS Brand Amplifier" -"976759_2606229_7285783","Kellogg's Rice Krispies Treats","Food/@ Manual Shelves - Food/Kellogg's Rice Krispies Treats" -"976759_2606229_6229293","Kit Kat Duo","Food/@ Manual Shelves - Food/Kit Kat Duo" -"976759_2606229_6952411","Local Finds","Food/@ Manual Shelves - Food/Local Finds" -"976759_2606229_2061744","Mosaic Beverages","Food/@ Manual Shelves - Food/Mosaic Beverages" -"976759_2606229_4445124","Mosaic Coffee Tea","Food/@ Manual Shelves - Food/Mosaic Coffee Tea" -"976759_2606229_4068629","Mosaic Food","Food/@ Manual Shelves - Food/Mosaic Food" -"976759_2606229_3339994","Mosaic Pantry","Food/@ Manual Shelves - Food/Mosaic Pantry" -"976759_2606229_6309744","Mosaic Snacks","Food/@ Manual Shelves - Food/Mosaic Snacks" -"976759_2606229_1841035","National Pancake Day","Food/@ Manual Shelves - Food/National Pancake Day" -"976759_2606229_2453624","NYNY Beverages","Food/@ Manual Shelves - Food/NYNY Beverages" -"976759_2606229_9334013","Pop-Tarts","Food/@ Manual Shelves - Food/Pop-Tarts" -"976759_2606229_1821433","Return to School","Food/@ Manual Shelves - Food/Return to School" -"976759_2606229_8588125","Splenda Stevia","Food/@ Manual Shelves - Food/Splenda Stevia" -"976759_2975985","Alcohol","Food/Alcohol" -"976759_2975985_3856625","About Alcohol","Food/Alcohol/About Alcohol" -"976759_2975985_2142284","Alcohol delivery & pickup in Kissimmee, Florida","Food/Alcohol/Alcohol delivery & pickup in Kissimmee, Florida" -"976759_2975985_7975157","Alcohol delivery & pickup in Naples, Florida","Food/Alcohol/Alcohol delivery & pickup in Naples, Florida" -"976759_2975985_1608388","Alcohol delivery & pickup in Panama City Beach, Florida","Food/Alcohol/Alcohol delivery & pickup in Panama City Beach, Florida" -"976759_2975985_2414163","Alcohol delivery & pickup in Phoenix, Arizona","Food/Alcohol/Alcohol delivery & pickup in Phoenix, Arizona" -"976759_2975985_8666737","Alcohol delivery & pickup in Tucson, Arizona","Food/Alcohol/Alcohol delivery & pickup in Tucson, Arizona" -"976759_2975985_7165884","Alcohol Workpage","Food/Alcohol/Alcohol Workpage" -"976759_2975985_5110387","Beer","Food/Alcohol/Beer" -"976759_2975985_5979903","Calorie & Carb Conscious Alcohol","Food/Alcohol/Calorie & Carb Conscious Alcohol" -"976759_2975985_8982253","Cocktails & Seltzers","Food/Alcohol/Cocktails & Seltzers" -"976759_2975985_8178984","Hard Seltzer","Food/Alcohol/Hard Seltzer" -"976759_2975985_3773575","Kentucky Derby Drinks","Food/Alcohol/Kentucky Derby Drinks" -"976759_2975985_5702937","Light & Organic Alcohol","Food/Alcohol/Light & Organic Alcohol" -"976759_2975985_7222931","New & Seasonal Alcohol","Food/Alcohol/New & Seasonal Alcohol" -"976759_2975985_4158159","Non-Alcoholic Beverages","Food/Alcohol/Non-Alcoholic Beverages" -"976759_2975985_9695490","Pre-Mixed Cocktails","Food/Alcohol/Pre-Mixed Cocktails" -"976759_2975985_8910898","Shop All Alcohol","Food/Alcohol/Shop All Alcohol" -"976759_2975985_8113561","Shop by Brand","Food/Alcohol/Shop by Brand" -"976759_2975985_4538947","Shop by Category","Food/Alcohol/Shop by Category" -"976759_2975985_9161098","Shop by Occasion","Food/Alcohol/Shop by Occasion" -"976759_2975985_1538854","Shop by Theme","Food/Alcohol/Shop by Theme" -"976759_2975985_1530122","Single Serve Adult Beverages","Food/Alcohol/Single Serve Adult Beverages" -"976759_2975985_3757145","Single Serve Beer, Seltzers, and Malt Beverages","Food/Alcohol/Single Serve Beer, Seltzers, and Malt Beverages" -"976759_2975985_8439204","Spirits","Food/Alcohol/Spirits" -"976759_2975985_8604954","Wine","Food/Alcohol/Wine" -"976759_9638107","All Food","Food/All Food" -"976759_976779","Bakery & Bread","Food/Bakery & Bread" -"976759_976779_4684016","Antonina’s Gluten-Free Bakery","Food/Bakery & Bread/Antonina’s Gluten-Free Bakery" -"976759_976779_3396508","Artisan Breads","Food/Bakery & Bread/Artisan Breads" -"976759_976779_1951361","Bakery Cookies","Food/Bakery & Bread/Bakery Cookies" -"976759_976779_4525853","Bakery Sweets","Food/Bakery & Bread/Bakery Sweets" -"976759_976779_8399244","Bread","Food/Bakery & Bread/Bread" -"976759_976779_3124271","Bread Shop","Food/Bakery & Bread/Bread Shop" -"976759_976779_1044115","Breakfast Breads","Food/Bakery & Bread/Breakfast Breads" -"976759_976779_4804035","Brownie Bites","Food/Bakery & Bread/Brownie Bites" -"976759_976779_3465538","Brownies","Food/Bakery & Bread/Brownies" -"976759_976779_5829009","Buns","Food/Bakery & Bread/Buns" -"976759_976779_9997386","Cakes","Food/Bakery & Bread/Cakes" -"976759_976779_4725038","Carlo's Bakery","Food/Bakery & Bread/Carlo's Bakery" -"976759_976779_6127426","Conchas","Food/Bakery & Bread/Conchas" -"976759_976779_2408821","Cupcakes","Food/Bakery & Bread/Cupcakes" -"976759_976779_4520008","Curated browse shelves","Food/Bakery & Bread/Curated browse shelves" -"976759_976779_4184097","Custom Bakery Cakes","Food/Bakery & Bread/Custom Bakery Cakes" -"976759_976779_7654499","David's Cookies","Food/Bakery & Bread/David's Cookies" -"976759_976779_1688722","Ethel's Baking Co.","Food/Bakery & Bread/Ethel's Baking Co." -"976759_976779_5265124","Fall Desserts","Food/Bakery & Bread/Fall Desserts" -"976759_976779_9431063","Freshness Guaranteed Bakery","Food/Bakery & Bread/Freshness Guaranteed Bakery" -"976759_976779_8001722","From Our Bakery","Food/Bakery & Bread/From Our Bakery" -"976759_976779_2496410","Gingerbread houses","Food/Bakery & Bread/Gingerbread houses" -"976759_976779_7881290","Hispanic Baking & Bakery","Food/Bakery & Bread/Hispanic Baking & Bakery" -"976759_976779_7869763","Marketside Bakery","Food/Bakery & Bread/Marketside Bakery" -"976759_976779_8869529","Order Ahead Cakes","Food/Bakery & Bread/Order Ahead Cakes" -"976759_976779_1001456","Pastries","Food/Bakery & Bread/Pastries" -"976759_976779_2464018","Pies","Food/Bakery & Bread/Pies" -"976759_976779_1001464","Pizza Crust","Food/Bakery & Bread/Pizza Crust" -"976759_976779_1037480","Rolls","Food/Bakery & Bread/Rolls" -"976759_976779_6865499","Rubicon Bakers","Food/Bakery & Bread/Rubicon Bakers" -"976759_976779_5635644","Shop all Bakery","Food/Bakery & Bread/Shop all Bakery" -"976759_976779_7756634","Shop all Bakery & Bread","Food/Bakery & Bread/Shop all Bakery & Bread" -"976759_976779_3649607","Sweet Craft Dolceria","Food/Bakery & Bread/Sweet Craft Dolceria" -"976759_976779_9318357","Sweet Treats","Food/Bakery & Bread/Sweet Treats" -"976759_976779_2993335","Tortillas","Food/Bakery & Bread/Tortillas" -"976759_976780","Baking","Food/Baking" -"976759_976780_2020825","Bake some fall goodies kits","Food/Baking/Bake some fall goodies kits" -"976759_976780_6748087","Baking fall flavors","Food/Baking/Baking fall flavors" -"976759_976780_9959366","Baking Ingredients","Food/Baking/Baking Ingredients" -"976759_976780_3850379","Baking Pumpkin Flavors","Food/Baking/Baking Pumpkin Flavors" -"976759_976780_5920135","Baking Supplies","Food/Baking/Baking Supplies" -"976759_976780_2678327","Biscuit mixes","Food/Baking/Biscuit mixes" -"976759_976780_4930324","Butter, Oils, & Shortening","Food/Baking/Butter, Oils, & Shortening" -"976759_976780_2911883","Cinnamon","Food/Baking/Cinnamon" -"976759_976780_4389538","Cloves","Food/Baking/Cloves" -"976759_976780_2432872","Cornbread mixes","Food/Baking/Cornbread mixes" -"976759_976780_4421440","Easter baking cakes & cupcakes","Food/Baking/Easter baking cakes & cupcakes" -"976759_976780_5195984","Easter baking candy","Food/Baking/Easter baking candy" -"976759_976780_2230875","Easter baking cookies","Food/Baking/Easter baking cookies" -"976759_976780_6314071","Easy to Make","Food/Baking/Easy to Make" -"976759_976780_5837233","Everyday baking essentials","Food/Baking/Everyday baking essentials" -"976759_976780_9442036","Great Value Deals","Food/Baking/Great Value Deals" -"976759_976780_4741545","Halloween baking HQ","Food/Baking/Halloween baking HQ" -"976759_976780_6426006","Healthy baking","Food/Baking/Healthy baking" -"976759_976780_4621790","Holiday sprinkles","Food/Baking/Holiday sprinkles" -"976759_976780_6127550","Homemade Cinnamon Rolls","Food/Baking/Homemade Cinnamon Rolls" -"976759_976780_5078092","Jell-O","Food/Baking/Jell-O" -"976759_976780_5330333","Low ingredient baking","Food/Baking/Low ingredient baking" -"976759_976780_9801587","M&M's Holiday Bake Center","Food/Baking/M&M's Holiday Bake Center" -"976759_976780_2148843","More baking","Food/Baking/More baking" -"976759_976780_7642292","Nestle bake magic for every basket","Food/Baking/Nestle bake magic for every basket" -"976759_976780_9361981","New & trending baking","Food/Baking/New & trending baking" -"976759_976780_5972699","Oreo. Imagine, bake & decorate","Food/Baking/Oreo. Imagine, bake & decorate" -"976759_976780_9071370","Pumpkin Spice","Food/Baking/Pumpkin Spice" -"976759_976780_3695230","Seasonal Baking","Food/Baking/Seasonal Baking" -"976759_976780_4879413","Shop Top Brands","Food/Baking/Shop Top Brands" -"976759_976780_2385246","Sprinkles","Food/Baking/Sprinkles" -"976759_976780_8946476","St. Patrick's Day Baking","Food/Baking/St. Patrick's Day Baking" -"976759_976780_4770372","Stranger Things dessert mixes","Food/Baking/Stranger Things dessert mixes" -"976759_976780_1044129","Sugar & Sweeteners","Food/Baking/Sugar & Sweeteners" -"976759_976780_1343575","Vanilla Extract","Food/Baking/Vanilla Extract" -"976759_976782","Beverages","Food/Beverages" -"976759_976782_4559367","@ manual shelves beverages","Food/Beverages/@ manual shelves beverages" -"976759_976782_8840632","@Browse Shelf SEO Curated","Food/Beverages/@Browse Shelf SEO Curated" -"976759_976782_2661356","Alani Nu","Food/Beverages/Alani Nu" -"976759_976782_3457307","Caliwater Hydration","Food/Beverages/Caliwater Hydration" -"976759_976782_5364044","Chilled Beverages","Food/Beverages/Chilled Beverages" -"976759_976782_8783063","Coca-Cola","Food/Beverages/Coca-Cola" -"976759_976782_5533811","Cocktail Mixers","Food/Beverages/Cocktail Mixers" -"976759_976782_8978567","Drink Mixes & Water Enhancers","Food/Beverages/Drink Mixes & Water Enhancers" -"976759_976782_9357528","Energy Drinks","Food/Beverages/Energy Drinks" -"976759_976782_1760088","Every Day Heroes","Food/Beverages/Every Day Heroes" -"976759_976782_5795119","Functional & Enhanced Beverages","Food/Beverages/Functional & Enhanced Beverages" -"976759_976782_6366289","Functional Beverages","Food/Beverages/Functional Beverages" -"976759_976782_4955760","Gameday Pepsi","Food/Beverages/Gameday Pepsi" -"976759_976782_7956282","Gatorade Cans","Food/Beverages/Gatorade Cans" -"976759_976782_4308612","Gatorade Zero","Food/Beverages/Gatorade Zero" -"976759_976782_9883261","Gatorlyte","Food/Beverages/Gatorlyte" -"976759_976782_5730567","Health-Inspired Beverages","Food/Beverages/Health-Inspired Beverages" -"976759_976782_1044077","Hot Cocoa","Food/Beverages/Hot Cocoa" -"976759_976782_2403214","Hydration Drinks","Food/Beverages/Hydration Drinks" -"976759_976782_3681207","Ice","Food/Beverages/Ice" -"976759_976782_7106941","Juice & Chilled Beverages","Food/Beverages/Juice & Chilled Beverages" -"976759_976782_1001321","Juices","Food/Beverages/Juices" -"976759_976782_5512594","KDP Holiday Mixers","Food/Beverages/KDP Holiday Mixers" -"976759_976782_9166028","Latin Beverages","Food/Beverages/Latin Beverages" -"976759_976782_8649877","Milk","Food/Beverages/Milk" -"976759_976782_8167813","Multipack Beverages Shipped to You","Food/Beverages/Multipack Beverages Shipped to You" -"976759_976782_4998827","Non Alcoholic Beer","Food/Beverages/Non Alcoholic Beer" -"976759_976782_8673018","Pepsi Beverages","Food/Beverages/Pepsi Beverages" -"976759_976782_1001683","Powdered Drink Mixes","Food/Beverages/Powdered Drink Mixes" -"976759_976782_4798856","Shop All Beverages","Food/Beverages/Shop All Beverages" -"976759_976782_4321403","Soda Mini Cans","Food/Beverages/Soda Mini Cans" -"976759_976782_1001680","Soda Pop","Food/Beverages/Soda Pop" -"976759_976782_2498001","Soda Shop","Food/Beverages/Soda Shop" -"976759_976782_1001682","Sports Drinks","Food/Beverages/Sports Drinks" -"976759_976782_1001320","Tea","Food/Beverages/Tea" -"976759_976782_1001659","Water","Food/Beverages/Water" -"976759_976782_1228026","Water Flavoring","Food/Beverages/Water Flavoring" -"976759_5024778","Brands","Food/Brands" -"976759_5024778_6400050","5 Hour Energy","Food/Brands/5 Hour Energy" -"976759_5024778_9064348","Atkins","Food/Brands/Atkins" -"976759_5024778_9936705","Bella","Food/Brands/Bella" -"976759_5024778_5425011","Ben's Original","Food/Brands/Ben's Original" -"976759_5024778_2639767","Big G Brands","Food/Brands/Big G Brands" -"976759_5024778_3942024","Bucked Up","Food/Brands/Bucked Up" -"976759_5024778_7042509","Bucked Up Energy","Food/Brands/Bucked Up Energy" -"976759_5024778_9275861","Cafe All Day","Food/Brands/Cafe All Day" -"976759_5024778_9857555","Cafe Bustelo","Food/Brands/Cafe Bustelo" -"976759_5024778_3454319","Campbell's Joy Night In","Food/Brands/Campbell's Joy Night In" -"976759_5024778_8842154","Cap'n Crunch","Food/Brands/Cap'n Crunch" -"976759_5024778_7147676","Cargill Holiday","Food/Brands/Cargill Holiday" -"976759_5024778_1438889","Carl Buddig & Company","Food/Brands/Carl Buddig & Company" -"976759_5024778_4543141","Chameleon Coffee","Food/Brands/Chameleon Coffee" -"976759_5024778_8051816","Cheerios Hearts","Food/Brands/Cheerios Hearts" -"976759_5024778_4454920","Cheetos","Food/Brands/Cheetos" -"976759_5024778_8996365","Country Crock","Food/Brands/Country Crock" -"976759_5024778_3769640","Del Monte","Food/Brands/Del Monte" -"976759_5024778_4029052","Doggie Days of Summer","Food/Brands/Doggie Days of Summer" -"976759_5024778_9842705","Doritos","Food/Brands/Doritos" -"976759_5024778_9379103","Doumak","Food/Brands/Doumak" -"976759_5024778_5256077","Dunkin","Food/Brands/Dunkin" -"976759_5024778_4565595","Easter Magic","Food/Brands/Easter Magic" -"976759_5024778_4489417","Essentia Water","Food/Brands/Essentia Water" -"976759_5024778_1378859","Fall Tailgating","Food/Brands/Fall Tailgating" -"976759_5024778_4039389","Family Feeds Family","Food/Brands/Family Feeds Family" -"976759_5024778_9270381","Ferrara Candy","Food/Brands/Ferrara Candy" -"976759_5024778_9269348","Ferrara Cookies","Food/Brands/Ferrara Cookies" -"976759_5024778_3296254","Ferrara Fruit Snacks","Food/Brands/Ferrara Fruit Snacks" -"976759_5024778_5302177","Ferrero Chocolate Halloween","Food/Brands/Ferrero Chocolate Halloween" -"976759_5024778_3546667","FisherNuts","Food/Brands/FisherNuts" -"976759_5024778_6559691","FlaminHot","Food/Brands/FlaminHot" -"976759_5024778_7535488","Flavor For Everyone","Food/Brands/Flavor For Everyone" -"976759_5024778_1257684","Folgers","Food/Brands/Folgers" -"976759_5024778_9426671","Forto","Food/Brands/Forto" -"976759_5024778_9418218","French's Dipping Sauces","Food/Brands/French's Dipping Sauces" -"976759_5024778_8235195","French's Fried Onions","Food/Brands/French's Fried Onions" -"976759_5024778_2981238","Funnest Summer","Food/Brands/Funnest Summer" -"976759_5024778_8527323","Gatorade","Food/Brands/Gatorade" -"976759_5024778_6464798","General Mills","Food/Brands/General Mills" -"976759_5024778_6446154","General Mills Cereal","Food/Brands/General Mills Cereal" -"976759_5024778_4041385","General Mills Tailgate Nation","Food/Brands/General Mills Tailgate Nation" -"976759_5024778_5177358","Ghirardelli Smores","Food/Brands/Ghirardelli Smores" -"976759_5024778_4457749","Goldfish Snacks","Food/Brands/Goldfish Snacks" -"976759_5024778_7351211","Green Bean Casserole","Food/Brands/Green Bean Casserole" -"976759_5024778_3582485","GT's Living Foods","Food/Brands/GT's Living Foods" -"976759_5024778_8040036","Happy On Hand","Food/Brands/Happy On Hand" -"976759_5024778_7134192","Happy Thursday","Food/Brands/Happy Thursday" -"976759_5024778_6329398","Heinz","Food/Brands/Heinz" -"976759_5024778_4613637","Hellman's Food Waste","Food/Brands/Hellman's Food Waste" -"976759_5024778_3259770","Hellmann's","Food/Brands/Hellmann's" -"976759_5024778_4399390","Hershey Baking","Food/Brands/Hershey Baking" -"976759_5024778_2054857","Hershey Celebrate SHE","Food/Brands/Hershey Celebrate SHE" -"976759_5024778_4334348","Hershey Halloween","Food/Brands/Hershey Halloween" -"976759_5024778_8211645","Hershey Holiday","Food/Brands/Hershey Holiday" -"976759_5024778_1941177","Hershey S'mores","Food/Brands/Hershey S'mores" -"976759_5024778_9553420","Hershey Summer Sweets","Food/Brands/Hershey Summer Sweets" -"976759_5024778_1318842","Hersheys Easter","Food/Brands/Hersheys Easter" -"976759_5024778_5466507","Hidden Valley Ranch","Food/Brands/Hidden Valley Ranch" -"976759_5024778_8975154","Jason","Food/Brands/Jason" -"976759_5024778_4126667","Jennie-O","Food/Brands/Jennie-O" -"976759_5024778_5203051","Jimmy Dean","Food/Brands/Jimmy Dean" -"976759_5024778_1367004","Kellogg's Feeding Reading","Food/Brands/Kellogg's Feeding Reading" -"976759_5024778_4559445","Kellogg's Ingrained","Food/Brands/Kellogg's Ingrained" -"976759_5024778_9866682","KIND","Food/Brands/KIND" -"976759_5024778_9421767","Kinder Bueno","Food/Brands/Kinder Bueno" -"976759_5024778_6719753","Kit Kat","Food/Brands/Kit Kat" -"976759_5024778_2186445","Klass","Food/Brands/Klass" -"976759_5024778_4554482","Kool Aid","Food/Brands/Kool Aid" -"976759_5024778_8191847","Kraft","Food/Brands/Kraft" -"976759_5024778_5736135","Kraft Art of the Burger","Food/Brands/Kraft Art of the Burger" -"976759_5024778_8988475","Kraft Baking","Food/Brands/Kraft Baking" -"976759_5024778_4102326","Kraft Beverages","Food/Brands/Kraft Beverages" -"976759_5024778_6969793","Kraft Breakfast","Food/Brands/Kraft Breakfast" -"976759_5024778_7326658","Kraft Condiments","Food/Brands/Kraft Condiments" -"976759_5024778_9001959","Kraft Dinners","Food/Brands/Kraft Dinners" -"976759_5024778_9004736","Kraft Fall Coffee","Food/Brands/Kraft Fall Coffee" -"976759_5024778_7885412","Kraft Favorite Essentials","Food/Brands/Kraft Favorite Essentials" -"976759_5024778_1680857","Kraft Favorites","Food/Brands/Kraft Favorites" -"976759_5024778_6868622","Kraft Grilling","Food/Brands/Kraft Grilling" -"976759_5024778_3897943","Kraft Holiday Coffee Sale","Food/Brands/Kraft Holiday Coffee Sale" -"976759_5024778_9492474","Kraft Holiday Favorites","Food/Brands/Kraft Holiday Favorites" -"976759_5024778_5332588","Kraft Lunchbox","Food/Brands/Kraft Lunchbox" -"976759_5024778_3905915","Kraft Sandwiches","Food/Brands/Kraft Sandwiches" -"976759_5024778_5616136","Kraft Seasonal","Food/Brands/Kraft Seasonal" -"976759_5024778_4494635","Kraft Summer","Food/Brands/Kraft Summer" -"976759_5024778_2733845","Kraft Tailgate Homegate","Food/Brands/Kraft Tailgate Homegate" -"976759_5024778_1826565","LaCroix","Food/Brands/LaCroix" -"976759_5024778_6089814","Land O Frost","Food/Brands/Land O Frost" -"976759_5024778_7432091","Lays-Ruffles-Favorites","Food/Brands/Lays-Ruffles-Favorites" -"976759_5024778_8513423","LesserEvil","Food/Brands/LesserEvil" -"976759_5024778_8445270","Lindt Chocolate","Food/Brands/Lindt Chocolate" -"976759_5024778_2432393","Lindt Lindor","Food/Brands/Lindt Lindor" -"976759_5024778_6238873","Made Better","Food/Brands/Made Better" -"976759_5024778_1729826","Make It Home","Food/Brands/Make It Home" -"976759_5024778_8099834","Make It Home","Food/Brands/Make It Home" -"976759_5024778_4640920","Mama Marys","Food/Brands/Mama Marys" -"976759_5024778_9410706","Maxwell House","Food/Brands/Maxwell House" -"976759_5024778_6070636","McCormick","Food/Brands/McCormick" -"976759_5024778_4544151","McCormick Holiday","Food/Brands/McCormick Holiday" -"976759_5024778_5601273","Merry in the making","Food/Brands/Merry in the making" -"976759_5024778_9392277","Mondelez","Food/Brands/Mondelez" -"976759_5024778_8908532","Morton Salt","Food/Brands/Morton Salt" -"976759_5024778_7202272","National Coffee Month","Food/Brands/National Coffee Month" -"976759_5024778_5515352","National Ice Cream Month","Food/Brands/National Ice Cream Month" -"976759_5024778_9565586","Nature Valley","Food/Brands/Nature Valley" -"976759_5024778_2990490","Nature Valley Recycle","Food/Brands/Nature Valley Recycle" -"976759_5024778_4292340","Nesquick","Food/Brands/Nesquick" -"976759_5024778_4926538","Nestle Back to Routine","Food/Brands/Nestle Back to Routine" -"976759_5024778_2103574","Nestle Holiday","Food/Brands/Nestle Holiday" -"976759_5024778_9424659","Nestle holiday meal solutions","Food/Brands/Nestle holiday meal solutions" -"976759_5024778_3552288","Nestle-holiday-baking","Food/Brands/Nestle-holiday-baking" -"976759_5024778_3547129","Old El Paso","Food/Brands/Old El Paso" -"976759_5024778_9303613","Oreo","Food/Brands/Oreo" -"976759_5024778_4890446","Oreo Pride","Food/Brands/Oreo Pride" -"976759_5024778_5968931","Oscar Mayer","Food/Brands/Oscar Mayer" -"976759_5024778_3296546","Pace Taco Night","Food/Brands/Pace Taco Night" -"976759_5024778_6278121","Pearl Milling","Food/Brands/Pearl Milling" -"976759_5024778_5461032","Pepsi Game Day Picks","Food/Brands/Pepsi Game Day Picks" -"976759_5024778_8428704","Permissible Snacking","Food/Brands/Permissible Snacking" -"976759_5024778_2631520","Pillsbury Cookies","Food/Brands/Pillsbury Cookies" -"976759_5024778_7073908","Planters","Food/Brands/Planters" -"976759_5024778_1666231","Pompeian","Food/Brands/Pompeian" -"976759_5024778_8586091","Quaker","Food/Brands/Quaker" -"976759_5024778_5431931","Quaker School Foods","Food/Brands/Quaker School Foods" -"976759_5024778_3359221","RedBull Gaming","Food/Brands/RedBull Gaming" -"976759_5024778_4226980","Reese's","Food/Brands/Reese's" -"976759_5024778_7828716","REIGN Total Body Fuel","Food/Brands/REIGN Total Body Fuel" -"976759_5024778_1434803","Ritz","Food/Brands/Ritz" -"976759_5024778_1660638","Ritz Pair Up for Good","Food/Brands/Ritz Pair Up for Good" -"976759_5024778_4249193","Rockstar Energy","Food/Brands/Rockstar Energy" -"976759_5024778_2290596","Rumba Meats","Food/Brands/Rumba Meats" -"976759_5024778_7508556","Sabra","Food/Brands/Sabra" -"976759_5024778_3053056","Simple Mills","Food/Brands/Simple Mills" -"976759_5024778_2984863","Smithfield Foods Inc","Food/Brands/Smithfield Foods Inc" -"976759_5024778_1401926","Smuckers","Food/Brands/Smuckers" -"976759_5024778_1901586","Smuckers NYNYRTS","Food/Brands/Smuckers NYNYRTS" -"976759_5024778_6299540","Splenda","Food/Brands/Splenda" -"976759_5024778_2587501","Spooky Season","Food/Brands/Spooky Season" -"976759_5024778_1879261","Starbucks by Nespresso","Food/Brands/Starbucks by Nespresso" -"976759_5024778_5086660","Starbucks Flavors","Food/Brands/Starbucks Flavors" -"976759_5024778_6944066","Starbucks Seasons","Food/Brands/Starbucks Seasons" -"976759_5024778_3687838","Starbucks Summer","Food/Brands/Starbucks Summer" -"976759_5024778_2262368","Stranger Things","Food/Brands/Stranger Things" -"976759_5024778_3335011","SunMaidBacktoSchool","Food/Brands/SunMaidBacktoSchool" -"976759_5024778_2280393","Swedish Fish & Friends","Food/Brands/Swedish Fish & Friends" -"976759_5024778_4264825","Tazo Regenerative","Food/Brands/Tazo Regenerative" -"976759_5024778_8962010","Thomas Breakfast","Food/Brands/Thomas Breakfast" -"976759_5024778_8195010","Together Tastes Better","Food/Brands/Together Tastes Better" -"976759_5024778_8442905","Toll House","Food/Brands/Toll House" -"976759_5024778_6590084","Totino's","Food/Brands/Totino's" -"976759_5024778_5973203","Totino's NBA 2K22","Food/Brands/Totino's NBA 2K22" -"976759_5024778_4558531","Truvia","Food/Brands/Truvia" -"976759_5024778_1985197","Tyson Foods","Food/Brands/Tyson Foods" -"976759_5024778_9609814","Velveeta","Food/Brands/Velveeta" -"976759_5024778_4410318","We Fix Water","Food/Brands/We Fix Water" -"976759_2241441","Breadcrumbs","Food/Breadcrumbs" -"976759_2241441_6518138","Italian Style","Food/Breadcrumbs/Italian Style" -"976759_2241441_4146513","Panko","Food/Breadcrumbs/Panko" -"976759_2241441_8369700","Plain","Food/Breadcrumbs/Plain" -"976759_2241441_5604485","Seasoned","Food/Breadcrumbs/Seasoned" -"976759_2241441_9924060","Shop All Breadcrumbs","Food/Breadcrumbs/Shop All Breadcrumbs" -"976759_976783","Breakfast & Cereal","Food/Breakfast & Cereal" -"976759_9392773","Breakfast & Cereal","Food/Breakfast & Cereal" -"976759_976783_7381533","belVita","Food/Breakfast & Cereal/belVita" -"976759_976783_1231208","Breakfast Cereal","Food/Breakfast & Cereal/Breakfast Cereal" -"976759_976783_2716997","Breakfast finds","Food/Breakfast & Cereal/Breakfast finds" -"976759_976783_2453448","Breakfast foods from our brands","Food/Breakfast & Cereal/Breakfast foods from our brands" -"976759_976783_8092506","Breakfast Ideas","Food/Breakfast & Cereal/Breakfast Ideas" -"976759_976783_8102529","Cereal & Granola","Food/Breakfast & Cereal/Cereal & Granola" -"976759_976783_7968162","Fiber One Bars","Food/Breakfast & Cereal/Fiber One Bars" -"976759_976783_5504375","General Mills Cereal Treat Bars","Food/Breakfast & Cereal/General Mills Cereal Treat Bars" -"976759_976783_8438428","Grab & Go Bars & Toaster Pastries","Food/Breakfast & Cereal/Grab & Go Bars & Toaster Pastries" -"976759_976783_1231209","Granola & Muesli","Food/Breakfast & Cereal/Granola & Muesli" -"976759_976783_2919949","Great Value Breakfast","Food/Breakfast & Cereal/Great Value Breakfast" -"976759_9392773_9884559","Hot Breakfast","Food/Breakfast & Cereal/Hot Breakfast" -"976759_9392773_4596114","Hot Cereal","Food/Breakfast & Cereal/Hot Cereal" -"976759_976783_1831416","Kellogg's Nutri-Grain Soft Baked Breakfast Bars","Food/Breakfast & Cereal/Kellogg's Nutri-Grain Soft Baked Breakfast Bars" -"976759_976783_8832936","Kid Friendly Breakfast","Food/Breakfast & Cereal/Kid Friendly Breakfast" -"976759_9392773_8196081","Muffins & Pastries","Food/Breakfast & Cereal/Muffins & Pastries" -"976759_9392773_9671607","Oatmeal","Food/Breakfast & Cereal/Oatmeal" -"976759_976783_1231210","Oatmeal & Grits","Food/Breakfast & Cereal/Oatmeal & Grits" -"976759_976783_7830606","Oatmeal & Grits","Food/Breakfast & Cereal/Oatmeal & Grits" -"976759_976783_6212246","On the go breakfast","Food/Breakfast & Cereal/On the go breakfast" -"976759_9392773_9285289","Pancake and Waffle Toppings","Food/Breakfast & Cereal/Pancake and Waffle Toppings" -"976759_976783_2228922","Pancakes & Waffles","Food/Breakfast & Cereal/Pancakes & Waffles" -"976759_976783_9424911","Quaker Breakfast","Food/Breakfast & Cereal/Quaker Breakfast" -"976759_976783_4572072","Rice Krispies Treats","Food/Breakfast & Cereal/Rice Krispies Treats" -"976759_9392773_6009402","Syrup","Food/Breakfast & Cereal/Syrup" -"976759_3407478","Bring the Juice","Food/Bring the Juice" -"976759_1096070","Candy","Food/Candy" -"976759_1096070_2851671","Better for you","Food/Candy/Better for you" -"976759_1096070_4888541","Brittle, Caramel & Toffee","Food/Candy/Brittle, Caramel & Toffee" -"976759_1096070_9143353","Bulk Candy","Food/Candy/Bulk Candy" -"976759_1096070_1429102","Candy Bars","Food/Candy/Candy Bars" -"976759_1096070_1224976","Chocolate","Food/Candy/Chocolate" -"976759_1096070_5658771","Christmas Candy","Food/Candy/Christmas Candy" -"976759_1096070_9134702","Cotton Candy","Food/Candy/Cotton Candy" -"976759_1096070_4780634","Easter Candy","Food/Candy/Easter Candy" -"976759_1096070_8799356","Feastables","Food/Candy/Feastables" -"976759_1096070_6555134","Fruit Flavored & Sour Candy","Food/Candy/Fruit Flavored & Sour Candy" -"976759_1096070_1224977","Gum","Food/Candy/Gum" -"976759_1096070_5851594","Gum & mints","Food/Candy/Gum & mints" -"976759_1096070_1224975","Gummy & Chewy Candy","Food/Candy/Gummy & Chewy Candy" -"976759_1096070_6041305","Halloween Candy","Food/Candy/Halloween Candy" -"976759_1096070_1224979","Hard Candy & Lollipops","Food/Candy/Hard Candy & Lollipops" -"976759_1096070_3090206","Mexican Candy","Food/Candy/Mexican Candy" -"976759_1096070_1224978","Mints","Food/Candy/Mints" -"976759_1096070_1224980","Multipacks & Bags","Food/Candy/Multipacks & Bags" -"976759_1096070_6336392","New in Candy","Food/Candy/New in Candy" -"976759_1096070_7069014","On the Go","Food/Candy/On the Go" -"976759_1096070_1241359","Shop by Brand","Food/Candy/Shop by Brand" -"976759_1096070_5013550","Shop candy by occasion","Food/Candy/Shop candy by occasion" -"976759_1096070_1517562","Sugar Free Candy","Food/Candy/Sugar Free Candy" -"976759_1096070_9457805","Valentine's Candy","Food/Candy/Valentine's Candy" -"976759_1086446","Coffee","Food/Coffee" -"976759_9570853","Coffee & Tea Subscription","Food/Coffee & Tea Subscription" -"976759_1086446_1889967","Allegro Coffee","Food/Coffee/Allegro Coffee" -"976759_1086446_2559085","Arabica Coffee","Food/Coffee/Arabica Coffee" -"976759_1086446_5857329","Barista Prima Coffeehouse","Food/Coffee/Barista Prima Coffeehouse" -"976759_1086446_3508859","Black Rifle Coffee","Food/Coffee/Black Rifle Coffee" -"976759_1086446_7859290","Black Rifle Coffee Pods","Food/Coffee/Black Rifle Coffee Pods" -"976759_1086446_2922868","Black Rifle Ship To Home Coffee","Food/Coffee/Black Rifle Ship To Home Coffee" -"976759_1086446_7443436","Blue Bottle Coffee","Food/Coffee/Blue Bottle Coffee" -"976759_1086446_1229654","Bottled Coffee","Food/Coffee/Bottled Coffee" -"976759_1086446_2931262","Boyers Coffee","Food/Coffee/Boyers Coffee" -"976759_1086446_5480182","Brooklyn Bean Roastery","Food/Coffee/Brooklyn Bean Roastery" -"976759_1086446_9164946","Bulk pods","Food/Coffee/Bulk pods" -"976759_1086446_4288945","Bulletproof","Food/Coffee/Bulletproof" -"976759_1086446_7145434","Cafe Bustelo","Food/Coffee/Cafe Bustelo" -"976759_1086446_5337550","Cafe Bustelo Ship To Home Coffee","Food/Coffee/Cafe Bustelo Ship To Home Coffee" -"976759_1086446_3060393","Cafe Du Monde Coffee","Food/Coffee/Cafe Du Monde Coffee" -"976759_1086446_7595739","Cafe Escapes Ship To Home Coffee","Food/Coffee/Cafe Escapes Ship To Home Coffee" -"976759_1086446_3775336","Cafe La Llave Coffee","Food/Coffee/Cafe La Llave Coffee" -"976759_1086446_2589844","Cameron's Coffee","Food/Coffee/Cameron's Coffee" -"976759_1086446_4163341","Cappuccino Coffee","Food/Coffee/Cappuccino Coffee" -"976759_1086446_5204146","Caribou Coffee","Food/Coffee/Caribou Coffee" -"976759_1086446_2894761","Chock full o'Nuts Coffee","Food/Coffee/Chock full o'Nuts Coffee" -"976759_1086446_9009206","Christopher Bean Coffee","Food/Coffee/Christopher Bean Coffee" -"976759_1086446_8753286","Coffee","Food/Coffee/Coffee" -"976759_1086446_7238690","Coffee & Coffee Pods","Food/Coffee/Coffee & Coffee Pods" -"976759_1086446_2310301","Coffee Additives","Food/Coffee/Coffee Additives" -"976759_1086446_5588831","Coffee and Tea","Food/Coffee/Coffee and Tea" -"976759_1086446_5475394","Coffee Bean and Tea Leaf","Food/Coffee/Coffee Bean and Tea Leaf" -"976759_1086446_1100007","Coffee Creamers","Food/Coffee/Coffee Creamers" -"976759_1086446_4542956","Coffee Drinks & Mix","Food/Coffee/Coffee Drinks & Mix" -"976759_1086446_1229655","Coffee Filters","Food/Coffee/Coffee Filters" -"976759_1086446_5228608","Coffee Flavors","Food/Coffee/Coffee Flavors" -"976759_1086446_6173211","Coffee Roast to Order","Food/Coffee/Coffee Roast to Order" -"976759_1086446_9241711","Coffee Syrups","Food/Coffee/Coffee Syrups" -"976759_1086446_5471454","Coffee With Chicory","Food/Coffee/Coffee With Chicory" -"976759_1086446_5484987","Cold Brew Ground Coffee","Food/Coffee/Cold Brew Ground Coffee" -"976759_1086446_3908418","Colombian Coffee","Food/Coffee/Colombian Coffee" -"976759_1086446_2425858","Community Coffee","Food/Coffee/Community Coffee" -"976759_1086446_1636748","Community Coffee Ship To Home","Food/Coffee/Community Coffee Ship To Home" -"976759_1086446_3387116","Crazy Cups","Food/Coffee/Crazy Cups" -"976759_1086446_7609659","Crazy Cups Coffee","Food/Coffee/Crazy Cups Coffee" -"976759_1086446_5303199","Cuban Coffee","Food/Coffee/Cuban Coffee" -"976759_1086446_3754266","Cuvee Coffee","Food/Coffee/Cuvee Coffee" -"976759_1086446_2469472","Dark Roast Coffee","Food/Coffee/Dark Roast Coffee" -"976759_1086446_3292988","Death Wish Coffee","Food/Coffee/Death Wish Coffee" -"976759_1086446_4286760","Death Wish Ship To Home Coffee","Food/Coffee/Death Wish Ship To Home Coffee" -"976759_1086446_2901896","Decaf Coffee","Food/Coffee/Decaf Coffee" -"976759_1086446_7310118","Dios Mio Coffee by Sofia Vergara","Food/Coffee/Dios Mio Coffee by Sofia Vergara" -"976759_1086446_2666670","Don Francisco's Coffee","Food/Coffee/Don Francisco's Coffee" -"976759_1086446_8067726","Double Donut Coffee","Food/Coffee/Double Donut Coffee" -"976759_1086446_7630289","Dunkin' Donuts","Food/Coffee/Dunkin' Donuts" -"976759_1086446_9091132","Dunkin' Ship To Home Coffee","Food/Coffee/Dunkin' Ship To Home Coffee" -"976759_1086446_1371544","Eight O'Clock Coffee","Food/Coffee/Eight O'Clock Coffee" -"976759_1086446_7224029","Eight O'Clock Ship To Home Coffee","Food/Coffee/Eight O'Clock Ship To Home Coffee" -"976759_1086446_4366064","Espresso","Food/Coffee/Espresso" -"976759_1086446_7775574","Espresso Pods & Capsules","Food/Coffee/Espresso Pods & Capsules" -"976759_1086446_9679850","Ethical Bean Coffee","Food/Coffee/Ethical Bean Coffee" -"976759_1086446_9426621","Fire department coffee","Food/Coffee/Fire department coffee" -"976759_1086446_8200909","Flavored Ground Coffee","Food/Coffee/Flavored Ground Coffee" -"976759_1086446_5919970","Folgers 1850 Coffee","Food/Coffee/Folgers 1850 Coffee" -"976759_1086446_9393075","Folgers Black Silk Coffee","Food/Coffee/Folgers Black Silk Coffee" -"976759_1086446_3100047","Folgers Coffee","Food/Coffee/Folgers Coffee" -"976759_1086446_6279310","Folgers Ship To Home Coffee","Food/Coffee/Folgers Ship To Home Coffee" -"976759_1086446_6460509","Four Sigmatic","Food/Coffee/Four Sigmatic" -"976759_1086446_9185846","Free rein","Food/Coffee/Free rein" -"976759_1086446_7463372","Free Rein Coffee","Food/Coffee/Free Rein Coffee" -"976759_1086446_9494964","French Market Coffee","Food/Coffee/French Market Coffee" -"976759_1086446_6833316","Frozen Bean Coffee","Food/Coffee/Frozen Bean Coffee" -"976759_1086446_3529850","Functional Coffee","Food/Coffee/Functional Coffee" -"976759_1086446_9208550","Gevalia","Food/Coffee/Gevalia" -"976759_1086446_6917621","Gloria Jean's Coffees","Food/Coffee/Gloria Jean's Coffees" -"976759_1086446_3653954","Great Value Coffee","Food/Coffee/Great Value Coffee" -"976759_1086446_5875478","Great Value Coffee Pods","Food/Coffee/Great Value Coffee Pods" -"976759_1086446_6490624","Green Mountain Coffee","Food/Coffee/Green Mountain Coffee" -"976759_1086446_6349793","Green Mountain Ship To Home Coffee","Food/Coffee/Green Mountain Ship To Home Coffee" -"976759_1086446_1229651","Ground Coffee","Food/Coffee/Ground Coffee" -"976759_1086446_2174088","Ground Coffee","Food/Coffee/Ground Coffee" -"976759_1086446_7002469","Hills Bros Coffee","Food/Coffee/Hills Bros Coffee" -"976759_1086446_3510432","Hot Beverages","Food/Coffee/Hot Beverages" -"976759_1086446_8601015","Iced Coffee","Food/Coffee/Iced Coffee" -"976759_1086446_7207956","illy Coffee","Food/Coffee/illy Coffee" -"976759_1086446_1229650","Instant Coffee","Food/Coffee/Instant Coffee" -"976759_1086446_7541069","Instant Coffee","Food/Coffee/Instant Coffee" -"976759_1086446_3896713","Javy Coffee","Food/Coffee/Javy Coffee" -"976759_1086446_3844694","Juan Valdez Coffee","Food/Coffee/Juan Valdez Coffee" -"976759_1086446_4490488","Kahawa 1893 Coffee","Food/Coffee/Kahawa 1893 Coffee" -"976759_1086446_9041445","Kauai Coffee","Food/Coffee/Kauai Coffee" -"976759_1086446_8135897","Keurig K-Cups & Coffee Pods","Food/Coffee/Keurig K-Cups & Coffee Pods" -"976759_1086446_5169965","Kicking Horse Coffee","Food/Coffee/Kicking Horse Coffee" -"976759_1086446_3763734","Krispy Kreme Coffee","Food/Coffee/Krispy Kreme Coffee" -"976759_1086446_2186660","La Colombe Coffee","Food/Coffee/La Colombe Coffee" -"976759_1086446_3345140","Lavazza Coffee","Food/Coffee/Lavazza Coffee" -"976759_1086446_7715337","Light Roast Coffee","Food/Coffee/Light Roast Coffee" -"976759_1086446_4029061","Lion Coffee","Food/Coffee/Lion Coffee" -"976759_1086446_4168978","Manual Shelf","Food/Coffee/Manual Shelf" -"976759_1086446_3591735","MAUD'S Ship To Home Coffee","Food/Coffee/MAUD'S Ship To Home Coffee" -"976759_1086446_8082858","MAUDS Coffee","Food/Coffee/MAUDS Coffee" -"976759_1086446_9154767","Maxwell House Coffee","Food/Coffee/Maxwell House Coffee" -"976759_1086446_2952758","McCafe","Food/Coffee/McCafe" -"976759_1086446_8054840","McCafe Ship To Home Coffee","Food/Coffee/McCafe Ship To Home Coffee" -"976759_1086446_2272924","Medium Roast Coffee","Food/Coffee/Medium Roast Coffee" -"976759_1086446_8415600","Mocha Mix","Food/Coffee/Mocha Mix" -"976759_1086446_4812120","National coffee month","Food/Coffee/National coffee month" -"976759_1086446_1801480","Nespresso Pods & Capsules","Food/Coffee/Nespresso Pods & Capsules" -"976759_1086446_1995978","New Coffee","Food/Coffee/New Coffee" -"976759_1086446_8218446","New England Coffee","Food/Coffee/New England Coffee" -"976759_1086446_4236581","Newman's Own Coffee","Food/Coffee/Newman's Own Coffee" -"976759_1086446_8551808","Organic Coffee","Food/Coffee/Organic Coffee" -"976759_1086446_5434583","Peet's Coffee","Food/Coffee/Peet's Coffee" -"976759_1086446_7853986","Peet's Ship To Home Coffee","Food/Coffee/Peet's Ship To Home Coffee" -"976759_1086446_5432700","Perfect Samplers Coffee","Food/Coffee/Perfect Samplers Coffee" -"976759_1086446_1394665","Postum Coffee","Food/Coffee/Postum Coffee" -"976759_1086446_9904732","Premium Ground Coffee","Food/Coffee/Premium Ground Coffee" -"976759_1086446_5248805","Rapid Fire Coffee","Food/Coffee/Rapid Fire Coffee" -"976759_1086446_1416772","Raven's Brew Coffee","Food/Coffee/Raven's Brew Coffee" -"976759_1086446_8320520","REVV Coffee","Food/Coffee/REVV Coffee" -"976759_1086446_3313187","San Francisco Bay Coffee","Food/Coffee/San Francisco Bay Coffee" -"976759_1086446_3520530","Seattle's Best Coffee","Food/Coffee/Seattle's Best Coffee" -"976759_1086446_2955086","Ship To Home Coffee","Food/Coffee/Ship To Home Coffee" -"976759_1086446_6683788","Ship To Home Coffee","Food/Coffee/Ship To Home Coffee" -"976759_1086446_3143145","Single-Serve Cups & Pods","Food/Coffee/Single-Serve Cups & Pods" -"976759_1086446_1229653","Single-Serve Cups & Pods","Food/Coffee/Single-Serve Cups & Pods" -"976759_1086446_2986178","Starbucks","Food/Coffee/Starbucks" -"976759_1086446_4252159","Starbucks Ship To Home Coffee","Food/Coffee/Starbucks Ship To Home Coffee" -"976759_1086446_5397338","Stone Street Coffee","Food/Coffee/Stone Street Coffee" -"976759_1086446_4754103","Strong & High Caffeine Coffee","Food/Coffee/Strong & High Caffeine Coffee" -"976759_1086446_5772849","Stumptown Coffee","Food/Coffee/Stumptown Coffee" -"976759_1086446_6202182","Super Coffee","Food/Coffee/Super Coffee" -"976759_1086446_9902997","Sustainably Sourced Coffee and Tea","Food/Coffee/Sustainably Sourced Coffee and Tea" -"976759_1086446_1587610","The Original Donut Shop","Food/Coffee/The Original Donut Shop" -"976759_1086446_1247843","The Original Donut Shop Ship To Home Coffee","Food/Coffee/The Original Donut Shop Ship To Home Coffee" -"976759_1086446_5550920","Tim Hortons Coffee","Food/Coffee/Tim Hortons Coffee" -"976759_1086446_2353747","Two Rivers Coffee","Food/Coffee/Two Rivers Coffee" -"976759_1086446_6364451","Victor Allen's","Food/Coffee/Victor Allen's" -"976759_1086446_9232806","Victor Allen's Ship To Home Coffee","Food/Coffee/Victor Allen's Ship To Home Coffee" -"976759_1086446_6427827","VitaCup","Food/Coffee/VitaCup" -"976759_1086446_7311495","Wake The Hell Up! Coffee","Food/Coffee/Wake The Hell Up! Coffee" -"976759_1086446_1229652","Whole Bean Coffee","Food/Coffee/Whole Bean Coffee" -"976759_1086446_6346545","Yuban Coffee","Food/Coffee/Yuban Coffee" -"976759_8149667","Comfort food","Food/Comfort food" -"976759_8149667_9003533","Chili","Food/Comfort food/Chili" -"976759_8149667_7475123","Comfort food","Food/Comfort food/Comfort food" -"976759_8149667_3415119","St. Patrick's day stews & chili","Food/Comfort food/St. Patrick's day stews & chili" -"976759_976786","Condiments, Sauces & Spices","Food/Condiments, Sauces & Spices" -"976759_976786_9696846","Condiments","Food/Condiments, Sauces & Spices/Condiments" -"976759_976786_4439815","Jams, Jellies and Preserves","Food/Condiments, Sauces & Spices/Jams, Jellies and Preserves" -"976759_976786_4515492","Smash Kitchen","Food/Condiments, Sauces & Spices/Smash Kitchen" -"976759_9176907","Dairy & Eggs","Food/Dairy & Eggs" -"976759_9176907_7545972","Biscuits, Cookies, Doughs & Crusts","Food/Dairy & Eggs/Biscuits, Cookies, Doughs & Crusts" -"976759_9176907_1001467","Butter & Margarine","Food/Dairy & Eggs/Butter & Margarine" -"976759_9176907_1001468","Cheese","Food/Dairy & Eggs/Cheese" -"976759_9176907_6336565","Chilled Snacks & Beverages","Food/Dairy & Eggs/Chilled Snacks & Beverages" -"976759_9176907_1341992","Cookie Dough","Food/Dairy & Eggs/Cookie Dough" -"976759_9176907_9550303","Cream & Creamers","Food/Dairy & Eggs/Cream & Creamers" -"976759_9176907_4752344","Dairy Brands","Food/Dairy & Eggs/Dairy Brands" -"976759_9176907_6053481","Dairy Essentials","Food/Dairy & Eggs/Dairy Essentials" -"976759_9176907_9527865","Dairy Recipe Food Trends","Food/Dairy & Eggs/Dairy Recipe Food Trends" -"976759_9176907_4504952","Doughs","Food/Dairy & Eggs/Doughs" -"976759_9176907_1001469","Eggs","Food/Dairy & Eggs/Eggs" -"976759_9176907_9413955","Great Value","Food/Dairy & Eggs/Great Value" -"976759_9176907_4405816","Milk","Food/Dairy & Eggs/Milk" -"976759_9176907_2427118","Non dairy milk buying guide","Food/Dairy & Eggs/Non dairy milk buying guide" -"976759_9176907_9893113","Plant-Based Dairy","Food/Dairy & Eggs/Plant-Based Dairy" -"976759_9176907_4899814","Protein Dairy","Food/Dairy & Eggs/Protein Dairy" -"976759_9176907_3733198","Pudding & Gelatin","Food/Dairy & Eggs/Pudding & Gelatin" -"976759_9176907_7287191","Sour Cream & Chilled Dips","Food/Dairy & Eggs/Sour Cream & Chilled Dips" -"976759_9176907_1001470","Yogurt","Food/Dairy & Eggs/Yogurt" -"976759_976789","Deli","Food/Deli" -"976759_976789_3827156","Budget Friendly Picks","Food/Deli/Budget Friendly Picks" -"976759_976789_4740967","Create Your Charcuterie Board","Food/Deli/Create Your Charcuterie Board" -"976759_976789_7155371","Deli Hot Case","Food/Deli/Deli Hot Case" -"976759_976789_8252357","Deli Meal Solutions","Food/Deli/Deli Meal Solutions" -"976759_976789_5428795","Deli Meat & Cheese","Food/Deli/Deli Meat & Cheese" -"976759_976789_3714576","Deli Nutrition Facts","Food/Deli/Deli Nutrition Facts" -"976759_976789_7419699","Deli Prepared Soups & Salads","Food/Deli/Deli Prepared Soups & Salads" -"976759_976789_7498848","Deli Private Brands","Food/Deli/Deli Private Brands" -"976759_976789_4194487","Deli Shop All","Food/Deli/Deli Shop All" -"976759_976789_2300191","Dinner Tonight","Food/Deli/Dinner Tonight" -"976759_976789_3251337","Fresh Meals","Food/Deli/Fresh Meals" -"976759_976789_7056897","Hummus, Dips & Salsa","Food/Deli/Hummus, Dips & Salsa" -"976759_976789_4237931","Kids Lunches & Snacks","Food/Deli/Kids Lunches & Snacks" -"976759_976789_9355377","Lunch Buying Guide","Food/Deli/Lunch Buying Guide" -"976759_976789_9449858","Lunch Combos","Food/Deli/Lunch Combos" -"976759_976789_8979491","Prepared Meals & Sides","Food/Deli/Prepared Meals & Sides" -"976759_976789_2985474","Sandwiches","Food/Deli/Sandwiches" -"976759_976789_1462139","Shop All Lunch","Food/Deli/Shop All Lunch" -"976759_976789_7852529","Soup and Sandwiches","Food/Deli/Soup and Sandwiches" -"976759_9801466","Delicious December","Food/Delicious December" -"976759_5004481","Dietary & Lifestyle Shop","Food/Dietary & Lifestyle Shop" -"976759_5004481_2475799","Alcohol Alternatives","Food/Dietary & Lifestyle Shop/Alcohol Alternatives" -"976759_5004481_7941405","Dairy Free Foods","Food/Dietary & Lifestyle Shop/Dairy Free Foods" -"976759_5004481_6339646","Feel Good Foods","Food/Dietary & Lifestyle Shop/Feel Good Foods" -"976759_5004481_9118701","Feel Good Meals","Food/Dietary & Lifestyle Shop/Feel Good Meals" -"976759_5004481_2536743","Feel Good Sides","Food/Dietary & Lifestyle Shop/Feel Good Sides" -"976759_5004481_9769485","Feel Good Snacks","Food/Dietary & Lifestyle Shop/Feel Good Snacks" -"976759_5004481_7089431","Gluten Free Meals and more","Food/Dietary & Lifestyle Shop/Gluten Free Meals and more" -"976759_5004481_1457133","Grab & Go Deli","Food/Dietary & Lifestyle Shop/Grab & Go Deli" -"976759_5004481_9114854","Health Inspired Breakfast","Food/Dietary & Lifestyle Shop/Health Inspired Breakfast" -"976759_5004481_3908039","Health Inspired Gut Foods","Food/Dietary & Lifestyle Shop/Health Inspired Gut Foods" -"976759_5004481_8465428","Health Inspired Lunch","Food/Dietary & Lifestyle Shop/Health Inspired Lunch" -"976759_5004481_4551059","Health Inspired Smoothies & Shakes","Food/Dietary & Lifestyle Shop/Health Inspired Smoothies & Shakes" -"976759_5004481_7963094","Healthy Eating","Food/Dietary & Lifestyle Shop/Healthy Eating" -"976759_5004481_4803116","Healthy Lifestyle","Food/Dietary & Lifestyle Shop/Healthy Lifestyle" -"976759_5004481_9339140","High Fiber Foods","Food/Dietary & Lifestyle Shop/High Fiber Foods" -"976759_5004481_8602597","High Protein Foods","Food/Dietary & Lifestyle Shop/High Protein Foods" -"976759_5004481_9418254","Keto Diet","Food/Dietary & Lifestyle Shop/Keto Diet" -"976759_5004481_7715306","Kosher","Food/Dietary & Lifestyle Shop/Kosher" -"976759_5004481_6036545","Low Calorie Foods","Food/Dietary & Lifestyle Shop/Low Calorie Foods" -"976759_5004481_8377127","Low sodium bacon","Food/Dietary & Lifestyle Shop/Low sodium bacon" -"976759_5004481_4632743","Low sodium broth","Food/Dietary & Lifestyle Shop/Low sodium broth" -"976759_5004481_6733659","Low Sodium Foods","Food/Dietary & Lifestyle Shop/Low Sodium Foods" -"976759_5004481_6518517","Low sodium meat","Food/Dietary & Lifestyle Shop/Low sodium meat" -"976759_5004481_8237330","Low Sugar Desserts","Food/Dietary & Lifestyle Shop/Low Sugar Desserts" -"976759_5004481_9198948","Mediterranean Diet","Food/Dietary & Lifestyle Shop/Mediterranean Diet" -"976759_5004481_1858214","No Sugar Foods","Food/Dietary & Lifestyle Shop/No Sugar Foods" -"976759_5004481_5915711","Paleo Diet","Food/Dietary & Lifestyle Shop/Paleo Diet" -"976759_5004481_6794259","Plant Based","Food/Dietary & Lifestyle Shop/Plant Based" -"976759_5004481_2468673","Plant Based Foods","Food/Dietary & Lifestyle Shop/Plant Based Foods" -"976759_5004481_1478340","Reduced Sugar","Food/Dietary & Lifestyle Shop/Reduced Sugar" -"976759_5004481_6169922","Salads, Dressings, & Toppings","Food/Dietary & Lifestyle Shop/Salads, Dressings, & Toppings" -"976759_5004481_6205394","Sugar Free Juice","Food/Dietary & Lifestyle Shop/Sugar Free Juice" -"976759_5004481_3986436","Vegan","Food/Dietary & Lifestyle Shop/Vegan" -"976759_5004481_7552486","Vegetarian","Food/Dietary & Lifestyle Shop/Vegetarian" -"976759_5004481_3122144","Whole Grain","Food/Dietary & Lifestyle Shop/Whole Grain" -"976759_6880042","Events","Food/Events" -"976759_6880042_9410871","7Up Canada Dry","Food/Events/7Up Canada Dry" -"976759_6880042_6725633","Airly","Food/Events/Airly" -"976759_6880042_9204345","Back To Routine","Food/Events/Back To Routine" -"976759_6880042_1330992","Back To School Feeding Reading","Food/Events/Back To School Feeding Reading" -"976759_6880042_3323756","Beneful Mainline","Food/Events/Beneful Mainline" -"976759_6880042_7415681","Big Game Food","Food/Events/Big Game Food" -"976759_6880042_8872455","Blue Buffalo Best Friends","Food/Events/Blue Buffalo Best Friends" -"976759_6880042_7404510","Blue Buffalo True Solutions","Food/Events/Blue Buffalo True Solutions" -"976759_6880042_4543687","Bumble Bee Foods","Food/Events/Bumble Bee Foods" -"976759_6880042_1729581","Bushschilibeans","Food/Events/Bushschilibeans" -"976759_6880042_9277567","Campbells Holiday Hauls","Food/Events/Campbells Holiday Hauls" -"976759_6880042_2917313","Campbells Snacks","Food/Events/Campbells Snacks" -"976759_6880042_7318177","Celsius","Food/Events/Celsius" -"976759_6880042_3111498","CELSIUS MLS Experience","Food/Events/CELSIUS MLS Experience" -"976759_6880042_9661163","Chex Holiday","Food/Events/Chex Holiday" -"976759_6880042_9699826","Chobani Coffee Creamer","Food/Events/Chobani Coffee Creamer" -"976759_6880042_4941827","Chobani High Protein","Food/Events/Chobani High Protein" -"976759_6880042_1851122","Chobani Recipes","Food/Events/Chobani Recipes" -"976759_6880042_6990598","Chobani Snack Smarter","Food/Events/Chobani Snack Smarter" -"976759_6880042_3241495","Chobanixpost","Food/Events/Chobanixpost" -"976759_6880042_1823875","Coca Cola Add Magic To Mealtime","Food/Events/Coca Cola Add Magic To Mealtime" -"976759_6880042_5437748","Coca Cola Spiced","Food/Events/Coca Cola Spiced" -"976759_6880042_7609123","Coca Cola Star Wars","Food/Events/Coca Cola Star Wars" -"976759_6880042_9494944","Coca Cola Y3000","Food/Events/Coca Cola Y3000" -"976759_6880042_2487708","CocaCola Olympic","Food/Events/CocaCola Olympic" -"976759_6880042_3088391","Contadina","Food/Events/Contadina" -"976759_6880042_8473562","Danone Happy Family","Food/Events/Danone Happy Family" -"976759_6880042_7667925","Delicious Wishes","Food/Events/Delicious Wishes" -"976759_6880042_2119415","DrPepperFootball","Food/Events/DrPepperFootball" -"976759_6880042_6636956","Duracell F1","Food/Events/Duracell F1" -"976759_6880042_9974125","Eight O Clock Coffee","Food/Events/Eight O Clock Coffee" -"976759_6880042_9602046","Energizer AC Pro","Food/Events/Energizer AC Pro" -"976759_6880042_7006146","Everyday Victories","Food/Events/Everyday Victories" -"976759_6880042_2099337","Familia Favorites","Food/Events/Familia Favorites" -"976759_6880042_4040262","Favorite Chips and Dip","Food/Events/Favorite Chips and Dip" -"976759_6880042_3715736","Ferrero Keebler El Fudge","Food/Events/Ferrero Keebler El Fudge" -"976759_6880042_9693458","Ferrero Keebler Superman","Food/Events/Ferrero Keebler Superman" -"976759_6880042_6926574","Fight Hunger","Food/Events/Fight Hunger" -"976759_6880042_6237437","Folgers1850","Food/Events/Folgers1850" -"976759_6880042_5974379","Frito Lay Official U.S.A Snacks of the FIFA World Cup 2022","Food/Events/Frito Lay Official U.S.A Snacks of the FIFA World Cup 2022" -"976759_6880042_9989982","Frito Lay's Snacksgiving","Food/Events/Frito Lay's Snacksgiving" -"976759_6880042_3038456","Fruit Roll Up Sour","Food/Events/Fruit Roll Up Sour" -"976759_6880042_3002751","Game Day","Food/Events/Game Day" -"976759_6880042_3903426","Get Your Grill On","Food/Events/Get Your Grill On" -"976759_6880042_8588436","Get Your Grill On Event","Food/Events/Get Your Grill On Event" -"976759_6880042_7767993","Get Your Grill On Week 1","Food/Events/Get Your Grill On Week 1" -"976759_6880042_3961639","Get Your Grill On Week 2","Food/Events/Get Your Grill On Week 2" -"976759_6880042_4552824","Get Your Grill On Week 3","Food/Events/Get Your Grill On Week 3" -"976759_6880042_9880435","Get Your Grill On Week 4","Food/Events/Get Your Grill On Week 4" -"976759_6880042_5220366","Get Your Grill On Week 5","Food/Events/Get Your Grill On Week 5" -"976759_6880042_5308043","Go Go Squeez","Food/Events/Go Go Squeez" -"976759_6880042_2517313","Goldfish","Food/Events/Goldfish" -"976759_6880042_6740781","Grow for Good","Food/Events/Grow for Good" -"976759_6880042_8525559","Hershey She Is","Food/Events/Hershey She Is" -"976759_6880042_1708308","Hershey's Halloween","Food/Events/Hershey's Halloween" -"976759_6880042_5215568","Hershey's Holiday","Food/Events/Hershey's Holiday" -"976759_6880042_4483105","Hidden Valley Ranch Marketside Pizza","Food/Events/Hidden Valley Ranch Marketside Pizza" -"976759_6880042_8320848","Holder 15","Food/Events/Holder 15" -"976759_6880042_4016549","Holder 2","Food/Events/Holder 2" -"976759_6880042_1749292","holder16","Food/Events/holder16" -"976759_6880042_6205832","Horizon Organic","Food/Events/Horizon Organic" -"976759_6880042_7178399","Hormel Back To School","Food/Events/Hormel Back To School" -"976759_6880042_4366685","Hydration Favorites","Food/Events/Hydration Favorites" -"976759_6880042_6072092","IAMS Checkup Challenge","Food/Events/IAMS Checkup Challenge" -"976759_6880042_3206318","International Delight","Food/Events/International Delight" -"976759_6880042_6700708","Jif Peanut Butter","Food/Events/Jif Peanut Butter" -"976759_6880042_3102111","Knorr Recipes","Food/Events/Knorr Recipes" -"976759_6880042_9530267","Kraft Easter","Food/Events/Kraft Easter" -"976759_6880042_7444397","Kraft Fight Hunger","Food/Events/Kraft Fight Hunger" -"976759_6880042_5934720","Kraft Funexpected","Food/Events/Kraft Funexpected" -"976759_6880042_9444397","Kraft Heinz Big Game","Food/Events/Kraft Heinz Big Game" -"976759_6880042_4587077","Kraft Summer Grilling","Food/Events/Kraft Summer Grilling" -"976759_6880042_3351800","Kraftheinz School Years Eve","Food/Events/Kraftheinz School Years Eve" -"976759_6880042_2408417","Love Made Fresh","Food/Events/Love Made Fresh" -"976759_6880042_7349374","Love Made Fresh Eventhub","Food/Events/Love Made Fresh Eventhub" -"976759_6880042_5929903","M&M's Purple","Food/Events/M&M's Purple" -"976759_6880042_6300689","Make New Traditions","Food/Events/Make New Traditions" -"976759_6880042_7210574","Marketplace Food Gifting","Food/Events/Marketplace Food Gifting" -"976759_6880042_3806935","Mars MMS","Food/Events/Mars MMS" -"976759_6880042_3181917","Mars Wrigley Valentine","Food/Events/Mars Wrigley Valentine" -"976759_6880042_4850282","MLBTRIVIA","Food/Events/MLBTRIVIA" -"976759_6880042_4744327","Mondelez Halloween","Food/Events/Mondelez Halloween" -"976759_6880042_5635927","Mondelez Holiday","Food/Events/Mondelez Holiday" -"976759_6880042_4560262","Mr Beast Feastables","Food/Events/Mr Beast Feastables" -"976759_6880042_4059112","National Candy Month June","Food/Events/National Candy Month June" -"976759_6880042_2258783","National Chocolate Month","Food/Events/National Chocolate Month" -"976759_6880042_3925062","National Grilled Cheese Month","Food/Events/National Grilled Cheese Month" -"976759_6880042_1663805","National Salad Month","Food/Events/National Salad Month" -"976759_6880042_7134408","Natures own back to school","Food/Events/Natures own back to school" -"976759_6880042_3980033","Natures Own Bread","Food/Events/Natures Own Bread" -"976759_6880042_6085385","natureseats","Food/Events/natureseats" -"976759_6880042_5165547","Nescafe","Food/Events/Nescafe" -"976759_6880042_4636682","Nespresso Machines","Food/Events/Nespresso Machines" -"976759_6880042_2435207","Nestle Summer Sparklers","Food/Events/Nestle Summer Sparklers" -"976759_6880042_8616455","New at Walmart","Food/Events/New at Walmart" -"976759_6880042_3317724","New Year Hydration","Food/Events/New Year Hydration" -"976759_6880042_1281851","New Year Reset","Food/Events/New Year Reset" -"976759_6880042_5542617","New Year's Survival Guide","Food/Events/New Year's Survival Guide" -"976759_6880042_2932278","Noosa Yoghurt","Food/Events/Noosa Yoghurt" -"976759_6880042_9667645","Ocean Spray Holidays","Food/Events/Ocean Spray Holidays" -"976759_6880042_1877880","Oikos","Food/Events/Oikos" -"976759_6880042_5147147","Once Upon a Fright","Food/Events/Once Upon a Fright" -"976759_6880042_9676449","Oreo Super Mario","Food/Events/Oreo Super Mario" -"976759_6880042_6923840","OreoSpace","Food/Events/OreoSpace" -"976759_6880042_4475163","OREOStarWars","Food/Events/OREOStarWars" -"976759_6880042_3885987","Pepsi Grills Night Out","Food/Events/Pepsi Grills Night Out" -"976759_6880042_6845429","Pepsi Meal Time","Food/Events/Pepsi Meal Time" -"976759_6880042_6420285","Pepsi Super Bowl","Food/Events/Pepsi Super Bowl" -"976759_6880042_5346825","Pepsi Zero Sugar","Food/Events/Pepsi Zero Sugar" -"976759_6880042_6955012","Peter Pan","Food/Events/Peter Pan" -"976759_6880042_3597782","Pilk","Food/Events/Pilk" -"976759_6880042_4058016","Planters Holiday","Food/Events/Planters Holiday" -"976759_6880042_4302521","Power up with Duracell","Food/Events/Power up with Duracell" -"976759_6880042_5695859","Pride It Forward","Food/Events/Pride It Forward" -"976759_6880042_9404714","Prime","Food/Events/Prime" -"976759_6880042_5684833","Propel Run Clubs","Food/Events/Propel Run Clubs" -"976759_6880042_3141538","Pure Life Hydration","Food/Events/Pure Life Hydration" -"976759_6880042_5991742","Pure Life Water Gold","Food/Events/Pure Life Water Gold" -"976759_6880042_6361455","Back to School","Food/Events/Pure Life/Back to School" -"976759_6880042_2963178","Quakermaplebacon","Food/Events/Quakermaplebacon" -"976759_6880042_1304504","quakermaplebacon-test","Food/Events/quakermaplebacon-test" -"976759_6880042_5379924","Regional Spring Water","Food/Events/Regional Spring Water" -"976759_6880042_1994622","Russell Stover 100th Anniversary","Food/Events/Russell Stover 100th Anniversary" -"976759_6880042_6388392","Share a Coke","Food/Events/Share a Coke" -"976759_6880042_9478572","Skippy Natures Own Back to School","Food/Events/Skippy Natures Own Back to School" -"976759_6880042_9875154","Skittles Popd","Food/Events/Skittles Popd" -"976759_6880042_2917613","Smartwater Alkaline","Food/Events/Smartwater Alkaline" -"976759_6880042_7029977","Smuckers Uncrustables","Food/Events/Smuckers Uncrustables" -"976759_6880042_4890344","Soup Meats Beer","Food/Events/Soup Meats Beer" -"976759_6880042_3581184","Sour Patch Kids Lemonade","Food/Events/Sour Patch Kids Lemonade" -"976759_6880042_8010671","Sparkling Ice","Food/Events/Sparkling Ice" -"976759_6880042_2683930","Spooky Fleet","Food/Events/Spooky Fleet" -"976759_6880042_8400671","Spring Ready","Food/Events/Spring Ready" -"976759_6880042_3229781","Starbucks Seasonal Flavors","Food/Events/Starbucks Seasonal Flavors" -"976759_6880042_6176670","Starbucks Sunsera","Food/Events/Starbucks Sunsera" -"976759_6880042_4208837","Starbucks Vertuo Line","Food/Events/Starbucks Vertuo Line" -"976759_6880042_7105659","Steakumm","Food/Events/Steakumm" -"976759_6880042_3685989","Stuffed Puffs Summer Smores","Food/Events/Stuffed Puffs Summer Smores" -"976759_6880042_9850140","Summer’s Sweet Soundtrack","Food/Events/Summer’s Sweet Soundtrack" -"976759_6880042_7018437","Sunkist Summer","Food/Events/Sunkist Summer" -"976759_6880042_4274131","Sweet Dreams Cereal","Food/Events/Sweet Dreams Cereal" -"976759_6880042_6500917","Taste of the NFL","Food/Events/Taste of the NFL" -"976759_6880042_1966432","Tea Favorites","Food/Events/Tea Favorites" -"976759_6880042_3454308","Tidy Care","Food/Events/Tidy Care" -"976759_6880042_9808343","TJIF","Food/Events/TJIF" -"976759_6880042_2872016","Tyson back to school","Food/Events/Tyson back to school" -"976759_6880042_2231545","Tyson Holiday","Food/Events/Tyson Holiday" -"976759_6880042_5140493","Tyson Tailgating","Food/Events/Tyson Tailgating" -"976759_6880042_5978999","ugly sundaes","Food/Events/ugly sundaes" -"976759_6880042_4409958","Werthers Merci Toffifay Mamba","Food/Events/Werthers Merci Toffifay Mamba" -"976759_6880042_9935922","Whisps","Food/Events/Whisps" -"976759_6880042_1659731","Your cup made simple","Food/Events/Your cup made simple" -"976759_1760124","Express Bundles","Food/Express Bundles" -"976759_1760124_4797708","Knorr Taste Combos","Food/Express Bundles/Knorr Taste Combos" -"976759_5098771","Flower Shop","Food/Flower Shop" -"976759_5098771_6211901","All Flowers","Food/Flower Shop/All Flowers" -"976759_1089004","Food Gifts","Food/Food Gifts" -"976759_1089004_4687507","Alcohol Gift Sets","Food/Food Gifts/Alcohol Gift Sets" -"976759_1089004_9419891","All Food Gifts","Food/Food Gifts/All Food Gifts" -"976759_1089004_2606831","Bakery Food Gifts","Food/Food Gifts/Bakery Food Gifts" -"976759_1089004_9826991","Baking Gift Sets","Food/Food Gifts/Baking Gift Sets" -"976759_1089004_8458568","Birthday food gifts","Food/Food Gifts/Birthday food gifts" -"976759_1089004_5102585","Cheryl's Cookies","Food/Food Gifts/Cheryl's Cookies" -"976759_1089004_3881850","Chocolates & Candies Food Gifts","Food/Food Gifts/Chocolates & Candies Food Gifts" -"976759_1089004_3332380","Christmas Cookies","Food/Food Gifts/Christmas Cookies" -"976759_1089004_3993317","Coffee, Cocoa, & Tea Gifts","Food/Food Gifts/Coffee, Cocoa, & Tea Gifts" -"976759_1089004_4904268","Congratulations food gifts","Food/Food Gifts/Congratulations food gifts" -"976759_1089004_4920203","Cookies Food Gifts","Food/Food Gifts/Cookies Food Gifts" -"976759_1089004_2441036","Dips & Sauces Food Gifts","Food/Food Gifts/Dips & Sauces Food Gifts" -"976759_1089004_9951830","Drink Gift Sets","Food/Food Gifts/Drink Gift Sets" -"976759_1089004_5514629","Easter Food Gifts","Food/Food Gifts/Easter Food Gifts" -"976759_1089004_6109961","Family fun gifts","Food/Food Gifts/Family fun gifts" -"976759_1089004_2424807","Flowers","Food/Food Gifts/Flowers" -"976759_1089004_5154746","Food gift baskets","Food/Food Gifts/Food gift baskets" -"976759_1089004_5921853","Fruit Food Gifts","Food/Food Gifts/Fruit Food Gifts" -"976759_1089004_8749065","Get well food gifts","Food/Food Gifts/Get well food gifts" -"976759_1089004_2320550","Giant candy","Food/Food Gifts/Giant candy" -"976759_1089004_4606677","Gift sets under $15","Food/Food Gifts/Gift sets under $15" -"976759_1089004_9926587","Graduation Food Gifts","Food/Food Gifts/Graduation Food Gifts" -"976759_1089004_9764589","Halloween food gifts","Food/Food Gifts/Halloween food gifts" -"976759_1089004_3182456","Harry & David","Food/Food Gifts/Harry & David" -"976759_1089004_4083698","Holiday food gift","Food/Food Gifts/Holiday food gift" -"976759_1089004_5093401","Hot Sauce Gift Sets","Food/Food Gifts/Hot Sauce Gift Sets" -"976759_1089004_8849619","Meat & Cheeses Food Gifts","Food/Food Gifts/Meat & Cheeses Food Gifts" -"976759_1089004_7604997","New years eve food gifts","Food/Food Gifts/New years eve food gifts" -"976759_1089004_6908381","Popcorn Tins","Food/Food Gifts/Popcorn Tins" -"976759_1089004_4290642","Premium gifts under $40","Food/Food Gifts/Premium gifts under $40" -"976759_1089004_8253593","Shop all 1800 Flowers Brands","Food/Food Gifts/Shop all 1800 Flowers Brands" -"976759_1089004_9064083","Shop by recipient","Food/Food Gifts/Shop by recipient" -"976759_1089004_7193935","Snacks Food Gifts","Food/Food Gifts/Snacks Food Gifts" -"976759_1089004_1838504","Thank you Food Gifts","Food/Food Gifts/Thank you Food Gifts" -"976759_1089004_2256714","Thanksgiving food gifts","Food/Food Gifts/Thanksgiving food gifts" -"976759_1089004_3039928","The Popcorn Factory","Food/Food Gifts/The Popcorn Factory" -"976759_1089004_3811104","The Swiss Colony","Food/Food Gifts/The Swiss Colony" -"976759_1089004_9676770","Valentine food gifts","Food/Food Gifts/Valentine food gifts" -"976759_1089004_6976753","White Elephant Food Gifts","Food/Food Gifts/White Elephant Food Gifts" -"976759_7206944","Food Immunity Boosters & Remedies","Food/Food Immunity Boosters & Remedies" -"976759_2191820","Food Workpage","Food/Food Workpage" -"976759_2709924","Fresh Prepared Foods","Food/Fresh Prepared Foods" -"976759_976793","Fresh Produce","Food/Fresh Produce" -"976759_976793_7495549","Apple Buying Guide","Food/Fresh Produce/Apple Buying Guide" -"976759_976793_2398120","Back to School Fruits & Vegetables","Food/Fresh Produce/Back to School Fruits & Vegetables" -"976759_976793_2377593","Chocolate dips for strawberries & fruit","Food/Fresh Produce/Chocolate dips for strawberries & fruit" -"976759_976793_8402496","Cut Fruits & Vegetables","Food/Fresh Produce/Cut Fruits & Vegetables" -"976759_976793_2766916","Easter Produce","Food/Fresh Produce/Easter Produce" -"976759_976793_4655469","Fall Produce","Food/Fresh Produce/Fall Produce" -"976759_976793_2741843","Feel Good Produce","Food/Fresh Produce/Feel Good Produce" -"976759_976793_9756351","Fresh Fruits","Food/Fresh Produce/Fresh Fruits" -"976759_976793_3513831","Fresh Herbs","Food/Fresh Produce/Fresh Herbs" -"976759_976793_3624746","Fresh rollbacks & more","Food/Fresh Produce/Fresh rollbacks & more" -"976759_976793_8910423","Fresh Vegetables","Food/Fresh Produce/Fresh Vegetables" -"976759_976793_7083253","Game Time Produce","Food/Fresh Produce/Game Time Produce" -"976759_976793_3527553","Indoor Grown Produce","Food/Fresh Produce/Indoor Grown Produce" -"976759_976793_7080049","Leafy Greens","Food/Fresh Produce/Leafy Greens" -"976759_976793_2741468","Lettuce","Food/Fresh Produce/Lettuce" -"976759_976793_1401552","Mother's Day & Graduation Fresh Flowers & Food","Food/Fresh Produce/Mother's Day & Graduation Fresh Flowers & Food" -"976759_976793_1913529","Organic Produce","Food/Fresh Produce/Organic Produce" -"976759_976793_9538337","Packaged Salads, Dressings & Dips","Food/Fresh Produce/Packaged Salads, Dressings & Dips" -"976759_976793_6919650","Plant Based Protein & Tofu","Food/Fresh Produce/Plant Based Protein & Tofu" -"976759_976793_5306523","Salad Shop","Food/Fresh Produce/Salad Shop" -"976759_976793_3379628","Salad Shop","Food/Fresh Produce/Salad Shop" -"976759_976793_5346472","Spring Produce","Food/Fresh Produce/Spring Produce" -"976759_976793_5988105","Summer Produce","Food/Fresh Produce/Summer Produce" -"976759_7128585","From Our Brands","Food/From Our Brands" -"976759_7128585_5705317","Back to school from our brands","Food/From Our Brands/Back to school from our brands" -"976759_7128585_8911633","bettergoods","Food/From Our Brands/bettergoods" -"976759_7128585_8451673","Beverages","Food/From Our Brands/Beverages" -"976759_7128585_3237507","Clear American Beverages","Food/From Our Brands/Clear American Beverages" -"976759_7128585_8107226","Everyday Favorites","Food/From Our Brands/Everyday Favorites" -"976759_7128585_7241748","For your goals","Food/From Our Brands/For your goals" -"976759_7128585_7700021","Freshness Guaranteed Dinner under $6","Food/From Our Brands/Freshness Guaranteed Dinner under $6" -"976759_7128585_7269273","Freshness Guaranteed Food","Food/From Our Brands/Freshness Guaranteed Food" -"976759_7128585_1453712","From our brands bakery","Food/From Our Brands/From our brands bakery" -"976759_7128585_7037616","From our brands baking","Food/From Our Brands/From our brands baking" -"976759_7128585_3780465","From our brands grilling","Food/From Our Brands/From our brands grilling" -"976759_7128585_9834661","Great Value Food","Food/From Our Brands/Great Value Food" -"976759_7128585_2270849","Manual Shelves","Food/From Our Brands/Manual Shelves" -"976759_7128585_8382924","Marketside","Food/From Our Brands/Marketside" -"976759_7128585_4993620","Marketside Food","Food/From Our Brands/Marketside Food" -"976759_7128585_3070377","New from our food brands","Food/From Our Brands/New from our food brands" -"976759_7128585_8268373","Pantry from our brands","Food/From Our Brands/Pantry from our brands" -"976759_7128585_4364629","Prima Della Food","Food/From Our Brands/Prima Della Food" -"976759_7128585_1478350","Private Label Beverages","Food/From Our Brands/Private Label Beverages" -"976759_7128585_2981759","Private Label Fresh Food","Food/From Our Brands/Private Label Fresh Food" -"976759_7128585_8934463","Private Label Frozen Food","Food/From Our Brands/Private Label Frozen Food" -"976759_7128585_3996130","Private Label Snacks","Food/From Our Brands/Private Label Snacks" -"976759_7128585_6898270","Sam's Choice Food","Food/From Our Brands/Sam's Choice Food" -"976759_7128585_3963101","Shop All Private Brands","Food/From Our Brands/Shop All Private Brands" -"976759_7128585_4483648","Tailgating from our brands","Food/From Our Brands/Tailgating from our brands" -"976759_7128585_5237296","Terrifying Treats","Food/From Our Brands/Terrifying Treats" -"976759_976791","Frozen Foods","Food/Frozen Foods" -"976759_976791_9554867","Asian Meals & Appetizers","Food/Frozen Foods/Asian Meals & Appetizers" -"976759_976791_1272219","Frozen Appetizers & Snacks","Food/Frozen Foods/Frozen Appetizers & Snacks" -"976759_976791_2211243","Frozen Beverages & Ice","Food/Frozen Foods/Frozen Beverages & Ice" -"976759_976791_5477020","Frozen Bread","Food/Frozen Foods/Frozen Bread" -"976759_976791_1001417","Frozen Breakfast Food","Food/Frozen Foods/Frozen Breakfast Food" -"976759_976791_9551235","Frozen Desserts","Food/Frozen Foods/Frozen Desserts" -"976759_976791_8386772","Frozen Dumplings","Food/Frozen Foods/Frozen Dumplings" -"976759_976791_5624760","Frozen Fruits & Vegetables","Food/Frozen Foods/Frozen Fruits & Vegetables" -"976759_976791_8450834","Frozen International Meals","Food/Frozen Foods/Frozen International Meals" -"976759_976791_3486654","Frozen Kids Meals","Food/Frozen Foods/Frozen Kids Meals" -"976759_976791_6259087","Frozen Meals","Food/Frozen Foods/Frozen Meals" -"976759_976791_5295075","Frozen Meat, Seafood, & Vegetarian","Food/Frozen Foods/Frozen Meat, Seafood, & Vegetarian" -"976759_976791_7460341","Frozen Pasta","Food/Frozen Foods/Frozen Pasta" -"976759_976791_2072073","Frozen Pizza","Food/Frozen Foods/Frozen Pizza" -"976759_976791_6170090","Frozen Potatoes","Food/Frozen Foods/Frozen Potatoes" -"976759_976791_4874456","Frozen Side Dishes","Food/Frozen Foods/Frozen Side Dishes" -"976759_976791_9621049","Holiday Meal & Entertaining Essentials","Food/Frozen Foods/Holiday Meal & Entertaining Essentials" -"976759_976791_1518625","Ice Cream & Novelties","Food/Frozen Foods/Ice Cream & Novelties" -"976759_976791_1439236","Ice Cream Shop","Food/Frozen Foods/Ice Cream Shop" -"976759_976791_9875366","Manual Shelves","Food/Frozen Foods/Manual Shelves" -"976759_976791_8717684","New in Frozen","Food/Frozen Foods/New in Frozen" -"976759_4039609","Game Time Food","Food/Game Time Food" -"976759_3701649","Grab & Go","Food/Grab & Go" -"976759_3701649_2386763","Beverages Singles","Food/Grab & Go/Beverages Singles" -"976759_3701649_9074834","Single serve candy","Food/Grab & Go/Single serve candy" -"976759_3701649_1389057","Single serve gum","Food/Grab & Go/Single serve gum" -"976759_3701649_8653665","Single serve snacks","Food/Grab & Go/Single serve snacks" -"976759_2191683","Hawaiian food & beverage","Food/Hawaiian food & beverage" -"976759_2191683_1477680","Frozen food","Food/Hawaiian food & beverage/Frozen food" -"976759_3063079","Healthy Food Benefits","Food/Healthy Food Benefits" -"976759_3063079_8391853","Healthy Benefits All Food","Food/Healthy Food Benefits/Healthy Benefits All Food" -"976759_3063079_1850880","OTC Network Healthy Foods","Food/Healthy Food Benefits/OTC Network Healthy Foods" -"976759_3063079_3930756","UHC Food Benefit","Food/Healthy Food Benefits/UHC Food Benefit" -"976759_3934653","Holiday baked goods","Food/Holiday baked goods" -"976759_7404240","International Food","Food/International Food" -"976759_7404240_1868664","AAPI Owned","Food/International Food/AAPI Owned" -"976759_7404240_1228102","Asian Foods","Food/International Food/Asian Foods" -"976759_7404240_7048976","Bachan's","Food/International Food/Bachan's" -"976759_7404240_8604552","Cajun","Food/International Food/Cajun" -"976759_7404240_2497056","European & Mediterranean","Food/International Food/European & Mediterranean" -"976759_7404240_8361972","Global Game Time Food","Food/International Food/Global Game Time Food" -"976759_7404240_8419886","Global Sauces & Spices","Food/International Food/Global Sauces & Spices" -"976759_7404240_9002484","Global Sweets & Candies","Food/International Food/Global Sweets & Candies" -"976759_7404240_2694309","Hispanic food","Food/International Food/Hispanic food" -"976759_7404240_3710367","Hispanic-Owned Food Brands","Food/International Food/Hispanic-Owned Food Brands" -"976759_7404240_7666074","Indian Foods","Food/International Food/Indian Foods" -"976759_7404240_2559136","International beverages","Food/International Food/International beverages" -"976759_7404240_1644664","International Picnic Favorites","Food/International Food/International Picnic Favorites" -"976759_7404240_5880939","International sauces","Food/International Food/International sauces" -"976759_7404240_1341357","International snacks & candy","Food/International Food/International snacks & candy" -"976759_7404240_6371570","Latin and Hispanic Foods","Food/International Food/Latin and Hispanic Foods" -"976759_7404240_9281017","Latin brands","Food/International Food/Latin brands" -"976759_7404240_4553385","Manual Shelves","Food/International Food/Manual Shelves" -"976759_7404240_5427632","Ocean's Halo","Food/International Food/Ocean's Halo" -"976759_7404240_3843722","Tamales","Food/International Food/Tamales" -"976759_5593013","LocalFinds Food","Food/LocalFinds Food" -"976759_5593013_9102530","LF Bakery","Food/LocalFinds Food/LF Bakery" -"976759_5593013_1745509","LocalFInds 99 Ranch Market","Food/LocalFinds Food/LocalFInds 99 Ranch Market" -"976759_5593013_8664922","LocalFinds Alcohol","Food/LocalFinds Food/LocalFinds Alcohol" -"976759_5593013_6558596","LocalFinds Bakery","Food/LocalFinds Food/LocalFinds Bakery" -"976759_5593013_4242268","LocalFinds International Food","Food/LocalFinds Food/LocalFinds International Food" -"976759_5593013_8412834","LocalFinds Specialty Food","Food/LocalFinds Food/LocalFinds Specialty Food" -"976759_8175310","Louisiana foods","Food/Louisiana foods" -"976759_7123943","Meal Delivery Services","Food/Meal Delivery Services" -"976759_7123943_1279068","All Meal Delivery Services","Food/Meal Delivery Services/All Meal Delivery Services" -"976759_7123943_6655570","Farm Crates","Food/Meal Delivery Services/Farm Crates" -"976759_9569500","Meat & Seafood","Food/Meat & Seafood" -"976759_9569500_3865404","$10 - $15","Food/Meat & Seafood/$10 - $15" -"976759_9569500_3827508","$15 - $20","Food/Meat & Seafood/$15 - $20" -"976759_9569500_2941132","Bacon, Hot Dogs, & Sausages","Food/Meat & Seafood/Bacon, Hot Dogs, & Sausages" -"976759_9569500_5889208","Baked Cod with Tomato & Basil","Food/Meat & Seafood/Baked Cod with Tomato & Basil" -"976759_9569500_1730435","Beef & Lamb","Food/Meat & Seafood/Beef & Lamb" -"976759_9569500_1001443","Chicken","Food/Meat & Seafood/Chicken" -"976759_9569500_7656626","Easter Main Meals","Food/Meat & Seafood/Easter Main Meals" -"976759_9569500_3315474","Easy Chili Meals","Food/Meat & Seafood/Easy Chili Meals" -"976759_9569500_4281241","Easy Taco Meals","Food/Meat & Seafood/Easy Taco Meals" -"976759_9569500_9764881","Easy Weeknight Dinners","Food/Meat & Seafood/Easy Weeknight Dinners" -"976759_9569500_2449072","EBT eligible items","Food/Meat & Seafood/EBT eligible items" -"976759_9569500_7038330","Fresh Seafood","Food/Meat & Seafood/Fresh Seafood" -"976759_9569500_4788736","Frozen Fish","Food/Meat & Seafood/Frozen Fish" -"976759_9569500_2105015","Get Your Grill On","Food/Meat & Seafood/Get Your Grill On" -"976759_9569500_3284566","Great Low Prices","Food/Meat & Seafood/Great Low Prices" -"976759_9569500_7599413","Grill it","Food/Meat & Seafood/Grill it" -"976759_9569500_7188777","Grilling Mains","Food/Meat & Seafood/Grilling Mains" -"976759_9569500_7144474","Health Inspired Meal Prep","Food/Meat & Seafood/Health Inspired Meal Prep" -"976759_9569500_7227878","Hearty Meat & Seafood","Food/Meat & Seafood/Hearty Meat & Seafood" -"976759_9569500_7651244","Hispanic Meat & Seafood","Food/Meat & Seafood/Hispanic Meat & Seafood" -"976759_9569500_9448605","Holiday Mains Buying Guide","Food/Meat & Seafood/Holiday Mains Buying Guide" -"976759_9569500_7630660","Lean Proteins","Food/Meat & Seafood/Lean Proteins" -"976759_9569500_2291850","Lobster and Crab","Food/Meat & Seafood/Lobster and Crab" -"976759_9569500_7460246","Lunar New Year Top Picks","Food/Meat & Seafood/Lunar New Year Top Picks" -"976759_9569500_2859606","Main Meals for Mom","Food/Meat & Seafood/Main Meals for Mom" -"976759_9569500_3501677","Mains for mom","Food/Meat & Seafood/Mains for mom" -"976759_9569500_4611327","Manual Shelves Meat & Seafood","Food/Meat & Seafood/Manual Shelves Meat & Seafood" -"976759_9569500_5667623","Meat & Seafood","Food/Meat & Seafood/Meat & Seafood" -"976759_9569500_6677247","Meat & Seafood Buying Guide","Food/Meat & Seafood/Meat & Seafood Buying Guide" -"976759_9569500_2907173","Meat and Seafood Ship to Home","Food/Meat & Seafood/Meat and Seafood Ship to Home" -"976759_9569500_7166541","New Year's Eve Apps","Food/Meat & Seafood/New Year's Eve Apps" -"976759_9569500_2603901","Oktoberfest Meat & Seafood","Food/Meat & Seafood/Oktoberfest Meat & Seafood" -"976759_9569500_5574987","Organic & Plant-Based","Food/Meat & Seafood/Organic & Plant-Based" -"976759_9569500_8154340","Plant Based Meat Options","Food/Meat & Seafood/Plant Based Meat Options" -"976759_9569500_1044143","Pork","Food/Meat & Seafood/Pork" -"976759_9569500_2543738","Pork Buying Guide","Food/Meat & Seafood/Pork Buying Guide" -"976759_9569500_6747100","Private Brand Grilling","Food/Meat & Seafood/Private Brand Grilling" -"976759_9569500_1001442","Seafood","Food/Meat & Seafood/Seafood" -"976759_9569500_1765164","Seafood Buying Guide","Food/Meat & Seafood/Seafood Buying Guide" -"976759_9569500_3410728","Seafood Meal Options","Food/Meat & Seafood/Seafood Meal Options" -"976759_9569500_4121939","Slow Cooking Meals","Food/Meat & Seafood/Slow Cooking Meals" -"976759_9569500_9352731","St. Patrick's Day Mains","Food/Meat & Seafood/St. Patrick's Day Mains" -"976759_9569500_7530377","Surf & Turf","Food/Meat & Seafood/Surf & Turf" -"976759_9569500_5826277","Sustainable Seafood","Food/Meat & Seafood/Sustainable Seafood" -"976759_9569500_7109525","Top Lent Picks","Food/Meat & Seafood/Top Lent Picks" -"976759_9569500_3765252","Turkey","Food/Meat & Seafood/Turkey" -"976759_9569500_8361663","Under $10","Food/Meat & Seafood/Under $10" -"976759_4143264","New Food Items","Food/New Food Items" -"976759_4143264_4136709","New in Beverages","Food/New Food Items/New in Beverages" -"976759_4143264_3215491","New in Snacks & Candy","Food/New Food Items/New in Snacks & Candy" -"976759_1228024","Organic Shop","Food/Organic Shop" -"976759_1228024_4995581","Organic Baby Food","Food/Organic Shop/Organic Baby Food" -"976759_1228024_3864282","Organic Beverages","Food/Organic Shop/Organic Beverages" -"976759_1228024_4985135","Organic Bread & Bakery","Food/Organic Shop/Organic Bread & Bakery" -"976759_1228024_3963019","Organic Dairy & Eggs","Food/Organic Shop/Organic Dairy & Eggs" -"976759_1228024_3042563","Organic Frozen","Food/Organic Shop/Organic Frozen" -"976759_1228024_1544474","Organic Fruits & Vegetables","Food/Organic Shop/Organic Fruits & Vegetables" -"976759_1228024_9713209","Organic Meat","Food/Organic Shop/Organic Meat" -"976759_1228024_2121347","Organic Pantry","Food/Organic Shop/Organic Pantry" -"976759_1228024_9181234","Organic Snacks","Food/Organic Shop/Organic Snacks" -"976759_1228024_9522882","Organic Supplements","Food/Organic Shop/Organic Supplements" -"976759_976794","Pantry","Food/Pantry" -"976759_1344638","Pantry, Snacks & Beverages","Food/Pantry, Snacks & Beverages" -"976759_976794_3398981","A Dozen Cousins","Food/Pantry/A Dozen Cousins" -"976759_976794_8370750","Back to routines","Food/Pantry/Back to routines" -"976759_976794_5437393","BBQ Sauces, Seasonings, and Marinades","Food/Pantry/BBQ Sauces, Seasonings, and Marinades" -"976759_976794_3758847","Beans and Rice","Food/Pantry/Beans and Rice" -"976759_976794_8259339","Beef stew","Food/Pantry/Beef stew" -"976759_976794_6289444","Blue Apron","Food/Pantry/Blue Apron" -"976759_976794_1358283","Buffalo chicken dip","Food/Pantry/Buffalo chicken dip" -"976759_976794_7062283","Bulk food","Food/Pantry/Bulk food" -"976759_976794_6094019","Burger and hot dog toppings","Food/Pantry/Burger and hot dog toppings" -"976759_976794_5856360","Cajun","Food/Pantry/Cajun" -"976759_976794_7433209","Canned goods","Food/Pantry/Canned goods" -"976759_976794_5245470","Chicken and tuna salads","Food/Pantry/Chicken and tuna salads" -"976759_976794_7975063","Chicken Parmesan","Food/Pantry/Chicken Parmesan" -"976759_976794_4718013","Chicken pot pie","Food/Pantry/Chicken pot pie" -"976759_976794_3751927","Cinco de Mayo Pantry","Food/Pantry/Cinco de Mayo Pantry" -"976759_976794_3534276","Classic soups","Food/Pantry/Classic soups" -"976759_976794_4525172","Comfort food","Food/Pantry/Comfort food" -"976759_976794_6025364","Conagra brands pantry","Food/Pantry/Conagra brands pantry" -"976759_976794_7981173","Condiments","Food/Pantry/Condiments" -"976759_976794_4649724","Cooking oils & vinegar","Food/Pantry/Cooking oils & vinegar" -"976759_976794_6172240","Date night picks","Food/Pantry/Date night picks" -"976759_976794_9242245","Dinner sides","Food/Pantry/Dinner sides" -"976759_976794_9474424","Dips","Food/Pantry/Dips" -"976759_976794_9565459","DIY Bowl Bases","Food/Pantry/DIY Bowl Bases" -"976759_976794_3229044","DIY Bowl Proteins & Produce","Food/Pantry/DIY Bowl Proteins & Produce" -"976759_976794_5871261","DIY Bowl Sauces and Spices","Food/Pantry/DIY Bowl Sauces and Spices" -"976759_976794_7262359","DIY Pizza","Food/Pantry/DIY Pizza" -"976759_976794_1001478","Dried Beans","Food/Pantry/Dried Beans" -"976759_976794_2816733","Enchiladas and Tamales","Food/Pantry/Enchiladas and Tamales" -"976759_976794_3736000","Feel Good Pasta","Food/Pantry/Feel Good Pasta" -"976759_976794_3881801","Food Essentials","Food/Pantry/Food Essentials" -"976759_976794_4207761","Fruit butters","Food/Pantry/Fruit butters" -"976759_976794_5687054","Game day dips","Food/Pantry/Game day dips" -"976759_976794_8041107","Game day wings & nachos","Food/Pantry/Game day wings & nachos" -"976759_976794_6311318","Gametime Pantry Essentials","Food/Pantry/Gametime Pantry Essentials" -"976759_976794_2002490","Great Value pantry deals","Food/Pantry/Great Value pantry deals" -"976759_976794_2802466","Grilling sides","Food/Pantry/Grilling sides" -"976759_976794_5877916","Gumbo and Jambalaya","Food/Pantry/Gumbo and Jambalaya" -"976759_976794_3029941","Herbs, spices & seasoning mixes","Food/Pantry/Herbs, spices & seasoning mixes" -"976759_976794_5459197","Honey","Food/Pantry/Honey" -"976759_976794_3781457","Hurricane food","Food/Pantry/Hurricane food" -"976759_976794_3854465","International foods","Food/Pantry/International foods" -"976759_976794_7295171","Jams, jellies & preserves","Food/Pantry/Jams, jellies & preserves" -"976759_976794_5354284","Jarred, Spreads, and Canned","Food/Pantry/Jarred, Spreads, and Canned" -"976759_976794_5737430","Kid approved dinners","Food/Pantry/Kid approved dinners" -"976759_976794_9050520","Kids heat and eat","Food/Pantry/Kids heat and eat" -"976759_976794_2074160","Lasagna","Food/Pantry/Lasagna" -"976759_976794_9657226","Mac & cheese mix-ins","Food/Pantry/Mac & cheese mix-ins" -"976759_976794_9113228","Manual Shelves","Food/Pantry/Manual Shelves" -"976759_976794_3981898","Meal starters","Food/Pantry/Meal starters" -"976759_976794_2945881","Microwave in minutes","Food/Pantry/Microwave in minutes" -"976759_976794_6601527","Mix-ins","Food/Pantry/Mix-ins" -"976759_976794_3438184","New in pantry","Food/Pantry/New in pantry" -"976759_976794_1834331","New soups","Food/Pantry/New soups" -"976759_976794_5614446","Packaged meals & Side dishes","Food/Pantry/Packaged meals & Side dishes" -"976759_976794_4252947","Pantry Basics","Food/Pantry/Pantry Basics" -"976759_976794_3338831","Pantry Brands","Food/Pantry/Pantry Brands" -"976759_976794_5176385","Pantry Essentials","Food/Pantry/Pantry Essentials" -"976759_976794_5728616","Pantry holiday stock up","Food/Pantry/Pantry holiday stock up" -"976759_976794_2807108","Pantry meal essentials","Food/Pantry/Pantry meal essentials" -"976759_976794_6130000","Pantry staples","Food/Pantry/Pantry staples" -"976759_976794_9527734","Pantry stock up","Food/Pantry/Pantry stock up" -"976759_976794_8772952","Party spreads","Food/Pantry/Party spreads" -"976759_976794_9940868","Pasta","Food/Pantry/Pasta" -"976759_976794_3246797","Pasta","Food/Pantry/Pasta" -"976759_976794_5403011","Pasta & pizza","Food/Pantry/Pasta & pizza" -"976759_976794_3642122","Peanut butter & spreads","Food/Pantry/Peanut butter & spreads" -"976759_976794_2441188","Pillsbury pizza night","Food/Pantry/Pillsbury pizza night" -"976759_976794_4308762","Pizza & burgers","Food/Pantry/Pizza & burgers" -"976759_976794_1262947","Pizzadilla","Food/Pantry/Pizzadilla" -"976759_976794_4918223","Precooked Pasta","Food/Pantry/Precooked Pasta" -"976759_976794_6576992","Proper Good Pasta","Food/Pantry/Proper Good Pasta" -"976759_976794_1001474","Rice","Food/Pantry/Rice" -"976759_976794_4879140","Rice, grains & dried beans","Food/Pantry/Rice, grains & dried beans" -"976759_976794_5580286","Salad dressings & toppings","Food/Pantry/Salad dressings & toppings" -"976759_976794_5883323","Salmon burgers","Food/Pantry/Salmon burgers" -"976759_976794_2223973","Sauces & marinades","Food/Pantry/Sauces & marinades" -"976759_976794_4283634","Sauces, mixes and more","Food/Pantry/Sauces, mixes and more" -"976759_976794_8010416","Seafood Boils","Food/Pantry/Seafood Boils" -"976759_976794_2469576","Siete Foods","Food/Pantry/Siete Foods" -"976759_976794_9125716","Simple Seafood","Food/Pantry/Simple Seafood" -"976759_976794_8248961","Soup","Food/Pantry/Soup" -"976759_976794_4851034","Soup & Chili","Food/Pantry/Soup & Chili" -"976759_976794_4284865","Souptober","Food/Pantry/Souptober" -"976759_976794_9451190","Spinach artichoke dip","Food/Pantry/Spinach artichoke dip" -"976759_976794_1522698","Stir fry rice and noodles","Food/Pantry/Stir fry rice and noodles" -"976759_976794_7882772","Taco's","Food/Pantry/Taco's" -"976759_976794_7250697","Tex Mex","Food/Pantry/Tex Mex" -"976759_976794_7654113","Tex Mex, Latin Seasonings and Sauces","Food/Pantry/Tex Mex, Latin Seasonings and Sauces" -"976759_976794_4610650","Thanksgiving Pantry Stock Up","Food/Pantry/Thanksgiving Pantry Stock Up" -"976759_976794_3068348","Tostadas","Food/Pantry/Tostadas" -"976759_976794_6413040","Tuna dishes","Food/Pantry/Tuna dishes" -"976759_976794_7534725","Wings & Nachos","Food/Pantry/Wings & Nachos" -"976759_8635300","Pasta Meals","Food/Pasta Meals" -"976759_1863726","Pringles & Cheez-It","Food/Pringles & Cheez-It" -"976759_3626943","S'mores","Food/S'mores" -"976759_1567409","Seasonal Grocery","Food/Seasonal Grocery" -"976759_1567409_7980092","Back to College Food","Food/Seasonal Grocery/Back to College Food" -"976759_1567409_8349567","Boil","Food/Seasonal Grocery/Boil" -"976759_1567409_2114017","Breakfast","Food/Seasonal Grocery/Breakfast" -"976759_1567409_2686773","Christmas Foods","Food/Seasonal Grocery/Christmas Foods" -"976759_1567409_8961483","Cinco de Mayo Food","Food/Seasonal Grocery/Cinco de Mayo Food" -"976759_1567409_3591080","College & Beyond Snacks & Beverages","Food/Seasonal Grocery/College & Beyond Snacks & Beverages" -"976759_1567409_3543723","Dinner","Food/Seasonal Grocery/Dinner" -"976759_1567409_6014885","Easter Foods","Food/Seasonal Grocery/Easter Foods" -"976759_1567409_6592518","Fall Foods","Food/Seasonal Grocery/Fall Foods" -"976759_1567409_5738183","Family movie night","Food/Seasonal Grocery/Family movie night" -"976759_1567409_2533414","Fathers Day Food","Food/Seasonal Grocery/Fathers Day Food" -"976759_1567409_6037937","Food Grilling Savings","Food/Seasonal Grocery/Food Grilling Savings" -"976759_1567409_2240892","Food Guides","Food/Seasonal Grocery/Food Guides" -"976759_1567409_3809225","Fresh Fall Pumpkins & Flowers","Food/Seasonal Grocery/Fresh Fall Pumpkins & Flowers" -"976759_1567409_3282877","Gametime Food","Food/Seasonal Grocery/Gametime Food" -"976759_1567409_8808777","Grilling Foods","Food/Seasonal Grocery/Grilling Foods" -"976759_1567409_2107086","Halloween Food","Food/Seasonal Grocery/Halloween Food" -"976759_1567409_6741138","Hanukkah Food","Food/Seasonal Grocery/Hanukkah Food" -"976759_1567409_3786819","Health Inspired Weeknight Dinner","Food/Seasonal Grocery/Health Inspired Weeknight Dinner" -"976759_1567409_5449716","Holiday Breakfast Essentials","Food/Seasonal Grocery/Holiday Breakfast Essentials" -"976759_1567409_6637513","Holiday Desserts","Food/Seasonal Grocery/Holiday Desserts" -"976759_1567409_7998220","Holiday Food","Food/Seasonal Grocery/Holiday Food" -"976759_1567409_4687377","Keto Desserts","Food/Seasonal Grocery/Keto Desserts" -"976759_1567409_7966149","Kids' Snacks & Lunches","Food/Seasonal Grocery/Kids' Snacks & Lunches" -"976759_1567409_3866200","Lunar New Year Foods","Food/Seasonal Grocery/Lunar New Year Foods" -"976759_1567409_4434825","Mardi Gras Party Food","Food/Seasonal Grocery/Mardi Gras Party Food" -"976759_1567409_7397770","Me & The Bees","Food/Seasonal Grocery/Me & The Bees" -"976759_1567409_5702155","Mother's Day Food","Food/Seasonal Grocery/Mother's Day Food" -"976759_1567409_6196875","National Heat Safety Coalition","Food/Seasonal Grocery/National Heat Safety Coalition" -"976759_1567409_4757643","Neilly's","Food/Seasonal Grocery/Neilly's" -"976759_1567409_3170630","New Year New You","Food/Seasonal Grocery/New Year New You" -"976759_1567409_9050139","New Year's Eve Food","Food/Seasonal Grocery/New Year's Eve Food" -"976759_1567409_2168938","Pantry Seasonal","Food/Seasonal Grocery/Pantry Seasonal" -"976759_1567409_3120670","Passover Foods","Food/Seasonal Grocery/Passover Foods" -"976759_1567409_4021557","Passover Seder","Food/Seasonal Grocery/Passover Seder" -"976759_1567409_6818540","Pepsico Back to School","Food/Seasonal Grocery/Pepsico Back to School" -"976759_1567409_8293700","Pi Day","Food/Seasonal Grocery/Pi Day" -"976759_1567409_7412754","Plant-Based Sides","Food/Seasonal Grocery/Plant-Based Sides" -"976759_1567409_7591100","PRIDE Party","Food/Seasonal Grocery/PRIDE Party" -"976759_1567409_9760211","Ramadan Food","Food/Seasonal Grocery/Ramadan Food" -"976759_1567409_9524483","Rosh Hashanah","Food/Seasonal Grocery/Rosh Hashanah" -"976759_1567409_7356816","S'mores","Food/Seasonal Grocery/S'mores" -"976759_1567409_6270627","Slow Cooker Essentials","Food/Seasonal Grocery/Slow Cooker Essentials" -"976759_1567409_5202257","Solar Eclipse Food","Food/Seasonal Grocery/Solar Eclipse Food" -"976759_1567409_1302668","Spring flavors food","Food/Seasonal Grocery/Spring flavors food" -"976759_1567409_5200930","Spring Flavors Food","Food/Seasonal Grocery/Spring Flavors Food" -"976759_1567409_9279251","Spring Grilling Foods","Food/Seasonal Grocery/Spring Grilling Foods" -"976759_1567409_7139732","St. Patrick's Day Food","Food/Seasonal Grocery/St. Patrick's Day Food" -"976759_1567409_3534625","Summer Flavors","Food/Seasonal Grocery/Summer Flavors" -"976759_1567409_6175095","Summer Grilling Food","Food/Seasonal Grocery/Summer Grilling Food" -"976759_1567409_6287342","Thanksgiving Foods","Food/Seasonal Grocery/Thanksgiving Foods" -"976759_1567409_6237396","Valentine's Day Food","Food/Seasonal Grocery/Valentine's Day Food" -"976759_1567409_8913642","Yom Kippur","Food/Seasonal Grocery/Yom Kippur" -"976759_1731677","Shop All Baking","Food/Shop All Baking" -"976759_1959090","Shop All Bread and Bakery","Food/Shop All Bread and Bakery" -"976759_9508401","Shop All Breakfast","Food/Shop All Breakfast" -"976759_5465032","Shop All Candy","Food/Shop All Candy" -"976759_9113477","Shop All Coffee","Food/Shop All Coffee" -"976759_7581029","Shop All Dairy","Food/Shop All Dairy" -"976759_5596430","Shop All Deli","Food/Shop All Deli" -"976759_1996374","Shop All Frozen","Food/Shop All Frozen" -"976759_3499685","Shop All Meat and Seafood","Food/Shop All Meat and Seafood" -"976759_2840383","Shop All Pantry","Food/Shop All Pantry" -"976759_8373901","Shop All Produce","Food/Shop All Produce" -"976759_5892971","Shop by Brand","Food/Shop by Brand" -"976759_5892971_6443712","Badia Spices","Food/Shop by Brand/Badia Spices" -"976759_5892971_9111954","F. Gaviña & Sons","Food/Shop by Brand/F. Gaviña & Sons" -"976759_7786229","Shop Kellogg's","Food/Shop Kellogg's" -"976759_7485418","Shop More Than Your Store","Food/Shop More Than Your Store" -"976759_8249800","Smart Suggestion","Food/Smart Suggestion" -"976759_3226996","Smart Suggestions OG Food","Food/Smart Suggestions OG Food" -"976759_2101546","Smoothies & Shakes","Food/Smoothies & Shakes" -"976759_976787","Snacks, Cookies & Chips","Food/Snacks, Cookies & Chips" -"976759_976787_9180367","@Manual Shelves Snacks","Food/Snacks, Cookies & Chips/@Manual Shelves Snacks" -"976759_976787_4480897","Be Happy Snacks","Food/Snacks, Cookies & Chips/Be Happy Snacks" -"976759_976787_1001405","Beef Jerky and Dried Meats","Food/Snacks, Cookies & Chips/Beef Jerky and Dried Meats" -"976759_976787_6167552","Bimbo","Food/Snacks, Cookies & Chips/Bimbo" -"976759_976787_7419236","Buffalo Flavored Snacks","Food/Snacks, Cookies & Chips/Buffalo Flavored Snacks" -"976759_976787_1001390","Chips","Food/Snacks, Cookies & Chips/Chips" -"976759_976787_1001391","Cookies","Food/Snacks, Cookies & Chips/Cookies" -"976759_976787_1001392","Crackers","Food/Snacks, Cookies & Chips/Crackers" -"976759_976787_1001393","Dips & Spreads","Food/Snacks, Cookies & Chips/Dips & Spreads" -"976759_976787_3585494","Doritos Roulette","Food/Snacks, Cookies & Chips/Doritos Roulette" -"976759_976787_4962467","Dried Fruits","Food/Snacks, Cookies & Chips/Dried Fruits" -"976759_976787_2058748","Frito Lay Variety Packs","Food/Snacks, Cookies & Chips/Frito Lay Variety Packs" -"976759_976787_1044133","Fruit Cups","Food/Snacks, Cookies & Chips/Fruit Cups" -"976759_976787_2667401","Fruit Pouches","Food/Snacks, Cookies & Chips/Fruit Pouches" -"976759_976787_1001395","Fruit Snacks","Food/Snacks, Cookies & Chips/Fruit Snacks" -"976759_976787_4507832","Great Value Applesauce","Food/Snacks, Cookies & Chips/Great Value Applesauce" -"976759_976787_9721503","Great Value Beef Jerky","Food/Snacks, Cookies & Chips/Great Value Beef Jerky" -"976759_976787_4336918","Great Value Fruit Cups","Food/Snacks, Cookies & Chips/Great Value Fruit Cups" -"976759_976787_2011342","Great Value Pudding","Food/Snacks, Cookies & Chips/Great Value Pudding" -"976759_976787_7023550","Great Value Snack Cakes","Food/Snacks, Cookies & Chips/Great Value Snack Cakes" -"976759_976787_7711625","Great Value Snacks","Food/Snacks, Cookies & Chips/Great Value Snacks" -"976759_976787_9773837","Great Value Trail Mix","Food/Snacks, Cookies & Chips/Great Value Trail Mix" -"976759_976787_9830353","Health-inspired Snacks","Food/Snacks, Cookies & Chips/Health-inspired Snacks" -"976759_976787_8970841","Little Bites","Food/Snacks, Cookies & Chips/Little Bites" -"976759_976787_1585364","Little Debbie Snacks","Food/Snacks, Cookies & Chips/Little Debbie Snacks" -"976759_976787_8698150","Marinela","Food/Snacks, Cookies & Chips/Marinela" -"976759_976787_1970158","Mexican Snack Cakes","Food/Snacks, Cookies & Chips/Mexican Snack Cakes" -"976759_976787_6543342","Multipack Snacks","Food/Snacks, Cookies & Chips/Multipack Snacks" -"976759_976787_9175273","Multipack Snacks Shipped to You","Food/Snacks, Cookies & Chips/Multipack Snacks Shipped to You" -"976759_976787_1001406","Nuts, Trail Mix & Seeds","Food/Snacks, Cookies & Chips/Nuts, Trail Mix & Seeds" -"976759_976787_1841284","Pepsi Super Bowl Place Holder","Food/Snacks, Cookies & Chips/Pepsi Super Bowl Place Holder" -"976759_976787_1400598","Pop-tarts","Food/Snacks, Cookies & Chips/Pop-tarts" -"976759_976787_1001407","Popcorn","Food/Snacks, Cookies & Chips/Popcorn" -"976759_976787_1044156","Pretzels","Food/Snacks, Cookies & Chips/Pretzels" -"976759_976787_9523882","Pudding & Gelatin","Food/Snacks, Cookies & Chips/Pudding & Gelatin" -"976759_976787_5433270","Puffed Snacks","Food/Snacks, Cookies & Chips/Puffed Snacks" -"976759_976787_5472713","Ruffles and LeBron James Family Foundation","Food/Snacks, Cookies & Chips/Ruffles and LeBron James Family Foundation" -"976759_976787_7672832","Salsa & Dips","Food/Snacks, Cookies & Chips/Salsa & Dips" -"976759_976787_1983636","Shop All Snacks","Food/Snacks, Cookies & Chips/Shop All Snacks" -"976759_976787_3225216","Snack & Sip Deals","Food/Snacks, Cookies & Chips/Snack & Sip Deals" -"976759_976787_1044154","Snack Bars","Food/Snacks, Cookies & Chips/Snack Bars" -"976759_976787_1001398","Snack Cakes","Food/Snacks, Cookies & Chips/Snack Cakes" -"976759_976787_2452884","Snack Mixes","Food/Snacks, Cookies & Chips/Snack Mixes" -"976759_976787_8191884","Snacks and Beverages $1","Food/Snacks, Cookies & Chips/Snacks and Beverages $1" -"976759_976787_3455839","Sugar-Free Snacks","Food/Snacks, Cookies & Chips/Sugar-Free Snacks" -"976759_976787_2353756","Summer Pepsi","Food/Snacks, Cookies & Chips/Summer Pepsi" -"976759_976787_7289509","Tastykake","Food/Snacks, Cookies & Chips/Tastykake" -"976759_976787_4929293","Trail Mix","Food/Snacks, Cookies & Chips/Trail Mix" -"976759_976787_6190886","Variety Pack Snacks","Food/Snacks, Cookies & Chips/Variety Pack Snacks" -"976759_976787_7951322","Wonder Snack Cakes","Food/Snacks, Cookies & Chips/Wonder Snack Cakes" -"976759_8040315","SNAP EBT Grocery Delivery and Pickup","Food/SNAP EBT Grocery Delivery and Pickup" -"976759_5148628","Specialty Shops","Food/Specialty Shops" -"976759_5148628_1228025","International Foods","Food/Specialty Shops/International Foods" -"976759_5148628_8049640","Party Food Shop","Food/Specialty Shops/Party Food Shop" -"976759_5148628_4569562","Premium Chocolate Shop","Food/Specialty Shops/Premium Chocolate Shop" -"976759_5148628_5831001","Premium Coffee Shop","Food/Specialty Shops/Premium Coffee Shop" -"976759_5148628_4583057","Wellness Foods","Food/Specialty Shops/Wellness Foods" -"976759_1511546","Super Bowl LII Starts Here","Food/Super Bowl LII Starts Here" -"976759_4321481","Testing Season","Food/Testing Season" -"976759_4321481_5471594","Fueling Beverages","Food/Testing Season/Fueling Beverages" -"976759_3544642","Texas foods","Food/Texas foods" -"976759_8757347","Thanksgiving food","Food/Thanksgiving food" -"976759_9630120","Trident Vibes with Sour Patch Kids","Food/Trident Vibes with Sour Patch Kids" -"976759_5283635","Value packs","Food/Value packs" -"976759_5283635_8482223","Baking","Food/Value packs/Baking" -"976759_5283635_7212622","Beverages","Food/Value packs/Beverages" -"976759_5283635_9409101","Canned & jarred","Food/Value packs/Canned & jarred" -"976759_5283635_5117494","Cereals & toaster pastries","Food/Value packs/Cereals & toaster pastries" -"976759_5283635_3000295","Coffee & tea multi-packs","Food/Value packs/Coffee & tea multi-packs" -"976759_5283635_4188257","Condiments & seasonings","Food/Value packs/Condiments & seasonings" -"976759_5283635_7855286","Packaged meals & sides","Food/Value packs/Packaged meals & sides" -"976759_5283635_6464562","Pantry","Food/Value packs/Pantry" -"976759_5283635_8130611","Snacks","Food/Value packs/Snacks" -"976759_5283635_9706814","Soups & broths","Food/Value packs/Soups & broths" -"976759_6298690","Weekly Trips","Food/Weekly Trips" -"976759_6298690_5681029","Dinner tonight","Food/Weekly Trips/Dinner tonight" -"976759_6298690_1735450","Groceries & Essentials","Food/Weekly Trips/Groceries & Essentials" -"976759_6298690_2366673","Recipe Ideas","Food/Weekly Trips/Recipe Ideas" -"976759_6298690_4186908","Weekly Ad","Food/Weekly Trips/Weekly Ad" diff --git a/WalmartPipeline/TaxonomyBrowsing/Note.txt b/WalmartPipeline/TaxonomyBrowsing/Note.txt deleted file mode 100644 index 1a3ea75..0000000 --- a/WalmartPipeline/TaxonomyBrowsing/Note.txt +++ /dev/null @@ -1,2 +0,0 @@ -Run generateTaxonomy.js to create a taxonomy.json file. -Then run browseTaxonomyExport.js and use that to output all subcategories under Food and Food_Grocery. \ No newline at end of file diff --git a/WalmartPipeline/TaxonomyBrowsing/browseTaxonomyExport.js b/WalmartPipeline/TaxonomyBrowsing/browseTaxonomyExport.js deleted file mode 100644 index 0928e98..0000000 --- a/WalmartPipeline/TaxonomyBrowsing/browseTaxonomyExport.js +++ /dev/null @@ -1,251 +0,0 @@ -import fs from 'fs'; -import readline from 'readline'; - -/* ---------- UX helpers ---------- */ -const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); - -/* ---------- Load taxonomy ---------- */ -function loadTaxonomy(filepath) { - const text = fs.readFileSync(filepath, 'utf8'); - let data = JSON.parse(text); - if (typeof data === 'string') data = JSON.parse(data); // double-encoded - return data; -} - -function escapeCSV(v) { - const s = String(v ?? ''); - return `"${s.replaceAll('"', '""')}"`; -} - -function sanitizeFilename(s) { - return ( - String(s ?? 'export') - .trim() - .replaceAll(/[^\w\-]+/g, '_') - .replaceAll(/_+/g, '_') - .replaceAll(/^_+|_+$/g, '') - .slice(0, 80) || 'export' - ); -} - -/* ---------- Tree utilities ---------- */ -function walkCollect(node, out) { - out.push({ - id: node.id ?? '', - name: node.name ?? '', - path: node.path ?? node.name ?? '', - }); - for (const child of node.children ?? []) walkCollect(child, out); -} - -function exportSubtreeToCSV(node, filename) { - const rows = []; - walkCollect(node, rows); - - rows.sort((a, b) => a.path.localeCompare(b.path)); - - const header = 'id,name,path\n'; - const body = rows - .map((r) => `${escapeCSV(r.id)},${escapeCSV(r.name)},${escapeCSV(r.path)}`) - .join('\n'); - - fs.writeFileSync(filename, header + body + '\n', 'utf8'); -} - -/* ---------- Index ---------- */ -function buildIndex(roots) { - const byId = new Map(); - const byName = new Map(); - - function index(node) { - byId.set(String(node.id), node); - const key = String(node.name ?? '').toLowerCase(); - if (key) { - if (!byName.has(key)) byName.set(key, []); - byName.get(key).push(node); - } - for (const c of node.children ?? []) index(c); - } - - roots.forEach(index); - return { byId, byName }; -} - -/* ---------- Printing ---------- */ -async function printTopLevel(roots) { - console.log('\nTop-level categories:'); - await sleep(400); - roots.forEach((r, i) => { - console.log(` ${i + 1}. ${r.name} [id=${r.id}]`); - }); -} - -async function printChildren(node) { - const children = node.children ?? []; - if (!children.length) { - console.log('(No children under this node.)'); - return; - } - console.log('\nChildren:'); - await sleep(400); - children.forEach((c, i) => { - console.log(` ${i + 1}. ${c.name} [id=${c.id}]`); - }); -} - -/* ---------- Main ---------- */ -async function main() { - console.log('Loading taxonomy...'); - await sleep(500); - - const taxonomy = loadTaxonomy('./taxonomy.json'); - const roots = Array.isArray(taxonomy) - ? taxonomy - : (taxonomy.categories ?? []); - - if (!roots.length) { - console.error('No root categories found.'); - process.exit(1); - } - - const { byId, byName } = buildIndex(roots); - - console.log(`Loaded ${roots.length} top-level categories.`); - await sleep(800); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - const ask = (q) => new Promise((res) => rl.question(q, res)); - - const virtualRoot = { id: '__ROOT__', name: 'ROOT', children: roots }; - const stack = [virtualRoot]; - - let exportsDone = 0; - const maxExports = 2; - - console.log('\nTaxonomy Browser + CSV Export'); - console.log('Navigate, export subtrees, or quit.'); - await sleep(800); - - while (true) { - const current = stack[stack.length - 1]; - - if (current === virtualRoot) { - await printTopLevel(roots); - } else { - console.log(`\nCurrent category: ${current.name} [id=${current.id}]`); - await sleep(300); - await printChildren(current); - } - - await sleep(300); - console.log('\nOptions:'); - console.log(' [s] Select / search category'); - console.log(' [e] Export this subtree to CSV'); - console.log(' [u] Go up one level'); - console.log(' [q] Quit'); - - const choice = (await ask('\nChoice: ')).trim().toLowerCase(); - - if (choice === 'q') { - console.log('\nExiting...'); - await sleep(500); - rl.close(); - return; - } - - if (choice === 'u') { - if (stack.length > 1) { - stack.pop(); - console.log('Moved up one level.'); - } else { - console.log('Already at top level.'); - } - await sleep(600); - continue; - } - - if (choice === 'e') { - if (exportsDone >= maxExports) { - console.log('Export limit reached.'); - await sleep(600); - continue; - } - - const base = sanitizeFilename( - current === virtualRoot ? 'top_level' : current.name, - ); - const file = `${base}_subtree.csv`; - - const exportNode = - current === virtualRoot - ? { name: 'Top Level', children: roots } - : current; - - console.log('Exporting subtree...'); - await sleep(500); - - exportSubtreeToCSV(exportNode, file); - exportsDone++; - - console.log(`Export complete → ${file}`); - console.log(`Exports used: ${exportsDone}/${maxExports}`); - await sleep(800); - continue; - } - - if (choice === 's') { - const children = current.children ?? []; - if (!children.length) { - console.log('No children to select here.'); - await sleep(600); - continue; - } - - const input = ( - await ask('Enter child NUMBER, or NAME/ID (direct child only): ') - ).trim(); - - const num = Number(input); - if (Number.isInteger(num) && num >= 1 && num <= children.length) { - const selected = children[num - 1]; - console.log(`Selected: ${selected.name}`); - await sleep(600); - stack.push(selected); - continue; - } - - const idMatch = children.find((c) => String(c.id) === input); - if (idMatch) { - console.log(`Selected: ${idMatch.name}`); - await sleep(600); - stack.push(idMatch); - continue; - } - - const nameMatch = children.find( - (c) => String(c.name).toLowerCase() === input.toLowerCase(), - ); - if (nameMatch) { - console.log(`Selected: ${nameMatch.name}`); - await sleep(600); - stack.push(nameMatch); - continue; - } - - console.log('No matching direct child found.'); - await sleep(700); - continue; - } - - console.log('Invalid option.'); - await sleep(600); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/WalmartPipeline/TaxonomyBrowsing/generateTaxonomy.js b/WalmartPipeline/TaxonomyBrowsing/generateTaxonomy.js deleted file mode 100644 index 3722cc0..0000000 --- a/WalmartPipeline/TaxonomyBrowsing/generateTaxonomy.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * generateTaxonomy.js - * - * Downloads the Walmart product taxonomy and saves it to JSON. - * Run this first, then use browseTaxonomyExport.js to pick which - * subtrees you want to export as CSVs. - * - * Needs WM_CONSUMER_ID, WM_KEY_VERSION, WM_PRIVATE_KEY in .env. - */ - -import dotenv from 'dotenv'; -dotenv.config(); -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); - -const BASE = 'https://developer.api.walmart.com'; -const TAXONOMY_PATH = '/api-proxy/service/affil/product/v2/taxonomy'; - -function loadCredentials() { - const consumerId = process.env.WM_CONSUMER_ID; - const keyVer = process.env.WM_KEY_VERSION; - const privateKeyPem = process.env.WM_PRIVATE_KEY; - - if (!consumerId || !keyVer || !privateKeyPem) { - throw new Error( - 'Missing Walmart credentials in .env: WM_CONSUMER_ID, WM_KEY_VERSION, WM_PRIVATE_KEY', - ); - } - - return { consumerId, keyVer, privateKeyPem }; -} - -// same signing logic as fetchProducts.js - Walmart needs this on every request -function buildAuthHeaders(creds) { - const id = String(creds.consumerId).trim(); - const kv = String(creds.keyVer).trim(); - const ts = String(Date.now()).trim(); - - const fields = { - 'WM_CONSUMER.ID': id, - 'WM_CONSUMER.INTIMESTAMP': ts, - 'WM_SEC.KEY_VERSION': kv, - }; - - const canonicalized = - Object.keys(fields) - .sort() - .map((k) => fields[k]) - .join('\n') + '\n'; - - const signature = crypto - .sign('RSA-SHA256', Buffer.from(canonicalized, 'utf8'), { - key: creds.privateKeyPem, - padding: crypto.constants.RSA_PKCS1_PADDING, - }) - .toString('base64'); - - return { - 'WM_CONSUMER.ID': id, - 'WM_CONSUMER.INTIMESTAMP': ts, - 'WM_SEC.TIMESTAMP': ts, - 'WM_SEC.KEY_VERSION': kv, - 'WM_SEC.AUTH_SIGNATURE': signature, - Accept: 'application/json', - }; -} - -// fetches the taxonomy and saves two files: the raw response and a pretty-printed version -export async function run(opts = {}) { - const outPretty = - opts.outPretty ?? path.resolve(process.cwd(), 'taxonomy.json'); - const outRaw = - opts.outRaw ?? path.resolve(process.cwd(), 'taxonomy_raw.json'); - - const creds = loadCredentials(); - - const url = new URL(BASE + TAXONOMY_PATH); - url.searchParams.set('format', 'json'); - - console.log('Requesting taxonomy:'); - console.log(url.toString(), '\n'); - - const headers = buildAuthHeaders(creds); - const res = await fetch(url.toString(), { headers }); - const text = await res.text(); - - // Always save raw response for debugging - fs.writeFileSync(outRaw, text, 'utf8'); - console.log(`Saved raw response to: ${outRaw}`); - - if (!res.ok) { - throw new Error(`HTTP ${res.status} ${res.statusText}\n${text}`); - } - - let data; - try { - data = JSON.parse(text); - } catch (e) { - throw new Error( - `Response was not valid JSON. Raw response saved to ${path.basename(outRaw)}.\nParse error: ${e.message}`, - ); - } - - fs.writeFileSync(outPretty, JSON.stringify(data, null, 4), 'utf8'); - console.log(`Saved taxonomy to: ${outPretty}`); - - return data; -} - -if (process.argv[1] === __filename) { - run().catch((e) => { - console.error('\nFailed to download taxonomy:', e.message); - process.exit(1); - }); -} diff --git a/WalmartPipeline/classify_ingredients.py b/WalmartPipeline/classify_ingredients.py deleted file mode 100644 index 43d8672..0000000 --- a/WalmartPipeline/classify_ingredients.py +++ /dev/null @@ -1,861 +0,0 @@ -#!/usr/bin/env python3 -""" -classify_ingredients.py - -Reads all the CSVs from walmart_CSVs/ and figures out which products are -cooking ingredients. Uses three passes: - 1. Keyword rules - catches ~65% of products instantly - 2. Category check - another ~25%, no LLM needed - 3. Ollama LLM - handles the remaining ~10% that are genuinely ambiguous - -Run with --no-llm to skip the LLM entirely (that's the default from runner.js). -If you do want LLM mode, make sure ollama is running first: - ollama pull llama3.2:1b - python classify_ingredients.py /path/to/csvs -m llama3.2:1b -w 8 - -The 15 category tags (PRODUCE, PROTEIN, DAIRY, etc.) are shared with -kroger_catalogue.js - any changes here should be reflected there too. -""" - -import csv -import json -import re -import time -import argparse -import urllib.request -import urllib.error -from pathlib import Path -from concurrent.futures import ThreadPoolExecutor, as_completed -from threading import Lock - -# --- Configuration --- -DEFAULT_MODEL = "llama3.2:1b" -DEFAULT_WORKERS = 8 -DEFAULT_BATCH = 25 -OLLAMA_BASE_URL = "http://localhost:11434" - -# --- keyword rules --- -# These two sets handle most of the work without touching the LLM. -# Non-ingredient check runs first so things like "frozen dinner" don't -# accidentally match "frozen vegetable" in the ingredient rules below. - -NON_INGREDIENT_KEYWORDS = { - # Prepared / frozen meals - "frozen meal", "frozen dinner", "frozen entree", "frozen pizza", - "tv dinner", "microwave meal", "microwave popcorn", "heat and serve", - "ready to eat", "ready-to-eat", "meal kit", "dinner kit", "lunch kit", - "skillet meal", "hamburger helper", "tuna helper", - "mac and cheese dinner", "boxed dinner", "gumbo mix", "protein bowl", - # Cocktail / bar mixers - "cocktail syrup", "margarita mix", "craft mixer", "cocktail mixer", - # Gift sets / care packages (multi-product bundles, not ingredients) - "gift set", "gift basket", "gift box", "gift tower", "care package", "snack care package", - # Drink mixes / powdered beverages - "drink mix", "powdered drink", "kool-aid", "crystal light", - # Pancake / table syrup (condiment, not a cooking ingredient) - "pancake syrup", "table syrup", "maple flavored syrup", - # Instant noodles / ramen cups (complete meals) - "instant noodle", "ramen noodle soup", "cup noodle", "cup of noodle", - # Cake / cupcake toppers (decorative plastic/acrylic, not food) - "cake topper", "cake toppers", "cupcake topper", "cupcake toppers", - # Snack foods - "potato chip", "tortilla chip", "corn chip", "pita chip", - "cheese puff", "cheese curl", "veggie straw", "veggie chip", - "rice cake", "popcorn", "pork rind", "pork skin", - "pretzel bag", "snack pretzel", "snack mix", - "snack bar", "granola bar", "cereal bar", "protein bar", "energy bar", - "fruit snack", "fruit roll", "fruit leather", "fruit gummy", - "gummy bear", "gummy worm", "gummy candy", - "candy bar", "chocolate bar", "candy bag", "hard candy", - "lollipop", "jawbreaker", "licorice", "taffy", "cotton candy", - # Beverages - "soda", "cola", "ginger ale", "root beer", "cream soda", - "energy drink", "sports drink", "electrolyte drink", - "fruit juice", "orange juice", "apple juice", "grape juice", - "cranberry juice", "pineapple juice", "tomato juice", - "lemonade", "limeade", "fruit punch", - "iced tea", "sweet tea", "diet tea", "bottled tea", "lemon tea", "leaf tea", "tea bag", - "yerba mate", "kombucha", - "coffee drink", "bottled coffee", "cold brew bottle", "frappuccino", - "smoothie bottle", "juice smoothie", "drinkable yogurt", "dairy drink", "yogurt drink", - "protein shake", "meal replacement shake", - "sparkling water", "mineral water", "bottled water", "distilled water", - "flavored water", "vitamin water", "coconut water bottle", - "beer", "wine bottle", "spirits bottle", "whiskey bottle", - "vodka bottle", "rum bottle", "gin bottle", "tequila bottle", - "hard seltzer", "hard cider", - "cold brew concentrate", - # Coffee pods / capsules / flavored coffees (not cooking ingredients) - "k-cup", "k cup", "coffee pod", "coffee capsule", "coffee single serve", "flavored coffee", - # General capsules (supplement capsules, espresso machine capsules, etc.) - "capsule", "softgel", "caplet", - # Candles (birthday candles, numeral candles, decorative - not food) - "candle", - # Clothing / merchandise - "hoodie", "sweatshirt", "t-shirt", "tote bag", "pullover shirt", - # Prepared soups (ready-to-eat, not cooking-ingredient condensed soups) - "chunky soup", "slow simmered soup", "chef boyardee", "chunky chili", - # Breakfast sandwiches and similar prepared foods (both forms since plural is irregular) - "breakfast sandwich", "breakfast sandwiches", "breakfast burrito", "biscuit sandwich", - # Snack jerky (not a cooking ingredient) - "jerky", - # Trail mix (snack, not an ingredient) - "trail mix", - # Pretzel snacks (pieces, bites, etc. — not a raw ingredient) - "pretzel", - # Flavored drink syrups (Torani, Monin etc. — for coffee drinks, not cooking) - "drink syrup", "coffee syrup", "flavored syrup", - # Snack packs / mini cookie packs - "snack pack", - # Pop-tarts and similar pastry snacks - "pop tarts", "pop-tart", - # Tea formats that slip past "tea bag" (pyramid sachets, loose leaf tins) - "pyramid sachet", "loose tea", "pyramid tea", - # Cake decoration companies / party supply decorations - "decopac", "wedding topper", "party topper", - # Non-food merchandise / signs - "banner", "yard sign", "signage", - # Candy / confections - "candy", "confection", "marshmallow", "breath mint", "chewing gum", "gum", "bubble gum", - # Breakfast cereals - "breakfast cereal", "corn flakes", "frosted flakes", "fruit loops", - "froot loops", "lucky charms", "cocoa puffs", "cap'n crunch", - "cheerios", "wheaties", "grape nuts", "honey smacks", - "rice krispies", "special k cereal", "raisin bran", - # Ice cream / frozen desserts - "ice cream", "gelato", "sorbet", "sherbet", "frozen yogurt", - "popsicle", "ice pop", "fudge bar", "ice cream bar", "ice cream sandwich", - "drumstick cone", "klondike", - # Pre-made bakery / desserts - "birthday cake", "wedding cake", "sheet cake", - "cupcake", "donut", "doughnut", "muffin pack", "danish pastry", - "croissant pack", "cinnamon roll pack", - "brownie pack", "cookie pack", "cookie assortment", - "pudding cup", "jello cup", "gelatin cup", "snack pudding", - # Supplements / vitamins (single-letter vitamin keywords removed - too broad, catch fortified foods) - "multivitamin", "supplement", "probiotic", "prebiotic", - "protein powder", "whey protein", "casein protein", - "plant protein powder", "creatine", "bcaa", "pre-workout", - "melatonin", "sleep aid", "fiber supplement", "metamucil", - "ensure bottle", "boost bottle", "pediasure", - # Baby / infant - "baby formula", "infant formula", "toddler formula", - "baby food", "baby puree", "baby cereal", "gerber", - # Pet food - "dog food", "cat food", "puppy food", "kitten food", - "dog treat", "cat treat", "bird seed", "fish food", - "pet food", "pet treat", - # Paper / cleaning / household - "paper towel", "paper plate", "napkin pack", - "plastic wrap", "aluminum foil", - "zip bag", "storage bag", "freezer bag", "sandwich bag", - "trash bag", "garbage bag", - "dish soap", "laundry detergent", "cleaning spray", "bleach", - # Kitchen equipment - "cutting board", "mixing bowl", "baking sheet", "baking pan", - "cast iron pan", "skillet pan", "saute pan", "sauce pan", - "dutch oven", "roasting pan", "muffin tin", "loaf pan", - "measuring cup", "measuring spoon", "spatula", "whisk", - "can opener", "vegetable peeler", -} - -NON_INGREDIENT_CATEGORIES = { - "beverage", "drinks", "soda", "juice", "water", - "snacks", "chips", "crackers", "popcorn", - "candy", "confectionery", "gum", - "ice cream", "frozen dessert", "frozen novelty", - "frozen meals", "frozen entrees", "prepared meals", - "breakfast cereal", "cereal", - "supplement", "vitamins", "health supplement", - "protein powder", "sports nutrition", - "baby food", "baby formula", "infant", - "pet food", "dog", "cat", "pet", - "paper goods", "cleaning supplies", "household", - "cookware", "bakeware", "kitchen tools", - "personal care", "beauty", "cosmetic", - "deli prepared", "deli meals", -} - -# Checked BEFORE INGREDIENT_RULES to fix tag conflicts caused by rule ordering. -# E.g. "blueberry yogurt" hits PRODUCE before DAIRY without this, "coffee with cardamom" -# hits SPICE, packaged "pie filling" hits PRODUCE. -PRIORITY_INGREDIENT_CHECKS: list[tuple[set[str], list[str]]] = [ - ({"yogurt", "kefir"}, ["DAIRY"]), - ({"pie filling"}, ["OTHER_INGR"]), - ({ - "ground coffee", "coffee bean", "coffee grounds", "roasted coffee", - "instant coffee", "espresso powder", "espresso bean", "coffee powder", - }, ["OTHER_INGR"]), - # Nuts/seeds with flavor modifiers (e.g. "sea salt") hit SPICE before NUT_SEED - ({ - "cashews", "almonds", "peanuts", "walnuts", "pecans", "pistachios", - "hazelnuts", "macadamia", "sunflower seeds", "pumpkin seeds", "pepitas", - }, ["NUT_SEED"]), - # Ham products hit PRODUCE when "apple" appears in "applewood smoked ham" - ({"smoked ham", "spiral ham", "whole ham", "applewood smoked"}, ["PROTEIN"]), -] - -# Ingredient rules - format is (keyword set, [tags to assign]). -# Synced with the search terms in kroger_catalogue.js FOOD_CATEGORIES. -# Kroger terms come first (marked "canonical"), then Walmart-specific variants. -# Keywords are matched against lowercased product name + category_name. - -INGREDIENT_RULES: list[tuple[set[str], list[str]]] = [ - - # --- PRODUCE --- - ({ - # Kroger search terms (canonical) - "fresh vegetable", "fresh fruit", "organic vegetable", "organic fruit", - "broccoli", "cauliflower", "spinach", "kale", "romaine", "mixed greens", - "brussels sprout", "cabbage", "carrot", "celery", "cucumber", "zucchini", - "bell pepper", "jalapeño", "cherry tomato", "roma tomato", - "red onion", "yellow onion", "shallot", "scallion", "leek", - "garlic bulb", "portobello", "shiitake", "cremini mushroom", - "asparagus", "artichoke", "beet", "turnip", "parsnip", - "sweet potato", "russet potato", "red potato", "yukon gold", - "corn on cob", "green bean", "snap pea", "snow pea", "eggplant", - "fennel", "radish", "apple", "pear", "orange", "lemon", "lime", - "banana", "mango", "pineapple", "papaya", "kiwi", - "strawberry", "blueberry", "raspberry", "blackberry", - "watermelon", "cantaloupe", "peach", "nectarine", "plum", "cherry", - "avocado", "pomegranate", - # Walmart-specific variants - "frozen vegetable", "frozen fruit", - "arugula", "iceberg lettuce", "butter lettuce", "collard greens", - "bok choy", "napa cabbage", - "serrano pepper", "habanero", "poblano", "anaheim pepper", "banana pepper", - "grape tomato", - "white onion", "green onion", - "garlic clove", "garlic head", - "cremini", "button mushroom", "oyster mushroom", "enoki", "chanterelle", - "fennel bulb", "jicama", "okra", - "yam", "fingerling potato", "fresh corn", "corn on the cob", - "sugar snap", "eggplant", - "grapefruit", "honeydew", "apricot", "fresh fig", "passion fruit", - }, ["PRODUCE"]), - - # --- FRESH_HERB --- - ({ - # Kroger search terms (canonical) - "fresh basil", "fresh parsley", "fresh cilantro", "fresh thyme", - "fresh rosemary", "fresh mint", "fresh dill", "fresh chives", - "fresh tarragon", "fresh oregano", "fresh sage", - "fresh lemongrass", "fresh ginger root", - # Walmart-specific variants - "fresh turmeric root", "herb bunch", "herb packet", - }, ["FRESH_HERB"]), - - # --- PROTEIN --- - ({ - # Kroger search terms (canonical) - "chicken breast", "chicken thigh", "chicken wing", "chicken drumstick", - "ground chicken", "whole chicken", "ground turkey", "turkey breast", - "ground beef", "beef chuck", "beef brisket", "ribeye", "sirloin", - "flank steak", "skirt steak", "beef roast", "beef short rib", - "pork chop", "pork loin", "pork belly", "pork shoulder", - "pork tenderloin", "baby back rib", "spiral ham", "ham steak", - "lamb chop", "lamb leg", "ground lamb", - "salmon fillet", "tuna fillet", "tilapia", "cod fillet", "halibut", - "mahi mahi", "sea bass", "trout", "catfish", - "shrimp", "scallop", "lobster tail", "crab leg", "crab meat", - "clam", "mussel", "oyster", - "bacon", "pancetta", "prosciutto", "salami", "pepperoni", - "chorizo", "andouille", "bratwurst", "italian sausage", - "breakfast sausage", - "deli turkey", "deli ham", "deli roast beef", "deli chicken", - "lunch meat", - "extra firm tofu", "silken tofu", "tempeh", "seitan", "edamame", - "black bean", "pinto bean", "kidney bean", "chickpea", "lentil", - "split pea", "navy bean", "cannellini bean", - "large eggs", "cage free egg", "organic egg", "egg whites", - # Walmart-specific variants - "chicken tender", "chicken leg", "whole turkey", - "beef rib", "tenderloin", "beef stew meat", - "pork rib", "spare rib", "uncured ham", - "rack of lamb", - "salmon steak", "whole salmon", "tuna steak", - "snapper", "squid", "octopus", - "sausage link", "sausage patty", "deli meat", "sliced meat", - "tofu", "firm tofu", "textured vegetable protein", - "great northern bean", "fava bean", - "dozen eggs", "medium eggs", "free range egg", "liquid egg", - }, ["PROTEIN"]), - - # --- DAIRY --- - ({ - # Kroger search terms (canonical) - "whole milk", "skim milk", "2% milk", "lactose free milk", "organic milk", - "buttermilk", "evaporated milk", "condensed milk", "powdered milk", - "heavy cream", "heavy whipping cream", "half and half", "light cream", - "sour cream", "creme fraiche", - "cream cheese", "mascarpone", "ricotta", "cottage cheese", - "fresh mozzarella", "burrata", - "cheddar cheese", "parmesan", "romano cheese", "asiago", - "gruyere", "swiss cheese", "gouda", "havarti", "fontina", "provolone", - "brie", "camembert", "gorgonzola", "blue cheese", - "feta cheese", "queso fresco", "monterey jack", "pepper jack", - "unsalted butter", "salted butter", "european butter", "ghee", - "greek yogurt", "plain yogurt", "whole milk yogurt", "skyr", "kefir", - # Walmart-specific variants - "1% milk", "nonfat milk", "reduced fat milk", - "whipping cream", "dry milk", - "neufchatel", "farmers cheese", - "cheddar block", "shredded cheddar", - "parmigiano", "emmental", "edam", - "roquefort", "stilton", "queso blanco", "colby", - "butter stick", "ghee jar", "clarified butter", - "nonfat yogurt", "whipped cream can", - }, ["DAIRY"]), - - # --- GRAIN --- - ({ - # Kroger search terms (canonical) - "all purpose flour", "bread flour", "whole wheat flour", - "cake flour", "almond flour", "coconut flour", "oat flour", "rye flour", - "chickpea flour", "rice flour", "cassava flour", - "white rice", "brown rice", "jasmine rice", "basmati rice", - "arborio rice", "wild rice", - "spaghetti", "penne", "rigatoni", "fusilli", "farfalle", - "linguine", "fettuccine", "angel hair", "orzo", "macaroni", - "lasagna noodle", "egg noodle", "ramen noodle", "soba noodle", - "udon noodle", "rice noodle", - "rolled oats", "quick oats", "steel cut oats", - "cornmeal", "polenta", "grits", "semolina", - "panko", "plain breadcrumb", - "sandwich bread", "whole wheat bread", "sourdough bread", - "french bread", "pita bread", "naan", "flatbread", - "flour tortilla", "corn tortilla", - "quinoa", "farro", "bulgur", "couscous", "barley", "millet", - # Walmart-specific variants - "pastry flour", "self rising flour", "spelt flour", "oat flour", - "instant rice", - "rotini", "tagliatelle", "vermicelli noodle", "glass noodle", - "rolled oat", "quick oat", "steel cut oat", "instant oat", - "breadcrumb", "italian breadcrumb", - "bread loaf", "white bread", "baguette", "ciabatta", - "amaranth", "teff", "freekeh", "crouton", "stuffing mix", - }, ["GRAIN"]), - - # --- BAKING --- - ({ - # Kroger search terms (canonical) - "baking soda", "baking powder", "cream of tartar", - "active dry yeast", "instant yeast", - "vanilla extract", "almond extract", "peppermint extract", - "cocoa powder", "dutch process cocoa", - "chocolate chips", "white chocolate chips", "baking chocolate", - "powdered sugar", "granulated sugar", "cane sugar", - "brown sugar", "turbinado sugar", "demerara sugar", - "corn syrup", "molasses", - "cake mix", "brownie mix", "pancake mix", "waffle mix", "muffin mix", - # Walmart-specific variants - "rapid rise yeast", - "lemon extract", "orange extract", - "food coloring", "gel food color", - "unsweetened cocoa", - "chocolate chip", "mini chocolate chip", "dark chocolate chip", - "white chocolate chip", "unsweetened chocolate", - "bittersweet chocolate", "semisweet chocolate", - "sprinkle", "nonpareil", "decorating sugar", "sanding sugar", - "cookie mix", "biscuit mix", - "confectioners sugar", "icing sugar", - "dark brown sugar", "light brown sugar", - "raw sugar", "light corn syrup", "dark corn syrup", - }, ["BAKING"]), - - # --- SPICE --- - ({ - # Kroger search terms (canonical) - "black pepper", "white pepper", "peppercorn", - "sea salt", "kosher salt", "himalayan salt", "garlic salt", - "garlic powder", "onion powder", - "cumin", "paprika", "smoked paprika", "chili powder", - "cayenne", "red pepper flake", - "cinnamon", "nutmeg", "oregano", "thyme", "rosemary", - "basil dried", "bay leaf", "turmeric", "coriander", - "fennel seed", "cardamom", "clove", "allspice", - "ground ginger", "mustard seed", "ground mustard", "fenugreek", - "sumac", "za'atar", "herbs de provence", "italian seasoning", - "cajun seasoning", "taco seasoning", - "curry powder", "garam masala", "ras el hanout", "five spice", - "lemon pepper", "steak seasoning", "bbq rub", - "vanilla bean", "saffron", "dill weed", "marjoram", - # Walmart-specific variants - "table salt", "fleur de sel", "smoked salt", "celery salt", - "ground cumin", "cumin seed", - "sweet paprika", "hot paprika", "ancho chili", "chipotle powder", - "crushed red pepper", - "ground cinnamon", "cinnamon stick", "ground nutmeg", - "dried oregano", "dried thyme", "dried rosemary", "dried basil", - "ground turmeric", "ground coriander", "caraway seed", - "nigella seed", - "old bay", "creole seasoning", "fajita seasoning", "ranch seasoning", - "everything bagel seasoning", - "dry rub", "annatto", "achiote", - "dried dill", "dried sage", - }, ["SPICE"]), - - # --- OIL_FAT --- - ({ - # Kroger search terms (canonical) - "olive oil", "extra virgin olive oil", - "vegetable oil", "canola oil", "sunflower oil", "safflower oil", - "corn oil", "soybean oil", "peanut oil", "grapeseed oil", - "avocado oil", "coconut oil", "sesame oil", "toasted sesame oil", - "walnut oil", "flaxseed oil", "truffle oil", - "cooking spray", "nonstick spray", - "shortening", "lard", "duck fat", "beef tallow", - "vegan butter", - # Walmart-specific variants - "palm oil", - "baking spray", - "vegetable shortening", "crisco", - "rendered lard", - "margarine stick", - }, ["OIL_FAT"]), - - # --- CONDIMENT --- - ({ - # Kroger search terms (canonical) - "soy sauce", "tamari", "liquid aminos", "coconut aminos", - "fish sauce", "oyster sauce", "hoisin sauce", "worcestershire sauce", - "hot sauce", "sriracha", "tabasco", "cholula", "sambal oelek", "gochujang", - "apple cider vinegar", "white vinegar", "red wine vinegar", - "white wine vinegar", "balsamic vinegar", "rice vinegar", "malt vinegar", - "dijon mustard", "whole grain mustard", "yellow mustard", - "ketchup", "mayonnaise", "relish", - "bbq sauce", "barbecue sauce", "steak sauce", "buffalo sauce", - "teriyaki sauce", "ponzu sauce", "sweet chili sauce", "stir fry sauce", - "tahini", "miso paste", - "tomato paste", "marinara sauce", "pasta sauce", "alfredo sauce", "pesto", - "enchilada sauce", "salsa verde", "salsa jar", - "pickle", "dill pickle", "pickled jalapeno", "giardiniera", - "capers", "sun dried tomato", "roasted red pepper", - "horseradish", "wasabi paste", - # Walmart-specific variants - "chili garlic sauce", - "distilled vinegar", "sherry vinegar", - "light mayonnaise", "sweet relish", "dill relish", - "wing sauce", "pad thai sauce", - "red miso", "white miso", - "pesto sauce", "mole sauce", "chunky salsa", - "bread and butter pickle", - "anchovy paste", "anchovy fillet", - "horseradish prepared", - }, ["CONDIMENT"]), - - # --- CANNED_GOOD --- - ({ - # Kroger search terms (canonical) - "canned tomato", "diced tomato", "crushed tomato", "whole peeled tomato", - "san marzano", "fire roasted tomato", - "canned black bean", "canned chickpea", "canned kidney bean", - "canned pinto bean", "canned navy bean", "canned cannellini", - "canned corn", "canned pumpkin", "canned artichoke", "canned mushroom", - "canned water chestnut", "canned green bean", - "coconut milk can", "coconut cream", - "chicken broth", "beef broth", "vegetable broth", - "chicken stock", "beef stock", "bone broth", - "canned tuna", "canned salmon", "canned sardine", "canned anchovy", - "canned crab", "canned clam", - "chipotle in adobo", "green chili can", - # Walmart-specific variants - "stewed tomato", - "canned bean", "canned lentil", "canned white bean", - "canned yam", "canned beet", "canned bamboo", - "canned pea", "canned spinach", - "coconut cream can", "lite coconut milk", - "rotel", - }, ["CANNED_GOOD"]), - - # --- NUT_SEED --- (before SWEETENER so "honey roasted cashews" tags as NUT_SEED not SWEETENER) - ({ - # Kroger search terms (canonical) - "raw almonds", "sliced almonds", "slivered almonds", - "walnut halves", "pecans", "cashews", "pistachios", - "pine nuts", "hazelnuts", "macadamia nut", "brazil nut", - "peanut butter", "almond butter", "cashew butter", - "sunflower seed", "pumpkin seed", "pepita", - "sesame seed", "chia seed", "flaxseed", "hemp seed", "poppy seed", - # Walmart-specific variants - "almonds", "roasted almonds", "almond meal", - "walnuts", "peanut", "raw peanut", "roasted peanut", - "ground flax", - }, ["NUT_SEED"]), - - # --- SWEETENER --- - ({ - # Kroger search terms (canonical) - "honey", "raw honey", "manuka honey", - "maple syrup", "pure maple syrup", - "agave nectar", "date syrup", - "stevia", "monk fruit sweetener", "erythritol", - # Walmart-specific variants - "clover honey", - "agave", - "date sugar", "brown rice syrup", - }, ["SWEETENER"]), - - # --- THICKENER --- - ({ - # Kroger search terms (canonical) - "cornstarch", "arrowroot powder", "tapioca starch", - "unflavored gelatin", "agar agar", "xanthan gum", "guar gum", "pectin", - # Walmart-specific variants - "corn starch", "arrowroot", - "tapioca pearl", "potato starch", - "agar powder", - }, ["THICKENER"]), - - # --- ALCOHOL (cooking only) --- - ({ - # Kroger search terms (canonical) - "cooking wine", "dry sherry", "mirin", "sake", "rice wine", "shaoxing wine", - # Walmart-specific variants - "sake cooking", - }, ["ALCOHOL"]), - - # --- OTHER_INGR (catch-all pantry) --- - ({ - # Kroger search terms (canonical) - "nutritional yeast", "dried mushroom", "nori sheet", "kombu", "wakame", - "dashi", "bonito flake", "matcha powder", - "rose water", "liquid smoke", - "raisins", "dried cranberry", "dried apricot", "dried fig", - "dried mango", "dried date", - "canned peach", "canned pear", "canned pineapple", - "lemon juice", "lime juice", - "jam", "jelly", "fruit preserves", "marmalade", "chutney", - "caramel sauce", "sweetened condensed milk", - "cream of mushroom soup", "cream of chicken soup", - "harissa", "red curry paste", "green curry paste", "yellow curry paste", - "coconut butter", "cacao nibs", "vital wheat gluten", "citric acid", - # Walmart-specific variants - "porcini dried", "seaweed", - "orange blossom water", - "raisin", "currant", "sultana", "dried cherry", "dried blueberry", - "dried tomato", "canned fruit", "maraschino cherry", - "lemon juice bottle", "lime juice bottle", - "fruit spread", - "caramel topping", "french onion soup can", - "curry paste", "massaman paste", - "cacao nib", "carob powder", "meat tenderizer", "vinegar", - }, ["OTHER_INGR"]), -] - -# Pass 2 - if the category name looks like a food/grocery category and the -# product didn't get flagged as a non-ingredient, assume it's an ingredient. -# This knocks out most of what's left without needing the LLM. -FOOD_CATEGORY_KEYWORDS = { - "food", "grocery", "groceries", "pantry", "cooking", "baking", - "ingredient", "produce", "meat", "seafood", "poultry", "dairy", - "deli", "bakery", "spice", "herb", "condiment", "sauce", "oil", - "flour", "grain", "pasta", "rice", "canned", "jarred", "dry good", - "bulk", "organic", "natural food", "gourmet", "specialty food", - "international food", "ethnic food", "asian food", "mexican food", - "italian food", "middle eastern", "latin food", - "frozen food", "refrigerated", "chilled", - "nut", "seed", "nut butter", "sweetener", "sugar", "honey", - "vinegar", "dressing", "marinade", "seasoning", "rub", "blend", - "broth", "stock", "soup base", - "cheese", "butter", "egg", "milk", "cream", "yogurt", - "bread", "tortilla", "wrap", - "breakfast", "cereal grain", # NOTE: "cereal grain" ≠ "cereal" (covered by non-ingredient) -} - - -def normalise(text: str) -> str: - return text.lower().strip() - - -def is_food_category(category: str) -> bool: - """Return True if the category string looks like a food/grocery category.""" - cat = normalise(category) - return any(kw in cat for kw in FOOD_CATEGORY_KEYWORDS) - - -def rule_classify(row: dict) -> dict | None: - name = normalise(row.get("name", "")) - category = normalise(row.get("category_name", "")) - combined = name + " " + category - # Strip punctuation so "cake topper," or "Coffee-" still match keyword boundaries - clean = re.sub(r'[^\w\s]', ' ', combined) - padded = f" {clean} " - - # Pass 1a: non-ingredient keywords - padded so brand names like "FirstChoiceCandy" - # don't trigger on the substring "candy". Also check plural form ({kw}s) so - # "tea bags", "energy bars", "cake toppers" etc. match alongside the singular. - for kw in NON_INGREDIENT_KEYWORDS: - if f" {kw} " in padded or f" {kw}s " in padded: - return {"ingredient": False, "classifiers": []} - for cat_kw in NON_INGREDIENT_CATEGORIES: - if cat_kw in category: - return {"ingredient": False, "classifiers": []} - - # Catch mint/breath-freshener candy products that slip through because a spice or - # produce keyword (e.g. "cinnamon", "strawberry") fires before the non-ingredient - # check can see that the product is a candy mint. - if any(k in name for k in ("breath mint", "sugar free mint", "sugar-free mint", - "mint tin", "mints bulk", "mint candy")): - return {"ingredient": False, "classifiers": []} - - # Priority ingredient checks - run before INGREDIENT_RULES to fix ordering conflicts - # (e.g. "blueberry yogurt" would match PRODUCE before DAIRY without this) - for keywords, tags in PRIORITY_INGREDIENT_CHECKS: - for kw in keywords: - if kw in combined: - return {"ingredient": True, "classifiers": tags} - - # Pass 1b: explicit ingredient keywords - for keywords, tags in INGREDIENT_RULES: - for kw in keywords: - if kw in combined: - return {"ingredient": True, "classifiers": tags} - - # Pass 2: food-category default - if is_food_category(category): - return {"ingredient": True, "classifiers": ["OTHER_INGR"]} - - return None # truly ambiguous -> LLM - - -def check_ollama_running(): - try: - req = urllib.request.urlopen(f"{OLLAMA_BASE_URL}/api/tags", timeout=5) - data = json.loads(req.read()) - return [m["name"].split(":")[0] for m in data.get("models", [])] - except (urllib.error.URLError, OSError): - return None - - -SYSTEM_PROMPT = """You are a food product classifier. For each product determine: -1. Is it an INGREDIENT a home cook would use in a recipe? -2. If yes, assign 1-3 tags: PROTEIN, DAIRY, PRODUCE, GRAIN, BAKING, SPICE, OIL_FAT, - CONDIMENT, CANNED_GOOD, SWEETENER, NUT_SEED, ALCOHOL, THICKENER, FRESH_HERB, OTHER_INGR - -Respond ONLY with a JSON array - one object per product in the same order. -Each: {"ingredient": true/false, "classifiers": ["TAG"]} -No explanation. No markdown. Only the JSON array.""" - - -def ollama_chat(model: str, system: str, user: str) -> str: - payload = json.dumps({ - "model": model, "stream": False, - "messages": [ - {"role": "system", "content": system}, - {"role": "user", "content": user}, - ], - "options": {"temperature": 0.0}, - }).encode() - req = urllib.request.Request( - f"{OLLAMA_BASE_URL}/api/chat", data=payload, - headers={"Content-Type": "application/json"}, method="POST", - ) - with urllib.request.urlopen(req, timeout=300) as resp: - return json.loads(resp.read())["message"]["content"].strip() - - -def parse_json_array(text: str, expected: int) -> list[dict]: - text = text.strip() - if "```" in text: - for part in text.split("```")[1:]: - part = part.lstrip("json").strip() - if part: - text = part - break - start, end = text.find("["), text.rfind("]") + 1 - if start != -1 and end > start: - try: - r = json.loads(text[start:end]) - if isinstance(r, list) and len(r) == expected: - return r - except json.JSONDecodeError: - pass - return [{"ingredient": False, "classifiers": []} for _ in range(expected)] - - -def build_prompt(row: dict) -> str: - """Build an LLM prompt line for a single product row.""" - parts = [f"Name: {row.get('name', '').strip()}"] - if brand := row.get("brandName", "").strip(): - parts.append(f"Brand: {brand}") - if size := row.get("size", "").strip(): - parts.append(f"Size: {size}") - if cat := row.get("category_name", "").strip(): - parts.append(f"Category: {cat}") - if sd := row.get("shortDescription", "").strip()[:200]: - parts.append(f"Desc: {sd}") - return " | ".join(parts) - - -def classify_batch_llm(model: str, batch: list[dict], retries: int = 2) -> list[dict]: - numbered = "\n".join(f"{i+1}. {build_prompt(r)}" for i, r in enumerate(batch)) - user_msg = ( - f"Classify these {len(batch)} food products. " - f"Return a JSON array with exactly {len(batch)} objects.\n\n" + numbered - ) - for attempt in range(retries + 1): - try: - raw = ollama_chat(model, SYSTEM_PROMPT, user_msg) - return parse_json_array(raw, len(batch)) - except Exception: - if attempt == retries: - return [{"ingredient": False, "classifiers": []} for _ in batch] - time.sleep(1) - - -# Output columns - matches what fetchProducts.js produces (we added size, brandName, upc -# and dropped the image/color fields we don't need). -OUTPUT_FIELDS = [ - "name", "brandName", "size", "upc", - "ingredient", "classifiers", - "retail_price", "thumbnailImage", -] - - -def read_csvs(folder: str) -> list[dict]: - csv.field_size_limit(10_000_000) - rows = [] - for csv_file in Path(folder).glob("*.csv"): - print(f" Reading {csv_file.name} ...") - with open(csv_file, newline="", encoding="utf-8-sig") as f: - for row in csv.DictReader(f): - rows.append(row) - print(f" Total rows: {len(rows):,}") - return rows - - -def write_output(classified: list[dict], output_path: str): - with open(output_path, "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=OUTPUT_FIELDS) - writer.writeheader() - writer.writerows(classified) - n_ingr = sum(1 for r in classified if r["ingredient"]) - print(f"\nSaved -> {output_path}") - print(f" {n_ingr:,} ingredients / {len(classified):,} total products") - - -def to_output_row(row: dict, result: dict) -> dict: - return { - "name": row.get("name", ""), - "brandName": row.get("brandName", ""), - "size": row.get("size", ""), - "upc": row.get("upc", ""), - "ingredient": result.get("ingredient", False), - "classifiers": "|".join(result.get("classifiers", [])), - "retail_price": row.get("retail_price", ""), - "thumbnailImage": row.get("thumbnailImage", ""), - } - - -def main(): - parser = argparse.ArgumentParser( - description="Classify Walmart products as cooking ingredients using keyword rules + optional LLM." - ) - parser.add_argument("input_folder") - parser.add_argument("-o", "--output", default="classified_ingredients.csv") - parser.add_argument("-m", "--model", default=DEFAULT_MODEL, - help=f"Ollama model (default: {DEFAULT_MODEL})") - parser.add_argument("-w", "--workers", type=int, default=DEFAULT_WORKERS, - help=f"Parallel workers (default: {DEFAULT_WORKERS})") - parser.add_argument("-b", "--batch", type=int, default=DEFAULT_BATCH, - help=f"Products per LLM call (default: {DEFAULT_BATCH})") - parser.add_argument("--no-llm", action="store_true", - help="Skip LLM pass - output rules + category-default only (fast)") - args = parser.parse_args() - - print(f"\nReading CSVs from: {args.input_folder}") - rows = read_csvs(args.input_folder) - if not rows: - print("No rows found.") - return - - # Pass 1 + 2: keyword rules + category fallback (no LLM needed) - print("\nPass 1+2: Rule-based + food-category default ...") - t0 = time.time() - - results = [None] * len(rows) - llm_queue = [] - - for i, row in enumerate(rows): - r = rule_classify(row) - if r is not None: - results[i] = r - else: - llm_queue.append((i, row)) - - rule_count = len(rows) - len(llm_queue) - rule_pct = 100 * rule_count / len(rows) - print(f" Rule-classified: {rule_count:,} ({rule_pct:.1f}%)") - print(f" Needs LLM: {len(llm_queue):,} ({100-rule_pct:.1f}%)") - print(f" Time: {time.time()-t0:.1f}s") - - if args.no_llm: - print(f"\n--no-llm: outputting rule-classified rows only.") - print(f" Skipping {len(llm_queue):,} unclassified products entirely.") - classified = [ - to_output_row(row, results[i]) - for i, row in enumerate(rows) - if results[i] is not None - ] - write_output(classified, args.output) - print(f" Total wall time: {(time.time()-t0)/60:.1f} min") - return - - # Pass 3: ask the LLM about anything the rules couldn't figure out - if llm_queue: - print(f"\nPass 3: LLM classification for {len(llm_queue):,} products ...") - print(f" Model: {args.model} | Workers: {args.workers} | Batch: {args.batch}") - print(f" Tip: make sure you ran ollama pull {args.model}\n") - - available = check_ollama_running() - if available is None: - print("Ollama not reachable - marking unknowns as non-ingredient.") - for orig_idx, _ in llm_queue: - results[orig_idx] = {"ingredient": False, "classifiers": []} - elif args.model.split(":")[0] not in available: - print(f"WARN Model '{args.model}' not found. Run: ollama pull {args.model}") - for orig_idx, _ in llm_queue: - results[orig_idx] = {"ingredient": False, "classifiers": []} - else: - only_rows = [r for _, r in llm_queue] - batches = [ - (llm_queue[i : i + args.batch], only_rows[i : i + args.batch]) - for i in range(0, len(llm_queue), args.batch) - ] - total_b = len(batches) - completed = 0 - lock = Lock() - t1 = time.time() - - def process(b_idx, idx_row_pairs, row_batch): - return b_idx, idx_row_pairs, classify_batch_llm(args.model, row_batch) - - with ThreadPoolExecutor(max_workers=args.workers) as pool: - futures = { - pool.submit(process, bi, idxs, rws): bi - for bi, (idxs, rws) in enumerate(batches) - } - for future in as_completed(futures): - b_idx, idx_row_pairs, llm_results = future.result() - for (orig_idx, _), res in zip(idx_row_pairs, llm_results): - results[orig_idx] = res - with lock: - completed += 1 - elapsed = time.time() - t1 - rate = completed / elapsed - eta = (total_b - completed) / rate if rate > 0 else 0 - print( - f" LLM batch {completed:>5}/{total_b} " - f"ETA: {eta/60:>5.1f} min", - end="\r", - ) - print() - - classified = [to_output_row(row, results[i]) for i, row in enumerate(rows)] - write_output(classified, args.output) - print(f" Total wall time: {(time.time()-t0)/60:.1f} min") - - -if __name__ == "__main__": - main() diff --git a/WalmartPipeline/deleteEmptyCSVs.js b/WalmartPipeline/deleteEmptyCSVs.js deleted file mode 100644 index bfea238..0000000 --- a/WalmartPipeline/deleteEmptyCSVs.js +++ /dev/null @@ -1,64 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -const TARGET_DIR = 'WalmartPipeline/walmart_CSVs'; - -/* -------------------- helpers -------------------- */ - -function isEmptyCSV(filePath) { - const stat = fs.statSync(filePath); - - // Truly empty file - if (stat.size === 0) { - return true; - } - - const content = fs.readFileSync(filePath, 'utf8').trim(); - if (!content) { - return true; - } - - // Header-only CSV (1 line) - const lines = content.split(/\r?\n/); - return lines.length <= 1; -} - -/* -------------------- main -------------------- */ - -function main() { - const dirPath = path.resolve(process.cwd(), TARGET_DIR); - - if (!fs.existsSync(dirPath)) { - console.error(`Folder not found: ${dirPath}`); - process.exit(1); - } - - const files = fs.readdirSync(dirPath); - - let deleted = 0; - let checked = 0; - - for (const file of files) { - if (!file.toLowerCase().endsWith('.csv')) { - continue; - } - - const filePath = path.join(dirPath, file); - checked++; - - try { - if (isEmptyCSV(filePath)) { - fs.unlinkSync(filePath); - deleted++; - console.log(`Deleted empty CSV: ${file}`); - } - } catch (e) { - console.warn(`Failed to process ${file}: ${e.message}`); - } - } - - console.log(`\nChecked ${checked} CSV files`); - console.log(`Deleted ${deleted} empty CSV files`); -} - -main(); diff --git a/WalmartPipeline/fetchProducts.js b/WalmartPipeline/fetchProducts.js deleted file mode 100644 index 8eddef1..0000000 --- a/WalmartPipeline/fetchProducts.js +++ /dev/null @@ -1,548 +0,0 @@ -/** - * fetchProducts.js - * - * Pulls Walmart food products by category and saves them to CSVs. - * You can import run() from another script or just run this file directly. - * - * Credentials go in .env: - * WM_CONSUMER_ID, WM_KEY_VERSION, WM_PRIVATE_KEY - * - * All the config options (page size, delays, dedup settings, etc.) have - * defaults so calling run() with no arguments should just work. - */ - -import dotenv from 'dotenv'; -dotenv.config(); -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); - -const BASE = 'https://developer.api.walmart.com'; -const PAGINATED_PATH = '/api-proxy/service/affil/product/v2/paginated/items'; -const MAX_ERROR_BODY_CHARS = 1200; - -// columns we actually care about for ingredient matching -// dropped a bunch of stuff (images, ratings, tracking URLs) that we don't need -// added size because it helps with dedup and matching later - -const COLUMNS = [ - 'item_id', - 'name', - 'brandName', - 'size', - 'retail_price', - 'upc', - 'subtree_name', - 'category_id', - 'category_name', - 'category_path', - 'shortDescription', - 'thumbnailImage', -]; - -function loadCredentials() { - const consumerId = process.env.WM_CONSUMER_ID; - const keyVer = process.env.WM_KEY_VERSION; - const privateKeyPem = process.env.WM_PRIVATE_KEY; - - if (!consumerId || !keyVer || !privateKeyPem) { - throw new Error( - 'Missing Walmart credentials in .env: WM_CONSUMER_ID, WM_KEY_VERSION, WM_PRIVATE_KEY', - ); - } - - return { consumerId, keyVer, privateKeyPem }; -} - -// Walmart requires a signed auth header on every request -function buildAuthHeaders(creds) { - const id = String(creds.consumerId).trim(); - const kv = String(creds.keyVer).trim(); - const ts = String(Date.now()).trim(); - - const fields = { - 'WM_CONSUMER.ID': id, - 'WM_CONSUMER.INTIMESTAMP': ts, - 'WM_SEC.KEY_VERSION': kv, - }; - - const canonicalized = - Object.keys(fields) - .sort() - .map((k) => fields[k]) - .join('\n') + '\n'; - - const signature = crypto - .sign('RSA-SHA256', Buffer.from(canonicalized, 'utf8'), { - key: creds.privateKeyPem, - padding: crypto.constants.RSA_PKCS1_PADDING, - }) - .toString('base64'); - - return { - 'WM_CONSUMER.ID': id, - 'WM_CONSUMER.INTIMESTAMP': ts, - 'WM_SEC.TIMESTAMP': ts, - 'WM_SEC.KEY_VERSION': kv, - 'WM_SEC.AUTH_SIGNATURE': signature, - Accept: 'application/json', - }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function escapeCSV(value) { - const s = String(value ?? ''); - return `"${s.replaceAll('"', '""')}"`; -} - -function sanitizeFilename(value) { - return ( - String(value ?? 'export') - .trim() - .replaceAll(/[^\w\-]+/g, '_') - .replaceAll(/_+/g, '_') - .replaceAll(/^_+|_+$/g, '') - .slice(0, 160) || 'export' - ); -} - -function formatISO(d) { - return d.toISOString(); -} - -function msToHMS(ms) { - const s = Math.floor(ms / 1000); - return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m ${s % 60}s`; -} - -function safeTruncate(s, n) { - const str = String(s ?? ''); - if (str.length <= n) return str; - return str.slice(0, n) + `... [truncated ${str.length - n} chars]`; -} - -// simple CSV parser - handles quoted fields and escaped quotes -function parseCSV(text) { - const rows = []; - let i = 0; - let field = ''; - let row = []; - let inQuotes = false; - - while (i < text.length) { - const c = text[i]; - - if (inQuotes) { - if (c === '"') { - if (text[i + 1] === '"') { - field += '"'; - i += 2; - continue; - } - inQuotes = false; - i++; - continue; - } - field += c; - i++; - continue; - } - - if (c === '"') { - inQuotes = true; - i++; - continue; - } - if (c === ',') { - row.push(field); - field = ''; - i++; - continue; - } - if (c === '\n') { - row.push(field); - field = ''; - rows.push(row); - row = []; - i++; - continue; - } - if (c === '\r') { - i++; - continue; - } - field += c; - i++; - } - - if (field.length > 0 || row.length > 0) { - row.push(field); - rows.push(row); - } - - return rows; -} - -function rowsToObjects(rows) { - if (!rows.length) return []; - const header = rows[0].map((h) => h.trim()); - return rows.slice(1).map((cols) => { - const obj = {}; - header.forEach((h, i) => { - obj[h] = cols[i] ?? ''; - }); - return obj; - }); -} - -function getDepth(pathValue) { - const s = String(pathValue ?? '').trim(); - if (!s) return Number.POSITIVE_INFINITY; - return s.split('/').length; -} - -function pickSubtreeRoot(objs) { - const candidates = objs.filter( - (o) => String(o.id ?? '').trim() && String(o.name ?? '').trim(), - ); - if (!candidates.length) return null; - - candidates.sort((a, b) => { - const da = getDepth(a.path); - const db = getDepth(b.path); - if (da !== db) return da - db; - return String(a.path ?? '').length - String(b.path ?? '').length; - }); - - return candidates[0]; -} - -function isParentRow(allRows, row) { - const p = String(row.path ?? '').trim(); - if (!p) return false; - const prefix = p.endsWith('/') ? p : p + '/'; - return allRows.some((o) => String(o.path ?? '').startsWith(prefix)); -} - -function itemToRow(item, extra) { - return { - item_id: item?.itemId ?? '', - name: item?.name ?? '', - brandName: item?.brandName ?? '', - size: item?.size ?? '', - retail_price: item?.salePrice ?? '', - upc: item?.upc ?? '', - subtree_name: extra.subtree_name, - category_id: extra.category_id, - category_name: extra.category_name, - category_path: extra.category_path, - shortDescription: item?.shortDescription ?? '', - thumbnailImage: item?.thumbnailImage ?? '', - }; -} - -function csvHeader() { - return COLUMNS.join(',') + '\n'; -} - -function rowToCSVLine(row) { - return COLUMNS.map((k) => escapeCSV(row[k])).join(',') + '\n'; -} - -async function fetchPage(url, creds, attempts, retryBaseDelayMs) { - for (let i = 0; i < attempts; i++) { - const headers = buildAuthHeaders(creds); - const res = await fetch(url, { headers }); - - if (res.ok) return await res.json(); - - const status = res.status; - if (status === 429 || (status >= 500 && status <= 599)) { - await sleep(retryBaseDelayMs * Math.pow(2, i)); - continue; - } - - const body = await res.text().catch(() => ''); - const err = new Error(`HTTP ${status} ${res.statusText}`); - Object.assign(err, { - httpStatus: status, - httpStatusText: res.statusText, - url, - responseBody: safeTruncate(body, MAX_ERROR_BODY_CHARS), - }); - throw err; - } - - const err = new Error(`Failed after ${attempts} attempts`); - err.url = url; - throw err; -} - -async function fetchAllItemsForCategory(categoryId, creds, cfg) { - let url = new URL(BASE + PAGINATED_PATH); - url.searchParams.set('category', categoryId); - url.searchParams.set('count', String(cfg.countPerPage)); - - const allItems = []; - - while (true) { - const data = await fetchPage( - url.toString(), - creds, - cfg.fetchAttempts, - cfg.retryBaseDelayMs, - ); - const items = Array.isArray(data?.items) ? data.items : []; - allItems.push(...items); - - if (!data?.nextPageExist || !data?.nextPage) break; - - url = new URL( - data.nextPage.startsWith('http') ? data.nextPage : BASE + data.nextPage, - ); - await sleep(cfg.requestDelayMs); - } - - return allItems; -} - -// write a JSON log file at the end so we can see what happened and debug failures -function writeRunLog(outPath, runState) { - const endedAt = runState.endedAt ?? new Date(); - const elapsed = endedAt.getTime() - runState.startedAt.getTime(); - const logName = `run_log_${sanitizeFilename(formatISO(runState.startedAt))}.json`; - const payload = { - startedAt: formatISO(runState.startedAt), - endedAt: formatISO(endedAt), - elapsed: msToHMS(elapsed), - exitReason: runState.exitReason, - counts: { - subtreeFilesProcessed: runState.subtreeFilesProcessed, - categoryRowsAttempted: runState.categoryRowsAttempted, - categoryRowsSucceeded: runState.categoryRowsSucceeded, - categoryRowsFailed: runState.categoryRowsFailed, - }, - failures: runState.failures, - }; - fs.writeFileSync( - path.join(outPath, logName), - JSON.stringify(payload, null, 2), - 'utf8', - ); - console.log(`Log written: ${logName}`); -} - -export async function run(config = {}) { - const cfg = { - categoriesDir: config.categoriesDir ?? 'WalmartPipeline/Categories', - outDir: config.outDir ?? 'WalmartPipeline/walmart_CSVs', - countPerPage: config.countPerPage ?? 500, - requestDelayMs: config.requestDelayMs ?? 25, - categoryDelayMs: config.categoryDelayMs ?? 25, - fetchAttempts: config.fetchAttempts ?? 5, - retryBaseDelayMs: config.retryBaseDelayMs ?? 50, - exportParentRows: config.exportParentRows ?? false, - dedupeWithinCategory: config.dedupeWithinCategory ?? true, - dedupeWithinSubtree: config.dedupeWithinSubtree ?? true, - dedupeMaster: config.dedupeMaster ?? false, - }; - - const creds = loadCredentials(); - - const categoriesPath = path.resolve(process.cwd(), cfg.categoriesDir); - const outPath = path.resolve(process.cwd(), cfg.outDir); - - if (!fs.existsSync(categoriesPath)) { - throw new Error(`Missing categories folder: ${categoriesPath}`); - } - if (!fs.existsSync(outPath)) { - fs.mkdirSync(outPath, { recursive: true }); - } - - const subtreeFiles = fs - .readdirSync(categoriesPath) - .filter((f) => f.toLowerCase().endsWith('.csv')); - - if (!subtreeFiles.length) { - throw new Error(`No .csv files found in ${categoriesPath}`); - } - - const runState = { - startedAt: new Date(), - endedAt: null, - exitReason: 'completed', - subtreeFilesProcessed: 0, - categoryRowsAttempted: 0, - categoryRowsSucceeded: 0, - categoryRowsFailed: 0, - failures: [], - }; - - const masterFile = path.join(outPath, 'ALL_SUBTREES_PRODUCTS.csv'); - const masterStream = fs.createWriteStream(masterFile, { encoding: 'utf8' }); - masterStream.write(csvHeader()); - - const seenMaster = cfg.dedupeMaster ? new Set() : null; - let masterCount = 0; - - try { - for (const file of subtreeFiles) { - const subtreeCsvPath = path.join(categoriesPath, file); - const text = fs.readFileSync(subtreeCsvPath, 'utf8'); - const parsed = parseCSV(text); - const objsRaw = rowsToObjects(parsed); - - if (!objsRaw.length) continue; - - const root = pickSubtreeRoot(objsRaw); - const subtree_name = root?.name ?? path.basename(file, '.csv'); - const subtree_id = root?.id ?? ''; - const subtreeLabel = sanitizeFilename( - `${subtree_name}_${subtree_id || 'unknown'}`, - ); - - const subtreeAggFile = path.join(outPath, `${subtreeLabel}__ALL.csv`); - const subtreeAggStream = fs.createWriteStream(subtreeAggFile, { - encoding: 'utf8', - }); - subtreeAggStream.write(csvHeader()); - - const seenSubtreeAgg = cfg.dedupeWithinSubtree ? new Set() : null; - let subtreeAggCount = 0; - - const rows = objsRaw - .map((o) => ({ - id: String(o.id ?? '').trim(), - name: String(o.name ?? '').trim(), - path: String(o.path ?? '').trim(), - })) - .filter((o) => o.id && o.name); - - for (const row of rows) { - if (!cfg.exportParentRows && isParentRow(rows, row)) continue; - - runState.categoryRowsAttempted++; - await sleep(cfg.categoryDelayMs); - - let items; - try { - items = await fetchAllItemsForCategory(row.id, creds, cfg); - runState.categoryRowsSucceeded++; - } catch (e) { - runState.categoryRowsFailed++; - runState.failures.push({ - type: 'categoryFetchFailed', - subtreeFile: file, - subtreeName: subtree_name, - categoryId: row.id, - categoryName: row.name, - categoryPath: row.path, - message: String(e?.message ?? e), - httpStatus: e?.httpStatus ?? null, - url: e?.url ?? null, - responseBody: e?.responseBody ?? null, - when: formatISO(new Date()), - }); - console.error( - `Failed category ${row.id} (${row.name}): ${e.message}`, - ); - continue; - } - - const categoryLabel = sanitizeFilename( - `${subtree_name}_${subtree_id}__${row.name}_${row.id}`, - ); - const categoryFile = path.join(outPath, `${categoryLabel}.csv`); - const categoryStream = fs.createWriteStream(categoryFile, { - encoding: 'utf8', - }); - categoryStream.write(csvHeader()); - - const seenCategory = cfg.dedupeWithinCategory ? new Set() : null; - let categoryCount = 0; - - for (const item of items) { - const itemId = String(item?.itemId ?? ''); - if (!itemId) continue; - - if (seenCategory) { - if (seenCategory.has(itemId)) continue; - seenCategory.add(itemId); - } - - const outRow = itemToRow(item, { - subtree_name, - category_id: row.id, - category_name: row.name, - category_path: row.path, - }); - - categoryStream.write(rowToCSVLine(outRow)); - categoryCount++; - - if (seenSubtreeAgg) { - if (!seenSubtreeAgg.has(itemId)) { - seenSubtreeAgg.add(itemId); - subtreeAggStream.write(rowToCSVLine(outRow)); - subtreeAggCount++; - } - } else { - subtreeAggStream.write(rowToCSVLine(outRow)); - subtreeAggCount++; - } - - if (seenMaster) { - if (!seenMaster.has(itemId)) { - seenMaster.add(itemId); - masterStream.write(rowToCSVLine(outRow)); - masterCount++; - } - } else { - masterStream.write(rowToCSVLine(outRow)); - masterCount++; - } - } - - await new Promise((resolve) => categoryStream.end(resolve)); - if (categoryCount > 0) { - console.log( - `Wrote ${path.basename(categoryFile)} (${categoryCount} rows)`, - ); - } - } - - await new Promise((resolve) => subtreeAggStream.end(resolve)); - runState.subtreeFilesProcessed++; - console.log( - `Wrote ${path.basename(subtreeAggFile)} (${subtreeAggCount} rows)`, - ); - } - } finally { - await new Promise((resolve) => masterStream.end(resolve)); - runState.endedAt = new Date(); - writeRunLog(outPath, runState); - } - - console.log(`Wrote ${path.basename(masterFile)} (${masterCount} rows)`); - - return { - subtreeFilesProcessed: runState.subtreeFilesProcessed, - categoryRowsAttempted: runState.categoryRowsAttempted, - categoryRowsSucceeded: runState.categoryRowsSucceeded, - categoryRowsFailed: runState.categoryRowsFailed, - }; -} - -// run directly if called as a script (not imported) -if (process.argv[1] === __filename) { - run().catch((err) => { - console.error(err.message ?? err); - process.exit(1); - }); -} diff --git a/WalmartPipeline/runningClassifyIngredients.md b/WalmartPipeline/runningClassifyIngredients.md deleted file mode 100644 index d8edd50..0000000 --- a/WalmartPipeline/runningClassifyIngredients.md +++ /dev/null @@ -1,102 +0,0 @@ -# Food Product Ingredient Classifier — Ollama Edition - -Classifies food products as cooking ingredients using a **local LLM via Ollama**. -No API key, no subscription, no data leaves your machine. - ---- - -## 1. Install Ollama - -Download from **https://ollama.com/download** (macOS, Windows, Linux). - -After installing, Ollama runs automatically in the background. -If it isn't running, start it with: - -```bash -ollama serve -``` - ---- - -## 2. Pull a model - -```bash -ollama pull llama3.2 # recommended — fast, accurate, ~2 GB -# alternatives: -ollama pull mistral -ollama pull gemma3 -``` - ---- - -## 3. Run the classifier - -```bash -python classify_ingredients.py /path/to/csv/folder -``` - -**Options:** - -| Flag | Default | Description | -| --------------- | ---------------------------- | ------------------------------- | -| `-o / --output` | `classified_ingredients.csv` | Output file path | -| `-m / --model` | `llama3.2` | Any model you've pulled locally | - -**Examples:** - -```bash -# Use default model -python classify_ingredients.py ./data - -# Use a different model, custom output file -python classify_ingredients.py ./data -m mistral -o results.csv -``` - ---- - -## Output columns - -| Column | Description | -| ---------------- | ------------------------------------------------ | -| `name` | Product name (from source CSV) | -| `ingredient` | `True` / `False` | -| `classifiers` | Pipe-separated tags, e.g. `PROTEIN\|CANNED_GOOD` | -| `retail_price` | From source CSV | -| `thumbnailImage` | From source CSV | -| `mediumImage` | From source CSV | -| `largeImage` | From source CSV | -| `color` | From source CSV | - ---- - -## Classifier tags - -| Tag | What it covers | -| ------------- | ----------------------------------------------- | -| `PROTEIN` | Meat, poultry, seafood, eggs, tofu, legumes | -| `DAIRY` | Milk, cheese, butter, cream, yogurt | -| `PRODUCE` | Fresh/frozen vegetables and fruits | -| `GRAIN` | Flour, rice, pasta, bread, oats | -| `BAKING` | Leavening, sugar, chocolate chips, extracts | -| `SPICE` | Dried spices, herbs, seasoning blends, salt | -| `OIL_FAT` | Oils, lard, shortening, ghee | -| `CONDIMENT` | Sauces, vinegars, mustard, ketchup, soy sauce | -| `CANNED_GOOD` | Canned/jarred veg, beans, broth, tomatoes | -| `SWEETENER` | Honey, maple syrup, agave, sugar (as sweetener) | -| `NUT_SEED` | Nuts, seeds, nut butters | -| `ALCOHOL` | Wine, beer, spirits used in cooking | -| `THICKENER` | Cornstarch, arrowroot, gelatin, agar | -| `FRESH_HERB` | Fresh basil, parsley, cilantro, etc. | -| `OTHER_INGR` | Genuine ingredient not fitting above | - -**Not classified as ingredients:** prepared meals, beverages, supplements, -kitchen equipment, or standalone snack foods. - ---- - -## Performance notes - -- The script processes one row at a time for reliability with local models. -- Speed depends on your hardware. On a modern laptop with `llama3.2`: - ~2–4 seconds per product on CPU, ~0.5–1s with a GPU. -- No internet connection required after the initial model pull. diff --git a/runner.js b/runner.js deleted file mode 100644 index 9d3a042..0000000 --- a/runner.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * runner.js - * - * Runs the Walmart and Kroger pipelines. You can run all stages, just one - * pipeline, or pick specific stages by name. - * - * To add a new stage later (like Supabase import), just append an object - * to the STAGES array - there's already a commented-out placeholder for that. - * - * Usage: - * node runner.js --all - * node runner.js --walmart - * node runner.js --kroger --zipcode=92507 - * node runner.js --all --dry-run - * node runner.js --help - */ - -import dotenv from 'dotenv'; -dotenv.config(); - -import { spawn } from 'child_process'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { run as walmartFetch } from './WalmartPipeline/fetchProducts.js'; -import { - importWalmart, - importKroger, - importKrogerStores, -} from './supabase_import.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const argv = process.argv.slice(2); - -function flag(name) { - return argv.includes(name); -} -function opt(prefix, def) { - return ( - (argv.find((a) => a.startsWith(prefix)) ?? '').replace(prefix, '') || def - ); -} - -const runAll = flag('--all'); -const runWalmart = flag('--walmart') || runAll; -const runKroger = flag('--kroger') || runAll; -const runSupabase = flag('--supabase') || runAll; -const runReclassify = flag('--reclassify'); // re-run classifier on existing walmart_CSVs without re-fetching -const runFindStores = flag('--find-stores'); // just run kroger:stores, skip everything else -const isDryRun = flag('--dry-run'); -const useLLM = flag('--llm'); -const continueErr = flag('--continue-on-error'); -const showHelp = flag('--help') || flag('-h') || argv.length === 0; - -const zipcode = opt('--zipcode=', ''); -const stores = opt('--stores=', '10'); -const radius = opt('--radius=', '25'); -const llmModel = opt('--model=', 'llama3.2:1b'); -const llmWorkers = opt('--workers=', '8'); -const stagesArg = opt('--stages=', ''); - -// runs a subprocess and waits for it to finish -// stdio: 'inherit' pipes the output straight to our terminal -function spawn_(cmd, args) { - return new Promise((resolve, reject) => { - const child = spawn(cmd, args, { - stdio: 'inherit', - env: process.env, - cwd: __dirname, - }); - child.on('close', (code) => { - if (code === 0 || code === null) resolve(); - else - reject( - new Error(`"${cmd} ${args.join(' ')}" exited with code ${code}`), - ); - }); - child.on('error', (err) => reject(new Error(`${cmd}: ${err.message}`))); - }); -} - -// Each stage has an id, label, group, and a run() that returns a Promise. -// To add a new stage later just append another object to this array. -const STAGES = [ - { - id: 'walmart:fetch', - label: 'Fetch Walmart products from category CSVs', - group: 'walmart', - run: async () => { - // calls into fetchProducts.js directly instead of spawning a subprocess - // pass a config object to run() if you want to change the output dir, etc. - const stats = await walmartFetch(); - console.log( - `\n Succeeded: ${stats.categoryRowsSucceeded} categories` + - ` | Failed: ${stats.categoryRowsFailed}` + - ` | Subtrees: ${stats.subtreeFilesProcessed}`, - ); - }, - }, - - { - id: 'walmart:classify', - label: 'Classify Walmart products as ingredients (Python)', - group: 'walmart', - // reads from walmart_CSVs/, writes to classified_ingredients.csv - // no LLM by default - add --llm to enable ollama, --model= to change the model - run: () => - spawn_('python3', [ - 'WalmartPipeline/classify_ingredients.py', - 'WalmartPipeline/walmart_CSVs', - '-o', - 'WalmartPipeline/classified_ingredients.csv', - ...(useLLM ? ['-m', llmModel, '-w', llmWorkers] : ['--no-llm']), - ]), - }, - - { - id: 'kroger:build', - label: 'Build Kroger food catalogue for stores near a zip code', - group: 'kroger', - // --zipcode is required - kroger_catalogue.js will error if it's missing - run: () => { - if (!zipcode) throw new Error('kroger:build requires --zipcode=XXXXX'); - return spawn_('node', [ - 'KrogerPipeline/kroger_catalogue.js', - `--zipcode=${zipcode}`, - `--stores=${stores}`, - `--radius=${radius}`, - ...(isDryRun ? ['--dry-run'] : []), - ]); - }, - }, - - { - id: 'kroger:stores', - label: 'Find nearest Kroger stores and save to Supabase', - group: 'kroger', - run: () => { - if (!zipcode) throw new Error('kroger:stores requires --zipcode=XXXXX'); - return importKrogerStores({ - zipcode, - stores: parseInt(stores, 10), - dryRun: isDryRun, - }); - }, - }, - - { - id: 'supabase:walmart', - label: 'Import Walmart ingredients into Supabase', - group: 'supabase', - run: () => importWalmart({ dryRun: isDryRun }), - }, - - { - id: 'supabase:kroger', - label: 'Import Kroger catalogue into Supabase', - group: 'supabase', - run: () => importKroger({ dryRun: isDryRun }), - }, -]; - -function selectStages() { - const validIds = new Set(STAGES.map((s) => s.id)); - - // --reclassify just re-runs the classifier on already-fetched CSVs - if (runReclassify) return STAGES.filter((s) => s.id === 'walmart:classify'); - - // --find-stores just runs the store search, no catalogue or ingredient importing - if (runFindStores) return STAGES.filter((s) => s.id === 'kroger:stores'); - - // --stages=... takes priority if given - if (stagesArg) { - const requested = stagesArg - .split(',') - .map((s) => s.trim()) - .filter(Boolean); - for (const id of requested) { - if (!validIds.has(id)) { - console.error( - `Unknown stage: "${id}". Valid stages: ${[...validIds].join(', ')}`, - ); - process.exit(1); - } - } - return STAGES.filter((s) => requested.includes(s.id)); - } - - return STAGES.filter((s) => { - if (s.group === 'walmart' && !runWalmart) return false; - if (s.group === 'kroger' && !runKroger) return false; - if (s.group === 'supabase' && !runSupabase) return false; - return true; - }); -} - -function printHelp() { - const stageList = STAGES.map((s) => ` ${s.id.padEnd(22)}${s.label}`).join( - '\n', - ); - console.log(` -Shopwise Pipeline Runner -===================================================== - -Usage: - node runner.js [flags] - -Flags: - --all Run all stages (kroger:enrich skipped unless --zipcode given) - --walmart Run walmart:fetch and walmart:classify - --reclassify Re-run walmart:classify on existing CSVs (skips the API fetch) - --find-stores Just find nearest stores and save to Supabase (requires --zipcode) - --kroger Run kroger:build (plus kroger:enrich if --zipcode is set) - --stages=a,b,c Pick specific stages by name, comma separated - --zipcode=XXXXX Zip code required for kroger:enrich - --stores=N Max stores for kroger:enrich (default: 10, or "all") - --radius=N Store search radius in miles (default: 25) - --dry-run Small batches, safe for testing - --llm Enable Ollama LLM in walmart:classify (default: rules-only) - --model=NAME Ollama model (default: llama3.2:1b) - --workers=N Parallel LLM workers (default: 8) - --supabase Run supabase:walmart and supabase:kroger - --continue-on-error Keep going after a stage fails instead of stopping - --help, -h Show this help text - -Stages (in order): -${stageList} - -Examples: - node runner.js --all - node runner.js --walmart - node runner.js --kroger --zipcode=90210 - node runner.js --all --dry-run - node runner.js --stages=walmart:fetch,kroger:build - node runner.js --walmart --llm --model=llama3.2:3b -`); -} - -async function main() { - if (showHelp) { - printHelp(); - return; - } - - const stages = selectStages(); - - if (!stages.length) { - console.error( - 'No stages selected. Use --all, --walmart, --kroger, --stages=..., or --help.', - ); - process.exit(1); - } - - const divider = '-'.repeat(60); - const divider2 = '='.repeat(60); - - console.log(`\nShopwise Pipeline Runner`); - console.log(divider2); - console.log(`Stages to run (${stages.length}):`); - stages.forEach((s, i) => console.log(` ${i + 1}. [${s.id}] ${s.label}`)); - if (isDryRun) console.log('\n DRY RUN - small batches only'); - - const results = []; - const t0 = Date.now(); - - for (const stage of stages) { - console.log(`\n${divider}`); - console.log(`[${stage.id}] ${stage.label}`); - console.log(divider); - - const ts = Date.now(); - try { - await stage.run(); - const elapsed = formatElapsed(ts); - console.log(`\n[${stage.id}] done in ${elapsed}`); - results.push({ id: stage.id, ok: true, elapsed }); - } catch (err) { - const elapsed = formatElapsed(ts); - console.error(`\n[${stage.id}] FAILED: ${err.message}`); - results.push({ id: stage.id, ok: false, elapsed, error: err.message }); - if (!continueErr) { - console.error( - `\nStopped. Run with --continue-on-error to keep going after failures.`, - ); - break; - } - } - } - - const total = formatElapsed(t0); - const failed = results.filter((r) => !r.ok); - - console.log(`\n${divider2}`); - console.log( - `Done in ${total} | ${results.length - failed.length}/${results.length} stages passed`, - ); - console.log(divider2); - results.forEach((r) => { - const status = r.ok ? '[ok] ' : '[FAIL]'; - const note = r.error ? ` - ${r.error}` : ''; - console.log(` ${status} [${r.id}] (${r.elapsed})${note}`); - }); - console.log(''); - - if (failed.length) process.exit(1); -} - -function formatElapsed(startTime) { - const ms = Date.now() - startTime; - return ms < 60_000 - ? `${(ms / 1000).toFixed(1)}s` - : `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; -} - -main().catch((err) => { - console.error('Fatal error:', err.message); - process.exit(1); -}); diff --git a/schema.sql b/schema.sql deleted file mode 100644 index 975e6e3..0000000 --- a/schema.sql +++ /dev/null @@ -1,110 +0,0 @@ --- Shopwise Supabase schema --- Run this once in the Supabase SQL editor before running the import stage. --- If you already created the tables via the dashboard, you can skip this. - --- ── Walmart ingredients ────────────────────────────────────────────────────── --- Only ingredient=True rows are imported. Re-runs require TRUNCATE first. - -CREATE TABLE IF NOT EXISTS walmart_ingredients ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, - name TEXT NOT NULL, - brand TEXT, - price NUMERIC NOT NULL, - classifiers TEXT[] NOT NULL, - image TEXT, - size TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS walmart_ingredients_classifiers_idx - ON walmart_ingredients USING GIN (classifiers); - - --- ── Kroger ingredients ─────────────────────────────────────────────────────── --- Full food catalogue. Re-runs require TRUNCATE first. --- store_id holds the semicolon-delimited store IDs as an array. --- price holds the first available price across stores. - -CREATE TABLE IF NOT EXISTS kroger_ingredients ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, - name TEXT NOT NULL, - brand TEXT, - price NUMERIC NOT NULL, - classifiers TEXT[] NOT NULL, - image TEXT, - size TEXT NOT NULL, - store_id TEXT[] NOT NULL, - search_keyword TEXT -); - --- ── Kroger ingredients v2 ───────────────────────────────────────────────────── --- Expanded schema capturing all Kroger API fields. --- price and store_ids are semicolon-delimited text (one entry per store). --- Re-runs require TRUNCATE first. - -CREATE TABLE IF NOT EXISTS kroger_ingredients2 ( - "productId" BIGINT PRIMARY KEY, - brand TEXT, - name TEXT, - categories TEXT, - "countryOrigin" TEXT, - "aisleLocations" TEXT, - image_url TEXT, - size TEXT, - "soldBy" TEXT, - classifier TEXT, - search_keyword TEXT, - store_ids TEXT, - price TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS kroger_ingredients_classifiers_idx - ON kroger_ingredients USING GIN (classifiers); - -CREATE INDEX IF NOT EXISTS kroger_ingredients_store_id_idx - ON kroger_ingredients USING GIN (store_id); - - --- ── Kroger stores ───────────────────────────────────────────────────────────── --- Populated by kroger:stores stage (requires --zipcode). --- Upserts on locationId so re-runs just refresh the data. - -CREATE TABLE IF NOT EXISTS kroger_locations ( - "locationId" BIGINT NOT NULL UNIQUE, - name TEXT, - chain TEXT, - phone BIGINT, - address_line1 TEXT, - address_line2 TEXT, - address_city TEXT, - address_state TEXT, - "address_zipCode" BIGINT, - address_county TEXT, - geo_latitude DOUBLE PRECISION, - geo_longitude DOUBLE PRECISION, - hours_timezone TEXT, - "hours_gmtOffset" TEXT, - hours_open24 BOOLEAN, - hours_monday_open TEXT, - hours_monday_close TEXT, - hours_monday_open24 BOOLEAN, - hours_tuesday_open TEXT, - hours_tuesday_close TEXT, - hours_tuesday_open24 BOOLEAN, - hours_wednesday_open TEXT, - hours_wednesday_close TEXT, - hours_wednesday_open24 BOOLEAN, - hours_thursday_open TEXT, - hours_thursday_close TEXT, - hours_thursday_open24 BOOLEAN, - hours_friday_open TEXT, - hours_friday_close TEXT, - hours_friday_open24 BOOLEAN, - hours_saturday_open TEXT, - hours_saturday_close TEXT, - hours_saturday_open24 BOOLEAN, - hours_sunday_open TEXT, - hours_sunday_close TEXT, - hours_sunday_open24 BOOLEAN -); diff --git a/supabase_import.js b/supabase_import.js deleted file mode 100644 index 466393b..0000000 --- a/supabase_import.js +++ /dev/null @@ -1,408 +0,0 @@ -/** - * supabase_import.js - * - * Reads the classified Walmart and Kroger CSVs and uploads them to Supabase. - * This runs as a stage inside runner.js, but you can also call it directly - * if you just want to re-upload without touching anything else: - * - * node supabase_import.js --all - * node supabase_import.js --walmart - * node supabase_import.js --kroger - * node supabase_import.js --all --dry-run - * - * Before running, make sure: - * - SUPABASE_URL and SUPABASE_SERVICE_KEY are set in .env - * (needs the service role key, not the anon key — anon key will hit RLS) - * - You've run schema.sql in the Supabase SQL editor to create the tables - * - If you're re-importing, TRUNCATE the tables first to avoid duplicates - */ - -import { createClient } from '@supabase/supabase-js'; -import { parse } from 'csv-parse'; -import { createReadStream, writeFileSync } from 'fs'; -import fetch from 'node-fetch'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const SUPABASE_URL = process.env.SUPABASE_URL; -const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY; - -// How many rows to send per API call. 1000 is a good balance between fewer -// round trips and not timing out on large payloads. -const BATCH_SIZE = 1000; - -const TABLE_WALMART = 'walmart_ingredients'; -const TABLE_KROGER = 'kroger_ingredients2'; -const TABLE_STORES = 'kroger_locations'; - -const KROGER_TOKEN_URL = 'https://api.kroger.com/v1/connect/oauth2/token'; -const KROGER_LOCATIONS_URL = 'https://api.kroger.com/v1/locations'; - -const DAYS = [ - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday', -]; - -if (!SUPABASE_URL || !SUPABASE_KEY) { - console.error('Missing SUPABASE_URL or SUPABASE_SERVICE_KEY in environment'); - process.exit(1); -} - -const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -// Splits a delimited string into a trimmed, filtered array. -// Used for comma-separated classifiers and semicolon-separated store IDs/prices. -function split(s, sep) { - if (!s) return []; - return s - .split(sep) - .map((x) => x.trim()) - .filter(Boolean); -} - -// Returns a float or null — null means the field was empty/unparseable, -// which lets callers decide whether to skip the row. -function toFloat(s) { - const n = parseFloat(s); - return isNaN(n) ? null : n; -} - -// Wraps a file path in a csv-parse stream so we can iterate rows with -// `for await`. The `columns: true` option uses the first row as header names. -function csvParser(filePath) { - return createReadStream(filePath).pipe( - parse({ columns: true, skip_empty_lines: true, trim: true }), - ); -} - -// Sends one batch of rows to Supabase. In dry-run mode it just logs what -// it would have done instead of actually writing anything. -// Pass onConflict to upsert instead of insert (e.g. 'productId'). -async function insertBatch(table, rows, dryRun, onConflict = null) { - if (dryRun) { - console.log(` [dry-run] would upsert ${rows.length} rows → ${table}`); - return; - } - const query = onConflict - ? supabase.from(table).upsert(rows, { onConflict }) - : supabase.from(table).insert(rows); - const { error } = await query; - if (error) throw new Error(`${table} upsert failed: ${error.message}`); -} - -// ── Walmart ─────────────────────────────────────────────────────────────────── -// Reads classified_ingredients.csv and uploads only the rows where -// ingredient=True (everything else was filtered out by classify_ingredients.py). -// Rows missing a name or price are skipped since both are NOT NULL in the table. - -export async function importWalmart({ dryRun = false } = {}) { - const file = path.join( - __dirname, - 'WalmartPipeline/classified_ingredients.csv', - ); - console.log(`\n Source: ${file}`); - - let batch = []; - let totalInserted = 0; - let skippedNonIngredient = 0; - let skippedMissingFields = 0; - - // Sends the current batch to Supabase and resets it. - async function flush() { - if (!batch.length) return; - await insertBatch(TABLE_WALMART, batch, dryRun); - totalInserted += batch.length; - batch = []; - process.stdout.write( - `\r Inserted ${totalInserted.toLocaleString()} rows...`, - ); - } - - for await (const row of csvParser(file)) { - // Only import rows that the classifier marked as a cooking ingredient - if (row.ingredient !== 'True') { - skippedNonIngredient++; - continue; - } - - const price = toFloat(row.retail_price); - - // Both name and price are NOT NULL in the table, so drop the row if either is missing - if (!row.name || price === null) { - skippedMissingFields++; - continue; - } - - batch.push({ - name: row.name, - brand: row.brandName || null, - price, - classifiers: split(row.classifiers, ','), - image: row.thumbnailImage || null, - size: row.size || '', - }); - - // Flush once we hit the batch limit to keep memory usage flat - if (batch.length >= BATCH_SIZE) await flush(); - } - - // Flush whatever is left over after the loop finishes - await flush(); - - console.log(`\n Done.`); - console.log(` Inserted : ${totalInserted.toLocaleString()}`); - console.log( - ` Skipped (not ingredient): ${skippedNonIngredient.toLocaleString()}`, - ); - console.log( - ` Skipped (no name/price) : ${skippedMissingFields.toLocaleString()}`, - ); -} - -// ── Kroger ──────────────────────────────────────────────────────────────────── -// Reads food_catalogue.csv and uploads the full catalogue. -// The price and store_id columns in the CSV are semicolon-delimited lists -// (one entry per store). We take the first price as a representative value -// and store all the store IDs as an array. - -export async function importKroger({ dryRun = false } = {}) { - const file = path.join( - __dirname, - 'kroger_output/catalogue/food_catalogue.csv', - ); - console.log(`\n Source: ${file}`); - - let batch = []; - let totalInserted = 0; - let skippedMissingFields = 0; - - async function flush() { - if (!batch.length) return; - await insertBatch(TABLE_KROGER, batch, dryRun, 'productId'); - totalInserted += batch.length; - batch = []; - process.stdout.write( - `\r Inserted ${totalInserted.toLocaleString()} rows...`, - ); - } - - for await (const row of csvParser(file)) { - // description is what we store as the product name — skip if it's blank - if (!row.description) { - skippedMissingFields++; - continue; - } - - // price is stored as-is (semicolon-delimited per-store prices). - // Skip if completely empty since the column is NOT NULL. - const price = row.price ? row.price.trim() : ''; - if (!price) { - skippedMissingFields++; - continue; - } - - batch.push({ - productId: row.productId ? parseInt(row.productId, 10) : null, - brand: row.brand || null, - name: row.description, - categories: row.categories || null, - countryOrigin: row.countryOrigin || null, - aisleLocations: row.aisleLocations || null, - image_url: row.image_url || null, - size: row.size || '', - soldBy: row.soldBy || null, - classifier: row.classifier || null, - search_keyword: row.search_keyword || null, - store_ids: row.store_ids || null, - price: String(price), - }); - - if (batch.length >= BATCH_SIZE) await flush(); - } - - await flush(); - - console.log(`\n Done.`); - console.log(` Inserted : ${totalInserted.toLocaleString()}`); - console.log( - ` Skipped (no name/price): ${skippedMissingFields.toLocaleString()}`, - ); -} - -// ── Kroger Stores ───────────────────────────────────────────────────────────── -// Fetches the N nearest Kroger-family stores for a given zip code via the -// Kroger Locations API and upserts them into kroger_stores. Using upsert -// (instead of insert) means re-running is safe — it just refreshes the data. - -async function fetchKrogerToken(clientId, clientSecret) { - const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( - 'base64', - ); - const res = await fetch(KROGER_TOKEN_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${credentials}`, - }, - body: 'grant_type=client_credentials&scope=product.compact', - }); - if (!res.ok) - throw new Error( - `Kroger token request failed (${res.status}): ${await res.text()}`, - ); - const data = await res.json(); - return data.access_token; -} - -export async function importKrogerStores({ - zipcode, - stores = 10, - dryRun = false, -} = {}) { - if (!zipcode) throw new Error('importKrogerStores requires a zipcode'); - - const clientId = process.env.KROGER_CLIENT_ID; - const clientSecret = process.env.KROGER_CLIENT_SECRET; - if (!clientId || !clientSecret) - throw new Error('Missing KROGER_CLIENT_ID or KROGER_CLIENT_SECRET'); - - console.log(`\n Fetching ${stores} nearest stores to zip ${zipcode}...`); - const token = await fetchKrogerToken(clientId, clientSecret); - - const params = new URLSearchParams({ - 'filter.zipCode.near': zipcode, - 'filter.limit': stores, - 'filter.radiusInMiles': 50, - }); - const res = await fetch(`${KROGER_LOCATIONS_URL}?${params}`, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, - }); - if (!res.ok) - throw new Error( - `Kroger locations API error (${res.status}): ${await res.text()}`, - ); - - const storeList = (await res.json())?.data ?? []; - if (!storeList.length) { - console.log(` No stores found near ${zipcode}.`); - return; - } - - // Map each store object to the exact column names in kroger_stores - const rows = storeList.map((s) => { - const addr = s.address ?? {}; - const geo = s.geolocation ?? {}; - const hrs = s.hours ?? {}; - - // phone comes back as "(555) 123-4567" — strip everything except digits for bigint - const phoneDigits = s.phone - ? parseInt(s.phone.replace(/\D/g, ''), 10) - : null; - - const record = { - locationId: s.locationId ? parseInt(s.locationId, 10) : null, - name: s.name ?? null, - chain: s.chain ?? null, - phone: isNaN(phoneDigits) ? null : phoneDigits, - address_line1: addr.addressLine1 ?? null, - address_line2: addr.addressLine2 ?? null, - address_city: addr.city ?? null, - address_state: addr.state ?? null, - address_zipCode: addr.zipCode ? parseInt(addr.zipCode, 10) : null, - address_county: addr.county ?? null, - geo_latitude: geo.latitude ?? null, - geo_longitude: geo.longitude ?? null, - hours_timezone: hrs.timezone ?? null, - hours_gmtOffset: hrs.gmtOffset != null ? String(hrs.gmtOffset) : null, - hours_open24: hrs.open24 ?? null, - }; - - // Flatten each day's open/close/open24 into individual columns - for (const day of DAYS) { - const d = hrs[day] ?? {}; - record[`hours_${day}_open`] = d.open ?? null; - record[`hours_${day}_close`] = d.close ?? null; - record[`hours_${day}_open24`] = d.open24 ?? null; - } - - return record; - }); - - if (dryRun) { - const schemaPath = path.join(__dirname, 'kroger_stores_schema.csv'); - const headers = Object.keys(rows[0]); - // Escape a value for CSV — wrap in quotes if it contains commas, quotes, or newlines - const esc = (v) => { - if (v === null || v === undefined) return ''; - const s = String(v); - return s.includes(',') || s.includes('"') || s.includes('\n') - ? `"${s.replace(/"/g, '""')}"` - : s; - }; - const lines = [ - headers.join(','), - ...rows.map((r) => headers.map((h) => esc(r[h])).join(',')), - ]; - writeFileSync(schemaPath, lines.join('\n'), 'utf8'); - console.log(` [dry-run] wrote ${rows.length} stores → ${schemaPath}`); - return; - } - - // Upsert so re-runs just refresh store data instead of erroring on duplicates - const { error } = await supabase - .from(TABLE_STORES) - .upsert(rows, { onConflict: 'locationId' }); - if (error) throw new Error(`${TABLE_STORES} upsert failed: ${error.message}`); - - console.log( - ` Upserted ${rows.length} stores near ${zipcode} → ${TABLE_STORES}`, - ); -} - -// ── Entry point (when run directly) ────────────────────────────────────────── -// This block only runs when you call `node supabase_import.js` directly. -// When runner.js imports this file it just gets the exported functions above. - -const argv = process.argv.slice(2); -const runAll = argv.includes('--all'); -const runWalmart = argv.includes('--walmart') || runAll; -const runKroger = argv.includes('--kroger') || runAll; -const isDryRun = argv.includes('--dry-run'); - -async function main() { - if (!runWalmart && !runKroger) { - console.error( - 'Specify --walmart, --kroger, or --all\n' + - 'Example: node supabase_import.js --all --dry-run', - ); - process.exit(1); - } - - if (runWalmart) { - console.log('\n[supabase:walmart] Importing Walmart ingredients...'); - await importWalmart({ dryRun: isDryRun }); - } - - if (runKroger) { - console.log('\n[supabase:kroger] Importing Kroger ingredients...'); - await importKroger({ dryRun: isDryRun }); - } - - console.log('\nImport complete.'); -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) { - main().catch((err) => { - console.error('Fatal:', err.message); - process.exit(1); - }); -} From 8b30a4bbe311506172da0e5d4b971f174c62fc9c Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Fri, 20 Mar 2026 14:22:48 -0700 Subject: [PATCH 2/2] Update README.md --- PlayWright/scraper.py | 35 ++++++------------ README.md | 85 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 README.md diff --git a/PlayWright/scraper.py b/PlayWright/scraper.py index 5a9949f..293fc06 100644 --- a/PlayWright/scraper.py +++ b/PlayWright/scraper.py @@ -53,10 +53,7 @@ from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError from supabase import create_client -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- - +# Config _ENV_PATH = Path(__file__).parent.parent / ".env" load_dotenv(_ENV_PATH) @@ -69,10 +66,8 @@ GRID_WAIT_MS = 5_000 -# --------------------------------------------------------------------------- -# Confirmed Instacart DOM selectors (verified against live DOM) -# --------------------------------------------------------------------------- +# Confirmed Instacart DOM selectors (verified against live DOM) STORE_ROW_SELECTOR = '[data-testid="CrossRetailerResultRowWrapper"]' ITEM_CARD_SELECTOR = '[data-testid^="item_list_item_"]' NEXT_PAGE_SELECTOR = '[aria-label="Next page"]' @@ -86,10 +81,7 @@ # Maximum carousel pages to advance per store row. MAX_CAROUSEL_PAGES = 10 -# --------------------------------------------------------------------------- # Ingredient parsing -# --------------------------------------------------------------------------- - def parse_ingredient(ingredient: str) -> tuple[str, str | None]: """ Parse a taxonomy ingredient string into (search_term, filter_word). @@ -114,10 +106,7 @@ def apply_name_filter(products: list[dict], filter_word: str | None) -> list[dic fw = filter_word.lower() return [p for p in products if p["name"] and fw in p["name"].lower()] -# --------------------------------------------------------------------------- # Core scraper -# --------------------------------------------------------------------------- - async def scrape_query(page, query: str) -> list[dict]: """ Scrape all product cards for *query* on the Instacart cross-retailer page. @@ -126,7 +115,7 @@ async def scrape_query(page, query: str) -> list[dict]: """ url = BASE_URL.format(query=quote_plus(query)) - # -- Navigate ---------------------------------------------------------- + # Navigate try: response = await page.goto(url, wait_until="domcontentloaded", timeout=30_000) if response and response.status >= 400: @@ -139,7 +128,7 @@ async def scrape_query(page, query: str) -> list[dict]: print(f" [ERROR] Navigation error for query '{query}': {exc}") return [] - # -- Block detection --------------------------------------------------- + # Block detection try: body_text = (await page.inner_text("body")).lower() except Exception: @@ -157,16 +146,16 @@ async def scrape_query(page, query: str) -> list[dict]: print(f" [WARN] Login modal detected for query '{query}' — skipping") return [] - # -- Wait for store rows ----------------------------------------------- + # Wait for store rows try: await page.wait_for_selector(STORE_ROW_SELECTOR, timeout=GRID_WAIT_MS) except PlaywrightTimeoutError: print(f" [WARN] No store grid found for query '{query}' after {GRID_WAIT_MS}ms") return [] - # -- Discovery + Scraping (merged) — process each carousel page inline -- - # Cards are clicked immediately while their carousel page is active, - # so ElementHandles never become stale from carousel advancement. + # Discovery + Scraping (merged) — process each carousel page inline -- + # Cards are clicked immediately while their carousel page is active, + # so ElementHandles never become stale from carousel advancement. store_rows = await page.query_selector_all(STORE_ROW_SELECTOR) if not store_rows: @@ -414,10 +403,8 @@ def _empty_product(store_name: str) -> dict: "description": None, "out_of_stock": False, } -# --------------------------------------------------------------------------- -# Pipeline entry point -# --------------------------------------------------------------------------- +# Pipeline entry point async def run_pipeline(limit: int | None = None) -> None: """ Full pipeline: fetch taxonomy → scrape Instacart → upsert to Supabase. @@ -428,7 +415,7 @@ async def run_pipeline(limit: int | None = None) -> None: "SUPABASE_URL and SUPABASE_SERVICE_KEY must be set in .env" ) - # -- Fetch taxonomy ---------------------------------------------------- + # Fetch taxonomy print("Fetching taxonomy from Supabase...") supabase = create_client(SUPABASE_URL, SUPABASE_KEY) response = supabase.table("taxonomy").select("ingredient").execute() @@ -441,7 +428,7 @@ async def run_pipeline(limit: int | None = None) -> None: total_upserted = 0 - # -- Scrape ------------------------------------------------------------ + # Scrape async with async_playwright() as p: browser = await p.chromium.launch( headless=False, diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0e76f7 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Shopwise + +A grocery price comparison app. The Instacart scraper collects product data from multiple retailers and stores it in Supabase, which the iOS frontend reads to display prices. + +## Project Structure + +``` +CS178-Shopwise/ +├── PlayWright/ +│ └── scraper.py # Instacart scraper -> Supabase +├── SWFrontUI/ # iOS app (Xcode) +└── .env # Credentials (not committed) +``` + +## Prerequisites + +- Python 3.11+ +- Xcode 15+ (for the iOS app) +- A Supabase project with the `taxonomy` and `ingredients` tables (see below) + +## Environment Setup + +Create a `.env` file at `CS178-Shopwise/.env` with the following: + +``` +SUPABASE_URL=your_supabase_project_url +SUPABASE_SERVICE_KEY=your_supabase_service_role_key +``` + +## Supabase Tables + +The scraper reads from `taxonomy` and writes to `ingredients`. + +**`taxonomy`** — ingredient list that drives all scraping: +```sql +create table taxonomy ( + ingredient text primary key +); +``` + +**`ingredients`** — scraped product results: +```sql +create table ingredients ( + id text primary key, + taxonomy text, + store text, + name text, + price text, + price_unit text, + quantity text, + image_url text, + description text, + out_of_stock boolean +); +``` + +## Running the Scraper + +### Install dependencies + +```bash +cd CS178-Shopwise/PlayWright +pip install playwright python-dotenv supabase +playwright install chromium +``` + +### Full run + +Scrapes all ingredients in the taxonomy table and upserts results to Supabase: + +```bash +python scraper.py +``` + +### Test run (first N ingredients only) + +```bash +python scraper.py --limit 3 +``` + +The browser runs in headed mode (visible) so Instacart's session/location cookies persist across queries. Do not close the browser window while the scraper is running. + +## iOS App + +Open `SWFrontUI/ShopwiseFrontEndUI.xcodeproj` in Xcode and run on a simulator or device. The app reads from the Supabase `ingredients` table.