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 +1 -1
- primitive/agent/process.py +8 -1
- primitive/agent/runner.py +128 -123
- primitive/agent/uploader.py +6 -7
- primitive/cli.py +0 -4
- primitive/client.py +5 -8
- primitive/files/actions.py +192 -29
- primitive/files/commands.py +4 -1
- primitive/files/graphql/fragments.py +18 -0
- primitive/files/graphql/mutations.py +17 -1
- primitive/files/graphql/queries.py +31 -0
- primitive/utils/auth.py +25 -17
- primitive/utils/chunk_size.py +87 -0
- primitive/utils/files.py +10 -7
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/METADATA +2 -1
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/RECORD +19 -22
- primitive/lint/__init__.py +0 -0
- primitive/lint/actions.py +0 -76
- primitive/lint/commands.py +0 -17
- primitive/sim/__init__.py +0 -0
- primitive/sim/actions.py +0 -129
- primitive/sim/commands.py +0 -19
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/WHEEL +0 -0
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/entry_points.txt +0 -0
- {primitive-0.1.68.dist-info → primitive-0.1.70.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py
CHANGED
primitive/agent/process.py
CHANGED
@@ -41,7 +41,14 @@ class Process:
|
|
41
41
|
continue
|
42
42
|
|
43
43
|
if key.fileobj is self.process.stdout:
|
44
|
-
|
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.
|
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
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
self.
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
-
#
|
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.
|
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
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
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=
|
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
|
-
|
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.
|
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)
|
primitive/agent/uploader.py
CHANGED
@@ -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
|
-
|
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 =
|
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)
|