rsm-roguchi commited on
Commit
82fc0f3
·
1 Parent(s): 7ebaa26

update inventory

Browse files
Files changed (3) hide show
  1. app.py +6 -2
  2. server/inventory.py +102 -0
  3. ui/inventory.py +20 -0
app.py CHANGED
@@ -8,7 +8,8 @@ from ui import (
8
  #general_blog,
9
  meta,
10
  twitter,
11
- price_matching
 
12
  )
13
 
14
  from server import (
@@ -16,7 +17,8 @@ from server import (
16
  #general_blog as general_blog_srv,
17
  meta as meta_srv,
18
  twitter as twitter_srv,
19
- price_matching as price_matching_srv
 
20
  )
21
 
22
 
@@ -27,6 +29,7 @@ ui = ui.page_fluid(
27
  meta.ui,
28
  twitter.ui,
29
  price_matching.ui,
 
30
  title="SEO Blog Writer",
31
  header=ui.tags.head(
32
  ui.tags.link(rel='stylesheet', type='text/css', href='style.css')
@@ -40,5 +43,6 @@ def server(input, output, session):
40
  meta_srv.server(input, output, session)
41
  twitter_srv.server(input, output, session)
42
  price_matching_srv.server(input, output, session)
 
43
 
44
  app = App(ui, server)
 
8
  #general_blog,
9
  meta,
10
  twitter,
11
+ price_matching,
12
+ inventory
13
  )
14
 
15
  from server import (
 
17
  #general_blog as general_blog_srv,
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
 
 
29
  meta.ui,
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')
 
43
  meta_srv.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)
server/inventory.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ 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 = [
14
+ "https://www.googleapis.com/auth/drive.readonly",
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)
21
+
22
+ def list_sheets_in_folder(folder_id):
23
+ query = (
24
+ f"'{folder_id}' in parents and "
25
+ "mimeType = 'application/vnd.google-apps.spreadsheet' and trashed = false"
26
+ )
27
+ results = drive_service.files().list(q=query, fields="files(id, name)").execute()
28
+ return results.get("files", [])
29
+
30
+ def load_sheet_to_duckdb(spreadsheet_id, sheet_tab="Sheet1", table_name="active_sheet"):
31
+ values = (
32
+ sheets_service.spreadsheets()
33
+ .values()
34
+ .get(spreadsheetId=spreadsheet_id, range=sheet_tab)
35
+ .execute()
36
+ .get("values", [])
37
+ )
38
+
39
+ if not values:
40
+ raise ValueError("Google Sheet is empty or not accessible.")
41
+
42
+ headers = values[0]
43
+ rows = [r + [''] * (len(headers) - len(r)) for r in values[1:]] # pad short rows
44
+
45
+ con = duckdb.connect()
46
+ insert_rows = ", ".join(str(tuple(r)) for r in rows)
47
+ insert_cols = ", ".join(f'"{h}"' for h in headers)
48
+
49
+ con.execute(f"""
50
+ CREATE OR REPLACE TABLE {table_name} AS
51
+ SELECT * FROM (VALUES {insert_rows}) AS t({insert_cols});
52
+ """)
53
+ return con
54
+
55
+ sheet_index = {}
56
+
57
+ def server(input, output, session):
58
+ con = {"conn": None}
59
+
60
+ @reactive.Effect
61
+ def init_dropdown_from_folder():
62
+ global sheet_index
63
+ try:
64
+ sheets = list_sheets_in_folder(GOOGLE_FOLDER_ID)
65
+ sheet_index = {s['name']: s['id'] for s in sheets}
66
+ sheet_names = list(sheet_index.keys())
67
+ print(f"[DEBUG] Found {len(sheet_names)} sheets in folder: {sheet_names}")
68
+
69
+ ui.update_select("sheet_dropdown", choices=sheet_names)
70
+
71
+ except Exception as e:
72
+ print(f"[ERROR] Failed to list folder contents or populate dropdown: {e}")
73
+
74
+ @reactive.Effect
75
+ def load_selected_sheet():
76
+ sheet_name = input.sheet_dropdown()
77
+ if sheet_name not in sheet_index:
78
+ return
79
+ spreadsheet_id = sheet_index[sheet_name]
80
+ print(f"[DEBUG] Loading sheet: {sheet_name} (ID: {spreadsheet_id})")
81
+ con["conn"] = load_sheet_to_duckdb(spreadsheet_id)
82
+
83
+ @output
84
+ @render.table
85
+ def results():
86
+ sheet_name = input.sheet_dropdown() # force reactivity on sheet change
87
+
88
+ try:
89
+ if con["conn"] is None:
90
+ return duckdb.query("SELECT 'Waiting for sheet selection' AS status").to_df()
91
+
92
+ name_query = input.name_filter()
93
+ if name_query:
94
+ return con["conn"].execute(
95
+ "SELECT * FROM active_sheet WHERE item_name ILIKE '%' || ? || '%'",
96
+ (name_query,)
97
+ ).fetchdf()
98
+ else:
99
+ return con["conn"].execute("SELECT * FROM active_sheet").fetchdf()
100
+
101
+ except Exception as e:
102
+ return duckdb.query(f"SELECT 'Error: {str(e).replace('\'','')}' AS error").to_df()
ui/inventory.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shiny import ui
2
+
3
+ ui = ui.nav_panel(
4
+ "Inventory",
5
+
6
+ ui.input_select(
7
+ "sheet_dropdown",
8
+ "Select a Sheet from Folder",
9
+ choices=[],
10
+ selected=None
11
+ ),
12
+
13
+ ui.input_text(
14
+ "name_filter",
15
+ "Search item_name",
16
+ placeholder="Type to filter by item_name"
17
+ ),
18
+
19
+ ui.output_table("results", class_="mt-3")
20
+ )