primitive 0.1.68__py3-none-any.whl → 0.1.70__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.68"
4
+ __version__ = "0.1.70"
@@ -41,7 +41,14 @@ class Process:
41
41
  continue
42
42
 
43
43
  if key.fileobj is self.process.stdout:
44
- logger.info(data.rstrip())
44
+ raw_data = data.rstrip()
45
+ if "error" in raw_data.lower():
46
+ logger.error(raw_data)
47
+ self._errors += 1
48
+ elif "warning" in raw_data.lower():
49
+ logger.warning(raw_data)
50
+ else:
51
+ logger.info(raw_data)
45
52
  elif key.fileobj is self.process.stderr:
46
53
  logger.error(data.rstrip())
47
54
  self._errors += 1
primitive/agent/runner.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import os
2
2
  import threading
3
3
  import typing
4
+ import json
4
5
  from pathlib import Path, PurePath
5
6
  from time import sleep
6
- from typing import Dict, Iterable, List, Optional, TypedDict
7
+ from typing import Dict, Iterable, List, Optional, TypedDict, Callable
7
8
 
8
9
  import yaml
9
10
  from loguru import logger
@@ -47,6 +48,7 @@ class AgentRunner:
47
48
  job_id: str,
48
49
  job_slug: str,
49
50
  max_log_size: int = 10 * 1024 * 1024,
51
+ log_to_file: bool = True,
50
52
  job: Optional[JobDescription] = None,
51
53
  ) -> None:
52
54
  self.primitive = primitive
@@ -55,44 +57,72 @@ class AgentRunner:
55
57
  self.job_id = job_id
56
58
  self.job_slug = job_slug
57
59
  self.max_log_size = max_log_size
58
- self.artifacts_dir = get_artifacts_cache(self.job_id)
59
- self.logs_dir = get_logs_cache(self.job_id)
60
- self.logger_handle = None
60
+ self.log_to_file = log_to_file
61
61
 
62
+ # Enable and configure logger
62
63
  logger.enable("primitive")
63
- self.swap_logs(label="init")
64
64
 
