primitive 0.1.60__py3-none-any.whl → 0.1.63__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.60"
4
+ __version__ = "0.1.63"
@@ -20,7 +20,7 @@ class Agent(BaseAction):
20
20
  logger.info(" [*] primitive")
21
21
  logger.info(f" [*] Version: {__version__}")
22
22
 
23
- # Create cache dir if it doesnt exist
23
+ # Create cache dir if it doesn't exist
24
24
  cache_dir = get_sources_cache()
25
25
 
26
26
  # Create uploader
@@ -53,6 +53,11 @@ class Agent(BaseAction):
53
53
  logger.debug("Active Reservation:")
54
54
  logger.debug(f"Node ID: {active_reservation_id}")
55
55
  logger.debug(f"PK: {active_reservation_pk}")
56
+
57
+ logger.debug("Running pre provisioning steps for reservation.")
58
+ self.primitive.provisioning.add_reservation_authorized_keys(
59
+ reservation_id=active_reservation_id
60
+ )
56
61
  else:
57
62
  if (
58
63
  hardware["activeReservation"] is None
@@ -62,6 +67,12 @@ class Agent(BaseAction):
62
67
  logger.debug("Previous Reservation is Complete:")
63
68
  logger.debug(f"Node ID: {active_reservation_id}")
64
69
  logger.debug(f"PK: {active_reservation_pk}")
70
+ logger.debug(
71
+ "Running cleanup provisioning steps for reservation."
72
+ )
73
+ self.primitive.provisioning.remove_reservation_authorized_keys(
74
+ reservation_id=active_reservation_id
75
+ )
65
76
  active_reservation_id = None
66
77
  active_reservation_pk = None
67
78
 
primitive/agent/runner.py CHANGED
@@ -191,7 +191,8 @@ class AgentRunner:
191
191
  logger.remove(self.logger_handle)
192
192
 
193
193
  self.logger_handle = logger.add(
194
- Path(self.logs_dir / f"{label}_{{time}}.primitive.log"), rotation=self.max_log_size
194
+ Path(self.logs_dir / f"{label}_{{time}}.primitive.log"),
195
+ rotation=self.max_log_size,
195
196
  )
196
197
 
197
198
  def provision(self) -> Optional[Dict]:
@@ -2,8 +2,10 @@ import typing
2
2
  from typing import Dict
3
3
  import shutil
4
4
  import os
5
- from loguru import logger
6
5
  from pathlib import Path, PurePath
6
+
7
+ from loguru import logger
8
+
7
9
  from ..utils.cache import get_artifacts_cache, get_logs_cache
8
10
 
9
11
  if typing.TYPE_CHECKING:
@@ -17,12 +19,6 @@ class Uploader:
17
19
  ):
18
20
  self.primitive = primitive
19
21
 
20
- def upload_file(self, path: Path, prefix: str, job_run_id: str) -> str:
21
- file_upload_response = self.primitive.files.file_upload(
22
- path, key_prefix=prefix, job_run_id=job_run_id
23
- )
24
- return file_upload_response.json()["data"]["fileUpload"]["id"]
25
-
26
22
  def upload_dir(self, cache: Path) -> Dict:
27
23
  file_ids = []
28
24
  job_run_id = cache.name
@@ -49,11 +45,12 @@ class Uploader:
49
45
  )
50
46
 
51
47
  for file in files:
52
- upload_id = self.upload_file(
53
- file,
54
- prefix=str(PurePath(file).relative_to(cache.parent).parent),
48
+ response = self.primitive.files.upload_file_via_api(
49
+ path=file,
50
+ key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
55
51
  job_run_id=job_run_id,
56
52
  )
53
+ upload_id = response.json()["data"]["fileUpload"]["id"]
57
54
 
58
55
  if upload_id:
59
56
  file_ids.append(upload_id)
@@ -66,23 +63,24 @@ class Uploader:
66
63
 
67
64
  return {job_run_id: file_ids}
68
65
 
69
-
70
66
  def scan(self) -> None:
71
67
  # Scan artifacts directory
72
68
  artifacts_dir = get_artifacts_cache()
73
69
  logs_dir = get_logs_cache()
74
70
 
75
- artifacts = sorted([
76
- artifacts_cache
77
- for artifacts_cache in artifacts_dir.iterdir()
78
- if artifacts_cache.is_dir()
79
- ], key=lambda p: p.stat().st_ctime)
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
+ )
80
79
 
