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.
- primitive-0.2.12/.github/workflows/pyright.yml +20 -0
- primitive-0.2.12/.vscode/extensions.json +13 -0
- {primitive-0.2.10 → primitive-0.2.12}/.vscode/settings.json +4 -0
- {primitive-0.2.10 → primitive-0.2.12}/PKG-INFO +3 -1
- {primitive-0.2.10 → primitive-0.2.12}/pyproject.toml +16 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/__about__.py +1 -1
- primitive-0.2.12/src/primitive/agent/actions.py +110 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/commands.py +2 -1
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/runner.py +43 -34
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/uploader.py +2 -2
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/cli.py +2 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/client.py +41 -16
- primitive-0.2.12/src/primitive/daemons/actions.py +61 -0
- primitive-0.2.12/src/primitive/daemons/commands.py +111 -0
- primitive-0.2.12/src/primitive/daemons/launch_agents.py +237 -0
- primitive-0.2.12/src/primitive/daemons/launch_service.py +239 -0
- primitive-0.2.12/src/primitive/daemons/ui.py +41 -0
- primitive-0.2.12/src/primitive/db/base.py +5 -0
- primitive-0.2.12/src/primitive/db/models.py +88 -0
- primitive-0.2.12/src/primitive/db/sqlite.py +34 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/actions.py +0 -1
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/actions.py +0 -1
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/actions.py +11 -10
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/commands.py +1 -68
- primitive-0.2.12/src/primitive/hardware/ui.py +67 -0
- primitive-0.2.12/src/primitive/monitor/actions.py +199 -0
- primitive-0.2.12/src/primitive/monitor/commands.py +13 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/actions.py +0 -2
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/auth.py +0 -2
- primitive-0.2.12/src/primitive/utils/daemons.py +54 -0
- primitive-0.2.12/uv.lock +1423 -0
- primitive-0.2.10/src/primitive/agent/actions.py +0 -168
- primitive-0.2.10/src/primitive/daemons/actions.py +0 -75
- primitive-0.2.10/src/primitive/daemons/commands.py +0 -65
- primitive-0.2.10/src/primitive/daemons/launch_agents.py +0 -154
- primitive-0.2.10/src/primitive/daemons/launch_service.py +0 -179
- primitive-0.2.10/uv.lock +0 -1201
- {primitive-0.2.10 → primitive-0.2.12}/.git-hooks/pre-commit +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/.gitattributes +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/.github/workflows/lint.yml +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/.github/workflows/publish.yml +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/.gitignore +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/LICENSE.txt +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/Makefile +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/README.md +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/linux setup.md +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/agent/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/auth/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/daemons/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/exec/interactive.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/files/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/git/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/relay.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/sdk.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/graphql/utility_fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/android.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/hardware/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/jobs/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/organizations/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/projects/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/provisioning/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/commands.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/fragments.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/mutations.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/reservations/graphql/queries.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/__init__.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/actions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/cache.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/chunk_size.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/config.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/exceptions.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/memory_size.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/printer.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/shell.py +0 -0
- {primitive-0.2.10 → primitive-0.2.12}/src/primitive/utils/text.py +0 -0
- {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.
|
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" }]
|
@@ -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...")
|
@@ -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
|
-
|
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
|
-
|
253
|
-
|
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
|
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
|
-
|
268
|
-
|
269
|
-
|
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
|
54
|
-
if "is empty" in str(
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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)
|