primitive 0.2.10__tar.gz → 0.2.12__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 (120) hide show
  1. primitive-0.2.12/.github/workflows/pyright.yml +20 -0
  2. primitive-0.2.12/.vscode/extensions.json +13 -0
  3. {primitive-0.2.10 → primitive-0.2.12}/.vscode/settings.json +4 -0
  4. {primitive-0.2.10 → primitive-0.2.12}/PKG-INFO +3 -1
  5. {primitive-0.2.10 → primitive-0.2.12}/pyproject.toml +16 -0
  6. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/__about__.py +1 -1
  7. primitive-0.2.12/src/primitive/agent/actions.py +110 -0
  8. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/commands.py +2 -1
  9. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/runner.py +43 -34
  10. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/uploader.py +2 -2
  11. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/cli.py +2 -0
  12. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/client.py +41 -16
  13. primitive-0.2.12/src/primitive/daemons/actions.py +61 -0
  14. primitive-0.2.12/src/primitive/daemons/commands.py +111 -0
  15. primitive-0.2.12/src/primitive/daemons/launch_agents.py +237 -0
  16. primitive-0.2.12/src/primitive/daemons/launch_service.py +239 -0
  17. primitive-0.2.12/src/primitive/daemons/ui.py +41 -0
  18. primitive-0.2.12/src/primitive/db/base.py +5 -0
  19. primitive-0.2.12/src/primitive/db/models.py +88 -0
  20. primitive-0.2.12/src/primitive/db/sqlite.py +34 -0
  21. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/actions.py +0 -1
  22. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/actions.py +0 -1
  23. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/actions.py +11 -10
  24. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/commands.py +1 -68
  25. primitive-0.2.12/src/primitive/hardware/ui.py +67 -0
  26. primitive-0.2.12/src/primitive/monitor/actions.py +199 -0
  27. primitive-0.2.12/src/primitive/monitor/commands.py +13 -0
  28. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/actions.py +0 -2
  29. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/auth.py +0 -2
  30. primitive-0.2.12/src/primitive/utils/daemons.py +54 -0
  31. primitive-0.2.12/uv.lock +1423 -0
  32. primitive-0.2.10/src/primitive/agent/actions.py +0 -168
  33. primitive-0.2.10/src/primitive/daemons/actions.py +0 -75
  34. primitive-0.2.10/src/primitive/daemons/commands.py +0 -65
  35. primitive-0.2.10/src/primitive/daemons/launch_agents.py +0 -154
  36. primitive-0.2.10/src/primitive/daemons/launch_service.py +0 -179
  37. primitive-0.2.10/uv.lock +0 -1201
  38. {primitive-0.2.10 → primitive-0.2.12}/.git-hooks/pre-commit +0 -0
  39. {primitive-0.2.10 → primitive-0.2.12}/.gitattributes +0 -0
  40. {primitive-0.2.10 → primitive-0.2.12}/.github/workflows/lint.yml +0 -0
  41. {primitive-0.2.10 → primitive-0.2.12}/.github/workflows/publish.yml +0 -0
  42. {primitive-0.2.10 → primitive-0.2.12}/.gitignore +0 -0
  43. {primitive-0.2.10 → primitive-0.2.12}/LICENSE.txt +0 -0
  44. {primitive-0.2.10 → primitive-0.2.12}/Makefile +0 -0
  45. {primitive-0.2.10 → primitive-0.2.12}/README.md +0 -0
  46. {primitive-0.2.10 → primitive-0.2.12}/linux setup.md +0 -0
  47. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/__init__.py +0 -0
  48. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/__init__.py +0 -0
  49. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/__init__.py +0 -0
  50. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/actions.py +0 -0
  51. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/commands.py +0 -0
  52. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/graphql/__init__.py +0 -0
  53. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/graphql/queries.py +0 -0
  54. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/daemons/__init__.py +0 -0
  55. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/__init__.py +0 -0
  56. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/commands.py +0 -0
  57. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/interactive.py +0 -0
  58. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/__init__.py +0 -0
  59. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/commands.py +0 -0
  60. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/__init__.py +0 -0
  61. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/fragments.py +0 -0
  62. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/mutations.py +0 -0
  63. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/queries.py +0 -0
  64. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/__init__.py +0 -0
  65. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/actions.py +0 -0
  66. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/commands.py +0 -0
  67. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/graphql/__init__.py +0 -0
  68. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/graphql/queries.py +0 -0
  69. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/__init__.py +0 -0
  70. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/relay.py +0 -0
  71. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/sdk.py +0 -0
  72. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/utility_fragments.py +0 -0
  73. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/__init__.py +0 -0
  74. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/android.py +0 -0
  75. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/__init__.py +0 -0
  76. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/fragments.py +0 -0
  77. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/mutations.py +0 -0
  78. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/queries.py +0 -0
  79. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/__init__.py +0 -0
  80. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/actions.py +0 -0
  81. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/commands.py +0 -0
  82. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/__init__.py +0 -0
  83. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/fragments.py +0 -0
  84. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/mutations.py +0 -0
  85. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/queries.py +0 -0
  86. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/__init__.py +0 -0
  87. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/actions.py +0 -0
  88. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/commands.py +0 -0
  89. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/__init__.py +0 -0
  90. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/fragments.py +0 -0
  91. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/mutations.py +0 -0
  92. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/queries.py +0 -0
  93. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/__init__.py +0 -0
  94. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/actions.py +0 -0
  95. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/commands.py +0 -0
  96. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/__init__.py +0 -0
  97. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/fragments.py +0 -0
  98. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/mutations.py +0 -0
  99. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/queries.py +0 -0
  100. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/__init__.py +0 -0
  101. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/actions.py +0 -0
  102. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/graphql/__init__.py +0 -0
  103. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/graphql/queries.py +0 -0
  104. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/__init__.py +0 -0
  105. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/commands.py +0 -0
  106. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/__init__.py +0 -0
  107. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/fragments.py +0 -0
  108. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/mutations.py +0 -0
  109. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/queries.py +0 -0
  110. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/__init__.py +0 -0
  111. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/actions.py +0 -0
  112. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/cache.py +0 -0
  113. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/chunk_size.py +0 -0
  114. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/config.py +0 -0
  115. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/exceptions.py +0 -0
  116. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/memory_size.py +0 -0
  117. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/printer.py +0 -0
  118. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/shell.py +0 -0
  119. {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/text.py +0 -0
  120. {primitive-0.2.10 → primitive-0.2.12}/tests/__init__.py +0 -0
@@ -0,0 +1,20 @@
1
+ name: Pyright
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: ["*"]
7
+ pull_request:
8
+ branches: ["*"]
9
+
10
+ jobs:
11
+ build:
12
+ name: Pyright
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ max-parallel: 4
16
+ matrix:
17
+ python-version: [3.13]
18
+
19
+ steps:
20
+ - uses: jakebailey/pyright-action@v1
@@ -0,0 +1,13 @@
1
+ {
2
+ "recommendations": [
3
+ "charliermarsh.ruff",
4
+ "github.copilot-chat",
5
+ "ms-python.debugpy",
6
+ "ms-python.python",
7
+ "ms-python.vscode-pylance",
8
+ "ms-vscode.remote-explorer",
9
+ "ms-vscode-remote.remote-ssh",
10
+ "ms-vsliveshare.vsliveshare",
11
+ "streetsidesoftware.code-spell-checker"
12
+ ]
13
+ }
@@ -6,6 +6,9 @@
6
6
  "Mbit",
7
7
  "mbps",
8
8
  "pkey",
9
+ "plutil",
10
+ "procs",
11
+ "sessionmaker",
9
12
  "testsuites",
10
13
  "untar",
11
14
  "Untaring",
@@ -13,6 +16,7 @@
13
16
  "verilog",
14
17
  "workdir"
15
18
  ],