81
- logs = sorted([
82
- logs_cache
83
- for logs_cache in logs_dir.iterdir()
84
- if logs_cache.is_dir()
85
- ], key=lambda p: p.stat().st_ctime)
80
+ logs = sorted(
81
+ [logs_cache for logs_cache in logs_dir.iterdir() if logs_cache.is_dir()],
82
+ key=lambda p: p.stat().st_ctime,
83
+ )
86
84
 
87
85
  log_files = {
88
86
  job_id: files
@@ -97,7 +95,7 @@ class Uploader:
97
95
  }
98
96
 
99
97
  files_by_id = {
100
- job_id: log_files.get(job_id, []) + artifact_files.get(job_id, [])
98
+ job_id: log_files.get(job_id, []) + artifact_files.get(job_id, [])
101
99
  for job_id in log_files.keys() | artifact_files.keys()
102
100
  }
103
101
 
primitive/client.py CHANGED
@@ -1,20 +1,22 @@
1
1
  import sys
2
+
3
+ from loguru import logger
4
+
5
+ from .agent.actions import Agent
2
6
  from .auth.actions import Auth
3
- from .projects.actions import Projects
4
- from .utils.config import read_config_file
7
+ from .daemons.actions import Daemons
8
+ from .exec.actions import Exec
5
9
  from .files.actions import Files
6
- from .sim.actions import Sim
7
- from .hardware.actions import Hardware
8
- from .lint.actions import Lint
9
- from .agent.actions import Agent
10
10
  from .git.actions import Git
11
- from .daemons.actions import Daemons
11
+ from .hardware.actions import Hardware
12
12
  from .jobs.actions import Jobs
13
+ from .lint.actions import Lint
13
14
  from .organizations.actions import Organizations
14
- from .exec.actions import Exec
15
+ from .projects.actions import Projects
16
+ from .provisioning.actions import Provisioning
15
17
  from .reservations.actions import Reservations
16
-
17
- from loguru import logger
18
+ from .sim.actions import Sim
19
+ from .utils.config import read_config_file
18
20
 
19
21
  logger.disable("primitive")
20
22
 
@@ -67,6 +69,7 @@ class Primitive:
67
69
  self.git: Git = Git(self)
68
70
  self.daemons: Daemons = Daemons(self)
69
71
  self.exec: Exec = Exec(self)
72
+ self.provisioning: Provisioning = Provisioning(self)
70
73
 
71
74
  def get_host_config(self):
72
75
  self.full_config = read_config_file()
primitive/exec/actions.py CHANGED
@@ -1,9 +1,13 @@
1
1
  import typing
2
2
 
3
+ from primitive.exec.interactive import interactive_shell
4
+
3
5
  if typing.TYPE_CHECKING:
4
6
  pass
5
7
 
6
8
 
9
+ from paramiko import SSHClient
10
+
7
11
  from primitive.utils.actions import BaseAction
8
12
 
9
13
 
@@ -41,7 +45,28 @@ class Exec(BaseAction):
41
45
  reservation_id=reservation["id"], desired_status="in_progress"
42
46
  )
43
47
 
44
- print(f"Executing command: {command} on {hardware['name']}")
48
+ ssh_hostname = hardware["hostname"]
49
+ ssh_username = hardware["ssh_username"]
50
+
51
+ ssh_client = SSHClient()
52
+ ssh_client.load_system_host_keys()
53
+
54
+ ssh_client.connect(
55
+ hostname=ssh_hostname,
56
+ username=ssh_username,
57
+ )
58
+
59
+ if command:
60
+ formatted_command = " ".join(command)
61
+ stdin, stdout, stderr = ssh_client.exec_command(formatted_command)
62
+ print(stdout.read())
63
+ ssh_client.close()
64
+ else:
65
+ channel = ssh_client.get_transport().open_session()
66
+ channel.get_pty()
67
+ channel.invoke_shell()
68
+ interactive_shell(channel)
69
+ ssh_client.close()
45
70
 
