rsm-roguchi commited on
Commit
d4463cf
·
1 Parent(s): 3d769ea
Files changed (1) hide show
  1. server/listing_checks.py +213 -110
server/listing_checks.py CHANGED
@@ -10,6 +10,7 @@ from dotenv import load_dotenv
10
  import time
11
  import pandas as pd
12
  import uuid
 
13
 
14
  load_dotenv()
15
 
@@ -30,6 +31,8 @@ drive_service = build("drive", "v3", credentials=credentials)
30
  sheets_service = build("sheets", "v4", credentials=credentials)
31
 
32
 
 
 
33
  def get_walmart_token(client_id: str, client_secret: str) -> str:
34
  auth_str = f"{client_id}:{client_secret}"
35
  auth_b64 = base64.b64encode(auth_str.encode()).decode()
@@ -48,88 +51,195 @@ def get_walmart_token(client_id: str, client_secret: str) -> str:
48
  if response.status_code != 200:
49
  raise Exception(f"Token request failed: {response.status_code}\n{response.text}")
50
 
51
- # Parse the XML response
52
  root = ET.fromstring(response.text)
53
  token_elem = root.find("accessToken")
54
-
55
  if token_elem is None:
56
  raise Exception("accessToken not found in response.")
57
-
58
  return token_elem.text
59
 
60
 
61
- def get_inventory_quantity(sku: str, access_token: str) -> int:
62
- url = f"https://marketplace.walmartapis.com/v3/inventory?sku={sku}"
 
 
 
 
 
 
63
  headers = {
64
  "Authorization": f"Bearer {access_token}",
65
  "WM_SEC.ACCESS_TOKEN": access_token,
66
  "WM_SVC.NAME": "Walmart Marketplace",
67
  "WM_QOS.CORRELATION_ID": str(uuid.uuid4()),
68
- "Accept": "application/json"
69
  }
70
 
71
- response = requests.get(url, headers=headers)
72
- if response.status_code == 200:
73
- return response.json().get("quantity", {}).get("amount")
74
- else:
75
- print(f"⚠️ Inventory fetch failed for {sku} ({response.status_code})")
76
- return None
 
 
 
 
77
 
 
 
 
78
 
79
- def get_item_ids_and_quantities(access_token: str) -> pd.DataFrame:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  url = "https://marketplace.walmartapis.com/v3/items"
81
  headers = {
82
  "Authorization": f"Bearer {access_token}",
83
  "WM_SEC.ACCESS_TOKEN": access_token,
84
  "WM_SVC.NAME": "Walmart Marketplace",
85
  "WM_QOS.CORRELATION_ID": str(uuid.uuid4()),
86
- "Accept": "application/json"
 
 
87
  }
88
 
89
- records = []
90
- offset = 0
91
- limit = 365
92
- total_items = None
93
- item_counter = 0
 
94
 
95
- while True:
96
- full_url = f"{url}?limit={limit}&offset={offset}"
97
- response = requests.get(full_url, headers=headers)
98
- if response.status_code != 200:
99
- print(f"❌ Error {response.status_code}: {response.text}")
100
- break
101
 