65
- if job:
66
- self.job = job
67
- else:
68
- self.load_job_from_file()
69
-
70
- def load_job_from_file(self) -> JobDescription:
71
- logger.info(f"Scanning directory for job file {self.job_slug}")
72
-
73
- # Look for job based on slug
74
- yaml_file = Path(self.source_dir / ".primitive" / f"{self.job_slug}.yaml")
75
- yml_file = Path(self.source_dir / ".primitive" / f"{self.job_slug}.yml")
76
-
77
- if yaml_file.exists() and yml_file.exists():
78
- logger.error(
79
- f"Found two job descriptions with the same slug: {self.job_slug}"
65
+ if self.log_to_file:
66
+ log_name = f"{self.job_slug}_{self.job_id}_{{time}}.primitive.log"
67
+ logger.add(
68
+ Path(get_logs_cache(self.job_id) / log_name),
69
+ rotation=self.max_log_size,
70
+ format=AgentRunner.log_serializer(),
80
71
  )
81
- self.conclude(conclusion="failure")
82
- raise FileExistsError
83
72
 
84
- if yaml_file.exists():
85
- self.job = yaml.load(open(yaml_file, "r"), Loader=Loader)
86
- elif yml_file.exists():
87
- self.job = yaml.load(open(yml_file, "r"), Loader=Loader)
73
+ # Attempt to load job from file
74
+ if not job:
75
+ logger.info(f"Scanning directory for job file {self.job_slug}")
76
+ yaml_file = Path(self.source_dir / ".primitive" / f"{self.job_slug}.yaml")
77
+ yml_file = Path(self.source_dir / ".primitive" / f"{self.job_slug}.yml")
78
+
79
+ if yaml_file.exists() and yml_file.exists():
80
+ logger.error(
81
+ f"Found two job descriptions with the same slug: {self.job_slug}"
82
+ )
83
+ self.primitive.jobs.job_run_update(
84
+ self.job_id, status="request_completed", conclusion="failure"
85
+ )
86
+ raise FileExistsError
87
+
88
+ if yaml_file.exists():
89
+ self.job = yaml.load(open(yaml_file, "r"), Loader=Loader)
90
+ elif yml_file.exists():
91
+ self.job = yaml.load(open(yml_file, "r"), Loader=Loader)
92
+ else:
93
+ logger.error(
94
+ f"No job description with matching slug '{self.job_slug}' found"
95
+ )
96
+ self.primitive.jobs.job_run_update(
97
+ self.job_id, status="request_completed", conclusion="failure"
98
+ )
99
+ raise FileNotFoundError
100
+
101
+ logger.info(f"Found job description for {self.job_slug}")
88
102
  else:
89
- logger.error(
90
- f"No job description with matching slug '{self.job_slug}' found"
91
- )
92
- self.conclude(conclusion="failure")
93
- raise FileNotFoundError
103
+ self.job = job
104
+
105
+ @staticmethod
106
+ def log_serializer() -> Callable:
107
+ def fmt(record):
108
+ step = ""
109
+ if "step" in record["extra"]:
110
+ step = record["extra"]["step"]
111
+
112
+ log = {
113
+ "time": record["time"].strftime("%Y-%m-%d %H:%M:%S.%f"),
114
+ "utc": record["time"].strftime("%Y-%m-%d %H:%M:%S.%f%z"),
115
+ "level": record["level"].name,
116
+ "message": record["message"],
117
+ "name": record["name"],
118
+ "step": step,
119
+ }
120
+
121
+ record["extra"]["serialized"] = json.dumps(log)
94
122
 
95
- logger.info(f"Found job description for {self.job_slug}")
123
+ return "{extra[serialized]}\n"
124
+
125
+ return fmt
96
126
 
97
127
  def name(self) -> str:
98
128
  return self.job["name"]
@@ -105,112 +135,85 @@ class AgentRunner:
105
135
  logger.info(f"Executing {self.job_slug} job")
106
136
  self.primitive.jobs.job_run_update(self.job_id, status="request_in_progress")
107
137
 
108
- # Initial environment is the system env
138
+ # Initialize the environment with the system
109
139
  environment = os.environ
110
140
  if "provision" in self.job:
111
141
  logger.info(f"Provisioning for {self.job['provision']} environment")
112
142
  environment = self.provision()
113
143
 
114
144
  if not environment:
115
- self.conclude("failure")
145
+ self.primitive.jobs.job_run_update(
146
+ self.job_id, status="request_completed", conclusion="failure"
147
+ )
148
+ logger.error(f"{self.job_slug} concluded with error(s)")
116
149
  return
117
150
 
118
- conclusion = None
119
- total_errors = 0
151
+ total_job_errors = 0
120
152
  for step in self.steps():
121
- # Swap logger
122
- self.swap_logs(label=step["name"])
123
-
124
153
  logger.info(f"Beginning step {step['name']}")
125
154
 
126
- # Update workdir
127
- if "workdir" in step:
128
- self.workdir = step["workdir"]
129
-
130
- # Define step proc
131
- proc = Process(
132
- cmd=step["cmd"],
133
- workdir=Path(self.source_dir / self.workdir),
134
- env=environment,
135
- )
136
-
137
- # Try to start
138
- try:
139
- proc.start()
140
- except Exception as e:
141
- logger.error(f"Error while attempting to run command {e}")
142
- conclusion = "failure"
143
- break
144
-
145
- def status_check():
146
- while proc.is_running():
147
- # Check job status
148
- status = self.primitive.jobs.get_job_status(self.job_id)
149
- status_value = status.data["jobRun"]["status"]
150
-
151
- # TODO: Should probably use request_cancelled or something
152
- # once we change it, we'll have to call conclude w/ cancelled status
153
- if status_value == "completed":
154
- logger.warning("Job cancelled by user")
155
- proc.terminate()
156
- return
155
+ with logger.contextualize(step=step["name"]):
156
+ if "workdir" in step:
157
+ self.workdir = step["workdir"]
158
+
159
+ proc = Process(
160
+ cmd=step["cmd"],
161
+ workdir=Path(self.source_dir / self.workdir),
162
+ env=environment,
163
+ )
164
+
165
+ try:
166
+ proc.start()
167
+ except Exception as e:
168
+ logger.error(f"Error while attempting to run process {e}")
169
+ self.primitive.jobs.job_run_update(
170
+ self.job_id, status="request_completed", conclusion="failure"
171
+ )
172
+ logger.error(f"{self.job_slug} concluded with error(s)")
173
+ return
174
+
175
+ def status_check():
176
+ while proc.is_running():
177
+ # Check job status
178
+ status = self.primitive.jobs.get_job_status(self.job_id)
179
+ status_value = status.data["jobRun"]["status"]
180
+
181
+ # TODO: Should probably use request_cancelled or something
182
+ # once we change it, we'll have to call conclude w/ cancelled status
183
+ if status_value == "completed":
184
+ logger.warning("Job cancelled by user")
185
+ proc.terminate()
186
+ return
187
+
188
+ sleep(5)
189
+
190
+ status_thread = threading.Thread(target=status_check)
191
+ status_thread.start()
192
+
193
+ returncode = proc.wait()
194
+ total_job_errors += proc.errors
195
+ status_thread.join()
196
+
197
+ self.collect_artifacts(step)
157
198
 
158
- sleep(5)
159
-
160
- status_thread = threading.Thread(target=status_check)
161
- status_thread.start()
162
-
163
- # Wait for proc to finish
164
- returncode = proc.wait()
165
- total_errors += proc.errors
166
-
167
- # Wait for status check
168
- status_thread.join()
169
-
170
- # Collect artifacts
171
- if "artifacts" in step:
172
- self.collect_artifacts(step)
173
-
174
- # Check if we have a good result
175
199
  if returncode > 0:
176
- conclusion = "failure"
177
- break
178
-
179
- if not conclusion and total_errors == 0:
180
- conclusion = "success"
181
- else:
182
- logger.error(f"Job failed with {total_errors} errors.")
183
- conclusion = "failure"
200
+ self.primitive.jobs.job_run_update(
201
+ self.job_id, status="request_completed", conclusion="failure"
202
+ )
203
+ logger.error(f"{self.job_slug} concluded with error(s)")
204
+ return
184
205
 
185
- self.conclude(conclusion)
206
+ if total_job_errors > 0:
207
+ self.primitive.jobs.job_run_update(
208
+ self.job_id, status="request_completed", conclusion="failure"
209
+ )
210
+ logger.error(f"{self.job_slug} concluded with error(s)")
211
+ return
186
212
 
187
- def conclude(self, conclusion: str) -> None:
188
213
  self.primitive.jobs.job_run_update(
189
- self.job_id, status="request_completed", conclusion=conclusion
190
- )
191
-
192
- logger.info(f"Completed {self.job_slug} job")
193
- logger.remove(self.logger_handle)
194
-
195
- def swap_logs(self, label: str):
196
- # Remove Handle
197
- if self.logger_handle:
198
- logger.remove(self.logger_handle)
199
-
200
- # Custom format for UTC time
201
- logger_format = (
202
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}</green> | "
203
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
204
- "<level>{level: <8}</level> | "
205
- "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
206
- "<level>{message}</level>"
207
- )
208
-
209
- self.logger_handle = logger.add(
210
- Path(self.logs_dir / f"{label}_{{time}}.primitive.log"),
211
- rotation=self.max_log_size,
212
- format=logger_format
214
+ self.job_id, status="request_completed", conclusion="success"
213
215
  )
