youngtsai commited on
Commit
cf3160b
·
1 Parent(s): 04304fc

get_sheet_data

Browse files
Files changed (2) hide show
  1. app.py +26 -2
  2. sheet_service.py +269 -0
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import urllib.parse
 
2
 
3
  import gradio as gr
4
  from starlette.middleware.base import BaseHTTPMiddleware
@@ -69,6 +70,7 @@ from googleapiclient.http import MediaIoBaseUpload
69
 
70
  from educational_material import EducationalMaterial
71
  from storage_service import GoogleCloudStorage
 
72
  from google.oauth2.service_account import Credentials
73
  import vertexai
74
  from vertexai.generative_models import GenerativeModel, Part
@@ -93,6 +95,7 @@ if is_env_local:
93
  GCS_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
94
  DRIVE_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
95
  GBQ_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
 
96
  OPEN_AI_KEY = config["OPEN_AI_KEY"]
97
  OPEN_AI_ASSISTANT_ID_GPT4_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT4_BOT1"]
98
  OPEN_AI_ASSISTANT_ID_GPT3_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT3_BOT1"]
@@ -142,6 +145,7 @@ GBQ_CLIENT = bigquery.Client.from_service_account_info(json.loads(GBQ_KEY))
142
  GROQ_CLIENT = Groq(api_key=GROQ_API_KEY)
143
  GCS_SERVICE = GoogleCloudStorage(GCS_KEY)
144
  GCS_CLIENT = GCS_SERVICE.client
 
145
  PERPLEXITY_CLIENT = OpenAI(api_key=PERPLEXITY_API_KEY, base_url="https://api.perplexity.ai")
146
 
147
  # check open ai access
@@ -2150,6 +2154,12 @@ def summary_add_markdown_version(video_id):
2150
 
2151
 
2152
  # LLM 強制重刷
 
 
 
 
 
 
2153
  def refresh_video_LLM_all_content(video_ids):
2154
  # 輸入影片 id,以 , 逗號分隔 或是 \n 換行
2155
  video_id_list = video_ids.replace('\n', ',').split(',')
@@ -3647,8 +3657,22 @@ def create_app():
3647
  with gr.Row():
3648
  gr.Markdown("## 清單影片:重新生成所有內容")
3649
  with gr.Row():