102
- data = response.json()
103
- if total_items is None:
104
- total_items = data.get("totalItems", 0)
105
- print(f"📦 Total items to process: {total_items}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- items = data.get("ItemResponse", [])
108
- if not items:
109
  break
110
 
111
- for item in items:
112
- item_counter += 1
113
- item_name = item.get('productName')
114
- sku = item.get("sku")
115
- gtin = item.get("gtin")
116
- qty = get_inventory_quantity(sku, access_token)
117
- time.sleep(0.1)
118
 
119
- print(f"🔢 [{item_counter}/{total_items}] gtin: {gtin} | Product: {item_name[:20]} | Qty: {qty}")
120
 
121
- records.append({
122
- 'productName': item_name,
123
- "gtin": gtin,
124
- "quantity": qty
125
- })
 
 
 
 
126
 
127
- offset += limit
128
- if offset >= total_items:
129
- break
130
-
131
- return pd.DataFrame(records)
132
 
 
133
 
134
  def list_sheets_in_folder(folder_id):
135
  query = (
@@ -151,101 +261,94 @@ def load_sheet_as_dataframe(sheet_id, range_name="Sheet1"):
151
  values = result.get("values", [])
152
  if not values:
153
  return pd.DataFrame()
154
-
155
- # First row = header
156
- df = pd.DataFrame(values[1:], columns=values[0])
157
- return df
158
 
159
 
160
- sheet_index = {}
161
- walmart_data = {"df": None}
162
-
163
 
164
  def server(input, output, session):
 
165
  @reactive.Effect
166
- def init_dropdown_from_folder():
167
- global sheet_index
168
  try:
169
  sheets = list_sheets_in_folder(GOOGLE_FOLDER_ID)
170
- sheet_index = {s['name']: s['id'] for s in sheets}
171
- sheet_names = list(sheet_index.keys())
172
- print(f"[DEBUG] Found {len(sheet_names)} sheets: {sheet_names}")
173
-
174
- ui.update_select("sheet_dropdown_check", choices=sheet_names)
175
-
176
  except Exception as e:
177
  print(f"[ERROR] Failed to list folder contents: {e}")
178
 
 
179
  @reactive.Effect
180
  @reactive.event(input.load_walmart_data)
181
- def load_walmart_data():
182
  try:
183
  print("[DEBUG] Getting Walmart token...")
184
  token = get_walmart_token(WALMART_CLIENT_ID, WALMART_CLIENT_SECRET)
185
- print("[DEBUG] Fetching Walmart inventory data...")
186
- walmart_data["df"] = get_item_ids_and_quantities(token)
187
- print(f"[DEBUG] Loaded {len(walmart_data['df'])} Walmart items")
 
188
  except Exception as e:
189
  print(f"[ERROR] Failed to load Walmart data: {e}")
190
- walmart_data["df"] = pd.DataFrame() # Set empty DataFrame on error
191
 
192
  @output
193
  @render.text
194
  def walmart_status():
195
- if walmart_data["df"] is None:
 
196
  return "Click 'Load Walmart Data' to fetch inventory data"
197
- elif len(walmart_data["df"]) == 0:
198
- return "Error loading Walmart data"
199
- else:
200
- return f"Walmart data loaded: {len(walmart_data['df'])} items"
201
 
202
  @output
203
  @render.table
204
  def results_check():
205
- sheet_name = input.sheet_dropdown()
206
-
 
 
 
 
 
 
 
 
 
207
  try:
208
- if not sheet_name or sheet_name not in sheet_index:
209
- return pd.DataFrame({"status": ["Select a sheet to compare"]})
210
-
211
- if walmart_data["df"] is None:
212
- return pd.DataFrame({"status": ["Loading Walmart data..."]})
213
-
214
- print(f"[DEBUG] Loading sheet: {sheet_name}")
215
- sheet_id = sheet_index[sheet_name]
216
  google_df = load_sheet_as_dataframe(sheet_id)
217
-
218
  if google_df.empty:
219
  return pd.DataFrame({"error": ["Google sheet is empty"]})
220
-
221
- walmart_df = walmart_data["df"]
222
-
223
- # Merge the data - adjust column names as needed
224
- if 'walmart_gtin' in google_df.columns:
225
  merged = google_df.merge(
226
- walmart_df[['gtin', 'productName', 'quantity']],
227
- left_on='walmart_gtin',
228
- right_on='gtin',
229
- how='inner'
230
  )
231
-
232
- # Show discrepancies
233
- if 'walmart_quantity' in merged.columns:
234
- discrepancies = merged[
235
- merged['walmart_quantity'].astype(int) !=
236
- merged['quantity'].astype(int)
237
- ][['productName', 'walmart_quantity', 'quantity']]
238
-
 
239
  if discrepancies.empty:
240
- return pd.DataFrame({
241
- "status": ["No quantity discrepancies found"]
242
- })
243
  return discrepancies
244
  else:
245
- return merged[['productName', 'gtin', 'quantity']]
246
  else:
247
- # If no walmart_gtin column, just show the merged data
248
- return walmart_df
249
-
250
  except Exception as e:
251
- return pd.DataFrame({"error": [f"Error: {str(e)}"]})
 
10
  import time
11
  import pandas as pd
12
  import uuid
13
+ from typing import Optional
14
 
15
  load_dotenv()
16
 
 
31
  sheets_service = build("sheets", "v4", credentials=credentials)
32
 
33
 
34
+ # ---------------- Walmart Auth ----------------
35
+
36
  def get_walmart_token(client_id: str, client_secret: str) -> str:
37
  auth_str = f"{client_id}:{client_secret}"
38
  auth_b64 = base64.b64encode(auth_str.encode()).decode()
 
51
  if response.status_code != 200:
52
  raise Exception(f"Token request failed: {response.status_code}\n{response.text}")
53
 
54
+ # XML response
55
  root = ET.fromstring(response.text)
56
  token_elem = root.find("accessToken")
 
57
  if token_elem is None:
58
  raise Exception("accessToken not found in response.")
 
59
  return token_elem.text
60
 
61
 
62
+ # ---------------- New ATS workflow ----------------
63
+
64
+ def fetch_all_ats(access_token: str, limit: int = 50) -> pd.DataFrame:
65
+ """
66
+ Pull all SKUs and total available-to-sell (ATS) across nodes using /v3/inventories (nextCursor).
67
+ Returns: DataFrame with ['sku','ats']
68
+ """
69
+ base_url = "https://marketplace.walmartapis.com/v3/inventories"
70
  headers = {
71
  "Authorization": f"Bearer {access_token}",
72
  "WM_SEC.ACCESS_TOKEN": access_token,
73
  "WM_SVC.NAME": "Walmart Marketplace",
74
  "WM_QOS.CORRELATION_ID": str(uuid.uuid4()),
75
+ "Accept": "application/json",
76
  }
77
 
78
+ rows = []
79
+ cursor: Optional[str] = None
80
+ while True:
81
+ params = {"limit": limit}
82
+ if cursor:
83
+ params["nextCursor"] = cursor
84
+
85
+ r = requests.get(base_url, headers=headers, params=params)
86
+ if r.status_code != 200:
87
+ raise RuntimeError(f"❌ /inventories {r.status_code}: {r.text}")
88
 
89
+ payload = r.json()
90
+ inventories = (payload.get("elements") or {}).get("inventories", []) or []
91
+ cursor = (payload.get("meta") or {}).get("nextCursor")
92
 
93
+ for inv in inventories:
94
+ sku = inv.get("sku")
95
+ nodes = inv.get("nodes") or []
96
+ ats = sum((n.get("availToSellQty", {}) or {}).get("amount", 0) or 0 for n in nodes)
97
+ rows.append({"sku": sku, "ats": ats})
98
+
99
+ if not cursor:
100
+ break
101
+
102
+ return pd.DataFrame(rows, columns=["sku", "ats"])
103
+
104
+
105
+ def _extract_gtin_like(item: dict) -> Optional[str]:
106
+ """
107
+ Prefer 'gtin', fallback to 'upc', then scan common identifier shapes.
108
+ """
109
+ gtin = item.get("gtin")
110
+ if gtin:
111
+ return gtin
112
+ upc = item.get("upc")
113
+ if upc:
114
+ return upc
115
+
116
+ candidates = []
117
+ for key in ("productIdentifiers", "identifiers", "additionalProductAttributes", "attributes"):
118
+ obj = item.get(key)
119
+ if isinstance(obj, list):
120
+ for e in obj:
121
+ if not isinstance(e, dict):
122
+ continue
123
+ t = (e.get("productIdType") or e.get("type") or "").upper()
124
+ v = e.get("productId") or e.get("id") or e.get("value")
125
+ if v and t in {"GTIN", "UPC"}:
126
+ candidates.append((t, v))
127
+ elif isinstance(obj, dict):
128
+ for t, v in obj.items():
129
+ if isinstance(v, str) and t.upper() in {"GTIN", "UPC"}:
130
+ candidates.append((t.upper(), v))
131
+
132
+ for t, v in candidates:
133
+ if t == "GTIN":
134
+ return v
135
+ for t, v in candidates:
136
+ if t == "UPC":
137
+ return v
138
+ return None
139
+
140
+
141
+ def _get_total_items(access_token: str) -> int:
142
+ """
143
+ Probe /v3/items to read total count from meta (meta.totalCount or totalItems).
144
+ """
145
+ url = "https://marketplace.walmartapis.com/v3/items"
146
+ headers = {
147
+ "Authorization": f"Bearer {access_token}",
148
+ "WM_SEC.ACCESS_TOKEN": access_token,
149
+ "WM_SVC.NAME": "Walmart Marketplace",
150
+ "WM_QOS.CORRELATION_ID": str(uuid.uuid4()),
151
+ "Accept": "application/json",
152
+ "WM_GLOBAL_VERSION": "3.1",
153
+ "WM_MARKET": "us",
154
+ }
155
+ params = {"limit": 1}
156
+ r = requests.get(url, headers=headers, params=params)
157
+ if r.status_code != 200:
158
+ raise RuntimeError(f"❌ /items (probe) {r.status_code}: {r.text}")
159
+ data = r.json()
160
+ meta = data.get("meta") or {}
161
+ total = meta.get("totalCount")
162
+ if total is None:
163
+ total = data.get("totalItems")
164
+ if total is None:
165
+ total = len(data.get("ItemResponse", []) or [])
166
+ return int(total)
167
+
168
+
169
+ def fetch_all_items_with_gtin_cursor(
170
+ access_token: str,
171
+ limit: Optional[int] = None
172
+ ) -> pd.DataFrame:
173
+ """
174
+ Pull items via /v3/items with nextCursor.
175
+ If limit is None, auto-sets to total items reported by meta.
176
+ Returns: ['sku','gtin','productName']
177
+ """
178
  url = "https://marketplace.walmartapis.com/v3/items"
179
  headers = {
180
  "Authorization": f"Bearer {access_token}",
181
  "WM_SEC.ACCESS_TOKEN": access_token,
182
  "WM_SVC.NAME": "Walmart Marketplace",
183
  "WM_QOS.CORRELATION_ID": str(uuid.uuid4()),
184
+ "Accept": "application/json",
185
+ "WM_GLOBAL_VERSION": "3.1",
186
+ "WM_MARKET": "us",
187
  }
188
 
189
+ if limit is None:
190
+ try:
191
+ limit = _get_total_items(access_token)
192
+ except Exception as e:
193
+ print(f"⚠️ Could not detect total items automatically: {e}. Falling back to 200.")
194
+ limit = 200
195
 
196
+ base_params = {"limit": limit}
197
+ recs = []
198
+ cursor: Optional[str] = None
 
 
 
199
 
200
+ while True:
201
+ q = dict(base_params)
202
+ if cursor:
203
+ q["nextCursor"] = cursor
204
+
205
+ resp = requests.get(url, headers=headers, params=q)
206
+ if resp.status_code != 200:
207
+ # If large limit is rejected, back off and paginate.
208
+ if q.get("limit", 0) > 500:
209
+ print(f"ℹ️ Large limit={q['limit']} not accepted. Backing off to 200 with pagination.")
210
+ base_params["limit"] = 200
211
+ continue
212
+ raise RuntimeError(f"❌ /items {resp.status_code}: {resp.text}")
213
+
214
+ data = resp.json()
215
+ items = data.get("ItemResponse", []) or []
216
+ cursor = (data.get("meta") or {}).get("nextCursor")
217
+
218
+ for it in items:
219
+ recs.append({
220
+ "sku": it.get("sku"),
221
+ "gtin": _extract_gtin_like(it),
222
+ "productName": it.get("productName"),
223
+ })
224
 
225
+ if not cursor:
 
226
  break
227
 
228
+ return pd.DataFrame(recs, columns=["sku", "gtin", "productName"])
 
 
 
 
 
 
229
 
 
230
 
231
+ def fetch_inventory_with_gtin_cursor(access_token: str) -> pd.DataFrame:
232
+ """
233
+ Join ATS (from /inventories) with GTIN/productName (from /items) on SKU.
234
+ Returns: ['sku','gtin','productName','ats']
235
+ """
236
+ ats_df = fetch_all_ats(access_token)
237
+ items_df = fetch_all_items_with_gtin_cursor(access_token, limit=None)
238
+ merged = ats_df.merge(items_df, on="sku", how="left")
239
+ return merged[["sku", "gtin", "productName", "ats"]]
240
 
 
 
 
 
 
241
 
242
+ # ---------------- Google Drive helpers ----------------
243
 
244
  def list_sheets_in_folder(folder_id):
245
  query = (
 
261
  values = result.get("values", [])
262
  if not values:
263
  return pd.DataFrame()
264
+ return pd.DataFrame(values[1:], columns=values[0])
 
 
 
265
 
266
 
267
+ # --- reactive state ---
268
+ sheet_index = reactive.Value({}) # Shiny tracks changes
269
+ walmart_df = reactive.Value(pd.DataFrame()) # <- Store ATS join here
270
 
271
  def server(input, output, session):
272
+ # Populate dropdown once (or whenever your Drive listing changes)
273
  @reactive.Effect
274
+ def _init_dropdown_from_folder():
 
275
  try:
276
  sheets = list_sheets_in_folder(GOOGLE_FOLDER_ID)
277
+ idx = {s['name']: s['id'] for s in sheets}
278
+ sheet_index.set(idx)
279
+ ui.update_select("sheet_dropdown_check", choices=list(idx.keys()))
 
 
 
280
  except Exception as e:
281
  print(f"[ERROR] Failed to list folder contents: {e}")
282
 
283
+ # Button press -> fetch token -> fetch ATS+items -> set reactive value
284
  @reactive.Effect
285
  @reactive.event(input.load_walmart_data)
286
+ def _load_walmart_data():
287
  try:
288
  print("[DEBUG] Getting Walmart token...")
289
  token = get_walmart_token(WALMART_CLIENT_ID, WALMART_CLIENT_SECRET)
290
+ print("[DEBUG] Fetching Walmart ATS + GTIN ...")
291
+ df = fetch_inventory_with_gtin_cursor(token) # <- your new ATS workflow
292
+ walmart_df.set(df) # <- invalidate dependents
293
+ print(f"[DEBUG] Loaded {len(df)} Walmart items (ATS)")
294
  except Exception as e:
295
  print(f"[ERROR] Failed to load Walmart data: {e}")
296
+ walmart_df.set(pd.DataFrame())
297
 
298
  @output
299
  @render.text
300
  def walmart_status():
301
+ df = walmart_df() # establish dependency
302
+ if df.empty:
303
  return "Click 'Load Walmart Data' to fetch inventory data"
304
+ return f"Walmart data loaded: {len(df)} items (ATS)"
 
 
 
305
 
306
  @output
307
  @render.table
308
  def results_check():
309
+ # Re-run when either the dropdown changes or the walmart_df changes
310
+ _ = walmart_df() # establish dependency on data
311
+ selected = input.sheet_dropdown_check()
312
+
313
+ idx = sheet_index()
314
+ if not selected or selected not in idx:
315
+ return pd.DataFrame({"status": ["Select a sheet to compare"]})
316
+
317
+ if walmart_df().empty:
318
+ return pd.DataFrame({"status": ["Click 'Load Walmart Data' first"]})
319
+
320
  try:
321
+ sheet_id = idx[selected]
 
 
 
 
 
 
 
322
  google_df = load_sheet_as_dataframe(sheet_id)
 
323
  if google_df.empty:
324
  return pd.DataFrame({"error": ["Google sheet is empty"]})
325
+
326
+ wdf = walmart_df()[["sku", "gtin", "productName", "ats"]]
327
+
328
+ if "walmart_gtin" in google_df.columns:
 
329
  merged = google_df.merge(
330
+ wdf[["gtin", "productName", "ats"]],
331
+ left_on="walmart_gtin",
332
+ right_on="gtin",
333
+ how="inner",
334
  )
335
+ compare_col = (
336
+ "walmart_ats"
337
+ if "walmart_ats" in merged.columns
338
+ else ("walmart_quantity" if "walmart_quantity" in merged.columns else None)
339
+ )
340
+ if compare_col:
341
+ lhs = pd.to_numeric(merged[compare_col], errors="coerce").fillna(0).astype(int)
342
+ rhs = pd.to_numeric(merged["ats"], errors="coerce").fillna(0).astype(int)
343
+ discrepancies = merged.loc[lhs.ne(rhs), ["productName", compare_col, "ats"]]
344
  if discrepancies.empty:
345
+ return pd.DataFrame({"status": ["No ATS discrepancies found"]})
 
 
346
  return discrepancies
347
  else:
348
+ return merged[["productName", "gtin", "ats"]]
349
  else:
350
+ # No GTIN in sheet: just show ATS snapshot
351
+ return wdf
352
+
353
  except Exception as e:
354
+ return pd.DataFrame({"error": [f"Error: {e}"]})