primitive 0.1.68__py3-none-any.whl → 0.1.70__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.
- primitive/__about__.py +1 -1
- primitive/agent/process.py +8 -1
- primitive/agent/runner.py +128 -123
- primitive/agent/uploader.py +6 -7
- primitive/cli.py +0 -4
- primitive/client.py +5 -8
- primitive/files/actions.py +192 -29
- primitive/files/commands.py +4 -1
- primitive/files/graphql/fragments.py +18 -0
- primitive/files/graphql/mutations.py +17 -1
- primitive/files/graphql/queries.py +31 -0
- primitive/utils/auth.py +25 -17
- primitive/utils/chunk_size.py +87 -0
- primitive/utils/files.py +10 -7
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/METADATA +2 -1
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/RECORD +19 -22
- primitive/lint/__init__.py +0 -0
- primitive/lint/actions.py +0 -76
- primitive/lint/commands.py +0 -17
- primitive/sim/__init__.py +0 -0
- primitive/sim/actions.py +0 -129
- primitive/sim/commands.py +0 -19
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/WHEEL +0 -0
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/entry_points.txt +0 -0
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/licenses/LICENSE.txt +0 -0
primitive/files/actions.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import concurrent
|
1
2
|
import hashlib
|
2
3
|
import sys
|
3
4
|
import threading
|
@@ -11,11 +12,15 @@ from loguru import logger
|
|
11
12
|
from primitive.graphql.sdk import create_requests_session
|
12
13
|
from primitive.utils.actions import BaseAction
|
13
14
|
|
14
|
-
from ..utils.auth import guard
|
15
|
+
from ..utils.auth import create_new_session, guard
|
16
|
+
from ..utils.chunk_size import calculate_optimal_chunk_size, get_upload_speed_mb
|
17
|
+
from ..utils.memory_size import MemorySize
|
15
18
|
from .graphql.mutations import (
|
16
19
|
file_update_mutation,
|
17
20
|
pending_file_create_mutation,
|
21
|
+
update_parts_details,
|
18
22
|
)
|
23
|
+
from .graphql.queries import files_list
|
19
24
|
|
20
25
|
|
21
26
|
# this class can be used in multithreaded S3 client uploader
|
@@ -41,6 +46,12 @@ class ProgressPercentage(object):
|
|
41
46
|
|
42
47
|
|
43
48
|
class Files(BaseAction):
|
49
|
+
def __init__(self, primitive):
|
50
|
+
super().__init__(primitive)
|
51
|
+
self.num_workers = 4
|
52
|
+
self.upload_speed_mbps = None
|
53
|
+
self.upload_speed_mbps = "100"
|
54
|
+
|
44
55
|
def _pending_file_create(
|
45
56
|
self,
|
46
57
|
file_name: str,
|
@@ -48,6 +59,8 @@ class Files(BaseAction):
|
|
48
59
|
file_checksum: str,
|
49
60
|
file_path: str,
|
50
61
|
key_prefix: str,
|
62
|
+
chunk_size: int,
|
63
|
+
number_of_parts: int,
|
51
64
|
is_public: bool = False,
|
52
65
|
):
|
53
66
|
mutation = gql(pending_file_create_mutation)
|
@@ -58,6 +71,8 @@ class Files(BaseAction):
|
|
58
71
|
"fileChecksum": file_checksum,
|
59
72
|
"keyPrefix": key_prefix,
|
60
73
|
"isPublic": is_public,
|
74
|
+
"chunkSize": chunk_size,
|
75
|
+
"numberOfParts": number_of_parts,
|
61
76
|
}
|
62
77
|
variables = {"input": input}
|
63
78
|
result = self.primitive.session.execute(
|
@@ -86,11 +101,102 @@ class Files(BaseAction):
|
|
86
101
|
)
|
87
102
|
return result
|
88
103
|
|
104
|
+
def _update_parts_details(
|
105
|
+
self,
|
106
|
+
file_id: str,
|
107
|
+
part_number: int,
|
108
|
+
etag: str,
|
109
|
+
):
|
110
|
+
mutation = gql(update_parts_details)
|
111
|
+
input = {
|
112
|
+
"fileId": file_id,
|
113
|
+
"partNumber": part_number,
|
114
|
+
"etag": etag,
|
115
|
+
}
|
116
|
+
variables = {"input": input}
|
117
|
+
|
118
|
+
# since this is called in a multithreaded environment,
|
119
|
+
# we need to create a new session for each thread.
|
120
|
+
session = create_new_session(self.primitive)
|
121
|
+
result = session.execute(
|
122
|
+
mutation, variable_values=variables, get_execution_result=True
|
123
|
+
)
|
124
|
+
return result
|
125
|
+
|
126
|
+
@guard
|
127
|
+
def get_file(self, file_id: Optional[str] = None):
|
128
|
+
query = gql(files_list)
|
129
|
+
|
130
|
+
filters = {}
|
131
|
+
if file_id:
|
132
|
+
filters["id"] = {"exact": file_id}
|
133
|
+
|
134
|
+
variables = {
|
135
|
+
"first": 1,
|
136
|
+
"filters": filters,
|
137
|
+
}
|
138
|
+
result = self.primitive.session.execute(
|
139
|
+
query, variable_values=variables, get_execution_result=True
|
140
|
+
)
|
141
|
+
return result
|
142
|
+
|
143
|
+
def _upload_part(
|
144
|
+
self,
|
145
|
+
file_id: str,
|
146
|
+
file_path: Path,
|
147
|
+
part_number: int,
|
148
|
+
presigned_url: str,
|
149
|
+
start_byte: int,
|
150
|
+
end_byte: int,
|
151
|
+
):
|
152
|
+
part_data = None
|
153
|
+
with open(file_path, "rb") as file:
|
154
|
+
file.seek(start_byte)
|
155
|
+
part_data = file.read(end_byte - start_byte)
|
156
|
+
assert len(part_data) > 0
|
157
|
+
file.seek(0)
|
158
|
+
logger.debug(
|
159
|
+
f"Part {part_number}. Start: {start_byte}. End: {end_byte}. Size: {len(part_data)}"
|
160
|
+
)
|
161
|
+
|
162
|
+
md5_hexdigest = hashlib.md5(part_data).hexdigest() # Get raw MD5 bytes
|
163
|
+
|
164
|
+
logger.debug(f"Uploading part {part_number}...")
|
165
|
+
# Upload the part using the pre-signed URL
|
166
|
+
response = requests.put(
|
167
|
+
presigned_url,
|
168
|
+
data=part_data,
|
169
|
+
)
|
170
|
+
# cleanup memory
|
171
|
+
part_data = None
|
172
|
+
|
173
|
+
if response.ok:
|
174
|
+
# Extract the ETag for the part from the response headers
|
175
|
+
etag = response.headers.get("ETag").replace('"', "")
|
176
|
+
if not etag:
|
177
|
+
logger.error("Failed to retrieve ETag from response headers")
|
178
|
+
else:
|
179
|
+
logger.error(response.text)
|
180
|
+
response.raise_for_status()
|
181
|
+
|
182
|
+
if etag != md5_hexdigest:
|
183
|
+
message = f"Part {part_number} ETag does not match MD5 checksum: {etag} != {md5_hexdigest}"
|
184
|
+
logger.error(message)
|
185
|
+
raise Exception(message)
|
186
|
+
|
187
|
+
if etag:
|
188
|
+
return self._update_parts_details(
|
189
|
+
file_id, part_number=part_number, etag=etag
|
190
|
+
)
|
191
|
+
else:
|
192
|
+
logger.error(f"Failed to upload part {part_number}")
|
193
|
+
return None
|
194
|
+
|
89
195
|
@guard
|
90
196
|
def upload_file_direct(
|
91
197
|
self,
|
92
198
|
path: Path,
|
93
|
-
is_public: False,
|
199
|
+
is_public: bool = False,
|
94
200
|
key_prefix: str = "",
|
95
201
|
file_id: Optional[str] = None,
|
96
202
|
):
|
@@ -104,7 +210,26 @@ class Files(BaseAction):
|
|
104
210
|
|
105
211
|
file_checksum = hashlib.md5(path.read_bytes()).hexdigest()
|
106
212
|
|
213
|
+
parts_details = None
|
214
|
+
|
215
|
+
if not self.upload_speed_mbps:
|
216
|
+
logger.info("Calculating upload speed...")
|
217
|
+
self.upload_speed_mbps = get_upload_speed_mb()
|
218
|
+
logger.info(f"Upload speed: {self.upload_speed_mbps} MB/s")
|
219
|
+
|
107
220
|
if not file_id:
|
221
|
+
chunk_size, number_of_parts = calculate_optimal_chunk_size(
|
222
|
+
upload_speed_mb=self.upload_speed_mbps,
|
223
|
+
file_size_bytes=file_size,
|
224
|
+
num_workers=self.num_workers,
|
225
|
+
optimal_time_seconds=5,
|
226
|
+
)
|
227
|
+
chunk_size_ms = MemorySize(chunk_size, "B")
|
228
|
+
chunk_size_ms.convert_to("MB")
|
229
|
+
logger.info(
|
230
|
+
f"Creating Pending File for {path}. File Size: {MemorySize(file_size, "B").get_human_readable()} ({file_size}). Chunk size: {chunk_size_ms} at Number of parts: {number_of_parts}"
|
231
|
+
)
|
232
|
+
|
108
233
|
pending_file_create = self._pending_file_create(
|
109
234
|
file_name=path.name,
|
110
235
|
file_size=path.stat().st_size,
|
@@ -112,34 +237,71 @@ class Files(BaseAction):
|
|
112
237
|
file_path=str(path),
|
113
238
|
key_prefix=key_prefix,
|
114
239
|
is_public=is_public,
|
240
|
+
chunk_size=chunk_size,
|
241
|
+
number_of_parts=number_of_parts,
|
242
|
+
)
|
243
|
+
file_id = pending_file_create.get("id")
|
244
|
+
parts_details = pending_file_create.get("partsDetails")
|
245
|
+
else:
|
246
|
+
get_file_result = self.get_file(file_id)
|
247
|
+
parts_details = (
|
248
|
+
get_file_result.data.get("files")
|
249
|
+
.get("edges")[0]
|
250
|
+
.get("node")
|
251
|
+
.get("partsDetails")
|
115
252
|
)
|
116
|
-
file_id = pending_file_create.get("id")
|
117
|
-
presigned_url = pending_file_create.get("presignedUrlForUpload")
|
118
253
|
|
119
254
|
if not file_id:
|
120
255
|
raise Exception("No file_id found or provided.")
|
121
|
-
if not
|
122
|
-
raise Exception("No
|
256
|
+
if not parts_details:
|
257
|
+
raise Exception(f"No parts_details returned for File ID: {file_id}.")
|
123
258
|
|
124
259
|
self._update_file_status(file_id, is_uploading=True)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
260
|
+
|
261
|
+
# now create a multithreaded uploader based on the number of cores available
|
262
|
+
# each thread will read from its starting and ending byte range provided by parts_details
|
263
|
+
# and upload that part to the presigned url
|
264
|
+
# an ETag will be returned for each part uploaded and will need to stored in the parts_details
|
265
|
+
# on the server side by updating the File object
|
266
|
+
|
267
|
+
is_complete = True
|
268
|
+
|
269
|
+
# Upload parts in parallel
|
270
|
+
with concurrent.futures.ThreadPoolExecutor(
|
271
|
+
max_workers=self.num_workers
|
272
|
+
) as executor:
|
273
|
+
future_to_part = {
|
274
|
+
executor.submit(
|
275
|
+
self._upload_part,
|
276
|
+
file_id=file_id,
|
277
|
+
file_path=path,
|
278
|
+
part_number=part_details.get("PartNumber"),
|
279
|
+
presigned_url=part_details.get("presigned_url"),
|
280
|
+
start_byte=part_details.get("start_byte"),
|
281
|
+
end_byte=part_details.get("end_byte"),
|
282
|
+
): part_details
|
283
|
+
for _key, part_details in parts_details.items()
|
284
|
+
}
|
285
|
+
for future in concurrent.futures.as_completed(future_to_part):
|
286
|
+
part_detail = future_to_part[future]
|
287
|
+
try:
|
288
|
+
future.result()
|
289
|
+
logger.debug(
|
290
|
+
f"Part {part_detail.get('PartNumber')} uploaded successfully."
|
291
|
+
)
|
292
|
+
except Exception as exception:
|
293
|
+
logger.error(
|
294
|
+
f"Part {part_detail.get('PartNumber')} generated an exception: {exception}"
|
295
|
+
)
|
296
|
+
is_complete = False
|
297
|
+
|
298
|
+
# when all parts have ETags (updated), send an update that
|
299
|
+
if is_complete:
|
300
|
+
update_file_status_result = self._update_file_status(
|
301
|
+
file_id, is_uploading=False, is_complete=True
|
302
|
+
)
|
303
|
+
logger.info(f"File {path} marked is_complete.")
|
304
|
+
return update_file_status_result
|
143
305
|
|
144
306
|
@guard
|
145
307
|
def upload_file_via_api(
|
@@ -147,7 +309,6 @@ class Files(BaseAction):
|
|
147
309
|
path: Path,
|
148
310
|
is_public: bool = False,
|
149
311
|
key_prefix: str = "",
|
150
|
-
job_run_id: str = "",
|
151
312
|
):
|
152
313
|
"""
|
153
314
|
This method uploads a file via the Primitive API.
|
@@ -163,8 +324,6 @@ class Files(BaseAction):
|
|
163
324
|
+ file_path
|
164
325
|
+ """\", "keyPrefix": \""""
|
165
326
|
+ key_prefix
|
166
|
-
+ """\", "jobRunId": \""""
|
167
|
-
+ job_run_id
|
168
327
|
+ """\" } } }"""
|
169
328
|
) # noqa
|
170
329
|
|
@@ -174,8 +333,6 @@ class Files(BaseAction):
|
|
174
333
|
+ file_path
|
175
334
|
+ """\", "keyPrefix": \""""
|
176
335
|
+ key_prefix
|
177
|
-
+ """\", "jobRunId": \""""
|
178
|
-
+ job_run_id
|
179
336
|
+ """\" } } }"""
|
180
337
|
) # noqa
|
181
338
|
body = {
|
@@ -189,3 +346,9 @@ class Files(BaseAction):
|
|
189
346
|
url = f"{transport}://{self.primitive.host}/"
|
190
347
|
response = session.post(url, files=body)
|
191
348
|
return response
|
349
|
+
|
350
|
+
def get_presigned_url(self, file_pk: str):
|
351
|
+
transport = self.primitive.host_config.get("transport")
|
352
|
+
host = self.primitive.host_config.get("host")
|
353
|
+
file_access_url = f"{transport}://{host}/files/{file_pk}/presigned-url/"
|
354
|
+
return file_access_url
|
primitive/files/commands.py
CHANGED
@@ -35,6 +35,9 @@ def file_upload_command(context, path, public, key_prefix, direct):
|
|
35
35
|
result = primitive.files.upload_file_via_api(
|
36
36
|
path, is_public=public, key_prefix=key_prefix
|
37
37
|
)
|
38
|
+
try:
|
39
|
+
message = json.dumps(result.data)
|
40
|
+
except AttributeError:
|
41
|
+
message = "File Upload Failed"
|
38
42
|
|
39
|
-
message = json.dumps(result.data)
|
40
43
|
print_result(message=message, context=context)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
file_fragment = """
|
2
|
+
fragment FileFragment on File {
|
3
|
+
id
|
4
|
+
pk
|
5
|
+
createdAt
|
6
|
+
updatedAt
|
7
|
+
createdBy
|
8
|
+
location
|
9
|
+
fileName
|
10
|
+
fileSize
|
11
|
+
fileChecksum
|
12
|
+
isUploading
|
13
|
+
isComplete
|
14
|
+
partsDetails
|
15
|
+
humanReadableMemorySize
|
16
|
+
contents
|
17
|
+
}
|
18
|
+
"""
|
@@ -23,7 +23,23 @@ mutation pendingFileCreate($input: PendingFileCreateInput!) {
|
|
23
23
|
... on File {
|
24
24
|
id
|
25
25
|
pk
|
26
|
-
|
26
|
+
partsDetails
|
27
|
+
}
|
28
|
+
...OperationInfoFragment
|
29
|
+
}
|
30
|
+
}
|
31
|
+
"""
|
32
|
+
)
|
33
|
+
|
34
|
+
update_parts_details = (
|
35
|
+
operation_info_fragment
|
36
|
+
+ """
|
37
|
+
mutation updatePartsDetails($input: UpdatePartsDetailsInput!) {
|
38
|
+
updatePartsDetails(input: $input) {
|
39
|
+
... on File {
|
40
|
+
id
|
41
|
+
pk
|
42
|
+
partsDetails
|
27
43
|
}
|
28
44
|
...OperationInfoFragment
|
29
45
|
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from .fragments import file_fragment
|
2
|
+
|
3
|
+
files_list = (
|
4
|
+
file_fragment
|
5
|
+
+ """
|
6
|
+
|
7
|
+
query files(
|
8
|
+
$before: String
|
9
|
+
$after: String
|
10
|
+
$first: Int
|
11
|
+
$last: Int
|
12
|
+
$filters: FileFilters
|
13
|
+
) {
|
14
|
+
files(
|
15
|
+
before: $before
|
16
|
+
after: $after
|
17
|
+
first: $first
|
18
|
+
last: $last
|
19
|
+
filters: $filters
|
20
|
+
) {
|
21
|
+
totalCount
|
22
|
+
edges {
|
23
|
+
cursor
|
24
|
+
node {
|
25
|
+
...FileFragment
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
"""
|
31
|
+
)
|
primitive/utils/auth.py
CHANGED
@@ -1,26 +1,34 @@
|
|
1
|
-
from ..graphql.sdk import create_session
|
2
1
|
import sys
|
3
2
|
|
3
|
+
from loguru import logger
|
4
|
+
|
5
|
+
from ..graphql.sdk import create_session
|
6
|
+
|
7
|
+
|
8
|
+
def create_new_session(primitive):
|
9
|
+
token = primitive.host_config.get("token")
|
10
|
+
transport = primitive.host_config.get("transport")
|
11
|
+
fingerprint = primitive.host_config.get("fingerprint")
|
12
|
+
|
13
|
+
if not token or not transport:
|
14
|
+
logger.enable("primitive")
|
15
|
+
logger.error(
|
16
|
+
"CLI is not configured. Run `primitive config` to add an auth token."
|
17
|
+
)
|
18
|
+
sys.exit(1)
|
19
|
+
|
20
|
+
return create_session(
|
21
|
+
host=primitive.host,
|
22
|
+
token=token,
|
23
|
+
transport=transport,
|
24
|
+
fingerprint=fingerprint,
|
25
|
+
)
|
26
|
+
|
4
27
|
|
5
28
|
def guard(func):
|
6
29
|
def wrapper(self, *args, **kwargs):
|
7
30
|
if self.primitive.session is None:
|
8
|
-
|
9
|
-
transport = self.primitive.host_config.get("transport")
|
10
|
-
fingerprint = self.primitive.host_config.get("fingerprint")
|
11
|
-
|
12
|
-
if not token or not transport:
|
13
|
-
print(
|
14
|
-
"CLI is not configured. Run primitive config to add an auth token."
|
15
|
-
)
|
16
|
-
sys.exit(1)
|
17
|
-
|
18
|
-
self.primitive.session = create_session(
|
19
|
-
host=self.primitive.host,
|
20
|
-
token=token,
|
21
|
-
transport=transport,
|
22
|
-
fingerprint=fingerprint,
|
23
|
-
)
|
31
|
+
self.primitive.session = create_new_session(self.primitive)
|
24
32
|
|
25
33
|
return func(self, *args, **kwargs)
|
26
34
|
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import math
|
2
|
+
|
3
|
+
import speedtest
|
4
|
+
|
5
|
+
from .memory_size import MemorySize
|
6
|
+
|
7
|
+
max_threads = 4
|
8
|
+
|
9
|
+
|
10
|
+
def get_upload_speed_mb() -> int:
|
11
|
+
"""
|
12
|
+
Calculate the Bytes per Second upload speed using the speedtest-cli library.
|
13
|
+
Return the upload speed in a round "MB" MegaBytes to the power of two format.
|
14
|
+
"""
|
15
|
+
speedtest_client = speedtest.Speedtest(secure=True)
|
16
|
+
speedtest_client.get_best_server()
|
17
|
+
upload_speed_bytes = speedtest_client.upload()
|
18
|
+
|
19
|
+
upload_speed_mb = MemorySize(upload_speed_bytes, "B")
|
20
|
+
upload_speed_mb.convert_to("MB")
|
21
|
+
return int(upload_speed_mb.value)
|
22
|
+
|
23
|
+
|
24
|
+
def nearest_classic_power_of_two_mb(chunk_size_bytes):
|
25
|
+
"""
|
26
|
+
Finds the nearest 'classic' power-of-2 multiple in MB (e.g., 8, 16, 32 MB).
|
27
|
+
|
28
|
+
Args:
|
29
|
+
chunk_size_bytes (int): The chunk size in bytes.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
int: The closest size in bytes that corresponds to 8, 16, 32, etc. MB.
|
33
|
+
"""
|
34
|
+
# Convert bytes to MB
|
35
|
+
chunk_size_mb = chunk_size_bytes / (10**6)
|
36
|
+
|
37
|
+
# Calculate log2 of the chunk size in MB
|
38
|
+
log2_size = math.log2(chunk_size_mb)
|
39
|
+
|
40
|
+
# Find the nearest powers of 2
|
41
|
+
lower_mb = 2 ** math.floor(log2_size)
|
42
|
+
upper_mb = 2 ** math.ceil(log2_size)
|
43
|
+
|
44
|
+
# Convert back to bytes
|
45
|
+
lower_bytes = lower_mb * (10**6)
|
46
|
+
upper_bytes = upper_mb * (10**6)
|
47
|
+
|
48
|
+
# Determine the closest one in bytes
|
49
|
+
return (
|
50
|
+
lower_bytes
|
51
|
+
if abs(chunk_size_bytes - lower_bytes) <= abs(chunk_size_bytes - upper_bytes)
|
52
|
+
else upper_bytes
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def calculate_optimal_chunk_size(
|
57
|
+
upload_speed_mb: int,
|
58
|
+
file_size_bytes: int,
|
59
|
+
num_workers: int = 4,
|
60
|
+
optimal_time_seconds: int = 5,
|
61
|
+
):
|
62
|
+
"""
|
63
|
+
Calculate the optimal chunk size for a multipart upload based on network speed, cores, and file size.
|
64
|
+
|
65
|
+
:param upload_speed_mbps: The upload speed in Mbit/s (but a single integer represented as MB).
|
66
|
+
:param file_size_bytes: The size of the file in bytes.
|
67
|
+
:param num_workers: The number of threads available for multi-threading (default is 4).
|
68
|
+
:param optimal_time_seconds: The target upload time per chunk in seconds (default is 5).
|
69
|
+
|
70
|
+
:return: A tuple (optimal_chunk_size_bytes, chunk_count, list_of_urls)
|
71
|
+
"""
|
72
|
+
|
73
|
+
# Estimate transfer rate per thread (Bytes/sec)
|
74
|
+
thread_speed_mbps = int(upload_speed_mb) / num_workers # Mbit/s per thread
|
75
|
+
thread_speed_bps = thread_speed_mbps * 125_000 # Convert to Bytes/sec
|
76
|
+
|
77
|
+
# Calculate optimal chunk size (Bytes)
|
78
|
+
optimal_chunk_size_bytes = thread_speed_bps * optimal_time_seconds
|
79
|
+
rounded_chunk_size_bytes = nearest_classic_power_of_two_mb(optimal_chunk_size_bytes)
|
80
|
+
|
81
|
+
# Adjust chunk size to ensure it's an integer and divide the file size into chunks
|
82
|
+
chunk_count = math.ceil(file_size_bytes / rounded_chunk_size_bytes)
|
83
|
+
# final_chunk_size_bytes = math.ceil(
|
84
|
+
# int(file_size_bytes) / chunk_count
|
85
|
+
# ) # Round up to fit file size evenly
|
86
|
+
|
87
|
+
return rounded_chunk_size_bytes, chunk_count
|
primitive/utils/files.py
CHANGED
@@ -7,16 +7,19 @@ import os
|
|
7
7
|
def find_files_for_extension(source: Path, extensions: Tuple[str]) -> List[Path]:
|
8
8
|
matching_files = []
|
9
9
|
logger.debug(f"Looking for files at {source} with extensions {extensions}")
|
10
|
-
# for those on python 3.12+
|
11
|
-
# for dirpath, dirnames, filenames in source.walk():
|
12
|
-
# for filename in filenames:
|
13
|
-
# if filename.endswith(extensions):
|
14
|
-
# matching_files.append(dirpath.joinpath(filename))
|
15
10
|
|
16
|
-
|
11
|
+
has_walk = getattr(source, "walk", None)
|
12
|
+
|
13
|
+
if has_walk:
|
14
|
+
files = source.walk()
|
15
|
+
else:
|
16
|
+
files = os.walk(source)
|
17
|
+
|
18
|
+
for dirpath, dirnames, filenames in files:
|
17
19
|
for filename in filenames:
|
18
20
|
if filename.endswith(extensions):
|
19
|
-
matching_files.append(
|
21
|
+
matching_files.append(dirpath.joinpath(filename))
|
22
|
+
|
20
23
|
logger.debug(
|
21
24
|
f"Found {len(matching_files)} following files that match: {matching_files}"
|
22
25
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: primitive
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.70
|
4
4
|
Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
|
5
5
|
Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
|
6
6
|
Project-URL: Source, https://github.com//primitivecorp/primitive-cli
|
@@ -23,6 +23,7 @@ Requires-Dist: loguru
|
|
23
23
|
Requires-Dist: paramiko[invoke]
|
24
24
|
Requires-Dist: primitive-pal==0.1.4
|
25
25
|
Requires-Dist: pyyaml
|
26
|
+
Requires-Dist: speedtest-cli
|
26
27
|
Description-Content-Type: text/markdown
|
27
28
|
|
28
29
|
# primitive
|
@@ -1,14 +1,14 @@
|
|
1
|
-
primitive/__about__.py,sha256
|
1
|
+
primitive/__about__.py,sha256=-U2lYL3xVMRxRRI2Jc3j5nMPLun6XNvp118ovjwO1R8,130
|
2
2
|
primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
|
3
|
-
primitive/cli.py,sha256=
|
4
|
-
primitive/client.py,sha256=
|
3
|
+
primitive/cli.py,sha256=CiI60bG3UZyNFuLTpchr0KeJRG5SALj455Ob11CegGE,2412
|
4
|
+
primitive/client.py,sha256=PPyIQRvKKSqCF9RRF5mJJ4Vqqolpzy1YXqffNLKIvAA,2390
|
5
5
|
primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
primitive/agent/actions.py,sha256=Hosy2o2FntfBtcNqqHuMFq9dm99EVfySy0v2JGeufvc,6474
|
7
7
|
primitive/agent/commands.py,sha256=-dVDilELfkGfbZB7qfEPs77Dm1oT62qJj4tsIk4KoxI,254
|
8
|
-
primitive/agent/process.py,sha256=
|
8
|
+
primitive/agent/process.py,sha256=7o8axjJ1OauKaFBL0jbZL9tGlZdZbDCXAdT5F-S_XQ8,2541
|
9
9
|
primitive/agent/provision.py,sha256=rmwnro1K5F8mwtd45XAq7RVQmpDWnbBCQ8X_qgWhm3M,1546
|
10
|
-
primitive/agent/runner.py,sha256=
|
11
|
-
primitive/agent/uploader.py,sha256=
|
10
|
+
primitive/agent/runner.py,sha256=WHmlfSRHngY6mNwpykIe90LxYGoIMTBB-F2h5KVV-VE,8504
|
11
|
+
primitive/agent/uploader.py,sha256=CRy928vI4z22PcJ-i8BOwAZnvut-jit64XM741xbDxM,2947
|
12
12
|
primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
primitive/auth/actions.py,sha256=MPsG9LcKcOPwA7gZ9Ewk0PZJhTQvIrGfODdz4GxSzgA,999
|
14
14
|
primitive/auth/commands.py,sha256=JahUq0E2e7Xa-FX1WEUv7TgM6ieDvNH4VwRRtxAW7HE,2340
|
@@ -24,10 +24,12 @@ primitive/exec/actions.py,sha256=onBOsMwANCKjCf8aSmDhep9RTX7LhtS8nlcjZM6av1w,263
|
|
24
24
|
primitive/exec/commands.py,sha256=66LO2kkJC-ynNZQpUCXv4Ol15QoacdSZAHblePDcmLo,510
|
25
25
|
primitive/exec/interactive.py,sha256=TscY6s2ZysijidKPheq6y-fCErUVLS0zcdTW8XyFWGI,2435
|
26
26
|
primitive/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
|
-
primitive/files/actions.py,sha256=
|
28
|
-
primitive/files/commands.py,sha256=
|
27
|
+
primitive/files/actions.py,sha256=ZBajWkrvM_pZjJrwXK-_LGnDP6qrUu4LxWg7ZF3RGHg,12444
|
28
|
+
primitive/files/commands.py,sha256=ZNW4y8JZF1li7P5ej1r-Xcqu0iGpRRlMYvthuZOLLbQ,1163
|
29
29
|
primitive/files/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
primitive/files/graphql/
|
30
|
+
primitive/files/graphql/fragments.py,sha256=II6WHZjzSqX4IELwdiWokqHTKvDq6mMHF5gp3rLnj3U,231
|
31
|
+
primitive/files/graphql/mutations.py,sha256=Da_e6WSp-fsCYVE9A6SGkIQy9WDzjeQycNyHEn7vJqE,935
|
32
|
+
primitive/files/graphql/queries.py,sha256=_ky-IRz928sKeSJuqaggTPxV4CGgmho3OyaAFu1z7nw,397
|
31
33
|
primitive/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
34
|
primitive/git/actions.py,sha256=0KHeHViZZqIhF6-Eqvhs0g_UmglqyWrOQKElQCm6jVw,1506
|
33
35
|
primitive/git/commands.py,sha256=sCeSjkRgSEjCEsB5seXgB_h6xfk0KpvMvzMKoRfUbRA,1177
|
@@ -51,9 +53,6 @@ primitive/jobs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
51
53
|
primitive/jobs/graphql/fragments.py,sha256=xtT168P-ChEj3UhSXxFiYPgYuMDyey9bXYkk-TtM3a4,542
|
52
54
|
primitive/jobs/graphql/mutations.py,sha256=8ASvCmwQh7cMeeiykOdYaYVryG8FRIuVF6v_J8JJZuw,219
|
53
55
|
primitive/jobs/graphql/queries.py,sha256=BrU_GnLjK0bTAmWsLSmGEUea7EM8MqTKxN1Qp6sSjwc,1597
|
54
|
-
primitive/lint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
|
-
primitive/lint/actions.py,sha256=tWsrht1dowGprcZjEUtjCJzozEQmh9sv2_C2__YHIOI,2825
|
56
|
-
primitive/lint/commands.py,sha256=3CZvkOEMpJspJWmaQzA5bpPKx0_VCijQIXA9l-eTnZE,487
|
57
56
|
primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
57
|
primitive/organizations/actions.py,sha256=o7WnTONtH-WI0kzn71Uq-kF0CRjDS8Xb9YA7DIjYnwY,1085
|
59
58
|
primitive/organizations/commands.py,sha256=_dwgVEJCqMa5VgB_7P1wLPFc0AuT1p9dtyR9JRr4kpw,487
|
@@ -79,22 +78,20 @@ primitive/reservations/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
79
78
|
primitive/reservations/graphql/fragments.py,sha256=_TQfJeHky-Hh3WCHWobQ6-A1lpSvU-YkS0V9cqj2nOU,476
|
80
79
|
primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
|
81
80
|
primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
|
82
|
-
primitive/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
83
|
-
primitive/sim/actions.py,sha256=oR77UmCp6PxDEuKvoNejeHOG6E5r6uHax3G9OZYoofM,4810
|
84
|
-
primitive/sim/commands.py,sha256=8PaOfL1MO6qxTn7mNVRnBU1X2wa3gk_mlbAhBW6MnI0,591
|
85
81
|
primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
86
82
|
primitive/utils/actions.py,sha256=HOFrmM3-0A_A3NS84MqrZ6JmQEiiPSoDqEeuu6b_qfQ,196
|
87
|
-
primitive/utils/auth.py,sha256=
|
83
|
+
primitive/utils/auth.py,sha256=kKvLjBCQXTo3k92RKhQj90B5clOIMBp6ocgaBqkGQBg,867
|
88
84
|
primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
|
85
|
+
primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw,2914
|
89
86
|
primitive/utils/config.py,sha256=DlFM5Nglo22WPtbpZSVtH7NX-PTMaKYlcrUE7GPRG4c,1058
|
90
|
-
primitive/utils/files.py,sha256=
|
87
|
+
primitive/utils/files.py,sha256=Aleb1U2pZZBGR24QYGP7LWt8A6WMaHM75_9oG9s2kbg,750
|
91
88
|
primitive/utils/git.py,sha256=1qNOu8X-33CavmrD580BmrFhD_WVO9PGWHUUboXJR_g,663
|
92
89
|
primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
|
93
90
|
primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
|
94
91
|
primitive/utils/shell.py,sha256=j7E1YwgNWw57dFHVfEbqRNVcPHX0xDefX2vFSNgeI_8,1648
|
95
92
|
primitive/utils/verible.py,sha256=Zb5NUISvcaIgEvgCDBWr-GCoceMa79Tcwvr5Wl9lfnA,2252
|
96
|
-
primitive-0.1.
|
97
|
-
primitive-0.1.
|
98
|
-
primitive-0.1.
|
99
|
-
primitive-0.1.
|
100
|
-
primitive-0.1.
|
93
|
+
primitive-0.1.70.dist-info/METADATA,sha256=i8Xfzp2x0MeOgBQWBq5oPABDrKf0oBS5XrDy5auHxQY,3806
|
94
|
+
primitive-0.1.70.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
95
|
+
primitive-0.1.70.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
|
96
|
+
primitive-0.1.70.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
|
97
|
+
primitive-0.1.70.dist-info/RECORD,,
|
primitive/lint/__init__.py
DELETED
File without changes
|