19
+ "python.languageServer": "Pylance",
16
20
  "[python]": {
17
21
  "editor.defaultFormatter": "charliermarsh.ruff",
18
22
  "editor.formatOnSave": true,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.10
3
+ Version: 0.2.12
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
@@ -23,9 +23,11 @@ Requires-Dist: gql[all]
23
23
  Requires-Dist: loguru
24
24
  Requires-Dist: paramiko[invoke]
25
25
  Requires-Dist: primitive-pal==0.1.4
26
+ Requires-Dist: psutil>=7.0.0
26
27
  Requires-Dist: pyyaml
27
28
  Requires-Dist: rich>=13.9.4
28
29
  Requires-Dist: speedtest-cli
30
+ Requires-Dist: sqlalchemy>=2.0.40
29
31
  Description-Content-Type: text/markdown
30
32
 
31
33
  # primitive
@@ -35,6 +35,8 @@ dependencies = [
35
35
  "paramiko[invoke]",
36
36
  "speedtest-cli",
37
37
  "rich>=13.9.4",
38
+ "sqlalchemy>=2.0.40",
39
+ "psutil>=7.0.0",
38
40
  ]
39
41
 
40
42
  [tool.uv]
@@ -79,3 +81,17 @@ tests = ["tests", "*/primitive/tests"]
79
81
 
80
82
  [tool.coverage.report]
81
83
  exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
84
+
85
+ [tool.pyright]
86
+ include = ["."]
87
+ exclude = ["**/__pycache__"]
88
+ defineConstant = { DEBUG = true }
89
+ stubPath = "stubs"
90
+
91
+ reportMissingImports = true
92
+ reportMissingTypeStubs = false
93
+
94
+ pythonVersion = "3.13"
95
+ pythonPlatform = "Linux"
96
+
97
+ executionEnvironments = [{ root = "src" }, { root = "tests" }]
@@ -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.2.10"
4
+ __version__ = "0.2.12"
@@ -0,0 +1,110 @@
1
+ import sys
2
+ from time import sleep
3
+
4
+ from loguru import logger
5
+
6
+ from primitive.__about__ import __version__
7
+ from primitive.utils.actions import BaseAction
8
+
9
+ from ..db import sqlite
10
+ from ..db.models import JobRun
11
+ from .runner import Runner
12
+ from .uploader import Uploader
13
+
14
+
15
+ class Agent(BaseAction):
16
+ def execute(
17
+ self,
18
+ ):
19
+ logger.remove()
20
+ logger.add(
21
+ sink=sys.stderr,
22
+ format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
23
+ backtrace=True,
24
+ diagnose=True,
25
+ level="DEBUG" if self.primitive.DEBUG else "INFO",
26
+ )
27
+ logger.info("[*] primitive agent")
28
+ logger.info(f"[*] Version: {__version__}")
29
+
30
+ # Initialize the database
31
+ sqlite.init()
32
+
33
+ # Create uploader
34
+ uploader = Uploader(primitive=self.primitive)
35
+
36
+ try:
37
+ while True:
38
+ logger.debug("Scanning for files to upload...")
39
+ uploader.scan()
40
+
41
+ db_job_run = JobRun.objects.first()
42
+
43
+ if not db_job_run:
44
+ sleep_amount = 5
45
+ logger.debug(
46
+ f"No pending job runs... [sleeping {sleep_amount} seconds]"
47
+ )
48
+ sleep(sleep_amount)
49
+ continue
50
+
51
+ api_job_run_data = self.primitive.jobs.get_job_run(
52
+ id=db_job_run.job_run_id,
53
+ )
54
+
55
+ if not api_job_run_data or not api_job_run_data.data:
56
+ logger.error(
57
+ f"Job Run {db_job_run.job_run_id} not found in API, deleting from DB"
58
+ )
59
+ JobRun.objects.filter_by(job_run_id=db_job_run.job_run_id).delete()
60
+ continue
61
+
62
+ api_job_run = api_job_run_data.data["jobRun"]
63
+
64
+ logger.debug("Found pending Job Run")
65
+ logger.debug(f"Job Run ID: {api_job_run.get('id')}")
66
+ logger.debug(f"Job Name: {api_job_run.get('name')}")
67
+
68
+ runner = Runner(
69
+ primitive=self.primitive,
70
+ job_run=api_job_run,
71
+ # max_log_size=500 * 1024,
72
+ )
73
+
74
+ try:
75
+ runner.setup()
76
+ except Exception as exception:
77
+ logger.exception(
78
+ f"Exception while initializing runner: {exception}"
79
+ )
80
+ self.primitive.jobs.job_run_update(
81
+ id=api_job_run.get("id"),
82
+ status="request_completed",
83
+ conclusion="failure",
84
+ )
85
+ JobRun.objects.filter_by(job_run_id=api_job_run.get("id")).delete()
86
+ continue
87
+
88
+ try:
89
+ runner.execute()
90
+ except Exception as exception:
91
+ logger.exception(f"Exception while executing job: {exception}")
92
+ self.primitive.jobs.job_run_update(
93
+ id=api_job_run.get("id"),
94
+ status="request_completed",
95
+ conclusion="failure",
96
+ )
97
+ finally:
98
+ runner.cleanup()
99
+
100
+ # NOTE: also run scan here to force upload of artifacts
101
+ # This should probably eventually be another daemon?
102
+ uploader.scan()
103
+
104
+ JobRun.objects.filter_by(
105
+ job_run_id=api_job_run.get("id"),
106
+ ).delete()
107
+
108
+ sleep(5)
109
+ except KeyboardInterrupt:
110
+ logger.info("[*] Stopping primitive agent...")
@@ -1,6 +1,7 @@
1
- import click
2
1
  import typing
3
2
 
3
+ import click
4
+
4
5
  if typing.TYPE_CHECKING:
5
6
  from ..client import Primitive
6
7
 
@@ -2,7 +2,6 @@ import asyncio
2
2
  import os
3
3
  import re
4
4
  import shutil
5
- import time
6
5
  import typing
7
6
  from abc import abstractmethod
8
7
  from enum import Enum, IntEnum
@@ -12,6 +11,7 @@ from typing import Dict, List, TypedDict
12
11
  import yaml
13
12
  from loguru import logger
14
13
 
14
+ from ..db.models import JobRun
15
15
  from ..utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
16
16
  from ..utils.shell import env_to_dict
17
17
 
@@ -80,8 +80,6 @@ class Runner:
80
80
  self.modified_env = {}
81
81
  self.file_logger = None
82
82
 
83
- logger.enable("primitive")
84
-
85
83
  # If max_log_size set to <= 0, disable file logging
86
84
  if max_log_size > 0:
87
85
  log_name = f"{self.job['slug']}_{self.job_run['jobRunNumber']}_{{time}}.primitive.log"
@@ -158,22 +156,52 @@ class Runner:
158
156
  self.modified_env = {**self.initial_env}
159
157
 
160
158
  task_failed = False
161
- conclusion = "success"
159
+ cancelled = False
160
+
162
161
  for task in self.config["executes"]:
162
+ # the get status check here is to ensure that if cancel is called
163
+ # while one task is running, we do not run any OTHER laebeled tasks
164
+ # THIS is required for MULTI STEP JOBS
165
+ status = self.primitive.jobs.get_job_status(self.job_run["id"])
166
+ status_value = status.data["jobRun"]["status"]
167
+ conclusion_value = status.data["jobRun"]["conclusion"]
168
+
169
+ if status_value == "completed" and conclusion_value == "cancelled":
170
+ cancelled = True
171
+ break
172
+
163
173
  with logger.contextualize(label=task["label"]):
164
174
  with asyncio.Runner() as async_runner:
165
175
  if task_failed := async_runner.run(self.run_task(task)):
166
176
  break
167
177
 
178
+ number_of_files_produced = self.get_number_of_files_produced()
179
+ logger.info(
180
+ f"Produced {number_of_files_produced} files for {self.job['slug']} job"
181
+ )
182
+
183
+ # FOR NONE MULTI STEP JOBS
184
+ # we still have to check that the job was cancelled here as well
185
+ status = self.primitive.jobs.get_job_status(self.job_run["id"])
186
+ status_value = status.data["jobRun"]["status"]
187
+ conclusion_value = status.data["jobRun"]["conclusion"]
188
+ if status_value == "completed" and conclusion_value == "cancelled":
189
+ cancelled = True
190
+
191
+ if cancelled:
192
+ logger.warning("Job cancelled by user")
193
+ self.primitive.jobs.job_run_update(
194
+ self.job_run["id"],
195
+ number_of_files_produced=number_of_files_produced,
196
+ )
197
+ return
198
+
199
+ conclusion = "success"
168
200
  if task_failed:
169
201
  conclusion = "failure"
170
202
  else:
171
203
  logger.success(f"Completed {self.job['slug']} job")
172
204
 
173
- number_of_files_produced = self.get_number_of_files_produced()
174
- logger.info(
175
- f"Produced {number_of_files_produced} files for {self.job['slug']} job"
176
- )
177
205
  self.primitive.jobs.job_run_update(
178
206
  self.job_run["id"],
179
207
  status="request_completed",
@@ -249,24 +277,24 @@ class Runner:
249
277
  stderr=asyncio.subprocess.PIPE,
250
278
  )
251
279
 
252
- loop = asyncio.get_running_loop()
253
- monitor_task = loop.run_in_executor(None, self.monitor_cmd, process)
280
+ JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
281
+ {"pid": process.pid}
282
+ )
254
283
 
