botrun-flow-lang 5.10.82__py3-none-any.whl → 5.10.83__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- botrun_flow_lang/api/auth_api.py +39 -39
- botrun_flow_lang/api/auth_utils.py +183 -183
- botrun_flow_lang/api/botrun_back_api.py +65 -65
- botrun_flow_lang/api/flow_api.py +3 -3
- botrun_flow_lang/api/hatch_api.py +481 -481
- botrun_flow_lang/api/langgraph_api.py +796 -796
- botrun_flow_lang/api/line_bot_api.py +1357 -1357
- botrun_flow_lang/api/model_api.py +300 -300
- botrun_flow_lang/api/rate_limit_api.py +32 -32
- botrun_flow_lang/api/routes.py +79 -79
- botrun_flow_lang/api/search_api.py +53 -53
- botrun_flow_lang/api/storage_api.py +316 -316
- botrun_flow_lang/api/subsidy_api.py +290 -290
- botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
- botrun_flow_lang/api/user_setting_api.py +70 -70
- botrun_flow_lang/api/version_api.py +31 -31
- botrun_flow_lang/api/youtube_api.py +26 -26
- botrun_flow_lang/constants.py +13 -13
- botrun_flow_lang/langgraph_agents/agents/agent_runner.py +174 -174
- botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
- botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
- botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +591 -548
- botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
- botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
- botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
- botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
- botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
- botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
- botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
- botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
- botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
- botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
- botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
- botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
- botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
- botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
- botrun_flow_lang/llm_agent/llm_agent.py +19 -19
- botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
- botrun_flow_lang/log/.gitignore +2 -2
- botrun_flow_lang/main.py +61 -61
- botrun_flow_lang/main_fast.py +51 -51
- botrun_flow_lang/mcp_server/__init__.py +10 -10
- botrun_flow_lang/mcp_server/default_mcp.py +711 -711
- botrun_flow_lang/models/nodes/utils.py +205 -205
- botrun_flow_lang/models/token_usage.py +34 -34
- botrun_flow_lang/requirements.txt +21 -21
- botrun_flow_lang/services/base/firestore_base.py +30 -30
- botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
- botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
- botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
- botrun_flow_lang/services/storage/storage_factory.py +12 -12
- botrun_flow_lang/services/storage/storage_store.py +65 -65
- botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
- botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
- botrun_flow_lang/static/docs/tools/index.html +926 -926
- botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
- botrun_flow_lang/tests/api_stress_test.py +357 -357
- botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
- botrun_flow_lang/tests/test_botrun_app.py +46 -46
- botrun_flow_lang/tests/test_html_util.py +31 -31
- botrun_flow_lang/tests/test_img_analyzer.py +190 -190
- botrun_flow_lang/tests/test_img_util.py +39 -39
- botrun_flow_lang/tests/test_local_files.py +114 -114
- botrun_flow_lang/tests/test_mermaid_util.py +103 -103
- botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
- botrun_flow_lang/tests/test_plotly_util.py +151 -151
- botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
- botrun_flow_lang/tools/generate_docs.py +133 -133
- botrun_flow_lang/tools/templates/tools.html +153 -153
- botrun_flow_lang/utils/__init__.py +7 -7
- botrun_flow_lang/utils/botrun_logger.py +344 -344
- botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
- botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
- botrun_flow_lang/utils/google_drive_utils.py +654 -654
- botrun_flow_lang/utils/langchain_utils.py +324 -324
- botrun_flow_lang/utils/yaml_utils.py +9 -9
- {botrun_flow_lang-5.10.82.dist-info → botrun_flow_lang-5.10.83.dist-info}/METADATA +3 -2
- botrun_flow_lang-5.10.83.dist-info/RECORD +99 -0
- botrun_flow_lang-5.10.82.dist-info/RECORD +0 -99
- {botrun_flow_lang-5.10.82.dist-info → botrun_flow_lang-5.10.83.dist-info}/WHEEL +0 -0
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
from google.cloud import storage
|
|
2
|
-
from google.cloud.exceptions import NotFound
|
|
3
|
-
from google.oauth2 import service_account
|
|
4
|
-
from io import BytesIO
|
|
5
|
-
import os
|
|
6
|
-
from typing import Optional, Tuple
|
|
7
|
-
from datetime import datetime, timedelta, UTC
|
|
8
|
-
|
|
9
|
-
from botrun_flow_lang.constants import HATCH_BUCKET_NAME
|
|
10
|
-
from botrun_flow_lang.services.storage.storage_store import StorageStore
|
|
11
|
-
from botrun_flow_lang.utils.botrun_logger import get_default_botrun_logger
|
|
12
|
-
|
|
13
|
-
logger = get_default_botrun_logger()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class StorageCsStore(StorageStore):
|
|
17
|
-
def __init__(self, env_name: str):
|
|
18
|
-
google_service_account_key_path = os.getenv(
|
|
19
|
-
"GOOGLE_APPLICATION_CREDENTIALS_FOR_FASTAPI",
|
|
20
|
-
"/app/keys/scoop-386004-d22d99a7afd9.json",
|
|
21
|
-
)
|
|
22
|
-
credentials = service_account.Credentials.from_service_account_file(
|
|
23
|
-
google_service_account_key_path,
|
|
24
|
-
scopes=["https://www.googleapis.com/auth/devstorage.full_control"],
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
self.storage_client = storage.Client(credentials=credentials)
|
|
28
|
-
self.bucket_name = f"{HATCH_BUCKET_NAME}-{env_name}"
|
|
29
|
-
self.bucket = self.create_bucket(self.bucket_name)
|
|
30
|
-
if not self.bucket:
|
|
31
|
-
raise Exception(f"Failed to create or get bucket: {self.bucket_name}")
|
|
32
|
-
|
|
33
|
-
def create_bucket(self, bucket_name: str) -> Optional[storage.Bucket]:
|
|
34
|
-
"""創建新的 bucket,如果已存在則返回現有的,並確保 lifecycle rules 正確設定"""
|
|
35
|
-
try:
|
|
36
|
-
bucket = self.storage_client.bucket(bucket_name)
|
|
37
|
-
|
|
38
|
-
desired_rules = [
|
|
39
|
-
{
|
|
40
|
-
"action": {"type": "Delete"},
|
|
41
|
-
"condition": {"age": 7, "matchesPrefix": ["tmp/"]},
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
if not bucket.exists():
|
|
46
|
-
logger.info(f"Creating new bucket: {bucket_name}")
|
|
47
|
-
# 設定 lifecycle rules
|
|
48
|
-
bucket.lifecycle_rules = desired_rules
|
|
49
|
-
# 創建 bucket
|
|
50
|
-
bucket = self.storage_client.create_bucket(
|
|
51
|
-
bucket, location="asia-east1"
|
|
52
|
-
)
|
|
53
|
-
logger.info(
|
|
54
|
-
f"Created bucket {bucket_name} in asia-east1 and set lifecycle rules."
|
|
55
|
-
)
|
|
56
|
-
else:
|
|
57
|
-
logger.info(
|
|
58
|
-
f"Bucket {bucket_name} already exists. Checking lifecycle rules."
|
|
59
|
-
)
|
|
60
|
-
bucket.reload() # 獲取最新的 bucket metadata
|
|
61
|
-
|
|
62
|
-
# google-cloud-storage 回傳的 rule 是 frozenset of dicts,需要轉換
|
|
63
|
-
current_rules = [dict(rule) for rule in bucket.lifecycle_rules]
|
|
64
|
-
|
|
65
|
-
if current_rules != desired_rules:
|
|
66
|
-
logger.info(f"Updating lifecycle rules for bucket {bucket_name}")
|
|
67
|
-
bucket.lifecycle_rules = desired_rules
|
|
68
|
-
bucket.patch()
|
|
69
|
-
logger.info(
|
|
70
|
-
f"Successfully updated lifecycle rules for bucket {bucket_name}"
|
|
71
|
-
)
|
|
72
|
-
else:
|
|
73
|
-
logger.info(
|
|
74
|
-
f"Lifecycle rules for bucket {bucket_name} are already up-to-date."
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return bucket
|
|
78
|
-
except Exception as e:
|
|
79
|
-
logger.error(f"Error creating or updating bucket {bucket_name}: {str(e)}")
|
|
80
|
-
return None
|
|
81
|
-
|
|
82
|
-
async def get_directory_sizes(self) -> dict:
|
|
83
|
-
"""
|
|
84
|
-
計算 bucket 中每個目錄的總檔案大小 (bytes) 與檔案數量,排除 tmp, html 目錄
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
dict: 包含每個目錄資訊的字典,格式為 {directory_name: {"size": total_size_in_bytes, "file_count": count}}
|
|
88
|
-
"""
|
|
89
|
-
try:
|
|
90
|
-
# 初始化結果字典
|
|
91
|
-
directory_info = {}
|
|
92
|
-
|
|
93
|
-
# 列出所有 blobs
|
|
94
|
-
blobs = list(self.bucket.list_blobs())
|
|
95
|
-
|
|
96
|
-
# 計算每個目錄的大小和檔案數量
|
|
97
|
-
for blob in blobs:
|
|
98
|
-
# 跳過 tmp 目錄
|
|
99
|
-
if blob.name.startswith("tmp/"):
|
|
100
|
-
continue
|
|
101
|
-
# 跳過 html 目錄
|
|
102
|
-
if blob.name.startswith("html/"):
|
|
103
|
-
continue
|
|
104
|
-
|
|
105
|
-
# 從 blob 名稱中提取目錄名稱 (第一層目錄)
|
|
106
|
-
parts = blob.name.split("/")
|
|
107
|
-
if len(parts) >= 1:
|
|
108
|
-
directory = parts[0]
|
|
109
|
-
|
|
110
|
-
# 如果這是一個新目錄,初始化其資訊
|
|
111
|
-
if directory not in directory_info:
|
|
112
|
-
directory_info[directory] = {"size": 0, "file_count": 0}
|
|
113
|
-
|
|
114
|
-
# 加上此 blob 的大小
|
|
115
|
-
directory_info[directory]["size"] += blob.size
|
|
116
|
-
# 增加檔案計數
|
|
117
|
-
directory_info[directory]["file_count"] += 1
|
|
118
|
-
|
|
119
|
-
return directory_info
|
|
120
|
-
except Exception as e:
|
|
121
|
-
logger.error(f"Error calculating directory sizes: {e}")
|
|
122
|
-
return {}
|
|
123
|
-
|
|
124
|
-
async def store_file(
|
|
125
|
-
self,
|
|
126
|
-
filepath: str,
|
|
127
|
-
file_object: BytesIO,
|
|
128
|
-
public: bool = False,
|
|
129
|
-
content_type: str = None,
|
|
130
|
-
) -> Tuple[bool, Optional[str]]:
|
|
131
|
-
try:
|
|
132
|
-
blob = self.bucket.blob(filepath)
|
|
133
|
-
|
|
134
|
-
# 設定 content_type 和其他 metadata
|
|
135
|
-
if content_type:
|
|
136
|
-
blob.content_type = content_type
|
|
137
|
-
# 如果是圖片,設定為 inline 顯示並加入 cache control
|
|
138
|
-
if content_type.startswith("image/"):
|
|
139
|
-
blob.content_disposition = (
|
|
140
|
-
'inline; filename="' + filepath.split("/")[-1] + '"'
|
|
141
|
-
)
|
|
142
|
-
blob.cache_control = "public, max-age=3600, no-transform"
|
|
143
|
-
|
|
144
|
-
# 上傳檔案
|
|
145
|
-
blob.upload_from_file(file_object, rewind=True)
|
|
146
|
-
|
|
147
|
-
# 確保 metadata 更新
|
|
148
|
-
blob.patch()
|
|
149
|
-
|
|
150
|
-
# 如果需要公開存取
|
|
151
|
-
if public:
|
|
152
|
-
blob.make_public()
|
|
153
|
-
return True, blob.public_url
|
|
154
|
-
|
|
155
|
-
return True, None
|
|
156
|
-
except Exception as e:
|
|
157
|
-
logger.error(f"Error storing file in Cloud Storage: {e}")
|
|
158
|
-
return False, None
|
|
159
|
-
|
|
160
|
-
async def get_public_url(self, filepath: str) -> Optional[str]:
|
|
161
|
-
try:
|
|
162
|
-
blob = self.bucket.blob(filepath)
|
|
163
|
-
if blob.exists():
|
|
164
|
-
return blob.public_url
|
|
165
|
-
return None
|
|
166
|
-
except Exception as e:
|
|
167
|
-
logger.error(f"Error getting public URL: {e}")
|
|
168
|
-
return None
|
|
169
|
-
|
|
170
|
-
async def retrieve_file(self, filepath: str) -> Optional[BytesIO]:
|
|
171
|
-
try:
|
|
172
|
-
blob = self.bucket.blob(filepath)
|
|
173
|
-
file_object = BytesIO()
|
|
174
|
-
blob.download_to_file(file_object)
|
|
175
|
-
file_object.seek(0) # Rewind the file object to the beginning
|
|
176
|
-
return file_object
|
|
177
|
-
except NotFound:
|
|
178
|
-
logger.error(f"File not found in Cloud Storage: {filepath}")
|
|
179
|
-
return None
|
|
180
|
-
except Exception as e:
|
|
181
|
-
logger.error(f"Error retrieving file from Cloud Storage: {e}")
|
|
182
|
-
return None
|
|
183
|
-
|
|
184
|
-
async def delete_file(self, filepath: str) -> bool:
|
|
185
|
-
try:
|
|
186
|
-
blob = self.bucket.blob(filepath)
|
|
187
|
-
blob.delete()
|
|
188
|
-
return True
|
|
189
|
-
except NotFound:
|
|
190
|
-
logger.error(f"File not found in Cloud Storage: {filepath}")
|
|
191
|
-
return False
|
|
192
|
-
except Exception as e:
|
|
193
|
-
logger.error(f"Error deleting file from Cloud Storage: {e}")
|
|
194
|
-
return False
|
|
195
|
-
|
|
196
|
-
async def file_exists(self, filepath: str) -> bool:
|
|
197
|
-
try:
|
|
198
|
-
blob = self.bucket.blob(filepath)
|
|
199
|
-
return blob.exists()
|
|
200
|
-
except Exception as e:
|
|
201
|
-
logger.error(f"Error checking file existence in Cloud Storage: {e}")
|
|
202
|
-
return False
|
|
1
|
+
from google.cloud import storage
|
|
2
|
+
from google.cloud.exceptions import NotFound
|
|
3
|
+
from google.oauth2 import service_account
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
from datetime import datetime, timedelta, UTC
|
|
8
|
+
|
|
9
|
+
from botrun_flow_lang.constants import HATCH_BUCKET_NAME
|
|
10
|
+
from botrun_flow_lang.services.storage.storage_store import StorageStore
|
|
11
|
+
from botrun_flow_lang.utils.botrun_logger import get_default_botrun_logger
|
|
12
|
+
|
|
13
|
+
logger = get_default_botrun_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StorageCsStore(StorageStore):
|
|
17
|
+
def __init__(self, env_name: str):
|
|
18
|
+
google_service_account_key_path = os.getenv(
|
|
19
|
+
"GOOGLE_APPLICATION_CREDENTIALS_FOR_FASTAPI",
|
|
20
|
+
"/app/keys/scoop-386004-d22d99a7afd9.json",
|
|
21
|
+
)
|
|
22
|
+
credentials = service_account.Credentials.from_service_account_file(
|
|
23
|
+
google_service_account_key_path,
|
|
24
|
+
scopes=["https://www.googleapis.com/auth/devstorage.full_control"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
self.storage_client = storage.Client(credentials=credentials)
|
|
28
|
+
self.bucket_name = f"{HATCH_BUCKET_NAME}-{env_name}"
|
|
29
|
+
self.bucket = self.create_bucket(self.bucket_name)
|
|
30
|
+
if not self.bucket:
|
|
31
|
+
raise Exception(f"Failed to create or get bucket: {self.bucket_name}")
|
|
32
|
+
|
|
33
|
+
def create_bucket(self, bucket_name: str) -> Optional[storage.Bucket]:
|
|
34
|
+
"""創建新的 bucket,如果已存在則返回現有的,並確保 lifecycle rules 正確設定"""
|
|
35
|
+
try:
|
|
36
|
+
bucket = self.storage_client.bucket(bucket_name)
|
|
37
|
+
|
|
38
|
+
desired_rules = [
|
|
39
|
+
{
|
|
40
|
+
"action": {"type": "Delete"},
|
|
41
|
+
"condition": {"age": 7, "matchesPrefix": ["tmp/"]},
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
if not bucket.exists():
|
|
46
|
+
logger.info(f"Creating new bucket: {bucket_name}")
|
|
47
|
+
# 設定 lifecycle rules
|
|
48
|
+
bucket.lifecycle_rules = desired_rules
|
|
49
|
+
# 創建 bucket
|
|
50
|
+
bucket = self.storage_client.create_bucket(
|
|
51
|
+
bucket, location="asia-east1"
|
|
52
|
+
)
|
|
53
|
+
logger.info(
|
|
54
|
+
f"Created bucket {bucket_name} in asia-east1 and set lifecycle rules."
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
logger.info(
|
|
58
|
+
f"Bucket {bucket_name} already exists. Checking lifecycle rules."
|
|
59
|
+
)
|
|
60
|
+
bucket.reload() # 獲取最新的 bucket metadata
|
|
61
|
+
|
|
62
|
+
# google-cloud-storage 回傳的 rule 是 frozenset of dicts,需要轉換
|
|
63
|
+
current_rules = [dict(rule) for rule in bucket.lifecycle_rules]
|
|
64
|
+
|
|
65
|
+
if current_rules != desired_rules:
|
|
66
|
+
logger.info(f"Updating lifecycle rules for bucket {bucket_name}")
|
|
67
|
+
bucket.lifecycle_rules = desired_rules
|
|
68
|
+
bucket.patch()
|
|
69
|
+
logger.info(
|
|
70
|
+
f"Successfully updated lifecycle rules for bucket {bucket_name}"
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
logger.info(
|
|
74
|
+
f"Lifecycle rules for bucket {bucket_name} are already up-to-date."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return bucket
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error creating or updating bucket {bucket_name}: {str(e)}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
async def get_directory_sizes(self) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
計算 bucket 中每個目錄的總檔案大小 (bytes) 與檔案數量,排除 tmp, html 目錄
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
dict: 包含每個目錄資訊的字典,格式為 {directory_name: {"size": total_size_in_bytes, "file_count": count}}
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
# 初始化結果字典
|
|
91
|
+
directory_info = {}
|
|
92
|
+
|
|
93
|
+
# 列出所有 blobs
|
|
94
|
+
blobs = list(self.bucket.list_blobs())
|
|
95
|
+
|
|
96
|
+
# 計算每個目錄的大小和檔案數量
|
|
97
|
+
for blob in blobs:
|
|
98
|
+
# 跳過 tmp 目錄
|
|
99
|
+
if blob.name.startswith("tmp/"):
|
|
100
|
+
continue
|
|
101
|
+
# 跳過 html 目錄
|
|
102
|
+
if blob.name.startswith("html/"):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# 從 blob 名稱中提取目錄名稱 (第一層目錄)
|
|
106
|
+
parts = blob.name.split("/")
|
|
107
|
+
if len(parts) >= 1:
|
|
108
|
+
directory = parts[0]
|
|
109
|
+
|
|
110
|
+
# 如果這是一個新目錄,初始化其資訊
|
|
111
|
+
if directory not in directory_info:
|
|
112
|
+
directory_info[directory] = {"size": 0, "file_count": 0}
|
|
113
|
+
|
|
114
|
+
# 加上此 blob 的大小
|
|
115
|
+
directory_info[directory]["size"] += blob.size
|
|
116
|
+
# 增加檔案計數
|
|
117
|
+
directory_info[directory]["file_count"] += 1
|
|
118
|
+
|
|
119
|
+
return directory_info
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error calculating directory sizes: {e}")
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
async def store_file(
|
|
125
|
+
self,
|
|
126
|
+
filepath: str,
|
|
127
|
+
file_object: BytesIO,
|
|
128
|
+
public: bool = False,
|
|
129
|
+
content_type: str = None,
|
|
130
|
+
) -> Tuple[bool, Optional[str]]:
|
|
131
|
+
try:
|
|
132
|
+
blob = self.bucket.blob(filepath)
|
|
133
|
+
|
|
134
|
+
# 設定 content_type 和其他 metadata
|
|
135
|
+
if content_type:
|
|
136
|
+
blob.content_type = content_type
|
|
137
|
+
# 如果是圖片,設定為 inline 顯示並加入 cache control
|
|
138
|
+
if content_type.startswith("image/"):
|
|
139
|
+
blob.content_disposition = (
|
|
140
|
+
'inline; filename="' + filepath.split("/")[-1] + '"'
|
|
141
|
+
)
|
|
142
|
+
blob.cache_control = "public, max-age=3600, no-transform"
|
|
143
|
+
|
|
144
|
+
# 上傳檔案
|
|
145
|
+
blob.upload_from_file(file_object, rewind=True)
|
|
146
|
+
|
|
147
|
+
# 確保 metadata 更新
|
|
148
|
+
blob.patch()
|
|
149
|
+
|
|
150
|
+
# 如果需要公開存取
|
|
151
|
+
if public:
|
|
152
|
+
blob.make_public()
|
|
153
|
+
return True, blob.public_url
|
|
154
|
+
|
|
155
|
+
return True, None
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error(f"Error storing file in Cloud Storage: {e}")
|
|
158
|
+
return False, None
|
|
159
|
+
|
|
160
|
+
async def get_public_url(self, filepath: str) -> Optional[str]:
|
|
161
|
+
try:
|
|
162
|
+
blob = self.bucket.blob(filepath)
|
|
163
|
+
if blob.exists():
|
|
164
|
+
return blob.public_url
|
|
165
|
+
return None
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Error getting public URL: {e}")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
async def retrieve_file(self, filepath: str) -> Optional[BytesIO]:
|
|
171
|
+
try:
|
|
172
|
+
blob = self.bucket.blob(filepath)
|
|
173
|
+
file_object = BytesIO()
|
|
174
|
+
blob.download_to_file(file_object)
|
|
175
|
+
file_object.seek(0) # Rewind the file object to the beginning
|
|
176
|
+
return file_object
|
|
177
|
+
except NotFound:
|
|
178
|
+
logger.error(f"File not found in Cloud Storage: {filepath}")
|
|
179
|
+
return None
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error retrieving file from Cloud Storage: {e}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
async def delete_file(self, filepath: str) -> bool:
|
|
185
|
+
try:
|
|
186
|
+
blob = self.bucket.blob(filepath)
|
|
187
|
+
blob.delete()
|
|
188
|
+
return True
|
|
189
|
+
except NotFound:
|
|
190
|
+
logger.error(f"File not found in Cloud Storage: {filepath}")
|
|
191
|
+
return False
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error deleting file from Cloud Storage: {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
async def file_exists(self, filepath: str) -> bool:
|
|
197
|
+
try:
|
|
198
|
+
blob = self.bucket.blob(filepath)
|
|
199
|
+
return blob.exists()
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Error checking file existence in Cloud Storage: {e}")
|
|
202
|
+
return False
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from dotenv import load_dotenv
|
|
3
|
-
|
|
4
|
-
from botrun_flow_lang.services.storage.storage_cs_store import StorageCsStore
|
|
5
|
-
from botrun_flow_lang.services.storage.storage_store import StorageStore
|
|
6
|
-
|
|
7
|
-
load_dotenv()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def storage_store_factory() -> StorageStore:
|
|
11
|
-
env_name = os.getenv("HATCH_ENV_NAME", "botrun-hatch-dev")
|
|
12
|
-
return StorageCsStore(env_name)
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
from botrun_flow_lang.services.storage.storage_cs_store import StorageCsStore
|
|
5
|
+
from botrun_flow_lang.services.storage.storage_store import StorageStore
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def storage_store_factory() -> StorageStore:
|
|
11
|
+
env_name = os.getenv("HATCH_ENV_NAME", "botrun-hatch-dev")
|
|
12
|
+
return StorageCsStore(env_name)
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Optional, Tuple
|
|
3
|
-
from io import BytesIO
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class StorageStore(ABC):
|
|
7
|
-
|
|
8
|
-
@abstractmethod
|
|
9
|
-
async def store_file(
|
|
10
|
-
self,
|
|
11
|
-
filepath: str,
|
|
12
|
-
file_object: BytesIO,
|
|
13
|
-
public: bool = False,
|
|
14
|
-
content_type: str = None,
|
|
15
|
-
) -> Tuple[bool, Optional[str]]:
|
|
16
|
-
"""
|
|
17
|
-
Store a file in the storage.
|
|
18
|
-
|
|
19
|
-
:param filepath: The path where the file should be stored
|
|
20
|
-
:param file_object: The file object to be stored (BytesIO)
|
|
21
|
-
:param public: Whether the file should be publicly accessible
|
|
22
|
-
:param content_type: The MIME type of the file
|
|
23
|
-
:return: Tuple of (success, public_url if public=True else None)
|
|
24
|
-
"""
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
@abstractmethod
|
|
28
|
-
async def get_public_url(self, filepath: str) -> Optional[str]:
|
|
29
|
-
"""
|
|
30
|
-
Get the public URL for a file.
|
|
31
|
-
|
|
32
|
-
:param filepath: The path of the file
|
|
33
|
-
:return: Public URL if the file exists and is public, None otherwise
|
|
34
|
-
"""
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
@abstractmethod
|
|
38
|
-
async def retrieve_file(self, filepath: str) -> Optional[BytesIO]:
|
|
39
|
-
"""
|
|
40
|
-
Retrieve a file from the storage.
|
|
41
|
-
|
|
42
|
-
:param filepath: The path of the file to retrieve
|
|
43
|
-
:return: BytesIO object containing the file data if found, None otherwise
|
|
44
|
-
"""
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
@abstractmethod
|
|
48
|
-
async def delete_file(self, filepath: str) -> bool:
|
|
49
|
-
"""
|
|
50
|
-
Delete a file from the storage.
|
|
51
|
-
|
|
52
|
-
:param filepath: The path of the file to delete
|
|
53
|
-
:return: True if the file was successfully deleted, False otherwise
|
|
54
|
-
"""
|
|
55
|
-
pass
|
|
56
|
-
|
|
57
|
-
@abstractmethod
|
|
58
|
-
async def file_exists(self, filepath: str) -> bool:
|
|
59
|
-
"""
|
|
60
|
-
Check if a file exists in the storage.
|
|
61
|
-
|
|
62
|
-
:param filepath: The path of the file to check
|
|
63
|
-
:return: True if the file exists, False otherwise
|
|
64
|
-
"""
|
|
65
|
-
pass
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StorageStore(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def store_file(
|
|
10
|
+
self,
|
|
11
|
+
filepath: str,
|
|
12
|
+
file_object: BytesIO,
|
|
13
|
+
public: bool = False,
|
|
14
|
+
content_type: str = None,
|
|
15
|
+
) -> Tuple[bool, Optional[str]]:
|
|
16
|
+
"""
|
|
17
|
+
Store a file in the storage.
|
|
18
|
+
|
|
19
|
+
:param filepath: The path where the file should be stored
|
|
20
|
+
:param file_object: The file object to be stored (BytesIO)
|
|
21
|
+
:param public: Whether the file should be publicly accessible
|
|
22
|
+
:param content_type: The MIME type of the file
|
|
23
|
+
:return: Tuple of (success, public_url if public=True else None)
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def get_public_url(self, filepath: str) -> Optional[str]:
|
|
29
|
+
"""
|
|
30
|
+
Get the public URL for a file.
|
|
31
|
+
|
|
32
|
+
:param filepath: The path of the file
|
|
33
|
+
:return: Public URL if the file exists and is public, None otherwise
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def retrieve_file(self, filepath: str) -> Optional[BytesIO]:
|
|
39
|
+
"""
|
|
40
|
+
Retrieve a file from the storage.
|
|
41
|
+
|
|
42
|
+
:param filepath: The path of the file to retrieve
|
|
43
|
+
:return: BytesIO object containing the file data if found, None otherwise
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def delete_file(self, filepath: str) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Delete a file from the storage.
|
|
51
|
+
|
|
52
|
+
:param filepath: The path of the file to delete
|
|
53
|
+
:return: True if the file was successfully deleted, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def file_exists(self, filepath: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if a file exists in the storage.
|
|
61
|
+
|
|
62
|
+
:param filepath: The path of the file to check
|
|
63
|
+
:return: True if the file exists, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from botrun_flow_lang.services.user_setting.user_setting_fs_store import (
|
|
3
|
-
UserSettingFsStore,
|
|
4
|
-
)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def user_setting_store_factory():
|
|
8
|
-
env_name = os.getenv("ENV_NAME", "dev")
|
|
9
|
-
return UserSettingFsStore(env_name)
|
|
1
|
+
import os
|
|
2
|
+
from botrun_flow_lang.services.user_setting.user_setting_fs_store import (
|
|
3
|
+
UserSettingFsStore,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def user_setting_store_factory():
|
|
8
|
+
env_name = os.getenv("ENV_NAME", "dev")
|
|
9
|
+
return UserSettingFsStore(env_name)
|