46
71
  if created_reservation_on_behalf_of_user:
47
72
  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
+ )
@@ -303,7 +303,7 @@ class Hardware(BaseAction):
303
303
  # and headers are set correctly
304
304
  self.primitive.get_host_config()
305
305
  self.check_in_http(is_healthy=True)
306
- return True
306
+ return result
307
307
 
308
308
  @guard
309
309
  def update_hardware_system_info(self):
@@ -10,6 +10,8 @@ fragment HardwareFragment on Hardware {
10
10
  isOnline
11
11
  isQuarantined
12
12
  isHealthy
13
+ hostname
14
+ sshUsername
13
15
  capabilities {
14
16
  id
15
17
  pk
File without changes
@@ -0,0 +1,62 @@
1
+ from pathlib import Path
2
+
3
+ from gql import gql
4
+
5
+ from primitive.utils.actions import BaseAction
6
+
7
+ from ..utils.auth import guard
8
+ from .graphql.queries import authorized_keys_query
9
+
10
+ HOME_DIRECTORY = Path.home()
11
+ AUTHORIZED_KEYS_FILEPATH = Path(HOME_DIRECTORY / ".ssh" / "authorized_keys")
12
+
13
+
14
+ class Provisioning(BaseAction):
15
+ @guard
16
+ def get_authorized_keys(self, reservation_id: str) -> str:
17
+ variables = {
18
+ "reservationId": reservation_id,
19
+ }
20
+ query = gql(authorized_keys_query)
21
+ result = self.primitive.session.execute(
22
+ query, variable_values=variables, get_execution_result=True
23
+ )
24
+ return result.data["authorizedKeys"]
25
+
26
+ def add_reservation_authorized_keys(self, reservation_id: str) -> None:
27
+ AUTHORIZED_KEYS_BACKUP_FILEPATH = Path(
28
+ HOME_DIRECTORY / ".ssh" / f"authorized_keys.bak-{reservation_id}"
29
+ )
30
+
31
+ if AUTHORIZED_KEYS_FILEPATH.exists():
32
+ AUTHORIZED_KEYS_BACKUP_FILEPATH.write_text(
33
+ AUTHORIZED_KEYS_FILEPATH.read_text()
34
+ )
35
+ else:
36
+ AUTHORIZED_KEYS_FILEPATH.touch()
37
+
38
+ authorized_keys = self.get_authorized_keys(reservation_id=reservation_id)
39
+
40
+ AUTHORIZED_KEYS_FILEPATH.write_text(
41
+ AUTHORIZED_KEYS_FILEPATH.read_text()
42
+ + f"\n## START PRIMITIVE SSH PUBLIC KEYS FOR RESERVATION ID {reservation_id}\n"
43
+ + authorized_keys
44
+ + f"\n## END PRIMITIVE SSH PUBLIC KEYS FOR RESERVATION ID {reservation_id}\n"
45
+ )
46
+
47
+ # self.primitive.sshd.reload()
48
+
49
+ def remove_reservation_authorized_keys(self, reservation_id: str) -> None:
50
+ AUTHORIZED_KEYS_BACKUP_FILEPATH = Path(
51
+ HOME_DIRECTORY / ".ssh" / f"authorized_keys.bak-{reservation_id}"
52
+ )
53
+
54
+ if AUTHORIZED_KEYS_BACKUP_FILEPATH.exists():
55
+ AUTHORIZED_KEYS_FILEPATH.write_text(
56
+ AUTHORIZED_KEYS_BACKUP_FILEPATH.read_text()
57
+ )
58
+ AUTHORIZED_KEYS_BACKUP_FILEPATH.unlink()
59
+ else:
60
+ AUTHORIZED_KEYS_FILEPATH.unlink()
61
+
62
+ # self.primitive.sshd.reload()
File without changes
@@ -0,0 +1,5 @@
1
+ authorized_keys_query = """
2
+ query authorizedKeys($reservationId: GlobalID!) {
3
+ authorizedKeys(reservationId: $reservationId)
4
+ }
5
+ """
@@ -32,7 +32,7 @@ fragment ReservationFragment on Reservation {
32
32
  reason
33
33
  startedAt
34
34
  endedAt
35
- elapsedTime
35
+ # elapsedTime
36
36
  status
37
37
  conclusion
38
38
  conclusionMessage
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
 
@@ -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,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: primitive
3
- Version: 0.1.60
3
+ Version: 0.1.63
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
7
7
  Author-email: Dylan Stein <dylan@primitive.tech>, Chase Zimmerman <chase@primitive.tech>
8
- License-Expression: MIT
9
- License-File: LICENSE.txt
8
+ License: MIT
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Programming Language :: Python
12
11
  Classifier: Programming Language :: Python :: 3
@@ -21,6 +20,7 @@ Requires-Python: >=3.11
21
20
  Requires-Dist: click
22
21
  Requires-Dist: gql[all]
23
22
  Requires-Dist: loguru
23
+ Requires-Dist: paramiko[all]
24
24
  Requires-Dist: primitive-pal==0.1.4
25
25
  Requires-Dist: pyyaml
26
26
  Description-Content-Type: text/markdown
@@ -1,14 +1,14 @@
1
- primitive/__about__.py,sha256=ltk-zb4IMmLsM4NDisEz2bXWrL5uUzzqsV5yJM95Caw,130
1
+ primitive/__about__.py,sha256=uiVaG3uarbeydunpM548F3cr2wnnZhtxTkRYjlZVUoU,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
3
  primitive/cli.py,sha256=CGmWiqqCLMHtHGOUPuf3tVO6VvChBZ1VdSwCCglnBgA,2582
4
- primitive/client.py,sha256=p-5z1iGM8ZydIrkYf4R6b7Yna73oszlGdXim9-Zsbyk,2364
4
+ primitive/client.py,sha256=P7cCDperMu3pxlmRAP-H2owM-cj1kRlZWrqujxnWa4o,2473
5
5
  primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- primitive/agent/actions.py,sha256=B7d2oNMjtjUP2RhD-QnNDWNl3jHwjUDk5KLWQ2OnNQ4,5883
6
+ primitive/agent/actions.py,sha256=Hosy2o2FntfBtcNqqHuMFq9dm99EVfySy0v2JGeufvc,6474
7
7
  primitive/agent/commands.py,sha256=-dVDilELfkGfbZB7qfEPs77Dm1oT62qJj4tsIk4KoxI,254
8
8
  primitive/agent/process.py,sha256=LVI-RB4a0YEuXUTYMXKL5Xi9euNwUI2nxj00mv8EFOg,2253
9
9
  primitive/agent/provision.py,sha256=rmwnro1K5F8mwtd45XAq7RVQmpDWnbBCQ8X_qgWhm3M,1546
10
- primitive/agent/runner.py,sha256=pX1KgYNxSxkSIDydsQa60C4zV2EvNaVXA9rQlG17wc8,7129
11
- primitive/agent/uploader.py,sha256=La33T2csENWKRsNagog3ELYe6110jufMnb7owmrPsIk,3150
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=hgUYi_G69SPbMRpVZv8EjeqIy41apXZIIw9lcg37zAY,2633
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,10 +38,10 @@ 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=jtthNgRyeRD8txt4WqEZskPtsDWU2Yg2gJZLSrEx1io,18603
41
42
  primitive/hardware/commands.py,sha256=_HaWOdRQSkhnA1xZZHZWgadSQ9Gijxtnzg2vc_IDSMA,1854
42
43
  primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- primitive/hardware/graphql/fragments.py,sha256=2uI0_WIhJISgd9Yx8tAdM7EUuAXEeQMUIXTIfkbZc2Q,246
44
+ primitive/hardware/graphql/fragments.py,sha256=PuqhW42fKUvRqOli5W7nOs2RfJ8FruSQC1gKBp3psBQ,271
44
45
  primitive/hardware/graphql/mutations.py,sha256=Zd6HxnIgTJ9mJQAfKJkdeDfstcPAal6Bj38pnKb_RuI,904
45
46
  primitive/hardware/graphql/queries.py,sha256=dhihQwr4O7zxDNRjeNWhkAXaSDOBsK-uqIczEGy1XLI,430
46
47
  primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -67,15 +68,19 @@ primitive/projects/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
67
68
  primitive/projects/graphql/fragments.py,sha256=02F5nyI8i-ML_bV5FFHUgFWM5bBBfjmz_tkP-4QOXjU,127
68
69
  primitive/projects/graphql/mutations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
70
  primitive/projects/graphql/queries.py,sha256=nFaVf6YOHA2L_FTgIUdRK-80hYTmv1a1X5ac7QPMp1k,646
71
+ primitive/provisioning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
+ primitive/provisioning/actions.py,sha256=IYZYAbtomtZtlkqDaBxx4e7PFKGkRNqek_tABH6q_zY,2116
73
+ primitive/provisioning/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ primitive/provisioning/graphql/queries.py,sha256=cBtuKa6shoatYZfKSnQoPJP6B8g8y3QhFqJ_pkvMcG0,134
70
75
  primitive/reservations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
76
  primitive/reservations/actions.py,sha256=XjjF0UJAgKryuSJqakLMAWshZIbuM-DkTmdU95cANs4,4434
72
77
  primitive/reservations/commands.py,sha256=OwWWE9DrvtrVBcBki0bKTOqCzCQk090c0rPIAt89JLY,2243
73
78
  primitive/reservations/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
- primitive/reservations/graphql/fragments.py,sha256=OPh8ylJR2kxfArBJ4IYIBLvWJyms3A2jVNNFLkTroRM,474
79
+ primitive/reservations/graphql/fragments.py,sha256=_TQfJeHky-Hh3WCHWobQ6-A1lpSvU-YkS0V9cqj2nOU,476
75
80
  primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
76
81
  primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
77
82
  primitive/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- primitive/sim/actions.py,sha256=8eGOL7_sRj-7aW4TfPDH6gX5bYvxSIJkUxzRvPb7rjo,4904
83
+ primitive/sim/actions.py,sha256=oR77UmCp6PxDEuKvoNejeHOG6E5r6uHax3G9OZYoofM,4810
79
84
  primitive/sim/commands.py,sha256=8PaOfL1MO6qxTn7mNVRnBU1X2wa3gk_mlbAhBW6MnI0,591
80
85
  primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
86
  primitive/utils/actions.py,sha256=HOFrmM3-0A_A3NS84MqrZ6JmQEiiPSoDqEeuu6b_qfQ,196
@@ -87,9 +92,9 @@ primitive/utils/git.py,sha256=1qNOu8X-33CavmrD580BmrFhD_WVO9PGWHUUboXJR_g,663
87
92
  primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
88
93
  primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
89
94
  primitive/utils/shell.py,sha256=j7E1YwgNWw57dFHVfEbqRNVcPHX0xDefX2vFSNgeI_8,1648
90
- primitive/utils/verible.py,sha256=r7c_hfqvL0UicMmIzK3Cy_BfZI1ZpcfBeLqKEWFWqJo,2252
91
- primitive-0.1.60.dist-info/METADATA,sha256=Ehe8DxemJilg_9uewSTTCQE9vobsu-P4EmSIKexXNLo,3782
92
- primitive-0.1.60.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
93
- primitive-0.1.60.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
94
- primitive-0.1.60.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
95
- primitive-0.1.60.dist-info/RECORD,,
95
+ primitive/utils/verible.py,sha256=Zb5NUISvcaIgEvgCDBWr-GCoceMa79Tcwvr5Wl9lfnA,2252
96
+ primitive-0.1.63.dist-info/METADATA,sha256=o-IZgpjA7lYJYn-sxuRBDA4EXxHA_c57G1U8Q3dcZzE,3774
97
+ primitive-0.1.63.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
98
+ primitive-0.1.63.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
99
+ primitive-0.1.63.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
100
+ primitive-0.1.63.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any