primitive 0.1.60__tar.gz → 0.1.62__tar.gz

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.
Files changed (107) hide show
  1. {primitive-0.1.60 → primitive-0.1.62}/.vscode/settings.json +1 -0
  2. {primitive-0.1.60 → primitive-0.1.62}/PKG-INFO +2 -1
  3. {primitive-0.1.60 → primitive-0.1.62}/pyproject.toml +1 -0
  4. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/__about__.py +1 -1
  5. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/runner.py +2 -1
  6. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/uploader.py +20 -22
  7. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/exec/actions.py +47 -1
  8. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/exec/commands.py +1 -1
  9. primitive-0.1.62/src/primitive/exec/interactive.py +99 -0
  10. primitive-0.1.62/src/primitive/files/actions.py +189 -0
  11. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/files/commands.py +17 -5
  12. primitive-0.1.62/src/primitive/files/graphql/mutations.py +32 -0
  13. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/actions.py +14 -1
  14. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/graphql/queries.py +10 -0
  15. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/sim/actions.py +5 -9
  16. {primitive-0.1.60 → primitive-0.1.62}/uv.lock +214 -1
  17. primitive-0.1.60/src/primitive/files/actions.py +0 -81
  18. primitive-0.1.60/src/primitive/files/graphql/mutations.py +0 -11
  19. {primitive-0.1.60 → primitive-0.1.62}/.git-hooks/pre-commit +0 -0
  20. {primitive-0.1.60 → primitive-0.1.62}/.gitattributes +0 -0
  21. {primitive-0.1.60 → primitive-0.1.62}/.github/workflows/lint.yml +0 -0
  22. {primitive-0.1.60 → primitive-0.1.62}/.github/workflows/publish.yml +0 -0
  23. {primitive-0.1.60 → primitive-0.1.62}/.gitignore +0 -0
  24. {primitive-0.1.60 → primitive-0.1.62}/LICENSE.txt +0 -0
  25. {primitive-0.1.60 → primitive-0.1.62}/Makefile +0 -0
  26. {primitive-0.1.60 → primitive-0.1.62}/README.md +0 -0
  27. {primitive-0.1.60 → primitive-0.1.62}/linux setup.md +0 -0
  28. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/__init__.py +0 -0
  29. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/__init__.py +0 -0
  30. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/actions.py +0 -0
  31. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/commands.py +0 -0
  32. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/process.py +0 -0
  33. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/agent/provision.py +0 -0
  34. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/auth/__init__.py +0 -0
  35. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/auth/actions.py +0 -0
  36. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/auth/commands.py +0 -0
  37. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/auth/graphql/__init__.py +0 -0
  38. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/auth/graphql/queries.py +0 -0
  39. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/cli.py +0 -0
  40. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/client.py +0 -0
  41. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/daemons/__init__.py +0 -0
  42. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/daemons/actions.py +0 -0
  43. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/daemons/commands.py +0 -0
  44. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/daemons/launch_agents.py +0 -0
  45. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/daemons/launch_service.py +0 -0
  46. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/exec/__init__.py +0 -0
  47. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/files/__init__.py +0 -0
  48. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/files/graphql/__init__.py +0 -0
  49. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/git/__init__.py +0 -0
  50. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/git/actions.py +0 -0
  51. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/git/commands.py +0 -0
  52. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/git/graphql/__init__.py +0 -0
  53. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/git/graphql/queries.py +0 -0
  54. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/graphql/__init__.py +0 -0
  55. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/graphql/relay.py +0 -0
  56. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/graphql/sdk.py +0 -0
  57. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/graphql/utility_fragments.py +0 -0
  58. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/__init__.py +0 -0
  59. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/commands.py +0 -0
  60. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/graphql/__init__.py +0 -0
  61. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/graphql/fragments.py +0 -0
  62. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/hardware/graphql/mutations.py +0 -0
  63. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/__init__.py +0 -0
  64. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/actions.py +0 -0
  65. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/commands.py +0 -0
  66. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/graphql/__init__.py +0 -0
  67. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/graphql/fragments.py +0 -0
  68. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/graphql/mutations.py +0 -0
  69. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/jobs/graphql/queries.py +0 -0
  70. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/lint/__init__.py +0 -0
  71. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/lint/actions.py +0 -0
  72. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/lint/commands.py +0 -0
  73. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/__init__.py +0 -0
  74. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/actions.py +0 -0
  75. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/commands.py +0 -0
  76. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/graphql/__init__.py +0 -0
  77. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/graphql/fragments.py +0 -0
  78. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/graphql/mutations.py +0 -0
  79. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/organizations/graphql/queries.py +0 -0
  80. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/__init__.py +0 -0
  81. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/actions.py +0 -0
  82. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/commands.py +0 -0
  83. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/graphql/__init__.py +0 -0
  84. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/graphql/fragments.py +0 -0
  85. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/graphql/mutations.py +0 -0
  86. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/projects/graphql/queries.py +0 -0
  87. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/__init__.py +0 -0
  88. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/actions.py +0 -0
  89. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/commands.py +0 -0
  90. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/graphql/__init__.py +0 -0
  91. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/graphql/fragments.py +0 -0
  92. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/graphql/mutations.py +0 -0
  93. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/reservations/graphql/queries.py +0 -0
  94. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/sim/__init__.py +0 -0
  95. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/sim/commands.py +0 -0
  96. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/__init__.py +0 -0
  97. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/actions.py +0 -0
  98. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/auth.py +0 -0
  99. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/cache.py +0 -0
  100. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/config.py +0 -0
  101. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/files.py +0 -0
  102. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/git.py +0 -0
  103. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/memory_size.py +0 -0
  104. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/printer.py +0 -0
  105. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/shell.py +0 -0
  106. {primitive-0.1.60 → primitive-0.1.62}/src/primitive/utils/verible.py +3 -3
  107. {primitive-0.1.60 → primitive-0.1.62}/tests/__init__.py +0 -0
