primitive 0.1.59__py3-none-any.whl → 0.1.62__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/actions.py +1 -0
- primitive/agent/process.py +2 -2
- primitive/agent/provision.py +2 -8
- primitive/agent/runner.py +19 -10
- primitive/agent/uploader.py +80 -53
- primitive/exec/actions.py +47 -1
- primitive/exec/commands.py +1 -1
- primitive/exec/interactive.py +99 -0
- primitive/files/actions.py +125 -17
- primitive/files/commands.py +17 -5
- primitive/files/graphql/mutations.py +27 -6
- primitive/hardware/actions.py +14 -1
- primitive/hardware/graphql/queries.py +10 -0
- primitive/sim/actions.py +5 -9
- primitive/utils/cache.py +14 -0
- primitive/utils/shell.py +25 -0
- primitive/utils/verible.py +3 -3
- {primitive-0.1.59.dist-info → primitive-0.1.62.dist-info}/METADATA +2 -1
- {primitive-0.1.59.dist-info → primitive-0.1.62.dist-info}/RECORD +23 -22
- {primitive-0.1.59.dist-info → primitive-0.1.62.dist-info}/WHEEL +0 -0
- {primitive-0.1.59.dist-info → primitive-0.1.62.dist-info}/entry_points.txt +0 -0
- {primitive-0.1.59.dist-info → primitive-0.1.62.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py
CHANGED
primitive/agent/actions.py
CHANGED
primitive/agent/process.py
CHANGED
@@ -34,7 +34,7 @@ class Process:
|
|
34
34
|
self.sel.register(self.process.stdout, selectors.EVENT_READ)
|
35
35
|
self.sel.register(self.process.stderr, selectors.EVENT_READ)
|
36
36
|
|
37
|
-
def
|
37
|
+
def log(self):
|
38
38
|
for key, _ in self.sel.select():
|
39
39
|
data = key.fileobj.readline()
|
40
40
|
if not data:
|
@@ -48,7 +48,7 @@ class Process:
|
|
48
48
|
|
49
49
|
def wait(self):
|
50
50
|
while True:
|
51
|
-
self.
|
51
|
+
self.log()
|
52
52
|
if not self.is_running():
|
53
53
|
break
|
54
54
|
|
primitive/agent/provision.py
CHANGED
@@ -2,6 +2,7 @@ import sys
|
|
2
2
|
from subprocess import Popen, PIPE
|
3
3
|
from pathlib import Path
|
4
4
|
from typing import Dict
|
5
|
+
from ..utils.shell import env_string_to_dict
|
5
6
|
|
6
7
|
|
7
8
|
class ProvisionPython:
|
@@ -34,14 +35,7 @@ class ProvisionPython:
|
|
34
35
|
output, _ = proc.communicate()
|
35
36
|
|
36
37
|
# Split the output into lines and parse it into a dictionary
|
37
|
-
env_vars =
|
38
|
-
|
39
|
-
for line in output.splitlines():
|
40
|
-
var_line = line.split("=", 1)
|
41
|
-
|
42
|
-
if len(var_line) == 2:
|
43
|
-
key, value = var_line
|
44
|
-
env_vars[key] = value
|
38
|
+
env_vars = env_string_to_dict(output)
|
45
39
|
|
46
40
|
cmd = f"python -m pip install -r {self.requirements_path}"
|
47
41
|
proc = Popen(
|
primitive/agent/runner.py
CHANGED
@@ -7,11 +7,10 @@ from typing import Dict, Iterable, List, Optional, TypedDict
|
|
7
7
|
|
8
8
|
import yaml
|
9
9
|
from loguru import logger
|
10
|
-
|
11
|
-
from ..utils.cache import get_artifacts_cache
|
12
|
-
from ..utils.files import find_files_for_extension
|
13
10
|
from .process import Process
|
14
11
|
from .provision import ProvisionPython
|
12
|
+
from ..utils.cache import get_artifacts_cache, get_logs_cache
|
13
|
+
from ..utils.files import find_files_for_extension
|
15
14
|
|
16
15
|
try:
|
17
16
|
from yaml import CLoader as Loader
|
@@ -56,12 +55,11 @@ class AgentRunner:
|
|
56
55
|
self.job_slug = job_slug
|
57
56
|
self.max_log_size = max_log_size
|
58
57
|
self.artifacts_dir = get_artifacts_cache(self.job_id)
|
58
|
+
self.logs_dir = get_logs_cache(self.job_id)
|
59
|
+
self.logger_handle = None
|
59
60
|
|
60
61
|
logger.enable("primitive")
|
61
|
-
self.
|
62
|
-
Path(self.artifacts_dir / "runner_{time}.log"),
|
63
|
-
rotation=self.max_log_size, # Rotate when the log file reaches 10MB
|
64
|
-
)
|
62
|
+
self.swap_logs(label="init")
|
65
63
|
|
66
64
|
logger.info(f"Scanning directory for job {self.job_slug}")
|
67
65
|
|
@@ -113,6 +111,9 @@ class AgentRunner:
|
|
113
111
|
conclusion = None
|
114
112
|
total_errors = 0
|
115
113
|
for step in self.steps():
|
114
|
+
# Swap logger
|
115
|
+
self.swap_logs(label=step["name"])
|
116
|
+
|
116
117
|
logger.info(f"Beginning step {step['name']}")
|
117
118
|
|
118
119
|
# Update workdir
|
@@ -138,7 +139,7 @@ class AgentRunner:
|
|
138
139
|
while proc.is_running():
|
139
140
|
# Check job status
|
140
141
|
status = self.primitive.jobs.get_job_status(self.job_id)
|
141
|
-
status_value = status["jobRun"]["status"]
|
142
|
+
status_value = status.data["jobRun"]["status"]
|
142
143
|
|
143
144
|
# TODO: Should probably use request_cancelled or something
|
144
145
|
# once we change it, we'll have to call conclude w/ cancelled status
|
@@ -184,6 +185,16 @@ class AgentRunner:
|
|
184
185
|
logger.info(f"Completed {self.job_slug} job")
|
185
186
|
logger.remove(self.logger_handle)
|
186
187
|
|
188
|
+
def swap_logs(self, label: str):
|
189
|
+
# Remove Handle
|
190
|
+
if self.logger_handle:
|
191
|
+
logger.remove(self.logger_handle)
|
192
|
+
|
193
|
+
self.logger_handle = logger.add(
|
194
|
+
Path(self.logs_dir / f"{label}_{{time}}.primitive.log"),
|
195
|
+
rotation=self.max_log_size,
|
196
|
+
)
|
197
|
+
|
187
198
|
def provision(self) -> Optional[Dict]:
|
188
199
|
match self.job["provision"]:
|
189
200
|
case "python":
|
@@ -199,8 +210,6 @@ class AgentRunner:
|
|
199
210
|
return prov.create_env()
|
200
211
|
|
201
212
|
def collect_artifacts(self, step: JobStep) -> None:
|
202
|
-
# str(PurePath(file_path).relative_to(Path(source))
|
203
|
-
|
204
213
|
# Search each artifact type
|
205
214
|
for artifact in step["artifacts"]:
|
206
215
|
files = find_files_for_extension(self.source_dir, artifact["extension"])
|
primitive/agent/uploader.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
import typing
|
2
|
+
from typing import Dict
|
2
3
|
import shutil
|
3
4
|
import os
|
4
|
-
from loguru import logger
|
5
5
|
from pathlib import Path, PurePath
|
6
|
-
|
6
|
+
|
7
|
+
from loguru import logger
|
8
|
+
|
9
|
+
from ..utils.cache import get_artifacts_cache, get_logs_cache
|
7
10
|
|
8
11
|
if typing.TYPE_CHECKING:
|
9
12
|
import primitive.client
|
@@ -16,62 +19,86 @@ class Uploader:
|
|
16
19
|
):
|
17
20
|
self.primitive = primitive
|
18
21
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
def upload_dir(self, cache: Path) -> Dict:
|
23
|
+
file_ids = []
|
24
|
+
job_run_id = cache.name
|
25
|
+
|
26
|
+
files = None
|
27
|
+
has_walk = getattr(cache, "walk", None)
|
28
|
+
if has_walk:
|
29
|
+
files = sorted(
|
30
|
+
[
|
31
|
+
current_path / file
|
32
|
+
for current_path, _, current_path_files in cache.walk()
|
33
|
+
for file in current_path_files
|
34
|
+
],
|
35
|
+
key=lambda p: p.stat().st_size,
|
36
|
+
)
|
37
|
+
else:
|
38
|
+
files = sorted(
|
39
|
+
[
|
40
|
+
Path(Path(current_path) / file)
|
41
|
+
for current_path, _, current_path_files in os.walk(cache)
|
42
|
+
for file in current_path_files
|
43
|
+
],
|
44
|
+
key=lambda p: p.stat().st_size,
|
45
|
+
)
|
46
|
+
|
47
|
+
for file in files:
|
48
|
+
response = self.primitive.files.upload_file_via_api(
|
49
|
+
path=file,
|
50
|
+
key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
|
51
|
+
job_run_id=job_run_id,
|
52
|
+
)
|
53
|
+
upload_id = response.json()["data"]["fileUpload"]["id"]
|
54
|
+
|
55
|
+
if upload_id:
|
56
|
+
file_ids.append(upload_id)
|
57
|
+
continue
|
58
|
+
|
59
|
+
logger.error(f"Unable to upload file {file}")
|
60
|
+
|
61
|
+
# Clean up job cache
|
62
|
+
shutil.rmtree(path=cache)
|
63
|
+
|
64
|
+
return {job_run_id: file_ids}
|
24
65
|
|
25
66
|
def scan(self) -> None:
|
26
67
|
# Scan artifacts directory
|
27
68
|
artifacts_dir = get_artifacts_cache()
|
69
|
+
logs_dir = get_logs_cache()
|
70
|
+
|
71
|
+
artifacts = sorted(
|
72
|
+
[
|
73
|
+
artifacts_cache
|
74
|
+
for artifacts_cache in artifacts_dir.iterdir()
|
75
|
+
if artifacts_cache.is_dir()
|
76
|
+
],
|
77
|
+
key=lambda p: p.stat().st_ctime,
|
78
|
+
)
|
28
79
|
|
29
|
-
|
30
|
-
[
|
80
|
+
logs = sorted(
|
81
|
+
[logs_cache for logs_cache in logs_dir.iterdir() if logs_cache.is_dir()],
|
31
82
|
key=lambda p: p.stat().st_ctime,
|
32
83
|
)
|
33
84
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
files
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
],
|
55
|
-
key=lambda p: p.stat().st_size,
|
56
|
-
)
|
57
|
-
|
58
|
-
file_ids = []
|
59
|
-
for file in files:
|
60
|
-
upload_id = self.upload_file(
|
61
|
-
file,
|
62
|
-
prefix=str(PurePath(file).relative_to(job_cache.parent).parent),
|
63
|
-
job_run_id=job_run_id,
|
64
|
-
)
|
65
|
-
|
66
|
-
if upload_id:
|
67
|
-
file_ids.append(upload_id)
|
68
|
-
continue
|
69
|
-
|
70
|
-
logger.error(f"Unable to upload file {file}")
|
71
|
-
|
72
|
-
# Update job run
|
73
|
-
if len(file_ids) > 0:
|
74
|
-
self.primitive.jobs.job_run_update(id=job_run_id, file_ids=file_ids)
|
75
|
-
|
76
|
-
# Clean up job cache
|
77
|
-
shutil.rmtree(path=job_cache)
|
85
|
+
log_files = {
|
86
|
+
job_id: files
|
87
|
+
for log_path in logs
|
88
|
+
for job_id, files in self.upload_dir(log_path).items()
|
89
|
+
}
|
90
|
+
|
91
|
+
artifact_files = {
|
92
|
+
job_id: files
|
93
|
+
for artifact_path in artifacts
|
94
|
+
for job_id, files in self.upload_dir(artifact_path).items()
|
95
|
+
}
|
96
|
+
|
97
|
+
files_by_id = {
|
98
|
+
job_id: log_files.get(job_id, []) + artifact_files.get(job_id, [])
|
99
|
+
for job_id in log_files.keys() | artifact_files.keys()
|
100
|
+
}
|
101
|
+
|
102
|
+
# Update job run
|
103
|
+
for job_id, files in files_by_id.items():
|
104
|
+
self.primitive.jobs.job_run_update(id=job_id, file_ids=files)
|
primitive/exec/actions.py
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
+
import tempfile
|
1
2
|
import typing
|
2
3
|
|
4
|
+
from primitive.exec.interactive import interactive_shell
|
5
|
+
|
3
6
|
if typing.TYPE_CHECKING:
|
4
7
|
pass
|
5
8
|
|
6
9
|
|
10
|
+
from paramiko import SSHClient
|
11
|
+
|
7
12
|
from primitive.utils.actions import BaseAction
|
8
13
|
|
9
14
|
|
@@ -41,7 +46,48 @@ class Exec(BaseAction):
|
|
41
46
|
reservation_id=reservation["id"], desired_status="in_progress"
|
42
47
|
)
|
43
48
|
|
44
|
-
|
49
|
+
ssh_credentials = self.primitive.hardware.get_hardware_ssh_credentials(
|
50
|
+
hardware_id=hardware["id"]
|
51
|
+
)
|
52
|
+
|
53
|
+
ssh_hostname = ssh_credentials["ssh_hostname"]
|
54
|
+
ssh_username = ssh_credentials["ssh_username"]
|
55
|
+
ssh_password = ssh_credentials["ssh_password"]
|
56
|
+
ssh_private_key = ssh_credentials["ssh_private_key"]
|
57
|
+
|
58
|
+
ssh_client = SSHClient()
|
59
|
+
ssh_client.load_system_host_keys()
|
60
|
+
keyfile = None
|
61
|
+
if ssh_private_key:
|
62
|
+
keyfile = tempfile.NamedTemporaryFile()
|
63
|
+
keyfile.write(ssh_private_key.encode())
|
64
|
+
keyfile.flush()
|
65
|
+
ssh_client.connect(
|
66
|
+
hostname=ssh_hostname,
|
67
|
+
username=ssh_username,
|
68
|
+
key_filename=keyfile.name,
|
69
|
+
)
|
70
|
+
else:
|
71
|
+
ssh_client.connect(
|
72
|
+
hostname=ssh_hostname,
|
73
|
+
username=ssh_username,
|
74
|
+
pkey=ssh_private_key,
|
75
|
+
password=ssh_password,
|
76
|
+
)
|
77
|
+
|
78
|
+
if command:
|
79
|
+
formatted_command = " ".join(command)
|
80
|
+
stdin, stdout, stderr = ssh_client.exec_command(formatted_command)
|
81
|
+
print(stdout.read())
|
82
|
+
ssh_client.close()
|
83
|
+
else:
|
84
|
+
channel = ssh_client.get_transport().open_session()
|
85
|
+
channel.get_pty()
|
86
|
+
channel.invoke_shell()
|
87
|
+
interactive_shell(channel)
|
88
|
+
ssh_client.close()
|
89
|
+
if keyfile:
|
90
|
+
keyfile.close()
|
45
91
|
|
46
92
|
if created_reservation_on_behalf_of_user:
|
47
93
|
print("Cleaning up reservation.")
|
primitive/exec/commands.py
CHANGED
@@ -13,7 +13,7 @@ if typing.TYPE_CHECKING:
|
|
13
13
|
type=str,
|
14
14
|
required=True,
|
15
15
|
)
|
16
|
-
@click.argument("command", nargs=-1, required=
|
16
|
+
@click.argument("command", nargs=-1, required=False)
|
17
17
|
def cli(context, hardware_identifier: str, command: str) -> None:
|
18
18
|
"""Exec"""
|
19
19
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# https://github.com/paramiko/paramiko/blob/main/demos/interactive.py
|
2
|
+
|
3
|
+
import socket
|
4
|
+
import sys
|
5
|
+
|
6
|
+
|
7
|
+
def _to_unicode(s):
|
8
|
+
"""
|
9
|
+
decode a string as ascii or utf8 if possible (as required by the sftp
|
10
|
+
protocol). if neither works, just return a byte string because the server
|
11
|
+
probably doesn't know the filename's encoding.
|
12
|
+
"""
|
13
|
+
try:
|
14
|
+
return s.encode("ascii")
|
15
|
+
except (UnicodeError, AttributeError):
|
16
|
+
try:
|
17
|
+
return s.decode("utf-8")
|
18
|
+
except UnicodeError:
|
19
|
+
return s
|
20
|
+
|
21
|
+
|
22
|
+
# windows does not have termios...
|
23
|
+
try:
|
24
|
+
import termios
|
25
|
+
import tty
|
26
|
+
|
27
|
+
has_termios = True
|
28
|
+
except ImportError:
|
29
|
+
has_termios = False
|
30
|
+
|
31
|
+
|
32
|
+
def interactive_shell(chan):
|
33
|
+
if has_termios:
|
34
|
+
posix_shell(chan)
|
35
|
+
else:
|
36
|
+
windows_shell(chan)
|
37
|
+
|
38
|
+
|
39
|
+
def posix_shell(chan):
|
40
|
+
import select
|
41
|
+
|
42
|
+
oldtty = termios.tcgetattr(sys.stdin)
|
43
|
+
try:
|
44
|
+
tty.setraw(sys.stdin.fileno())
|
45
|
+
tty.setcbreak(sys.stdin.fileno())
|
46
|
+
chan.settimeout(0.0)
|
47
|
+
|
48
|
+
while True:
|
49
|
+
r, w, e = select.select([chan, sys.stdin], [], [])
|
50
|
+
if chan in r:
|
51
|
+
try:
|
52
|
+
x = _to_unicode(chan.recv(1024))
|
53
|
+
if len(x) == 0:
|
54
|
+
sys.stdout.write("\r\n*** EOF\r\n")
|
55
|
+
break
|
56
|
+
sys.stdout.write(x)
|
57
|
+
sys.stdout.flush()
|
58
|
+
except socket.timeout:
|
59
|
+
pass
|
60
|
+
if sys.stdin in r:
|
61
|
+
x = sys.stdin.read(1)
|
62
|
+
if len(x) == 0:
|
63
|
+
break
|
64
|
+
chan.send(x)
|
65
|
+
|
66
|
+
finally:
|
67
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
|
68
|
+
|
69
|
+
|
70
|
+
# thanks to Mike Looijmans for this code
|
71
|
+
def windows_shell(chan):
|
72
|
+
import threading
|
73
|
+
|
74
|
+
sys.stdout.write(
|
75
|
+
"Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n"
|
76
|
+
)
|
77
|
+
|
78
|
+
def writeall(sock):
|
79
|
+
while True:
|
80
|
+
data = sock.recv(256)
|
81
|
+
if not data:
|
82
|
+
sys.stdout.write("\r\n*** EOF ***\r\n\r\n")
|
83
|
+
sys.stdout.flush()
|
84
|
+
break
|
85
|
+
sys.stdout.write(data)
|
86
|
+
sys.stdout.flush()
|
87
|
+
|
88
|
+
writer = threading.Thread(target=writeall, args=(chan,))
|
89
|
+
writer.start()
|
90
|
+
|
91
|
+
try:
|
92
|
+
while True:
|
93
|
+
d = sys.stdin.read(1)
|
94
|
+
if not d:
|
95
|
+
break
|
96
|
+
chan.send(d)
|
97
|
+
except EOFError:
|
98
|
+
# user hit ^Z or F6
|
99
|
+
pass
|
primitive/files/actions.py
CHANGED
@@ -1,34 +1,85 @@
|
|
1
|
+
import hashlib
|
2
|
+
import sys
|
3
|
+
import threading
|
1
4
|
from pathlib import Path
|
5
|
+
from typing import Dict, Optional
|
2
6
|
|
7
|
+
import requests
|
3
8
|
from gql import gql
|
9
|
+
from loguru import logger
|
4
10
|
|
5
11
|
from primitive.graphql.sdk import create_requests_session
|
6
12
|
from primitive.utils.actions import BaseAction
|
7
13
|
|
8
14
|
from ..utils.auth import guard
|
9
|
-
from .graphql.mutations import
|
15
|
+
from .graphql.mutations import (
|
16
|
+
file_update_mutation,
|
17
|
+
pending_file_create_mutation,
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
# this class can be used in multithreaded S3 client uploader
|
22
|
+
# this requires getting an S3 access token to this machine however
|
23
|
+
# we are using presigned urls instead at this time Oct 29th, 2024
|
24
|
+
class ProgressPercentage(object):
|
25
|
+
def __init__(self, filepath: Path) -> None:
|
26
|
+
self._filename = filepath.name
|
27
|
+
self._size = float(filepath.stat().st_size)
|
28
|
+
self._seen_so_far = 0
|
29
|
+
self._lock = threading.Lock()
|
30
|
+
|
31
|
+
def __call__(self, bytes_amount):
|
32
|
+
# To simplify, assume this is hooked up to a single filename
|
33
|
+
with self._lock:
|
34
|
+
self._seen_so_far += bytes_amount
|
35
|
+
percentage = (self._seen_so_far / self._size) * 100
|
36
|
+
sys.stdout.write(
|
37
|
+
"\r%s %s / %s (%.2f%%)"
|
38
|
+
% (self._filename, self._seen_so_far, self._size, percentage)
|
39
|
+
)
|
40
|
+
sys.stdout.flush()
|
10
41
|
|
11
42
|
|
12
43
|
class Files(BaseAction):
|
13
|
-
|
14
|
-
def trace_create(
|
44
|
+
def _pending_file_create(
|
15
45
|
self,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
46
|
+
file_name: str,
|
47
|
+
file_size: int,
|
48
|
+
file_checksum: str,
|
49
|
+
file_path: str,
|
50
|
+
key_prefix: str,
|
51
|
+
is_public: bool = False,
|
22
52
|
):
|
23
|
-
mutation = gql(
|
53
|
+
mutation = gql(pending_file_create_mutation)
|
24
54
|
input = {
|
25
|
-
"
|
26
|
-
"
|
27
|
-
"
|
28
|
-
"
|
29
|
-
"
|
30
|
-
"
|
55
|
+
"filePath": file_path,
|
56
|
+
"fileName": file_name,
|
57
|
+
"fileSize": file_size,
|
58
|
+
"fileChecksum": file_checksum,
|
59
|
+
"keyPrefix": key_prefix,
|
60
|
+
"isPublic": is_public,
|
61
|
+
}
|
62
|
+
variables = {"input": input}
|
63
|
+
result = self.primitive.session.execute(
|
64
|
+
mutation, variable_values=variables, get_execution_result=True
|
65
|
+
)
|
66
|
+
return result.data.get("pendingFileCreate")
|
67
|
+
|
68
|
+
def _update_file_status(
|
69
|
+
self,
|
70
|
+
file_id: str,
|
71
|
+
is_uploading: Optional[bool] = None,
|
72
|
+
is_complete: Optional[bool] = None,
|
73
|
+
):
|
74
|
+
mutation = gql(file_update_mutation)
|
75
|
+
input: Dict[str, str | bool] = {
|
76
|
+
"id": file_id,
|
31
77
|
}
|
78
|
+
if is_uploading is not None:
|
79
|
+
input["isUploading"] = is_uploading
|
80
|
+
if is_complete is not None:
|
81
|
+
input["isComplete"] = is_complete
|
82
|
+
|
32
83
|
variables = {"input": input}
|
33
84
|
result = self.primitive.session.execute(
|
34
85
|
mutation, variable_values=variables, get_execution_result=True
|
@@ -36,13 +87,70 @@ class Files(BaseAction):
|
|
36
87
|
return result
|
37
88
|
|
38
89
|
@guard
|
39
|
-
def
|
90
|
+
def upload_file_direct(
|
91
|
+
self,
|
92
|
+
path: Path,
|
93
|
+
is_public: False,
|
94
|
+
key_prefix: str = "",
|
95
|
+
file_id: Optional[str] = None,
|
96
|
+
):
|
97
|
+
logger.enable("primitive")
|
98
|
+
if path.exists() is False:
|
99
|
+
raise Exception(f"File {path} does not exist.")
|
100
|
+
|
101
|
+
file_size = path.stat().st_size
|
102
|
+
if file_size == 0:
|
103
|
+
raise Exception(f"{path} is empty.")
|
104
|
+
|
105
|
+
file_checksum = hashlib.md5(path.read_bytes()).hexdigest()
|
106
|
+
|
107
|
+
if not file_id:
|
108
|
+
pending_file_create = self._pending_file_create(
|
109
|
+
file_name=path.name,
|
110
|
+
file_size=path.stat().st_size,
|
111
|
+
file_checksum=file_checksum,
|
112
|
+
file_path=str(path),
|
113
|
+
key_prefix=key_prefix,
|
114
|
+
is_public=is_public,
|
115
|
+
)
|
116
|
+
file_id = pending_file_create.get("id")
|
117
|
+
presigned_url = pending_file_create.get("presignedUrlForUpload")
|
118
|
+
|
119
|
+
if not file_id:
|
120
|
+
raise Exception("No file_id found or provided.")
|
121
|
+
if not presigned_url:
|
122
|
+
raise Exception("No presigned_url returned.")
|
123
|
+
|
124
|
+
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
|
+
file_access_url = f"{self.primitive.host_config.get("transport")}://{self.primitive.host}/files/{file_pk}/presigned-url/"
|
139
|
+
logger.info(f"Available at: {file_access_url}")
|
140
|
+
return update_file_status_result
|
141
|
+
|
142
|
+
@guard
|
143
|
+
def upload_file_via_api(
|
40
144
|
self,
|
41
145
|
path: Path,
|
42
146
|
is_public: bool = False,
|
43
147
|
key_prefix: str = "",
|
44
148
|
job_run_id: str = "",
|
45
149
|
):
|
150
|
+
"""
|
151
|
+
This method uploads a file via the Primitive API.
|
152
|
+
This does NOT upload the file straight to S3
|
153
|
+
"""
|
46
154
|
file_path = str(path.resolve())
|
47
155
|
if path.exists() is False:
|
48
156
|
raise FileNotFoundError(f"File not found at {file_path}")
|
primitive/files/commands.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
import json
|
2
|
+
import typing
|
3
|
+
from pathlib import Path
|
4
|
+
|
1
5
|
import click
|
6
|
+
|
2
7
|
from ..utils.printer import print_result
|
3
|
-
from pathlib import Path
|
4
|
-
import typing
|
5
8
|
|
6
9
|
if typing.TYPE_CHECKING:
|
7
10
|
from ..client import Primitive
|
@@ -19,10 +22,19 @@ def cli(context):
|
|
19
22
|
@click.argument("path", type=click.Path(exists=True))
|
20
23
|
@click.option("--public", "-p", help="Is this a Public file", is_flag=True)
|
21
24
|
@click.option("--key-prefix", "-k", help="Key Prefix", default="")
|
22
|
-
|
25
|
+
@click.option("--direct", "-k", help="direct", is_flag=True)
|
26
|
+
def file_upload_command(context, path, public, key_prefix, direct):
|
23
27
|
"""File Upload"""
|
24
28
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
25
29
|
path = Path(path)
|
26
|
-
|
27
|
-
|
30
|
+
if direct:
|
31
|
+
result = primitive.files.upload_file_direct(
|
32
|
+
path, is_public=public, key_prefix=key_prefix
|
33
|
+
)
|
34
|
+
else:
|
35
|
+
result = primitive.files.upload_file_via_api(
|
36
|
+
path, is_public=public, key_prefix=key_prefix
|
37
|
+
)
|
38
|
+
|
39
|
+
message = json.dumps(result.data)
|
28
40
|
print_result(message=message, context=context)
|
@@ -1,11 +1,32 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
from primitive.graphql.utility_fragments import operation_info_fragment
|
2
|
+
|
3
|
+
file_update_mutation = (
|
4
|
+
operation_info_fragment
|
5
|
+
+ """
|
6
|
+
mutation fileUpdate($input: FileInputPartial!) {
|
7
|
+
fileUpdate(input: $input) {
|
8
|
+
... on File {
|
5
9
|
id
|
6
|
-
|
7
|
-
signalName
|
10
|
+
pk
|
8
11
|
}
|
12
|
+
...OperationInfoFragment
|
9
13
|
}
|
10
14
|
}
|
11
15
|
"""
|
16
|
+
)
|
17
|
+
|
18
|
+
pending_file_create_mutation = (
|
19
|
+
operation_info_fragment
|
20
|
+
+ """
|
21
|
+
mutation pendingFileCreate($input: PendingFileCreateInput!) {
|
22
|
+
pendingFileCreate(input: $input) {
|
23
|
+
... on File {
|
24
|
+
id
|
25
|
+
pk
|
26
|
+
presignedUrlForUpload
|
27
|
+
}
|
28
|
+
...OperationInfoFragment
|
29
|
+
}
|
30
|
+
}
|
31
|
+
"""
|
32
|
+
)
|
primitive/hardware/actions.py
CHANGED
@@ -22,7 +22,7 @@ from .graphql.mutations import (
|
|
22
22
|
hardware_update_mutation,
|
23
23
|
register_hardware_mutation,
|
24
24
|
)
|
25
|
-
from .graphql.queries import hardware_list
|
25
|
+
from .graphql.queries import hardware_list, hardware_ssh_credentials
|
26
26
|
|
27
27
|
if typing.TYPE_CHECKING:
|
28
28
|
pass
|
@@ -479,3 +479,16 @@ class Hardware(BaseAction):
|
|
479
479
|
raise Exception(f"No hardware found with slug {hardware_identifier}")
|
480
480
|
|
481
481
|
return hardware
|
482
|
+
|
483
|
+
@guard
|
484
|
+
def get_hardware_ssh_credentials(self, hardware_id: str):
|
485
|
+
query = gql(hardware_ssh_credentials)
|
486
|
+
|
487
|
+
variables = {
|
488
|
+
"id": hardware_id,
|
489
|
+
}
|
490
|
+
result = self.primitive.session.execute(
|
491
|
+
query, variable_values=variables, get_execution_result=True
|
492
|
+
)
|
493
|
+
ssh_credentials = result.data.get("hardware").get("sshCredentials")
|
494
|
+
return ssh_credentials
|
primitive/sim/actions.py
CHANGED
@@ -46,10 +46,6 @@ class Sim(BaseAction):
|
|
46
46
|
|
47
47
|
return True, message
|
48
48
|
|
49
|
-
def upload_file(self, path: Path, prefix: str) -> str:
|
50
|
-
file_upload_response = self.primitive.files.file_upload(path, key_prefix=prefix)
|
51
|
-
return file_upload_response.json()["data"]["fileUpload"]["id"]
|
52
|
-
|
53
49
|
def collect_artifacts(self, source: Path, job_run_id: str) -> None:
|
54
50
|
# Parse VCD artifacts using rust binding
|
55
51
|
# TODO: eventually make this smarter, only parsing VCDs for failed tests
|
@@ -72,12 +68,12 @@ class Sim(BaseAction):
|
|
72
68
|
)
|
73
69
|
for file_path in files:
|
74
70
|
try:
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
prefix=f"{job_run_id}/{str(PurePath(file_path).relative_to(Path(source)).parent)}",
|
79
|
-
)
|
71
|
+
key_prefix = f"{job_run_id}/{str(PurePath(file_path).relative_to(Path(source)).parent)}"
|
72
|
+
file_upload_response = self.primitive.files.upload_file_via_api(
|
73
|
+
file_path, key_prefix=key_prefix
|
80
74
|
)
|
75
|
+
file_id = file_upload_response.json()["data"]["fileUpload"]["id"]
|
76
|
+
file_ids.append(file_id)
|
81
77
|
except FileNotFoundError:
|
82
78
|
logger.warning(f"{file_path} not found...")
|
83
79
|
|
primitive/utils/cache.py
CHANGED
@@ -44,6 +44,20 @@ def get_artifacts_cache(cache_id: str = None) -> Path:
|
|
44
44
|
return artifacts_dir
|
45
45
|
|
46
46
|
|
47
|
+
def get_logs_cache(cache_id: str = None) -> Path:
|
48
|
+
cache_dir = get_cache_dir()
|
49
|
+
|
50
|
+
logs_dir = cache_dir / "logs"
|
51
|
+
|
52
|
+
if cache_id:
|
53
|
+
logs_dir = logs_dir / cache_id
|
54
|
+
|
55
|
+
if not logs_dir.exists():
|
56
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
57
|
+
|
58
|
+
return logs_dir
|
59
|
+
|
60
|
+
|
47
61
|
def get_deps_cache() -> Path:
|
48
62
|
cache_dir = get_cache_dir()
|
49
63
|
|
primitive/utils/shell.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
import subprocess
|
3
|
+
from typing import Dict
|
3
4
|
|
4
5
|
|
5
6
|
def add_path_to_shell(path: Path):
|
@@ -31,3 +32,27 @@ def add_path_to_shell(path: Path):
|
|
31
32
|
file.write(f"export PATH={path}:$PATH\n")
|
32
33
|
|
33
34
|
return True
|
35
|
+
|
36
|
+
|
37
|
+
def env_string_to_dict(env_str: str) -> Dict:
|
38
|
+
lines = env_str.splitlines()
|
39
|
+
|
40
|
+
current_key = None
|
41
|
+
current_value = []
|
42
|
+
env_dict = {}
|
43
|
+
for line in lines:
|
44
|
+
if "=" in line:
|
45
|
+
if current_key is not None:
|
46
|
+
env_dict[current_key] = "\n".join(current_value)
|
47
|
+
|
48
|
+
key, value = line.split("=", 1)
|
49
|
+
|
50
|
+
current_key = key
|
51
|
+
current_value = [value]
|
52
|
+
else:
|
53
|
+
current_value.append(line)
|
54
|
+
|
55
|
+
if current_key is not None:
|
56
|
+
env_dict[current_key] = "\n".join(current_value)
|
57
|
+
|
58
|
+
return env_dict
|
primitive/utils/verible.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
import tarfile
|
2
|
-
import requests
|
3
|
-
from .shell import add_path_to_shell
|
4
|
-
from .cache import get_deps_cache
|
5
2
|
|
3
|
+
import requests
|
6
4
|
from loguru import logger
|
7
5
|
|
6
|
+
from .cache import get_deps_cache
|
7
|
+
from .shell import add_path_to_shell
|
8
8
|
|
9
9
|
VERIBLE_MAC_OS_LINK = "https://github.com/chipsalliance/verible/releases/download/v0.0-3752-g8b64887e/verible-v0.0-3752-g8b64887e-macOS.tar.gz"
|
10
10
|
VERIBLE_WINDOWS_64_OS_LINK = "https://github.com/chipsalliance/verible/releases/download/v0.0-3752-g8b64887e/verible-v0.0-3752-g8b64887e-win64.zip"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: primitive
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.62
|
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
|
@@ -21,6 +21,7 @@ Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: click
|
22
22
|
Requires-Dist: gql[all]
|
23
23
|
Requires-Dist: loguru
|
24
|
+
Requires-Dist: paramiko[all]
|
24
25
|
Requires-Dist: primitive-pal==0.1.4
|
25
26
|
Requires-Dist: pyyaml
|
26
27
|
Description-Content-Type: text/markdown
|
@@ -1,14 +1,14 @@
|
|
1
|
-
primitive/__about__.py,sha256=
|
1
|
+
primitive/__about__.py,sha256=geKDLLFDQydMN_sAK9SDHgrotZtzInxl2yNgIL4tHng,130
|
2
2
|
primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
|
3
3
|
primitive/cli.py,sha256=CGmWiqqCLMHtHGOUPuf3tVO6VvChBZ1VdSwCCglnBgA,2582
|
4
4
|
primitive/client.py,sha256=p-5z1iGM8ZydIrkYf4R6b7Yna73oszlGdXim9-Zsbyk,2364
|
5
5
|
primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
primitive/agent/actions.py,sha256=
|
6
|
+
primitive/agent/actions.py,sha256=B7d2oNMjtjUP2RhD-QnNDWNl3jHwjUDk5KLWQ2OnNQ4,5883
|
7
7
|
primitive/agent/commands.py,sha256=-dVDilELfkGfbZB7qfEPs77Dm1oT62qJj4tsIk4KoxI,254
|
8
|
-
primitive/agent/process.py,sha256=
|
9
|
-
primitive/agent/provision.py,sha256=
|
10
|
-
primitive/agent/runner.py,sha256=
|
11
|
-
primitive/agent/uploader.py,sha256=
|
8
|
+
primitive/agent/process.py,sha256=LVI-RB4a0YEuXUTYMXKL5Xi9euNwUI2nxj00mv8EFOg,2253
|
9
|
+
primitive/agent/provision.py,sha256=rmwnro1K5F8mwtd45XAq7RVQmpDWnbBCQ8X_qgWhm3M,1546
|
10
|
+
primitive/agent/runner.py,sha256=xt0Ty-dAc8VatNIZwBZUJbC3mHXLpywpRVVyr_RvVYw,7142
|
11
|
+
primitive/agent/uploader.py,sha256=W-aXUgKZvcm9LbTXq8su_cgBl_mFrmcFfkkU9t8W04Q,3002
|
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
|
@@ -20,13 +20,14 @@ primitive/daemons/commands.py,sha256=-Muh-6ib4uAVtPn_67AcMrDwuCwYlCnRQozCi2Xurmk
|
|
20
20
|
primitive/daemons/launch_agents.py,sha256=qovt32gwpjGDd82z_SY5EGCUjaUyNA49pZFajZsw3eE,4796
|
21
21
|
primitive/daemons/launch_service.py,sha256=FPB9qKEjhllRfEpct0ng2L9lpIaGJbQwn1JdFT8uBA8,5600
|
22
22
|
primitive/exec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
-
primitive/exec/actions.py,sha256=
|
24
|
-
primitive/exec/commands.py,sha256=
|
23
|
+
primitive/exec/actions.py,sha256=Q7Nj4SVIh2XH83mirpBRQietwWZDZXdoBF3gGvUVzCU,3425
|
24
|
+
primitive/exec/commands.py,sha256=66LO2kkJC-ynNZQpUCXv4Ol15QoacdSZAHblePDcmLo,510
|
25
|
+
primitive/exec/interactive.py,sha256=TscY6s2ZysijidKPheq6y-fCErUVLS0zcdTW8XyFWGI,2435
|
25
26
|
primitive/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
-
primitive/files/actions.py,sha256=
|
27
|
-
primitive/files/commands.py,sha256=
|
27
|
+
primitive/files/actions.py,sha256=q33aP7UvCFfhhJ8iOnvI57jOpobBUaJspRN4_3vJApU,6675
|
28
|
+
primitive/files/commands.py,sha256=x1fxixMrZFvYZGeQb3u5ElsbmWXMmYGq0f_zZArGp8Q,1084
|
28
29
|
primitive/files/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
-
primitive/files/graphql/mutations.py,sha256=
|
30
|
+
primitive/files/graphql/mutations.py,sha256=SWxq6rwVWhouiuC72--Avpg9vybURFxmxiwkMY6dX7E,642
|
30
31
|
primitive/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
32
|
primitive/git/actions.py,sha256=0KHeHViZZqIhF6-Eqvhs0g_UmglqyWrOQKElQCm6jVw,1506
|
32
33
|
primitive/git/commands.py,sha256=sCeSjkRgSEjCEsB5seXgB_h6xfk0KpvMvzMKoRfUbRA,1177
|
@@ -37,12 +38,12 @@ primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,72
|
|
37
38
|
primitive/graphql/sdk.py,sha256=BhCGmDtc4sNnH8CxbQSJyFwOZ-ZSqMtjsxMB3JRBhPw,1456
|
38
39
|
primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
|
39
40
|
primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
-
primitive/hardware/actions.py,sha256=
|
41
|
+
primitive/hardware/actions.py,sha256=fgwTASzElwk4PxLS2_sdIQYdwbRnuKczThV-aN_MT5A,19049
|
41
42
|
primitive/hardware/commands.py,sha256=_HaWOdRQSkhnA1xZZHZWgadSQ9Gijxtnzg2vc_IDSMA,1854
|
42
43
|
primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
44
|
primitive/hardware/graphql/fragments.py,sha256=2uI0_WIhJISgd9Yx8tAdM7EUuAXEeQMUIXTIfkbZc2Q,246
|
44
45
|
primitive/hardware/graphql/mutations.py,sha256=Zd6HxnIgTJ9mJQAfKJkdeDfstcPAal6Bj38pnKb_RuI,904
|
45
|
-
primitive/hardware/graphql/queries.py,sha256=
|
46
|
+
primitive/hardware/graphql/queries.py,sha256=1xEe8JoXt_MmqJ3bIeCk13PpzyNFJn4OgRDU2_93mZA,574
|
46
47
|
primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
48
|
primitive/jobs/actions.py,sha256=CtyO-Z9614TgIoXJJX1QGsoll0fgpBIjG9PJH5JwCQs,4901
|
48
49
|
primitive/jobs/commands.py,sha256=MxPCkBEYW_eLNqgCRYeyj7ZcLOFAWfpVZlqDR2Y_S0o,830
|
@@ -75,21 +76,21 @@ primitive/reservations/graphql/fragments.py,sha256=OPh8ylJR2kxfArBJ4IYIBLvWJyms3
|
|
75
76
|
primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
|
76
77
|
primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
|
77
78
|
primitive/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
78
|
-
primitive/sim/actions.py,sha256=
|
79
|
+
primitive/sim/actions.py,sha256=oR77UmCp6PxDEuKvoNejeHOG6E5r6uHax3G9OZYoofM,4810
|
79
80
|
primitive/sim/commands.py,sha256=8PaOfL1MO6qxTn7mNVRnBU1X2wa3gk_mlbAhBW6MnI0,591
|
80
81
|
primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
81
82
|
primitive/utils/actions.py,sha256=HOFrmM3-0A_A3NS84MqrZ6JmQEiiPSoDqEeuu6b_qfQ,196
|
82
83
|
primitive/utils/auth.py,sha256=TtJKTR6tLmNrtWbOjJI-KJh4ZSJ1uG7ApE9GcY63m00,836
|
83
|
-
primitive/utils/cache.py,sha256=
|
84
|
+
primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
|
84
85
|
primitive/utils/config.py,sha256=DlFM5Nglo22WPtbpZSVtH7NX-PTMaKYlcrUE7GPRG4c,1058
|
85
86
|
primitive/utils/files.py,sha256=Yv__bQes3YIlzhOT9kVxtYhoA5CmUjPSvphl9PZ41k4,867
|
86
87
|
primitive/utils/git.py,sha256=1qNOu8X-33CavmrD580BmrFhD_WVO9PGWHUUboXJR_g,663
|
87
88
|
primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
|
88
89
|
primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
|
89
|
-
primitive/utils/shell.py,sha256
|
90
|
-
primitive/utils/verible.py,sha256=
|
91
|
-
primitive-0.1.
|
92
|
-
primitive-0.1.
|
93
|
-
primitive-0.1.
|
94
|
-
primitive-0.1.
|
95
|
-
primitive-0.1.
|
90
|
+
primitive/utils/shell.py,sha256=j7E1YwgNWw57dFHVfEbqRNVcPHX0xDefX2vFSNgeI_8,1648
|
91
|
+
primitive/utils/verible.py,sha256=Zb5NUISvcaIgEvgCDBWr-GCoceMa79Tcwvr5Wl9lfnA,2252
|
92
|
+
primitive-0.1.62.dist-info/METADATA,sha256=sQ2iai9V1lS1U_snKtzwP4JsUgUaHPYyqzoRnmaHVhk,3811
|
93
|
+
primitive-0.1.62.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
94
|
+
primitive-0.1.62.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
|
95
|
+
primitive-0.1.62.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
|
96
|
+
primitive-0.1.62.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|