primitive 0.1.53__tar.gz → 0.1.55__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 (63) hide show
  1. {primitive-0.1.53 → primitive-0.1.55}/PKG-INFO +1 -1
  2. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/__about__.py +1 -1
  3. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/agent/actions.py +1 -1
  4. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/agent/process.py +27 -32
  5. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/agent/runner.py +21 -15
  6. primitive-0.1.55/src/primitive/agent/uploader.py +64 -0
  7. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/files/actions.py +11 -1
  8. primitive-0.1.53/src/primitive/agent/uploader.py +0 -46
  9. {primitive-0.1.53 → primitive-0.1.55}/.git-hooks/pre-commit +0 -0
  10. {primitive-0.1.53 → primitive-0.1.55}/.gitattributes +0 -0
  11. {primitive-0.1.53 → primitive-0.1.55}/.github/workflows/lint.yml +0 -0
  12. {primitive-0.1.53 → primitive-0.1.55}/.github/workflows/publish.yml +0 -0
  13. {primitive-0.1.53 → primitive-0.1.55}/.gitignore +0 -0
  14. {primitive-0.1.53 → primitive-0.1.55}/.vscode/settings.json +0 -0
  15. {primitive-0.1.53 → primitive-0.1.55}/LICENSE.txt +0 -0
  16. {primitive-0.1.53 → primitive-0.1.55}/Makefile +0 -0
  17. {primitive-0.1.53 → primitive-0.1.55}/README.md +0 -0
  18. {primitive-0.1.53 → primitive-0.1.55}/linux setup.md +0 -0
  19. {primitive-0.1.53 → primitive-0.1.55}/pyproject.toml +0 -0
  20. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/__init__.py +0 -0
  21. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/agent/commands.py +0 -0
  22. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/agent/provision.py +0 -0
  23. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/auth/__init__.py +0 -0
  24. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/auth/actions.py +0 -0
  25. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/auth/commands.py +0 -0
  26. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/cli.py +0 -0
  27. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/client.py +0 -0
  28. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/daemons/actions.py +0 -0
  29. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/daemons/commands.py +0 -0
  30. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/daemons/launch_agents.py +0 -0
  31. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/daemons/launch_service.py +0 -0
  32. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/files/commands.py +0 -0
  33. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/git/__init__.py +0 -0
  34. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/git/actions.py +0 -0
  35. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/git/commands.py +0 -0
  36. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/graphql/__init__.py +0 -0
  37. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/graphql/sdk.py +0 -0
  38. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/hardware/actions.py +0 -0
  39. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/hardware/commands.py +0 -0
  40. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/jobs/actions.py +0 -0
  41. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/jobs/commands.py +0 -0
  42. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/lint/actions.py +0 -0
  43. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/lint/commands.py +0 -0
  44. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/organizations/actions.py +0 -0
  45. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/organizations/commands.py +0 -0
  46. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/projects/__init__.py +0 -0
  47. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/projects/actions.py +0 -0
  48. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/projects/commands.py +0 -0
  49. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/sim/__init__.py +0 -0
  50. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/sim/actions.py +0 -0
  51. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/sim/commands.py +0 -0
  52. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/actions.py +0 -0
  53. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/auth.py +0 -0
  54. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/cache.py +0 -0
  55. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/config.py +0 -0
  56. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/files.py +0 -0
  57. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/git.py +0 -0
  58. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/memory_size.py +0 -0
  59. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/printer.py +0 -0
  60. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/shell.py +0 -0
  61. {primitive-0.1.53 → primitive-0.1.55}/src/primitive/utils/verible.py +0 -0
  62. {primitive-0.1.53 → primitive-0.1.55}/tests/__init__.py +0 -0
  63. {primitive-0.1.53 → primitive-0.1.55}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: primitive
3
- Version: 0.1.53
3
+ Version: 0.1.55
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.53"
4
+ __version__ = "0.1.55"
@@ -120,7 +120,7 @@ class Agent(BaseAction):
120
120
  )
121
121
 