@@ -3,6 +3,7 @@
3
3
  "Checkin",
4
4
  "loguru",
5
5
  "machdep",
6
+ "pkey",
6
7
  "testsuites",
7
8
  "untar",
8
9
  "Untaring",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: primitive
3
- Version: 0.1.60
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
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "loguru",
33
33
  "pyyaml",
34
34
  "primitive-pal == 0.1.4",
35
+ "paramiko[all]"
35
36
  ]
36
37
 
37
38
  [tool.uv]
@@ -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.62"
@@ -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
 
@@ -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
@@ -0,0 +1,189 @@
1
+ import hashlib
2
+ import sys
3
+ import threading
4
+ from pathlib import Path
5
+ from typing import Dict, Optional
6
+
7
+ import requests
8
+ from gql import gql
9
+ from loguru import logger
10
+
11
+ from primitive.graphql.sdk import create_requests_session
12
+ from primitive.utils.actions import BaseAction
13
+
14
+ from ..utils.auth import guard
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()
41
+
42
+
43
+ class Files(BaseAction):
44
+ def _pending_file_create(
45
+ self,
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,
52
+ ):
53
+ mutation = gql(pending_file_create_mutation)
54
+ input = {
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,
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
+
83
+ variables = {"input": input}
84
+ result = self.primitive.session.execute(
85
+ mutation, variable_values=variables, get_execution_result=True
86
+ )
87
+ return result
88
+
89
+ @guard
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(
144
+ self,
145
+ path: Path,
146
+ is_public: bool = False,
147
+ key_prefix: str = "",
148
+ job_run_id: str = "",
149
+ ):
150
+ """
151
+ This method uploads a file via the Primitive API.
152
+ This does NOT upload the file straight to S3
153
+ """
154
+ file_path = str(path.resolve())
155
+ if path.exists() is False:
156
+ raise FileNotFoundError(f"File not found at {file_path}")
157
+
158
+ if is_public:
159
+ operations = (
160
+ """{ "query": "mutation fileUpload($input: FileUploadInput!) { fileUpload(input: $input) { ... on File { id } } }", "variables": { "input": { "fileObject": null, "isPublic": true, "filePath": \""""
161
+ + file_path
162
+ + """\", "keyPrefix": \""""
163
+ + key_prefix
164
+ + """\", "jobRunId": \""""
165
+ + job_run_id
166
+ + """\" } } }"""
167
+ ) # noqa
168
+
169
+ else:
170
+ operations = (
171
+ """{ "query": "mutation fileUpload($input: FileUploadInput!) { fileUpload(input: $input) { ... on File { id } } }", "variables": { "input": { "fileObject": null, "isPublic": false, "filePath": \""""
172
+ + file_path
173
+ + """\", "keyPrefix": \""""
174
+ + key_prefix
175
+ + """\", "jobRunId": \""""
176
+ + job_run_id
177
+ + """\" } } }"""
178
+ ) # noqa
179
+ body = {
180
+ "operations": ("", operations),
181
+ "map": ("", '{"fileObject": ["variables.input.fileObject"]}'),
182
+ "fileObject": (path.name, open(path, "rb")),
183
+ }
184
+
185
+ session = create_requests_session(self.primitive.host_config)
186
+ transport = self.primitive.host_config.get("transport")
187
+ url = f"{transport}://{self.primitive.host}/"
188
+ response = session.post(url, files=body)
189
+ return response
@@ -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)
@@ -0,0 +1,32 @@
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 {
9
+ id
10
+ pk
11
+ }
12
+ ...OperationInfoFragment
13
+ }
14
+ }
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
+ """
@@ -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