botrun-flow-lang 5.12.263__py3-none-any.whl → 6.2.21__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 +508 -508
- botrun_flow_lang/api/langgraph_api.py +816 -811
- botrun_flow_lang/api/langgraph_constants.py +11 -0
- botrun_flow_lang/api/line_bot_api.py +1484 -1484
- 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 +395 -395
- 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 +178 -178
- 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/gemini_subsidy_graph.py +460 -460
- 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 +730 -723
- 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 +336 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
- 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 +562 -486
- botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
- botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
- 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/usage_metadata.py +34 -0
- 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 +854 -744
- 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 +419 -419
- botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
- 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.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/METADATA +6 -6
- botrun_flow_lang-6.2.21.dist-info/RECORD +104 -0
- botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/WHEEL +0 -0
|
@@ -1,508 +1,508 @@
|
|
|
1
|
-
from fastapi import APIRouter, HTTPException, Depends, Query, Body
|
|
2
|
-
import logging
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
|
|
5
|
-
from botrun_flow_lang.api.auth_utils import (
|
|
6
|
-
CurrentUser,
|
|
7
|
-
verify_admin_permission,
|
|
8
|
-
verify_hatch_access,
|
|
9
|
-
verify_hatch_owner,
|
|
10
|
-
verify_jwt_token,
|
|
11
|
-
verify_user_permission,
|
|
12
|
-
)
|
|
13
|
-
from botrun_flow_lang.api.user_setting_api import get_user_setting_store
|
|
14
|
-
|
|
15
|
-
from botrun_flow_lang.services.hatch.hatch_factory import hatch_store_factory
|
|
16
|
-
|
|
17
|
-
from botrun_flow_lang.services.hatch.hatch_fs_store import HatchFsStore
|
|
18
|
-
|
|
19
|
-
from botrun_hatch.models.hatch import Hatch
|
|
20
|
-
|
|
21
|
-
from typing import List
|
|
22
|
-
from pydantic import BaseModel
|
|
23
|
-
|
|
24
|
-
from botrun_flow_lang.services.user_setting.user_setting_fs_store import (
|
|
25
|
-
UserSettingFsStore,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
from botrun_flow_lang.utils.google_drive_utils import fetch_google_doc_content
|
|
29
|
-
|
|
30
|
-
router = APIRouter()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class HatchResponse(BaseModel):
|
|
34
|
-
hatch: Hatch
|
|
35
|
-
gdoc_update_success: bool = False
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async def get_hatch_store():
|
|
39
|
-
return hatch_store_factory()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@router.post("/hatch", response_model=HatchResponse)
|
|
43
|
-
async def create_hatch(
|
|
44
|
-
hatch: Hatch,
|
|
45
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
46
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
47
|
-
):
|
|
48
|
-
# Verify user permission to create hatch for the specified user_id
|
|
49
|
-
verify_user_permission(current_user, hatch.user_id)
|
|
50
|
-
|
|
51
|
-
hatch.last_sync_gdoc_success = False
|
|
52
|
-
# Process Google Doc logic if enabled
|
|
53
|
-
if (
|
|
54
|
-
hatch.enable_google_doc_link
|
|
55
|
-
and hatch.google_doc_link
|
|
56
|
-
and hatch.google_doc_link.strip()
|
|
57
|
-
):
|
|
58
|
-
logging.info(
|
|
59
|
-
f"Processing Google Doc link for hatch {hatch.id}: {hatch.google_doc_link}"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# Fetch content from Google Doc
|
|
63
|
-
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
64
|
-
|
|
65
|
-
if fetched_content:
|
|
66
|
-
# Update prompt_template with fetched content
|
|
67
|
-
hatch.prompt_template = fetched_content
|
|
68
|
-
# Update last_sync_gdoc_time with current UTC time
|
|
69
|
-
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
70
|
-
hatch.last_sync_gdoc_success = True
|
|
71
|
-
logging.info(
|
|
72
|
-
f"Successfully updated prompt_template for hatch {hatch.id} from Google Doc"
|
|
73
|
-
)
|
|
74
|
-
else:
|
|
75
|
-
# Log warning but continue with the operation
|
|
76
|
-
logging.warning(
|
|
77
|
-
f"Failed to fetch Google Doc content for hatch {hatch.id}, keeping existing prompt_template"
|
|
78
|
-
)
|
|
79
|
-
else:
|
|
80
|
-
# If Google Doc link is disabled, clear last_sync_gdoc_time
|
|
81
|
-
if not hatch.enable_google_doc_link:
|
|
82
|
-
hatch.last_sync_gdoc_time = ""
|
|
83
|
-
|
|
84
|
-
# Save to Firestore
|
|
85
|
-
success, created_hatch = await store.set_hatch(hatch)
|
|
86
|
-
if not success:
|
|
87
|
-
raise HTTPException(status_code=500, detail="Failed to create hatch")
|
|
88
|
-
|
|
89
|
-
return HatchResponse(
|
|
90
|
-
hatch=created_hatch, gdoc_update_success=created_hatch.last_sync_gdoc_success
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@router.put("/hatch/{hatch_id}", response_model=HatchResponse)
|
|
95
|
-
async def update_hatch(
|
|
96
|
-
hatch_id: str,
|
|
97
|
-
hatch: Hatch,
|
|
98
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
99
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
100
|
-
):
|
|
101
|
-
# Verify user is owner of the hatch
|
|
102
|
-
await verify_hatch_owner(current_user, hatch_id, store)
|
|
103
|
-
|
|
104
|
-
existing_hatch = await store.get_hatch(hatch_id)
|
|
105
|
-
if not existing_hatch:
|
|
106
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
107
|
-
|
|
108
|
-
hatch.id = hatch_id
|
|
109
|
-
hatch.last_sync_gdoc_success = False
|
|
110
|
-
|
|
111
|
-
# Process Google Doc logic if enabled
|
|
112
|
-
if (
|
|
113
|
-
hatch.enable_google_doc_link
|
|
114
|
-
and hatch.google_doc_link
|
|
115
|
-
and hatch.google_doc_link.strip()
|
|
116
|
-
):
|
|
117
|
-
logging.info(
|
|
118
|
-
f"Processing Google Doc link for hatch {hatch.id}: {hatch.google_doc_link}"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
# Fetch content from Google Doc
|
|
122
|
-
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
123
|
-
|
|
124
|
-
if fetched_content:
|
|
125
|
-
# Update prompt_template with fetched content
|
|
126
|
-
hatch.prompt_template = fetched_content
|
|
127
|
-
# Update last_sync_gdoc_time with current UTC time
|
|
128
|
-
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
129
|
-
hatch.last_sync_gdoc_success = True
|
|
130
|
-
logging.info(
|
|
131
|
-
f"Successfully updated prompt_template for hatch {hatch.id} from Google Doc"
|
|
132
|
-
)
|
|
133
|
-
else:
|
|
134
|
-
# Log warning but continue with the operation
|
|
135
|
-
# Keep existing last_sync_gdoc_time from the original hatch
|
|
136
|
-
hatch.last_sync_gdoc_time = existing_hatch.last_sync_gdoc_time
|
|
137
|
-
logging.warning(
|
|
138
|
-
f"Failed to fetch Google Doc content for hatch {hatch.id}, keeping existing prompt_template and last_sync_gdoc_time"
|
|
139
|
-
)
|
|
140
|
-
else:
|
|
141
|
-
# If Google Doc link is disabled, clear last_sync_gdoc_time
|
|
142
|
-
if not hatch.enable_google_doc_link:
|
|
143
|
-
hatch.last_sync_gdoc_time = ""
|
|
144
|
-
|
|
145
|
-
# Save to Firestore
|
|
146
|
-
success, updated_hatch = await store.set_hatch(hatch)
|
|
147
|
-
if not success:
|
|
148
|
-
raise HTTPException(status_code=500, detail="Failed to update hatch")
|
|
149
|
-
|
|
150
|
-
return HatchResponse(
|
|
151
|
-
hatch=updated_hatch, gdoc_update_success=updated_hatch.last_sync_gdoc_success
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@router.delete("/hatch/{hatch_id}")
|
|
156
|
-
async def delete_hatch(
|
|
157
|
-
hatch_id: str,
|
|
158
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
159
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
160
|
-
):
|
|
161
|
-
# Verify user is owner of the hatch
|
|
162
|
-
await verify_hatch_owner(current_user, hatch_id, store)
|
|
163
|
-
|
|
164
|
-
# Get the hatch to verify it exists
|
|
165
|
-
hatch = await store.get_hatch(hatch_id)
|
|
166
|
-
if not hatch:
|
|
167
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
168
|
-
|
|
169
|
-
# Delete all sharing relationships for this hatch
|
|
170
|
-
sharing_success, sharing_message = await store.delete_all_hatch_sharing(hatch_id)
|
|
171
|
-
if not sharing_success:
|
|
172
|
-
raise HTTPException(
|
|
173
|
-
status_code=500,
|
|
174
|
-
detail=f"Failed to delete hatch sharing relationships: {sharing_message}",
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# Delete the hatch itself
|
|
178
|
-
success = await store.delete_hatch(hatch_id)
|
|
179
|
-
if not success:
|
|
180
|
-
raise HTTPException(
|
|
181
|
-
status_code=500,
|
|
182
|
-
detail={"success": False, "message": f"Failed to delete hatch {hatch_id}"},
|
|
183
|
-
)
|
|
184
|
-
return {
|
|
185
|
-
"success": True,
|
|
186
|
-
"message": f"Hatch {hatch_id} and all sharing relationships deleted successfully",
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@router.get("/hatch/{hatch_id}", response_model=Hatch)
|
|
191
|
-
async def get_hatch(
|
|
192
|
-
hatch_id: str,
|
|
193
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
194
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
195
|
-
):
|
|
196
|
-
# Verify user has access to the hatch (owner or shared)
|
|
197
|
-
await verify_hatch_access(current_user, hatch_id, store)
|
|
198
|
-
|
|
199
|
-
hatch = await store.get_hatch(hatch_id)
|
|
200
|
-
if not hatch:
|
|
201
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
202
|
-
return hatch
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@router.post("/hatch/{hatch_id}/reload-template")
|
|
206
|
-
async def reload_template_from_doc(
|
|
207
|
-
hatch_id: str,
|
|
208
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
209
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
210
|
-
):
|
|
211
|
-
"""
|
|
212
|
-
Reload prompt_template from linked Google Doc.
|
|
213
|
-
|
|
214
|
-
This endpoint fetches the latest content from the Google Doc specified
|
|
215
|
-
in the hatch's google_doc_link field and updates the prompt_template.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
hatch_id: ID of the hatch to reload template for
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
dict: Success status and message
|
|
222
|
-
|
|
223
|
-
Raises:
|
|
224
|
-
HTTPException:
|
|
225
|
-
- 404 if hatch not found
|
|
226
|
-
- 400 if Google Doc link feature is not enabled or no link configured
|
|
227
|
-
- 500 if failed to fetch content or save hatch
|
|
228
|
-
"""
|
|
229
|
-
try:
|
|
230
|
-
# Verify user is owner of the hatch
|
|
231
|
-
await verify_hatch_owner(current_user, hatch_id, store)
|
|
232
|
-
|
|
233
|
-
# 1. Get the hatch
|
|
234
|
-
hatch = await store.get_hatch(hatch_id)
|
|
235
|
-
if not hatch:
|
|
236
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
237
|
-
|
|
238
|
-
# 2. Check Google Doc configuration
|
|
239
|
-
if not hatch.enable_google_doc_link:
|
|
240
|
-
raise HTTPException(
|
|
241
|
-
status_code=400,
|
|
242
|
-
detail="Google Doc link feature is not enabled for this Hatch",
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
if not hatch.google_doc_link or not hatch.google_doc_link.strip():
|
|
246
|
-
raise HTTPException(
|
|
247
|
-
status_code=400, detail="No Google Doc link configured for this Hatch"
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
# 3. Fetch content from Google Doc
|
|
251
|
-
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
252
|
-
|
|
253
|
-
if not fetched_content:
|
|
254
|
-
raise HTTPException(
|
|
255
|
-
status_code=500, detail="Failed to fetch content from Google Doc"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# 4. Update and save hatch
|
|
259
|
-
hatch.prompt_template = fetched_content
|
|
260
|
-
# Update last_sync_gdoc_time with current UTC time
|
|
261
|
-
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
262
|
-
success, _ = await store.set_hatch(hatch)
|
|
263
|
-
|
|
264
|
-
if not success:
|
|
265
|
-
raise HTTPException(
|
|
266
|
-
status_code=500, detail="Failed to save Hatch after reloading template"
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
"success": True,
|
|
271
|
-
"message": "Prompt template successfully reloaded from Google Doc",
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
except HTTPException:
|
|
275
|
-
# Re-raise HTTPException as-is
|
|
276
|
-
raise
|
|
277
|
-
except Exception as e:
|
|
278
|
-
logging.error(f"Error reloading prompt template for hatch {hatch_id}: {e}")
|
|
279
|
-
raise HTTPException(
|
|
280
|
-
status_code=500, detail=f"An error occurred while reloading: {str(e)}"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
@router.get("/hatches", response_model=List[Hatch])
|
|
285
|
-
async def get_hatches(
|
|
286
|
-
user_id: str,
|
|
287
|
-
offset: int = Query(0, ge=0),
|
|
288
|
-
limit: int = Query(20, ge=1, le=100),
|
|
289
|
-
sort_by: str = Query("updated_at", description="Field to sort by (name, updated_at)"),
|
|
290
|
-
order: str = Query("desc", regex="^(asc|desc)$", description="Sort order: asc or desc"),
|
|
291
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
292
|
-
hatch_store=Depends(get_hatch_store),
|
|
293
|
-
):
|
|
294
|
-
"""Get hatches for a user with sorting options.
|
|
295
|
-
|
|
296
|
-
Args:
|
|
297
|
-
user_id: User ID to get hatches for
|
|
298
|
-
offset: Pagination offset
|
|
299
|
-
limit: Maximum number of results (1-100)
|
|
300
|
-
sort_by: Field to sort by - only 'name' or 'updated_at' are supported (default: updated_at)
|
|
301
|
-
order: Sort order - 'asc' or 'desc' (default: desc for newest first)
|
|
302
|
-
|
|
303
|
-
Returns:
|
|
304
|
-
List of hatches sorted by the specified field
|
|
305
|
-
|
|
306
|
-
Raises:
|
|
307
|
-
HTTPException: 400 if sort_by field is not supported
|
|
308
|
-
"""
|
|
309
|
-
# Verify user permission to access hatches for the specified user_id
|
|
310
|
-
verify_user_permission(current_user, user_id)
|
|
311
|
-
|
|
312
|
-
# Validate sort_by field - only allow fields with Firestore indexes
|
|
313
|
-
allowed_sort_fields = ["name", "updated_at"]
|
|
314
|
-
if sort_by not in allowed_sort_fields:
|
|
315
|
-
raise HTTPException(
|
|
316
|
-
status_code=400,
|
|
317
|
-
detail=f"Invalid sort_by field '{sort_by}'. Allowed fields: {', '.join(allowed_sort_fields)}",
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
hatches, error = await hatch_store.get_hatches(
|
|
321
|
-
user_id, offset, limit, sort_by, order
|
|
322
|
-
)
|
|
323
|
-
if error:
|
|
324
|
-
raise HTTPException(status_code=500, detail=error)
|
|
325
|
-
return hatches
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
@router.get("/hatch/default/{user_id}", response_model=Hatch)
|
|
329
|
-
async def get_default_hatch(
|
|
330
|
-
user_id: str,
|
|
331
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
332
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
333
|
-
):
|
|
334
|
-
# Verify user permission to access default hatch for the specified user_id
|
|
335
|
-
verify_user_permission(current_user, user_id)
|
|
336
|
-
|
|
337
|
-
default_hatch = await store.get_default_hatch(user_id)
|
|
338
|
-
if not default_hatch:
|
|
339
|
-
raise HTTPException(status_code=404, detail="Default hatch not found")
|
|
340
|
-
return default_hatch
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
@router.post("/hatch/set_default")
|
|
344
|
-
async def set_default_hatch(
|
|
345
|
-
user_id: str = Body(...),
|
|
346
|
-
hatch_id: str = Body(...),
|
|
347
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
348
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
349
|
-
):
|
|
350
|
-
# Verify user permission to set default hatch for the specified user_id
|
|
351
|
-
verify_user_permission(current_user, user_id)
|
|
352
|
-
|
|
353
|
-
success, message = await store.set_default_hatch(user_id, hatch_id)
|
|
354
|
-
if not success:
|
|
355
|
-
raise HTTPException(status_code=500, detail=message)
|
|
356
|
-
return {"success": True, "message": message}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
@router.get("/hatches/statistics")
|
|
360
|
-
async def get_hatches_statistics(
|
|
361
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
362
|
-
user_setting_store: UserSettingFsStore = Depends(get_user_setting_store),
|
|
363
|
-
hatch_store: HatchFsStore = Depends(get_hatch_store),
|
|
364
|
-
):
|
|
365
|
-
"""Get statistics about hatches across all users.
|
|
366
|
-
|
|
367
|
-
Returns:
|
|
368
|
-
dict: Contains total hatch count and per-user hatch counts
|
|
369
|
-
"""
|
|
370
|
-
# Verify admin permission
|
|
371
|
-
verify_admin_permission(current_user)
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
# Get all user IDs
|
|
375
|
-
user_ids = await user_setting_store.get_all_user_ids()
|
|
376
|
-
|
|
377
|
-
# Initialize statistics
|
|
378
|
-
all_hatches = []
|
|
379
|
-
total_count = 0
|
|
380
|
-
|
|
381
|
-
# Get hatch counts for each user
|
|
382
|
-
for user_id in user_ids:
|
|
383
|
-
hatches, _ = await hatch_store.get_hatches(user_id)
|
|
384
|
-
count = len(hatches)
|
|
385
|
-
if count > 0: # Only include users who have hatches
|
|
386
|
-
all_hatches.append({"user_id": user_id, "hatches_count": count})
|
|
387
|
-
total_count += count
|
|
388
|
-
|
|
389
|
-
return {"all_hatches_count": total_count, "all_hatches": all_hatches}
|
|
390
|
-
except Exception as e:
|
|
391
|
-
raise HTTPException(
|
|
392
|
-
status_code=500, detail=f"Failed to fetch hatch statistics: {str(e)}"
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
@router.post("/hatch/{hatch_id}/share")
|
|
397
|
-
async def share_hatch(
|
|
398
|
-
hatch_id: str,
|
|
399
|
-
user_id: str = Body(..., embed=True),
|
|
400
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
401
|
-
):
|
|
402
|
-
"""Share a hatch with another user.
|
|
403
|
-
|
|
404
|
-
Args:
|
|
405
|
-
hatch_id: ID of the hatch to share
|
|
406
|
-
user_id: ID of the user to share the hatch with (in request body)
|
|
407
|
-
|
|
408
|
-
Returns:
|
|
409
|
-
dict: Success status and message
|
|
410
|
-
"""
|
|
411
|
-
|
|
412
|
-
# Get the hatch to verify it exists and to get owner_id
|
|
413
|
-
hatch = await store.get_hatch(hatch_id)
|
|
414
|
-
if not hatch:
|
|
415
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
416
|
-
|
|
417
|
-
# Share the hatch
|
|
418
|
-
success, message = await store.share_hatch(hatch_id, hatch.user_id, user_id)
|
|
419
|
-
if not success:
|
|
420
|
-
raise HTTPException(status_code=400, detail=message)
|
|
421
|
-
|
|
422
|
-
return {"success": True, "message": message}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
@router.delete("/hatch/{hatch_id}/share/{target_user_id}")
|
|
426
|
-
async def unshare_hatch(
|
|
427
|
-
hatch_id: str,
|
|
428
|
-
target_user_id: str,
|
|
429
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
430
|
-
):
|
|
431
|
-
"""Remove sharing of a hatch with a user.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
hatch_id: ID of the hatch to unshare
|
|
435
|
-
target_user_id: ID of the user to remove sharing from
|
|
436
|
-
|
|
437
|
-
Returns:
|
|
438
|
-
dict: Success status and message
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
|
-
# Get the hatch to verify it exists and to get owner_id
|
|
442
|
-
hatch = await store.get_hatch(hatch_id)
|
|
443
|
-
if not hatch:
|
|
444
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
445
|
-
|
|
446
|
-
# Unshare the hatch
|
|
447
|
-
success, message = await store.unshare_hatch(
|
|
448
|
-
hatch_id, hatch.user_id, target_user_id
|
|
449
|
-
)
|
|
450
|
-
if not success:
|
|
451
|
-
raise HTTPException(status_code=400, detail=message)
|
|
452
|
-
|
|
453
|
-
return {"success": True, "message": message}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
@router.get("/hatches/shared", response_model=List[Hatch])
|
|
457
|
-
async def get_shared_hatches(
|
|
458
|
-
user_id: str,
|
|
459
|
-
offset: int = Query(0, ge=0),
|
|
460
|
-
limit: int = Query(20, ge=1, le=100),
|
|
461
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
462
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
463
|
-
):
|
|
464
|
-
"""Get all hatches shared with a user.
|
|
465
|
-
|
|
466
|
-
Args:
|
|
467
|
-
user_id: ID of the user to get shared hatches for
|
|
468
|
-
offset: Pagination offset
|
|
469
|
-
limit: Maximum number of results to return
|
|
470
|
-
|
|
471
|
-
Returns:
|
|
472
|
-
List[Hatch]: List of shared hatches
|
|
473
|
-
"""
|
|
474
|
-
# Verify user permission to access shared hatches for the specified user_id
|
|
475
|
-
verify_user_permission(current_user, user_id)
|
|
476
|
-
|
|
477
|
-
hatches, error = await store.get_shared_hatches(user_id, offset, limit)
|
|
478
|
-
if error:
|
|
479
|
-
raise HTTPException(status_code=500, detail=error)
|
|
480
|
-
|
|
481
|
-
return hatches
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
@router.get("/hatch/{hatch_id}/share/{user_id}")
|
|
485
|
-
async def is_hatch_shared_with_user(
|
|
486
|
-
hatch_id: str,
|
|
487
|
-
user_id: str,
|
|
488
|
-
store: HatchFsStore = Depends(get_hatch_store),
|
|
489
|
-
):
|
|
490
|
-
"""Check if a hatch is shared with a specific user.
|
|
491
|
-
|
|
492
|
-
Args:
|
|
493
|
-
hatch_id: ID of the hatch to check
|
|
494
|
-
user_id: ID of the user to check sharing with
|
|
495
|
-
|
|
496
|
-
Returns:
|
|
497
|
-
dict: Whether the hatch is shared with the user and a message
|
|
498
|
-
"""
|
|
499
|
-
|
|
500
|
-
# Get the hatch to verify it exists
|
|
501
|
-
hatch = await store.get_hatch(hatch_id)
|
|
502
|
-
if not hatch:
|
|
503
|
-
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
504
|
-
|
|
505
|
-
# Check if the hatch is shared with the user
|
|
506
|
-
is_shared, message = await store.is_hatch_shared_with_user(hatch_id, user_id)
|
|
507
|
-
|
|
508
|
-
return {"is_shared": is_shared, "message": message}
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Depends, Query, Body
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from botrun_flow_lang.api.auth_utils import (
|
|
6
|
+
CurrentUser,
|
|
7
|
+
verify_admin_permission,
|
|
8
|
+
verify_hatch_access,
|
|
9
|
+
verify_hatch_owner,
|
|
10
|
+
verify_jwt_token,
|
|
11
|
+
verify_user_permission,
|
|
12
|
+
)
|
|
13
|
+
from botrun_flow_lang.api.user_setting_api import get_user_setting_store
|
|
14
|
+
|
|
15
|
+
from botrun_flow_lang.services.hatch.hatch_factory import hatch_store_factory
|
|
16
|
+
|
|
17
|
+
from botrun_flow_lang.services.hatch.hatch_fs_store import HatchFsStore
|
|
18
|
+
|
|
19
|
+
from botrun_hatch.models.hatch import Hatch
|
|
20
|
+
|
|
21
|
+
from typing import List
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from botrun_flow_lang.services.user_setting.user_setting_fs_store import (
|
|
25
|
+
UserSettingFsStore,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from botrun_flow_lang.utils.google_drive_utils import fetch_google_doc_content
|
|
29
|
+
|
|
30
|
+
router = APIRouter()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HatchResponse(BaseModel):
|
|
34
|
+
hatch: Hatch
|
|
35
|
+
gdoc_update_success: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def get_hatch_store():
|
|
39
|
+
return hatch_store_factory()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post("/hatch", response_model=HatchResponse)
|
|
43
|
+
async def create_hatch(
|
|
44
|
+
hatch: Hatch,
|
|
45
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
46
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
47
|
+
):
|
|
48
|
+
# Verify user permission to create hatch for the specified user_id
|
|
49
|
+
verify_user_permission(current_user, hatch.user_id)
|
|
50
|
+
|
|
51
|
+
hatch.last_sync_gdoc_success = False
|
|
52
|
+
# Process Google Doc logic if enabled
|
|
53
|
+
if (
|
|
54
|
+
hatch.enable_google_doc_link
|
|
55
|
+
and hatch.google_doc_link
|
|
56
|
+
and hatch.google_doc_link.strip()
|
|
57
|
+
):
|
|
58
|
+
logging.info(
|
|
59
|
+
f"Processing Google Doc link for hatch {hatch.id}: {hatch.google_doc_link}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Fetch content from Google Doc
|
|
63
|
+
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
64
|
+
|
|
65
|
+
if fetched_content:
|
|
66
|
+
# Update prompt_template with fetched content
|
|
67
|
+
hatch.prompt_template = fetched_content
|
|
68
|
+
# Update last_sync_gdoc_time with current UTC time
|
|
69
|
+
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
70
|
+
hatch.last_sync_gdoc_success = True
|
|
71
|
+
logging.info(
|
|
72
|
+
f"Successfully updated prompt_template for hatch {hatch.id} from Google Doc"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
# Log warning but continue with the operation
|
|
76
|
+
logging.warning(
|
|
77
|
+
f"Failed to fetch Google Doc content for hatch {hatch.id}, keeping existing prompt_template"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# If Google Doc link is disabled, clear last_sync_gdoc_time
|
|
81
|
+
if not hatch.enable_google_doc_link:
|
|
82
|
+
hatch.last_sync_gdoc_time = ""
|
|
83
|
+
|
|
84
|
+
# Save to Firestore
|
|
85
|
+
success, created_hatch = await store.set_hatch(hatch)
|
|
86
|
+
if not success:
|
|
87
|
+
raise HTTPException(status_code=500, detail="Failed to create hatch")
|
|
88
|
+
|
|
89
|
+
return HatchResponse(
|
|
90
|
+
hatch=created_hatch, gdoc_update_success=created_hatch.last_sync_gdoc_success
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.put("/hatch/{hatch_id}", response_model=HatchResponse)
|
|
95
|
+
async def update_hatch(
|
|
96
|
+
hatch_id: str,
|
|
97
|
+
hatch: Hatch,
|
|
98
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
99
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
100
|
+
):
|
|
101
|
+
# Verify user is owner of the hatch
|
|
102
|
+
await verify_hatch_owner(current_user, hatch_id, store)
|
|
103
|
+
|
|
104
|
+
existing_hatch = await store.get_hatch(hatch_id)
|
|
105
|
+
if not existing_hatch:
|
|
106
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
107
|
+
|
|
108
|
+
hatch.id = hatch_id
|
|
109
|
+
hatch.last_sync_gdoc_success = False
|
|
110
|
+
|
|
111
|
+
# Process Google Doc logic if enabled
|
|
112
|
+
if (
|
|
113
|
+
hatch.enable_google_doc_link
|
|
114
|
+
and hatch.google_doc_link
|
|
115
|
+
and hatch.google_doc_link.strip()
|
|
116
|
+
):
|
|
117
|
+
logging.info(
|
|
118
|
+
f"Processing Google Doc link for hatch {hatch.id}: {hatch.google_doc_link}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Fetch content from Google Doc
|
|
122
|
+
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
123
|
+
|
|
124
|
+
if fetched_content:
|
|
125
|
+
# Update prompt_template with fetched content
|
|
126
|
+
hatch.prompt_template = fetched_content
|
|
127
|
+
# Update last_sync_gdoc_time with current UTC time
|
|
128
|
+
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
129
|
+
hatch.last_sync_gdoc_success = True
|
|
130
|
+
logging.info(
|
|
131
|
+
f"Successfully updated prompt_template for hatch {hatch.id} from Google Doc"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
# Log warning but continue with the operation
|
|
135
|
+
# Keep existing last_sync_gdoc_time from the original hatch
|
|
136
|
+
hatch.last_sync_gdoc_time = existing_hatch.last_sync_gdoc_time
|
|
137
|
+
logging.warning(
|
|
138
|
+
f"Failed to fetch Google Doc content for hatch {hatch.id}, keeping existing prompt_template and last_sync_gdoc_time"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# If Google Doc link is disabled, clear last_sync_gdoc_time
|
|
142
|
+
if not hatch.enable_google_doc_link:
|
|
143
|
+
hatch.last_sync_gdoc_time = ""
|
|
144
|
+
|
|
145
|
+
# Save to Firestore
|
|
146
|
+
success, updated_hatch = await store.set_hatch(hatch)
|
|
147
|
+
if not success:
|
|
148
|
+
raise HTTPException(status_code=500, detail="Failed to update hatch")
|
|
149
|
+
|
|
150
|
+
return HatchResponse(
|
|
151
|
+
hatch=updated_hatch, gdoc_update_success=updated_hatch.last_sync_gdoc_success
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@router.delete("/hatch/{hatch_id}")
|
|
156
|
+
async def delete_hatch(
|
|
157
|
+
hatch_id: str,
|
|
158
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
159
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
160
|
+
):
|
|
161
|
+
# Verify user is owner of the hatch
|
|
162
|
+
await verify_hatch_owner(current_user, hatch_id, store)
|
|
163
|
+
|
|
164
|
+
# Get the hatch to verify it exists
|
|
165
|
+
hatch = await store.get_hatch(hatch_id)
|
|
166
|
+
if not hatch:
|
|
167
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
168
|
+
|
|
169
|
+
# Delete all sharing relationships for this hatch
|
|
170
|
+
sharing_success, sharing_message = await store.delete_all_hatch_sharing(hatch_id)
|
|
171
|
+
if not sharing_success:
|
|
172
|
+
raise HTTPException(
|
|
173
|
+
status_code=500,
|
|
174
|
+
detail=f"Failed to delete hatch sharing relationships: {sharing_message}",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Delete the hatch itself
|
|
178
|
+
success = await store.delete_hatch(hatch_id)
|
|
179
|
+
if not success:
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=500,
|
|
182
|
+
detail={"success": False, "message": f"Failed to delete hatch {hatch_id}"},
|
|
183
|
+
)
|
|
184
|
+
return {
|
|
185
|
+
"success": True,
|
|
186
|
+
"message": f"Hatch {hatch_id} and all sharing relationships deleted successfully",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.get("/hatch/{hatch_id}", response_model=Hatch)
|
|
191
|
+
async def get_hatch(
|
|
192
|
+
hatch_id: str,
|
|
193
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
194
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
195
|
+
):
|
|
196
|
+
# Verify user has access to the hatch (owner or shared)
|
|
197
|
+
await verify_hatch_access(current_user, hatch_id, store)
|
|
198
|
+
|
|
199
|
+
hatch = await store.get_hatch(hatch_id)
|
|
200
|
+
if not hatch:
|
|
201
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
202
|
+
return hatch
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@router.post("/hatch/{hatch_id}/reload-template")
|
|
206
|
+
async def reload_template_from_doc(
|
|
207
|
+
hatch_id: str,
|
|
208
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
209
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
210
|
+
):
|
|
211
|
+
"""
|
|
212
|
+
Reload prompt_template from linked Google Doc.
|
|
213
|
+
|
|
214
|
+
This endpoint fetches the latest content from the Google Doc specified
|
|
215
|
+
in the hatch's google_doc_link field and updates the prompt_template.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
hatch_id: ID of the hatch to reload template for
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
dict: Success status and message
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
HTTPException:
|
|
225
|
+
- 404 if hatch not found
|
|
226
|
+
- 400 if Google Doc link feature is not enabled or no link configured
|
|
227
|
+
- 500 if failed to fetch content or save hatch
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
# Verify user is owner of the hatch
|
|
231
|
+
await verify_hatch_owner(current_user, hatch_id, store)
|
|
232
|
+
|
|
233
|
+
# 1. Get the hatch
|
|
234
|
+
hatch = await store.get_hatch(hatch_id)
|
|
235
|
+
if not hatch:
|
|
236
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
237
|
+
|
|
238
|
+
# 2. Check Google Doc configuration
|
|
239
|
+
if not hatch.enable_google_doc_link:
|
|
240
|
+
raise HTTPException(
|
|
241
|
+
status_code=400,
|
|
242
|
+
detail="Google Doc link feature is not enabled for this Hatch",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if not hatch.google_doc_link or not hatch.google_doc_link.strip():
|
|
246
|
+
raise HTTPException(
|
|
247
|
+
status_code=400, detail="No Google Doc link configured for this Hatch"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# 3. Fetch content from Google Doc
|
|
251
|
+
fetched_content = await fetch_google_doc_content(hatch.google_doc_link.strip())
|
|
252
|
+
|
|
253
|
+
if not fetched_content:
|
|
254
|
+
raise HTTPException(
|
|
255
|
+
status_code=500, detail="Failed to fetch content from Google Doc"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# 4. Update and save hatch
|
|
259
|
+
hatch.prompt_template = fetched_content
|
|
260
|
+
# Update last_sync_gdoc_time with current UTC time
|
|
261
|
+
hatch.last_sync_gdoc_time = datetime.now(timezone.utc).isoformat()
|
|
262
|
+
success, _ = await store.set_hatch(hatch)
|
|
263
|
+
|
|
264
|
+
if not success:
|
|
265
|
+
raise HTTPException(
|
|
266
|
+
status_code=500, detail="Failed to save Hatch after reloading template"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"success": True,
|
|
271
|
+
"message": "Prompt template successfully reloaded from Google Doc",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
except HTTPException:
|
|
275
|
+
# Re-raise HTTPException as-is
|
|
276
|
+
raise
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logging.error(f"Error reloading prompt template for hatch {hatch_id}: {e}")
|
|
279
|
+
raise HTTPException(
|
|
280
|
+
status_code=500, detail=f"An error occurred while reloading: {str(e)}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@router.get("/hatches", response_model=List[Hatch])
|
|
285
|
+
async def get_hatches(
|
|
286
|
+
user_id: str,
|
|
287
|
+
offset: int = Query(0, ge=0),
|
|
288
|
+
limit: int = Query(20, ge=1, le=100),
|
|
289
|
+
sort_by: str = Query("updated_at", description="Field to sort by (name, updated_at)"),
|
|
290
|
+
order: str = Query("desc", regex="^(asc|desc)$", description="Sort order: asc or desc"),
|
|
291
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
292
|
+
hatch_store=Depends(get_hatch_store),
|
|
293
|
+
):
|
|
294
|
+
"""Get hatches for a user with sorting options.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
user_id: User ID to get hatches for
|
|
298
|
+
offset: Pagination offset
|
|
299
|
+
limit: Maximum number of results (1-100)
|
|
300
|
+
sort_by: Field to sort by - only 'name' or 'updated_at' are supported (default: updated_at)
|
|
301
|
+
order: Sort order - 'asc' or 'desc' (default: desc for newest first)
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of hatches sorted by the specified field
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
HTTPException: 400 if sort_by field is not supported
|
|
308
|
+
"""
|
|
309
|
+
# Verify user permission to access hatches for the specified user_id
|
|
310
|
+
verify_user_permission(current_user, user_id)
|
|
311
|
+
|
|
312
|
+
# Validate sort_by field - only allow fields with Firestore indexes
|
|
313
|
+
allowed_sort_fields = ["name", "updated_at"]
|
|
314
|
+
if sort_by not in allowed_sort_fields:
|
|
315
|
+
raise HTTPException(
|
|
316
|
+
status_code=400,
|
|
317
|
+
detail=f"Invalid sort_by field '{sort_by}'. Allowed fields: {', '.join(allowed_sort_fields)}",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
hatches, error = await hatch_store.get_hatches(
|
|
321
|
+
user_id, offset, limit, sort_by, order
|
|
322
|
+
)
|
|
323
|
+
if error:
|
|
324
|
+
raise HTTPException(status_code=500, detail=error)
|
|
325
|
+
return hatches
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@router.get("/hatch/default/{user_id}", response_model=Hatch)
|
|
329
|
+
async def get_default_hatch(
|
|
330
|
+
user_id: str,
|
|
331
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
332
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
333
|
+
):
|
|
334
|
+
# Verify user permission to access default hatch for the specified user_id
|
|
335
|
+
verify_user_permission(current_user, user_id)
|
|
336
|
+
|
|
337
|
+
default_hatch = await store.get_default_hatch(user_id)
|
|
338
|
+
if not default_hatch:
|
|
339
|
+
raise HTTPException(status_code=404, detail="Default hatch not found")
|
|
340
|
+
return default_hatch
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@router.post("/hatch/set_default")
|
|
344
|
+
async def set_default_hatch(
|
|
345
|
+
user_id: str = Body(...),
|
|
346
|
+
hatch_id: str = Body(...),
|
|
347
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
348
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
349
|
+
):
|
|
350
|
+
# Verify user permission to set default hatch for the specified user_id
|
|
351
|
+
verify_user_permission(current_user, user_id)
|
|
352
|
+
|
|
353
|
+
success, message = await store.set_default_hatch(user_id, hatch_id)
|
|
354
|
+
if not success:
|
|
355
|
+
raise HTTPException(status_code=500, detail=message)
|
|
356
|
+
return {"success": True, "message": message}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@router.get("/hatches/statistics")
|
|
360
|
+
async def get_hatches_statistics(
|
|
361
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
362
|
+
user_setting_store: UserSettingFsStore = Depends(get_user_setting_store),
|
|
363
|
+
hatch_store: HatchFsStore = Depends(get_hatch_store),
|
|
364
|
+
):
|
|
365
|
+
"""Get statistics about hatches across all users.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
dict: Contains total hatch count and per-user hatch counts
|
|
369
|
+
"""
|
|
370
|
+
# Verify admin permission
|
|
371
|
+
verify_admin_permission(current_user)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
# Get all user IDs
|
|
375
|
+
user_ids = await user_setting_store.get_all_user_ids()
|
|
376
|
+
|
|
377
|
+
# Initialize statistics
|
|
378
|
+
all_hatches = []
|
|
379
|
+
total_count = 0
|
|
380
|
+
|
|
381
|
+
# Get hatch counts for each user
|
|
382
|
+
for user_id in user_ids:
|
|
383
|
+
hatches, _ = await hatch_store.get_hatches(user_id)
|
|
384
|
+
count = len(hatches)
|
|
385
|
+
if count > 0: # Only include users who have hatches
|
|
386
|
+
all_hatches.append({"user_id": user_id, "hatches_count": count})
|
|
387
|
+
total_count += count
|
|
388
|
+
|
|
389
|
+
return {"all_hatches_count": total_count, "all_hatches": all_hatches}
|
|
390
|
+
except Exception as e:
|
|
391
|
+
raise HTTPException(
|
|
392
|
+
status_code=500, detail=f"Failed to fetch hatch statistics: {str(e)}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@router.post("/hatch/{hatch_id}/share")
|
|
397
|
+
async def share_hatch(
|
|
398
|
+
hatch_id: str,
|
|
399
|
+
user_id: str = Body(..., embed=True),
|
|
400
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
401
|
+
):
|
|
402
|
+
"""Share a hatch with another user.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
hatch_id: ID of the hatch to share
|
|
406
|
+
user_id: ID of the user to share the hatch with (in request body)
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
dict: Success status and message
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
# Get the hatch to verify it exists and to get owner_id
|
|
413
|
+
hatch = await store.get_hatch(hatch_id)
|
|
414
|
+
if not hatch:
|
|
415
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
416
|
+
|
|
417
|
+
# Share the hatch
|
|
418
|
+
success, message = await store.share_hatch(hatch_id, hatch.user_id, user_id)
|
|
419
|
+
if not success:
|
|
420
|
+
raise HTTPException(status_code=400, detail=message)
|
|
421
|
+
|
|
422
|
+
return {"success": True, "message": message}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@router.delete("/hatch/{hatch_id}/share/{target_user_id}")
|
|
426
|
+
async def unshare_hatch(
|
|
427
|
+
hatch_id: str,
|
|
428
|
+
target_user_id: str,
|
|
429
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
430
|
+
):
|
|
431
|
+
"""Remove sharing of a hatch with a user.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
hatch_id: ID of the hatch to unshare
|
|
435
|
+
target_user_id: ID of the user to remove sharing from
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
dict: Success status and message
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
# Get the hatch to verify it exists and to get owner_id
|
|
442
|
+
hatch = await store.get_hatch(hatch_id)
|
|
443
|
+
if not hatch:
|
|
444
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
445
|
+
|
|
446
|
+
# Unshare the hatch
|
|
447
|
+
success, message = await store.unshare_hatch(
|
|
448
|
+
hatch_id, hatch.user_id, target_user_id
|
|
449
|
+
)
|
|
450
|
+
if not success:
|
|
451
|
+
raise HTTPException(status_code=400, detail=message)
|
|
452
|
+
|
|
453
|
+
return {"success": True, "message": message}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@router.get("/hatches/shared", response_model=List[Hatch])
|
|
457
|
+
async def get_shared_hatches(
|
|
458
|
+
user_id: str,
|
|
459
|
+
offset: int = Query(0, ge=0),
|
|
460
|
+
limit: int = Query(20, ge=1, le=100),
|
|
461
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
462
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
463
|
+
):
|
|
464
|
+
"""Get all hatches shared with a user.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
user_id: ID of the user to get shared hatches for
|
|
468
|
+
offset: Pagination offset
|
|
469
|
+
limit: Maximum number of results to return
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List[Hatch]: List of shared hatches
|
|
473
|
+
"""
|
|
474
|
+
# Verify user permission to access shared hatches for the specified user_id
|
|
475
|
+
verify_user_permission(current_user, user_id)
|
|
476
|
+
|
|
477
|
+
hatches, error = await store.get_shared_hatches(user_id, offset, limit)
|
|
478
|
+
if error:
|
|
479
|
+
raise HTTPException(status_code=500, detail=error)
|
|
480
|
+
|
|
481
|
+
return hatches
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@router.get("/hatch/{hatch_id}/share/{user_id}")
|
|
485
|
+
async def is_hatch_shared_with_user(
|
|
486
|
+
hatch_id: str,
|
|
487
|
+
user_id: str,
|
|
488
|
+
store: HatchFsStore = Depends(get_hatch_store),
|
|
489
|
+
):
|
|
490
|
+
"""Check if a hatch is shared with a specific user.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
hatch_id: ID of the hatch to check
|
|
494
|
+
user_id: ID of the user to check sharing with
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
dict: Whether the hatch is shared with the user and a message
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
# Get the hatch to verify it exists
|
|
501
|
+
hatch = await store.get_hatch(hatch_id)
|
|
502
|
+
if not hatch:
|
|
503
|
+
raise HTTPException(status_code=404, detail="Hatch not found")
|
|
504
|
+
|
|
505
|
+
# Check if the hatch is shared with the user
|
|
506
|
+
is_shared, message = await store.is_hatch_shared_with_user(hatch_id, user_id)
|
|
507
|
+
|
|
508
|
+
return {"is_shared": is_shared, "message": message}
|