122
122
  runner = AgentRunner(
123
- self.primitive,
123
+ primitive=self.primitive,
124
124
  source_dir=source_dir,
125
125
  job_id=job_run["id"],
126
126
  job_slug=job_run["job"]["slug"],
@@ -1,6 +1,6 @@
1
1
  from subprocess import Popen, PIPE
2
- import threading
3
2
  import shlex
3
+ import selectors
4
4
  from loguru import logger
5
5
 
6
6
 
@@ -21,8 +21,7 @@ class Process:
21
21
 
22
22
  def start(self):
23
23
  # Start the process
24
- logger.info(f"cmd: {self.cmd}")
25
- logger.info(f"workdir: {self.workdir}")
24
+ self.sel = selectors.DefaultSelector()
26
25
  self.process = Popen(
27
26
  self.cmd,
28
27
  env=self.env,
@@ -32,40 +31,33 @@ class Process:
32
31
  text=True,
33
32
  )
34
33
 
35
- # Function to read and log output from a pipe
36
- def log_output(pipe, level):
37
- for line in iter(pipe.readline, ""):
38
- if line:
39
- logger.log(level, line.rstrip())
40
- if level == "ERROR":
41
- self._errors += 1
34
+ self.sel.register(self.process.stdout, selectors.EVENT_READ)
35
+ self.sel.register(self.process.stderr, selectors.EVENT_READ)
42
36
 
43
- pipe.close()
37
+ def probe_logs(self):
38
+ for key, _ in self.sel.select():
39
+ data = key.fileobj.readline()
40
+ if not data:
41
+ continue
44
42
 
45
- # Create threads for stdout and stderr
46
- self.stdout_thread = threading.Thread(
47
- target=log_output, args=(self.process.stdout, "INFO")
48
- )
49
- self.stderr_thread = threading.Thread(
50
- target=log_output, args=(self.process.stderr, "ERROR")
51
- )
52
-
53
- # Start the threads
54
- self.stdout_thread.start()
55
- self.stderr_thread.start()
43
+ if key.fileobj is self.process.stdout:
44
+ logger.info(data.rstrip())
45
+ elif key.fileobj is self.process.stderr:
46
+ logger.error(data.rstrip())
47
+ self._errors += 1
56
48
 
57
49
  def wait(self):
58
- if self.process:
59
- # Wait for the process to complete
60
- self.process.wait()
61
- # Wait for the threads to finish reading output
62
- self.stdout_thread.join()
63
- self.stderr_thread.join()
50
+ while True:
51
+ self.probe_logs()
52
+ if not self.is_running():
53
+ break
54
+
55
+ return self.finish()
64
56
 
65
57
  def run(self):
66
58
  """Start and wait for the process."""
67
59
  self.start()
68
- self.wait()
60
+ return self.wait()
69
61
 
70
62
  def is_running(self):
71
63
  """Check if the process is still running."""
@@ -73,10 +65,13 @@ class Process:
73
65
 
74
66
  def finish(self):
75
67
  """Make sure that logging finishes"""
76
- self.stderr_thread.join()
77
- self.stderr_thread.join()
68
+ if self.process:
69
+ self.sel.unregister(self.process.stdout)
70
+ self.sel.unregister(self.process.stderr)
71
+ self.process.stdout.close()
72
+ self.process.stderr.close()
78
73
 
79
- return self.process.poll()
74
+ return self.process.poll()
80
75
 
81
76
  def terminate(self):
82
77
  """Terminate the process."""
@@ -2,6 +2,7 @@ import yaml
2
2
  import sys
3
3
  import typing
4
4
  import os
5
+ import threading
5
6
  from time import sleep
6
7
  from typing import TypedDict, Iterable, List, Optional, Dict
7
8
  from pathlib import Path, PurePath
@@ -111,10 +112,8 @@ class AgentRunner:
111
112
  logger.info(f"Beginning step {step['name']}")
112
113
 
113
114
  # Define step proc
114
- logger.info(f"first cmd: {step['cmd']}")
115
- logger.info(f"first workdir: {Path(self.source_dir / step['workdir'])}")
116
115
  proc = Process(
117
- step["cmd"],
116
+ cmd=step["cmd"],
118
117
  workdir=Path(self.source_dir / step["workdir"]),
119
118
  env=environment,
120
119
  )
@@ -127,24 +126,31 @@ class AgentRunner:
127
126
  conclusion = "failure"
128
127
  break
129
128
 
130
- # Check for updates to status while running
131
- while proc.is_running():
132
- status = self.primitive.jobs.get_job_status(self.job_id)
129
+ def status_check():
130
+ while proc.is_running():
131
+ # Check job status
132
+ status = self.primitive.jobs.get_job_status(self.job_id)
133
+ status_value = status["jobRun"]["status"]
133
134
 
134
- status_value = status["jobRun"]["status"]
135
+ # TODO: Should probably use request_cancelled or something
136
+ # once we change it, we'll have to call conclude w/ cancelled status
137
+ if status_value == "completed":
138
+ logger.warning("Job cancelled by user")
139
+ proc.terminate()
140
+ return
135
141
 
136
- # TODO: Should probably use request_cancelled or something
137
- # once we change it, we'll have to call conclude w/ cancelled status
138
- if status_value == "completed":
139
- logger.warning("Job cancelled by user")
140
- proc.terminate()
141
- return
142
+ sleep(5)
142
143
 
143
- sleep(5)
144
+ status_thread = threading.Thread(target=status_check)
145
+ status_thread.start()
144
146
 
145
- returncode = proc.finish()
147
+ # Wait for proc to finish
148
+ returncode = proc.wait()
146
149
  total_errors += proc.errors
147
150
 
151
+ # Wait for status check
152
+ status_thread.join()
153
+
148
154
  # Collect artifacts
149
155
  if "artifacts" in step:
150
156
  self.collect_artifacts(step)
@@ -0,0 +1,64 @@
1
+ import typing
2
+ import shutil
3
+ from loguru import logger
4
+ from pathlib import Path, PurePath
5
+ from ..utils.cache import get_artifacts_cache
6
+
7
+ if typing.TYPE_CHECKING:
8
+ import primitive.client
9
+
10
+
11
+ class Uploader:
12
+ def __init__(
13
+ self,
14
+ primitive: "primitive.client.Primitive",
15
+ ):
16
+ self.primitive = primitive
17
+
18
+ def upload_file(self, path: Path, prefix: str, job_run_id: str) -> str:
19
+ file_upload_response = self.primitive.files.file_upload(
20
+ path, key_prefix=prefix, job_run_id=job_run_id
21
+ )
22
+ return file_upload_response.json()["data"]["fileUpload"]["id"]
23
+
24
+ def scan(self) -> None:
25
+ # Scan artifacts directory
26
+ artifacts_dir = get_artifacts_cache()
27
+
28
+ subdirs = sorted(
29
+ [job_cache for job_cache in artifacts_dir.iterdir() if job_cache.is_dir()],
30
+ key=lambda p: p.stat().st_ctime,
31
+ )
32
+
33
+ for job_cache in subdirs:
34
+ job_run_id = job_cache.name
35
+
36
+ files = sorted(
37
+ [
38
+ w_path / file
39
+ for w_path, _, w_files in job_cache.walk()
40
+ for file in w_files
41
+ ],
42
+ key=lambda p: p.stat().st_size,
43
+ )
44
+
45
+ file_ids = []
46
+ for file in files:
47
+ upload_id = self.upload_file(
48
+ file,
49
+ prefix=str(PurePath(file).relative_to(job_cache.parent).parent),
50
+ job_run_id=job_run_id,
51
+ )
52
+
53
+ if upload_id:
54
+ file_ids.append(upload_id)
55
+ continue
56
+
57
+ logger.error(f"Unable to upload file {file}")
58
+
59
+ # Update job run
60
+ if len(file_ids) > 0:
61
+ self.primitive.jobs.job_run_update(id=job_run_id, file_ids=file_ids)
62
+
63
+ # Clean up job cache
64
+ shutil.rmtree(path=job_cache)
@@ -43,7 +43,13 @@ class Files(BaseAction):
43
43
  return result
44
44
 
45
45
  @guard
46
- def file_upload(self, path: Path, is_public: bool = False, key_prefix: str = ""):
46
+ def file_upload(
47
+ self,
48
+ path: Path,
49
+ is_public: bool = False,
50
+ key_prefix: str = "",
51
+ job_run_id: str = "",
52
+ ):
47
53
  file_path = str(path.resolve())
48
54
  if path.exists() is False:
49
55
  raise FileNotFoundError(f"File not found at {file_path}")
@@ -54,6 +60,8 @@ class Files(BaseAction):
54
60
  + file_path
55
61
  + """\", "keyPrefix": \""""
56
62
  + key_prefix
63
+ + """\", "jobRunId": \""""
64
+ + job_run_id
57
65
  + """\" } } }"""
58
66
  ) # noqa
