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,150 @@
1
+ """License models for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+
7
+
8
+ @dataclass
9
+ class License:
10
+ """Represents a license in the Binalyze AIR system.
11
+
12
+ Attributes:
13
+ key: The license key identifier
14
+ is_locked_down: Whether the license is in locked down mode
15
+ activated_on: When the license was activated
16
+ period_days: Total license period in days
17
+ remaining_days: Remaining days until expiration
18
+ expires_on: License expiration date
19
+ device_count: Current number of devices
20
+ max_device_count: Maximum allowed devices
21
+ client_count: Current number of clients
22
+ max_client_count: Maximum allowed clients
23
+ model: License model type
24
+ is_lifetime: Whether this is a lifetime license
25
+ customer_name: Name of the customer
26
+ """
27
+ key: str
28
+ is_locked_down: bool
29
+ activated_on: datetime
30
+ period_days: int
31
+ remaining_days: int
32
+ expires_on: datetime
33
+ device_count: int
34
+ max_device_count: int
35
+ client_count: int
36
+ max_client_count: int
37
+ model: int
38
+ is_lifetime: bool
39
+ customer_name: str
40
+
41
+ @classmethod
42
+ def from_dict(cls, data: dict) -> 'License':
43
+ """Create License instance from dictionary data.
44
+
45
+ Args:
46
+ data: Dictionary containing license data
47
+
48
+ Returns:
49
+ License instance
50
+ """
51
+ return cls(
52
+ key=data.get('key', ''),
53
+ is_locked_down=data.get('isLockedDown', False),
54
+ activated_on=datetime.fromisoformat(
55
+ data.get('activatedOn', '').replace('Z', '+00:00')
56
+ ) if data.get('activatedOn') else datetime.now(),
57
+ period_days=data.get('periodDays', 0),
58
+ remaining_days=data.get('remainingDays', 0),
59
+ expires_on=datetime.fromisoformat(
60
+ data.get('expiresOn', '').replace('Z', '+00:00')
61
+ ) if data.get('expiresOn') else datetime.now(),
62
+ device_count=data.get('deviceCount', 0),
63
+ max_device_count=data.get('maxDeviceCount', 0),
64
+ client_count=data.get('clientCount', 0),
65
+ max_client_count=data.get('maxClientCount', 0),
66
+ model=data.get('model', 0),
67
+ is_lifetime=data.get('isLifetime', False),
68
+ customer_name=data.get('customerName', '')
69
+ )
70
+
71
+ def to_dict(self) -> dict:
72
+ """Convert License instance to dictionary.
73
+
74
+ Returns:
75
+ Dictionary representation of the license
76
+ """
77
+ return {
78
+ 'key': self.key,
79
+ 'isLockedDown': self.is_locked_down,
80
+ 'activatedOn': self.activated_on.isoformat(),
81
+ 'periodDays': self.period_days,
82
+ 'remainingDays': self.remaining_days,
83
+ 'expiresOn': self.expires_on.isoformat(),
84
+ 'deviceCount': self.device_count,
85
+ 'maxDeviceCount': self.max_device_count,
86
+ 'clientCount': self.client_count,
87
+ 'maxClientCount': self.max_client_count,
88
+ 'model': self.model,
89
+ 'isLifetime': self.is_lifetime,
90
+ 'customerName': self.customer_name
91
+ }
92
+
93
+ def is_expired(self) -> bool:
94
+ """Check if the license is expired.
95
+
96
+ Returns:
97
+ True if license is expired, False otherwise
98
+ """
99
+ return self.remaining_days <= 0
100
+
101
+ def is_near_expiry(self, days_threshold: int = 30) -> bool:
102
+ """Check if license is near expiry.
103
+
104
+ Args:
105
+ days_threshold: Number of days to consider as near expiry
106
+
107
+ Returns:
108
+ True if license expires within threshold days
109
+ """
110
+ return self.remaining_days <= days_threshold
111
+
112
+ def get_usage_percentage(self) -> dict:
113
+ """Get usage percentages for devices and clients.
114
+
115
+ Returns:
116
+ Dictionary with device and client usage percentages
117
+ """
118
+ device_usage = (
119
+ (self.device_count / self.max_device_count * 100)
120
+ if self.max_device_count > 0 else 0
121
+ )
122
+ client_usage = (
123
+ (self.client_count / self.max_client_count * 100)
124
+ if self.max_client_count > 0 else 0
125
+ )
126
+
127
+ return {
128
+ 'device_usage_percent': round(device_usage, 2),
129
+ 'client_usage_percent': round(client_usage, 2)
130
+ }
131
+
132
+
133
+ @dataclass
134
+ class LicenseUpdateRequest:
135
+ """Request model for updating license.
136
+
137
+ Attributes:
138
+ license_key: The new license key to set
139
+ """
140
+ license_key: str
141
+
142
+ def to_dict(self) -> dict:
143
+ """Convert to dictionary for API request.
144
+
145
+ Returns:
146
+ Dictionary representation for API
147
+ """
148
+ return {
149
+ 'licenseKey': self.license_key
150
+ }
@@ -0,0 +1,83 @@
1
+ """Logger models for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class LogDownloadRequest:
9
+ """Request model for downloading logs.
10
+
11
+ Attributes:
12
+ latest_log_file: Whether to download only the latest log file
13
+ """
14
+ latest_log_file: bool = False
15
+
16
+ def to_dict(self) -> dict:
17
+ """Convert to dictionary for API request.
18
+
19
+ Returns:
20
+ Dictionary representation for API
21
+ """
22
+ return {
23
+ 'latestLogFile': self.latest_log_file
24
+ }
25
+
26
+
27
+ @dataclass
28
+ class LogDownloadResponse:
29
+ """Response model for log download operation.
30
+
31
+ Attributes:
32
+ content: The downloaded log content (binary data)
33
+ filename: Suggested filename for the downloaded logs
34
+ content_type: MIME type of the downloaded content
35
+ size: Size of the downloaded content in bytes
36
+ """
37
+ content: bytes
38
+ filename: Optional[str] = None
39
+ content_type: Optional[str] = None
40
+ size: Optional[int] = None
41
+
42
+ @classmethod
43
+ def from_response(cls, response_data: bytes, headers: Optional[dict] = None) -> 'LogDownloadResponse':
44
+ """Create LogDownloadResponse from HTTP response.
45
+
46
+ Args:
47
+ response_data: Binary response data
48
+ headers: HTTP response headers
49
+
50
+ Returns:
51
+ LogDownloadResponse instance
52
+ """
53
+ if headers is None:
54
+ headers = {}
55
+
56
+ # Extract filename from Content-Disposition header if available
57
+ filename = None
58
+ content_disposition = headers.get('Content-Disposition', '')
59
+ if 'filename=' in content_disposition:
60
+ filename = content_disposition.split('filename=')[1].strip('"')
61
+
62
+ return cls(
63
+ content=response_data,
64
+ filename=filename or 'logs.zip',
65
+ content_type=headers.get('Content-Type', 'application/zip'),
66
+ size=len(response_data) if response_data else 0
67
+ )
68
+
69
+ def save_to_file(self, filepath: str) -> bool:
70
+ """Save downloaded logs to a file.
71
+
72
+ Args:
73
+ filepath: Path where to save the log file
74
+
75
+ Returns:
76
+ True if saved successfully, False otherwise
77
+ """
78
+ try:
79
+ with open(filepath, 'wb') as f:
80
+ f.write(self.content)
81
+ return True
82
+ except Exception:
83
+ return False
@@ -0,0 +1,352 @@
1
+ """Multipart Upload models for Binalyze AIR SDK."""
2
+
3
+ from typing import Optional, List, Dict, Any, BinaryIO
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ import hashlib
7
+ import os
8
+
9
+
10
+ @dataclass
11
+ class UploadInitializeRequest:
12
+ """Request model for initializing a multipart upload.
13
+
14
+ This model represents the request to start a new multipart upload session.
15
+ No parameters are typically required for initialization.
16
+ """
17
+
18
+ def to_dict(self) -> Dict[str, Any]:
19
+ """Convert to dictionary for API request.
20
+
21
+ Returns:
22
+ Dictionary representation for API (typically empty)
23
+ """
24
+ return {}
25
+
26
+
27
+ @dataclass
28
+ class UploadInitializeResponse:
29
+ """Response model for upload initialization.
30
+
31
+ Attributes:
32
+ file_id: Unique identifier for the upload session
33
+ success: Whether the initialization was successful
34
+ """
35
+ file_id: str
36
+ success: bool = True
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: Dict[str, Any]) -> 'UploadInitializeResponse':
40
+ """Create UploadInitializeResponse from API response.
41
+
42
+ Args:
43
+ data: API response dictionary
44
+
45
+ Returns:
46
+ UploadInitializeResponse instance
47
+ """
48
+ result = data.get('result', {})
49
+ return cls(
50
+ file_id=result.get('fileId', ''),
51
+ success=data.get('success', False)
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class UploadPartRequest:
57
+ """Request model for uploading a file part.
58
+
59
+ Attributes:
60
+ file_id: Upload session identifier
61
+ part_number: Sequential part number (starting from 1)
62
+ file_data: Binary file data for this part
63
+ filename: Optional filename for the part
64
+ """
65
+ file_id: str
66
+ part_number: int
67
+ file_data: bytes
68
+ filename: Optional[str] = None
69
+
70
+ def to_form_data(self) -> Dict[str, Any]:
71
+ """Convert to form data for multipart file upload.
72
+
73
+ Returns:
74
+ Dictionary with form data fields
75
+ """
76
+ return {
77
+ 'fileId': self.file_id,
78
+ 'partNumber': str(self.part_number),
79
+ 'file': self.file_data
80
+ }
81
+
82
+ def get_part_size(self) -> int:
83
+ """Get the size of this part in bytes.
84
+
85
+ Returns:
86
+ Size of the file data in bytes
87
+ """
88
+ return len(self.file_data)
89
+
90
+ def get_part_hash(self) -> str:
91
+ """Generate MD5 hash of the part data for integrity checking.
92
+
93
+ Returns:
94
+ MD5 hash of the part data
95
+ """
96
+ return hashlib.md5(self.file_data).hexdigest()
97
+
98
+
99
+ @dataclass
100
+ class UploadPartResponse:
101
+ """Response model for file part upload.
102
+
103
+ Attributes:
104
+ success: Whether the part upload was successful
105
+ part_number: The part number that was uploaded
106
+ error_message: Error message if upload failed
107
+ """
108
+ success: bool
109
+ part_number: int
110
+ error_message: Optional[str] = None
111
+
112
+ @classmethod
113
+ def from_dict(cls, data: Dict[str, Any], part_number: int) -> 'UploadPartResponse':
114
+ """Create UploadPartResponse from API response.
115
+
116
+ Args:
117
+ data: API response dictionary
118
+ part_number: The part number that was uploaded
119
+
120
+ Returns:
121
+ UploadPartResponse instance
122
+ """
123
+ success = data.get('success', False)
124
+ error_message = None
125
+ if not success and data.get('errors'):
126
+ error_message = '; '.join(data['errors'])
127
+
128
+ return cls(
129
+ success=success,
130
+ part_number=part_number,
131
+ error_message=error_message
132
+ )
133
+
134
+
135
+ @dataclass
136
+ class UploadStatusRequest:
137
+ """Request model for checking upload status.
138
+
139
+ Attributes:
140
+ file_id: Upload session identifier to check
141
+ """
142
+ file_id: str
143
+
144
+ def to_dict(self) -> Dict[str, Any]:
145
+ """Convert to dictionary for API request.
146
+
147
+ Returns:
148
+ Dictionary with query parameters
149
+ """
150
+ return {'fileId': self.file_id}
151
+
152
+
153
+ @dataclass
154
+ class UploadStatusResponse:
155
+ """Response model for upload status check.
156
+
157
+ Attributes:
158
+ ready: Whether the upload is ready to be finalized
159
+ file_id: Upload session identifier
160
+ success: Whether the status check was successful
161
+ """
162
+ ready: bool
163
+ file_id: str
164
+ success: bool = True
165
+
166
+ @classmethod
167
+ def from_dict(cls, data: Dict[str, Any], file_id: str) -> 'UploadStatusResponse':
168
+ """Create UploadStatusResponse from API response.
169
+
170
+ Args:
171
+ data: API response dictionary
172
+ file_id: Upload session identifier
173
+
174
+ Returns:
175
+ UploadStatusResponse instance
176
+ """
177
+ result = data.get('result', {})
178
+ return cls(
179
+ ready=result.get('ready', False),
180
+ file_id=file_id,
181
+ success=data.get('success', False)
182
+ )
183
+
184
+
185
+ @dataclass
186
+ class UploadFinalizeRequest:
187
+ """Request model for finalizing a multipart upload.
188
+
189
+ Attributes:
190
+ file_id: Upload session identifier to finalize
191
+ """
192
+ file_id: str
193
+
194
+ def to_dict(self) -> Dict[str, Any]:
195
+ """Convert to dictionary for API request.
196
+
197
+ Returns:
198
+ Dictionary representation for API
199
+ """
200
+ return {'fileId': self.file_id}
201
+
202
+
203
+ @dataclass
204
+ class UploadFinalizeResponse:
205
+ """Response model for upload finalization.
206
+
207
+ Attributes:
208
+ success: Whether the finalization was successful
209
+ file_id: Upload session identifier
210
+ error_message: Error message if finalization failed
211
+ """
212
+ success: bool
213
+ file_id: str
214
+ error_message: Optional[str] = None
215
+
216
+ @classmethod
217
+ def from_dict(cls, data: Dict[str, Any], file_id: str) -> 'UploadFinalizeResponse':
218
+ """Create UploadFinalizeResponse from API response.
219
+
220
+ Args:
221
+ data: API response dictionary
222
+ file_id: Upload session identifier
223
+
224
+ Returns:
225
+ UploadFinalizeResponse instance
226
+ """
227
+ success = data.get('success', False)
228
+ error_message = None
229
+ if not success and data.get('errors'):
230
+ error_message = '; '.join(data['errors'])
231
+
232
+ return cls(
233
+ success=success,
234
+ file_id=file_id,
235
+ error_message=error_message
236
+ )
237
+
238
+
239
+ @dataclass
240
+ class MultipartUploadSession:
241
+ """Complete multipart upload session information.
242
+
243
+ Attributes:
244
+ file_id: Upload session identifier
245
+ total_parts: Total number of parts to upload
246
+ uploaded_parts: List of successfully uploaded part numbers
247
+ file_path: Path to the file being uploaded
248
+ chunk_size: Size of each chunk in bytes
249
+ total_size: Total size of the file in bytes
250
+ """
251
+ file_id: str
252
+ total_parts: int
253
+ uploaded_parts: List[int]
254
+ file_path: Optional[str] = None
255
+ chunk_size: int = 5 * 1024 * 1024 # 5MB default
256
+ total_size: int = 0
257
+
258
+ def is_complete(self) -> bool:
259
+ """Check if all parts have been uploaded.
260
+
261
+ Returns:
262
+ True if all parts are uploaded, False otherwise
263
+ """
264
+ return len(self.uploaded_parts) == self.total_parts
265
+
266
+ def get_progress_percentage(self) -> float:
267
+ """Get upload progress as percentage.
268
+
269
+ Returns:
270
+ Progress percentage (0.0 to 100.0)
271
+ """
272
+ if self.total_parts == 0:
273
+ return 0.0
274
+ return (len(self.uploaded_parts) / self.total_parts) * 100.0
275
+
276
+ def get_missing_parts(self) -> List[int]:
277
+ """Get list of part numbers that still need to be uploaded.
278
+
279
+ Returns:
280
+ List of missing part numbers
281
+ """
282
+ all_parts = set(range(1, self.total_parts + 1))
283
+ uploaded_set = set(self.uploaded_parts)
284
+ return sorted(list(all_parts - uploaded_set))
285
+
286
+ def add_uploaded_part(self, part_number: int) -> None:
287
+ """Mark a part as successfully uploaded.
288
+
289
+ Args:
290
+ part_number: Part number that was uploaded
291
+ """
292
+ if part_number not in self.uploaded_parts:
293
+ self.uploaded_parts.append(part_number)
294
+ self.uploaded_parts.sort()
295
+
296
+
297
+ class FileChunker:
298
+ """Utility class for splitting files into chunks for multipart upload."""
299
+
300
+ @staticmethod
301
+ def calculate_parts(file_size: int, chunk_size: int = 5 * 1024 * 1024) -> int:
302
+ """Calculate number of parts needed for a file.
303
+
304
+ Args:
305
+ file_size: Size of the file in bytes
306
+ chunk_size: Size of each chunk in bytes
307
+
308
+ Returns:
309
+ Number of parts needed
310
+ """
311
+ return (file_size + chunk_size - 1) // chunk_size
312
+
313
+ @staticmethod
314
+ def read_chunk(file_path: str, part_number: int, chunk_size: int = 5 * 1024 * 1024) -> bytes:
315
+ """Read a specific chunk from a file.
316
+
317
+ Args:
318
+ file_path: Path to the file
319
+ part_number: Part number to read (1-based)
320
+ chunk_size: Size of each chunk in bytes
321
+
322
+ Returns:
323
+ Binary data for the specified chunk
324
+ """
325
+ offset = (part_number - 1) * chunk_size
326
+
327
+ with open(file_path, 'rb') as f:
328
+ f.seek(offset)
329
+ return f.read(chunk_size)
330
+
331
+ @staticmethod
332
+ def create_session_from_file(file_path: str, chunk_size: int = 5 * 1024 * 1024) -> MultipartUploadSession:
333
+ """Create an upload session from a file path.
334
+
335
+ Args:
336
+ file_path: Path to the file to upload
337
+ chunk_size: Size of each chunk in bytes
338
+
339
+ Returns:
340
+ MultipartUploadSession configured for the file
341
+ """
342
+ file_size = os.path.getsize(file_path)
343
+ total_parts = FileChunker.calculate_parts(file_size, chunk_size)
344
+
345
+ return MultipartUploadSession(
346
+ file_id='', # Will be set after initialization
347
+ total_parts=total_parts,
348
+ uploaded_parts=[],
349
+ file_path=file_path,
350
+ chunk_size=chunk_size,
351
+ total_size=file_size
352
+ )