primitive 0.1.68__py3-none-any.whl → 0.1.69__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.
@@ -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 presigned_url:
122
- raise Exception("No presigned_url returned.")
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
- with open(path, "rb") as object_file:
126
- object_text = object_file.read()
127
- response = requests.put(presigned_url, data=object_text)
128
- if response.ok:
129
- logger.info(f"File {path} uploaded successfully.")
130
- update_file_status_result = self._update_file_status(
131
- file_id, is_uploading=False, is_complete=True
132
- )
133
- else:
134
- message = f"Failed to upload file {path}. {response.status_code}: {response.text}"
135
- logger.error(message)
136
- raise Exception(message)
137
- file_pk = update_file_status_result.data.get("fileUpdate").get("pk")
138
- transport = self.primitive.host_config.get("transport")
139
- host = self.primitive.host_config.get("host")
140
- file_access_url = f"{transport}://{host}/files/{file_pk}/presigned-url/"
141
- logger.info(f"Available at: {file_access_url}")
142
- return update_file_status_result
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
@@ -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
- presignedUrlForUpload
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
- token = self.primitive.host_config.get("token")
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
- for dirpath, dirnames, filenames in os.walk(source):
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(Path(dirpath).joinpath(filename))
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.68
3
+ Version: 0.1.69
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=HwbDpGY94x-5mM9iqNDH53F3G8OAv0XgJ3P44BhArVQ,130
1
+ primitive/__about__.py,sha256=saM2C0K9xd6ArRZ-ka1Araq4qbPyGBjdh5fngoELO7c,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
- primitive/cli.py,sha256=CGmWiqqCLMHtHGOUPuf3tVO6VvChBZ1VdSwCCglnBgA,2582
4
- primitive/client.py,sha256=P7cCDperMu3pxlmRAP-H2owM-cj1kRlZWrqujxnWa4o,2473
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=LVI-RB4a0YEuXUTYMXKL5Xi9euNwUI2nxj00mv8EFOg,2253
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=URCyk9Di423tTWAT6ObQcT62F-n27Q2MFGTcTNCPU8Y,7754
11
- primitive/agent/uploader.py,sha256=W-aXUgKZvcm9LbTXq8su_cgBl_mFrmcFfkkU9t8W04Q,3002
10
+ primitive/agent/runner.py,sha256=VGHvlu0JOu8sKcia7HFzFtynH596lKGH-mQY94lqpq0,8301
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=zA1vGXz56oKM1_eiDHGO0GVPHt0QMNrhWDHxQktiEqw,6744
28
- primitive/files/commands.py,sha256=x1fxixMrZFvYZGeQb3u5ElsbmWXMmYGq0f_zZArGp8Q,1084
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/mutations.py,sha256=SWxq6rwVWhouiuC72--Avpg9vybURFxmxiwkMY6dX7E,642
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=TtJKTR6tLmNrtWbOjJI-KJh4ZSJ1uG7ApE9GcY63m00,836
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=Yv__bQes3YIlzhOT9kVxtYhoA5CmUjPSvphl9PZ41k4,867
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.68.dist-info/METADATA,sha256=2zoWXWeqIRffEJBD8D_kzJ5zbGTJIh5H6TVe6wSEWlU,3777
97
- primitive-0.1.68.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
98
- primitive-0.1.68.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
99
- primitive-0.1.68.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
100
- primitive-0.1.68.dist-info/RECORD,,
93
+ primitive-0.1.69.dist-info/METADATA,sha256=8j9Ig3fsWbfeyVOD3XJAprWrNRt-25-iWOBQ0ecAmgo,3806
94
+ primitive-0.1.69.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
95
+ primitive-0.1.69.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
96
+ primitive-0.1.69.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
97
+ primitive-0.1.69.dist-info/RECORD,,
File without changes