216
+ logger.success(f"Completed {self.job_slug} job")
214
217
 
215
218
  def provision(self) -> Optional[Dict]:
216
219
  match self.job["provision"]:
@@ -227,7 +230,9 @@ class AgentRunner:
227
230
  return prov.create_env()
228
231
 
229
232
  def collect_artifacts(self, step: JobStep) -> None:
230
- # Search each artifact type
233
+ if "artifacts" not in step:
234
+ return
235
+
231
236
  for artifact in step["artifacts"]:
232
237
  files = find_files_for_extension(self.source_dir, artifact["extension"])
233
238
 
@@ -236,7 +241,7 @@ class AgentRunner:
236
241
  relative_path = PurePath(file).relative_to(self.source_dir)
237
242
 
238
243
  # Construct destination to preserve directory structure
239
- destination = Path(self.artifacts_dir / relative_path)
244
+ destination = Path(get_artifacts_cache(self.job_id) / relative_path)
240
245
 
241
246
  # Create directories if they don't exist
242
247
  destination.parent.mkdir(parents=True, exist_ok=True)
@@ -1,8 +1,8 @@
1
- import typing
2
- from typing import Dict
3
- import shutil
4
1
  import os
2
+ import shutil
3
+ import typing
5
4
  from pathlib import Path, PurePath
