binalyze-air-sdk 1.0.1__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 (82) hide show
  1. binalyze_air/__init__.py +77 -0
  2. binalyze_air/apis/__init__.py +27 -0
  3. binalyze_air/apis/authentication.py +27 -0
  4. binalyze_air/apis/auto_asset_tags.py +75 -0
  5. binalyze_air/apis/endpoints.py +22 -0
  6. binalyze_air/apis/event_subscription.py +97 -0
  7. binalyze_air/apis/evidence.py +53 -0
  8. binalyze_air/apis/evidences.py +216 -0
  9. binalyze_air/apis/interact.py +36 -0
  10. binalyze_air/apis/params.py +40 -0
  11. binalyze_air/apis/settings.py +27 -0
  12. binalyze_air/apis/user_management.py +74 -0
  13. binalyze_air/apis/users.py +68 -0
  14. binalyze_air/apis/webhooks.py +231 -0
  15. binalyze_air/base.py +133 -0
  16. binalyze_air/client.py +1338 -0
  17. binalyze_air/commands/__init__.py +146 -0
  18. binalyze_air/commands/acquisitions.py +387 -0
  19. binalyze_air/commands/assets.py +363 -0
  20. binalyze_air/commands/authentication.py +37 -0
  21. binalyze_air/commands/auto_asset_tags.py +231 -0
  22. binalyze_air/commands/baseline.py +396 -0
  23. binalyze_air/commands/cases.py +603 -0
  24. binalyze_air/commands/event_subscription.py +102 -0
  25. binalyze_air/commands/evidences.py +988 -0
  26. binalyze_air/commands/interact.py +58 -0
  27. binalyze_air/commands/organizations.py +221 -0
  28. binalyze_air/commands/policies.py +203 -0
  29. binalyze_air/commands/settings.py +29 -0
  30. binalyze_air/commands/tasks.py +56 -0
  31. binalyze_air/commands/triage.py +360 -0
  32. binalyze_air/commands/user_management.py +126 -0
  33. binalyze_air/commands/users.py +101 -0
  34. binalyze_air/config.py +245 -0
  35. binalyze_air/exceptions.py +50 -0
  36. binalyze_air/http_client.py +306 -0
  37. binalyze_air/models/__init__.py +285 -0
  38. binalyze_air/models/acquisitions.py +251 -0
  39. binalyze_air/models/assets.py +439 -0
  40. binalyze_air/models/audit.py +273 -0
  41. binalyze_air/models/authentication.py +70 -0
  42. binalyze_air/models/auto_asset_tags.py +117 -0
  43. binalyze_air/models/baseline.py +232 -0
  44. binalyze_air/models/cases.py +276 -0
  45. binalyze_air/models/endpoints.py +76 -0
  46. binalyze_air/models/event_subscription.py +172 -0
  47. binalyze_air/models/evidence.py +66 -0
  48. binalyze_air/models/evidences.py +349 -0
  49. binalyze_air/models/interact.py +136 -0
  50. binalyze_air/models/organizations.py +294 -0
  51. binalyze_air/models/params.py +128 -0
  52. binalyze_air/models/policies.py +250 -0
  53. binalyze_air/models/settings.py +84 -0
  54. binalyze_air/models/tasks.py +149 -0
  55. binalyze_air/models/triage.py +143 -0
  56. binalyze_air/models/user_management.py +97 -0
  57. binalyze_air/models/users.py +82 -0
  58. binalyze_air/queries/__init__.py +134 -0
  59. binalyze_air/queries/acquisitions.py +156 -0
  60. binalyze_air/queries/assets.py +105 -0
  61. binalyze_air/queries/audit.py +417 -0
  62. binalyze_air/queries/authentication.py +56 -0
  63. binalyze_air/queries/auto_asset_tags.py +60 -0
  64. binalyze_air/queries/baseline.py +185 -0
  65. binalyze_air/queries/cases.py +293 -0
  66. binalyze_air/queries/endpoints.py +25 -0
  67. binalyze_air/queries/event_subscription.py +55 -0
  68. binalyze_air/queries/evidence.py +140 -0
  69. binalyze_air/queries/evidences.py +280 -0
  70. binalyze_air/queries/interact.py +28 -0
  71. binalyze_air/queries/organizations.py +223 -0
  72. binalyze_air/queries/params.py +115 -0
  73. binalyze_air/queries/policies.py +150 -0
  74. binalyze_air/queries/settings.py +20 -0
  75. binalyze_air/queries/tasks.py +82 -0
  76. binalyze_air/queries/triage.py +231 -0
  77. binalyze_air/queries/user_management.py +83 -0
  78. binalyze_air/queries/users.py +69 -0
  79. binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
  80. binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
  81. binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
  82. binalyze_air_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,603 @@