59
67
 
@@ -63,6 +71,8 @@ class Files(BaseAction):
63
71
  + file_path
64
72
  + """\", "keyPrefix": \""""
65
73
  + key_prefix
74
+ + """\", "jobRunId": \""""
75
+ + job_run_id
66
76
  + """\" } } }"""
67
77
  ) # noqa
68
78
  body = {
@@ -1,46 +0,0 @@
1
- import typing
2
- import shutil
3
- from pathlib import Path, PurePath
4
- from ..utils.cache import get_artifacts_cache
5
-
6
- if typing.TYPE_CHECKING:
7
- import primitive.client
8
-
9
-
10
- class Uploader:
11
- def __init__(
12
- self,
13
- primitive: "primitive.client.Primitive",
14
- ):
15
- self.primitive = primitive
16
-
17
- def upload_file(self, path: Path, prefix: str) -> str:
18
- file_upload_response = self.primitive.files.file_upload(path, key_prefix=prefix)
19
- return file_upload_response.json()["data"]["fileUpload"]["id"]
20
-
21
- def scan(self) -> None:
22
- # Scan artifacts directory
23
- artifacts_dir = get_artifacts_cache()
24
-
25
- subdirs = [
26
- job_cache for job_cache in artifacts_dir.iterdir() if job_cache.is_dir()
27
- ]
28
-
29
- for job_cache in subdirs:
30
- job_run_id = job_cache.name
31
- files = [file for file in job_cache.rglob("*") if file.is_file()]
32
-
33
- file_ids = []
34
- for file in files:
35
- file_ids.append(
36
- self.upload_file(
37
- file,
38
- prefix=str(PurePath(file).relative_to(job_cache.parent).parent),
39
- )
40
- )
41
-
42
- # Update job run
43
- self.primitive.jobs.job_run_update(id=job_run_id, file_ids=file_ids)
44
-
45
- # Clean up job cache
46
- shutil.rmtree(path=job_cache)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes