primitive 0.1.58__tar.gz → 0.1.60__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 (109) hide show
  1. primitive-0.1.60/.vscode/settings.json +22 -0
  2. {primitive-0.1.58 → primitive-0.1.60}/PKG-INFO +1 -1
  3. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/__about__.py +1 -1
  4. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/agent/actions.py +8 -4
  5. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/agent/process.py +2 -2
  6. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/agent/provision.py +2 -8
  7. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/agent/runner.py +22 -12
  8. primitive-0.1.60/src/primitive/agent/uploader.py +106 -0
  9. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/auth/actions.py +5 -12
  10. primitive-0.1.60/src/primitive/auth/graphql/queries.py +13 -0
  11. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/cli.py +11 -5
  12. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/client.py +4 -0
  13. primitive-0.1.60/src/primitive/exec/actions.py +50 -0
  14. primitive-0.1.60/src/primitive/exec/commands.py +22 -0
  15. primitive-0.1.60/src/primitive/files/__init__.py +0 -0
  16. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/files/actions.py +9 -16
  17. primitive-0.1.60/src/primitive/files/graphql/__init__.py +0 -0
  18. primitive-0.1.60/src/primitive/files/graphql/mutations.py +11 -0
  19. primitive-0.1.60/src/primitive/git/__init__.py +0 -0
  20. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/git/actions.py +11 -11
  21. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/git/commands.py +7 -6
  22. primitive-0.1.60/src/primitive/git/graphql/__init__.py +0 -0
  23. primitive-0.1.60/src/primitive/git/graphql/queries.py +7 -0
  24. primitive-0.1.60/src/primitive/graphql/__init__.py +0 -0
  25. primitive-0.1.60/src/primitive/graphql/relay.py +32 -0
  26. primitive-0.1.60/src/primitive/graphql/utility_fragments.py +19 -0
  27. primitive-0.1.60/src/primitive/hardware/__init__.py +0 -0
  28. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/hardware/actions.py +74 -121
  29. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/hardware/commands.py +15 -5
  30. primitive-0.1.60/src/primitive/hardware/graphql/__init__.py +0 -0
  31. primitive-0.1.60/src/primitive/hardware/graphql/fragments.py +22 -0
  32. primitive-0.1.60/src/primitive/hardware/graphql/mutations.py +45 -0
  33. primitive-0.1.60/src/primitive/hardware/graphql/queries.py +31 -0
  34. primitive-0.1.60/src/primitive/jobs/__init__.py +0 -0
  35. primitive-0.1.60/src/primitive/jobs/actions.py +162 -0
  36. primitive-0.1.60/src/primitive/jobs/graphql/__init__.py +0 -0
  37. primitive-0.1.60/src/primitive/jobs/graphql/fragments.py +47 -0
  38. primitive-0.1.60/src/primitive/jobs/graphql/mutations.py +11 -0
  39. primitive-0.1.60/src/primitive/jobs/graphql/queries.py +100 -0
  40. primitive-0.1.60/src/primitive/lint/__init__.py +0 -0
  41. primitive-0.1.60/src/primitive/organizations/__init__.py +0 -0
  42. primitive-0.1.60/src/primitive/organizations/actions.py +41 -0
  43. primitive-0.1.60/src/primitive/organizations/graphql/__init__.py +0 -0
  44. primitive-0.1.60/src/primitive/organizations/graphql/fragments.py +10 -0
  45. primitive-0.1.60/src/primitive/organizations/graphql/mutations.py +0 -0
  46. primitive-0.1.60/src/primitive/organizations/graphql/queries.py +38 -0
  47. primitive-0.1.60/src/primitive/projects/__init__.py +0 -0
  48. primitive-0.1.60/src/primitive/projects/actions.py +38 -0
  49. primitive-0.1.60/src/primitive/projects/graphql/__init__.py +0 -0
  50. primitive-0.1.60/src/primitive/projects/graphql/fragments.py +10 -0
  51. primitive-0.1.60/src/primitive/projects/graphql/mutations.py +0 -0
  52. primitive-0.1.60/src/primitive/projects/graphql/queries.py +36 -0
  53. primitive-0.1.60/src/primitive/reservations/__init__.py +0 -0
  54. primitive-0.1.60/src/primitive/reservations/actions.py +134 -0
  55. primitive-0.1.60/src/primitive/reservations/commands.py +67 -0
  56. primitive-0.1.60/src/primitive/reservations/graphql/__init__.py +0 -0
  57. primitive-0.1.60/src/primitive/reservations/graphql/fragments.py +40 -0
  58. primitive-0.1.60/src/primitive/reservations/graphql/mutations.py +29 -0
  59. primitive-0.1.60/src/primitive/reservations/graphql/queries.py +47 -0
  60. primitive-0.1.60/src/primitive/sim/__init__.py +0 -0
  61. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/sim/actions.py +12 -9
  62. primitive-0.1.60/src/primitive/utils/__init__.py +0 -0
  63. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/cache.py +14 -0
  64. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/shell.py +25 -0
  65. primitive-0.1.58/.vscode/settings.json +0 -11
  66. primitive-0.1.58/src/primitive/agent/uploader.py +0 -77
  67. primitive-0.1.58/src/primitive/jobs/actions.py +0 -331
  68. primitive-0.1.58/src/primitive/organizations/actions.py +0 -86
  69. primitive-0.1.58/src/primitive/projects/actions.py +0 -80
  70. {primitive-0.1.58 → primitive-0.1.60}/.git-hooks/pre-commit +0 -0
  71. {primitive-0.1.58 → primitive-0.1.60}/.gitattributes +0 -0
  72. {primitive-0.1.58 → primitive-0.1.60}/.github/workflows/lint.yml +0 -0
  73. {primitive-0.1.58 → primitive-0.1.60}/.github/workflows/publish.yml +0 -0
  74. {primitive-0.1.58 → primitive-0.1.60}/.gitignore +0 -0
  75. {primitive-0.1.58 → primitive-0.1.60}/LICENSE.txt +0 -0
  76. {primitive-0.1.58 → primitive-0.1.60}/Makefile +0 -0
  77. {primitive-0.1.58 → primitive-0.1.60}/README.md +0 -0
  78. {primitive-0.1.58 → primitive-0.1.60}/linux setup.md +0 -0
  79. {primitive-0.1.58 → primitive-0.1.60}/pyproject.toml +0 -0
  80. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/__init__.py +0 -0
  81. {primitive-0.1.58/src/primitive/auth → primitive-0.1.60/src/primitive/agent}/__init__.py +0 -0
  82. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/agent/commands.py +0 -0
  83. {primitive-0.1.58/src/primitive/git → primitive-0.1.60/src/primitive/auth}/__init__.py +0 -0
  84. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/auth/commands.py +0 -0
  85. {primitive-0.1.58/src/primitive → primitive-0.1.60/src/primitive/auth}/graphql/__init__.py +0 -0
  86. {primitive-0.1.58/src/primitive/projects → primitive-0.1.60/src/primitive/daemons}/__init__.py +0 -0
  87. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/daemons/actions.py +0 -0
  88. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/daemons/commands.py +0 -0
  89. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/daemons/launch_agents.py +0 -0
  90. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/daemons/launch_service.py +0 -0
  91. {primitive-0.1.58/src/primitive/sim → primitive-0.1.60/src/primitive/exec}/__init__.py +0 -0
  92. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/files/commands.py +0 -0
  93. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/graphql/sdk.py +0 -0
  94. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/jobs/commands.py +0 -0
  95. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/lint/actions.py +0 -0
  96. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/lint/commands.py +0 -0
  97. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/organizations/commands.py +0 -0
  98. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/projects/commands.py +0 -0
  99. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/sim/commands.py +0 -0
  100. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/actions.py +0 -0
  101. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/auth.py +0 -0
  102. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/config.py +0 -0
  103. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/files.py +0 -0
  104. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/git.py +0 -0
  105. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/memory_size.py +0 -0
  106. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/printer.py +0 -0
  107. {primitive-0.1.58 → primitive-0.1.60}/src/primitive/utils/verible.py +0 -0
  108. {primitive-0.1.58 → primitive-0.1.60}/tests/__init__.py +0 -0
  109. {primitive-0.1.58 → primitive-0.1.60}/uv.lock +0 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Checkin",
