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
@@ -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
|
+
)
|