binalyze-air-sdk 1.0.1__py3-none-any.whl → 1.0.3__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.
- binalyze_air/__init__.py +77 -77
- binalyze_air/apis/__init__.py +67 -27
- binalyze_air/apis/acquisitions.py +107 -0
- binalyze_air/apis/api_tokens.py +49 -0
- binalyze_air/apis/assets.py +161 -0
- binalyze_air/apis/audit_logs.py +26 -0
- binalyze_air/apis/{authentication.py → auth.py} +29 -27
- binalyze_air/apis/auto_asset_tags.py +79 -75
- binalyze_air/apis/backup.py +177 -0
- binalyze_air/apis/baseline.py +46 -0
- binalyze_air/apis/cases.py +225 -0
- binalyze_air/apis/cloud_forensics.py +116 -0
- binalyze_air/apis/event_subscription.py +96 -96
- binalyze_air/apis/evidence.py +249 -53
- binalyze_air/apis/interact.py +153 -36
- binalyze_air/apis/investigation_hub.py +234 -0
- binalyze_air/apis/license.py +104 -0
- binalyze_air/apis/logger.py +83 -0
- binalyze_air/apis/multipart_upload.py +201 -0
- binalyze_air/apis/notifications.py +115 -0
- binalyze_air/apis/organizations.py +267 -0
- binalyze_air/apis/params.py +44 -39
- binalyze_air/apis/policies.py +186 -0
- binalyze_air/apis/preset_filters.py +79 -0
- binalyze_air/apis/recent_activities.py +71 -0
- binalyze_air/apis/relay_server.py +104 -0
- binalyze_air/apis/settings.py +395 -27
- binalyze_air/apis/tasks.py +80 -0
- binalyze_air/apis/triage.py +197 -0
- binalyze_air/apis/user_management.py +183 -74
- binalyze_air/apis/webhook_executions.py +50 -0
- binalyze_air/apis/webhooks.py +322 -230
- binalyze_air/base.py +207 -133
- binalyze_air/client.py +217 -1337
- binalyze_air/commands/__init__.py +175 -145
- binalyze_air/commands/acquisitions.py +661 -387
- binalyze_air/commands/api_tokens.py +55 -0
- binalyze_air/commands/assets.py +324 -362
- binalyze_air/commands/{authentication.py → auth.py} +36 -36
- binalyze_air/commands/auto_asset_tags.py +230 -230
- binalyze_air/commands/backup.py +47 -0
- binalyze_air/commands/baseline.py +32 -396
- binalyze_air/commands/cases.py +609 -602
- binalyze_air/commands/cloud_forensics.py +88 -0
- binalyze_air/commands/event_subscription.py +101 -101
- binalyze_air/commands/evidences.py +918 -988
- binalyze_air/commands/interact.py +172 -58
- binalyze_air/commands/investigation_hub.py +315 -0
- binalyze_air/commands/license.py +183 -0
- binalyze_air/commands/logger.py +126 -0
- binalyze_air/commands/multipart_upload.py +363 -0
- binalyze_air/commands/notifications.py +45 -0
- binalyze_air/commands/organizations.py +200 -221
- binalyze_air/commands/policies.py +175 -203
- binalyze_air/commands/preset_filters.py +55 -0
- binalyze_air/commands/recent_activities.py +32 -0
- binalyze_air/commands/relay_server.py +144 -0
- binalyze_air/commands/settings.py +431 -29
- binalyze_air/commands/tasks.py +95 -56
- binalyze_air/commands/triage.py +224 -360
- binalyze_air/commands/user_management.py +351 -126
- binalyze_air/commands/webhook_executions.py +77 -0
- binalyze_air/config.py +244 -244
- binalyze_air/exceptions.py +49 -49
- binalyze_air/http_client.py +426 -305
- binalyze_air/models/__init__.py +287 -285
- binalyze_air/models/acquisitions.py +365 -250
- binalyze_air/models/api_tokens.py +73 -0
- binalyze_air/models/assets.py +438 -438
- binalyze_air/models/audit.py +247 -272
- binalyze_air/models/audit_logs.py +14 -0
- binalyze_air/models/{authentication.py → auth.py} +69 -69
- binalyze_air/models/auto_asset_tags.py +227 -116
- binalyze_air/models/backup.py +138 -0
- binalyze_air/models/baseline.py +231 -231
- binalyze_air/models/cases.py +275 -275
- binalyze_air/models/cloud_forensics.py +145 -0
- binalyze_air/models/event_subscription.py +170 -171
- binalyze_air/models/evidence.py +65 -65
- binalyze_air/models/evidences.py +367 -348
- binalyze_air/models/interact.py +266 -135
- binalyze_air/models/investigation_hub.py +265 -0
- binalyze_air/models/license.py +150 -0
- binalyze_air/models/logger.py +83 -0
- binalyze_air/models/multipart_upload.py +352 -0
- binalyze_air/models/notifications.py +138 -0
- binalyze_air/models/organizations.py +293 -293
- binalyze_air/models/params.py +153 -127
- binalyze_air/models/policies.py +260 -249
- binalyze_air/models/preset_filters.py +79 -0
- binalyze_air/models/recent_activities.py +70 -0
- binalyze_air/models/relay_server.py +121 -0
- binalyze_air/models/settings.py +538 -84
- binalyze_air/models/tasks.py +215 -149
- binalyze_air/models/triage.py +141 -142
- binalyze_air/models/user_management.py +200 -97
- binalyze_air/models/webhook_executions.py +33 -0
- binalyze_air/queries/__init__.py +121 -133
- binalyze_air/queries/acquisitions.py +155 -155
- binalyze_air/queries/api_tokens.py +46 -0
- binalyze_air/queries/assets.py +186 -105
- binalyze_air/queries/audit.py +400 -416
- binalyze_air/queries/{authentication.py → auth.py} +55 -55
- binalyze_air/queries/auto_asset_tags.py +59 -59
- binalyze_air/queries/backup.py +66 -0
- binalyze_air/queries/baseline.py +21 -185
- binalyze_air/queries/cases.py +292 -292
- binalyze_air/queries/cloud_forensics.py +137 -0
- binalyze_air/queries/event_subscription.py +54 -54
- binalyze_air/queries/evidence.py +139 -139
- binalyze_air/queries/evidences.py +279 -279
- binalyze_air/queries/interact.py +140 -28
- binalyze_air/queries/investigation_hub.py +329 -0
- binalyze_air/queries/license.py +85 -0
- binalyze_air/queries/logger.py +58 -0
- binalyze_air/queries/multipart_upload.py +180 -0
- binalyze_air/queries/notifications.py +71 -0
- binalyze_air/queries/organizations.py +222 -222
- binalyze_air/queries/params.py +154 -115
- binalyze_air/queries/policies.py +149 -149
- binalyze_air/queries/preset_filters.py +60 -0
- binalyze_air/queries/recent_activities.py +44 -0
- binalyze_air/queries/relay_server.py +42 -0
- binalyze_air/queries/settings.py +533 -20
- binalyze_air/queries/tasks.py +125 -81
- binalyze_air/queries/triage.py +230 -230
- binalyze_air/queries/user_management.py +193 -83
- binalyze_air/queries/webhook_executions.py +39 -0
- binalyze_air_sdk-1.0.3.dist-info/METADATA +752 -0
- binalyze_air_sdk-1.0.3.dist-info/RECORD +132 -0
- {binalyze_air_sdk-1.0.1.dist-info → binalyze_air_sdk-1.0.3.dist-info}/WHEEL +1 -1
- binalyze_air/apis/endpoints.py +0 -22
- binalyze_air/apis/evidences.py +0 -216
- binalyze_air/apis/users.py +0 -68
- binalyze_air/commands/users.py +0 -101
- binalyze_air/models/endpoints.py +0 -76
- binalyze_air/models/users.py +0 -82
- binalyze_air/queries/endpoints.py +0 -25
- binalyze_air/queries/users.py +0 -69
- binalyze_air_sdk-1.0.1.dist-info/METADATA +0 -635
- binalyze_air_sdk-1.0.1.dist-info/RECORD +0 -82
- {binalyze_air_sdk-1.0.1.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
binalyze_air/commands/cases.py
CHANGED
@@ -1,603 +1,610 @@
|
|
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
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
if
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
params=
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
filename
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
"
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
"
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
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, Union
|
7
|
+
|
8
|
+
from ..base import Command, ensure_organization_ids, format_organization_ids_param
|
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 with organization ID protection
|
331
|
+
params = {}
|
332
|
+
if self.filter_params:
|
333
|
+
params.update(self.filter_params)
|
334
|
+
|
335
|
+
# Ensure organizationIds is set using our utility functions
|
336
|
+
if 'filter[organizationIds]' not in params:
|
337
|
+
# Use our organization ID utilities to ensure proper formatting
|
338
|
+
org_ids = ensure_organization_ids(None) # Returns [0] if None
|
339
|
+
org_params = format_organization_ids_param(org_ids)
|
340
|
+
params.update(org_params)
|
341
|
+
|
342
|
+
response = requests.get(
|
343
|
+
url,
|
344
|
+
headers=headers,
|
345
|
+
params=params,
|
346
|
+
verify=self.http_client.config.verify_ssl,
|
347
|
+
timeout=self.http_client.config.timeout
|
348
|
+
)
|
349
|
+
|
350
|
+
if response.status_code == 200:
|
351
|
+
# Handle file downloads (CSV export)
|
352
|
+
content_type = response.headers.get('content-type', '')
|
353
|
+
if 'text/csv' in content_type or 'application/octet-stream' in content_type:
|
354
|
+
# Extract filename from content disposition if available
|
355
|
+
content_disposition = response.headers.get('content-disposition', '')
|
356
|
+
filename = "cases_export.csv"
|
357
|
+
if 'filename=' in content_disposition:
|
358
|
+
filename = content_disposition.split('filename=')[1].strip('"')
|
359
|
+
|
360
|
+
# Count lines in CSV to get case count
|
361
|
+
csv_content = response.text
|
362
|
+
case_count = 0
|
363
|
+
if csv_content:
|
364
|
+
lines = csv_content.strip().split('\n')
|
365
|
+
case_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
|
366
|
+
|
367
|
+
return {
|
368
|
+
"success": True,
|
369
|
+
"result": {
|
370
|
+
"export_type": "csv_download",
|
371
|
+
"content_type": content_type,
|
372
|
+
"file_size": len(response.content),
|
373
|
+
"filename": filename,
|
374
|
+
"csv_content": csv_content,
|
375
|
+
"case_count": case_count,
|
376
|
+
"file_content": response.content # Binary content for file download
|
377
|
+
},
|
378
|
+
"statusCode": response.status_code,
|
379
|
+
"errors": []
|
380
|
+
}
|
381
|
+
else:
|
382
|
+
# Fallback to JSON parsing
|
383
|
+
return response.json()
|
384
|
+
else:
|
385
|
+
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
386
|
+
|
387
|
+
|
388
|
+
class ExportCaseEndpointsCommand(Command[Dict[str, Any]]):
|
389
|
+
"""Command to export case endpoints as a file download - Based on API documentation."""
|
390
|
+
|
391
|
+
def __init__(self, http_client: HTTPClient, case_id: str, filter_params: Optional[Dict[str, Any]] = None):
|
392
|
+
self.http_client = http_client
|
393
|
+
self.case_id = case_id
|
394
|
+
self.filter_params = filter_params or {}
|
395
|
+
|
396
|
+
def execute(self) -> Dict[str, Any]:
|
397
|
+
"""Execute the export case endpoints command."""
|
398
|
+
import requests
|
399
|
+
|
400
|
+
# GET /api/public/cases/{id}/endpoints/export
|
401
|
+
# This endpoint returns a file download (CSV), so we need raw HTTP handling
|
402
|
+
headers = {
|
403
|
+
'Authorization': f'Bearer {self.http_client.config.api_token}',
|
404
|
+
'Content-Type': 'application/json'
|
405
|
+
}
|
406
|
+
|
407
|
+
url = f"{self.http_client.config.host}/api/public/cases/{self.case_id}/endpoints/export"
|
408
|
+
|
409
|
+
# Prepare query parameters - ensure organizationIds is included
|
410
|
+
params = {}
|
411
|
+
if self.filter_params:
|
412
|
+
params.update(self.filter_params)
|
413
|
+
|
414
|
+
# Ensure organizationIds is set if not provided
|
415
|
+
if 'filter[organizationIds]' not in params:
|
416
|
+
params['filter[organizationIds]'] = '0' # Default organization
|
417
|
+
|
418
|
+
response = requests.get(
|
419
|
+
url,
|
420
|
+
headers=headers,
|
421
|
+
params=params,
|
422
|
+
verify=self.http_client.config.verify_ssl,
|
423
|
+
timeout=self.http_client.config.timeout
|
424
|
+
)
|
425
|
+
|
426
|
+
if response.status_code == 200:
|
427
|
+
# Handle file downloads (CSV export)
|
428
|
+
content_type = response.headers.get('content-type', '')
|
429
|
+
if 'text/csv' in content_type or 'application/octet-stream' in content_type:
|
430
|
+
# Extract filename from content disposition if available
|
431
|
+
content_disposition = response.headers.get('content-disposition', '')
|
432
|
+
filename = "case_endpoints_export.csv"
|
433
|
+
if 'filename=' in content_disposition:
|
434
|
+
filename = content_disposition.split('filename=')[1].strip('"')
|
435
|
+
|
436
|
+
# Count lines in CSV to get endpoint count
|
437
|
+
csv_content = response.text
|
438
|
+
endpoint_count = 0
|
439
|
+
if csv_content:
|
440
|
+
lines = csv_content.strip().split('\n')
|
441
|
+
endpoint_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
|
442
|
+
|
443
|
+
return {
|
444
|
+
"success": True,
|
445
|
+
"result": {
|
446
|
+
"export_type": "csv_download",
|
447
|
+
"content_type": content_type,
|
448
|
+
"file_size": len(response.content),
|
449
|
+
"filename": filename,
|
450
|
+
"csv_content": csv_content,
|
451
|
+
"endpoint_count": endpoint_count,
|
452
|
+
"file_content": response.content # Binary content for file download
|
453
|
+
},
|
454
|
+
"statusCode": response.status_code,
|
455
|
+
"errors": []
|
456
|
+
}
|
457
|
+
else:
|
458
|
+
# Fallback to JSON parsing
|
459
|
+
return response.json()
|
460
|
+
else:
|
461
|
+
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
462
|
+
|
463
|
+
|
464
|
+
class ExportCaseActivitiesCommand(Command[Dict[str, Any]]):
|
465
|
+
"""Command to export case activities as a file download - Based on API documentation."""
|
466
|
+
|
467
|
+
def __init__(self, http_client: HTTPClient, case_id: str, filter_params: Optional[Dict[str, Any]] = None):
|
468
|
+
self.http_client = http_client
|
469
|
+
self.case_id = case_id
|
470
|
+
self.filter_params = filter_params or {}
|
471
|
+
|
472
|
+
def execute(self) -> Dict[str, Any]:
|
473
|
+
"""Execute the export case activities command."""
|
474
|
+
import requests
|
475
|
+
|
476
|
+
# GET /api/public/cases/{id}/activities/export
|
477
|
+
# This endpoint returns a file download (CSV), so we need raw HTTP handling
|
478
|
+
headers = {
|
479
|
+
'Authorization': f'Bearer {self.http_client.config.api_token}',
|
480
|
+
'Content-Type': 'application/json'
|
481
|
+
}
|
482
|
+
|
483
|
+
url = f"{self.http_client.config.host}/api/public/cases/{self.case_id}/activities/export"
|
484
|
+
|
485
|
+
# Prepare query parameters - support pagination and filtering
|
486
|
+
params = {}
|
487
|
+
if self.filter_params:
|
488
|
+
params.update(self.filter_params)
|
489
|
+
|
490
|
+
# Set default pagination if not provided
|
491
|
+
if 'pageNumber' not in params:
|
492
|
+
params['pageNumber'] = '1'
|
493
|
+
if 'pageSize' not in params:
|
494
|
+
params['pageSize'] = '50' # Reasonable default for export
|
495
|
+
if 'sortBy' not in params:
|
496
|
+
params['sortBy'] = 'createdAt'
|
497
|
+
if 'sortType' not in params:
|
498
|
+
params['sortType'] = 'ASC'
|
499
|
+
|
500
|
+
response = requests.get(
|
501
|
+
url,
|
502
|
+
headers=headers,
|
503
|
+
params=params,
|
504
|
+
verify=self.http_client.config.verify_ssl,
|
505
|
+
timeout=self.http_client.config.timeout
|
506
|
+
)
|
507
|
+
|
508
|
+
if response.status_code == 200:
|
509
|
+
# Handle file downloads (ZIP/CSV export)
|
510
|
+
content_type = response.headers.get('content-type', '')
|
511
|
+
|
512
|
+
if ('text/csv' in content_type or 'application/octet-stream' in content_type or
|
513
|
+
'application/vnd.ms-excel' in content_type or 'application/zip' in content_type):
|
514
|
+
# Extract filename from content disposition if available
|
515
|
+
content_disposition = response.headers.get('content-disposition', '')
|
516
|
+
filename = "case_activities_export"
|
517
|
+
if 'filename=' in content_disposition:
|
518
|
+
filename_part = content_disposition.split('filename=')[1].strip('"')
|
519
|
+
if 'filename*=' in content_disposition:
|
520
|
+
# Handle UTF-8 encoded filenames
|
521
|
+
filename_part = content_disposition.split("filename*=UTF-8''")[1] if "filename*=UTF-8''" in content_disposition else filename_part
|
522
|
+
filename = filename_part
|
523
|
+
elif 'application/zip' in content_type:
|
524
|
+
filename = "case_activities_export.zip"
|
525
|
+
elif 'text/csv' in content_type:
|
526
|
+
filename = "case_activities_export.csv"
|
527
|
+
|
528
|
+
# Determine export type and count activities
|
529
|
+
if 'application/zip' in content_type:
|
530
|
+
export_type = "zip_download"
|
531
|
+
# For ZIP files, we can't easily count activities without extracting
|
532
|
+
activity_count = "unknown_zip_content"
|
533
|
+
else:
|
534
|
+
export_type = "csv_download"
|
535
|
+
# Count lines in CSV to get activity count
|
536
|
+
csv_content = response.text
|
537
|
+
activity_count = 0
|
538
|
+
if csv_content:
|
539
|
+
lines = csv_content.strip().split('\n')
|
540
|
+
activity_count = len(lines) - 1 if len(lines) > 0 else 0 # Subtract header
|
541
|
+
|
542
|
+
result_data = {
|
543
|
+
"export_type": export_type,
|
544
|
+
"content_type": content_type,
|
545
|
+
"file_size": len(response.content),
|
546
|
+
"filename": filename,
|
547
|
+
"activity_count": activity_count,
|
548
|
+
"file_content": response.content # Binary content for file download
|
549
|
+
}
|
550
|
+
|
551
|
+
# Add content-specific fields
|
552
|
+
if export_type == "csv_download":
|
553
|
+
result_data["csv_content"] = response.text
|
554
|
+
elif export_type == "zip_download":
|
555
|
+
result_data["zip_content"] = response.content
|
556
|
+
|
557
|
+
return {
|
558
|
+
"success": True,
|
559
|
+
"result": result_data,
|
560
|
+
"statusCode": response.status_code,
|
561
|
+
"errors": []
|
562
|
+
}
|
563
|
+
else:
|
564
|
+
# Try to handle as text response first
|
565
|
+
try:
|
566
|
+
if response.text.strip():
|
567
|
+
# If it looks like JSON, try to parse it
|
568
|
+
if response.text.strip().startswith('{'):
|
569
|
+
return response.json()
|
570
|
+
else:
|
571
|
+
# Treat as plain text/CSV
|
572
|
+
return {
|
573
|
+
"success": True,
|
574
|
+
"result": {
|
575
|
+
"export_type": "text_download",
|
576
|
+
"content_type": content_type,
|
577
|
+
"file_size": len(response.content),
|
578
|
+
"filename": "case_activities_export.txt",
|
579
|
+
"text_content": response.text,
|
580
|
+
"activity_count": len(response.text.strip().split('\n')) - 1 if response.text.strip() else 0,
|
581
|
+
"file_content": response.content
|
582
|
+
},
|
583
|
+
"statusCode": response.status_code,
|
584
|
+
"errors": []
|
585
|
+
}
|
586
|
+
else:
|
587
|
+
# Empty response
|
588
|
+
return {
|
589
|
+
"success": True,
|
590
|
+
"result": {
|
591
|
+
"export_type": "empty_response",
|
592
|
+
"content_type": content_type,
|
593
|
+
"file_size": 0,
|
594
|
+
"filename": "empty_export",
|
595
|
+
"activity_count": 0,
|
596
|
+
"file_content": b""
|
597
|
+
},
|
598
|
+
"statusCode": response.status_code,
|
599
|
+
"errors": []
|
600
|
+
}
|
601
|
+
except Exception as e:
|
602
|
+
print(f"[DEBUG] Failed to parse response: {e}")
|
603
|
+
return {
|
604
|
+
"success": False,
|
605
|
+
"result": None,
|
606
|
+
"statusCode": response.status_code,
|
607
|
+
"errors": [f"Failed to parse response: {e}"]
|
608
|
+
}
|
609
|
+
else:
|
603
610
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|