4
+ "loguru",
5
+ "machdep",
6
+ "testsuites",
7
+ "untar",
8
+ "Untaring",
9
+ "VERIBLE",
10
+ "verilog",
11
+ "workdir"
12
+ ],
13
+ "[python]": {
14
+ "editor.defaultFormatter": "charliermarsh.ruff",
15
+ "editor.formatOnSave": true,
16
+ "editor.codeActionsOnSave": {
17
+ "source.fixAll": "explicit"
18
+ }
19
+ },
20
+ "ruff.fixAll": true,
21
+ "ruff.organizeImports": true
22
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: primitive
3
- Version: 0.1.58
3
+ Version: 0.1.60
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
@@ -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.58"
4
+ __version__ = "0.1.60"
@@ -1,9 +1,12 @@
1
- import sys
2
1
  import shutil
2
+ import sys
3
3
  from time import sleep
4
- from primitive.utils.actions import BaseAction
4
+
5
5
  from loguru import logger
6
+
6
7
  from primitive.__about__ import __version__
8
+ from primitive.utils.actions import BaseAction
9
+
7
10
  from ..utils.cache import get_sources_cache
8
11
  from .runner import AgentRunner
9
12
  from .uploader import Uploader
@@ -73,12 +76,12 @@ class Agent(BaseAction):
73
76
  sleep(sleep_amount)
