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.
Files changed (142) hide show
  1. binalyze_air/__init__.py +77 -77
  2. binalyze_air/apis/__init__.py +67 -27
  3. binalyze_air/apis/acquisitions.py +107 -0
  4. binalyze_air/apis/api_tokens.py +49 -0
  5. binalyze_air/apis/assets.py +161 -0
  6. binalyze_air/apis/audit_logs.py +26 -0
  7. binalyze_air/apis/{authentication.py → auth.py} +29 -27
  8. binalyze_air/apis/auto_asset_tags.py +79 -75
  9. binalyze_air/apis/backup.py +177 -0
  10. binalyze_air/apis/baseline.py +46 -0
  11. binalyze_air/apis/cases.py +225 -0
  12. binalyze_air/apis/cloud_forensics.py +116 -0
  13. binalyze_air/apis/event_subscription.py +96 -96
  14. binalyze_air/apis/evidence.py +249 -53
  15. binalyze_air/apis/interact.py +153 -36
  16. binalyze_air/apis/investigation_hub.py +234 -0
  17. binalyze_air/apis/license.py +104 -0
  18. binalyze_air/apis/logger.py +83 -0
  19. binalyze_air/apis/multipart_upload.py +201 -0
  20. binalyze_air/apis/notifications.py +115 -0
  21. binalyze_air/apis/organizations.py +267 -0
  22. binalyze_air/apis/params.py +44 -39
  23. binalyze_air/apis/policies.py +186 -0
  24. binalyze_air/apis/preset_filters.py +79 -0
  25. binalyze_air/apis/recent_activities.py +71 -0
  26. binalyze_air/apis/relay_server.py +104 -0
  27. binalyze_air/apis/settings.py +395 -27
  28. binalyze_air/apis/tasks.py +80 -0
  29. binalyze_air/apis/triage.py +197 -0
  30. binalyze_air/apis/user_management.py +183 -74
  31. binalyze_air/apis/webhook_executions.py +50 -0
  32. binalyze_air/apis/webhooks.py +322 -230
  33. binalyze_air/base.py +207 -133
  34. binalyze_air/client.py +217 -1337
  35. binalyze_air/commands/__init__.py +175 -145
  36. binalyze_air/commands/acquisitions.py +661 -387
  37. binalyze_air/commands/api_tokens.py +55 -0
  38. binalyze_air/commands/assets.py +324 -362
  39. binalyze_air/commands/{authentication.py → auth.py} +36 -36
  40. binalyze_air/commands/auto_asset_tags.py +230 -230
  41. binalyze_air/commands/backup.py +47 -0
  42. binalyze_air/commands/baseline.py +32 -396
  43. binalyze_air/commands/cases.py +609 -602
  44. binalyze_air/commands/cloud_forensics.py +88 -0
  45. binalyze_air/commands/event_subscription.py +101 -101
  46. binalyze_air/commands/evidences.py +918 -988
  47. binalyze_air/commands/interact.py +172 -58
  48. binalyze_air/commands/investigation_hub.py +315 -0
  49. binalyze_air/commands/license.py +183 -0
  50. binalyze_air/commands/logger.py +126 -0
  51. binalyze_air/commands/multipart_upload.py +363 -0
  52. binalyze_air/commands/notifications.py +45 -0
  53. binalyze_air/commands/organizations.py +200 -221
  54. binalyze_air/commands/policies.py +175 -203
  55. binalyze_air/commands/preset_filters.py +55 -0
  56. binalyze_air/commands/recent_activities.py +32 -0
  57. binalyze_air/commands/relay_server.py +144 -0
  58. binalyze_air/commands/settings.py +431 -29
  59. binalyze_air/commands/tasks.py +95 -56
  60. binalyze_air/commands/triage.py +224 -360
  61. binalyze_air/commands/user_management.py +351 -126
  62. binalyze_air/commands/webhook_executions.py +77 -0
  63. binalyze_air/config.py +244 -244
  64. binalyze_air/exceptions.py +49 -49
  65. binalyze_air/http_client.py +426 -305
  66. binalyze_air/models/__init__.py +287 -285
  67. binalyze_air/models/acquisitions.py +365 -250
  68. binalyze_air/models/api_tokens.py +73 -0
  69. binalyze_air/models/assets.py +438 -438
  70. binalyze_air/models/audit.py +247 -272
  71. binalyze_air/models/audit_logs.py +14 -0
  72. binalyze_air/models/{authentication.py → auth.py} +69 -69
  73. binalyze_air/models/auto_asset_tags.py +227 -116
  74. binalyze_air/models/backup.py +138 -0
  75. binalyze_air/models/baseline.py +231 -231
  76. binalyze_air/models/cases.py +275 -275
  77. binalyze_air/models/cloud_forensics.py +145 -0
  78. binalyze_air/models/event_subscription.py +170 -171
  79. binalyze_air/models/evidence.py +65 -65
  80. binalyze_air/models/evidences.py +367 -348
  81. binalyze_air/models/interact.py +266 -135
  82. binalyze_air/models/investigation_hub.py +265 -0
  83. binalyze_air/models/license.py +150 -0
  84. binalyze_air/models/logger.py +83 -0
  85. binalyze_air/models/multipart_upload.py +352 -0
  86. binalyze_air/models/notifications.py +138 -0
  87. binalyze_air/models/organizations.py +293 -293
  88. binalyze_air/models/params.py +153 -127
  89. binalyze_air/models/policies.py +260 -249
  90. binalyze_air/models/preset_filters.py +79 -0
  91. binalyze_air/models/recent_activities.py +70 -0
  92. binalyze_air/models/relay_server.py +121 -0
  93. binalyze_air/models/settings.py +538 -84
  94. binalyze_air/models/tasks.py +215 -149
  95. binalyze_air/models/triage.py +141 -142
  96. binalyze_air/models/user_management.py +200 -97
  97. binalyze_air/models/webhook_executions.py +33 -0
  98. binalyze_air/queries/__init__.py +121 -133
  99. binalyze_air/queries/acquisitions.py +155 -155
  100. binalyze_air/queries/api_tokens.py +46 -0
  101. binalyze_air/queries/assets.py +186 -105
  102. binalyze_air/queries/audit.py +400 -416
  103. binalyze_air/queries/{authentication.py → auth.py} +55 -55
  104. binalyze_air/queries/auto_asset_tags.py +59 -59
  105. binalyze_air/queries/backup.py +66 -0
  106. binalyze_air/queries/baseline.py +21 -185
  107. binalyze_air/queries/cases.py +292 -292
  108. binalyze_air/queries/cloud_forensics.py +137 -0
  109. binalyze_air/queries/event_subscription.py +54 -54
  110. binalyze_air/queries/evidence.py +139 -139
  111. binalyze_air/queries/evidences.py +279 -279
  112. binalyze_air/queries/interact.py +140 -28
  113. binalyze_air/queries/investigation_hub.py +329 -0
  114. binalyze_air/queries/license.py +85 -0
  115. binalyze_air/queries/logger.py +58 -0
  116. binalyze_air/queries/multipart_upload.py +180 -0
  117. binalyze_air/queries/notifications.py +71 -0
  118. binalyze_air/queries/organizations.py +222 -222
  119. binalyze_air/queries/params.py +154 -115
  120. binalyze_air/queries/policies.py +149 -149
  121. binalyze_air/queries/preset_filters.py +60 -0
  122. binalyze_air/queries/recent_activities.py +44 -0
  123. binalyze_air/queries/relay_server.py +42 -0
  124. binalyze_air/queries/settings.py +533 -20
  125. binalyze_air/queries/tasks.py +125 -81
  126. binalyze_air/queries/triage.py +230 -230
  127. binalyze_air/queries/user_management.py +193 -83
  128. binalyze_air/queries/webhook_executions.py +39 -0
  129. binalyze_air_sdk-1.0.3.dist-info/METADATA +752 -0
  130. binalyze_air_sdk-1.0.3.dist-info/RECORD +132 -0
  131. {binalyze_air_sdk-1.0.1.dist-info → binalyze_air_sdk-1.0.3.dist-info}/WHEEL +1 -1
  132. binalyze_air/apis/endpoints.py +0 -22
  133. binalyze_air/apis/evidences.py +0 -216
  134. binalyze_air/apis/users.py +0 -68
  135. binalyze_air/commands/users.py +0 -101
  136. binalyze_air/models/endpoints.py +0 -76
  137. binalyze_air/models/users.py +0 -82
  138. binalyze_air/queries/endpoints.py +0 -25
  139. binalyze_air/queries/users.py +0 -69
  140. binalyze_air_sdk-1.0.1.dist-info/METADATA +0 -635
  141. binalyze_air_sdk-1.0.1.dist-info/RECORD +0 -82
  142. {binalyze_air_sdk-1.0.1.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
1
+ """License command classes for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from ..models.license import License
5
+ from ..queries.license import GetLicenseQuery, SetLicenseQuery
6
+
7
+
8
+ class LicenseCommand:
9
+ """Base command class for license operations."""
10
+
11
+ def __init__(self, api_client):
12
+ """Initialize license command.
13
+
14
+ Args:
15
+ api_client: The License API client instance
16
+ """
17
+ self._api_client = api_client
18
+
19
+
20
+ class GetLicenseCommand(LicenseCommand):
21
+ """Command for retrieving current license information.
22
+
23
+ This command provides comprehensive license details including
24
+ expiration status, usage statistics, and validation.
25
+ """
26
+
27
+ def execute(self) -> License:
28
+ """Execute get license command.
29
+
30
+ Returns:
31
+ License object with current license information
32
+
33
+ Raises:
34
+ APIError: If the request fails
35
+ """
36
+ query = GetLicenseQuery()
37
+ response = self._api_client._get_license_query(query)
38
+
39
+ if response.get('success') and response.get('result'):
40
+ return License.from_dict(response['result'])
41
+ else:
42
+ raise ValueError(
43
+ f"Failed to get license: {response.get('errors', [])}"
44
+ )
45
+
46
+ def get_license_status(self) -> Dict[str, Any]:
47
+ """Get comprehensive license status information.
48
+
49
+ Returns:
50
+ Dictionary with license status, usage, and warnings
51
+ """
52
+ license_info = self.execute()
53
+ usage_stats = license_info.get_usage_percentage()
54
+
55
+ status = {
56
+ 'license_key': license_info.key,
57
+ 'customer_name': license_info.customer_name,
58
+ 'is_active': not license_info.is_expired(),
59
+ 'is_lifetime': license_info.is_lifetime,
60
+ 'expires_on': license_info.expires_on.isoformat(),
61
+ 'remaining_days': license_info.remaining_days,
62
+ 'device_usage': {
63
+ 'current': license_info.device_count,
64
+ 'maximum': license_info.max_device_count,
65
+ 'percentage': usage_stats['device_usage_percent']
66
+ },
67
+ 'client_usage': {
68
+ 'current': license_info.client_count,
69
+ 'maximum': license_info.max_client_count,
70
+ 'percentage': usage_stats['client_usage_percent']
71
+ },
72
+ 'warnings': []
73
+ }
74
+
75
+ # Add warnings for various conditions
76
+ if license_info.is_expired():
77
+ status['warnings'].append('License has expired')
78
+ elif license_info.is_near_expiry(30):
79
+ status['warnings'].append(
80
+ f'License expires in {license_info.remaining_days} days'
81
+ )
82
+
83
+ if usage_stats['device_usage_percent'] > 80:
84
+ status['warnings'].append(
85
+ f'Device usage at {usage_stats["device_usage_percent"]}%'
86
+ )
87
+
88
+ if usage_stats['client_usage_percent'] > 80:
89
+ status['warnings'].append(
90
+ f'Client usage at {usage_stats["client_usage_percent"]}%'
91
+ )
92
+
93
+ if license_info.is_locked_down:
94
+ status['warnings'].append('License is in locked down mode')
95
+
96
+ return status
97
+
98
+
99
+ class SetLicenseCommand(LicenseCommand):
100
+ """Command for setting/updating license key.
101
+
102
+ This command allows updating the system license key
103
+ and validates the operation.
104
+ """
105
+
106
+ def __init__(self, api_client, license_key: str):
107
+ """Initialize set license command.
108
+
109
+ Args:
110
+ api_client: The License API client instance
111
+ license_key: The new license key to set
112
+ """
113
+ super().__init__(api_client)
114
+ self._license_key = license_key
115
+
116
+ def execute(self) -> bool:
117
+ """Execute set license command.
118
+
119
+ Returns:
120
+ True if license was set successfully
121
+
122
+ Raises:
123
+ APIError: If the request fails
124
+ ValueError: If license key is invalid
125
+ """
126
+ if not self._license_key or not self._license_key.strip():
127
+ raise ValueError("License key cannot be empty")
128
+
129
+ query = SetLicenseQuery(self._license_key)
130
+ response = self._api_client._set_license_query(query)
131
+
132
+ if response.get('success'):
133
+ return True
134
+ else:
135
+ raise ValueError(
136
+ f"Failed to set license: {response.get('errors', [])}"
137
+ )
138
+
139
+ def set_and_verify(self) -> Dict[str, Any]:
140
+ """Set license key and verify the operation.
141
+
142
+ Returns:
143
+ Dictionary with operation result and new license status
144
+ """
145
+ # Set the license
146
+ success = self.execute()
147
+
148
+ if success:
149
+ # Verify by getting current license info
150
+ get_command = GetLicenseCommand(self._api_client)
151
+ try:
152
+ new_license = get_command.execute()
153
+ return {
154
+ 'success': True,
155
+ 'message': 'License updated successfully',
156
+ 'license_info': {
157
+ 'key': new_license.key,
158
+ 'customer_name': new_license.customer_name,
159
+ 'expires_on': new_license.expires_on.isoformat(),
160
+ 'remaining_days': new_license.remaining_days,
161
+ 'is_active': not new_license.is_expired()
162
+ }
163
+ }
164
+ except Exception as e:
165
+ return {
166
+ 'success': True,
167
+ 'message': 'License set but verification failed',
168
+ 'warning': str(e)
169
+ }
170
+
171
+ return {
172
+ 'success': False,
173
+ 'message': 'Failed to set license'
174
+ }
175
+
176
+ @property
177
+ def license_key(self) -> str:
178
+ """Get the license key to be set.
179
+
180
+ Returns:
181
+ The license key
182
+ """
183
+ return self._license_key
@@ -0,0 +1,126 @@
1
+ """Logger command classes for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from pathlib import Path
5
+ from ..models.logger import LogDownloadResponse
6
+ from ..queries.logger import DownloadLogsQuery
7
+
8
+
9
+ class LoggerCommand:
10
+ """Base command class for logger operations."""
11
+
12
+ def __init__(self, api_client):
13
+ """Initialize logger command.
14
+
15
+ Args:
16
+ api_client: The Logger API client instance
17
+ """
18
+ self._api_client = api_client
19
+
20
+
21
+ class DownloadLogsCommand(LoggerCommand):
22
+ """Command for downloading application logs as ZIP file.
23
+
24
+ This command provides comprehensive log download functionality
25
+ with options for latest logs only and automatic file saving.
26
+ """
27
+
28
+ def __init__(self, api_client, latest_log_file: bool = False):
29
+ """Initialize download logs command.
30
+
31
+ Args:
32
+ api_client: The Logger API client instance
33
+ latest_log_file: Whether to download only the latest log file
34
+ """
35
+ super().__init__(api_client)
36
+ self._latest_log_file = latest_log_file
37
+
38
+ def execute(self) -> LogDownloadResponse:
39
+ """Execute download logs command.
40
+
41
+ Returns:
42
+ LogDownloadResponse with downloaded log data
43
+
44
+ Raises:
45
+ APIError: If the request fails
46
+ """
47
+ query = DownloadLogsQuery(self._latest_log_file)
48
+ response_data, headers = self._api_client._download_logs_query(query)
49
+
50
+ # Create response object from binary data and headers
51
+ return LogDownloadResponse.from_response(response_data, headers)
52
+
53
+ def download_and_save(self, save_path: Optional[str] = None) -> Dict[str, Any]:
54
+ """Download logs and save to file.
55
+
56
+ Args:
57
+ save_path: Optional path where to save the logs.
58
+ If not provided, saves to current directory.
59
+
60
+ Returns:
61
+ Dictionary with download result and file information
62
+ """
63
+ try:
64
+ # Download the logs
65
+ log_response = self.execute()
66
+
67
+ # Determine save path
68
+ if save_path is None:
69
+ save_path = log_response.filename or 'air_logs.zip'
70
+
71
+ # Ensure directory exists
72
+ Path(save_path).parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Save to file
75
+ success = log_response.save_to_file(save_path)
76
+
77
+ return {
78
+ 'success': success,
79
+ 'file_path': save_path,
80
+ 'file_size': log_response.size,
81
+ 'content_type': log_response.content_type,
82
+ 'filename': log_response.filename,
83
+ 'latest_only': self._latest_log_file
84
+ }
85
+
86
+ except Exception as e:
87
+ return {
88
+ 'success': False,
89
+ 'error': str(e),
90
+ 'file_path': save_path,
91
+ 'latest_only': self._latest_log_file
92
+ }
93
+
94
+ def get_log_info(self) -> Dict[str, Any]:
95
+ """Get information about available logs without downloading.
96
+
97
+ Returns:
98
+ Dictionary with log information
99
+ """
100
+ try:
101
+ # Download logs to get metadata
102
+ log_response = self.execute()
103
+
104
+ return {
105
+ 'available': True,
106
+ 'size': log_response.size,
107
+ 'content_type': log_response.content_type,
108
+ 'filename': log_response.filename,
109
+ 'latest_only': self._latest_log_file
110
+ }
111
+
112
+ except Exception as e:
113
+ return {
114
+ 'available': False,
115
+ 'error': str(e),
116
+ 'latest_only': self._latest_log_file
117
+ }
118
+
119
+ @property
120
+ def latest_log_file(self) -> bool:
121
+ """Get the latest log file flag.
122
+
123
+ Returns:
124
+ Whether downloading only the latest log file
125
+ """
126
+ return self._latest_log_file
@@ -0,0 +1,363 @@
1
+ """Multipart Upload command classes for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional, Dict, Any, List, Callable
4
+ from pathlib import Path
5
+ import time
6
+ from ..models.multipart_upload import (
7
+ UploadInitializeResponse, UploadPartResponse, UploadStatusResponse,
8
+ UploadFinalizeResponse, MultipartUploadSession, FileChunker
9
+ )
10
+ from ..queries.multipart_upload import (
11
+ InitializeUploadQuery, UploadPartQuery, CheckUploadStatusQuery, FinalizeUploadQuery
12
+ )
13
+
14
+
15
+ class MultipartUploadCommand:
16
+ """Base command class for multipart upload operations."""
17
+
18
+ def __init__(self, api_client):
19
+ """Initialize multipart upload command.
20
+
21
+ Args:
22
+ api_client: The Multipart Upload API client instance
23
+ """
24
+ self._api_client = api_client
25
+
26
+
27
+ class InitializeUploadCommand(MultipartUploadCommand):
28
+ """Command for initializing a multipart upload session."""
29
+
30
+ def execute(self) -> UploadInitializeResponse:
31
+ """Execute upload initialization command.
32
+
33
+ Returns:
34
+ UploadInitializeResponse with file ID for the session
35
+
36
+ Raises:
37
+ APIError: If the request fails
38
+ """
39
+ query = InitializeUploadQuery()
40
+ response_data = self._api_client._initialize_upload_query(query)
41
+
42
+ return UploadInitializeResponse.from_dict(response_data)
43
+
44
+
45
+ class UploadPartCommand(MultipartUploadCommand):
46
+ """Command for uploading a file part in a multipart upload."""
47
+
48
+ def __init__(self, api_client, file_id: str, part_number: int, file_data: bytes, filename: Optional[str] = None):
49
+ """Initialize upload part command.
50
+
51
+ Args:
52
+ api_client: The Multipart Upload API client instance
53
+ file_id: Upload session identifier
54
+ part_number: Sequential part number (starting from 1)
55
+ file_data: Binary file data for this part
56
+ filename: Optional filename for the part
57
+ """
58
+ super().__init__(api_client)
59
+ self._file_id = file_id
60
+ self._part_number = part_number
61
+ self._file_data = file_data
62
+ self._filename = filename
63
+
64
+ def execute(self) -> UploadPartResponse:
65
+ """Execute upload part command.
66
+
67
+ Returns:
68
+ UploadPartResponse indicating success/failure
69
+
70
+ Raises:
71
+ APIError: If the request fails
72
+ """
73
+ query = UploadPartQuery(self._file_id, self._part_number, self._file_data, self._filename)
74
+ response_data = self._api_client._upload_part_query(query)
75
+
76
+ return UploadPartResponse.from_dict(response_data, self._part_number)
77
+
78
+
79
+ class CheckUploadStatusCommand(MultipartUploadCommand):
80
+ """Command for checking upload status."""
81
+
82
+ def __init__(self, api_client, file_id: str):
83
+ """Initialize upload status check command.
84
+
85
+ Args:
86
+ api_client: The Multipart Upload API client instance
87
+ file_id: Upload session identifier to check
88
+ """
89
+ super().__init__(api_client)
90
+ self._file_id = file_id
91
+
92
+ def execute(self) -> UploadStatusResponse:
93
+ """Execute upload status check command.
94
+
95
+ Returns:
96
+ UploadStatusResponse with ready status
97
+
98
+ Raises:
99
+ APIError: If the request fails
100
+ """
101
+ query = CheckUploadStatusQuery(self._file_id)
102
+ response_data = self._api_client._check_upload_status_query(query)
103
+
104
+ return UploadStatusResponse.from_dict(response_data, self._file_id)
105
+
106
+
107
+ class FinalizeUploadCommand(MultipartUploadCommand):
108
+ """Command for finalizing a multipart upload."""
109
+
110
+ def __init__(self, api_client, file_id: str):
111
+ """Initialize upload finalization command.
112
+
113
+ Args:
114
+ api_client: The Multipart Upload API client instance
115
+ file_id: Upload session identifier to finalize
116
+ """
117
+ super().__init__(api_client)
118
+ self._file_id = file_id
119
+
120
+ def execute(self) -> UploadFinalizeResponse:
121
+ """Execute upload finalization command.
122
+
123
+ Returns:
124
+ UploadFinalizeResponse indicating success/failure
125
+
126
+ Raises:
127
+ APIError: If the request fails
128
+ """
129
+ query = FinalizeUploadQuery(self._file_id)
130
+ response_data = self._api_client._finalize_upload_query(query)
131
+
132
+ return UploadFinalizeResponse.from_dict(response_data, self._file_id)
133
+
134
+
135
+ class CompleteFileUploadCommand(MultipartUploadCommand):
136
+ """Command for uploading a complete file using multipart upload.
137
+
138
+ This command orchestrates the entire upload process:
139
+ 1. Initialize upload session
140
+ 2. Upload all file parts
141
+ 3. Check upload status
142
+ 4. Finalize upload
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ api_client,
148
+ file_path: str,
149
+ chunk_size: int = 5 * 1024 * 1024,
150
+ progress_callback: Optional[Callable[[float, int, int], None]] = None
151
+ ):
152
+ """Initialize complete file upload command.
153
+
154
+ Args:
155
+ api_client: The Multipart Upload API client instance
156
+ file_path: Path to the file to upload
157
+ chunk_size: Size of each chunk in bytes (default 5MB)
158
+ progress_callback: Optional callback for progress updates (percentage, current_part, total_parts)
159
+ """
160
+ super().__init__(api_client)
161
+ self._file_path = file_path
162
+ self._chunk_size = chunk_size
163
+ self._progress_callback = progress_callback
164
+ self._session: Optional[MultipartUploadSession] = None
165
+
166
+ def execute(self) -> Dict[str, Any]:
167
+ """Execute complete file upload.
168
+
169
+ Returns:
170
+ Dictionary with upload results and session information
171
+
172
+ Raises:
173
+ APIError: If any step of the upload fails
174
+ FileNotFoundError: If the file doesn't exist
175
+ """
176
+ try:
177
+ # Validate file exists
178
+ if not Path(self._file_path).exists():
179
+ raise FileNotFoundError(f"File not found: {self._file_path}")
180
+
181
+ # Step 1: Initialize upload session
182
+ init_response = self._initialize_session()
183
+ if not init_response.success:
184
+ return {
185
+ 'success': False,
186
+ 'error': 'Failed to initialize upload session',
187
+ 'file_path': self._file_path
188
+ }
189
+
190
+ # Step 2: Upload all parts
191
+ upload_results = self._upload_all_parts()
192
+ if not upload_results['success']:
193
+ return {
194
+ 'success': False,
195
+ 'error': upload_results['error'],
196
+ 'file_path': self._file_path,
197
+ 'file_id': self._session.file_id if self._session else None,
198
+ 'uploaded_parts': len(self._session.uploaded_parts) if self._session else 0,
199
+ 'total_parts': self._session.total_parts if self._session else 0
200
+ }
201
+
202
+ # Step 3: Check upload status
203
+ status_response = self._check_status()
204
+ if not status_response.success or not status_response.ready:
205
+ return {
206
+ 'success': False,
207
+ 'error': 'Upload not ready for finalization',
208
+ 'file_path': self._file_path,
209
+ 'file_id': self._session.file_id if self._session else None,
210
+ 'ready': status_response.ready
211
+ }
212
+
213
+ # Step 4: Finalize upload
214
+ finalize_response = self._finalize_upload()
215
+ if not finalize_response.success:
216
+ return {
217
+ 'success': False,
218
+ 'error': finalize_response.error_message or 'Failed to finalize upload',
219
+ 'file_path': self._file_path,
220
+ 'file_id': self._session.file_id if self._session else None
221
+ }
222
+
223
+ # Success!
224
+ return {
225
+ 'success': True,
226
+ 'file_path': self._file_path,
227
+ 'file_id': self._session.file_id if self._session else None,
228
+ 'total_parts': self._session.total_parts if self._session else 0,
229
+ 'total_size': self._session.total_size if self._session else 0,
230
+ 'chunk_size': self._session.chunk_size if self._session else 0,
231
+ 'upload_complete': True
232
+ }
233
+
234
+ except Exception as e:
235
+ return {
236
+ 'success': False,
237
+ 'error': str(e),
238
+ 'file_path': self._file_path,
239
+ 'file_id': self._session.file_id if self._session else None
240
+ }
241
+
242
+ def _initialize_session(self) -> UploadInitializeResponse:
243
+ """Initialize the upload session."""
244
+ # Create session from file
245
+ self._session = FileChunker.create_session_from_file(self._file_path, self._chunk_size)
246
+
247
+ # Initialize upload
248
+ init_command = InitializeUploadCommand(self._api_client)
249
+ init_response = init_command.execute()
250
+
251
+ # Update session with file ID
252
+ if init_response.success:
253
+ self._session.file_id = init_response.file_id
254
+
255
+ return init_response
256
+
257
+ def _upload_all_parts(self) -> Dict[str, Any]:
258
+ """Upload all file parts."""
259
+ if not self._session:
260
+ return {'success': False, 'error': 'Session not initialized'}
261
+
262
+ failed_parts = []
263
+
264
+ for part_number in range(1, self._session.total_parts + 1):
265
+ try:
266
+ # Read chunk data
267
+ chunk_data = FileChunker.read_chunk(
268
+ self._file_path,
269
+ part_number,
270
+ self._session.chunk_size
271
+ )
272
+
273
+ # Upload part
274
+ upload_command = UploadPartCommand(
275
+ self._api_client,
276
+ self._session.file_id,
277
+ part_number,
278
+ chunk_data,
279
+ Path(self._file_path).name
280
+ )
281
+
282
+ part_response = upload_command.execute()
283
+
284
+ if part_response.success:
285
+ self._session.add_uploaded_part(part_number)
286
+
287
+ # Call progress callback if provided
288
+ if self._progress_callback:
289
+ progress = self._session.get_progress_percentage()
290
+ self._progress_callback(progress, part_number, self._session.total_parts)
291
+ else:
292
+ failed_parts.append({
293
+ 'part_number': part_number,
294
+ 'error': part_response.error_message
295
+ })
296
+
297
+ except Exception as e:
298
+ failed_parts.append({
299
+ 'part_number': part_number,
300
+ 'error': str(e)
301
+ })
302
+
303
+ if failed_parts:
304
+ return {
305
+ 'success': False,
306
+ 'error': f"Failed to upload {len(failed_parts)} parts",
307
+ 'failed_parts': failed_parts
308
+ }
309
+
310
+ return {'success': True}
311
+
312
+ def _check_status(self) -> UploadStatusResponse:
313
+ """Check upload status."""
314
+ if not self._session:
315
+ raise ValueError("Session not initialized")
316
+ status_command = CheckUploadStatusCommand(self._api_client, self._session.file_id)
317
+ return status_command.execute()
318
+
319
+ def _finalize_upload(self) -> UploadFinalizeResponse:
320
+ """Finalize the upload."""
321
+ if not self._session:
322
+ raise ValueError("Session not initialized")
323
+ finalize_command = FinalizeUploadCommand(self._api_client, self._session.file_id)
324
+ return finalize_command.execute()
325
+
326
+ def get_session_info(self) -> Optional[Dict[str, Any]]:
327
+ """Get current session information.
328
+
329
+ Returns:
330
+ Dictionary with session details or None if not initialized
331
+ """
332
+ if not self._session:
333
+ return None
334
+
335
+ return {
336
+ 'file_id': self._session.file_id,
337
+ 'file_path': self._session.file_path,
338
+ 'total_parts': self._session.total_parts,
339
+ 'uploaded_parts': len(self._session.uploaded_parts),
340
+ 'total_size': self._session.total_size,
341
+ 'chunk_size': self._session.chunk_size,
342
+ 'progress_percentage': self._session.get_progress_percentage(),
343
+ 'is_complete': self._session.is_complete(),
344
+ 'missing_parts': self._session.get_missing_parts()
345
+ }
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # Abort Upload Command
350
+ # ---------------------------------------------------------------------------
351
+
352
+
353
+ class AbortUploadCommand(MultipartUploadCommand):
354
+ """Command for aborting a multipart upload session (discard all parts)."""
355
+
356
+ def __init__(self, api_client, file_id: str):
357
+ super().__init__(api_client)
358
+ self._file_id = file_id
359
+
360
+ def execute(self) -> Dict[str, Any]: # returns plain dict. specific model not needed
361
+ """Abort upload session by POSTing fileId to multipart-upload/abort."""
362
+ payload = {"fileId": self._file_id}
363
+ return self._api_client.http_client.post("multipart-upload/abort", json_data=payload)