botrun-flow-lang 5.9.301__py3-none-any.whl → 5.10.82__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 (84) 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 +481 -481
  6. botrun_flow_lang/api/langgraph_api.py +796 -796
  7. botrun_flow_lang/api/line_bot_api.py +1357 -1357
  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 +316 -316
  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 +174 -174
  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/gov_researcher_2_graph.py +1002 -1002
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  25. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +548 -542
  26. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  27. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  28. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  29. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  30. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  31. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  32. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
  33. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  34. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  35. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
  36. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  37. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  38. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  39. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  40. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  41. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  42. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  43. botrun_flow_lang/log/.gitignore +2 -2
  44. botrun_flow_lang/main.py +61 -61
  45. botrun_flow_lang/main_fast.py +51 -51
  46. botrun_flow_lang/mcp_server/__init__.py +10 -10
  47. botrun_flow_lang/mcp_server/default_mcp.py +711 -711
  48. botrun_flow_lang/models/nodes/utils.py +205 -205
  49. botrun_flow_lang/models/token_usage.py +34 -34
  50. botrun_flow_lang/requirements.txt +21 -21
  51. botrun_flow_lang/services/base/firestore_base.py +30 -30
  52. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  53. botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
  54. botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
  55. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  56. botrun_flow_lang/services/storage/storage_store.py +65 -65
  57. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  58. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  59. botrun_flow_lang/static/docs/tools/index.html +926 -926
  60. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  61. botrun_flow_lang/tests/api_stress_test.py +357 -357
  62. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  63. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  64. botrun_flow_lang/tests/test_html_util.py +31 -31
  65. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  66. botrun_flow_lang/tests/test_img_util.py +39 -39
  67. botrun_flow_lang/tests/test_local_files.py +114 -114
  68. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  69. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  70. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  71. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  72. botrun_flow_lang/tools/generate_docs.py +133 -133
  73. botrun_flow_lang/tools/templates/tools.html +153 -153
  74. botrun_flow_lang/utils/__init__.py +7 -7
  75. botrun_flow_lang/utils/botrun_logger.py +344 -344
  76. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  77. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  78. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  79. botrun_flow_lang/utils/langchain_utils.py +324 -324
  80. botrun_flow_lang/utils/yaml_utils.py +9 -9
  81. {botrun_flow_lang-5.9.301.dist-info → botrun_flow_lang-5.10.82.dist-info}/METADATA +2 -2
  82. botrun_flow_lang-5.10.82.dist-info/RECORD +99 -0
  83. botrun_flow_lang-5.9.301.dist-info/RECORD +0 -99
  84. {botrun_flow_lang-5.9.301.dist-info → botrun_flow_lang-5.10.82.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()