74
77
  continue
75
78
 
76
- job_runs_data = self.primitive.jobs.get_job_runs(
79
+ job_runs_result = self.primitive.jobs.get_job_runs(
77
80
  status="pending", first=1, reservation_id=active_reservation_id
78
81
  )
79
82
 
80
83
  pending_job_runs = [
81
- edge["node"] for edge in job_runs_data["jobRuns"]["edges"]
84
+ edge["node"] for edge in job_runs_result.data["jobRuns"]["edges"]
82
85
  ]
83
86
 
84
87
  if not pending_job_runs:
@@ -126,6 +129,7 @@ class Agent(BaseAction):
126
129
  source_dir=source_dir,
127
130
  job_id=job_run["id"],
128
131
  job_slug=job_run["job"]["slug"],
132
+ max_log_size=500 * 1024,
129
133
  )
130
134
  except Exception as e:
131
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(
@@ -1,14 +1,15 @@
1
- import yaml
2
- import typing
3
1
  import os
4
2
  import threading
5
- from time import sleep
6
- from typing import TypedDict, Iterable, List, Optional, Dict
3
+ import typing
7
4
  from pathlib import Path, PurePath
5
+ from time import sleep
6
+ from typing import Dict, Iterable, List, Optional, TypedDict
7
+
8
+ import yaml
8
9
  from loguru import logger
9
10
  from .process import Process
10
11
  from .provision import ProvisionPython
11
- from ..utils.cache import get_artifacts_cache
12
+ from ..utils.cache import get_artifacts_cache, get_logs_cache
12
13
  from ..utils.files import find_files_for_extension
13
14
 
14
15
  try:
@@ -54,12 +55,11 @@ class AgentRunner:
54
55
  self.job_slug = job_slug
55
56
  self.max_log_size = max_log_size
56
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
57
60
 
58
61
  logger.enable("primitive")
59
- self.logger_handle = logger.add(
60
- Path(self.artifacts_dir / "runner_{time}.log"),
61
- rotation=self.max_log_size, # Rotate when the log file reaches 10MB
62
- )
62
+ self.swap_logs(label="init")
63
63
 
64
64
  logger.info(f"Scanning directory for job {self.job_slug}")
65
65
 
@@ -111,6 +111,9 @@ class AgentRunner:
111
111
  conclusion = None
112
112
  total_errors = 0
113
113
  for step in self.steps():
114
+ # Swap logger
115
+ self.swap_logs(label=step["name"])
116
+
114
117
  logger.info(f"Beginning step {step['name']}")
115
118
 
116
119
  # Update workdir
@@ -136,7 +139,7 @@ class AgentRunner:
136
139
  while proc.is_running():
137
140
  # Check job status
138
141
  status = self.primitive.jobs.get_job_status(self.job_id)
139
- status_value = status["jobRun"]["status"]
142
+ status_value = status.data["jobRun"]["status"]
140
143
 
141
144
  # TODO: Should probably use request_cancelled or something
142
145
  # once we change it, we'll have to call conclude w/ cancelled status
@@ -182,6 +185,15 @@ class AgentRunner:
182
185
  logger.info(f"Completed {self.job_slug} job")
183
186
  logger.remove(self.logger_handle)
184
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"), rotation=self.max_log_size
195
+ )
196
+
185
197
  def provision(self) -> Optional[Dict]:
186
198
  match self.job["provision"]:
187
199
  case "python":
@@ -197,8 +209,6 @@ class AgentRunner:
197
209
  return prov.create_env()
198
210
 
199
211
  def collect_artifacts(self, step: JobStep) -> None:
200
- # str(PurePath(file_path).relative_to(Path(source))
201
-
202
212
  # Search each artifact type
203
213
  for artifact in step["artifacts"]:
204
214
  files = find_files_for_extension(self.source_dir, artifact["extension"])
@@ -0,0 +1,106 @@
1
+ import typing
2
+ from typing import Dict
3
+ import shutil
4
+ import os
5
+ from loguru import logger
6
+ from pathlib import Path, PurePath
7
+ from ..utils.cache import get_artifacts_cache, get_logs_cache
8
+
9
+ if typing.TYPE_CHECKING:
10
+ import primitive.client
11
+
12
+
13
+ class Uploader:
14
+ def __init__(
15
+ self,
16
+ primitive: "primitive.client.Primitive",
17
+ ):
18
+ self.primitive = primitive
19
+
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
+ def upload_dir(self, cache: Path) -> Dict:
27
+ file_ids = []
28
+ job_run_id = cache.name
29
+
30
+ files = None
31
+ has_walk = getattr(cache, "walk", None)
32
+ if has_walk:
33
+ files = sorted(
34
+ [
35
+ current_path / file
36
+ for current_path, _, current_path_files in cache.walk()
37
+ for file in current_path_files
38
+ ],
39
+ key=lambda p: p.stat().st_size,
40
+ )
41
+ else:
42
+ files = sorted(
43
+ [
44
+ Path(Path(current_path) / file)
45
+ for current_path, _, current_path_files in os.walk(cache)
46
+ for file in current_path_files
47
+ ],
48
+ key=lambda p: p.stat().st_size,
49
+ )
50
+
51
+ for file in files:
52
+ upload_id = self.upload_file(
53
+ file,
54
+ prefix=str(PurePath(file).relative_to(cache.parent).parent),
55
+ job_run_id=job_run_id,
56
+ )
57
+
58
+ if upload_id:
59
+ file_ids.append(upload_id)
60
+ continue
61
+
62
+ logger.error(f"Unable to upload file {file}")
63
+
64
+ # Clean up job cache
65
+ shutil.rmtree(path=cache)
66
+
67
+ return {job_run_id: file_ids}
68
+
69
+
70
+ def scan(self) -> None:
71
+ # Scan artifacts directory
72
+ artifacts_dir = get_artifacts_cache()
73
+ logs_dir = get_logs_cache()
74
+
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)
80
+
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)
86
+
87
+ log_files = {
88
+ job_id: files
89
+ for log_path in logs
90
+ for job_id, files in self.upload_dir(log_path).items()
91
+ }
92
+
93
+ artifact_files = {
94
+ job_id: files
95
+ for artifact_path in artifacts
96
+ for job_id, files in self.upload_dir(artifact_path).items()
97
+ }
98
+
99
+ files_by_id = {
100
+ job_id: log_files.get(job_id, []) + artifact_files.get(job_id, [])
101
+ for job_id in log_files.keys() | artifact_files.keys()
102
+ }
103
+
104
+ # Update job run
105
+ for job_id, files in files_by_id.items():
106
+ self.primitive.jobs.job_run_update(id=job_id, file_ids=files)
@@ -1,23 +1,16 @@
1
1
  from gql import gql
