botrun-flow-lang 5.12.263__py3-none-any.whl → 5.12.264__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.
Files changed (87) hide show
  1. botrun_flow_lang/api/auth_api.py +39 -39
  2. botrun_flow_lang/api/auth_utils.py +183 -183
  3. botrun_flow_lang/api/botrun_back_api.py +65 -65
  4. botrun_flow_lang/api/flow_api.py +3 -3
  5. botrun_flow_lang/api/hatch_api.py +508 -508
  6. botrun_flow_lang/api/langgraph_api.py +811 -811
  7. botrun_flow_lang/api/line_bot_api.py +1484 -1484
  8. botrun_flow_lang/api/model_api.py +300 -300
  9. botrun_flow_lang/api/rate_limit_api.py +32 -32
  10. botrun_flow_lang/api/routes.py +79 -79
  11. botrun_flow_lang/api/search_api.py +53 -53
  12. botrun_flow_lang/api/storage_api.py +395 -395
  13. botrun_flow_lang/api/subsidy_api.py +290 -290
  14. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  15. botrun_flow_lang/api/user_setting_api.py +70 -70
  16. botrun_flow_lang/api/version_api.py +31 -31
  17. botrun_flow_lang/api/youtube_api.py +26 -26
  18. botrun_flow_lang/constants.py +13 -13
  19. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
  20. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  21. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  22. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  25. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  26. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
  27. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  28. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  29. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  30. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  31. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  32. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  33. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
  34. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  35. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  36. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
  37. botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
  38. botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
  39. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  40. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  41. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  42. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  43. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  44. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  45. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  46. botrun_flow_lang/log/.gitignore +2 -2
  47. botrun_flow_lang/main.py +61 -61
  48. botrun_flow_lang/main_fast.py +51 -51
  49. botrun_flow_lang/mcp_server/__init__.py +10 -10
  50. botrun_flow_lang/mcp_server/default_mcp.py +744 -744
  51. botrun_flow_lang/models/nodes/utils.py +205 -205
  52. botrun_flow_lang/models/token_usage.py +34 -34
  53. botrun_flow_lang/requirements.txt +21 -21
  54. botrun_flow_lang/services/base/firestore_base.py +30 -30
  55. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  56. botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
  57. botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
  58. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  59. botrun_flow_lang/services/storage/storage_store.py +65 -65
  60. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  61. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  62. botrun_flow_lang/static/docs/tools/index.html +926 -926
  63. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  64. botrun_flow_lang/tests/api_stress_test.py +357 -357
  65. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  66. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  67. botrun_flow_lang/tests/test_html_util.py +31 -31
  68. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  69. botrun_flow_lang/tests/test_img_util.py +39 -39
  70. botrun_flow_lang/tests/test_local_files.py +114 -114
  71. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  72. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  73. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  74. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  75. botrun_flow_lang/tools/generate_docs.py +133 -133
  76. botrun_flow_lang/tools/templates/tools.html +153 -153
  77. botrun_flow_lang/utils/__init__.py +7 -7
  78. botrun_flow_lang/utils/botrun_logger.py +344 -344
  79. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  80. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  81. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  82. botrun_flow_lang/utils/langchain_utils.py +324 -324
  83. botrun_flow_lang/utils/yaml_utils.py +9 -9
  84. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
  85. botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
  86. botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
  87. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/WHEEL +0 -0