255
- stdout_failed, stderr_failed, cancelled = await asyncio.gather(
284
+ stdout_failed, stderr_failed = await asyncio.gather(
256
285
  self.log_cmd(
257
286
  process=process, stream=process.stdout, tags=task.get("tags", {})
258
287
  ),
259
288
  self.log_cmd(
260
289
  process=process, stream=process.stderr, tags=task.get("tags", {})
261
290
  ),
262
- monitor_task,
263
291
  )
264
292
 
265
293
  returncode = await process.wait()
266
294
 
267
- if cancelled:
268
- logger.warning("Job cancelled by user")
269
- return True
295
+ JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
296
+ {"pid": None}
297
+ )
270
298
 
271
299
  if returncode > 0:
272
300
  logger.error(
@@ -355,25 +383,6 @@ class Runner:
355
383
 
356
384
  return [line for line in lines if len(line) > 0]
357
385
 
358
- def monitor_cmd(self, process) -> bool:
359
- while process.returncode is None:
360
- status = self.primitive.jobs.get_job_status(self.job_run["id"])
361
-
362
- status_value = status.data["jobRun"]["status"]
363
- conclusion_value = status.data["jobRun"]["conclusion"]
364
-
365
- if status_value == "completed" and conclusion_value == "cancelled":
366
- try:
367
- process.terminate()
368
- except ProcessLookupError:
369
- pass
370
-
371
- return True
372
-
373
- time.sleep(10)
374
-
375
- return False
376
-
377
386
  def cleanup(self) -> None:
378
387
  logger.remove(self.file_logger)
379
388
 
@@ -50,8 +50,8 @@ class Uploader:
50
50
  path=file,
51
51
  key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
52
52
  )