2
2
 
3
- from ..utils.config import read_config_file, update_config_file
4
- from ..utils.auth import guard
5
-
6
3
  from primitive.utils.actions import BaseAction
7
4
 
5
+ from ..utils.auth import guard
6
+ from ..utils.config import read_config_file, update_config_file
7
+ from .graphql.queries import whoami_query
8
+
8
9
 
9
10
  class Auth(BaseAction):
10
11
  @guard
11
12
  def whoami(self):
12
- query = gql(
13
- """
14
- query whoami {
15
- whoami {
16
- username
17
- }
18
- }
19
- """
20
- )
13
+ query = gql(whoami_query)
21
14
 
22
15
  result = self.primitive.session.execute(query, get_execution_result=True)
23
16
 
@@ -0,0 +1,13 @@
1
+ whoami_query = """
2
+ query whoami {
3
+ whoami {
4
+ username
5
+ defaultOrganization {
6
+ id
7
+ pk
8
+ name
9
+ slug
10
+ }
11
+ }
12
+ }
13
+ """
@@ -1,18 +1,22 @@
1
1
  import os
2
2
  import sys
3
+
3
4
  import click
5
+
4
6
  from .__about__ import __version__
5
- from .client import Primitive
7
+ from .agent.commands import cli as agent_commands
6
8
  from .auth.commands import config_command, whoami_command
9
+ from .client import Primitive
10
+ from .daemons.commands import cli as daemons_commands
11
+ from .exec.commands import cli as exec_commands
7
12
  from .files.commands import cli as file_commands
8
- from .hardware.commands import cli as hardware_commands
9
- from .lint.commands import cli as lint_commands
10
- from .agent.commands import cli as agent_commands
11
13
  from .git.commands import cli as git_commands
12
- from .daemons.commands import cli as daemons_commands
14
+ from .hardware.commands import cli as hardware_commands
13
15
  from .jobs.commands import cli as jobs_commands
16
+ from .lint.commands import cli as lint_commands
14
17
  from .organizations.commands import cli as organizations_commands
15
18
  from .projects.commands import cli as projects_commands
19
+ from .reservations.commands import cli as reservations_commands
16
20
  from .sim.commands import cli as sim_commands
17
21
 
18
22
 
@@ -69,6 +73,8 @@ cli.add_command(jobs_commands, "jobs")
69
73
  cli.add_command(organizations_commands, "organizations")
70
74
  cli.add_command(projects_commands, "projects")
71
75
  cli.add_command(sim_commands, "sim")
76
+ cli.add_command(reservations_commands, "reservations")
77
+ cli.add_command(exec_commands, "exec")
72
78
 
73
79
  if __name__ == "__main__":
74
80
  cli(obj={})
@@ -11,6 +11,8 @@ from .git.actions import Git
11
11
  from .daemons.actions import Daemons
12
12
  from .jobs.actions import Jobs
13
13
  from .organizations.actions import Organizations
14
+ from .exec.actions import Exec
15
+ from .reservations.actions import Reservations
14
16
 
15
17
  from loguru import logger
16
18
 
@@ -58,11 +60,13 @@ class Primitive:
58
60
  self.jobs: Jobs = Jobs(self)
59
61
  self.files: Files = Files(self)
60
62
  self.sim: Sim = Sim(self)
63
+ self.reservations: Reservations = Reservations(self)
61
64
  self.hardware: Hardware = Hardware(self)
62
65
  self.lint: Lint = Lint(self)
63
66
  self.agent: Agent = Agent(self)
64
67
  self.git: Git = Git(self)
65
68
  self.daemons: Daemons = Daemons(self)
