primitive 0.2.10__tar.gz → 0.2.11__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 (119) hide show
  1. primitive-0.2.11/.github/workflows/pyright.yml +20 -0
  2. primitive-0.2.11/.vscode/extensions.json +13 -0
  3. {primitive-0.2.10 → primitive-0.2.11}/.vscode/settings.json +4 -1
  4. {primitive-0.2.10 → primitive-0.2.11}/PKG-INFO +3 -1
  5. {primitive-0.2.10 → primitive-0.2.11}/pyproject.toml +16 -0
  6. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/__about__.py +1 -1
  7. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/agent/actions.py +20 -18
  8. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/agent/runner.py +30 -33
  9. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/cli.py +2 -0
  10. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/client.py +41 -16
  11. primitive-0.2.11/src/primitive/daemons/actions.py +61 -0
  12. primitive-0.2.11/src/primitive/daemons/commands.py +111 -0
  13. primitive-0.2.11/src/primitive/daemons/launch_agents.py +237 -0
  14. primitive-0.2.11/src/primitive/daemons/launch_service.py +239 -0
  15. primitive-0.2.11/src/primitive/daemons/ui.py +41 -0
  16. primitive-0.2.11/src/primitive/db/base.py +5 -0
  17. primitive-0.2.11/src/primitive/db/models.py +78 -0
  18. primitive-0.2.11/src/primitive/db/sqlite.py +27 -0
  19. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/exec/actions.py +0 -1
  20. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/actions.py +0 -1
  21. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/actions.py +4 -8
  22. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/commands.py +1 -68
  23. primitive-0.2.11/src/primitive/hardware/ui.py +67 -0
  24. primitive-0.2.11/src/primitive/monitor/actions.py +102 -0
  25. primitive-0.2.11/src/primitive/monitor/commands.py +13 -0
  26. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/actions.py +0 -2
  27. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/auth.py +0 -2
  28. primitive-0.2.11/src/primitive/utils/daemons.py +54 -0
  29. primitive-0.2.11/uv.lock +1423 -0
  30. primitive-0.2.10/src/primitive/daemons/actions.py +0 -75
  31. primitive-0.2.10/src/primitive/daemons/commands.py +0 -65
  32. primitive-0.2.10/src/primitive/daemons/launch_agents.py +0 -154
  33. primitive-0.2.10/src/primitive/daemons/launch_service.py +0 -179
  34. primitive-0.2.10/uv.lock +0 -1201
  35. {primitive-0.2.10 → primitive-0.2.11}/.git-hooks/pre-commit +0 -0
  36. {primitive-0.2.10 → primitive-0.2.11}/.gitattributes +0 -0
  37. {primitive-0.2.10 → primitive-0.2.11}/.github/workflows/lint.yml +0 -0
  38. {primitive-0.2.10 → primitive-0.2.11}/.github/workflows/publish.yml +0 -0
  39. {primitive-0.2.10 → primitive-0.2.11}/.gitignore +0 -0
  40. {primitive-0.2.10 → primitive-0.2.11}/LICENSE.txt +0 -0
  41. {primitive-0.2.10 → primitive-0.2.11}/Makefile +0 -0
  42. {primitive-0.2.10 → primitive-0.2.11}/README.md +0 -0
  43. {primitive-0.2.10 → primitive-0.2.11}/linux setup.md +0 -0
  44. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/__init__.py +0 -0
  45. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/agent/__init__.py +0 -0
  46. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/agent/commands.py +0 -0
  47. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/agent/uploader.py +0 -0
  48. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/auth/__init__.py +0 -0
  49. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/auth/actions.py +0 -0
  50. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/auth/commands.py +0 -0
  51. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/auth/graphql/__init__.py +0 -0
  52. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/auth/graphql/queries.py +0 -0
  53. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/daemons/__init__.py +0 -0
  54. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/exec/__init__.py +0 -0
  55. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/exec/commands.py +0 -0
  56. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/exec/interactive.py +0 -0
  57. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/__init__.py +0 -0
  58. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/commands.py +0 -0
  59. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/graphql/__init__.py +0 -0
  60. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/graphql/fragments.py +0 -0
  61. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/graphql/mutations.py +0 -0
  62. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/files/graphql/queries.py +0 -0
  63. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/git/__init__.py +0 -0
  64. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/git/actions.py +0 -0
  65. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/git/commands.py +0 -0
  66. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/git/graphql/__init__.py +0 -0
  67. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/git/graphql/queries.py +0 -0
  68. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/graphql/__init__.py +0 -0
  69. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/graphql/relay.py +0 -0
  70. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/graphql/sdk.py +0 -0
  71. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/graphql/utility_fragments.py +0 -0
  72. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/__init__.py +0 -0
  73. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/android.py +0 -0
  74. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/graphql/__init__.py +0 -0
  75. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/graphql/fragments.py +0 -0
  76. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/graphql/mutations.py +0 -0
  77. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/hardware/graphql/queries.py +0 -0
  78. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/__init__.py +0 -0
  79. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/actions.py +0 -0
  80. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/commands.py +0 -0
  81. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/graphql/__init__.py +0 -0
  82. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/graphql/fragments.py +0 -0
  83. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/graphql/mutations.py +0 -0
  84. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/jobs/graphql/queries.py +0 -0
  85. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/__init__.py +0 -0
  86. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/actions.py +0 -0
  87. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/commands.py +0 -0
  88. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/graphql/__init__.py +0 -0
  89. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/graphql/fragments.py +0 -0
  90. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/graphql/mutations.py +0 -0
  91. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/organizations/graphql/queries.py +0 -0
  92. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/__init__.py +0 -0
  93. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/actions.py +0 -0
  94. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/commands.py +0 -0
  95. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/graphql/__init__.py +0 -0
  96. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/graphql/fragments.py +0 -0
  97. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/graphql/mutations.py +0 -0
  98. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/projects/graphql/queries.py +0 -0
  99. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/provisioning/__init__.py +0 -0
  100. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/provisioning/actions.py +0 -0
  101. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/provisioning/graphql/__init__.py +0 -0
  102. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/provisioning/graphql/queries.py +0 -0
  103. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/__init__.py +0 -0
  104. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/commands.py +0 -0
  105. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/graphql/__init__.py +0 -0
  106. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/graphql/fragments.py +0 -0
  107. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/graphql/mutations.py +0 -0
  108. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/reservations/graphql/queries.py +0 -0
  109. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/__init__.py +0 -0
  110. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/actions.py +0 -0
  111. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/cache.py +0 -0
  112. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/chunk_size.py +0 -0
  113. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/config.py +0 -0
  114. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/exceptions.py +0 -0
  115. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/memory_size.py +0 -0
  116. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/printer.py +0 -0
  117. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/shell.py +0 -0
  118. {primitive-0.2.10 → primitive-0.2.11}/src/primitive/utils/text.py +0 -0
  119. {primitive-0.2.10 → primitive-0.2.11}/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