3650
- refresh_video_ids = gr.Textbox(label="輸入影片 id,以 , 逗號分隔")
3651
- refresh_btn = gr.Button("refresh", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3652
  with gr.Row():
3653
  refresh_result = gr.JSON()
3654
 
 
1
  import urllib.parse
2
+ import re
3
 
4
  import gradio as gr
5
  from starlette.middleware.base import BaseHTTPMiddleware
 
70
 
71
  from educational_material import EducationalMaterial
72
  from storage_service import GoogleCloudStorage
73
+ from sheet_service import SheetService
74
  from google.oauth2.service_account import Credentials
75
  import vertexai
76
  from vertexai.generative_models import GenerativeModel, Part
 
95
  GCS_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
96
  DRIVE_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
97
  GBQ_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
98
+ SHEET_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
99
  OPEN_AI_KEY = config["OPEN_AI_KEY"]
100
  OPEN_AI_ASSISTANT_ID_GPT4_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT4_BOT1"]
101
  OPEN_AI_ASSISTANT_ID_GPT3_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT3_BOT1"]
 
145
  GROQ_CLIENT = Groq(api_key=GROQ_API_KEY)
146
  GCS_SERVICE = GoogleCloudStorage(GCS_KEY)
147
  GCS_CLIENT = GCS_SERVICE.client
148
+ SHEET_SERVICE = SheetService(SHEET_KEY)
149
  PERPLEXITY_CLIENT = OpenAI(api_key=PERPLEXITY_API_KEY, base_url="https://api.perplexity.ai")
150
 
151
  # check open ai access
 
2154
 
2155
 
2156
  # LLM 強制重刷
2157
+ def get_sheet_data(sheet_url, range_name):
2158
+ data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
2159
+ flattened_data = SHEET_SERVICE.flatten_column_data(data)
2160
+ return flattened_data
2161
+
2162
+
2163
  def refresh_video_LLM_all_content(video_ids):
2164
  # 輸入影片 id,以 , 逗號分隔 或是 \n 換行
2165
  video_id_list = video_ids.replace('\n', ',').split(',')
 
3657
  with gr.Row():
3658
  gr.Markdown("## 清單影片:重新生成所有內容")
3659
  with gr.Row():
3660
+ # tab refresh_video_ids & by sheets
3661
+ with gr.Tab("refresh_video_ids"):
3662
+ refresh_video_ids = gr.Textbox(label="輸入影片 id,以 , 逗號分隔")
3663
+ refresh_btn = gr.Button("refresh", variant="primary")
3664
+ with gr.Tab("by sheets"):
3665
+ sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
3666
+ sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
3667
+ sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
3668
+ sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
3669
+
3670
+ sheet_get_value_btn.click(
3671
+ get_sheet_data,
3672
+ inputs=[sheet_url, gr.Textbox(value="D:D", visible=True)], # 將範圍修改為 D 欄
3673
+ outputs=[sheet_get_value_result]
3674
+ )
3675
+
3676
  with gr.Row():
3677
  refresh_result = gr.JSON()
3678
 
sheet_service.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.oauth2.credentials
2
+ import googleapiclient.discovery
3
+ from google.oauth2 import service_account
4
+ import json
5
+ from urllib.parse import urlparse, parse_qs
6
+ import logging # 建議使用 logging 而非 print
7
+
8
+ # 設定基本的 logging
9
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
+
11
+ class SheetService:
12
+ """
13
+ 一個用於與 Google Sheets API 互動的服務類別。
14
+ """
15
+ SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
16
+
17
+ def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
18
+ """
19
+ 初始化 SheetService。
20
+
21
+ Args:
22
+ service_account_key_string (str): 包含 Google 服務帳戶憑證資訊的 JSON 字串。
23
+ 通常是從 JSON 金鑰檔案讀取的內容。
24
+ api_service_name (str): 要使用的 Google API 服務名稱。預設為 'sheets'。
25
+ api_version (str): 要使用的 Google API 版本。預設為 'v4'。
26
+ """
27
+ try:
28
+ credentials_info = json.loads(service_account_key_string)
29
+ self.credentials = service_account.Credentials.from_service_account_info(
30
+ credentials_info, scopes=self.SCOPES
31
+ )
32
+ self.service = googleapiclient.discovery.build(
33
+ api_service_name, api_version, credentials=self.credentials
34
+ )
35
+ self.sheet = self.service.spreadsheets()
36
+ logging.info("成功連接 Google Sheets API")
37
+ except json.JSONDecodeError as e:
38
+ logging.error(f"解析憑證 JSON 字串時發生錯誤: {e}")
39
+ self.service = None
40
+ self.sheet = None
41
+ except Exception as e:
42
+ logging.error(f"連接 Google Sheets API 時發生錯誤: {e}")
43
+ self.service = None
44
+ self.sheet = None
45
+
46
+ def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
47
+ """
48
+ 從 Google Sheets URL 中提取試算表 ID。
49
+ """
50
+ parsed_url = urlparse(sheet_url)
51
+ path_parts = parsed_url.path.split('/')
52
+ try:
53
+ # Google Sheet URL 格式通常是 /spreadsheets/d/SPREADSHEET_ID/edit...
54
+ if 'd' in path_parts:
55
+ id_index = path_parts.index('d') + 1
56
+ if id_index < len(path_parts):
57
+ spreadsheet_id = path_parts[id_index]
58
+ # 進行一些基本檢查,確保它看起來像一個 ID
59
+ if len(spreadsheet_id) > 30: # Google Sheet ID 通常很長
60
+ return spreadsheet_id
61
+ except ValueError:
62
+ pass # 'd' 不在路徑中
63
+
64
+ logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
65
+ return None
66
+
67
+ def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
68
+ """
69
+ 從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
70
+ 返回整數型別的 gid 或 None。
71
+ """
72
+ parsed_url = urlparse(sheet_url)
73
+ query_params = parse_qs(parsed_url.query)
74
+ fragment_params = parse_qs(parsed_url.fragment) # gid 也可能在 # 後面
75
+
76
+ gid_str = None
77
+ if 'gid' in query_params:
78
+ gid_str = query_params['gid'][0]
79
+ elif 'gid' in fragment_params:
80
+ gid_str = fragment_params['gid'][0]
81
+
82
+ if gid_str:
83
+ try:
84
+ return int(gid_str)
85
+ except ValueError:
86
+ logging.warning(f"URL 中的 gid 不是有效的整數: {gid_str}")
87
+ return None
88
+ else:
89
+ # logging.info(f"URL 中未找到 gid 參數,將嘗試使用第一個工作表: {sheet_url}")
90
+ # 如果 URL 沒有 gid,通常表示是第一個工作表,其 gid 通常是 0
91
+ # 但我們在這裡返回 None,讓後續邏輯決定如何處理
92
+ return None
93
+
94
+ def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
95
+ """
96
+ 使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
97
+ 如果 gid 為 None,則返回第一個工作表的名稱。
98
+
99
+ Args:
100
+ spreadsheet_id (str): Google 試算表的 ID。
101
+ gid (int | None): 目標工作表分頁的 ID。如果為 None,則獲取第一個工作表。
102
+
103
+ Returns:
104
+ str | None: 工作表的名稱 (title),如果找不到或發生錯誤則返回 None。
105
+ """
106
+ if not self.service:
107
+ logging.error("Sheet API 服務未成功初始化。")
108
+ return None
109
+ try:
110
+ # 使用 spreadsheets.get 獲取試算表的中繼資料
111
+ # fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
112
+ sheet_metadata = self.service.spreadsheets().get(
113
+ spreadsheetId=spreadsheet_id,
114
+ fields='sheets(properties(sheetId,title))'
115
+ ).execute()
116
+
117
+ sheets = sheet_metadata.get('sheets', [])
118
+ if not sheets:
119
+ logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
120
+ return None
121
+
122
+ if gid is not None:
123
+ # 如果提供了 gid,尋找匹配的工作表
124
+ for sheet in sheets:
125
+ properties = sheet.get('properties', {})
126
+ if properties.get('sheetId') == gid:
127
+ sheet_title = properties.get('title')
128
+ if sheet_title:
129
+ logging.info(f"找到 gid={gid} 對應的工作表名稱: '{sheet_title}'")
130
+ return sheet_title
131
+ else:
132
+ logging.warning(f"找到 gid={gid} 但缺少 title 屬性。")
133
+ return None
134
+ # 如果遍歷完畢沒有找到匹配的 gid
135
+ logging.warning(f"在試算表 {spreadsheet_id} 中未找到 gid={gid} 的工作表。")
136
+ return None
137
+ else:
138
+ # 如果 gid 為 None,返回第一個工作表的名稱
139
+ first_sheet_properties = sheets[0].get('properties', {})
140
+ first_sheet_title = first_sheet_properties.get('title')
141
+ first_sheet_gid = first_sheet_properties.get('sheetId', '未知')
142
+ if first_sheet_title:
143
+ logging.info(f"未提供 gid,使用第一個工作表 (gid={first_sheet_gid}): '{first_sheet_title}'")
144
+ return first_sheet_title
145
+ else:
146
+ logging.warning(f"第一個工作表 (gid={first_sheet_gid}) 缺少 title 屬性。")
147
+ return None
148
+
149
+ except googleapiclient.errors.HttpError as error:
150
+ logging.error(f"獲取工作表名稱時發生 API 錯誤: {error}")
151
+ return None
152
+ except Exception as e:
153
+ logging.error(f"獲取工作表名稱時發生未知錯誤: {e}")
154
+ return None
155
+
156
+ def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
157
+ """
158
+ 通過 Google Sheets URL 自動獲取 Spreadsheet ID 和工作表名稱,並讀取數據。
159
+ 如果 URL 中包含 gid,則讀取對應的工作表;否則讀取第一個工作表。
160
+ 默認讀取整個工作表的數據。
161
+
162
+ Args:
163
+ sheet_url (str): Google 試算表的完整 URL。
164
+ read_range (str | None): 可選。指定要讀取的儲存格範圍 (例如 'A1:C10')。
165
+ 如果提供,則只讀取此範圍;否則讀取整個工作表。
166
+
167
+ Returns:
168
+ list | None: 包含讀取到的資料的列表 (list of lists),如果發生錯誤則返回 None。
169
+ """
170
+ spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
171
+ if not spreadsheet_id:
172
+ logging.error("無法從 URL 獲取 Spreadsheet ID。")
173
+ return None
174
+
175
+ gid = self.get_sheet_gid_by_url(sheet_url)
176
+ # 無論 gid 是否為 None,都嘗試獲取工作表名稱
177
+ sheet_name = self.get_sheet_name_by_gid(spreadsheet_id, gid)
178
+
179
+ if not sheet_name:
180
+ logging.error(f"無法根據 URL ({sheet_url}) 確定要讀取的工作表名稱。")
181
+ return None
182
+
183
+ # 組合 range_name
184
+ if read_range:
185
+ # 如果使用者指定了範圍,將其與工作表名稱結合
186
+ # 需要確保工作表名稱不包含特殊字符,或者正確引用
187
+ # 簡單起見,如果名稱包含空格或特殊符號,用單引號括起來
188
+ if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name:
189
+ range_name = f"'{sheet_name}'!{read_range}"
190
+ else:
191
+ range_name = f"{sheet_name}!{read_range}"
192
+ else:
193
+ # 如果未指定範圍,則讀取整個工作表
194
+ # 只需要提供工作表名稱即可
195
+ if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name:
196
+ range_name = f"'{sheet_name}'"
197
+ else:
198
+ range_name = sheet_name
199
+
200
+
201
+ logging.info(f"準備從試算表 '{spreadsheet_id}' 的 '{range_name}' 範圍讀取數據。")
202
+ # 使用現有的 get_sheet_value 方法讀取數據
203
+ return self.get_sheet_value(spreadsheet_id, range_name)
204
+
205
+ def get_sheet_value(self, spreadsheet_id: str, range_name: str) -> list | None:
206
+ """
207
+ 從指定的試算表和範圍讀取資料。
208
+
209
+ Args:
210
+ spreadsheet_id (str): Google 試算表的 ID。
211
+ range_name (str): 要讀取的範圍,例如 'Sheet1!A1:B2' 或僅 'Sheet1' (讀取整個工作表)。
212
+
213
+ Returns:
214
+ list | None: 包含讀取到的資料的列表 (list of lists),如果發生錯誤則返回 None。
215
+ """
216
+ if not self.sheet:
217
+ logging.error("Sheet API 服務未成功初始化。")
218
+ return None
219
+
220
+ try:
221
+ logging.info(f"正在讀取 Spreadsheet ID: {spreadsheet_id}, Range: {range_name}")
222
+
223
+ result = self.sheet.values().get(
224
+ spreadsheetId=spreadsheet_id,
225
+ range=range_name
226
+ ).execute()
227
+ values = result.get('values', [])
228
+ logging.info(f"成功從 {spreadsheet_id} 的 {range_name} 讀取 {len(values)} 列資料。")
229
+ # 如果 values 是 None 或空列表,直接返回
230
+ if not values:
231
+ logging.warning(f"在 {spreadsheet_id} 的 {range_name} 範圍內未找到任何資料。")
232
+ return [] # 返回空列表而不是 None,以便後續處理
233
+ return values
234
+ except googleapiclient.errors.HttpError as error:
235
+ # 更詳細地記錄錯誤信息
236
+ error_details = error.resp.get('content', '{}')
237
+ try:
238
+ error_json = json.loads(error_details)
239
+ error_message = error_json.get('error', {}).get('message', str(error))
240
+ except json.JSONDecodeError:
241
+ error_message = str(error)
242
+ logging.error(f"讀取試算表時發生 API 錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {error_message}")
243
+ return None
244
+ except Exception as e:
245
+ logging.error(f"讀取試算表時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
246
+ return None
247
+
248
+ @staticmethod
249
+ def flatten_column_data(data: list[list[str]]) -> list[str]:
250
+ """
251
+ 將從 Google Sheets API 獲取的單欄數據(列表的列表)扁平化為單一列表。
252
+
253
+ 例如,將 [['A'], ['B'], ['C']] 轉換為 ['A', 'B', 'C']。
254
+ 此方法會跳過空的內部列表,並假設每個非空內部列表只取第一個元素。
255
+
256
+ Args:
257
+ data (list[list[str]]): 從 API 獲取的原始數據,通常是 list of lists。
258
+
259
+ Returns:
260
+ list[str]: 包含所有第一欄元素的單一列表。如果輸入為 None 或空列表,
261
+ 則返回空列表。
262
+ """
263
+ if not data:
264
+ return []
265
+ # 使用列表推導式,提取每個子列表的第一個元素
266
+ # 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
267
+ # 並將其轉換為字串 str() 以確保類型一致性
268
+ flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
269
+ return flattened