5
+ from typing import Dict
6
6
 
7
7
  from loguru import logger
8
8
 
@@ -45,12 +45,11 @@ class Uploader:
45
45
  )
46
46
 
47
47
  for file in files:
48
- response = self.primitive.files.upload_file_via_api(
48
+ result = self.primitive.files.upload_file_direct(
49
49
  path=file,
50
- key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
51
- job_run_id=job_run_id,
50
+ key_prefix=str(PurePath(file).relative_to(cache.parent).parent)
52
51
  )
53
- upload_id = response.json()["data"]["fileUpload"]["id"]
52
+ upload_id = result.data["fileUpdate"]["id"]
54
53
 
55
54
  if upload_id:
56
55
  file_ids.append(upload_id)
primitive/cli.py CHANGED
@@ -13,11 +13,9 @@ from .files.commands import cli as file_commands
13
13
  from .git.commands import cli as git_commands
14
14
  from .hardware.commands import cli as hardware_commands
15
15
  from .jobs.commands import cli as jobs_commands
16
- from .lint.commands import cli as lint_commands
17
16
  from .organizations.commands import cli as organizations_commands
18
17
  from .projects.commands import cli as projects_commands
19
18
  from .reservations.commands import cli as reservations_commands
20
- from .sim.commands import cli as sim_commands
21
19
 
22
20
 
23
21
  @click.group()
@@ -65,14 +63,12 @@ cli.add_command(config_command, "config")
65
63
  cli.add_command(whoami_command, "whoami")
66
64
  cli.add_command(file_commands, "files")
67
65
  cli.add_command(hardware_commands, "hardware")
68
- cli.add_command(lint_commands, "lint")
69
66
  cli.add_command(agent_commands, "agent")
70
67
  cli.add_command(git_commands, "git")
71
68
  cli.add_command(daemons_commands, "daemons")
72
69
  cli.add_command(jobs_commands, "jobs")
73
70
  cli.add_command(organizations_commands, "organizations")
74
71
  cli.add_command(projects_commands, "projects")
75
- cli.add_command(sim_commands, "sim")
76
72
  cli.add_command(reservations_commands, "reservations")
77
73
  cli.add_command(exec_commands, "exec")
78
74
 
primitive/client.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
 
3
+ from gql import Client
3
4
  from loguru import logger
4
5
 
5
6
  from .agent.actions import Agent
@@ -10,12 +11,10 @@ from .files.actions import Files
10
11
  from .git.actions import Git
11
12
  from .hardware.actions import Hardware
12
13
  from .jobs.actions import Jobs
13
- from .lint.actions import Lint
14
14
  from .organizations.actions import Organizations
15
15
  from .projects.actions import Projects
16
16
  from .provisioning.actions import Provisioning
17
17
  from .reservations.actions import Reservations
18
- from .sim.actions import Sim
19
18
  from .utils.config import read_config_file
20
19
 
21
20
  logger.disable("primitive")
@@ -30,10 +29,10 @@ class Primitive:
30
29
  token: str = None,
31
30
  transport: str = None,
32
31
  ) -> None:
33
- self.host = host
34
- self.session = None
35
- self.DEBUG = DEBUG
36
- self.JSON = JSON
32
+ self.host: str = host
33
+ self.session: Client = None
34
+ self.DEBUG: bool = DEBUG
35
+ self.JSON: bool = JSON
37
36
 
38
37
  if self.DEBUG:
39
38
  logger.enable("primitive")
@@ -61,10 +60,8 @@ class Primitive:
61
60
  self.projects: Projects = Projects(self)
62
61
  self.jobs: Jobs = Jobs(self)
63
62
  self.files: Files = Files(self)
64
- self.sim: Sim = Sim(self)
65
63
  self.reservations: Reservations = Reservations(self)
66
64
  self.hardware: Hardware = Hardware(self)
67
- self.lint: Lint = Lint(self)
68
65
  self.agent: Agent = Agent(self)
69
66
  self.git: Git = Git(self)
70
67
  self.daemons: Daemons = Daemons(self)