+ }
@@ -11,8 +11,11 @@
11
11
  "Untaring",
12
12
  "VERIBLE",
13
13
  "verilog",
14
- "workdir"
14
+ "workdir",
15
+ "plutil",
16
+ "procs"
15
17
  ],
18
+ "python.languageServer": "Pylance",
16
19
  "[python]": {
17
20
  "editor.defaultFormatter": "charliermarsh.ruff",
18
21
  "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.11
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.11"
@@ -6,25 +6,29 @@ from loguru import logger
6
6
  from primitive.__about__ import __version__
7
7
  from primitive.utils.actions import BaseAction
8
8
 
9
- from ..utils.exceptions import P_CLI_100
10
9
  from .runner import Runner
11
10
  from .uploader import Uploader
11
+ from ..db import sqlite
12
+ from ..db.models import JobRun
12
13
 
13
14
 
14
15
  class Agent(BaseAction):
15
16
  def execute(
16
17
  self,
17
18
  ):
18
- logger.enable("primitive")
19
19
  logger.remove()
20
20
  logger.add(
21
21
  sink=sys.stderr,
22
- # catch=True,
22
+ format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
23
23
  backtrace=True,
24
24
  diagnose=True,
25
+ level="DEBUG" if self.primitive.DEBUG else "INFO",
25
26
  )
26
- logger.info(" [*] primitive")
27
- logger.info(f" [*] Version: {__version__}")
27
+ logger.info("[*] primitive agent")
28
+ logger.info(f"[*] Version: {__version__}")
29
+
30
+ # Initialize the database
31
+ sqlite.init()
28
32
 
29
33
  # Create uploader
30
34
  uploader = Uploader(primitive=self.primitive)
@@ -49,9 +53,6 @@ class Agent(BaseAction):
49
53
  logger.debug("Scanning for files to upload...")
50
54
  uploader.scan()
51
55
 
52
- logger.debug("Syncing children...")
53
- self.primitive.hardware._sync_children()
54
-
55
56
  hardware = self.primitive.hardware.get_own_hardware_details()
56
57
 
57
58
  if hardware["activeReservation"]:
@@ -108,6 +109,7 @@ class Agent(BaseAction):
108
109
  ]
109
110
 
110
111
  if not pending_job_runs:
112
+ self.primitive.hardware.check_in_http(is_online=True)
111
113
  sleep_amount = 5