69
+ self.exec: Exec = Exec(self)
66
70
 
67
71
  def get_host_config(self):
68
72
  self.full_config = read_config_file()
@@ -0,0 +1,50 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ pass
5
+
6
+
7
+ from primitive.utils.actions import BaseAction
8
+
9
+
10
+ class Exec(BaseAction):
11
+ def __init__(self, *args, **kwargs) -> None:
12
+ super().__init__(*args, **kwargs)
13
+
14
+ def execute_command(self, hardware_identifier: str, command: str) -> None:
15
+ hardware = self.primitive.hardware.get_hardware_from_slug_or_id(
16
+ hardware_identifier=hardware_identifier
17
+ )
18
+
19
+ # since we found hardware, we need to check that the user:
20
+ # - has a valid reservation on it
21
+ # - OR if the device is free we can reserve it
22
+
23
+ # if we create a reservation on behalf of the user, we need to release it after
24
+ created_reservation_on_behalf_of_user = False
25
+
26
+ if active_reservation := hardware["activeReservation"]:
27
+ active_reservation_id = active_reservation["id"]
28
+ reservation_result = self.primitive.reservations.get_reservation(
29
+ reservation_id=active_reservation_id
30
+ )
31
+ reservation = reservation_result.data["reservation"]
32
+ else:
33
+ reservation_result = self.primitive.reservations.create_reservation(
34
+ requested_hardware_ids=[hardware["id"]],
35
+ reason="Executing command from Primitive CLI",
36
+ )
37
+ reservation = reservation_result.data["reservationCreate"]
38
+ created_reservation_on_behalf_of_user = True
39
+
40
+ reservation = self.primitive.reservations.wait_for_reservation_status(
41
+ reservation_id=reservation["id"], desired_status="in_progress"
42
+ )
43
+
44
+ print(f"Executing command: {command} on {hardware['name']}")
45
+
46
+ if created_reservation_on_behalf_of_user:
47
+ print("Cleaning up reservation.")
48
+ self.primitive.reservations.release_reservation(
49
+ reservation_or_hardware_identifier=reservation["id"]
50
+ )
@@ -0,0 +1,22 @@
1
+ import typing
2
+
3
+ import click
4
+
5
+ if typing.TYPE_CHECKING:
6
+ from ..client import Primitive
7
+
8
+
9
+ @click.command("exec")
10
+ @click.pass_context
11
+ @click.argument(
12
+ "hardware_identifier",
13
+ type=str,
14
+ required=True,
15
+ )
16
+ @click.argument("command", nargs=-1, required=True)
17
+ def cli(context, hardware_identifier: str, command: str) -> None:
18
+ """Exec"""
19
+ primitive: Primitive = context.obj.get("PRIMITIVE")
20
+ primitive.exec.execute_command(
21
+ hardware_identifier=hardware_identifier, command=command
22
+ )
File without changes
@@ -1,10 +1,13 @@
1
1
  from pathlib import Path
2
+
2
3
  from gql import gql
3
- from primitive.graphql.sdk import create_requests_session
4
- from ..utils.auth import guard
5
4
 
5
+ from primitive.graphql.sdk import create_requests_session
6
6
  from primitive.utils.actions import BaseAction
7
7
 
8
+ from ..utils.auth import guard
9
+ from .graphql.mutations import create_trace_mutation
10
+
8
11
 
9
12
  class Files(BaseAction):
10
13
  @guard
@@ -17,19 +20,7 @@ class Files(BaseAction):
17
20
  is_vector: bool,
18
21
  size: int,
19
22
  ):
20
- mutation = gql(
21
- """
22
- mutation createTrace($input: TraceCreateInput!) {
23
- traceCreate(input: $input) {
24
- ... on Trace {
25
- id
26
- signalId
27
- signalName
28
- }
29
- }
30
- }
31
- """
32
- )
23
+ mutation = gql(create_trace_mutation)
33
24
  input = {
34
25
  "fileId": file_id,
35
26
  "signalId": signal_id,
@@ -39,7 +30,9 @@ class Files(BaseAction):
39
30
  "size": size,
40
31
  }