53
- except Exception as e:
54
- if "is empty" in str(e):
53
+ except Exception as exception:
54
+ if "is empty" in str(exception):
55
55
  logger.warning(f"{file} is empty, skipping upload")
56
56
  continue
57
57
 
@@ -16,6 +16,7 @@ from .jobs.commands import cli as jobs_commands
16
16
  from .organizations.commands import cli as organizations_commands
17
17
  from .projects.commands import cli as projects_commands
18
18
  from .reservations.commands import cli as reservations_commands
19
+ from .monitor.commands import cli as monitor_commands
19
20
 
20
21
 
21
22
  @click.group()
@@ -71,6 +72,7 @@ cli.add_command(organizations_commands, "organizations")
71
72
  cli.add_command(projects_commands, "projects")
72
73
  cli.add_command(reservations_commands, "reservations")
73
74
  cli.add_command(exec_commands, "exec")
75
+ cli.add_command(monitor_commands, "monitor")
74
76
 
75
77
  if __name__ == "__main__":
76
78
  cli(obj={})
@@ -1,7 +1,8 @@
1
- import sys
2
-
3
1
  from gql import Client
4
2
  from loguru import logger
3
+ from rich.logging import RichHandler
4
+ from rich.traceback import install
5
+ from typing import Optional
5
6
 