@@ -1,333 +1,333 @@
1
- import unittest
2
- import requests
3
- import json
4
- import time
5
- import uuid
6
-
7
-
8
- class TestHatchSharing(unittest.TestCase):
9
- """Test class for Hatch sharing functionality tests"""
10
-
11
- def setUp(self):
12
- """Setup method that runs before each test"""
13
- # Default base URL, can be overridden by setting the class attribute
14
- if not hasattr(self, "base_url"):
15
- self.base_url = "http://localhost:8080"
16
-
17
- # Common headers
18
- self.headers = {"Content-Type": "application/json"}
19
-
20
- # Test data - create unique IDs for each test run
21
- self.owner_id = f"owner_{uuid.uuid4().hex[:8]}@example.com"
22
- self.shared_user_id = f"shared_{uuid.uuid4().hex[:8]}@example.com"
23
- self.hatch_id = f"test_hatch_{uuid.uuid4().hex[:8]}"
24
-
25
- # Create a test hatch for the owner
26
- self.create_test_hatch()
27
-
28
- def tearDown(self):
29
- """Cleanup method that runs after each test"""
30
- # Delete the test hatch
31
- self.delete_test_hatch()
32
-
33
- def create_test_hatch(self):
34
- """Helper method to create a test hatch"""
35
- url = f"{self.base_url}/api/hatch"
36
- payload = {
37
- "user_id": self.owner_id,
38
- "id": self.hatch_id,
39
- "model_name": "test-model",
40
- "prompt_template": "This is a test prompt template for sharing functionality testing",
41
- "name": "Test Hatch for Sharing Tests",
42
- }
43
-
44
- response = requests.post(url, headers=self.headers, json=payload)
45
- self.assertEqual(
46
- response.status_code, 200, f"Failed to create test hatch: {response.text}"
47
- )
48
- print(f"Created test hatch with ID: {self.hatch_id} for owner: {self.owner_id}")
49
- return response.json()
50
-
51
- def delete_test_hatch(self):
52
- """Helper method to delete the test hatch"""
53
- url = f"{self.base_url}/api/hatch/{self.hatch_id}"
54
- response = requests.delete(url, headers=self.headers)
55
- self.assertEqual(
56
- response.status_code, 200, f"Failed to delete test hatch: {response.text}"
57
- )
58
- print(f"Deleted test hatch with ID: {self.hatch_id}")
59
-
60
- def test_share_hatch_workflow(self):
61
- """Test the complete hatch sharing workflow:
62
- 1. Share a hatch with another user
63
- 2. Verify the hatch is shared
64
- 3. Unshare the hatch
65
- 4. Verify the hatch is no longer shared
66
- """
67
- # 1. Share the hatch
68
- self.share_hatch_with_user()
69
-
70
- # 2. Verify the hatch is shared
71
- shared_hatches = self.get_shared_hatches()
72
- self.assertEqual(len(shared_hatches), 1, "Expected exactly one shared hatch")
73
- self.assertEqual(
74
- shared_hatches[0]["id"], self.hatch_id, "Shared hatch ID doesn't match"
75
- )
76
-
77
- # 3. Unshare the hatch
78
- self.unshare_hatch_from_user()
79
-
80
- # 4. Verify the hatch is no longer shared
81
- shared_hatches = self.get_shared_hatches()
82
- self.assertEqual(
83
- len(shared_hatches), 0, "Expected no shared hatches after unsharing"
84
- )
85
-
86
- def test_is_hatch_shared_with_user(self):
87
- """Test checking if a hatch is shared with a specific user"""
88
- # Initially the hatch should not be shared
89
- result = self.check_is_hatch_shared_with_user()
90
- self.assertFalse(result["is_shared"], "Hatch should not be shared initially")
91
-
92
- # Share the hatch
93
- self.share_hatch_with_user()
94
-
95
- # Verify the hatch is now shared
96
- result = self.check_is_hatch_shared_with_user()
97
- self.assertTrue(result["is_shared"], "Hatch should be shared after sharing")
98
-
99
- # Unshare the hatch
100
- self.unshare_hatch_from_user()
101
-
102
- # Verify the hatch is no longer shared
103
- result = self.check_is_hatch_shared_with_user()
104
- self.assertFalse(
105
- result["is_shared"], "Hatch should not be shared after unsharing"
106
- )
107
-
108
- def check_is_hatch_shared_with_user(self):
109
- """Helper method to check if a hatch is shared with a user"""
110
- url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{self.shared_user_id}"
111
-
112
- response = requests.get(url, headers=self.headers)
113
- self.assertEqual(
114
- response.status_code,
115
- 200,
116
- f"Failed to check if hatch is shared: {response.text}",
117
- )
118
- return response.json()
119
-
120
- def share_hatch_with_user(self):
121
- """Helper method to share a hatch with another user"""
122
- url = f"{self.base_url}/api/hatch/{self.hatch_id}/share"
123
- payload = {"user_id": self.shared_user_id}
124
-
125
- response = requests.post(url, headers=self.headers, json=payload)
126
- self.assertEqual(
127
- response.status_code, 200, f"Failed to share hatch: {response.text}"
128
- )
129
- result = response.json()
130
- self.assertTrue(
131
- result["success"], f"Share operation failed: {result.get('message', '')}"
132
- )
133
- print(f"Shared hatch {self.hatch_id} with user {self.shared_user_id}")
134
- return result
135
-
136
- def unshare_hatch_from_user(self):
137
- """Helper method to unshare a hatch from a user"""
138
- url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{self.shared_user_id}"
139
-
140
- response = requests.delete(url, headers=self.headers)
141
- self.assertEqual(
142
- response.status_code, 200, f"Failed to unshare hatch: {response.text}"
143
- )
144
- result = response.json()
145
- self.assertTrue(
146
- result["success"], f"Unshare operation failed: {result.get('message', '')}"
147
- )
148
- print(f"Unshared hatch {self.hatch_id} from user {self.shared_user_id}")
149
- return result
150
-
151
- def get_shared_hatches(self):
152
- """Helper method to get all hatches shared with a user"""
153
- url = f"{self.base_url}/api/hatches/shared?user_id={self.shared_user_id}"
154
-
155
- response = requests.get(url, headers=self.headers)
156
- self.assertEqual(
157
- response.status_code, 200, f"Failed to get shared hatches: {response.text}"
158
- )
159
- return response.json()
160
-
161
- def test_share_nonexistent_hatch(self):
162
- """Test sharing a non-existent hatch"""
163
- url = f"{self.base_url}/api/hatch/nonexistent-hatch-id/share"
164
- payload = {"user_id": self.shared_user_id}
165
-
166
- response = requests.post(url, headers=self.headers, json=payload)
167
- self.assertEqual(
168
- response.status_code, 404, "Expected 404 for non-existent hatch"
169
- )
170
-
171
- def test_unshare_nonexistent_hatch(self):
172
- """Test unsharing a non-existent hatch"""
173
- url = f"{self.base_url}/api/hatch/nonexistent-hatch-id/share/{self.shared_user_id}"
174
-
175
- response = requests.delete(url, headers=self.headers)
176
- self.assertEqual(
177
- response.status_code, 404, "Expected 404 for non-existent hatch"
178
- )
179
-
180
- def test_share_hatch_multiple_times(self):
181
- """Test sharing a hatch with the same user multiple times"""
182
- # First share
183
- self.share_hatch_with_user()
184
-
185
- # Second share (should be idempotent)
186
- result = self.share_hatch_with_user()
187
- self.assertTrue(result["success"], "Second share operation should succeed")
188
-
189
- # Verify only one share exists
190
- shared_hatches = self.get_shared_hatches()
191
- self.assertEqual(len(shared_hatches), 1, "Expected exactly one shared hatch")
192
-
193
- # Cleanup
194
- self.unshare_hatch_from_user()
195
-
196
- def test_share_with_multiple_users(self):
197
- """Test sharing a hatch with multiple users"""
198
- # Create another user ID
199
- second_user_id = f"user2_{uuid.uuid4().hex[:8]}@example.com"
200
-
201
- # Share with first user
202
- self.share_hatch_with_user()
203
-
204
- # Share with second user
205
- url = f"{self.base_url}/api/hatch/{self.hatch_id}/share"
206
- payload = {"user_id": second_user_id}
207
-
208
- response = requests.post(url, headers=self.headers, json=payload)
209
- self.assertEqual(
210
- response.status_code,
211
- 200,
212
- f"Failed to share hatch with second user: {response.text}",
213
- )
214
-
215
- # Verify first user's shared hatches
216
- shared_hatches_1 = self.get_shared_hatches()
217
- self.assertEqual(
218
- len(shared_hatches_1), 1, "Expected exactly one shared hatch for first user"
219
- )
220
-
221
- # Verify second user's shared hatches
222
- url = f"{self.base_url}/api/hatches/shared?user_id={second_user_id}"
223
- response = requests.get(url, headers=self.headers)
224
- self.assertEqual(
225
- response.status_code,
226
- 200,
227
- f"Failed to get shared hatches for second user: {response.text}",
228
- )
229
- shared_hatches_2 = response.json()
230
- self.assertEqual(
231
- len(shared_hatches_2),
232
- 1,
233
- "Expected exactly one shared hatch for second user",
234
- )
235
-
236
- # Cleanup
237
- self.unshare_hatch_from_user()
238
- url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{second_user_id}"
239
- requests.delete(url, headers=self.headers)
240
-
241
- def test_manual_curl_commands(self):
242
- """Test the API using the same curl commands provided for manual testing"""
243
- # These tests follow the same pattern as the curl commands but programmatically
244
-
245
- # Create a new hatch with curl-like parameters
246
- curl_hatch_id = "123abc"
247
- url = f"{self.base_url}/api/hatch"
248
- payload = {
249
- "user_id": "sebastian.hsu@gmail.com",
250
- "id": curl_hatch_id,
251
- "prompt_template": "妳是臺灣人,回答要用臺灣繁體中文正式用語,需要的時候也可以用英文,可以親切、俏皮、幽默,但不能隨便輕浮。在使用者合理的要求下請盡量配合他的需求,不要隨便拒絕",
252
- }
253
-
254
- create_response = requests.post(url, headers=self.headers, json=payload)
255
- self.assertEqual(
256
- create_response.status_code,
257
- 200,
258
- f"Failed to create curl test hatch: {create_response.text}",
259
- )
260
-
261
- # Get the hatch
262
- url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
263
- get_response = requests.get(url, headers=self.headers)
264
- self.assertEqual(
265
- get_response.status_code,
266
- 200,
267
- f"Failed to get curl test hatch: {get_response.text}",
268
- )
269
- self.assertEqual(
270
- get_response.json()["id"], curl_hatch_id, "Retrieved hatch ID doesn't match"
271
- )
272
-
273
- # Update the hatch
274
- url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
275
- update_payload = {
276
- "user_id": "sebastian.hsu@gmail.com",
277
- "id": curl_hatch_id,
278
- "prompt_template": "You are a helpful agent",
279
- }
280
-
281
- update_response = requests.put(url, headers=self.headers, json=update_payload)
282
- self.assertEqual(
283
- update_response.status_code,
284
- 200,
285
- f"Failed to update curl test hatch: {update_response.text}",
286
- )
287
- self.assertEqual(
288
- update_response.json()["prompt_template"],
289
- "You are a helpful agent",
290
- "Prompt template not updated",
291
- )
292
-
293
- # Share the hatch with another user
294
- url = f"{self.base_url}/api/hatch/{curl_hatch_id}/share"
295
- share_payload = {"user_id": self.shared_user_id}
296
-
297
- share_response = requests.post(url, headers=self.headers, json=share_payload)
298
- self.assertEqual(
299
- share_response.status_code,
300
- 200,
301
- f"Failed to share curl test hatch: {share_response.text}",
302
- )
303
-
304
- # Get shared hatches
305
- url = f"{self.base_url}/api/hatches/shared?user_id={self.shared_user_id}"
306
- shared_response = requests.get(url, headers=self.headers)
307
- self.assertEqual(
308
- shared_response.status_code,
309
- 200,
310
- f"Failed to get shared hatches: {shared_response.text}",
311
- )
312
-
313
- # Unshare the hatch
314
- url = f"{self.base_url}/api/hatch/{curl_hatch_id}/share/{self.shared_user_id}"
315
- unshare_response = requests.delete(url, headers=self.headers)
316
- self.assertEqual(
317
- unshare_response.status_code,
318
- 200,
319
- f"Failed to unshare curl test hatch: {unshare_response.text}",
320
- )
321
-
322
- # Delete the hatch
323
- url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
324
- delete_response = requests.delete(url, headers=self.headers)
325
- self.assertEqual(
326
- delete_response.status_code,
327
- 200,
328
- f"Failed to delete curl test hatch: {delete_response.text}",
329
- )
330
-
331
-
332
- if __name__ == "__main__":
333
- unittest.main()
1
+ import unittest
2
+ import requests
3
+ import json
4
+ import time
5
+ import uuid
6
+
7
+
8
+ class TestHatchSharing(unittest.TestCase):
9
+ """Test class for Hatch sharing functionality tests"""
10
+
11
+ def setUp(self):
12
+ """Setup method that runs before each test"""
13
+ # Default base URL, can be overridden by setting the class attribute
14
+ if not hasattr(self, "base_url"):
15
+ self.base_url = "http://localhost:8080"
16
+
17
+ # Common headers
18
+ self.headers = {"Content-Type": "application/json"}
19
+
20
+ # Test data - create unique IDs for each test run
21
+ self.owner_id = f"owner_{uuid.uuid4().hex[:8]}@example.com"
22
+ self.shared_user_id = f"shared_{uuid.uuid4().hex[:8]}@example.com"
23
+ self.hatch_id = f"test_hatch_{uuid.uuid4().hex[:8]}"
24
+
25
+ # Create a test hatch for the owner
26
+ self.create_test_hatch()
27
+
28
+ def tearDown(self):
29
+ """Cleanup method that runs after each test"""
30
+ # Delete the test hatch
31
+ self.delete_test_hatch()
32
+
33
+ def create_test_hatch(self):
34
+ """Helper method to create a test hatch"""
35
+ url = f"{self.base_url}/api/hatch"
36
+ payload = {
37
+ "user_id": self.owner_id,
38
+ "id": self.hatch_id,
39
+ "model_name": "test-model",
40
+ "prompt_template": "This is a test prompt template for sharing functionality testing",
41
+ "name": "Test Hatch for Sharing Tests",
42
+ }
43
+
44
+ response = requests.post(url, headers=self.headers, json=payload)
45
+ self.assertEqual(
46
+ response.status_code, 200, f"Failed to create test hatch: {response.text}"
47
+ )
48
+ print(f"Created test hatch with ID: {self.hatch_id} for owner: {self.owner_id}")
49
+ return response.json()
50
+
51
+ def delete_test_hatch(self):
52
+ """Helper method to delete the test hatch"""
53
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}"
54
+ response = requests.delete(url, headers=self.headers)
55
+ self.assertEqual(
56
+ response.status_code, 200, f"Failed to delete test hatch: {response.text}"
57
+ )
58
+ print(f"Deleted test hatch with ID: {self.hatch_id}")
59
+
60
+ def test_share_hatch_workflow(self):
61
+ """Test the complete hatch sharing workflow:
62
+ 1. Share a hatch with another user
63
+ 2. Verify the hatch is shared
64
+ 3. Unshare the hatch
65
+ 4. Verify the hatch is no longer shared
66
+ """
67
+ # 1. Share the hatch
68
+ self.share_hatch_with_user()
69
+
70
+ # 2. Verify the hatch is shared
71
+ shared_hatches = self.get_shared_hatches()
72
+ self.assertEqual(len(shared_hatches), 1, "Expected exactly one shared hatch")
73
+ self.assertEqual(
74
+ shared_hatches[0]["id"], self.hatch_id, "Shared hatch ID doesn't match"
75
+ )
76
+
77
+ # 3. Unshare the hatch
78
+ self.unshare_hatch_from_user()
79
+
80
+ # 4. Verify the hatch is no longer shared
81
+ shared_hatches = self.get_shared_hatches()
82
+ self.assertEqual(
83
+ len(shared_hatches), 0, "Expected no shared hatches after unsharing"
84
+ )
85
+
86
+ def test_is_hatch_shared_with_user(self):
87
+ """Test checking if a hatch is shared with a specific user"""
88
+ # Initially the hatch should not be shared
89
+ result = self.check_is_hatch_shared_with_user()
90
+ self.assertFalse(result["is_shared"], "Hatch should not be shared initially")
91
+
92
+ # Share the hatch
93
+ self.share_hatch_with_user()
94
+
95
+ # Verify the hatch is now shared
96
+ result = self.check_is_hatch_shared_with_user()
97
+ self.assertTrue(result["is_shared"], "Hatch should be shared after sharing")
98
+
99
+ # Unshare the hatch
100
+ self.unshare_hatch_from_user()
101
+
102
+ # Verify the hatch is no longer shared
103
+ result = self.check_is_hatch_shared_with_user()
104
+ self.assertFalse(
105
+ result["is_shared"], "Hatch should not be shared after unsharing"
106
+ )
107
+
108
+ def check_is_hatch_shared_with_user(self):
109
+ """Helper method to check if a hatch is shared with a user"""
110
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{self.shared_user_id}"
111
+
112
+ response = requests.get(url, headers=self.headers)
113
+ self.assertEqual(
114
+ response.status_code,
115
+ 200,
116
+ f"Failed to check if hatch is shared: {response.text}",
117
+ )
118
+ return response.json()
119
+
120
+ def share_hatch_with_user(self):
121
+ """Helper method to share a hatch with another user"""
122
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}/share"
123
+ payload = {"user_id": self.shared_user_id}
124
+
125
+ response = requests.post(url, headers=self.headers, json=payload)
126
+ self.assertEqual(
127
+ response.status_code, 200, f"Failed to share hatch: {response.text}"
128
+ )
129
+ result = response.json()
130
+ self.assertTrue(
131
+ result["success"], f"Share operation failed: {result.get('message', '')}"
132
+ )
133
+ print(f"Shared hatch {self.hatch_id} with user {self.shared_user_id}")
134
+ return result
135
+
136
+ def unshare_hatch_from_user(self):
137
+ """Helper method to unshare a hatch from a user"""
138
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{self.shared_user_id}"
139
+
140
+ response = requests.delete(url, headers=self.headers)
141
+ self.assertEqual(
142
+ response.status_code, 200, f"Failed to unshare hatch: {response.text}"
143
+ )
144
+ result = response.json()
145
+ self.assertTrue(
146
+ result["success"], f"Unshare operation failed: {result.get('message', '')}"
147
+ )
148
+ print(f"Unshared hatch {self.hatch_id} from user {self.shared_user_id}")
149
+ return result
150
+
151
+ def get_shared_hatches(self):
152
+ """Helper method to get all hatches shared with a user"""
153
+ url = f"{self.base_url}/api/hatches/shared?user_id={self.shared_user_id}"
154
+
155
+ response = requests.get(url, headers=self.headers)
156
+ self.assertEqual(
157
+ response.status_code, 200, f"Failed to get shared hatches: {response.text}"
158
+ )
159
+ return response.json()
160
+
161
+ def test_share_nonexistent_hatch(self):
162
+ """Test sharing a non-existent hatch"""
163
+ url = f"{self.base_url}/api/hatch/nonexistent-hatch-id/share"
164
+ payload = {"user_id": self.shared_user_id}
165
+
166
+ response = requests.post(url, headers=self.headers, json=payload)
167
+ self.assertEqual(
168
+ response.status_code, 404, "Expected 404 for non-existent hatch"
169
+ )
170
+
171
+ def test_unshare_nonexistent_hatch(self):
172
+ """Test unsharing a non-existent hatch"""
173
+ url = f"{self.base_url}/api/hatch/nonexistent-hatch-id/share/{self.shared_user_id}"
174
+
175
+ response = requests.delete(url, headers=self.headers)
176
+ self.assertEqual(
177
+ response.status_code, 404, "Expected 404 for non-existent hatch"
178
+ )
179
+
180
+ def test_share_hatch_multiple_times(self):
181
+ """Test sharing a hatch with the same user multiple times"""
182
+ # First share
183
+ self.share_hatch_with_user()
184
+
185
+ # Second share (should be idempotent)
186
+ result = self.share_hatch_with_user()
187
+ self.assertTrue(result["success"], "Second share operation should succeed")
188
+
189
+ # Verify only one share exists
190
+ shared_hatches = self.get_shared_hatches()
191
+ self.assertEqual(len(shared_hatches), 1, "Expected exactly one shared hatch")
192
+
193
+ # Cleanup
194
+ self.unshare_hatch_from_user()
195
+
196
+ def test_share_with_multiple_users(self):
197
+ """Test sharing a hatch with multiple users"""
198
+ # Create another user ID
199
+ second_user_id = f"user2_{uuid.uuid4().hex[:8]}@example.com"
200
+
201
+ # Share with first user
202
+ self.share_hatch_with_user()
203
+
204
+ # Share with second user
205
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}/share"
206
+ payload = {"user_id": second_user_id}
207
+
208
+ response = requests.post(url, headers=self.headers, json=payload)
209
+ self.assertEqual(
210
+ response.status_code,
211
+ 200,
212
+ f"Failed to share hatch with second user: {response.text}",
213
+ )
214
+
215
+ # Verify first user's shared hatches
216
+ shared_hatches_1 = self.get_shared_hatches()
217
+ self.assertEqual(
218
+ len(shared_hatches_1), 1, "Expected exactly one shared hatch for first user"
219
+ )
220
+
221
+ # Verify second user's shared hatches
222
+ url = f"{self.base_url}/api/hatches/shared?user_id={second_user_id}"
223
+ response = requests.get(url, headers=self.headers)
224
+ self.assertEqual(
225
+ response.status_code,
226
+ 200,
227
+ f"Failed to get shared hatches for second user: {response.text}",
228
+ )
229
+ shared_hatches_2 = response.json()
230
+ self.assertEqual(
231
+ len(shared_hatches_2),
232
+ 1,
233
+ "Expected exactly one shared hatch for second user",
234
+ )
235
+
236
+ # Cleanup
237
+ self.unshare_hatch_from_user()
238
+ url = f"{self.base_url}/api/hatch/{self.hatch_id}/share/{second_user_id}"
239
+ requests.delete(url, headers=self.headers)
240
+
241
+ def test_manual_curl_commands(self):
242
+ """Test the API using the same curl commands provided for manual testing"""
243
+ # These tests follow the same pattern as the curl commands but programmatically
244
+
245
+ # Create a new hatch with curl-like parameters
246
+ curl_hatch_id = "123abc"
247
+ url = f"{self.base_url}/api/hatch"
248
+ payload = {
249
+ "user_id": "sebastian.hsu@gmail.com",
250
+ "id": curl_hatch_id,
251
+ "prompt_template": "妳是臺灣人,回答要用臺灣繁體中文正式用語,需要的時候也可以用英文,可以親切、俏皮、幽默,但不能隨便輕浮。在使用者合理的要求下請盡量配合他的需求,不要隨便拒絕",
252
+ }
253
+
254
+ create_response = requests.post(url, headers=self.headers, json=payload)
255
+ self.assertEqual(
256
+ create_response.status_code,
257
+ 200,
258
+ f"Failed to create curl test hatch: {create_response.text}",
259
+ )
260
+
261
+ # Get the hatch
262
+ url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
263
+ get_response = requests.get(url, headers=self.headers)
264
+ self.assertEqual(
265
+ get_response.status_code,
266
+ 200,
267
+ f"Failed to get curl test hatch: {get_response.text}",
268
+ )
269
+ self.assertEqual(
270
+ get_response.json()["id"], curl_hatch_id, "Retrieved hatch ID doesn't match"
271
+ )
272
+
273
+ # Update the hatch
274
+ url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
275
+ update_payload = {
276
+ "user_id": "sebastian.hsu@gmail.com",
277
+ "id": curl_hatch_id,
278
+ "prompt_template": "You are a helpful agent",
279
+ }
280
+
281
+ update_response = requests.put(url, headers=self.headers, json=update_payload)
282
+ self.assertEqual(
283
+ update_response.status_code,
284
+ 200,
285
+ f"Failed to update curl test hatch: {update_response.text}",
286
+ )
287
+ self.assertEqual(
288
+ update_response.json()["prompt_template"],
289
+ "You are a helpful agent",
290
+ "Prompt template not updated",
291
+ )
292
+
293
+ # Share the hatch with another user
294
+ url = f"{self.base_url}/api/hatch/{curl_hatch_id}/share"
295
+ share_payload = {"user_id": self.shared_user_id}
296
+
297
+ share_response = requests.post(url, headers=self.headers, json=share_payload)
298
+ self.assertEqual(
299
+ share_response.status_code,
300
+ 200,
301
+ f"Failed to share curl test hatch: {share_response.text}",
302
+ )
303
+
304
+ # Get shared hatches
305
+ url = f"{self.base_url}/api/hatches/shared?user_id={self.shared_user_id}"
306
+ shared_response = requests.get(url, headers=self.headers)
307
+ self.assertEqual(
308
+ shared_response.status_code,
309
+ 200,
310
+ f"Failed to get shared hatches: {shared_response.text}",
311
+ )
312
+
313
+ # Unshare the hatch
314
+ url = f"{self.base_url}/api/hatch/{curl_hatch_id}/share/{self.shared_user_id}"
315
+ unshare_response = requests.delete(url, headers=self.headers)
316
+ self.assertEqual(
317
+ unshare_response.status_code,
318
+ 200,
319
+ f"Failed to unshare curl test hatch: {unshare_response.text}",
320
+ )
321
+
322
+ # Delete the hatch
323
+ url = f"{self.base_url}/api/hatch/{curl_hatch_id}"
324
+ delete_response = requests.delete(url, headers=self.headers)
325
+ self.assertEqual(
326
+ delete_response.status_code,
327
+ 200,
328
+ f"Failed to delete curl test hatch: {delete_response.text}",
329
+ )
330
+
331
+
332
+ if __name__ == "__main__":
333
+ unittest.main()