41
32
  variables = {"input": input}
42
- result = self.primitive.session.execute(mutation, variable_values=variables)
33
+ result = self.primitive.session.execute(
34
+ mutation, variable_values=variables, get_execution_result=True
35
+ )
43
36
  return result
44
37
 
45
38
  @guard
@@ -0,0 +1,11 @@
1
+ create_trace_mutation = """
2
+ mutation createTrace($input: TraceCreateInput!) {
3
+ traceCreate(input: $input) {
4
+ ... on Trace {
5
+ id
6
+ signalId
7
+ signalName
8
+ }
9
+ }
10
+ }
11
+ """
File without changes
@@ -1,26 +1,26 @@
1
+ import os
1
2
  from pathlib import Path
3
+
4
+ from gql import gql
5
+ from loguru import logger
6
+
2
7
  from primitive.utils.actions import BaseAction
8
+
3
9
  from ..utils.auth import guard
4
- from loguru import logger
5
- import os
10
+ from .graphql.queries import github_app_token_query
6
11
 
7
12
 
8
13
  class Git(BaseAction):
9
14
  @guard
10
15
  def get_github_access_token(self) -> str:
11
- query = """
12
- query githubAppToken{
13
- githubAppToken {
14
- token
15
- }
16
- }
17
- """
18
-
16
+ query = gql(github_app_token_query)
19
17
  filters = {}
20
18
  variables = {
21
19
  "filters": filters,
22
20
  }
23
- result = self.primitive.session.execute(query, variable_values=variables)
21
+ result = self.primitive.session.execute(
22
+ query, variable_values=variables, get_execution_result=True
23
+ )
24
24
  return result
25
25
 
26
26
  def download_git_repository_at_ref(
@@ -1,8 +1,10 @@
1
- import click
2
- from pathlib import Path
1
+ import os
3
2
  import typing
3
+ from pathlib import Path
4
+
5
+ import click
6
+
4
7
  from ..utils.printer import print_result
5
- import os
6
8
 
7
9
  if typing.TYPE_CHECKING:
8
10
  from ..client import Primitive
@@ -39,11 +41,10 @@ def download_ref_command(
39
41
  destination: Path = Path.cwd(),
40
42
  ):
41
43
  primitive: Primitive = context.obj.get("PRIMITIVE")
42
- result, message = primitive.git.download_git_repository_at_ref(
44
+ path = primitive.git.download_git_repository_at_ref(
43
45
  git_repo_full_name=git_repo_full_name,
44
46
  git_ref=git_ref,
45
47
  github_access_token=github_access_token,
46
48
  destination=destination,
47
49
  )
48
- fg = "red" if not result else "green"
49
- print_result(message, context=context, fg=fg)
50
+ print_result(message=path, context=context)
File without changes
@@ -0,0 +1,7 @@
1
+ github_app_token_query = """
2
+ query githubAppToken{
3
+ githubAppToken {
4
+ token
5
+ }
6
+ }
7
+ """
File without changes
@@ -0,0 +1,32 @@
1
+ import base64
2
+ from typing import Tuple
3
+
4
+
5
+ def from_base64(value: str) -> Tuple[str, str]:
6
+ """
7
+ FROM:
8
+ https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/relay/utils.py#L16C1-L40C1
9
+
10
+ Parse the base64 encoded relay value.
11
+
12
+ Args:
13
+ value:
14
+ The value to be parsed
15
+
16
+ Returns:
17
+ A tuple of (TypeName, NodeID).
18
+
19
+ Raises:
20
+ ValueError:
21
+ If the value is not in the expected format
22
+
23
+ """
24
+ try:
25
+ res = base64.b64decode(value.encode()).decode().split(":", 1)
26
+ except Exception as e:
27
+ raise ValueError(str(e)) from e
28
+
29
+ if len(res) != 2:
30
+ raise ValueError(f"{res} expected to contain only 2 items")
31
+
32
+ return res[0], res[1]