6
7
  from .agent.actions import Agent
7
8
  from .auth.actions import Auth
@@ -15,10 +16,9 @@ from .organizations.actions import Organizations
15
16
  from .projects.actions import Projects
16
17
  from .provisioning.actions import Provisioning
17
18
  from .reservations.actions import Reservations
19
+ from .monitor.actions import Monitor
18
20
  from .utils.config import read_config_file
19
21
 
20
- logger.disable("primitive")
21
-
22
22
 
23
23
  class Primitive:
24
24
  def __init__(
@@ -26,24 +26,48 @@ class Primitive:
26
26
  host: str = "api.primitive.tech",
27
27
  DEBUG: bool = False,
28
28
  JSON: bool = False,
29
- token: str = None,
30
- transport: str = None,
29
+ token: Optional[str] = None,
30
+ transport: Optional[str] = None,
31
31
  ) -> None:
32
32
  self.host: str = host
33
- self.session: Client = None
33
+ self.session: Optional[Client] = None
34
34
  self.DEBUG: bool = DEBUG
35
35
  self.JSON: bool = JSON
36
36
 
37
+ # Enable tracebacks with local variables
37
38
  if self.DEBUG:
38
- logger.enable("primitive")
39
- logger.remove()
40
- logger.add(
41
- sink=sys.stderr,
42
- serialize=self.JSON,
43
- catch=True,
44
- backtrace=True,
45
- diagnose=True,
46
- )
39
+ install(show_locals=True)
40
+
41
+ # Configure rich logging handler
42
+ rich_handler = RichHandler(
43
+ rich_tracebacks=self.DEBUG, # Pretty tracebacks
44
+ markup=True, # Allow Rich markup tags
45
+ show_time=self.DEBUG, # Show timestamps
46
+ show_level=self.DEBUG, # Show log levels
47
+ show_path=self.DEBUG, # Hide source path (optional)
48
+ )
49
+
50
+ def formatter(record) -> str:
51
+ match record["level"].name:
52
+ case "ERROR":
53
+ return "[bold red]Error>[/bold red] {name}:{function}:{line} - {message}"
54
+ case "CRITICAL":
55
+ return "[italic bold red]Critical>[/italic bold red] {name}:{function}:{line} - {message}"
56
+ case "WARNING":
57
+ return "[bold yellow]Warning>[/bold yellow] {message}"
58
+ case _:
59
+ return "[#666666]>[/#666666] {message}"
60
+
61
+ logger.remove()
62
+ logger.add(
63
+ sink=rich_handler,
64
+ format="{message}" if self.DEBUG else formatter,
65
+ level="DEBUG" if self.DEBUG else "INFO",
66
+ backtrace=self.DEBUG,
67
+ )
68
+
69
+ # Nothing will print here if DEBUG is false
70
+ logger.debug("Debug mode enabled")
47
71
 
