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 CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.1.59"
4
+ __version__ = "0.1.62"
@@ -129,6 +129,7 @@ class Agent(BaseAction):
129
129
  source_dir=source_dir,
130
130
  job_id=job_run["id"],
131
131
  job_slug=job_run["job"]["slug"],
132
+ max_log_size=500 * 1024,
132
133
  )
133
134
  except Exception as e:
134
135
  # Log Error
@@ -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 probe_logs(self):
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.probe_logs()
51
+ self.log()
52
52
  if not self.is_running():
53
53
  break
54
54
 
@@ -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.logger_handle = logger.add(
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"])
@@ -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
- from ..utils.cache import get_artifacts_cache
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 upload_file(self, path: Path, prefix: str, job_run_id: str) -> str:
20
- file_upload_response = self.primitive.files.file_upload(
21
- path, key_prefix=prefix, job_run_id=job_run_id
22
- )
23
- return file_upload_response.json()["data"]["fileUpload"]["id"]
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
- subdirs = sorted(
30
- [job_cache for job_cache in artifacts_dir.iterdir() if job_cache.is_dir()],
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
- for job_cache in subdirs:
35
- job_run_id = job_cache.name
36
-
37
- files = None
38
- has_walk = getattr(job_cache, "walk", None)
39
- if has_walk:
40
- files = sorted(
41
- [
42
- w_path / file
43
- for w_path, _, w_files in job_cache.walk()
44
- for file in w_files
45
- ],
46
- key=lambda p: p.stat().st_size,
47
- )
48
- else:
49
- files = sorted(
50
- [
51
- Path(Path(w_path) / file)
52
- for w_path, _, w_files in os.walk(job_cache)
53
- for file in w_files
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
- print(f"Executing command: {command} on {hardware['name']}")
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.")
@@ -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=True)
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
@@ -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 create_trace_mutation
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
- @guard
14
- def trace_create(
44
+ def _pending_file_create(
15
45
  self,
16
- file_id: str,
17
- signal_id: str,
18
- signal_name: str,
19
- module_name: str,
20
- is_vector: bool,
21
- size: int,
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(create_trace_mutation)
53
+ mutation = gql(pending_file_create_mutation)
24
54
  input = {
25
- "fileId": file_id,
26
- "signalId": signal_id,
27
- "signalName": signal_name,
28
- "moduleName": module_name,
29
- "isVector": is_vector,
30
- "size": size,
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 file_upload(
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}")
@@ -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
- def file_upload_command(context, path, public, key_prefix):
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
- result = primitive.files.file_upload(path, is_public=public, key_prefix=key_prefix)
27
- message = result.json()
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
- create_trace_mutation = """
2
- mutation createTrace($input: TraceCreateInput!) {
3
- traceCreate(input: $input) {
4
- ... on Trace {
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
- signalId
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
+ )
@@ -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
@@ -29,3 +29,13 @@ query hardwareList(
29
29
  }
30
30
  """
31
31
  )
32
+
33
+ hardware_ssh_credentials = """
34
+ query hardwareSSHCredentials($id: GlobalID!) {
35
+ hardware(id: $id) {
36
+ id
37
+ pk
38
+ sshCredentials
39
+ }
40
+ }
41
+ """
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
- file_ids.append(
76
- self.upload_file(
77
- file_path,
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
@@ -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.59
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=C9j2a-6ERCVy_C_OHbf9s91MGCcAIm1Dlc-h-vRuSew,130
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=HuKR0bi3VVO8bR5IgcZ1paZw0lGK__8FTgfbWTm68go,5830
6
+ primitive/agent/actions.py,sha256=B7d2oNMjtjUP2RhD-QnNDWNl3jHwjUDk5KLWQ2OnNQ4,5883
7
7
  primitive/agent/commands.py,sha256=-dVDilELfkGfbZB7qfEPs77Dm1oT62qJj4tsIk4KoxI,254
8
- primitive/agent/process.py,sha256=2ZY3YoJHvoukrsCAZIt-AF2YKY4HEO5_jWji5K3W9fM,2267
9
- primitive/agent/provision.py,sha256=3EEzOV-ria6zf-pvfNddad1lzzd1QmfKInTIjmwX71Y,1673
10
- primitive/agent/runner.py,sha256=La1XSjH1vVj75DNI2rLR8GSRJvk3BosG_h_x1evGLCE,6878
11
- primitive/agent/uploader.py,sha256=5ZxonvRlMGRVBYx3hPEaWiPio5lMPi_zhtxz64V5S-A,2379
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=GdqoVbD65JuDCzdelmygMVuA5a8V-RZHdhxuAqrFzaA,1907
24
- primitive/exec/commands.py,sha256=iX8SP_9vwyy-R2unk5HaesLIdUAbhXCqCSWOOdFCtY0,509
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=AYh2o2BY4-glySkILoHZia9nVADKaAFM0z9Pe3GH2EM,2686
27
- primitive/files/commands.py,sha256=DDizo3xJnU3KLUBTMeeM72viVpnJinLwxs84tmqKhqo,810
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=iVolIhWu8-QktsdavBghr7VxomkxOV4de75BnovP9JM,215
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=KPRlpGbmeOKViAPWb_nzX6BMyMAjBHjdEz_lyBKqod0,18601
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=dhihQwr4O7zxDNRjeNWhkAXaSDOBsK-uqIczEGy1XLI,430
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=8eGOL7_sRj-7aW4TfPDH6gX5bYvxSIJkUxzRvPb7rjo,4904
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=hDVpEL2TePrWOH6q7Me_Oi-DH_viFrRxrVta-z4wBhM,1295
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=-7UjQaBqSGHzEEyX8pNjeYFFP0P3lVnDV0OkgPz1qHU,1050
90
- primitive/utils/verible.py,sha256=r7c_hfqvL0UicMmIzK3Cy_BfZI1ZpcfBeLqKEWFWqJo,2252
91
- primitive-0.1.59.dist-info/METADATA,sha256=pkPSFAV49Rz8KgwQyXJX9sjkuSlyc_mzsC_owgi7cto,3782
92
- primitive-0.1.59.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
93
- primitive-0.1.59.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
94
- primitive-0.1.59.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
95
- primitive-0.1.59.dist-info/RECORD,,
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,,