1
+ """
2
+ Case-related commands for the Binalyze AIR SDK.
3
+ Fixed to match API documentation exactly.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional
7
+
8
+ from ..base import Command
9
+ from ..models.cases import CreateCaseRequest, UpdateCaseRequest, Case, CaseNote
10
+ from ..models.assets import AssetFilter
11
+ from ..http_client import HTTPClient
12
+
13
+
14
+ class CreateCaseCommand(Command[Case]):
15
+ """Command to create a new case - FIXED endpoint URL."""
16
+
17
+ def __init__(self, http_client: HTTPClient, case_data: CreateCaseRequest):
18
+ self.http_client = http_client
19
+ self.case_data = case_data
20
+
21
+ def execute(self) -> Case:
22
+ """Execute the create case command."""
23
+ payload = {
24
+ "organizationId": self.case_data.organization_id,
25
+ "name": self.case_data.name,
26
+ "ownerUserId": self.case_data.owner_user_id,
27
+ "visibility": self.case_data.visibility,
28
+ "assignedUserIds": self.case_data.assigned_user_ids,
29
+ }
30
+
31
+ # FIXED: Remove api/public/ prefix
32
+ response = self.http_client.post("cases", json_data=payload)
33
+
34
+ # Use Pydantic parsing with proper field aliasing
35
+ entity_data = response.get("result", {})
36
+ return Case.model_validate(entity_data)
37
+
38
+
39
+ class UpdateCaseCommand(Command[Case]):
40
+ """Command to update an existing case - FIXED endpoint URL."""
41
+
42
+ def __init__(self, http_client: HTTPClient, case_id: str, update_data: UpdateCaseRequest):
43
+ self.http_client = http_client
44
+ self.case_id = case_id
45
+ self.update_data = update_data
46
+
47
+ def execute(self) -> Case:
48
+ """Execute the update case command."""
49
+ payload = {}
50
+
51
+ # Only include fields that are set
52
+ if self.update_data.name is not None:
53
+ payload["name"] = self.update_data.name
54
+ if self.update_data.owner_user_id is not None:
55
+ payload["ownerUserId"] = self.update_data.owner_user_id
56
+ if self.update_data.visibility is not None:
57
+ payload["visibility"] = self.update_data.visibility
58
+ if self.update_data.assigned_user_ids is not None:
59
+ payload["assignedUserIds"] = self.update_data.assigned_user_ids
60
+ if self.update_data.status is not None:
61
+ payload["status"] = self.update_data.status
62
+ if self.update_data.notes is not None:
63
+ payload["notes"] = self.update_data.notes
64
+
65
+ # FIXED: Use PATCH method to match API specification (was PUT)
66
+ response = self.http_client.patch(f"cases/{self.case_id}", json_data=payload)
67
+
68
+ # Use Pydantic parsing with proper field aliasing
69
+ entity_data = response.get("result", {})
70
+ return Case.model_validate(entity_data)
71
+
72
+
73
+ class CloseCaseCommand(Command[Case]):
74
+ """Command to close a case - FIXED endpoint URL."""
75
+
76
+ def __init__(self, http_client: HTTPClient, case_id: str):
77
+ self.http_client = http_client
78
+ self.case_id = case_id
79
+
80
+ def execute(self) -> Case:
81
+ """Execute the close case command."""
82
+ # FIXED: Remove api/public/ prefix
83
+ response = self.http_client.post(f"cases/{self.case_id}/close", json_data={})
84
+
85
+ # Use Pydantic parsing with proper field aliasing
86
+ entity_data = response.get("result", {})
87
+ return Case.model_validate(entity_data)
88
+
89
+
90
+ class OpenCaseCommand(Command[Case]):
91
+ """Command to open a case - FIXED endpoint URL."""
92
+
93
+ def __init__(self, http_client: HTTPClient, case_id: str):
94
+ self.http_client = http_client
95
+ self.case_id = case_id
96
+
97
+ def execute(self) -> Case:
98
+ """Execute the open case command."""
99
+ # FIXED: Remove api/public/ prefix
100
+ response = self.http_client.post(f"cases/{self.case_id}/open", json_data={})
101
+
102
+ # Use Pydantic parsing with proper field aliasing
103
+ entity_data = response.get("result", {})
104
+ return Case.model_validate(entity_data)
105
+
106
+
107
+ class ArchiveCaseCommand(Command[Case]):
108
+ """Command to archive a case - FIXED endpoint URL."""
109
+
110
+ def __init__(self, http_client: HTTPClient, case_id: str):
111
+ self.http_client = http_client
112
+ self.case_id = case_id
113
+
114
+ def execute(self) -> Case:
115
+ """Execute the archive case command."""
116
+ # FIXED: Remove api/public/ prefix
117
+ response = self.http_client.post(f"cases/{self.case_id}/archive", json_data={})
118
+
119
+ # Use Pydantic parsing with proper field aliasing
120
+ entity_data = response.get("result", {})
121
+ return Case.model_validate(entity_data)
122
+
123
+
124
+ class ChangeCaseOwnerCommand(Command[Case]):
125
+ """Command to change case owner - FIXED to match API specification exactly."""
126
+
127
+ def __init__(self, http_client: HTTPClient, case_id: str, new_owner_id: str):
128
+ self.http_client = http_client
129
+ self.case_id = case_id
130
+ self.new_owner_id = new_owner_id
131
+
132
+ def execute(self) -> Case:
133
+ """Execute the change case owner command."""
134
+ # FIXED: Use correct payload field name as per API specification
135
+ payload = {"newOwnerId": self.new_owner_id}
136
+
137
+ # FIXED: Use correct endpoint URL and HTTP method as per API specification
138
+ # POST /api/public/cases/{id}/change-owner
139
+ response = self.http_client.post(f"cases/{self.case_id}/change-owner", json_data=payload)
140
+
141
+ # Use Pydantic parsing with proper field aliasing
142
+ entity_data = response.get("result", {})
143
+ return Case.model_validate(entity_data)
144
+
145
+
146
+ class RemoveEndpointsFromCaseCommand(Command[Dict[str, Any]]):
147
+ """Command to remove endpoints from a case - FIXED to match API documentation exactly."""
148
+
149
+ def __init__(self, http_client: HTTPClient, case_id: str, asset_filter: AssetFilter):
150
+ self.http_client = http_client
151
+ self.case_id = case_id
152
+ self.asset_filter = asset_filter
153
+
154
+ def execute(self) -> Dict[str, Any]:
155
+ """Execute the remove endpoints from case command with correct structure."""
156
+ # FIXED: Use proper filter structure as per API documentation
157
+ payload = {
158
+ "filter": self.asset_filter.to_filter_dict()
159
+ }
160
+
161
+ # FIXED: Correct endpoint URL and HTTP method (DELETE)
162
+ return self.http_client.delete(f"cases/{self.case_id}/endpoints", json_data=payload)
163
+
164
+
165
+ class RemoveTaskAssignmentFromCaseCommand(Command[Dict[str, Any]]):
166
+ """Command to remove task assignment from a case - FIXED endpoint URL."""
167
+
168
+ def __init__(self, http_client: HTTPClient, case_id: str, task_assignment_id: str):
169
+ self.http_client = http_client
170
+ self.case_id = case_id
171
+ self.task_assignment_id = task_assignment_id
172
+
173
+ def execute(self) -> Dict[str, Any]:
174
+ """Execute the remove task assignment from case command."""
175
+ # FIXED: Remove api/public/ prefix
176
+ return self.http_client.post(
177
+ f"cases/{self.case_id}/tasks/{self.task_assignment_id}/remove",
178
+ json_data={}
179
+ )
180
+
181
+
182
+ class ImportTaskAssignmentsToCaseCommand(Command[Dict[str, Any]]):
183
+ """Command to import task assignments to a case - FIXED endpoint URL."""
184
+
185
+ def __init__(self, http_client: HTTPClient, case_id: str, task_assignment_ids: List[str]):
186
+ self.http_client = http_client
187
+ self.case_id = case_id
188
+ self.task_assignment_ids = task_assignment_ids
189
+
190
+ def execute(self) -> Dict[str, Any]:
191
+ """Execute the import task assignments to case command."""
192
+ payload = {"taskAssignmentIds": self.task_assignment_ids}
193
+
194
+ # FIXED: Remove api/public/ prefix
195
+ return self.http_client.post(f"cases/{self.case_id}/tasks/import", json_data=payload)
196
+
197
+
198
+ class AddNoteToCaseCommand(Command[CaseNote]):
199
+ """Command to add a note to a case - Based on API documentation."""
200
+
201
+ def __init__(self, http_client: HTTPClient, case_id: str, note_value: str):
202
+ self.http_client = http_client
203
+ self.case_id = case_id
204
+ self.note_value = note_value
205
+
206
+ def execute(self) -> CaseNote:
207
+ """Execute the add note to case command."""
208
+ payload = {"value": self.note_value}
209
+
210
+ # POST /api/public/cases/{id}/notes
211
+ response = self.http_client.post(f"cases/{self.case_id}/notes", json_data=payload)
212
+
213
+ # Use Pydantic parsing with proper field aliasing
214
+ entity_data = response.get("result", {})
215
+ return CaseNote.model_validate(entity_data)
216
+
217
+
218
+ class UpdateNoteToCaseCommand(Command[CaseNote]):
219
+ """Command to update a note in a case - Based on API documentation."""
220
+
221
+ def __init__(self, http_client: HTTPClient, case_id: str, note_id: str, note_value: str):
222
+ self.http_client = http_client
223
+ self.case_id = case_id
224
+ self.note_id = note_id
225
+ self.note_value = note_value
226
+
227
+ def execute(self) -> CaseNote:
228
+ """Execute the update note in case command."""
229
+ payload = {"value": self.note_value}
230
+
231
+ # PATCH /api/public/cases/{caseId}/notes/{noteId}
232
+ response = self.http_client.patch(f"cases/{self.case_id}/notes/{self.note_id}", json_data=payload)
233
+
234
+ # Use Pydantic parsing with proper field aliasing
235
+ entity_data = response.get("result", {})
236
+ return CaseNote.model_validate(entity_data)
237
+
238
+
239
+ class DeleteNoteToCaseCommand(Command[Dict[str, Any]]):
240
+ """Command to delete a note from a case - Based on API documentation."""
241
+
242
+ def __init__(self, http_client: HTTPClient, case_id: str, note_id: str):
243
+ self.http_client = http_client
244
+ self.case_id = case_id
245
+ self.note_id = note_id
246
+
247
+ def execute(self) -> Dict[str, Any]:
248
+ """Execute the delete note from case command."""
249
+ # DELETE /api/public/cases/{caseId}/notes/{noteId}
250
+ response = self.http_client.delete(f"cases/{self.case_id}/notes/{self.note_id}")
251
+ return response
252
+
253
+
254
+ class ExportCaseNotesCommand(Command[Dict[str, Any]]):
255
+ """Command to export case notes as a file download - Based on API documentation."""
256
+
257
+ def __init__(self, http_client: HTTPClient, case_id: str):
258
+ self.http_client = http_client
259
+ self.case_id = case_id
260
+
261
+ def execute(self) -> Dict[str, Any]:
262
+ """Execute the export case notes command."""
263
+ import requests
264
+
265
+ # GET /api/public/cases/{id}/notes/export
266
+ # This endpoint returns a file download (ZIP/CSV), so we need raw HTTP handling
267
+ headers = {
268
+ 'Authorization': f'Bearer {self.http_client.config.api_token}',
269
+ 'Content-Type': 'application/json'
270
+ }
271
+
272
+ url = f"{self.http_client.config.host}/api/public/cases/{self.case_id}/notes/export"
273
+
274
+ response = requests.get(
275
+ url,
276
+ headers=headers,
277
+ verify=self.http_client.config.verify_ssl,
278
+ timeout=self.http_client.config.timeout
279
+ )
280
+
281
+ if response.status_code == 200:
282
+ # Handle file downloads (ZIP/CSV export)
283
+ content_type = response.headers.get('content-type', '')
284
+ if 'application/zip' in content_type or 'text/csv' in content_type:
285
+ # Extract filename from content disposition if available
286
+ content_disposition = response.headers.get('content-disposition', '')
287
+ filename = "case_notes_export"
288
+ if 'filename=' in content_disposition:
289
+ filename = content_disposition.split('filename=')[1].strip('"')
290
+
291
+ return {
292
+ "success": True,
293
+ "result": {
294
+ "export_type": "file_download",
295
+ "content_type": content_type,
296
+ "file_size": len(response.content),
297
+ "filename": filename,
298
+ "file_content": response.content # Binary content for file download
299
+ },
300
+ "statusCode": response.status_code,
301
+ "errors": []
302
+ }
303
+ else:
304
+ # Fallback to JSON parsing
305
+ return response.json()
306
+ else:
307
+ raise Exception(f"HTTP {response.status_code}: {response.text}")
308
+
309
+
310
+ class ExportCasesCommand(Command[Dict[str, Any]]):
311
+ """Command to export cases as a file download - Based on API documentation."""
312
+
313
+ def __init__(self, http_client: HTTPClient, filter_params: Optional[Dict[str, Any]] = None):
314
+ self.http_client = http_client
315
+ self.filter_params = filter_params or {}
316
+
317
+ def execute(self) -> Dict[str, Any]:
318
+ """Execute the export cases command."""
319
+ import requests
320
+
321
+ # GET /api/public/cases/export
322
+ # This endpoint returns a file download (CSV), so we need raw HTTP handling
323
+ headers = {
324
+ 'Authorization': f'Bearer {self.http_client.config.api_token}',
325
+ 'Content-Type': 'application/json'
326
+ }
327
+
328
+ url = f"{self.http_client.config.host}/api/public/cases/export"
329
+
330
+ # Prepare query parameters
331
+ params = {}
332
+ if self.filter_params:
333
+ params.update(self.filter_params)
334
+
335
+ response = requests.get(
336
+ url,
337
+ headers=headers,
338
+ params=params,
339
+ verify=self.http_client.config.verify_ssl,
340
+ timeout=self.http_client.config.timeout
341
+ )
342
+
343
+ if response.status_code == 200:
344
+ # Handle file downloads (CSV export)
345
+ content_type = response.headers.get('content-type', '')
346
+ if 'text/csv' in content_type or 'application/octet-stream' in content_type:
347
+ # Extract filename from content disposition if available
348
+ content_disposition = response.headers.get('content-disposition', '')
349
+ filename = "cases_export.csv"
350
+ if 'filename=' in content_disposition:
351
+ filename = content_disposition.split('filename=')[1].strip('"')
352
+
353
+ # Count lines in CSV to get case count
354
+ csv_content = response.text
355
+ case_count = 0
356
+ if csv_content:
357
+ lines = csv_content.strip().split('\n')
358
+ case_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
359
+
360
+ return {
361
+ "success": True,
362
+ "result": {
363
+ "export_type": "csv_download",
364
+ "content_type": content_type,
365
+ "file_size": len(response.content),
366
+ "filename": filename,
367
+ "csv_content": csv_content,
368
+ "case_count": case_count,
369
+ "file_content": response.content # Binary content for file download
370
+ },
371
+ "statusCode": response.status_code,
372
+ "errors": []
373
+ }
374
+ else:
375
+ # Fallback to JSON parsing
376
+ return response.json()
377
+ else:
378
+ raise Exception(f"HTTP {response.status_code}: {response.text}")
379
+
380
+
381
+ class ExportCaseEndpointsCommand(Command[Dict[str, Any]]):
382
+ """Command to export case endpoints as a file download - Based on API documentation."""
383
+
384
+ def __init__(self, http_client: HTTPClient, case_id: str, filter_params: Optional[Dict[str, Any]] = None):
385
+ self.http_client = http_client
386
+ self.case_id = case_id
387
+ self.filter_params = filter_params or {}
388
+
389
+ def execute(self) -> Dict[str, Any]:
390
+ """Execute the export case endpoints command."""
391
+ import requests
392
+
393
+ # GET /api/public/cases/{id}/endpoints/export
394
+ # This endpoint returns a file download (CSV), so we need raw HTTP handling
395
+ headers = {
396
+ 'Authorization': f'Bearer {self.http_client.config.api_token}',
397
+ 'Content-Type': 'application/json'
398
+ }
399
+
400
+ url = f"{self.http_client.config.host}/api/public/cases/{self.case_id}/endpoints/export"
401
+
402
+ # Prepare query parameters - ensure organizationIds is included
403
+ params = {}
404
+ if self.filter_params:
405
+ params.update(self.filter_params)
406
+
407
+ # Ensure organizationIds is set if not provided
408
+ if 'filter[organizationIds]' not in params:
409
+ params['filter[organizationIds]'] = '0' # Default organization
410
+
411
+ response = requests.get(
412
+ url,
413
+ headers=headers,
414
+ params=params,
415
+ verify=self.http_client.config.verify_ssl,
416
+ timeout=self.http_client.config.timeout
417
+ )
418
+
419
+ if response.status_code == 200:
420
+ # Handle file downloads (CSV export)
421
+ content_type = response.headers.get('content-type', '')
422
+ if 'text/csv' in content_type or 'application/octet-stream' in content_type:
423
+ # Extract filename from content disposition if available
424
+ content_disposition = response.headers.get('content-disposition', '')
425
+ filename = "case_endpoints_export.csv"
426
+ if 'filename=' in content_disposition:
427
+ filename = content_disposition.split('filename=')[1].strip('"')
428
+
429
+ # Count lines in CSV to get endpoint count
430
+ csv_content = response.text
431
+ endpoint_count = 0
432
+ if csv_content:
433
+ lines = csv_content.strip().split('\n')
434
+ endpoint_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
435
+
436
+ return {
437
+ "success": True,
438
+ "result": {
439
+ "export_type": "csv_download",
440
+ "content_type": content_type,
441
+ "file_size": len(response.content),
442
+ "filename": filename,
443
+ "csv_content": csv_content,
444
+ "endpoint_count": endpoint_count,
445
+ "file_content": response.content # Binary content for file download
446
+ },
447
+ "statusCode": response.status_code,
448
+ "errors": []
449
+ }
450
+ else:
451
+ # Fallback to JSON parsing
452
+ return response.json()
453
+ else:
454
+ raise Exception(f"HTTP {response.status_code}: {response.text}")
455
+
456
+
457
+ class ExportCaseActivitiesCommand(Command[Dict[str, Any]]):
458
+ """Command to export case activities as a file download - Based on API documentation."""
459
+
460
+ def __init__(self, http_client: HTTPClient, case_id: str, filter_params: Optional[Dict[str, Any]] = None):
461
+ self.http_client = http_client
462
+ self.case_id = case_id
463
+ self.filter_params = filter_params or {}
464
+
465
+ def execute(self) -> Dict[str, Any]:
466
+ """Execute the export case activities command."""
467
+ import requests
468
+
469
+ # GET /api/public/cases/{id}/activities/export
470
+ # This endpoint returns a file download (CSV), so we need raw HTTP handling
471
+ headers = {
472
+ 'Authorization': f'Bearer {self.http_client.config.api_token}',
473
+ 'Content-Type': 'application/json'
474
+ }
475
+
476
+ url = f"{self.http_client.config.host}/api/public/cases/{self.case_id}/activities/export"
477
+
478
+ # Prepare query parameters - support pagination and filtering
479
+ params = {}
480
+ if self.filter_params:
481
+ params.update(self.filter_params)
482
+
483
+ # Set default pagination if not provided
484
+ if 'pageNumber' not in params:
485
+ params['pageNumber'] = '1'
486
+ if 'pageSize' not in params:
487
+ params['pageSize'] = '50' # Reasonable default for export
488
+ if 'sortBy' not in params:
489
+ params['sortBy'] = 'createdAt'
490
+ if 'sortType' not in params:
491
+ params['sortType'] = 'ASC'
492
+
493
+ response = requests.get(
494
+ url,
495
+ headers=headers,
496
+ params=params,
497
+ verify=self.http_client.config.verify_ssl,
498
+ timeout=self.http_client.config.timeout
499
+ )
500
+
501
+ if response.status_code == 200:
502
+ # Handle file downloads (ZIP/CSV export)
503
+ content_type = response.headers.get('content-type', '')
504
+
505
+ if ('text/csv' in content_type or 'application/octet-stream' in content_type or
506
+ 'application/vnd.ms-excel' in content_type or 'application/zip' in content_type):
507
+ # Extract filename from content disposition if available
508
+ content_disposition = response.headers.get('content-disposition', '')
509
+ filename = "case_activities_export"
510
+ if 'filename=' in content_disposition:
511
+ filename_part = content_disposition.split('filename=')[1].strip('"')
512
+ if 'filename*=' in content_disposition:
513
+ # Handle UTF-8 encoded filenames
514
+ filename_part = content_disposition.split("filename*=UTF-8''")[1] if "filename*=UTF-8''" in content_disposition else filename_part
515
+ filename = filename_part
516
+ elif 'application/zip' in content_type:
517
+ filename = "case_activities_export.zip"
518
+ elif 'text/csv' in content_type:
519
+ filename = "case_activities_export.csv"
520
+
521
+ # Determine export type and count activities
522
+ if 'application/zip' in content_type:
523
+ export_type = "zip_download"
524
+ # For ZIP files, we can't easily count activities without extracting
525
+ activity_count = "unknown_zip_content"
526
+ else:
527
+ export_type = "csv_download"
528
+ # Count lines in CSV to get activity count
529
+ csv_content = response.text
530
+ activity_count = 0
531
+ if csv_content:
532
+ lines = csv_content.strip().split('\n')
533
+ activity_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
534
+
535
+ result_data = {
536
+ "export_type": export_type,
537
+ "content_type": content_type,
538
+ "file_size": len(response.content),
539
+ "filename": filename,
540
+ "activity_count": activity_count,
541
+ "file_content": response.content # Binary content for file download
542
+ }
543
+
544
+ # Add content-specific fields
545
+ if export_type == "csv_download":
546
+ result_data["csv_content"] = response.text
547
+ elif export_type == "zip_download":
548
+ result_data["zip_content"] = response.content
549
+
550
+ return {
551
+ "success": True,
552
+ "result": result_data,
553
+ "statusCode": response.status_code,
554
+ "errors": []
555
+ }
556
+ else:
557
+ # Try to handle as text response first
558
+ try:
559
+ if response.text.strip():
560
+ # If it looks like JSON, try to parse it
561
+ if response.text.strip().startswith('{'):
562
+ return response.json()
563
+ else:
564
+ # Treat as plain text/CSV
565
+ return {
566
+ "success": True,
567
+ "result": {
568
+ "export_type": "text_download",
569
+ "content_type": content_type,
570
+ "file_size": len(response.content),
571
+ "filename": "case_activities_export.txt",
572
+ "text_content": response.text,
573
+ "activity_count": len(response.text.strip().split('\n')) - 1 if response.text.strip() else 0,
574
+ "file_content": response.content
575
+ },
576
+ "statusCode": response.status_code,
577
+ "errors": []
578
+ }
579
+ else:
580
+ # Empty response
581
+ return {
582
+ "success": True,
583
+ "result": {
584
+ "export_type": "empty_response",
585
+ "content_type": content_type,
586
+ "file_size": 0,
587
+ "filename": "empty_export",
588
+ "activity_count": 0,
589
+ "file_content": b""
590
+ },
591
+ "statusCode": response.status_code,
592
+ "errors": []
593
+ }
594
+ except Exception as e:
595
+ print(f"[DEBUG] Failed to parse response: {e}")
596
+ return {
597
+ "success": False,
598
+ "result": None,
599
+ "statusCode": response.status_code,
600
+ "errors": [f"Failed to parse response: {e}"]
601
+ }
602
+ else:
603
+ raise Exception(f"HTTP {response.status_code}: {response.text}")