48
72
  # Generate full or partial host config
49
73
  if not token and not transport:
@@ -67,6 +91,7 @@ class Primitive:
67
91
  self.daemons: Daemons = Daemons(self)
68
92
  self.exec: Exec = Exec(self)
69
93
  self.provisioning: Provisioning = Provisioning(self)
94
+ self.monitor: Monitor = Monitor(self)
70
95
 
71
96
  def get_host_config(self):
72
97
  self.full_config = read_config_file()
@@ -0,0 +1,61 @@
1
+ import platform
2
+ import typing
3
+ from typing import Dict, Optional, List
4
+
5
+ if typing.TYPE_CHECKING:
6
+ from ..client import Primitive
7
+
8
+ from .launch_agents import LaunchAgent
9
+ from .launch_service import LaunchService
10
+ from ..utils.daemons import Daemon
11
+
12
+
13
+ class Daemons:
14
+ def __init__(self, primitive) -> None:
15
+ self.primitive: Primitive = primitive
16
+ self.os_family = platform.system()
17
+
18
+ match self.os_family:
19
+ case "Darwin":
20
+ self.daemons: Dict[str, Daemon] = {
21
+ "agent": LaunchAgent("tech.primitive.agent"),
22
+ "monitor": LaunchAgent("tech.primitive.monitor"),
23
+ }
24
+ case "Linux":
25
+ self.daemons: Dict[str, Daemon] = {
26
+ "agent": LaunchService("tech.primitive.agent"),
27
+ "monitor": LaunchService("tech.primitive.monitor"),
28
+ }
29
+ case _:
30
+ raise NotImplementedError(f"{self.os_family} is not supported.")
31
+
32
+ def install(self, name: Optional[str]) -> bool:
33
+ if name:
34
+ return self.daemons[name].install()
35
+ else:
36
+ return all([daemon.install() for daemon in self.daemons.values()])
37
+
38
+ def uninstall(self, name: Optional[str]) -> bool:
39
+ if name:
40
+ return self.daemons[name].uninstall()
41
+ else:
42
+ return all([daemon.uninstall() for daemon in self.daemons.values()])
43
+
44
+ def stop(self, name: Optional[str]) -> bool:
45
+ if name:
46
+ return self.daemons[name].stop()
47
+ else:
48
+ return all([daemon.stop() for daemon in self.daemons.values()])
49
+
50
+ def start(self, name: Optional[str]) -> bool:
51
+ if name:
52
+ return self.daemons[name].start()
53
+ else:
54
+ return all([daemon.start() for daemon in self.daemons.values()])
55
+
56
+ def list(self) -> List[Daemon]:
57
+ """List all daemons"""
58
+ return list(self.daemons.values())
59
+
60
+ def logs(self, name: str) -> None:
61
+ self.daemons[name].view_logs()
@@ -0,0 +1,111 @@
1
+ import click
2
+
3
+ import typing
4
+ from typing import Optional
5
+ from .ui import render_daemon_list
6
+
7
+ from loguru import logger
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from ..client import Primitive
11
+
12
+
13
+ @click.group()
14
+ @click.pass_context
15
+ def cli(context):
16
+ """Daemon"""
17
+ pass
18
+
19
+
20
+ @cli.command("install")
21
+ @click.pass_context
22
+ @click.argument(
23
+ "name",
24
+ type=str,
25
+ required=False,
26
+ )
27
+ def install_daemon_command(context, name: Optional[str]):
28
+ """Install the full primitive daemon"""
29
+ primitive: Primitive = context.obj.get("PRIMITIVE")
30
+ installed = primitive.daemons.install(name=name)
31
+
32
+ if installed:
33
+ logger.info(":white_check_mark: daemon(s) installed successfully!")
34
+ else:
35
+ logger.error("Unable to install daemon(s).")
36
+
37
+
38
+ @cli.command("uninstall")
39
+ @click.pass_context
40
+ @click.argument(
41
+ "name",
42
+ type=str,
43
+ required=False,
44
+ )
45
+ def uninstall_daemon_command(context, name: Optional[str]):
46
+ """Uninstall the full primitive Daemon"""
47
+ primitive: Primitive = context.obj.get("PRIMITIVE")
48
+ uninstalled = primitive.daemons.uninstall(name=name)
49
+
50
+ if uninstalled:
51
+ logger.info(":white_check_mark: daemon(s) uninstalled successfully!")
52
+ else:
53
+ logger.error("Unable to uninstall daemon(s).")
54
+
55
+
56
+ @cli.command("stop")
57
+ @click.pass_context
58
+ @click.argument(
59
+ "name",
60
+ type=str,
61
+ required=False,
62
+ )
63
+ def stop_daemon_command(context, name: Optional[str]):
64
+ """Stop primitive Daemon"""
65
+ primitive: Primitive = context.obj.get("PRIMITIVE")
66
+ stopped = primitive.daemons.stop(name=name)
67
+
68
+ if stopped:
69
+ logger.info(":white_check_mark: daemon(s) stopped successfully!")
70
+ else:
71
+ logger.error("Unable to stop daemon(s).")
72
+
73
+
74
+ @cli.command("start")
75
+ @click.pass_context
76
+ @click.argument(
77
+ "name",
78
+ type=str,
79
+ required=False,
80
+ )
81
+ def start_daemon_command(context, name: Optional[str]):
82
+ """Start primitive Daemon"""
83
+ primitive: Primitive = context.obj.get("PRIMITIVE")
84
+ started = primitive.daemons.start(name=name)
85
+
86
+ if started:
87
+ logger.info(":white_check_mark: daemon(s) started successfully!")
88
+ else:
89
+ logger.error("Unable to start daemon(s).")
90
+
91
+
92
+ @cli.command("logs")
93
+ @click.pass_context
94
+ @click.argument(
95
+ "name",
96
+ type=str,
97
+ required=True,
98
+ )
99
+ def log_daemon_command(context, name: str):
100
+ """Logs from primitive Daemon"""
101
+ primitive: Primitive = context.obj.get("PRIMITIVE")
102
+ primitive.daemons.logs(name=name)
103
+
104
+
105
+ @cli.command("list")
106
+ @click.pass_context
107
+ def list_daemon_command(context):
108
+ """List all daemons"""
109
+ primitive: Primitive = context.obj.get("PRIMITIVE")
110
+ daemon_list = primitive.daemons.list()
111
+ render_daemon_list(daemons=daemon_list)