112
114
  logger.debug(
113
115
  f"Waiting for Job Runs... [sleeping {sleep_amount} seconds]"
@@ -120,6 +122,11 @@ class Agent(BaseAction):
120
122
  logger.debug(f"Job Run ID: {job_run['id']}")
121
123
  logger.debug(f"Job Name: {job_run['job']['name']}")
122
124
 
125
+ JobRun.objects.create(
126
+ job_run_id=job_run["id"],
127
+ pid=None,
128
+ )
129
+
123
130
  runner = Runner(
124
131
  primitive=self.primitive,
125
132
  job_run=job_run,
@@ -137,6 +144,7 @@ class Agent(BaseAction):
137
144
  status="request_completed",
138
145
  conclusion="failure",
139
146
  )
147
+ JobRun.objects.filter_by(job_run_id=job_run["id"]).delete()
140
148
  continue
141
149
 
142
150
  try:
@@ -152,17 +160,11 @@ class Agent(BaseAction):
152
160
  runner.cleanup()
153
161
 
154
162
  # NOTE: also run scan here to force upload of artifacts
155
- # This should probably eventuall be another daemon?
163
+ # This should probably eventually be another daemon?
156
164
  uploader.scan()
157
165
 
166
+ JobRun.objects.filter_by(job_run_id=job_run["id"]).delete()
167
+
158
168
  sleep(5)
159
169
  except KeyboardInterrupt:
160
- logger.info(" [*] Stopping primitive...")
161
- try:
162
- self.primitive.hardware.check_in_http(
163
- is_available=False, is_online=False, stopping_agent=True
164
- )
165
- except P_CLI_100 as exception:
166
- logger.error(" [*] Error stopping primitive.")
167
- logger.error(str(exception))
168
- sys.exit()
170
+ logger.info("[*] Stopping primitive agent...")
@@ -2,12 +2,12 @@ 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
9
8
  from pathlib import Path, PurePath
10
9
  from typing import Dict, List, TypedDict
10
+ from ..db.models import JobRun
11
11
 
12
12
  import yaml
13
13
  from loguru import logger
@@ -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,40 @@ class Runner:
158
156
  self.modified_env = {**self.initial_env}
159
157
 
160
158
  task_failed = False
159
+ cancelled = False
161
160
  conclusion = "success"
162
161
  for task in self.config["executes"]:
162
+ status = self.primitive.jobs.get_job_status(self.job_run["id"])
163
+ status_value = status.data["jobRun"]["status"]
164
+ conclusion_value = status.data["jobRun"]["conclusion"]
165
+
166
+ if status_value == "completed" and conclusion_value == "cancelled":
167
+ cancelled = True
168
+ break
169
+
163
170
  with logger.contextualize(label=task["label"]):
164
171
  with asyncio.Runner() as async_runner:
165
172
  if task_failed := async_runner.run(self.run_task(task)):
166
173
  break
167
174
 
175
+ number_of_files_produced = self.get_number_of_files_produced()
176
+ logger.info(
177
+ f"Produced {number_of_files_produced} files for {self.job['slug']} job"
178
+ )
179
+
180
+ if cancelled:
181
+ logger.warning("Job cancelled by user")
182
+ self.primitive.jobs.job_run_update(
183
+ self.job_run["id"],
184
+ number_of_files_produced=number_of_files_produced,
185
+ )
186
+ return
187
+
168
188
  if task_failed:
169
189
  conclusion = "failure"
170
190
  else:
171
191
  logger.success(f"Completed {self.job['slug']} job")
172
192
 
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
193
  self.primitive.jobs.job_run_update(
178
194
  self.job_run["id"],
179
195
  status="request_completed",
@@ -249,24 +265,24 @@ class Runner:
249
265
  stderr=asyncio.subprocess.PIPE,
250
266
  )
251
267
 
252
- loop = asyncio.get_running_loop()
253
- monitor_task = loop.run_in_executor(None, self.monitor_cmd, process)
268
+ JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
269
+ {"pid": process.pid}
270
+ )
254
271
 
255
- stdout_failed, stderr_failed, cancelled = await asyncio.gather(
272
+ stdout_failed, stderr_failed = await asyncio.gather(
256
273
  self.log_cmd(
257
274
  process=process, stream=process.stdout, tags=task.get("tags", {})
258
275
  ),
259
276
  self.log_cmd(
260
277
  process=process, stream=process.stderr, tags=task.get("tags", {})
261
278
  ),
262
- monitor_task,
263
279
  )
264
280
 
265
281
  returncode = await process.wait()
266
282
 
267
- if cancelled:
268
- logger.warning("Job cancelled by user")
269
- return True
283
+ JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
284
+ {"pid": None}
285
+ )
270
286
 
271
287
  if returncode > 0:
272
288
  logger.error(
@@ -355,25 +371,6 @@ class Runner:
355
371
 
356
372
  return [line for line in lines if len(line) > 0]
357
373
 
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
374
  def cleanup(self) -> None:
378
375
  logger.remove(self.file_logger)
379
376
 
@@ -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)