rsm-roguchi commited on
Commit
5a68d82
·
1 Parent(s): 82fc0f3
app.py CHANGED
@@ -9,7 +9,8 @@ from ui import (
9
  meta,
10
  twitter,
11
  price_matching,
12
- inventory
 
13
  )
14
 
15
  from server import (
@@ -18,7 +19,8 @@ from server import (
18
  meta as meta_srv,
19
  twitter as twitter_srv,
20
  price_matching as price_matching_srv,
21
- inventory as inventory_srv
 
22
  )
23
 
24
 
@@ -30,6 +32,7 @@ ui = ui.page_fluid(
30
  twitter.ui,
31
  price_matching.ui,
32
  inventory.ui,
 
33
  title="SEO Blog Writer",
34
  header=ui.tags.head(
35
  ui.tags.link(rel='stylesheet', type='text/css', href='style.css')
@@ -44,5 +47,6 @@ def server(input, output, session):
44
  twitter_srv.server(input, output, session)
45
  price_matching_srv.server(input, output, session)
46
  inventory_srv.server(input, output, session)
 
47
 
48
  app = App(ui, server)
 
9
  meta,
10
  twitter,
11
  price_matching,
12
+ inventory,
13
+ listing_checks
14
  )
15
 
16
  from server import (
 
19
  meta as meta_srv,
20
  twitter as twitter_srv,
21
  price_matching as price_matching_srv,
22
+ inventory as inventory_srv,
23
+ listing_checks as listing_checks_srv
24
  )
25
 
26
 
 
32
  twitter.ui,
33
  price_matching.ui,
34
  inventory.ui,
35
+ listing_checks.ui,
36
  title="SEO Blog Writer",
37
  header=ui.tags.head(
38
  ui.tags.link(rel='stylesheet', type='text/css', href='style.css')
 
47
  twitter_srv.server(input, output, session)
48
  price_matching_srv.server(input, output, session)
49
  inventory_srv.server(input, output, session)
50
+ listing_checks_srv.server(input, output, session)
51
 
52
  app = App(ui, server)
server/general_blog.py CHANGED
@@ -28,6 +28,7 @@ async def scrape_section_content_from_url(url: str) -> str:
28
  await browser.close()
29
 
30
  soup = BeautifulSoup(html, "html.parser")
 
31
 
32
  # Match all divs and extract text
33
  content_blocks = soup.find_all("div")
 
28
  await browser.close()
29
 
30
  soup = BeautifulSoup(html, "html.parser")
31
+ print(soup)
32
 
33
  # Match all divs and extract text
34
  content_blocks = soup.find_all("div")
server/inventory.py CHANGED
@@ -4,10 +4,18 @@ import duckdb
4
  from shiny import render, reactive, ui
5
  from google.oauth2 import service_account
6
  from googleapiclient.discovery import build
 
 
 
7
  from dotenv import load_dotenv
 
 
8
 
9
  load_dotenv()
10
 
 
 
 
11
  SERVICE_ACCOUNT_JSON = json.loads(os.getenv("SERVICE_ACCOUNT_JSON"))
12
  GOOGLE_FOLDER_ID = os.getenv("GOOGLE_DRIVE_FOLDER_ID")
13
  SCOPES = [
@@ -15,6 +23,7 @@ SCOPES = [
15
  "https://www.googleapis.com/auth/spreadsheets.readonly"
16
  ]
17
 
 
18
  credentials = service_account.Credentials.from_service_account_info(SERVICE_ACCOUNT_JSON, scopes=SCOPES)
19
  drive_service = build("drive", "v3", credentials=credentials)
20
  sheets_service = build("sheets", "v4", credentials=credentials)
 
4
  from shiny import render, reactive, ui
5
  from google.oauth2 import service_account
6
  from googleapiclient.discovery import build
7
+ import requests
8
+ import base64
9
+ import xml.etree.ElementTree as ET
10
  from dotenv import load_dotenv
11
+ import time
12
+ import pandas as pd
13
 
14
  load_dotenv()
15
 
16
+ WALMART_CLIENT_ID = os.getenv('WALMART_CLIENT_ID')
17
+ WALMART_CLIENT_SECRET = os.getenv('WALMART_CLIENT_SECRET')
18
+
19
  SERVICE_ACCOUNT_JSON = json.loads(os.getenv("SERVICE_ACCOUNT_JSON"))
20
  GOOGLE_FOLDER_ID = os.getenv("GOOGLE_DRIVE_FOLDER_ID")
21
  SCOPES = [
 
23
  "https://www.googleapis.com/auth/spreadsheets.readonly"
24
  ]
25
 
26
+
27
  credentials = service_account.Credentials.from_service_account_info(SERVICE_ACCOUNT_JSON, scopes=SCOPES)
28
  drive_service = build("drive", "v3", credentials=credentials)
29
  sheets_service = build("sheets", "v4", credentials=credentials)
server/listing_checks.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from shiny import render, reactive, ui
4
+ from google.oauth2 import service_account
5
+ from googleapiclient.discovery import build
6
+ import requests
7
+ import base64
8
+ import xml.etree.ElementTree as ET
9
+ from dotenv import load_dotenv
10
+ import time
11
+ import pandas as pd
12
+ import uuid
13
+
14
+ load_dotenv()
15
+
16
+ WALMART_CLIENT_ID = os.getenv('WALMART_CLIENT_ID')
17
+ WALMART_CLIENT_SECRET = os.getenv('WALMART_CLIENT_SECRET')
18
+
19
+ SERVICE_ACCOUNT_JSON = json.loads(os.getenv("SERVICE_ACCOUNT_JSON"))
20
+ GOOGLE_FOLDER_ID = os.getenv("GOOGLE_DRIVE_FOLDER_ID")
21
+ SCOPES = [
22
+ "https://www.googleapis.com/auth/drive.readonly",
23
+ "https://www.googleapis.com/auth/spreadsheets.readonly"
24
+ ]
25
+
26
+ credentials = service_account.Credentials.from_service_account_info(
27
+ SERVICE_ACCOUNT_JSON, scopes=SCOPES
28
+ )
29
+ 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()
36
+
37
+ headers = {
38
+ "Authorization": f"Basic {auth_b64}",
39
+ "Content-Type": "application/x-www-form-urlencoded",
40
+ "WM_SVC.NAME": "Walmart Marketplace"
41
+ }
42
+
43
+ payload = {"grant_type": "client_credentials"}
44
+ url = "https://marketplace.walmartapis.com/v3/token"
45
+
46
+ response = requests.post(url, headers=headers, data=payload)
47
+
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 = (
136
+ f"'{folder_id}' in parents and "
137
+ "mimeType = 'application/vnd.google-apps.spreadsheet' and trashed = false"
138
+ )
139
+ results = drive_service.files().list(
140
+ q=query, fields="files(id, name)"
141
+ ).execute()
142
+ return results.get("files", [])
143
+
144
+
145
+ def load_sheet_as_dataframe(sheet_id, range_name="Sheet1"):
146
+ result = sheets_service.spreadsheets().values().get(
147
+ spreadsheetId=sheet_id,
148
+ range=range_name
149
+ ).execute()
150
+
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)}"]})
ui/general_blog.py CHANGED
@@ -4,7 +4,7 @@ ui = ui.nav_panel(
4
  "General Blog Writer",
5
 
6
  # Scoped URL input
7
- ui.input_text("blog_url", "Enter Pokémon.com Article URL", placeholder="https://www.pokemon.com/us/pokemon-news/..."),
8
 
9
  # Scoped buttons
10
  ui.div(
 
4
  "General Blog Writer",
5
 
6
  # Scoped URL input
7
+ ui.input_text("blog_url", "Enter Pokémon.com Article URL", placeholder="https://www.pokemon.com/us/..."),
8
 
9
  # Scoped buttons
10
  ui.div(
ui/listing_checks.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shiny import ui
2
+
3
+ ui = ui.nav_panel(
4
+ "Listing Checks",
5
+
6
+ ui.input_select(
7
+ "sheet_dropdown_check",
8
+ "Select a Sheet from Folder",
9
+ choices=[],
10
+ selected=None
11
+ ),
12
+
13
+ ui.input_action_button(
14
+ "load_walmart_data",
15
+ "Load Walmart Data",
16
+ class_="btn btn-primary mb-3"
17
+ ),
18
+
19
+ ui.output_text_verbatim("walmart_status"),
20
+
21
+ ui.output_table("results_check", class_="mt-3")
22
+ )